mirror of
https://github.com/R0m1k3/Presencia.git
synced 2026-10-22 22:58:03 +02:00
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 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.
This commit is contained in:
7 files changed
+146
-11
No files matched your search
@@ -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)) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
);
|
||||
|
||||
@@ -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',
|
||||
};
|
||||
|
||||
Reference in new issue
Block a user