## Contexte
Sur le calendrier, une absence est stockée **une ligne par jour** sans lien entre les jours (cf. `AbsenceWriteProcessor::expandAbsenceRange`). Conséquences pour la RH :
- **Impossible de retirer une plage** : la suppression n'effaçait que le jour cliqué → il fallait supprimer chaque jour un par un.
- **Modifier une plage laissait des incohérences** : le `PATCH` réutilisait une ligne et en recréait d'autres sans nettoyer l'ancien bloc → jours « fantômes » au raccourcissement et doublons à l'allongement.
## Changements (`frontend/pages/calendar.vue`)
- **Supprimer** (`handleDelete`) : efface **toutes** les absences de l'employé comprises dans la plage `[début ; fin]` du drawer. Flux RH : clic sur un jour → étendre la date de fin → Supprimer. Jours sans absence ignorés (aucune erreur) ; jour validé (`isValid`/site) protégé côté backend. Confirmation avec nombre de jours + intervalle.
- **Modifier** (`handleSubmit`) : **remplacement de bloc** — supprime l'ancien bloc contigu de même type (vers l'avant depuis le jour cliqué) + les absences recouvertes par la nouvelle plage, puis recrée la plage via `createAbsence`. Corrige le bug du `PATCH`. Les jours antérieurs au jour cliqué ne sont jamais touchés ; confirmation « chevauche une autre » seulement pour un autre type. `updateAbsence` n'est plus appelé depuis le calendrier.
## Pourquoi côté frontend
Le backend ne peut pas reconstituer « la plage » (aucun identifiant de groupe en BDD) ; le frontend a la plage visible. Vérifié : les écrans **Heures** et **Heures Conducteurs** verrouillent les dates du drawer (`lock-dates`), donc le `PATCH` y reste mono-jour — le calendrier est le seul écran à reshaper une plage. `AbsenceWriteProcessor` non modifié.
## Documentation
- `doc/functional-rules.md`, `frontend/data/documentation-content.ts` (in-app), `CLAUDE.md`.
## Tests
- 253 tests PHPUnit verts (hook pre-commit). Pas de framework de test frontend dans le projet.
- À valider en réel : raccourcir / allonger une plage (pas de jour fantôme ni doublon), supprimer une plage d'un coup.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Reviewed-on: #34
Co-authored-by: tristan <tristan@yuno.malio.fr>
Co-committed-by: tristan <tristan@yuno.malio.fr>
## Contexte
Certains comptes sont **partagés** par plusieurs personnes (ex. compte « Usine »), y compris depuis des smartphones. Le journal d'activité ne stockait que le `username` → impossible de distinguer les intervenants. Cette PR ajoute un **contexte forensique automatique** à chaque entrée du journal.
## Ce qui est ajouté (capté automatiquement, sans friction utilisateur)
- **Adresse IP** de la requête
- **User-Agent brut** (borné à 1024 caractères)
- **Libellé appareil lisible** dérivé du User-Agent : `Type · OS · Navigateur` (ex. `Mobile · Android · Chrome`)
- **Identifiant d'appareil persistant** envoyé par le front (header `X-Device-Id`, stocké en `localStorage`, borné à 64 car.) — distingue les **appareils** derrière un compte partagé
## Implémentation
- `UserAgentParser` (service maison, sans dépendance) — détection ordonnée OS/navigateur, testée
- 4 colonnes **nullable** sur `audit_logs` + migration réversible (pas de backfill, rétro-compatible)
- Capture **centralisée** dans `AuditLogger::log()` via `RequestStack` — aucun processor modifié
- Champs exposés dans l'API lecture (`AuditLogProvider` + DTO TS aligné) via `AuditLogReadRepositoryInterface` (suit le pattern existant des autres read-repos)
- Front : `useDeviceId` + injection du header `X-Device-Id` dans `useApi` (sur toutes les requêtes, SSR-safe)
- `framework.trusted_proxies` documenté (commenté) pour une IP correcte derrière un reverse proxy
- Docs : `doc/audit-logging.md` + `CLAUDE.md`
## Hors périmètre (étapes suivantes)
- **Écran du journal (`audit-logs.vue`) non modifié** — l'affichage des nouvelles colonnes fera l'objet d'une refonte séparée. Les données sont prêtes côté API.
- La doc in-app (`documentation-content.ts`) n'est pas touchée : le journal est un outil caché `ROLE_SUPER_ADMIN` sans article existant ni niveau de doc super-admin.
## À noter pour le déploiement
- L'IP n'est fiable derrière un reverse proxy qu'une fois `framework.trusted_proxies` activé (livré commenté).
## Tests
`OK (249 tests, 533 assertions)` — sortie PHPUnit propre (aucune notice).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Reviewed-on: #33
Co-authored-by: tristan <tristan@yuno.malio.fr>
Co-committed-by: tristan <tristan@yuno.malio.fr>
## 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 récap salaire comptait les congés (C) tombant un dimanche via
countAbsencesByCode, alors que l'onglet Congés, le rollover et les jours de
présence l'ignoraient déjà. Garde ajoutée (C + dimanche → ignoré) pour aligner :
poser une période à cheval sur un week-end (ex. jeu→mar) ne fait plus perdre le
dimanche. Correctif au comptage uniquement : les lignes d'absence du dimanche
restent créées et affichées sur le calendrier (volonté RH), l'existant cesse de
compter sans migration. Périmètre strict : code C (maladie/AT inchangés), samedi
inchangé (budget dédié).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
Un contrat CUSTOM < 35h qui ne travaille pas le lundi (jour de solidarité,
workDaysHours[lundi] absent → attendu = 0) ne portait à tort un déficit
forfaitaire ((0 − 0) − prorata = −prorata). Garde ajoutée : aucun déficit
quand expectedMinutes === 0. Ewa (Lun+Jeu) reste à −0h48 ; Nadia (Mar+Ven)
passe de −0h48 à 0.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Les heures contractuelles au-delà de 35h (ex. 39h → 17,33h décimales = 17h20/mois)
sont payées chaque mois sans transiter par les paiements RTT (référence 39h). Elles
manquaient au contingent. Ajout via StructuralOvertimeContingentCalculator :
(weeklyHours-35)×260 min/mois, généralisé aux contrats non-forfait/non-intérim >35h,
proratisé aux jours sous contrat. Branché sur l'encart fiche et l'export PDF.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
## Résumé
Nouvel export PDF **Contingent heures de nuit** dans le drawer Export de la liste employés.
- PDF **A4 paysage** : lignes = employés (groupés par site, triés displayOrder/nom/prénom), colonnes = 12 mois civils, chaque mois avec 2 sous-colonnes **H.nuit** et **N.jours**.
- Heures de nuit = minutes dans la fenêtre **21h→6h** via un service partagé `NightHoursCalculator` (mutualisé avec `WorkHourWeeklySummaryProvider` et `YearlyHoursExportBuilder` — duplication supprimée, sans changement de comportement).
- **Conducteurs inclus** via `WorkHour.nightHoursMinutes`. Statut conducteur résolu par date.
- **N.jours** = nb de jours où les minutes de nuit ≥ 240 (4h). Aucun crédit absence/férié.
- Périmètre via `EmployeeRepository::findScoped` (admin → tous, chef de site → ses sites), endpoint `GET /night-hours-contingent/print?year=YYYY` (`ROLE_USER`).
- Sélecteur d'année (année civile). Colonne Nom calibrée, séparateurs de mois épais.
## Composants
- Service `NightHoursCalculator`, builder `NightContingentExportBuilder`, DTO `NightContingentRow`
- Provider `NightHoursContingentPrintProvider` + opération API `NightHoursContingentPrint`
- Gabarit `templates/night-hours-contingent/print.html.twig`
- Option frontend dans `frontend/pages/employees/index.vue`
- Docs : `doc/functional-rules.md`, `CLAUDE.md`, `frontend/data/documentation-content.ts`
## Tests
- Nouveaux tests unitaires : `NightHoursCalculatorTest` (fenêtre 21h→6h, passage minuit, bornes), `NightContingentExportBuilderTest` (agrégation mensuelle, règle ≥4h=1j, conducteur, cas sans heures)
- Suite complète : **208 tests OK**
- Rendu PDF validé visuellement (Twig→Dompdf)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Reviewed-on: #28
Co-authored-by: tristan <tristan@yuno.malio.fr>
Co-committed-by: tristan <tristan@yuno.malio.fr>
Le récap des congés se fige désormais à la fin de la semaine précédente
(S-1) au lieu de l'avant-dernière (S-2), incluant ainsi une semaine de
plus. Demande métier.
- LeaveRecapCutoff : -14j -> -7j
- Test unitaire figeant la règle S-1
- Doc fonctionnelle, doc in-app et CLAUDE.md mis à jour
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
L'export des heures de la vue Jour était réservé aux admins. Il est désormais
ouvert aux chefs de site, restreint à leurs sites :
- sécurité endpoint ROLE_ADMIN -> ROLE_USER
- périmètre résolu côté backend via EmployeeRepository::findScoped() (un siteIds
hors périmètre est ignoré, aucune fuite inter-sites)
- bouton Exporter visible pour admin + chef de site (masqué pour ROLE_SELF)
- doc, doc in-app et CLAUDE.md mis à jour
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Le tri intra-site de l'export PDF des heures (vue Jour) reprend désormais celui du calendrier : **`displayOrder` (ordre manuel) → nom → prénom**, au lieu du nom seul.
`doc/` (CLAUDE.md) mis à jour. Tests backend verts (173/361).
Reviewed-on: #26
Co-authored-by: tristan <tristan@yuno.malio.fr>
Co-committed-by: tristan <tristan@yuno.malio.fr>
Affinements de l'export PDF des heures (vue Jour) :
- **Colonne Statut** : affiche le **code** du type d'absence (ex. `AT`) au lieu du libellé, sur sa couleur de fond. Férié sans absence inchangé (nom du férié sur fond bleu clair).
- **Colonne Total** en gras.
- **Légende** sous le tableau : carré coloré contenant le code + libellé à droite, 6 éléments par ligne, triée et dédupliquée (hors férié).
- **Bouton Exporter masqué en vue Semaine** (visible uniquement en vue Jour).
Docs mises à jour : `doc/hours-day-export.md`, `frontend/data/documentation-content.ts`, `CLAUDE.md`. Tests backend verts (173/361).
Reviewed-on: #25
Co-authored-by: tristan <tristan@yuno.malio.fr>
Co-committed-by: tristan <tristan@yuno.malio.fr>
## Résumé
Ajoute un bouton **Exporter** (admin uniquement) à droite du titre « Heures » qui génère un **PDF d'une journée**, regroupé par site, reprenant les colonnes de la vue Jour **sans la colonne « Valider »**.
- Drawer : champ date (préremplit la date affichée) + cases à cocher des sites (préselectionnées sur le filtre courant).
- Portée identique à l'écran : non-conducteurs, sous contrat à la date, sites cochés (lignes vides incluses).
- Jour/Nuit/Total incluent le crédit d'absence et le crédit virtuel férié.
## Implémentation
- Back : `WorkHourDayExport` (ApiResource) + `WorkHourDayExportProvider`, endpoint `GET /work-hours/day-export?workDate=&siteIds=` (ROLE_ADMIN).
- Calcul des cellules mutualisé via `YearlyHoursExportBuilder::buildDayRowsForEmployees` (source unique de vérité).
- Gabarit `templates/work-hour-day-export/print.html.twig` (A4 portrait compact).
- Front : `HoursDayExportDrawer.vue` + câblage dans `pages/hours.vue`.
- Docs : `doc/hours-day-export.md`, `documentation-content.ts`, `CLAUDE.md`.
## Tests
- Test unitaire `YearlyHoursDayRowsTest` ajouté.
- Suite complète verte : 173 tests, 359 assertions.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Reviewed-on: #24
Co-authored-by: tristan <tristan@yuno.malio.fr>
Co-committed-by: tristan <tristan@yuno.malio.fr>
## Besoin RH
Pouvoir saisir un paiement RTT sur l'exercice précédent (ex. RTT de mai réglés après la bascule du 1er juin).
## Implémentation (Option B)
- Paiement autorisé sur l'exercice courant + l'exercice immédiatement précédent (N-1).
- Après saisie sur N-1, le report d'ouverture de l'exercice courant est recalculé automatiquement (computeClosingBalance) dans une transaction → aucun double comptage.
- Refus si ce report est verrouillé (is_locked) : la RH le déverrouille d'abord.
- Fallback EmployeeRttSummaryProvider::resolveCarry aligné sur computeClosingBalance : disponible correct même sans ligne stockée.
- Front : bouton « + Payer les RTT » actif sur l'exercice précédent.
- Docs : CLAUDE.md, doc/rtt-tab.md, documentation-content.ts.
## Vérification
- ✅ 172 tests OK, cs-fixer OK, conteneur compile.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Reviewed-on: #23
Co-authored-by: tristan <tristan@yuno.malio.fr>
Co-committed-by: tristan <tristan@yuno.malio.fr>
La bascule app:rtt:rollover ne reprenait que les RTT acquis de l'exercice qui
se terminait : le report d'ouverture déjà présent était perdu et les paiements
n'étaient pas déduits. Le nouveau report reprend le solde de clôture =
report d'ouverture(N-1) + acquis(N-1) − RTT payés(N-1), soit le "Disponible"
affiché par EmployeeRttSummaryProvider.
- nouveau RttClosingBalanceService (fold pur testé : invariant somme tranches =
disponible, cascade déficit 50% avant 25%, récup CUSTOM non perdue)
- RttRolloverCommand branché dessus + option --recompute (écrase les lignes
existantes non verrouillées, pour reprise d'une bascule erronée)
- test date-sensible EmployeeRttSummaryProviderTest rendu robuste
- docs: doc/rtt-rollover.md, CLAUDE.md, documentation-content.ts
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
| Numéro du ticket | Titre du ticket |
|------------------|-----------------|
| | |
## Description de la PR
## Modification du .env
## Check list
- [ ] Pas de régression
- [ ] TU/TI/TF rédigée
- [ ] TU/TI/TF OK
- [ ] CHANGELOG modifié
Reviewed-on: #22
Co-authored-by: tristan <tristan@yuno.malio.fr>
Co-committed-by: tristan <tristan@yuno.malio.fr>
## 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>
| Numéro du ticket | Titre du ticket |
|------------------|-----------------|
| | |
## Description de la PR
## Modification du .env
## Check list
- [x] Pas de régression
- [x] TU/TI/TF rédigée
- [x] TU/TI/TF OK
- [x] CHANGELOG modifié
Reviewed-on: #19
Co-authored-by: tristan <tristan@yuno.malio.fr>
Co-committed-by: tristan <tristan@yuno.malio.fr>
Permet de consulter les exercices passés (table hebdomadaire RTT) en
réutilisant le pattern de l'onglet Congés. Plage bornée par
max(début historique contrat, RTT_START_DATE). Bouton + Payer les RTT
verrouillé sur exercices clos. Onglet masqué pour FORFAIT (inchangé).
Backend : rttStartDate désormais toujours exposé sur EmployeeRttSummary
pour que le sélecteur conserve sa borne lors de la navigation vers un
exercice passé. Le masquage existant des lignes Report continue de
fonctionner (comparaison mois-à-mois).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Permet de consulter les exercices passés (calendrier + compteurs) sur
l'onglet Congés. La plage proposée est bornée par max(début historique
contrat, RTT_START_DATE) pour ne pas remonter avant la mise en service
du logiciel. Édition des stocks N-1 et fractionnés verrouillée sur
exercices clos.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Nouvelle entité InterimAgency (table interim_agencies, API lecture seule)
- Sélecteur agence conditionnel dans les formulaires création employé et ajout contrat
- Affichage "Intérim (NomAgence)" sur la liste employés et l'historique contrat
- Date de fin obligatoire côté frontend pour CDD et INTERIM (aligné backend)
- Renommage "Types d'absence" → "Types de statut" (sidebar, page, titre)
- Renommage en-tête "Absence" → "Statut" sur les vues jour heures et conducteurs
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>