diff --git a/backend/src/routes/attendance.js b/backend/src/routes/attendance.js index 97b5ecd..e80c209 100644 --- a/backend/src/routes/attendance.js +++ b/backend/src/routes/attendance.js @@ -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)) { diff --git a/backend/src/utils/excel.js b/backend/src/utils/excel.js index 70b4634..68e123b 100644 --- a/backend/src/utils/excel.js +++ b/backend/src/utils/excel.js @@ -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; diff --git a/backend/src/utils/pdf.js b/backend/src/utils/pdf.js index 194e9bb..93e9502 100644 --- a/backend/src/utils/pdf.js +++ b/backend/src/utils/pdf.js @@ -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 ); diff --git a/backend/src/utils/status.js b/backend/src/utils/status.js index a4cb011..7d23c5b 100644 --- a/backend/src/utils/status.js +++ b/backend/src/utils/status.js @@ -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', }; diff --git a/frontend/public/css/style.css b/frontend/public/css/style.css index 281c6ce..5f814a3 100644 --- a/frontend/public/css/style.css +++ b/frontend/public/css/style.css @@ -250,7 +250,35 @@ h2 { font-size: 17px; margin: 0 0 4px; } .half-day.st-conge { background: var(--color-conge-bg); color: var(--color-conge); border-style: solid; border-color: transparent; } .half-day.st-rtt { background: var(--color-rtt-bg); color: var(--color-rtt); border-style: solid; border-color: transparent; } -.calendar-actions { margin-top: 22px; display: flex; justify-content: flex-end; } +.calendar-actions { margin-top: 22px; display: flex; justify-content: flex-end; gap: 10px; flex-wrap: wrap; } + +/* Highlight of the current day */ +.day-cell.today { border-color: var(--color-primary); box-shadow: inset 0 0 0 1px var(--color-primary); } +.day-cell.today .day-number { color: var(--color-primary); } + +/* ---------- Monthly summary tiles ---------- */ +.month-summary { + display: flex; + gap: 10px; + flex-wrap: wrap; + margin-bottom: 16px; +} +.month-summary:empty { display: none; } +.summary-tile { + display: flex; + align-items: baseline; + gap: 7px; + background: var(--color-surface); + border: 1px solid var(--color-border); + border-radius: 8px; + padding: 8px 14px; +} +.summary-count { font-size: 18px; font-weight: 700; } +.summary-label { font-size: 12px; color: var(--color-muted); font-weight: 600; } +.summary-present .summary-count { color: var(--color-present); } +.summary-absent .summary-count { color: var(--color-absent); } +.summary-conge .summary-count { color: var(--color-conge); } +.summary-rtt .summary-count { color: var(--color-rtt); } /* ---------- Tables ---------- */ .data-table { width: 100%; border-collapse: collapse; font-size: 14px; } @@ -318,6 +346,8 @@ h2 { font-size: 17px; margin: 0 0 4px; } .toast-container { position: fixed; top: 18px; right: 18px; z-index: 200; display: flex; flex-direction: column; gap: 10px; + /* Notifications must never block clicks on the UI underneath them. */ + pointer-events: none; } .toast { background: var(--color-text); color: #fff; padding: 12px 18px; border-radius: 8px; diff --git a/frontend/public/index.html b/frontend/public/index.html index 7ed86af..d4581a5 100644 --- a/frontend/public/index.html +++ b/frontend/public/index.html @@ -4,6 +4,7 @@ Presencia — Gestion des présences + @@ -51,7 +52,7 @@
- +
@@ -75,18 +76,21 @@
Présent - Absent + Non-présent Congé RTT Non renseigné
+
+
LunMarMerJeuVenSamDim
+
@@ -190,7 +194,7 @@