mirror of
https://github.com/R0m1k3/Presencia.git
synced 2026-10-15 03:09:43 +02:00
Vérification de bout en bout via la vraie config nginx (21 checks Playwright, tous passants) : parcours admin complet, saisie cadre, double validation, verrouillages, exports, erreurs de connexion. Bugs corrigés découverts par ce contrôle : - La création d'utilisateur via l'interface était cassée depuis le début : l'id "user-role" existait en double (badge de l'en-tête + select de la modale), getElementById renvoyait le span du header et le rôle n'était jamais envoyé (400 systématique). Le badge devient user-role-badge. - Les toasts de notification interceptaient les clics sur les boutons situés dessous pendant 4 s (pointer-events: none ajouté). Améliorations UI : - « Absent » renommé « Non-présent » partout (légende, popover, exports PDF et Excel — code NP au lieu de A). - Récapitulatif mensuel : tuiles de compteurs par statut au-dessus du calendrier, mises à jour à chaque saisie. - Bouton « Remplir les jours ouvrés en Présent » : renseigne en un clic toutes les demi-journées vides du lundi au vendredi (nouvel endpoint PUT /api/attendance/bulk, transactionnel, mêmes contrôles de verrouillage), sans toucher aux saisies existantes. - Jour actuel mis en évidence dans le calendrier. - Favicon.
95 lines
2.9 KiB
JavaScript
95 lines
2.9 KiB
JavaScript
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} · Non-présent: ${counts.absent} · Congé: ${counts.conge} · RTT: ${counts.rtt}`,
|
|
startX,
|
|
y
|
|
);
|
|
});
|
|
|
|
doc.end();
|
|
return doc;
|
|
}
|
|
|
|
module.exports = { buildCompanyPdf };
|