Ajout de l'application Presencia (gestion des présences multi-sociétés)

Application complète : API Node/Express + PostgreSQL, frontend HTML/JS
mono-page, et déploiement Docker sur ports non standards (8781/4790/6543).

- Cadres : calendrier mensuel, saisie de présence par demi-journée
  (présent/absent/congé/RTT), validation de fin de mois qui verrouille
  leurs propres saisies.
- Admin : gestion des sociétés et utilisateurs (rattachement société <->
  cadre), consultation de tous les plannings, validation d'une société
  entière une fois tous ses cadres validés, réouverture en cas de
  correction, export PDF et Excel par société/mois.
This commit is contained in:
Claude committed 2026-07-04 20:41:27 +00:00
commit a73ab28fae
27 files changed
+5492

No files matched your search

+94
View File
@@ -0,0 +1,94 @@
const PDFDocument = require('pdfkit');
const { STATUS_LABELS, daysInMonth } = require('./status');
const { MONTH_NAMES } = require('./excel');
const STATUS_HEX = {
present: '#2E7D32',
absent: '#C62828',
conge: '#EF6C00',
rtt: '#1565C0',
};
/**
* cadres: [{ id, fullName, entries: Map('YYYY-MM-DD:AM'|'PM' -> status) }]
* Returns a PDFDocument stream (caller pipes it to res).
*/
function buildCompanyPdf({ companyName, year, month, cadres }) {
const doc = new PDFDocument({ margin: 40, size: 'A4' });
const nbDays = daysInMonth(year, month);
doc.fontSize(18).fillColor('#111').text('Presencia — Récapitulatif de présences', { align: 'left' });
doc.moveDown(0.2);
doc.fontSize(12).fillColor('#444').text(`${companyName} — ${MONTH_NAMES[month - 1]} ${year}`);
doc.moveDown(1);
cadres.forEach((cadre, idx) => {
if (idx > 0) doc.addPage();
else doc.moveDown(0.5);
doc.fontSize(14).fillColor('#111').text(cadre.fullName, { underline: true });
doc.moveDown(0.5);
const counts = { present: 0, absent: 0, conge: 0, rtt: 0 };
const startX = doc.x;
let y = doc.y;
const rowHeight = 16;
const colDay = 30;
const colAM = 90;
const colPM = 90;
doc.fontSize(9).fillColor('#fff');
doc.rect(startX, y, colDay + colAM + colPM, rowHeight).fill('#333');
doc.fillColor('#fff').text('Jour', startX + 4, y + 4, { width: colDay - 8 });
doc.text('Matin (AM)', startX + colDay + 4, y + 4, { width: colAM - 8 });
doc.text('Après-midi (PM)', startX + colDay + colAM + 4, y + 4, { width: colPM - 8 });
y += rowHeight;
for (let d = 1; d <= nbDays; d++) {
const dateStr = `${year}-${String(month).padStart(2, '0')}-${String(d).padStart(2, '0')}`;
const am = cadre.entries.get(`${dateStr}:AM`);
const pm = cadre.entries.get(`${dateStr}:PM`);
if (am) counts[am]++;
if (pm) counts[pm]++;
if (y > 760) {
doc.addPage();
y = 40;
}
doc.rect(startX, y, colDay + colAM + colPM, rowHeight).strokeColor('#ddd').stroke();
doc.fillColor('#111').fontSize(9).text(String(d), startX + 4, y + 4, { width: colDay - 8 });
doc.fillColor(am ? STATUS_HEX[am] : '#999').text(
am ? STATUS_LABELS[am] : '—',
startX + colDay + 4,
y + 4,
{ width: colAM - 8 }
);
doc.fillColor(pm ? STATUS_HEX[pm] : '#999').text(
pm ? STATUS_LABELS[pm] : '—',
startX + colDay + colAM + 4,
y + 4,
{ width: colPM - 8 }
);
y += rowHeight;
}
y += 10;
if (y > 740) {
doc.addPage();
y = 40;
}
doc.fontSize(10).fillColor('#111').text(
`Total demi-journées — Présent: ${counts.present} · Absent: ${counts.absent} · Congé: ${counts.conge} · RTT: ${counts.rtt}`,
startX,
y
);
});
doc.end();
return doc;
}
module.exports = { buildCompanyPdf };