Files
SIRH/docs/superpowers/specs/2026-06-11-night-hours-contingent-export-design.md
T
2026-06-11 11:24:54 +02:00

133 lines
6.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Export « Contingent H.nuit »
Date : 2026-06-11
Statut : validé (design)
## Objectif
Ajouter un nouvel export PDF sur la **liste des employés** : un tableau du
contingent d'heures de nuit, employés en lignes, mois en colonnes. Chaque mois
porte deux sous-colonnes : **Total H.nuit** (heures travaillées de nuit) et
**Total N.jours** (nombre de nuits où ≥ 4h ont été travaillées de nuit).
## Décisions cadrées
- **Période** : année civile Janvier → Décembre, choisie via un sélecteur d'année
dans le drawer (réutilise `exportYearOptions`).
- **Fenêtre de nuit** : 21h → 6h — on **réutilise le calcul existant** de l'app
(constante `[0,360]` + `[1260,1440]` minutes, projection J+1 pour les shifts
traversant minuit). NB : la demande initiale mentionnait 21h-5h ; arbitré sur
21h→6h pour rester cohérent avec le reste de l'application.
- **Règle « 1 jour »** : un jour compte 1 dans « N.jours » dès que les minutes de
nuit du jour ≥ 240 (4h).
- **Conducteurs** : inclus. Leurs minutes de nuit = champ manuel
`WorkHour.nightHoursMinutes` (pas de fenêtre horaire — total saisi). La règle
≥ 240 min = 1 jour s'applique aussi.
- **Non-conducteurs** : minutes de nuit calculées depuis les plages
matin/après-midi/soir via la fenêtre 21h→6h.
- **Pas de crédit** absence/férié : on ne compte que les heures de nuit
réellement travaillées (pas de crédit virtuel férié ni crédit absence).
- **Pas de colonne « Total annuel »** (hors périmètre pour l'instant).
- **Mise en page** : A4 **paysage**.
- **Groupement / tri** : par site, identique au day-export et au calendrier —
sites triés par `displayOrder` puis nom ; employés triés par `displayOrder`,
puis nom, puis prénom.
## Architecture
### Frontend
`frontend/pages/employees/index.vue` (drawer Export existant) :
- Ajouter une option `{ label: 'Contingent H.nuit', value: 'night-contingent' }`
dans `exportTypeOptions`.
- Quand `exportChoice === 'night-contingent'` : afficher le sélecteur d'année
(réutiliser le `MalioSelect` année déjà utilisé par `yearly-hours`).
- `isExportValid` : valide si une année est choisie.
- `handleExportValidate` : `await printPdf('/night-hours-contingent/print?year=' + exportYear)`.
Aucun sélecteur de site (cohérent avec les autres exports de ce drawer — le
périmètre vient du back via `findScoped`).
### Backend
**Endpoint** : `GET /night-hours-contingent/print?year=YYYY`
- Operation API Platform custom (Provider, output PDF `Response`), `ROLE_USER`.
- `year` : entier validé 2000-2100, défaut = année courante.
**Provider** `App\State\NightHoursContingentPrintProvider`
1. Auth + parse `year`.
2. `employees = employeeRepository->findScoped($user)` (périmètre admin / chef de site).
3. Garder les employés ayant ≥ 1 période de contrat intersectant
`[YYYY-01-01 ; YYYY-12-31]` (helper `hasContractInRange`, même esprit que
`AbsencePrintProvider`/`SalaryRecapPrintProvider`).
4. Grouper par site ; trier sites par `displayOrder` puis nom ; trier employés
intra-site par `displayOrder`, nom, prénom.
5. Construire les lignes via `NightContingentExportBuilder`.
6. Rendre `templates/night-hours-contingent/print.html.twig` → Dompdf (paysage).
**Builder** `App\Service\WorkHours\NightContingentExportBuilder`
- `buildRows(list<Employee> $employees, int $year): list<NightContingentRow>`
- Pour chaque employé : charge ses `WorkHour` de l'année (1 requête par lot ou
par employé selon le repo existant), répartit par mois (1..12).
- Par jour : `nightMinutes = NightHoursCalculator::nightMinutesForWorkHour($wh, $isDriverThatDay)`.
- Driver ce jour-là → `nightHoursMinutes ?? 0`.
- Non-driver → somme des `nightIntervalMinutes` sur les 3 plages.
- Agrégats par mois : `nightMinutesTotal += nightMinutes` ;
`if (nightMinutes >= 240) nightDays += 1`.
- Retour DTO : `{ employeeId, employeeName, isDriver, months: [{nightMinutes, nightDays} × 12] }`.
> Le statut driver est résolu **par date** (un employé peut changer de nature de
> contrat dans l'année), via le mécanisme existant (`EmployeeContractResolver` /
> période de contrat couvrant la date du `WorkHour`). On suit l'approche déjà en
> place dans les providers heures pour résoudre `isDriver` à la date.
**Service partagé** `App\Service\WorkHours\NightHoursCalculator`
- Extrait la logique 21h→6h aujourd'hui **dupliquée** dans
`WorkHourWeeklySummaryProvider::nightIntervalMinutes/computeMetrics` et
`YearlyHoursExportBuilder::nightIntervalMinutes/computeMetrics`.
- Méthodes :
- `nightIntervalMinutes(?string $from, ?string $to): int` (fenêtres
`[0,360]` + `[1260,1440]`, projection J+1).
- `nightMinutesFromRanges(WorkHour $wh): int` (somme sur les 3 plages).
- `nightMinutesForWorkHour(WorkHour $wh, bool $isDriver): int`
(driver → `nightHoursMinutes ?? 0`, sinon `nightMinutesFromRanges`).
- `WorkHourWeeklySummaryProvider` et `YearlyHoursExportBuilder` délèguent à ce
service pour la partie nuit (résultats identiques garantis ; les tests
existants couvrent la non-régression).
### Template
`templates/night-hours-contingent/print.html.twig`
- A4 paysage.
- En-tête : « Contingent heures de nuit — {{ year }} », date d'export.
- Colonnes : `Nom` + 12 groupes de mois, chacun deux sous-colonnes
`H.nuit` / `N.jours`.
- Lignes d'en-tête de site colorées (couleur site), comme le day-export.
- `H.nuit` formaté `12h30` (helper minutes → HH h MM), `N.jours` entier.
- Mois sans données → `0h00` / `0` (ou vide selon rendu — défaut 0).
## Tests
- `NightHoursCalculatorTest` : fenêtre 21h→6h, shift traversant minuit
(21:00→05:00 = 8h nuit), plage de jour pur (0 nuit), plages nulles.
- `NightContingentExportBuilderTest` : agrégation mensuelle, règle ≥4h=1 jour
(3h59 → 0 jour, 4h → 1 jour), conducteur via `nightHoursMinutes`, employé
multi-mois.
- Non-régression : `make test` (les tests existants de
`WorkHourWeeklySummaryProvider` / `YearlyHoursExportBuilder` valident le
refactor du service partagé).
## Documentation à mettre à jour (même intervention)
- `doc/functional-rules.md` : nouvelle section export contingent nuit.
- `CLAUDE.md` : entrée sous une rubrique export.
- `frontend/data/documentation-content.ts` : article utilisateur (niveau admin/
chef de site).
## Hors périmètre
- Colonne total annuel.
- Sélecteur de site dans le drawer.
- Export depuis un autre écran que la liste employés.
- Changement de la fenêtre de nuit ailleurs dans l'app (reste 21h→6h partout).