From a8fe244b5ce6a96b6fc2951edf3fb05d1d12a2bd Mon Sep 17 00:00:00 2001 From: tristan Date: Thu, 16 Apr 2026 15:52:19 +0200 Subject: [PATCH] =?UTF-8?q?feat=20:=20modification=20de=20la=20gestion=20d?= =?UTF-8?q?es=20jours=20f=C3=A9ri=C3=A9s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CLAUDE.md | 6 +- doc/holiday-virtual-hours.md | 110 +++ frontend/components/AppDrawer.vue | 6 +- .../driver-hours/DriverHoursDayView.vue | 9 +- frontend/components/employees/ContractTab.vue | 20 +- frontend/components/hours/HoursDayView.vue | 4 +- frontend/composables/useDriverHoursPage.ts | 20 +- frontend/composables/useEmployeeContract.ts | 48 +- frontend/composables/useHoursPage.ts | 18 +- frontend/data/documentation-content.ts | 4 +- frontend/pages/employees/[id].vue | 4 + frontend/pages/employees/index.vue | 39 +- frontend/services/dto/employee.ts | 1 + frontend/services/dto/work-hour.ts | 2 + frontend/services/employees.ts | 8 +- frontend/utils/contract.ts | 40 + migrations/Version20260416100000.php | 46 + .../DumpVerificationSnapshotCommand.php | 805 ++++++++++++++++++ src/Dto/Employees/ContractHistoryItem.php | 5 + src/Dto/WorkHours/DayContextRow.php | 5 +- src/Dto/WorkHours/WeeklyDaySummary.php | 1 + src/Entity/Employee.php | 35 + src/Entity/EmployeeContractPeriod.php | 28 + .../EmployeeContractChangeRequest.php | 4 + .../EmployeeContractChangeRequestFactory.php | 1 + .../EmployeeContractPeriodBuilder.php | 5 + .../EmployeeContractPeriodManager.php | 14 +- ...EmployeeContractPeriodManagerInterface.php | 8 + .../EmployeeContractPeriodValidator.php | 61 ++ .../Contracts/EmployeeContractResolver.php | 84 ++ .../Rtt/RttRecoveryComputationService.php | 39 +- .../DailyReferenceMinutesResolver.php | 47 + .../WorkHours/HolidayVirtualHoursResolver.php | 116 +++ .../WorkHours/WorkedHoursCreditPolicy.php | 45 +- src/State/AbsenceWriteProcessor.php | 33 +- src/State/EmployeeWriteProcessor.php | 2 + src/State/WorkHourDayContextProvider.php | 12 + src/State/WorkHourWeeklySummaryProvider.php | 44 +- .../WorkHours/WorkedHoursCreditPolicyTest.php | 72 +- tests/State/AbsenceWriteProcessorTest.php | 17 +- .../State/WorkHourDayContextProviderTest.php | 24 +- .../WorkHourWeeklySummaryProviderTest.php | 27 +- 42 files changed, 1752 insertions(+), 167 deletions(-) create mode 100644 doc/holiday-virtual-hours.md create mode 100644 migrations/Version20260416100000.php create mode 100644 src/Command/DumpVerificationSnapshotCommand.php create mode 100644 src/Service/WorkHours/DailyReferenceMinutesResolver.php create mode 100644 src/Service/WorkHours/HolidayVirtualHoursResolver.php diff --git a/CLAUDE.md b/CLAUDE.md index 07d1184..abe8e70 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -31,6 +31,7 @@ - Contract types: FORFAIT, THIRTY_FIVE_HOURS, THIRTY_NINE_HOURS, INTERIM, CUSTOM - Contract nature (per period): CDI, CDD, INTERIM - Employee contract history: `employee_contract_periods`, resolved by `EmployeeContractResolver` +- **Planning jours travaillés** (`EmployeeContractPeriod.workDaysHours` : JSON `{iso_day: minutes}`) : obligatoire pour tout contrat TIME **hors 35h/39h/INTERIM** (ex. 4h, 25h, 28h). Somme = `weeklyHours × 60`. Utilisé par `HolidayVirtualHoursResolver` (crédit férié) et `WorkedHoursCreditPolicy` (crédit absence) pour ne créditer que les jours effectivement travaillés. Validation : `EmployeeContractPeriodValidator::assertWorkDaysHours`. - Absences: stored per day (auto-split), AM/PM/full day, clear corresponding hour slots - Absences with `countAsWorkedHours=true`: credit minutes (TIME) or nothing (PRESENCE) - Driver periods (`isDriver=true` on `EmployeeContractPeriod`): separate screen `/driver-hours`, uses `dayHoursMinutes`/`nightHoursMinutes` + meal/overnight flags on `WorkHour` @@ -39,8 +40,9 @@ - Source : API gouv via `PublicHolidayService` (cache 30j) - Exclusions : env `EXCLUDED_PUBLIC_HOLIDAYS` (CSV de libellés), défaut `"Lundi de Pentecôte"`. Le filtre s'applique après le cache, côté service, donc frontend et calculs backend voient la même liste. - Écrans Heures / Heures Conducteurs (vue jour) : le nom du férié est affiché en badge `#b3e5fc` avec icône `mdi:calendar-star` dans la colonne Absence (distinct du pill absence). Bouton "Modifier" absence masqué sur férié (comme pour les formations). -- Création/édition d'absence bloquée sur un férié -- Saisie d'heures (ou de jours de présence) autorisée sur un férié — nécessaire pour éviter un déficit hebdomadaire (la référence hebdo n'est pas réduite par les fériés) +- Création/édition d'absence **autorisée** sur un férié (bouton Modifier visible). En présence d'absence, le crédit d'heures suit `absence.type.countAsWorkedHours` (WorkedHoursCreditPolicy), pas le crédit virtuel férié. +- Saisie d'heures (ou de jours de présence) autorisée sur un férié +- **Crédit automatique des heures contractuelles** sur un férié Lun-Ven pour tout contrat hors Forfait, **uniquement en l'absence d'absence déclarée** : le total journalier = `max(saisie + credited_absence, référence_contractuelle)`. Référence : 35h→7h, 39h→8h Lun-Jeu/7h Ven, CUSTOM→weeklyHours/5, INTERIM→idem 35h/39h/custom selon weeklyHours. Aucune ligne BDD créée (crédit virtuel). Drivers : crédité en `dayHoursMinutes`. Impacte directement le total hebdo RTT (tranches 25%/50%). Dès qu'une absence est posée sur le férié, le crédit virtuel saute — c'est le `countAsWorkedHours` du type d'absence qui pilote. Services : `App\Service\WorkHours\HolidayVirtualHoursResolver` + `DailyReferenceMinutesResolver`. Doc complète : `doc/holiday-virtual-hours.md`. ## Validation Rules - `isValid` (RH): locks line for everyone (admin can only untoggle validation) diff --git a/doc/holiday-virtual-hours.md b/doc/holiday-virtual-hours.md new file mode 100644 index 0000000..7c03997 --- /dev/null +++ b/doc/holiday-virtual-hours.md @@ -0,0 +1,110 @@ +# Crédit automatique des heures sur jour férié (Lun-Ven) + +## Règle + +Tout jour férié du **lundi au vendredi** crédite automatiquement les **heures contractuelles attendues** pour ce jour, pour tout contrat **autre que Forfait** (`trackingMode` ≠ `PRESENCE`). Les heures ainsi créditées sont dites *virtuelles* : aucune ligne n'est créée dans `work_hours`, elles sont injectées à l'affichage et au calcul. + +### Référence contractuelle par jour + +| Contrat | Lun-Jeu | Ven | Sam-Dim | +|-----------------|---------|-------|---------| +| 35h | 7h | 7h | 0 | +| 39h | 8h | 7h | 0 | +| CUSTOM (avec planning `workDaysHours`) | minutes du jour programmé, 0 sinon | idem | 0 | +| INTERIM 35h | 7h | 7h | 0 | +| FORFAIT | — | — | — | + +La référence par jour est calculée par `App\Service\WorkHours\DailyReferenceMinutesResolver`. + +### Planning `workDaysHours` + +Tout contrat TIME **hors 35h/39h/INTERIM** (ex. 4h, 25h, 28h) doit déclarer un planning précis sur sa `EmployeeContractPeriod` : colonne JSON `work_days_hours = {"1": 120, "4": 120}` (iso day → minutes). La somme doit égaler `weeklyHours × 60`. + +- **Sur un jour du planning** : crédit férié = minutes programmées (ex. Ewa Lun → 120 min). +- **Sur un jour hors planning** : crédit férié = 0 (elle n'aurait pas travaillé). +- Même logique appliquée par `WorkedHoursCreditPolicy::resolveContractDayMinutes` pour les crédits d'absence — un 4h en absence mardi (non programmée) = 0 crédit. + +Validation à l'écriture : `EmployeeContractPeriodValidator::assertWorkDaysHours`. Le frontend expose un bloc « Jours travaillés » (cases Lun-Ven + input `HH:MM`) sur les formulaires de création employé + d'ajout de contrat, visible uniquement quand le contrat le requiert. + +**Limitation actuelle** : l'édition in-place d'un schedule sur une période active existante n'est **pas exposée** via l'UI. Le drawer « Modifier le contrat » affiche le schedule en lecture seule à titre informatif. Pour corriger un schedule, la démarche est : clôturer le contrat en cours + créer un nouveau contrat avec le schedule corrigé. Si un besoin d'édition directe émerge, ajouter `workDaysHours` dans `EmployeeContractChangeRequest::hasPeriodChangeRequest()` et la logique d'update dans `EmployeeContractPeriodManager`. + +### Fériés exclus + +Les fériés listés dans l'env `EXCLUDED_PUBLIC_HOLIDAYS` (par défaut `Lundi de Pentecôte` — journée de solidarité) **ne donnent pas** de crédit virtuel : le `PublicHolidayService` les filtre en amont, donc `HolidayVirtualHoursResolver` ne les voit pas comme fériés. + +### Interaction avec saisie + +Quand l'employé saisit des heures ce jour-là : + +- `heures finales = max(heures saisies + crédit d'absence éventuel, heures contractuelles de référence)` + +Exemples avec un contrat 39h et un férié un lundi : + +| Saisie employé | Total affiché | Interprétation | +|------------------|---------------|----------------| +| Aucune | 8h | Crédit 100% virtuel | +| Matin 09:00-13:00 (4h) | 8h | Le minimum contractuel l'emporte | +| 09:00-12:00 + 13:00-19:00 (9h) | 9h | Les heures saisies l'emportent | + +### Interaction avec absences + +La création d'absence sur un férié Lun-Ven est **autorisée** (bouton Modifier visible). Dès qu'une absence est déclarée sur le jour (matin et/ou après-midi), le crédit virtuel férié **est désactivé** pour ce jour : c'est `absence.type.countAsWorkedHours` qui pilote le crédit d'heures, via `WorkedHoursCreditPolicy`. + +- `countAsWorkedHours = true` (ex. maladie payée) : crédit calculé normalement (7h/8h selon contrat × halfUnits/2). Même quantité que la référence virtuelle si journée complète, donc résultat identique — mais la source du crédit est l'absence, pas le férié. +- `countAsWorkedHours = false` (ex. congé sans solde) : crédit = 0. Le férié ne compense pas. + +Cette règle évite le double-crédit (absence + férié virtuel) et respecte le paramétrage fonctionnel du type d'absence. + +## Impact technique + +### Affichage + +- **Écran Heures (vue jour)** : sur un férié Lun-Ven non-Forfait, la colonne Total affiche la valeur effective (référence ou saisie, selon max). Un chip "Férié : Xh comptées" apparaît sous le pill bleu du férié. +- **Écran Heures Conducteurs (vue jour)** : idem, plus un indicateur `= Xh (férié)` sous l'input "Heures jour" pour signaler que le crédit est imputé au bucket jour. +- **Vues semaine** : les totaux hebdomadaires intègrent les minutes virtuelles. Un marqueur `F + Xh` apparaît dans la cellule du jour férié. +- **Onglet RTT** : les semaines contenant un férié Lun-Ven gagnent du temps crédité, ce qui peut générer des heures sup (25% / 50%) là où l'ancienne règle produisait un déficit. + +### Calcul RTT + +Le service `App\Service\WorkHours\HolidayVirtualHoursResolver` est injecté dans `RttRecoveryComputationService::computeRecoveryByWeek()`. Pour chaque jour ouvré : + +``` +effectiveMinutes = resolveEffectiveDailyMinutes(contract, date, metrics.totalMinutes + credited) +weeklyTotalMinutes += effectiveMinutes +``` + +Le reste du calcul (tranches +25%, +50%, base 25% à partir de 35h/39h) demeure inchangé ; seul le total hebdo injecté a évolué. + +### Calcul hebdomadaire d'affichage + +`WorkHourWeeklySummaryProvider` applique la même substitution sur `weeklyDayMinutes` et `weeklyTotalMinutes`. Le DTO `WeeklyDaySummary` expose désormais un champ `virtualHolidayMinutes` utilisé par les vues semaine. + +### Contexte jour + +`WorkHourDayContextProvider` expose `virtualHolidayMinutes` dans `DayContextRow` pour permettre au frontend de calculer le total journalier en temps réel pendant la saisie (sans aller-retour). + +### Frontend + +Le composable `frontend/composables/useHolidayVirtualHours.ts` réplique la règle côté client et est consommé par `useHoursPage.ts::getRowMetrics` et `useDriverHoursPage.ts::getRowMetrics`. + +## Impact historique + +La règle est appliquée **à chaque lecture** depuis les `WorkHour` — donc l'exercice courant et tout exercice recalculé live bénéficient automatiquement de la nouvelle règle sans migration. + +Les reports N-1 stockés dans `employee_rtt_balances.opening_*_minutes` ont été saisis manuellement par la RH (valeurs officielles) et ne sont **pas recalculés** : ces snapshots restent la source de vérité pour les soldes d'ouverture. + +## Services impliqués + +| Composant | Rôle | +|-----------|------| +| `DailyReferenceMinutesResolver` | Résolution "minutes contractuelles par jour" (logique partagée, anciennement dupliquée). | +| `HolidayVirtualHoursResolver` | Décide si la règle s'applique et renvoie le crédit virtuel ou la valeur effective. | +| `RttRecoveryComputationService` | Applique la substitution dans le calcul hebdo RTT. | +| `WorkHourWeeklySummaryProvider` | Applique la substitution dans les totaux hebdo UI. | +| `WorkHourDayContextProvider` | Expose `virtualHolidayMinutes` par salarié/jour. | +| `useHolidayVirtualHours.ts` (frontend) | Réplique la règle en live côté client. | + +## Tests + +- `tests/Service/WorkHours/HolidayVirtualHoursResolverTest.php` couvre les scénarios par contrat + jours ouvrés/chômés. +- `make test` (PHPUnit) valide l'intégration RTT / hebdo / contexte jour. diff --git a/frontend/components/AppDrawer.vue b/frontend/components/AppDrawer.vue index 375d052..01d9776 100644 --- a/frontend/components/AppDrawer.vue +++ b/frontend/components/AppDrawer.vue @@ -4,13 +4,13 @@
-
-
+
+

{{ title }}

-
+
diff --git a/frontend/components/driver-hours/DriverHoursDayView.vue b/frontend/components/driver-hours/DriverHoursDayView.vue index 89989e3..2606cf6 100644 --- a/frontend/components/driver-hours/DriverHoursDayView.vue +++ b/frontend/components/driver-hours/DriverHoursDayView.vue @@ -76,7 +76,6 @@

void onToggleValidationBulk: (checked: boolean) => Promise | void onToggleSiteValidationBulk: (checked: boolean) => Promise | void - getRowMetrics: (employeeId: number) => { dayMinutes: number; nightMinutes: number; totalMinutes: number } + getRowMetrics: (employeeId: number) => { dayMinutes: number; nightMinutes: number; workshopMinutes: number; totalMinutes: number; virtualHolidayMinutes: number } getRowAbsenceLabel: (employeeId: number) => string getRowAbsenceStyle: (employeeId: number) => { backgroundColor: string } | undefined getRowUpdatedAt: (employeeId: number) => string diff --git a/frontend/components/employees/ContractTab.vue b/frontend/components/employees/ContractTab.vue index 5651292..230bf7c 100644 --- a/frontend/components/employees/ContractTab.vue +++ b/frontend/components/employees/ContractTab.vue @@ -108,6 +108,13 @@

La date de fin est obligatoire.

+ +
-
+ + +
+