# Enregistrement des heures — envoi des seules lignes modifiées ## Problème corrigé (perte de données par écrasement « à l'aveugle ») L'écran **Heures** (et **Heures Conducteurs**) présente une grille d'une journée avec **tous** les employés du périmètre. L'enregistrement (`POST /work-hours/bulk-upsert`, `WorkHourBulkUpsertProcessor`) a une sémantique **upsert par (employé, date)** où une **entrée vide supprime** la ligne existante (« une ligne vide supprime l'enregistrement »). Avant correctif, `handleSave` (front) envoyait une entrée pour **chaque** employé visible non verrouillé, à partir de l'état en mémoire de la grille. Conséquence en cas de **concurrence** : 1. Un admin ouvre l'écran ; la ligne d'un salarié (ex. utilisateur `ROLE_SELF`) est vide. 2. Ce salarié saisit ses heures dans sa propre session → ligne créée en BDD, **non validée** (donc non verrouillée). 3. L'admin, sur sa grille **périmée**, saisit les heures d'**autres** employés et enregistre. 4. Le payload contient une entrée **vide** pour le salarié (état périmé). Le backend relit la BDD (ligne désormais remplie), constate « entrée vide ≠ existant » → **supprime** la ligne fraîchement saisie. Perte de données. ## Correctif (suivi des lignes modifiées côté front) `hydrateRows` capture désormais un **instantané** des lignes telles que chargées depuis le serveur (`loadedRows`, clone indépendant de `rows`). À l'enregistrement, `handleSave` ne transmet **que les lignes dont l'état courant diffère de l'instantané chargé** : ```ts const entries = candidates .map((employee) => ({ current: buildEntry(employee, rows.value[employee.id] ?? emptyRow()), original: buildEntry(employee, loadedRows.value[employee.id] ?? emptyRow()), })) .filter(({ current, original }) => JSON.stringify(current) !== JSON.stringify(original)) .map(({ current }) => current) ``` Conséquences : - Une ligne **intouchée** n'est jamais transmise → jamais supprimée, même si un autre utilisateur l'a saisie/modifiée entre-temps. **C'est le correctif du bug.** - Une ligne **vidée volontairement** par l'utilisateur diffère de l'instantané → transmise vide → supprimée (comportement métier conservé). - Une ligne **remplie** diffère → transmise → créée/mise à jour. Implémenté symétriquement dans `frontend/composables/useHoursPage.ts` (non-conducteurs) et `frontend/composables/useDriverHoursPage.ts` (conducteurs). ## Limite connue (hors périmètre de ce correctif) Le suivi des lignes modifiées **ne couvre pas** le cas où l'admin **édite explicitement** une ligne sur des données périmées (il voit la ligne vide, tape une valeur, écrasant une saisie concurrente sur cette même ligne). Ce cas résiduel relèverait d'un **verrou optimiste** (comparaison d'`updatedAt`/version côté backend), non implémenté ici. Le backend n'a aucune détection de conflit concurrent (pas de version, pas d'horodatage comparé).