feat(heures) : calendrier des jours validés (vue Jour) + harmonisation Malio UI

- Calendrier MalioDate en vue Jour (Heures + Heures Conducteurs) : jours
  entièrement validés (admin) peints en vert. Endpoint GET
  /work-hours/validation-status?from=&to=[&driver=1] (scope conducteur inversé),
  chargement à la volée par mois, refresh après validation/saisie/absence.
- Suite à @malio/layer-ui 1.7.11 : reserveMessageSpace=false sur les champs ;
  tous les drawers migrés sur MalioDrawer (titre via slot #header, AppDrawer
  custom supprimé) ; boutons d'action en MalioButton (deux boutons partagent
  l'espace) ; inputs date en MalioDate ; MalioDateWeek en vue Semaine.
- Boutons d'ajout uniformisés sur « Ajouter » + icône.
- .env : EXCLUDED_PUBLIC_HOLIDAYS="null".
- Doc : doc/hours-validated-days.md, documentation-content.ts, CLAUDE.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-16 15:47:23 +02:00
parent 5d2b5d1c54
commit 34dc52d92b
37 changed files with 1881 additions and 495 deletions
+75
View File
@@ -0,0 +1,75 @@
# Calendrier des jours validés (écran Heures — vue Jour)
## Objectif
Sur l'écran **Heures**, en **vue Jour**, le sélecteur de date est un calendrier
(composant `MalioDate` du layer `@malio/layer-ui`) qui **peint en vert** les jours
entièrement validés par un admin. La RH repère ainsi d'un coup d'œil les jours où
il reste de la validation à faire.
## Définition « jour validé » (vert)
Un jour est vert ssi, dans le **périmètre complet** de l'utilisateur :
- il porte **au moins une ligne `WorkHour`** dans le scope ciblé ce jour-là, **et**
- **aucune** de ces lignes n'est en attente de validation (`isValid = false`).
La même mécanique sert les **deux écrans**, avec un scope opposé : écran **Heures**
non-conducteurs (défaut) ; écran **Heures Conducteurs** → conducteurs (`?driver=1`).
Conséquences :
- Un jour **sans aucune ligne** (rien saisi, ex. week-end, jour futur) reste **neutre**
(jamais vert) — « rien fait » n'est pas « tout validé ».
- On se base sur la **seule** colonne `work_hours.is_valid` (validation admin/RH).
`isSiteValid` (chef de site) n'entre pas en compte → modifier une validation site
ne change pas la couleur.
- **Scope conducteur** : écran Heures → conducteurs exclus ; écran Heures Conducteurs →
seuls les conducteurs (le filtre est inversé via `?driver=1`).
## Périmètre
- `ROLE_ADMIN` → tous les employés / tous les sites.
- Chef de site → ses sites uniquement.
- Le **filtre sites de l'écran est volontairement ignoré** : le vert reflète tout le
périmètre (objectif : repérer le moindre jour incomplet, où qu'il soit). Changer le
filtre sites de la vue Jour ne recalcule pas le calendrier.
## Chargement des données
- Endpoint : `GET /work-hours/validation-status?from=YYYY-MM-DD&to=YYYY-MM-DD[&driver=1]`
(`ROLE_USER`). Réponse : `{ from, to, validatedDays: string[] }` (dates `Y-m-d`).
- Provider : `App\State\WorkHourValidationStatusProvider`
(ressource `App\ApiResource\WorkHourValidationStatus`).
- `EmployeeRepository::findScoped($user)` pour le périmètre (ignore tout `siteIds`).
- Une requête `WorkHourReadRepositoryInterface::findByDateRangeAndEmployees`.
- Filtrage conducteur **par date** via `EmployeeContractResolver::resolveIsDriverForEmployeeAndDate`
(mémoïsé par couple employé/jour) : `if ($isDriver !== $driverOnly) continue;`
(`driverOnly` = `?driver=1`).
- Agrégation par jour : vert ⇔ `total > 0` (lignes du scope) et `pending = 0`
(aucune `isValid=false`).
- Garde-fou : plage bornée à 366 jours.
- Le chargement est **à la volée par mois affiché** (jamais préchargé sur plusieurs
années) : `MalioDate` émet `@month-change { month, year }` à l'ouverture du popover et
à chaque navigation ; le front fetch la **grille visible** (lundi avant le 1er →
dimanche après le dernier jour, pour colorer aussi les jours débordants) et met le
résultat en cache par mois (`useHoursPage` / `useDriverHoursPage``validatedDaysByMonth` ;
ce dernier appelle le service avec `{ driver: true }`). La prop réactive `markedDates`
(ISO → `'success'`) recolore la grille.
## Rafraîchissement
Toute action qui touche la validation d'un jour recharge le mois concerné s'il est en
cache (`reloadValidationMonth`), donc le calendrier se recolore aussitôt :
- validation admin d'une ligne (`toggleValidation`) ou en masse (`toggleValidationBulk`) ;
- sauvegarde d'heures (`handleSave`) — toute modification réelle remet `isValid=false` ;
- création / suppression d'absence (`refreshAfterAbsenceChange`).
La validation **site** ne déclenche pas de rechargement (sans effet sur le vert).
## Périmètre d'affichage
- **Vue Jour uniquement** : le vert (calendrier `MalioDate` + `markedDates`) est à la maille
jour, sur les **deux écrans** (Heures et Heures Conducteurs, via `showValidationCalendar`).
La **vue Semaine** utilise un `MalioDateWeek` (sélecteur de semaine, sans coloration).
Le `PeriodStepperPicker` ne subsiste que comme fallback de la vue Jour quand
`showValidationCalendar` est absent (aucun appelant actuel, conservé par flexibilité).
- Précédence d'affichage dans la grille (côté layer) : sélection (fond plein primary) >
variante marquée ; le **jour courant** (`today`) garde sa bordure **et** reçoit le fond
vert s'il est validé.
## Dépendance layer
Nécessite `@malio/layer-ui >= 1.7.x` : prop `markedDates`
(`Record<"YYYY-MM-DD", 'success' | 'danger'>`) + event `month-change` sur `MalioDate`
(ticket Malio UI MUI-45).