mirror of
https://github.com/R0m1k3/Presencia.git
synced 2026-10-23 07:08: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
@@ -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;
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Presencia — Gestion des présences</title>
|
||||
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Crect width='100' height='100' rx='20' fill='%233949ab'/%3E%3Ctext x='50' y='72' font-size='64' text-anchor='middle' fill='white' font-family='Arial' font-weight='bold'%3EP%3C/text%3E%3C/svg%3E" />
|
||||
<link rel="stylesheet" href="css/style.css" />
|
||||
</head>
|
||||
<body>
|
||||
@@ -51,7 +52,7 @@
|
||||
<div class="topbar-user">
|
||||
<div class="user-chip">
|
||||
<span id="user-name" class="user-name"></span>
|
||||
<span id="user-role" class="user-role-badge"></span>
|
||||
<span id="user-role-badge" class="user-role-badge"></span>
|
||||
</div>
|
||||
<button id="logout-btn" class="btn btn-ghost btn-sm">Déconnexion</button>
|
||||
</div>
|
||||
@@ -75,18 +76,21 @@
|
||||
|
||||
<div class="legend">
|
||||
<span class="legend-item"><i class="dot dot-present"></i>Présent</span>
|
||||
<span class="legend-item"><i class="dot dot-absent"></i>Absent</span>
|
||||
<span class="legend-item"><i class="dot dot-absent"></i>Non-présent</span>
|
||||
<span class="legend-item"><i class="dot dot-conge"></i>Congé</span>
|
||||
<span class="legend-item"><i class="dot dot-rtt"></i>RTT</span>
|
||||
<span class="legend-item"><i class="dot dot-empty"></i>Non renseigné</span>
|
||||
</div>
|
||||
|
||||
<div id="month-summary" class="month-summary"></div>
|
||||
|
||||
<div class="calendar-weekdays">
|
||||
<span>Lun</span><span>Mar</span><span>Mer</span><span>Jeu</span><span>Ven</span><span>Sam</span><span>Dim</span>
|
||||
</div>
|
||||
<div id="calendar-grid" class="calendar-grid"></div>
|
||||
|
||||
<div id="calendar-actions" class="calendar-actions">
|
||||
<button id="fill-workdays-btn" class="btn btn-outline">Remplir les jours ouvrés en « Présent »</button>
|
||||
<button id="validate-month-btn" class="btn btn-primary">Valider mon mois</button>
|
||||
</div>
|
||||
</section>
|
||||
@@ -190,7 +194,7 @@
|
||||
<div id="status-popover" class="popover" hidden>
|
||||
<div class="popover-title" id="popover-title"></div>
|
||||
<button class="popover-option" data-status="present"><i class="dot dot-present"></i>Présent</button>
|
||||
<button class="popover-option" data-status="absent"><i class="dot dot-absent"></i>Absent</button>
|
||||
<button class="popover-option" data-status="absent"><i class="dot dot-absent"></i>Non-présent</button>
|
||||
<button class="popover-option" data-status="conge"><i class="dot dot-conge"></i>Congé</button>
|
||||
<button class="popover-option" data-status="rtt"><i class="dot dot-rtt"></i>RTT</button>
|
||||
<button class="popover-option popover-clear" data-status="">Effacer</button>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
'Janvier', 'Février', 'Mars', 'Avril', 'Mai', 'Juin',
|
||||
'Juillet', 'Août', 'Septembre', 'Octobre', 'Novembre', 'Décembre',
|
||||
];
|
||||
const STATUS_LABELS = { present: 'Présent', absent: 'Absent', conge: 'Congé', rtt: 'RTT' };
|
||||
const STATUS_LABELS = { present: 'Présent', absent: 'Non-présent', conge: 'Congé', rtt: 'RTT' };
|
||||
|
||||
const state = {
|
||||
user: null,
|
||||
@@ -69,7 +69,7 @@
|
||||
document.getElementById('view-login').hidden = true;
|
||||
document.getElementById('app').hidden = false;
|
||||
document.getElementById('user-name').textContent = user.fullName;
|
||||
document.getElementById('user-role').textContent = user.role === 'admin' ? 'Administrateur' : (user.companyName || 'Cadre');
|
||||
document.getElementById('user-role-badge').textContent = user.role === 'admin' ? 'Administrateur' : (user.companyName || 'Cadre');
|
||||
|
||||
const now = new Date();
|
||||
state.cal.year = now.getFullYear();
|
||||
@@ -197,10 +197,26 @@
|
||||
actionsBox.hidden = false;
|
||||
validateBtn.disabled = data.cadreValidated || data.companyValidated;
|
||||
validateBtn.textContent = data.cadreValidated ? 'Mois déjà validé' : 'Valider mon mois';
|
||||
document.getElementById('fill-workdays-btn').hidden = !data.editable;
|
||||
} else {
|
||||
actionsBox.hidden = true;
|
||||
}
|
||||
|
||||
// monthly totals (half-days per status)
|
||||
const counts = { present: 0, absent: 0, conge: 0, rtt: 0 };
|
||||
data.entries.forEach((e) => { if (counts[e.status] !== undefined) counts[e.status]++; });
|
||||
document.getElementById('month-summary').innerHTML = ['present', 'absent', 'conge', 'rtt']
|
||||
.map((s) => `
|
||||
<div class="summary-tile summary-${s}">
|
||||
<span class="summary-count">${counts[s]}</span>
|
||||
<span class="summary-label">${STATUS_LABELS[s]}</span>
|
||||
</div>`)
|
||||
.join('') + `
|
||||
<div class="summary-tile summary-note">
|
||||
<span class="summary-count">${data.entries.length}</span>
|
||||
<span class="summary-label">demi-journées saisies</span>
|
||||
</div>`;
|
||||
|
||||
const firstOfMonth = new Date(year, month - 1, 1);
|
||||
const daysInMonth = new Date(year, month, 0).getDate();
|
||||
let firstWeekday = firstOfMonth.getDay(); // 0=Sun
|
||||
@@ -215,13 +231,15 @@
|
||||
}
|
||||
|
||||
const editable = data.editable;
|
||||
const now = new Date();
|
||||
const todayDay = now.getFullYear() === year && now.getMonth() + 1 === month ? now.getDate() : null;
|
||||
for (let d = 1; d <= daysInMonth; d++) {
|
||||
const dateObj = new Date(year, month - 1, d);
|
||||
const iso = `${year}-${String(month).padStart(2, '0')}-${String(d).padStart(2, '0')}`;
|
||||
const isWeekend = dateObj.getDay() === 0 || dateObj.getDay() === 6;
|
||||
|
||||
const cell = document.createElement('div');
|
||||
cell.className = 'day-cell' + (isWeekend ? ' weekend' : '');
|
||||
cell.className = 'day-cell' + (isWeekend ? ' weekend' : '') + (d === todayDay ? ' today' : '');
|
||||
|
||||
const num = document.createElement('div');
|
||||
num.className = 'day-number';
|
||||
@@ -318,6 +336,34 @@
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('fill-workdays-btn').addEventListener('click', async () => {
|
||||
const { year, month, data } = state.cal;
|
||||
if (!data) return;
|
||||
const filled = new Set(data.entries.map((e) => `${e.date}:${e.period}`));
|
||||
const entries = [];
|
||||
const daysInMonth = new Date(year, month, 0).getDate();
|
||||
for (let d = 1; d <= daysInMonth; d++) {
|
||||
const dow = new Date(year, month - 1, d).getDay();
|
||||
if (dow === 0 || dow === 6) continue; // week-ends exclus
|
||||
const iso = `${year}-${String(month).padStart(2, '0')}-${String(d).padStart(2, '0')}`;
|
||||
for (const period of ['AM', 'PM']) {
|
||||
if (!filled.has(`${iso}:${period}`)) entries.push({ date: iso, period, status: 'present' });
|
||||
}
|
||||
}
|
||||
if (entries.length === 0) {
|
||||
toast('Tous les jours ouvrés sont déjà renseignés', 'info');
|
||||
return;
|
||||
}
|
||||
if (!confirm(`Renseigner ${entries.length} demi-journées vides en « Présent » ? Les saisies existantes ne seront pas modifiées.`)) return;
|
||||
try {
|
||||
await api('/attendance/bulk', { method: 'PUT', body: { entries } });
|
||||
toast(`${entries.length} demi-journées renseignées`, 'success');
|
||||
await loadCalendar();
|
||||
} catch (err) {
|
||||
toast(err.message, 'error');
|
||||
}
|
||||
});
|
||||
|
||||
// ================= COMPANIES (admin) =================
|
||||
async function loadCompaniesForSelects() {
|
||||
try {
|
||||
|
||||
Reference in new issue
Block a user