diff --git a/CLAUDE.md b/CLAUDE.md index 5fc905f..48828dd 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -35,7 +35,7 @@ - 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). 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). - **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_ADMIN`) : bouton « Exporter » à droite du titre « Heures », **visible uniquement en vue Jour** (`v-if="isAdmin && viewMode === 'day'"`, masqué en vue Semaine). 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 (lignes vides incluses). 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`. +- **Export heures vue Jour** (`WorkHourDayExportProvider`, endpoint `GET /work-hours/day-export?workDate=&siteIds=`, `ROLE_ADMIN`) : bouton « Exporter » à droite du titre « Heures », **visible uniquement en vue Jour** (`v-if="isAdmin && viewMode === 'day'"`, masqué en vue Semaine). 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 (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`. - **É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`. - Absences: stored per day (auto-split), AM/PM/full day, clear corresponding hour slots diff --git a/frontend/components/hours/HoursDayExportDrawer.vue b/frontend/components/hours/HoursDayExportDrawer.vue index aaa1501..dca3799 100644 --- a/frontend/components/hours/HoursDayExportDrawer.vue +++ b/frontend/components/hours/HoursDayExportDrawer.vue @@ -23,6 +23,7 @@ groupClass="w-full mt-2" label="Sites" display-select-all + display-tag /> @@ -49,7 +50,6 @@ const props = defineProps<{ modelValue: boolean sites: Site[] initialDate: string - initialSiteIds: number[] isLoading?: boolean }>() @@ -64,7 +64,7 @@ const drawerOpen = computed({ }) const selectedDate = ref(props.initialDate) -const selectedSites = ref([...props.initialSiteIds]) +const selectedSites = ref([]) const siteOptions = computed(() => props.sites.map((site) => ({ label: site.name, value: site.id })) @@ -80,7 +80,7 @@ watch( (isOpen) => { if (isOpen) { selectedDate.value = props.initialDate - selectedSites.value = [...props.initialSiteIds] + selectedSites.value = [] } } ) diff --git a/frontend/pages/hours.vue b/frontend/pages/hours.vue index b36de7c..5b59120 100644 --- a/frontend/pages/hours.vue +++ b/frontend/pages/hours.vue @@ -16,7 +16,6 @@ v-model="isExportDrawerOpen" :sites="sites" :initial-date="selectedDate" - :initial-site-ids="selectedSiteIds" :is-loading="isExporting" @submit="handleExport" /> diff --git a/src/State/WorkHourDayExportProvider.php b/src/State/WorkHourDayExportProvider.php index e9b4aa9..90731c3 100644 --- a/src/State/WorkHourDayExportProvider.php +++ b/src/State/WorkHourDayExportProvider.php @@ -78,7 +78,11 @@ class WorkHourDayExportProvider implements ProviderInterface $legend = []; foreach ($siteMeta as $siteId => $meta) { $siteEmployees = $bySite[$siteId]; - usort($siteEmployees, static fn ($a, $b) => ($a->getLastName() ?? '') <=> ($b->getLastName() ?? '')); + // Même tri que le calendrier : ordre manuel (displayOrder) puis nom, puis prénom. + usort($siteEmployees, static function ($a, $b): int { + return [$a->getDisplayOrder(), $a->getLastName(), $a->getFirstName()] + <=> [$b->getDisplayOrder(), $b->getLastName(), $b->getFirstName()]; + }); $rows = $this->exportBuilder->buildDayRowsForEmployees($siteEmployees, $date); if ([] === $rows) {