From 74abecbe03b9f003c0be31193925cf104a25703e Mon Sep 17 00:00:00 2001 From: tristan Date: Tue, 16 Jun 2026 13:53:03 +0000 Subject: [PATCH] =?UTF-8?q?feat(heures)=20:=20calendrier=20des=20jours=20v?= =?UTF-8?q?alid=C3=A9s=20(vue=20Jour)=20+=20harmonisation=20Malio=20UI=20(?= =?UTF-8?q?#30)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 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: https://gitea.malio.fr/MALIO-DEV/SIRH/pulls/30 Co-authored-by: tristan Co-committed-by: tristan --- .env | 2 +- CLAUDE.md | 15 +- doc/hours-validated-days.md | 75 ++ frontend/components/AbsenceFormDrawer.vue | 77 +- frontend/components/AbsencePrintDrawer.vue | 85 +- frontend/components/AppDrawer.vue | 56 -- frontend/components/BulkYearlyHoursDrawer.vue | 24 +- .../components/EmployeeYearlyHoursDrawer.vue | 19 +- frontend/components/SalaryRecapDrawer.vue | 27 +- frontend/components/employees/BonusTab.vue | 45 +- frontend/components/employees/ContractTab.vue | 61 +- .../components/employees/FormationTab.vue | 45 +- frontend/components/employees/LeaveTab.vue | 63 +- frontend/components/employees/MileageTab.vue | 45 +- .../components/employees/ObservationTab.vue | 45 +- frontend/components/employees/RttTab.vue | 32 +- .../components/hours/HoursDayExportDrawer.vue | 62 +- frontend/components/hours/HoursToolbar.vue | 66 +- .../components/hours/WeekCommentDrawer.vue | 12 +- frontend/composables/useDriverHoursPage.ts | 58 +- frontend/composables/useHoursPage.ts | 65 +- frontend/data/documentation-content.ts | 11 + frontend/package-lock.json | 838 +++++++++++++++++- frontend/package.json | 2 +- frontend/pages/absence-types.vue | 11 +- frontend/pages/calendar.vue | 6 +- frontend/pages/driver-hours.vue | 5 + frontend/pages/employees/[id].vue | 2 +- frontend/pages/employees/index.vue | 101 +-- frontend/pages/hours.vue | 5 + frontend/pages/login.vue | 4 +- frontend/pages/sites.vue | 9 +- frontend/pages/users.vue | 16 +- frontend/services/work-hours.ts | 20 + src/ApiResource/WorkHourValidationStatus.php | 35 + .../WorkHourValidationStatusProvider.php | 143 +++ .../WorkHourValidationStatusProviderTest.php | 189 ++++ 37 files changed, 1881 insertions(+), 495 deletions(-) create mode 100644 doc/hours-validated-days.md delete mode 100644 frontend/components/AppDrawer.vue create mode 100644 src/ApiResource/WorkHourValidationStatus.php create mode 100644 src/State/WorkHourValidationStatusProvider.php create mode 100644 tests/State/WorkHourValidationStatusProviderTest.php diff --git a/.env b/.env index 54c5f51..a979df5 100644 --- a/.env +++ b/.env @@ -40,7 +40,7 @@ DATABASE_URL="postgresql://app:!ChangeMe!@127.0.0.1:5432/app?serverVersion=16&ch RTT_START_DATE=2026-02-23 # Comma-separated list of public holiday labels to exclude from the government API response # (typically the "journée de solidarité" worked in many companies) -EXCLUDED_PUBLIC_HOLIDAYS="Lundi de Pentecôte" +EXCLUDED_PUBLIC_HOLIDAYS="null" ###< app ### ###> nelmio/cors-bundle ### diff --git a/CLAUDE.md b/CLAUDE.md index a6f7614..97db153 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -36,6 +36,7 @@ - **Écrans Heures / Heures Conducteurs (vue jour)** : le libellé nature (CDI/CDD/Intérim) sous le nom de l'employé est résolu **à la date filtrée** via `WorkHourDayContext.contractNature` (alimenté par `EmployeeContractResolver::resolveNatureForEmployeeAndDate`), pas via `Employee.currentContractNature` (qui est résolu à aujourd'hui). Idem pour le **mode de suivi (TIME/PRESENCE), les heures hebdo et le libellé de contrat** sur la vue Jour : résolus à la date filtrée via `WorkHourDayContext` (`trackingMode`/`weeklyHours`/`contractType`/`contractName`, peuplés depuis `EmployeeContractResolver::resolveForEmployeeAndDate`), pas via `employee.contract` (résolu à aujourd'hui). Côté front, `resolveDayContract()` (`useHoursPage.ts`) pilote l'affichage et `handleSave` (heures vs présence par date). **Jours travaillés (CUSTOM)** : le libellé sous le nom affiche en suffixe les jours du planning `workDaysHours` au format court `LU,MA,ME,JE,VE` (ex. `BUREAU — CDI — LU,JE`). Exposé via `WorkHourDayContext.workDaysHours` (peuplé par `EmployeeContractResolver::resolveWorkDaysMinutesForEmployeeAndDate`, à la date filtrée), formaté front par `formatWorkedDaysShort` (`utils/contract.ts`) et accédé via `getRowWorkedDaysLabel` (`useHoursPage.ts`). Affiché **uniquement écran Heures** (`HoursDayView.vue`, mobile + desktop) ; naturellement limité aux CUSTOM (seuls eux ont `workDaysHours` → null sinon, rien affiché). Pas sur Heures Conducteurs (pas de planning workDaysHours). - **Exports heures annuelles** (par salarié `EmployeeYearlyHoursPrintProvider` + tous `EmployeeYearlyHoursBulkPrintProvider`, via `YearlyHoursExportBuilder`) : **tous les jours sous contrat sont affichés**, même vides ou non saisis (jusqu'à aujourd'hui). Seuls les jours hors contrat sont omis (`buildSegments` : un seul filtre `!$hasData && null === $contract`). Ne pas réintroduire de saut des jours de semaine vides. Samedis/dimanches grisés (`#c0c0c0`) dans les templates `employee-yearly-hours/print*.html.twig`. NB : l'export *tous employés* sur l'année peut dépasser `memory_limit=256M` (Dompdf) — limitation pré-existante, voir avec l'infra si besoin. - **Export heures vue Jour** (`WorkHourDayExportProvider`, endpoint `GET /work-hours/day-export?workDate=&siteIds=`, `ROLE_USER`) : bouton « Exporter » à droite du titre « Heures », **visible uniquement en vue Jour** (`v-if="(isAdmin || isSiteManager) && viewMode === 'day'"`, masqué en vue Semaine et pour `ROLE_SELF`). **Accessible aux admins ET aux chefs de site** : le périmètre est résolu côté backend via `EmployeeRepository::findScoped($user)` (admin → tous les sites, chef de site → ses sites uniquement, cf. `EmployeeScopeService`), donc un `siteIds` hors périmètre est ignoré ; le drawer front ne propose que les sites visibles (`sites` dérivé des employés scopés). PDF A4 portrait d'**une seule journée**, **regroupé par site**, colonnes de la vue Jour **sans « Valider »** (colonne **Total en gras**). Mêmes employés que l'écran : non-conducteurs, sous contrat à la date, sites cochés et dans le périmètre (lignes vides incluses). **Tri intra-site identique au calendrier** : `displayOrder` (ordre manuel), puis nom, puis prénom (cf. `compareEmployeesInSite` front). Calcul des cellules mutualisé via `YearlyHoursExportBuilder::buildDayRowsForEmployees` (Jour/Nuit/Total incluent crédit absence + crédit virtuel férié). Colonne **Statut = code** du type d'absence (`AbsenceType::getCode`, ex. `AT`) sur sa couleur de fond ; férié sans absence → nom du férié sur `#b3e5fc`. Chaque row porte `statut` (code), `statutLabel` (libellé, pour la légende) et `statutColor`. **Légende** sous le tableau (carré coloré contenant le code + libellé à droite), construite côté provider à partir des codes présents (hors férié, dédupliquée par code, triée). Gabarit `templates/work-hour-day-export/print.html.twig`. +- **Calendrier des jours validés (vue Jour)** (`WorkHourValidationStatusProvider`, ressource `WorkHourValidationStatus`, endpoint `GET /work-hours/validation-status?from=&to=[&driver=1]`, `ROLE_USER`) : en **vue Jour**, sur les **deux écrans** (Heures **et** Heures Conducteurs), le sélecteur de date est un `MalioDate` (layer `@malio/layer-ui >= 1.7.x` : prop `markedDates` + event `@month-change`) qui peint **en vert** (`markedDates` → `'success'`) les jours **entièrement validés**. **Définition** : un jour est vert ssi il porte ≥1 ligne `WorkHour` du scope ce jour-là **et** aucune n'est `isValid=false` — on se base sur la **seule** colonne `is_valid` (validation admin ; `isSiteValid` ignoré). Jour **sans aucune ligne** → neutre (jamais vert). **Périmètre complet** via `EmployeeRepository::findScoped` (admin = tous sites, chef de site = ses sites), **indépendant du filtre sites** de l'écran. **Scope conducteur inversé** par `?driver=1` : écran Heures → non-conducteurs (défaut), écran Heures Conducteurs → conducteurs (résolu par date via `EmployeeContractResolver::resolveIsDriverForEmployeeAndDate`, mémoïsé ; garde `if ($isDriver !== $driverOnly) continue`). Provider : une requête `WorkHourReadRepositoryInterface::findByDateRangeAndEmployees`, agrégation par jour (`total`/`pending`), plage bornée à 366 j. **Chargement à la volée par mois** (jamais préchargé) : `@month-change {month,year}` (à l'ouverture + nav) → fetch de la **grille visible** (lundi avant le 1er → dimanche après le dernier) → cache `validatedDaysByMonth` (`useHoursPage` / `useDriverHoursPage`, ce dernier passe `{ driver: true }` au service) → `markedDates` réactif. **Rafraîchissement** du mois en cache (`reloadValidationMonth`) après `toggleValidation`/`toggleValidationBulk`/`handleSave`/`refreshAfterAbsenceChange` (pas la validation site). La **vue Semaine** utilise un `MalioDateWeek` (sélecteur de semaine, v-model ISO week `YYYY-Www`, sans coloration). Le **stepper ‹ › du mode jour est remplacé** par le `MalioDate` (raccourcis Hier/Aujourd'hui/Demain conservés) ; `PeriodStepperPicker` reste un fallback de la vue Jour quand `showValidationCalendar` est absent (aucun appelant actuel). Activation par écran via la prop `showValidationCalendar` de `HoursToolbar` (les deux pages la passent à `true` + `markedDates` + `@month-change`). Alignement vertical de la ligne via `lg:items-center` (les champs Malio font `h-12` vs `h-10` des boutons). Doc complète : `doc/hours-validated-days.md`. - **Export Contingent heures de nuit** (`NightHoursContingentPrintProvider`, endpoint `GET /night-hours-contingent/print?year=YYYY`, `ROLE_USER`) : option « Contingent H.nuit » du drawer Export de la liste employés. PDF **A4 paysage**, lignes = employés **groupés par site** et triés `displayOrder`/nom/prénom (comme le day-export), colonnes = 12 mois civils, chacun avec 2 sous-colonnes **H.nuit** et **N.jours**. Heures de nuit = minutes dans la fenêtre **21h→6h** via le service partagé `App\Service\WorkHours\NightHoursCalculator` (source unique mutualisée avec `WorkHourWeeklySummaryProvider`, `YearlyHoursExportBuilder`, `RttRecoveryComputationService` et `SalaryRecapPrintProvider`). Conducteurs inclus via `WorkHour.nightHoursMinutes`. **N.jours** = nb de jours où minutes de nuit ≥ 240 (4h). **Aucun crédit** absence/férié. Agrégation : `App\Service\WorkHours\NightContingentExportBuilder`. Gabarit `templates/night-hours-contingent/print.html.twig`. - **Écran Calendrier** : un employé est affiché uniquement si au moins une de ses périodes de contrat (`employee.contractHistory`) intersecte le mois affiché (`[1er ; dernier jour]`). Filtre côté frontend dans `visibleEmployees` (`pages/calendar.vue`). **L'impression PDF des absences applique le même filtre** côté backend (`AbsencePrintProvider::hasContractInRange` sur la période `[from, to]`) : un salarié parti en avril n'apparaît pas sur une impression de mai. **Le récap salaire applique le même filtre** (`SalaryRecapPrintProvider::hasContractInRange` sur le mois imprimé) : un salarié sans contrat sur le mois (ex. parti en février) n'apparaît pas sur le récap de juin. - **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`. @@ -187,11 +188,15 @@ - Rows: `grid items-center gap-4 border-b border-primary-500 px-6 py-3 text-md font-bold text-primary-500 last:border-b-0 cursor-pointer hover:bg-tertiary-500` - Page wrapper for scroll: `h-full flex flex-col overflow-hidden`, table container: `min-h-0 overflow-auto rounded-md bg-white` -### Drawer buttons (AppDrawer) -- Edit mode: `grid grid-cols-2 gap-3` → Supprimer (red, left) + Modifier (primary, right) -- Create mode: centered `+ Ajouter` button, w-[200px] -- Exception: Users drawer has NO delete button -- All "Ajouter" buttons across the app use "+" prefix +### Drawers (MalioDrawer) +- **Tous les drawers utilisent `MalioDrawer`** (couche Malio, auto-importé). L'ancien composant custom `AppDrawer` a été supprimé — ne pas le réintroduire. +- **Titre via le slot `#header`** (MalioDrawer n'a PAS de prop `title`) : ``. +- `v-model` = ouverture ; bouton de fermeture + clic overlay/Échap gérés par MalioDrawer (`showClose`/`dismissable`/`closeOnEscape` défaut `true`). Largeur `max-w-md`. +- **Boutons d'action = `MalioButton`** (dans le slot par défaut ; plus de `