From e796741dd87b48e9abe182fb0311eb7d325498e5 Mon Sep 17 00:00:00 2001 From: Matthieu Date: Tue, 24 Mar 2026 15:47:33 +0100 Subject: [PATCH] docs : add time entry export design spec (LST-41) Co-Authored-By: Claude Opus 4.6 (1M context) --- .../2026-03-24-time-entry-export-design.md | 144 ++++++++++++++++++ 1 file changed, 144 insertions(+) create mode 100644 docs/superpowers/specs/2026-03-24-time-entry-export-design.md diff --git a/docs/superpowers/specs/2026-03-24-time-entry-export-design.md b/docs/superpowers/specs/2026-03-24-time-entry-export-design.md new file mode 100644 index 0000000..64a248c --- /dev/null +++ b/docs/superpowers/specs/2026-03-24-time-entry-export-design.md @@ -0,0 +1,144 @@ +# Export temps suivi de temps (XLSX) + +**Ticket** : LST-41 +**Date** : 2026-03-24 +**Statut** : Approuvé + +## Contexte + +Les exports de suivi de temps sont nécessaires pour constituer des dossiers CIR (Crédit Impôt Recherche) et JEI (Jeune Entreprise Innovante). Ces dossiers exigent une ventilation détaillée du temps passé par collaborateur, par projet et par mois. + +## Décisions + +- **Format** : XLSX (via PhpSpreadsheet côté backend) +- **Déclenchement** : bouton "Exporter" sur la page time-tracking, reprenant les filtres en cours +- **Récap** : double tableau croisé (user × projet + user × mois) + +## Architecture + +``` +Frontend Backend +───────── ─────── +Bouton "Exporter" + → GET /api/time_entries/export → TimeEntryExportController + ?after=2026-01-01 → Validation params + authz + &before=2026-03-31 → TimeEntryRepository (query) + &user=5 → TimeEntryExportService (XLSX) + &project=5 → BinaryFileResponse (.xlsx) + &tags[]=2 +``` + +## Backend + +### Dépendance + +`phpoffice/phpspreadsheet` ajouté via Composer. + +### TimeEntryExportController + +- Fichier : `src/Controller/TimeEntryExportController.php` +- Route : `GET /api/time_entries/export` avec `priority: 1` +- Sécurité : `#[IsGranted('ROLE_USER')]` +- **Autorisation** : si l'utilisateur n'a pas `ROLE_ADMIN`, le filtre `user` est forcé à l'utilisateur courant (ignore toute valeur fournie). Seuls les admins peuvent exporter les données d'autres utilisateurs ou de tous les utilisateurs. +- Paramètres query (IDs numériques, pas d'IRIs — c'est un controller custom, pas API Platform) : + - `after` (obligatoire) — date YYYY-MM-DD + - `before` (obligatoire) — date YYYY-MM-DD + - `user` (optionnel) — ID numérique (ex: `5`) + - `project` (optionnel) — ID numérique (ex: `5`) + - `tags[]` (optionnel) — tableau d'IDs numériques (ex: `tags[]=2&tags[]=3`) +- **Validation** : + - `after` et `before` obligatoires, sinon 400 Bad Request + - Plage maximale : 12 mois, sinon 400 Bad Request + - Si aucune entrée trouvée : retourne un XLSX avec en-têtes uniquement (pas d'erreur) +- Construit une query Doctrine avec ces filtres +- Appelle `TimeEntryExportService::generate()` +- Retourne `BinaryFileResponse` avec header `Content-Disposition: attachment; filename="export-temps-YYYY-MM-DD_YYYY-MM-DD.xlsx"` + +### TimeEntryExportService + +- Fichier : `src/Service/TimeEntryExportService.php` +- Méthode : `generate(array $timeEntries, \DateTimeImmutable $from, \DateTimeImmutable $to): string` (retourne le chemin du fichier temp) + +#### Feuille 1 — "Détail" + +Toutes les entrées triées par date croissante. + +| Colonne | Source | Format | +|---------|--------|--------| +| Date | `startedAt` | YYYY-MM-DD | +| Utilisateur | `user.username` | texte | +| Projet | `project.name` | texte (vide si null) | +| Tâche | `task` | "{code}-{number} - {title}" (vide si null) | +| Titre | `title` | texte | +| Tags | `tags` | labels séparés par ", " | +| Début | `startedAt` | HH:mm | +| Fin | `stoppedAt` | HH:mm (vide si null) | +| Durée (h) | calculée | nombre décimal (ex: 3.50) | +| Description | `description` | texte | + +- En-têtes en gras +- Colonnes auto-dimensionnées +- Ligne de total en bas (somme de la colonne Durée) + +#### Feuille 2 — "Récap par projet" + +Tableau croisé dynamique : +- Lignes = utilisateurs (triés alphabétiquement) +- Colonnes = projets (triés alphabétiquement) +- Cellules = total heures (décimal) +- Dernière colonne = total par utilisateur +- Dernière ligne = total par projet + +#### Feuille 3 — "Récap par mois" + +Tableau croisé dynamique : +- Lignes = utilisateurs (triés alphabétiquement) +- Colonnes = mois de la période (format "Mars 2026") +- Cellules = total heures (décimal) +- Dernière colonne = total par utilisateur +- Dernière ligne = total par mois + +## Frontend + +### Page time-tracking + +- Ajout d'un bouton "Exporter" dans la barre d'actions (à côté des filtres existants) +- Icône de téléchargement + label "Exporter" +- Au clic : construit l'URL `/api/time_entries/export` avec les filtres actuels (période affichée, user sélectionné, projet sélectionné, tags sélectionnés) et déclenche le téléchargement + +### Service time-entries.ts + +Ajout d'une méthode : +```typescript +function getExportUrl(params: { + after: string // YYYY-MM-DD + before: string // YYYY-MM-DD + user?: number // ID numérique + project?: number // ID numérique + tags?: number[] // tableau d'IDs +}): string +``` + +Construit l'URL complète avec query params. Le téléchargement est déclenché via un élément `` temporaire avec attribut `download` (le cookie JWT est envoyé automatiquement sur une requête same-origin). En cas d'erreur, un toast est affiché. + +### i18n + +- `timeEntries.export` → "Exporter" (fr) + +## Sécurité + +- Accessible à `ROLE_USER` (même niveau que la consultation des time entries) +- **Non-admin : export limité à ses propres données** (filtre `user` forcé côté serveur) +- Le fichier XLSX est généré dans un fichier temporaire et supprimé après envoi +- Les filtres utilisent des IDs numériques (controller custom, pas d'IRI) + +## Langue + +Le contenu du XLSX est toujours en français (noms de feuilles, en-têtes de colonnes, noms de mois). C'est volontaire car les documents CIR/JEI sont des dossiers destinés à l'administration française. + +## Hors scope + +- Export PDF +- Export CSV +- Stockage des exports générés +- Planification d'exports automatiques