# Contingent d'heures supplémentaires payées — Design

Date : 2026-06-11
Statut : validé (brainstorming)

## Objectif

La RH a besoin de suivre, **par année civile (Janvier→Décembre)**, le volume d'heures
supplémentaires payées à chaque employé non-forfait (chauffeurs inclus), rapporté au
plafond réglementaire annuel (le « contingent ») :

- **350 h** pour les chauffeurs (conducteurs),
- **220 h** pour les autres non-forfait.

Deux livrables :

1. **Fiche employé** — un encart dans le header affichant `Contingent {année} : X h / plafond h`.
2. **Écran liste employés** — un export PDF supplémentaire : par employé, les heures payées
   de chaque mois + une colonne finale « Total payé / Total payable », groupé par site.

## Règles métier (validées)

- **Heures payées** = `base25Minutes + base50Minutes` (en minutes), **hors majoration
  (bonus)**. Cohérent avec la colonne « Heures payés » du récap salaire, déjà définie hors
  bonus.
- **Période = vraie année civile (Janv–Déc).** Les paiements RTT (`EmployeeRttPayment`)
  sont stockés par **exercice** (`year` = année d'exercice Juin N-1 → Mai N) + `month`
  (1–12). L'année civile d'un paiement se reconstitue avec la même formule que
  `RttTab.vue:392` :

  ```
  annéeCivile = month >= 6 ? exerciseYear - 1 : exerciseYear
  ```

  Donc l'année civile **Y** agrège :
  - exercice `Y`, mois 1–5 (Janv–Mai Y),
  - exercice `Y+1`, mois 6–12 (Juin–Déc Y).

- **Plafond** : `isDriver` du **contrat courant** → 350 h, sinon → 220 h.
- **Périmètre** : non-forfait uniquement. Les FORFAIT sont exclus (pas d'heures supp
  payées ; onglet RTT déjà masqué pour eux).

## Architecture

### Cœur partagé — `App\Service\WorkHours\OvertimePaidContingentCalculator`

Source de vérité unique, consommée par l'endpoint fiche employé ET le builder PDF.

```php
final readonly class OvertimePaidContingentCalculator
{
    public const int CAP_HOURS_DRIVER = 350;
    public const int CAP_HOURS_DEFAULT = 220;

    // Heures payées (base25+base50) ventilées par mois civil 1..12 pour l'année civile.
    public function monthlyBaseMinutes(Employee $employee, int $civilYear): array; // <int,int> 1..12

    // Somme des 12 mois.
    public function totalBaseMinutes(Employee $employee, int $civilYear): int;

    // 350 si conducteur (contrat courant isDriver), sinon 220.
    public function capHours(Employee $employee): int;
}
```

Calcul de `monthlyBaseMinutes` :
1. Récupérer les paiements des exercices `civilYear` et `civilYear+1` (fetch groupé).
2. Pour chaque paiement, calculer son année civile via la formule ci-dessus ; ne garder que
   ceux dont l'année civile == `civilYear`.
3. Bucketiser par `month`, sommer `base25Minutes + base50Minutes`.

Statut conducteur : résolu via le contrat courant de l'employé (cohérent avec le choix
« contrat courant » pour le plafond). Réutiliser le mécanisme existant
(`employee.currentContract` / `EmployeeContractResolver`).

### Repository

Ajout à `EmployeeRttPaymentRepository` :

```php
// Fetch groupé pour le PDF (évite N+1 sur N employés).
public function findByEmployeesAndYears(array $employees, array $years): array;
```

Le calculator pour un seul employé peut réutiliser `findByEmployeeAndYear()` (existant) deux
fois (exercices `civilYear` et `civilYear+1`).

## Partie A — Encart fiche employé (header)

### Backend
- ApiResource `EmployeeOvertimeContingentOutput` + opération
  `GET /employees/{id}/overtime-contingent?year=YYYY` (`ROLE_ADMIN`).
- Défaut `year` = année civile courante. Validation 2000–2100.
- Provider : retourne `{ year, paidMinutes, capHours, isDriver }`.

### Frontend
- Service + composable : fetch sur la fiche employé **uniquement pour les non-forfait**
  (même condition que l'affichage de l'onglet RTT).
- Affichage : ligne texte dans le header, sous le libellé contrat
  (`useEmployeeDetailPage` / header de `pages/employees/[id].vue`), au format :

  ```
  Contingent 2026 : 142 h / 220 h
  ```

  Passe en **rouge** (`text-m-danger` / classe danger) si `paidMinutes > capHours*60`.
- **Année civile courante uniquement, pas de sélecteur** dans le header. L'historique se
  consulte via le PDF.

## Partie B — Export PDF (écran liste employés)

Calque exact de l'export contingent heures de nuit (`night-hours-contingent`).

### Backend
- ApiResource `OvertimeContingentPrint` → `GET /overtime-contingent/print?year=&siteIds=`
  (`ROLE_USER`).
- Provider `OvertimeContingentPrintProvider` :
  - Périmètre via `EmployeeRepository::findScoped($user)` (admin → tous, chef de site → ses
    sites). `siteIds` hors périmètre ignoré.
  - **Exclut les FORFAIT** (contrat courant) en plus du filtre `hasContractInRange` sur
    l'année.
  - Groupe par site (`displayOrder`), tri intra-site `displayOrder → nom → prénom`
    (identique au calendrier / aux autres exports).
- Builder `OvertimeContingentExportBuilder::buildRows($employees, $year)` :
  - utilise `OvertimePaidContingentCalculator` (fetch groupé via `findByEmployeesAndYears`),
  - retourne par employé : `months[1..12]` (minutes base payées), `totalMinutes`, `capHours`.
- DTO `App\Dto\WorkHours\OvertimeContingentRow`.

### Template
- `templates/overtime-contingent/print.html.twig` — **A4 paysage**.
- Colonnes : Nom employé · Janv … Déc (heures payées du mois, format `XhYY` ou `—` si 0) ·
  **Total : `total payé h / plafond h`** (ex. `142 h / 220 h`).
- Total en gras ; cellule total en rouge si dépassement.
- En-têtes de site colorées (comme night-contingent).

### Frontend (drawer existant `pages/employees/index.vue`)
- Ajouter le choix `overtime-contingent` à `exportTypeOptions`
  (libellé ex. « Contingent H.supp. »).
- Bloc de formulaire dédié : sélecteur **Année** (`exportYearOptions`) + sélecteur **Sites**
  multi-sélection (tags, calqué sur le drawer d'export jour ; valeurs = sites visibles).
- `isExportValid` : `exportYear > 0` (sites optionnels — vide = tous les sites du périmètre).
- `handleExportValidate` : `printPdf('/overtime-contingent/print?year=${exportYear}${siteIdsParam}')`.

## Tests

- `OvertimePaidContingentCalculatorTest` :
  - mapping année civile (paiement exercice 2027 mois 9 → compté en 2026),
  - frontière mois 5/6 (mai = exercice, juin = exercice-1),
  - somme `base25+base50` hors bonus,
  - plafond 350 (driver) vs 220.
- `OvertimeContingentExportBuilderTest` : ventilation mensuelle + total + plafond par
  employé, fetch groupé.
- Test provider : exclusion forfait, périmètre `findScoped`, tri/groupement par site.

## Documentation à mettre à jour (règle projet obligatoire)

- `doc/overtime-contingent.md` (nouveau) — règles + mapping civil/exercice.
- `CLAUDE.md` — section dédiée (cœur partagé, mapping, plafonds, périmètre).
- `frontend/data/documentation-content.ts` — section utilisateur (admin) décrivant l'encart
  et l'export.

## Hors périmètre (consigné pour plus tard)

- **Bug latent du récap salaire** : `SalaryRecapPrintProvider:86` requête
  `findByYearAndMonth(annéeCivile, mois)` alors que les paiements sont stockés par exercice.
  Pour les mois Juin–Déc, un paiement RTT est donc probablement mal rattaché sur le récap
  mensuel. À corriger dans une intervention séparée.
- Plafonds 350/220 en constantes nommées dans le calculator ; passage en config/env
  envisageable ultérieurement.
