Files
SIRH/docs/superpowers/specs/2026-06-08-hours-day-export-design.md
T
tristan 6f9d19bda3
Auto Tag Develop / tag (push) Successful in 7s
feat(heures) : export PDF des heures (vue jour) par sites (#24)
## 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>
2026-06-09 12:59:04 +00:00

7.3 KiB

Export PDF des heures — vue Jour (par sites)

Date : 2026-06-08 Branche : feature/SIRH-35-export-des-heures-employe

Objectif

Ajouter un bouton Exporter sur l'écran « Heures », réservé aux administrateurs, qui produit un PDF d'une journée reprenant les colonnes de la vue Jour (sans la colonne de validation), pour les employés des sites sélectionnés, regroupés par site.

Décisions validées

Sujet Choix
Format PDF (Twig → Dompdf)
Période Un seul jour
Orientation A4 portrait, mise en page compacte (objectif : tenir sur une page ; débordement multipage seulement si le nombre d'employés l'impose)
Regroupement Une section par site
Accès ROLE_ADMIN uniquement

Comportement frontend

Bouton

  • Dans frontend/pages/hours.vue, à droite du titre « Heures » (le conteneur titre est déjà flex flex-wrap items-center justify-between).
  • Visible uniquement si isAdmin (déjà exposé par useHoursPage).
  • Style cohérent avec les autres boutons d'action de l'app ; libellé « Exporter » (préfixe non requis ici, ce n'est pas un « + Ajouter »).

Drawer HoursDayExportDrawer.vue

Nouveau composant utilisant AppDrawer (mode create — bouton centré).

Champs :

  1. Date — champ date (input date), prérempli avec selectedDate de l'écran.
  2. SitesMalioSelectCheckbox avec display-select-all, mêmes options que la toolbar (sites du composable), présélectionné sur selectedSiteIds courants.

Bouton « Exporter » : désactivé si aucune date ou aucun site sélectionné.

Déclenchement

  • À la validation : usePdfPrinter().printPdf(url) avec GET /work-hours/day-export?workDate=YYYY-MM-DD&siteIds=1,2,3.
  • Le téléchargement réutilise le pattern blob existant (usePdfPrinter).
  • État isLoading sur le bouton pendant la génération.

Câblage dans hours.vue / useHoursPage.ts

  • hours.vue gère l'état d'ouverture du drawer et passe sites, selectedSiteIds, selectedDate, isAdmin.
  • L'appel d'export peut vivre dans un petit handler local (hours.vue) ou dans le composable ; au choix de l'implémentation, en gardant useHoursPage comme source des données affichées.

Portée des données (identique à l'écran Jour)

  • Employés non-conducteurs (isDriver !== true).
  • Sous contrat à la date choisie.
  • Appartenant aux sites cochés.
  • Tous les employés sous contrat sont affichés, même sans saisie (lignes vides) — cohérent avec la règle des exports heures annuelles.

Colonnes du PDF

Mêmes colonnes que la vue Jour, sans la colonne Valider :

Nom · Statut · Début matin · Fin matin · Début après-midi · Fin après-midi · Début soir · Fin soir · Jour · Nuit · Total

  • Statut : libellé d'absence (ou formation, ou nom du férié) si présent, sinon vide.
  • Heures (Début/Fin matin/après-midi/soir) : valeurs WorkHour brutes (HH:MM), vides si non saisies.
  • Jour / Nuit / Total : calculés comme à l'écran — minutes jour vs nuit, total incluant le crédit d'absence (countAsWorkedHours) et le crédit virtuel férié (HolidayVirtualHoursResolver).
  • Week-ends / fériés : lignes grisées/colorées comme dans les templates existants.

Architecture backend

ApiResource WorkHourDayExport

src/ApiResource/WorkHourDayExport.php — calqué sur EmployeeYearlyHoursBulkPrint :

new Get(
    uriTemplate: '/work-hours/day-export',
    provider: WorkHourDayExportProvider::class,
    parameters: [
        new QueryParameter(key: 'workDate', required: true),
        new QueryParameter(key: 'siteIds', required: true),
    ],
    security: "is_granted('ROLE_ADMIN')"
)

Provider WorkHourDayExportProvider

src/State/WorkHourDayExportProvider.php :

  1. Lire/valider workDate (Y-m-d) et siteIds (CSV d'entiers).
  2. Charger les employés (EmployeeRepository::findAll() — feature admin-only), filtrer : non-drivers, site ∈ siteIds.
  3. Pour chaque site (ordre displayOrder), trier les employés par nom.
  4. Filtrer les employés sous contrat à la date (le builder ignore déjà les jours hors contrat — un employé sans contrat ce jour produit une ligne vide à exclure).
  5. Construire les lignes via YearlyHoursExportBuilder (méthode dédiée, voir ci-dessous).
  6. Rendre le Twig → Dompdf (A4, portrait), renvoyer Response binaire avec Content-Disposition: attachment; filename="heures_jour_YYYY-MM-DD.pdf".

Réutilisation YearlyHoursExportBuilder

Ajouter une méthode publique :

/**
 * @param list<Employee> $employees
 * @return list<array{employeeId:int, employeeName:string, statut:?string,
 *   morningFrom:string, morningTo:string, afternoonFrom:string, afternoonTo:string,
 *   eveningFrom:string, eveningTo:string, dayHours:string, nightHours:string,
 *   total:string, isWeekend:bool, hasContract:bool}>
 */
public function buildDayRowsForEmployees(array $employees, DateTimeImmutable $date): array
  • Réutilise les helpers privés existants (computeMetrics, résolution d'absences, HolidayVirtualHoursResolver, EmployeeContractResolver, fériés) — source unique de vérité pour le calcul des cellules d'une journée.
  • Émet en plus dayHours / nightHours (issus de WorkMetrics.dayMinutes / nightMinutes) que l'export annuel n'affichait pas par ligne en mode TIME.
  • Les employés sans contrat ce jour sont exclus (pas de ligne).
  • Le statut agrège absence / formation / libellé férié (réutilise la logique de résolution d'absence/formation déjà présente dans le contexte jour si nécessaire).

Note : la vue Jour mélange potentiellement modes TIME et PRESENCE selon le contrat à la date. Pour l'export, on suit le mode résolu à la date (comme l'écran). En mode PRESENCE, les cellules horaires restent vides et Total exprime les demi-journées, identique à l'affichage écran.

Template templates/work-hour-day-export/print.html.twig

  • A4 portrait, marges fines, police ~9px (réf. employee-yearly-hours/print.html.twig).
  • Barre de titre : « Heures — {date} » + date d'export en haut à droite.
  • Une <h2> par site, suivie d'un tableau avec les 11 colonnes ci-dessus.
  • Week-ends / fériés grisés (#c0c0c0 / #b3e5fc) comme les templates existants.
  • table-layout: auto, largeurs compactes pour viser une page.

Limites connues

  • Un grand nombre d'employés (beaucoup de sites cochés) peut déborder sur plusieurs pages — on vise une page sans la garantir.
  • Pas de risque mémoire particulier (un seul jour, volume très inférieur à l'export annuel tous employés).

Documentation à mettre à jour (règles CLAUDE.md)

  1. doc/ : nouvelle section (ou ajout à un doc heures existant) décrivant l'export jour.
  2. frontend/data/documentation-content.ts : entrée niveau admin dans la section Heures.
  3. CLAUDE.md : note sous la section heures/exports (provider, builder réutilisé, colonnes, scope identique écran, portrait).

Tests

  • Test unitaire YearlyHoursExportBuilder::buildDayRowsForEmployees : un employé TIME avec saisie (vérifier day/night/total), un employé sans contrat (exclu), un jour férié (crédit virtuel), une absence countAsWorkedHours.
  • (Optionnel) test provider : validation des paramètres workDate / siteIds.