Implémenter le design Presencia (projet Claude Design)

Reprend la maquette Presencia.dc.html sur l'application réelle : thème
clair chaud à accent vert forêt, navigation latérale groupée par rôle et
tableau de bord, en conservant l'intégralité du fonctionnel existant.

Frontend
- Écran de connexion en deux panneaux (argumentaire + formulaire).
- Tableau de bord par rôle : compteurs du mois et liste « à faire
  maintenant » (sociétés prêtes à valider, cadres en retard, saisie à
  compléter), plus l'état du mois par société côté admin.
- Planning : saisie rapide au pinceau, trois affichages (grille, compact,
  liste), sélecteur avec raccourcis clavier (1–4, ⌫, ↵ pour appliquer à
  la journée), barre d'état de verrouillage, « Tout effacer ».
- Nouvelle vue Historique pour les cadres (totaux par mois et statut).
- Validation & export : barre de progression et compteurs de
  demi-journées par cadre.
- Mobile : liste verticale, navigation en barre basse, cibles 44 px.

Backend
- GET /api/validations/overview — état de toutes les sociétés d'un mois.
- GET /api/attendance/history — totaux par mois et par statut.
- DELETE /api/attendance/month — effacement d'un mois (mêmes verrous).
- Compteur de demi-journées saisies par cadre dans /validations/company.

Polices et icônes servies localement (Inter, sous-ensemble de 18 icônes
Phosphor) au lieu de Google Fonts et unpkg : l'application ne fait plus
aucune requête externe au chargement.

Vérifié de bout en bout via la vraie configuration nginx : 62 contrôles
Playwright (desktop et mobile), tous passants, sur base réinitialisée et
rejoués deux fois pour garantir le déterminisme.
This commit is contained in:
Claude committed 2026-08-03 18:00:34 +00:00
1 parent 7ea223a442
commit 626b46d9d6
14 files changed
+2033 -1234

No files matched your search

+44 -2
View File
@@ -65,14 +65,19 @@ router.get('/company/:companyId', requireAdmin, async (req, res) => {
const companyId = req.params.companyId;
if (!year || !month) return res.status(400).json({ error: 'year et month requis' });
const start = `${year}-${String(month).padStart(2, '0')}-01`;
const { rows: cadres } = await db.query(
`SELECT u.id, u.full_name, u.email,
ml.cadre_validated, ml.cadre_validated_at
ml.cadre_validated, ml.cadre_validated_at,
COUNT(ae.id) AS filled
FROM users u
LEFT JOIN month_locks ml ON ml.user_id = u.id AND ml.year = $2 AND ml.month = $3
LEFT JOIN attendance_entries ae ON ae.user_id = u.id
AND ae.entry_date >= $4::date AND ae.entry_date < ($4::date + INTERVAL '1 month')
WHERE u.company_id = $1 AND u.role = 'cadre' AND u.active = true
GROUP BY u.id, ml.cadre_validated, ml.cadre_validated_at
ORDER BY u.full_name`,
[companyId, year, month]
[companyId, year, month, start]
);
const { rows: cv } = await db.query(
@@ -86,6 +91,7 @@ router.get('/company/:companyId', requireAdmin, async (req, res) => {
id: c.id,
fullName: c.full_name,
email: c.email,
filled: parseInt(c.filled, 10),
cadreValidated: !!c.cadre_validated,
cadreValidatedAt: c.cadre_validated_at,
})),
@@ -95,6 +101,42 @@ router.get('/company/:companyId', requireAdmin, async (req, res) => {
});
});
// Admin: one-shot overview of every company for a given month (dashboard).
router.get('/overview', requireAdmin, async (req, res) => {
const year = parseInt(req.query.year, 10);
const month = parseInt(req.query.month, 10);
if (!year || !month) return res.status(400).json({ error: 'year et month requis' });
const { rows } = await db.query(
`SELECT c.id, c.name,
COUNT(u.id) FILTER (WHERE u.id IS NOT NULL) AS active_cadres,
COUNT(u.id) FILTER (WHERE ml.cadre_validated) AS validated_cadres,
BOOL_OR(cmv.admin_validated) AS company_validated
FROM companies c
LEFT JOIN users u ON u.company_id = c.id AND u.role = 'cadre' AND u.active = true
LEFT JOIN month_locks ml ON ml.user_id = u.id AND ml.year = $1 AND ml.month = $2
LEFT JOIN company_month_validations cmv ON cmv.company_id = c.id AND cmv.year = $1 AND cmv.month = $2
GROUP BY c.id
ORDER BY c.name`,
[year, month]
);
const companies = rows.map((r) => ({
id: r.id,
name: r.name,
activeCadres: parseInt(r.active_cadres, 10),
validatedCadres: parseInt(r.validated_cadres, 10),
companyValidated: !!r.company_validated,
}));
res.json({
companies,
totalActiveCadres: companies.reduce((n, c) => n + c.activeCadres, 0),
lateCadres: companies.reduce((n, c) => n + (c.companyValidated ? 0 : c.activeCadres - c.validatedCadres), 0),
pendingCompanies: companies.filter((c) => !c.companyValidated).length,
});
});
// Admin validates the whole company for the month (requires every cadre validated).
router.post('/company/:companyId', requireAdmin, async (req, res) => {
const ym = parseYearMonth(req);