## Problème
Sur l'écran **Heures** / **Heures Conducteurs**, l'enregistrement envoyait au bulk-upsert une entrée pour **tous** les employés visibles non verrouillés, à partir de l'état en mémoire de la grille. Le backend (`WorkHourBulkUpsertProcessor`) traitant une **entrée vide comme une suppression**, un admin avec une grille **périmée** pouvait supprimer une ligne saisie entre-temps par un autre utilisateur.
### Scénario reproduit
1. Un admin ouvre l'écran ; la ligne d'un salarié `ROLE_SELF` est vide.
2. Ce salarié saisit ses heures dans sa propre session → ligne créée, **non validée** (donc non verrouillée).
3. L'admin, sur sa grille périmée, enregistre d'autres employés.
4. Le payload contient une entrée **vide** pour le salarié → le backend supprime sa ligne. **Perte de données.**
## Correctif (suivi des lignes modifiées)
`hydrateRows` capture un instantané `loadedRows` de l'état chargé depuis le serveur. `handleSave` ne transmet plus que les lignes **dont l'état courant diffère de l'instantané**.
- Ligne **intouchée** → jamais envoyée → jamais supprimée ✅
- Ligne **vidée volontairement** → envoyée vide → supprimée (métier conservé)
- Ligne **remplie/modifiée** → envoyée → créée/mise à jour
Symétrique dans `useHoursPage.ts` et `useDriverHoursPage.ts`.
## Limite connue
Pas de verrou optimiste backend : l'édition **explicite** d'une ligne sur données périmées peut toujours écraser une saisie concurrente sur cette même ligne (hors périmètre).
## Doc
- `doc/hours-save-dirty-tracking.md` (nouveau)
- Note `CLAUDE.md` (section *Validation Rules*)
## Vérification
- Pre-commit hook : **236 tests PHPUnit OK**.
- Pas de harnais de tests frontend (revue de code uniquement).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Reviewed-on: #31
Co-authored-by: tristan <tristan@yuno.malio.fr>
Co-committed-by: tristan <tristan@yuno.malio.fr>
## Fonctionnel
- Calendrier MalioDate en vue Jour (écrans Heures ET Heures Conducteurs) : les jours entièrement validés par un admin sont peints en vert.
- Endpoint `GET /work-hours/validation-status?from=&to=[&driver=1]` (scope conducteur inversé via `driver=1`), périmètre complet (ignore le filtre sites).
- Chargement à la volée par mois (event `@month-change`), refresh après validation / saisie / absence.
## Harmonisation @malio/layer-ui 1.7.11
- `reserveMessageSpace=false` sur tous les champs (alignement).
- Tous les drawers migrés sur `MalioDrawer` (titre via slot `#header`, `AppDrawer` custom supprimé).
- Boutons d'action en `MalioButton` ; deux boutons côte à côte partagent l'espace.
- Inputs date en `MalioDate`, sélecteur semaine en `MalioDateWeek`.
- Boutons d'ajout uniformisés sur « Ajouter » + icône.
## Divers
- `.env` : `EXCLUDED_PUBLIC_HOLIDAYS="null"`.
- Doc : `doc/hours-validated-days.md`, `documentation-content.ts`, `CLAUDE.md`.
- Tests : provider `WorkHourValidationStatus` (suite complète 236/236 OK via pre-commit hook).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Reviewed-on: #30
Co-authored-by: tristan <tristan@yuno.malio.fr>
Co-committed-by: tristan <tristan@yuno.malio.fr>
Le libellé sous le nom de l'employé affiche en suffixe les jours du planning
workDaysHours au format court (ex. BUREAU — CDI — LU,JE) pour les contrats CUSTOM.
Résolu à la date filtrée et exposé via WorkHourDayContext.workDaysHours ; formaté
front par formatWorkedDaysShort. Limité aux CUSTOM (35h/39h/forfait/intérim sans
planning → rien affiché) et à l'écran Heures (pas Heures Conducteurs).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
## Correctifs RH (branche fix/retour-rh)
### Vue Jour (Heures)
- Mode saisie/présence, libellé de contrat et sauvegarde résolus **à la date affichée** (et non au contrat courant). Corrige les salariés passés 39h/35h → Forfait.
### RTT — heures supplémentaires
- Proratisation du **plafond 25%/50%** pour les embauches en milieu de semaine (la bande +25% se décale au lieu de rester bloquée à 43h). Témoin Dylan : 4h à 25% + 3h à 50%.
### Récap salaire (PDF mensuel)
- Forfait : congés imputés **N-1** non affichés et comptés en présence.
- Colonne « Heures payés » **scindée 25% / 50%** (en-tête fusionné).
- **Exclusion des salariés sans contrat** sur le mois (ex. Marine, contrat terminé).
### Exports heures annuelles (par salarié + tous)
- **Tous les jours sous contrat** affichés, même vides/non saisis (corrige les lignes manquantes).
- Samedis/dimanches en **gris plus foncé**.
### Panier de nuit
- **Ne s'applique pas aux conducteurs** (vue semaine + récap salaire).
## Tests
- 11 tests ajoutés. Suite verte hors un test legacy pré-existant dépendant de la date (`EmployeeRttSummaryProviderTest::testNoQueryParamsKeepsLegacyYearDefaulting`, non modifié par cette branche).
## À noter (hors scope)
- L'export heures annuelles *tous salariés* peut dépasser `memory_limit=256M` (Dompdf) — limitation **pré-existante**, non corrigée ici.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Reviewed-on: #21
Co-authored-by: tristan <tristan@yuno.malio.fr>
Co-committed-by: tristan <tristan@yuno.malio.fr>