Contrôle complet + corrections de bugs + améliorations UI

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 intercep­taient 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.
This commit is contained in:
Claude committed 2026-07-06 15:04:03 +00:00
1 parent 441a1758a5
commit 7ea223a442
7 files changed
+146 -11

No files matched your search

+55
View File
@@ -117,6 +117,61 @@ router.put('/', async (req, res) => {
res.json({ ok: true });
});
// Bulk upsert (e.g. « fill all empty weekdays with présent »). All entries
// must pass the same lock checks as single writes; months are checked once
// per distinct month present in the payload.
router.put('/bulk', async (req, res) => {
const { entries, user_id: requestedUserId } = req.body || {};
if (!Array.isArray(entries) || entries.length === 0 || entries.length > 200) {
return res.status(400).json({ error: 'Paramètres invalides' });
}
for (const e of entries) {
if (!e || !e.date || !PERIODS.includes(e.period) || !STATUSES.includes(e.status)) {
return res.status(400).json({ error: 'Paramètres invalides' });
}
}
if (requestedUserId && req.user.role !== 'admin') {
return res.status(403).json({ error: 'Accès refusé' });
}
const target = await getTargetUser(req, requestedUserId);
if (!target) return res.status(404).json({ error: 'Utilisateur introuvable' });
const months = new Set(entries.map((e) => e.date.slice(0, 7)));
for (const ym of months) {
const [year, month] = ym.split('-').map(Number);
const lock = await getMonthLock(target.id, year, month);
const companyValidation = await getCompanyValidation(target.company_id, year, month);
if (companyValidation.admin_validated) {
return res.status(423).json({ error: 'Ce mois a été validé par l’administrateur et est verrouillé' });
}
if (req.user.role !== 'admin' && lock.cadre_validated) {
return res.status(423).json({ error: 'Vous avez déjà validé ce mois. Contactez un administrateur pour le modifier.' });
}
}
const client = await db.pool.connect();
try {
await client.query('BEGIN');
for (const e of entries) {
await client.query(
`INSERT INTO attendance_entries (user_id, entry_date, period, status, updated_at)
VALUES ($1, $2, $3, $4, now())
ON CONFLICT (user_id, entry_date, period)
DO UPDATE SET status = EXCLUDED.status, updated_at = now()`,
[target.id, e.date, e.period, e.status]
);
}
await client.query('COMMIT');
} catch (err) {
await client.query('ROLLBACK');
throw err;
} finally {
client.release();
}
res.json({ ok: true, count: entries.length });
});
router.delete('/', async (req, res) => {
const { date, period, user_id: requestedUserId } = req.body || {};
if (!date || !PERIODS.includes(period)) {
+1 -1
View File
@@ -75,7 +75,7 @@ async function buildCompanyWorkbook({ companyName, year, month, cadres }) {
}
const legendRowIdx = sheet.rowCount + 2;
sheet.getCell(legendRowIdx, 1).value = 'Légende : P = Présent · A = Absent · CP = Congé · RTT = RTT';
sheet.getCell(legendRowIdx, 1).value = 'Légende : P = Présent · NP = Non-présent · CP = Congé · RTT = RTT';
sheet.getCell(legendRowIdx, 1).font = { italic: true, size: 9 };
return workbook;
+1 -1
View File
@@ -81,7 +81,7 @@ function buildCompanyPdf({ companyName, year, month, cadres }) {
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}`,
`Total demi-journées — Présent: ${counts.present} · Non-présent: ${counts.absent} · Congé: ${counts.conge} · RTT: ${counts.rtt}`,
startX,
y
);
+2 -2
View File
@@ -1,13 +1,13 @@
const STATUS_LABELS = {
present: 'Présent',
absent: 'Absent',
absent: 'Non-présent',
conge: 'Congé',
rtt: 'RTT',
};
const STATUS_CODES = {
present: 'P',
absent: 'A',
absent: 'NP',
conge: 'CP',
rtt: 'RTT',
};