From bd93c52197d134687dbbcfdb2502cbe0993b9a31 Mon Sep 17 00:00:00 2001
From: tristan
Date: Fri, 24 Apr 2026 16:54:38 +0200
Subject: [PATCH] feat : migrate filter/form UI to @malio/layer-ui + fix
hours/calendar contract scoping
- Add @malio/layer-ui as Nuxt layer (extends in nuxt.config.ts)
- Migrate site/employee/contract filters to MalioSelectCheckbox / MalioInputText / MalioSelect on employees, calendar and hours screens
- Migrate absence drawer selects + submit button to Malio (MalioSelect + MalioButton)
- Migrate calendar "Ajouter une absence" / "Imprimer" actions to MalioButton
- Drop now-unused EmployeeNameFilterInput and SiteFilterSelector components
- Hours day view: resolve contract nature at selected date (WorkHourDayContext.contractNature) instead of employee.currentContractNature (today-based); fixes interim contracts showing as CDI when mission ended
- Calendar: hide employees whose contract periods do not intersect the displayed month
- Layout: scrollbar-gutter:stable on to avoid horizontal shift when dropdowns open
- Update functional-rules.md, in-app documentation, CLAUDE.md to match
Co-Authored-By: Claude Opus 4.7 (1M context)
---
CLAUDE.md | 3 +
doc/functional-rules.md | 7 +
frontend/.npmrc | 1 +
frontend/components/AbsenceFormDrawer.vue | 130 ++++-----
frontend/components/AppDrawer.vue | 2 +-
.../components/EmployeeNameFilterInput.vue | 26 --
frontend/components/SiteFilterSelector.vue | 69 -----
.../driver-hours/DriverHoursDayView.vue | 3 +-
frontend/components/hours/HoursDayView.vue | 5 +-
frontend/components/hours/HoursToolbar.vue | 38 ++-
frontend/composables/useDriverHoursPage.ts | 5 +
frontend/composables/useHoursPage.ts | 5 +
frontend/data/documentation-content.ts | 2 +
frontend/layouts/default.vue | 2 +-
frontend/nuxt.config.ts | 1 +
frontend/package-lock.json | 260 +++++++-----------
frontend/package.json | 1 +
frontend/pages/calendar.vue | 57 ++--
frontend/pages/driver-hours.vue | 2 +
frontend/pages/employees/index.vue | 37 ++-
frontend/pages/hours.vue | 2 +
frontend/services/dto/work-hour.ts | 1 +
src/Dto/WorkHours/DayContextRow.php | 5 +-
src/State/WorkHourDayContextProvider.php | 8 +-
.../State/WorkHourDayContextProviderTest.php | 5 +
25 files changed, 309 insertions(+), 368 deletions(-)
create mode 100644 frontend/.npmrc
delete mode 100644 frontend/components/EmployeeNameFilterInput.vue
delete mode 100644 frontend/components/SiteFilterSelector.vue
diff --git a/CLAUDE.md b/CLAUDE.md
index a9aa367..8daf08f 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -15,6 +15,7 @@
## Stack
- Backend: Symfony + API Platform + Doctrine ORM
- Frontend: Nuxt 4 + Vue 3 + TypeScript + Tailwind CSS
+- UI library: `@malio/layer-ui` (Nuxt layer, `extends: ['@malio/layer-ui']` dans `nuxt.config.ts`). Composants auto-importés avec préfixe `Malio*` (ex. `MalioSelectCheckbox`, `MalioInputText`…). Doc d'usage dans `node_modules/@malio/layer-ui/COMPONENTS.md`. Tokens Tailwind `m-*` (primary/muted/danger/success/…) et variables CSS `--m-*` fournies par la couche.
## Project Structure
- `src/` — Symfony domain, API resources, state providers/processors, services
@@ -32,6 +33,8 @@
- Contract nature (per period): CDI, CDD, INTERIM
- **Agence d'intérim** (`InterimAgency` entity, table `interim_agencies`): optionnelle sur `EmployeeContractPeriod` quand nature = INTERIM. Pas de CRUD UI — gérée en BDD. API lecture seule `GET /interim_agencies`. Affichée "Intérim (NomAgence)" sur la liste employés et l'historique contrat.
- Employee contract history: `employee_contract_periods`, resolved by `EmployeeContractResolver`
+- **É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).
+- **É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`).
- **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)
diff --git a/doc/functional-rules.md b/doc/functional-rules.md
index e732e9f..49db4b4 100644
--- a/doc/functional-rules.md
+++ b/doc/functional-rules.md
@@ -58,6 +58,9 @@ Documents complementaires:
- mise à jour uniquement quand un employé (`ROLE_SELF`) modifie ses propres heures
- non mise à jour lors de modifications admin ou chef de site
- affichée sous le nom de l'employé (visible admin uniquement)
+- Libellé nature de contrat (CDI/CDD/Intérim) affiché sous le nom:
+ - résolu à la date filtrée (période de contrat couvrant ce jour), pas à aujourd'hui
+ - masqué si aucun contrat à cette date (cas rarissime en vue jour puisque l'employé est alors déjà filtré)
## 4) Absences
@@ -71,6 +74,10 @@ Documents complementaires:
- Calendrier congés: fond coloré selon la couleur du type d'absence (`AbsenceType.color`)
- demi-journée: dégradé diagonal
- journée complète: fond plein
+- Visibilité des employés dans le Calendrier:
+ - un employé est affiché si au moins une de ses périodes de contrat intersecte le mois affiché
+ - un employé dont toutes les périodes se terminent avant le 1er du mois (ou commencent après la fin du mois) est masqué
+ - même logique que l'écran Heures : « pas de contrat sur la période → masqué »
### Effet absence sur les heures
diff --git a/frontend/.npmrc b/frontend/.npmrc
new file mode 100644
index 0000000..303a6d3
--- /dev/null
+++ b/frontend/.npmrc
@@ -0,0 +1 @@
+@malio:registry=https://gitea.malio.fr/api/packages/MALIO-DEV/npm/
diff --git a/frontend/components/AbsenceFormDrawer.vue b/frontend/components/AbsenceFormDrawer.vue
index ec25735..d663372 100644
--- a/frontend/components/AbsenceFormDrawer.vue
+++ b/frontend/components/AbsenceFormDrawer.vue
@@ -1,44 +1,26 @@
@@ -189,20 +166,23 @@ const submitButtonClass = computed(() => {
return ''
})
-const baseSelectClass =
- 'mt-2 w-full rounded-md border bg-white px-3 py-2 text-md text-neutral-900'
-const employeeFieldClass = computed(() => {
- if (showEmployeeError.value) {
- return `${baseSelectClass} border-red-500`
- }
- return `${baseSelectClass} border-neutral-300`
-})
-const typeFieldClass = computed(() => {
- if (showTypeError.value) {
- return `${baseSelectClass} border-red-500`
- }
- return `${baseSelectClass} border-neutral-300`
-})
+const employeeOptions = computed(() =>
+ props.employees.map((e) => ({ label: `${e.firstName} ${e.lastName}`, value: e.id }))
+)
+const typeOptions = computed(() =>
+ props.absenceTypes.map((t) => ({ label: `${t.label} (${t.code})`, value: t.id }))
+)
+const halfDayOptions = HALF_DAYS.map((h) => ({ label: h.label, value: h.value }))
+
+const dateInputBaseClass =
+ 'h-10 w-full rounded-md border px-3 text-md text-black outline-none focus:border-2 focus:border-m-primary'
+
+const onEmployeeChange = (value: string | number | null) => {
+ absenceForm.value.employeeId = value === null ? '' : Number(value)
+}
+const onTypeChange = (value: string | number | null) => {
+ absenceForm.value.typeId = value === null ? '' : Number(value)
+}
watch(
() => props.modelValue,
diff --git a/frontend/components/AppDrawer.vue b/frontend/components/AppDrawer.vue
index 553721e..ce60222 100644
--- a/frontend/components/AppDrawer.vue
+++ b/frontend/components/AppDrawer.vue
@@ -17,7 +17,7 @@
-
diff --git a/frontend/components/EmployeeNameFilterInput.vue b/frontend/components/EmployeeNameFilterInput.vue
deleted file mode 100644
index f77a596..0000000
--- a/frontend/components/EmployeeNameFilterInput.vue
+++ /dev/null
@@ -1,26 +0,0 @@
-
-
-
-
-
-
-
-
diff --git a/frontend/components/SiteFilterSelector.vue b/frontend/components/SiteFilterSelector.vue
deleted file mode 100644
index b0ea53d..0000000
--- a/frontend/components/SiteFilterSelector.vue
+++ /dev/null
@@ -1,69 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/frontend/components/driver-hours/DriverHoursDayView.vue b/frontend/components/driver-hours/DriverHoursDayView.vue
index 4aebc0a..a6e9afc 100644
--- a/frontend/components/driver-hours/DriverHoursDayView.vue
+++ b/frontend/components/driver-hours/DriverHoursDayView.vue
@@ -43,7 +43,7 @@
- {{ employee.site?.name ?? 'Sans site' }} — {{ contractNatureLabel(employee.currentContractNature) }}
+ {{ employee.site?.name ?? 'Sans site' }} — {{ contractNatureLabel(getRowContractNature(employee.id) ?? undefined) }}
{ dayMinutes: number; nightMinutes: number; workshopMinutes: number; totalMinutes: number; virtualHolidayMinutes: number }
getRowAbsenceLabel: (employeeId: number) => string
getRowAbsenceStyle: (employeeId: number) => { backgroundColor: string } | undefined
+ getRowContractNature: (employeeId: number) => 'CDI' | 'CDD' | 'INTERIM' | null
getRowUpdatedAt: (employeeId: number) => string
onAbsenceClick: (employeeId: number) => void
formatMinutes: (minutes: number) => string
diff --git a/frontend/components/hours/HoursDayView.vue b/frontend/components/hours/HoursDayView.vue
index 4230886..21e9c2b 100644
--- a/frontend/components/hours/HoursDayView.vue
+++ b/frontend/components/hours/HoursDayView.vue
@@ -14,7 +14,7 @@
({{ contractLabel(employee) }})
- {{ employee.site?.name ?? 'Sans site' }} — {{ contractNatureLabel(employee.currentContractNature) }}
+ {{ employee.site?.name ?? 'Sans site' }} — {{ contractNatureLabel(getRowContractNature(employee.id) ?? undefined) }}
@@ -212,7 +212,7 @@
- {{ employee.site?.name ?? 'Sans site' }} — {{ contractNatureLabel(employee.currentContractNature) }}
+ {{ employee.site?.name ?? 'Sans site' }} — {{ contractNatureLabel(getRowContractNature(employee.id) ?? undefined) }}
{ backgroundColor: string } | undefined
hasRowFormation: (employeeId: number) => boolean
getRowFormationLabel: (employeeId: number) => string
+ getRowContractNature: (employeeId: number) => 'CDI' | 'CDD' | 'INTERIM' | null
getRowUpdatedAt: (employeeId: number) => string
getPresenceDayValue: (employeeId: number) => string
onAbsenceClick: (employeeId: number) => void
diff --git a/frontend/components/hours/HoursToolbar.vue b/frontend/components/hours/HoursToolbar.vue
index f96e236..3a56390 100644
--- a/frontend/components/hours/HoursToolbar.vue
+++ b/frontend/components/hours/HoursToolbar.vue
@@ -1,17 +1,32 @@