diff --git a/docs/superpowers/specs/2026-04-24-bovine-inventory-excel-export-design.md b/docs/superpowers/specs/2026-04-24-bovine-inventory-excel-export-design.md new file mode 100644 index 0000000..b83aa7a --- /dev/null +++ b/docs/superpowers/specs/2026-04-24-bovine-inventory-excel-export-design.md @@ -0,0 +1,123 @@ +# Export Excel de l'inventaire bovin — Design Spec + +Bouton sur la page `/inventory` qui télécharge un XLSX listant tous les bovins actuellement présents sur l'exploitation. + +## Contexte + +Le métier veut un Excel exportable depuis l'écran inventaire. Ferme n'a aujourd'hui aucun outil d'export Excel (uniquement PDF via dompdf). On choisit `phpoffice/phpspreadsheet` côté serveur, en suivant le même pattern que la génération PDF actuelle (endpoint qui streame le fichier, front qui télécharge via blob). + +Périmètre : tous les bovins actifs (`exitedAt IS NULL`), ordre `birthDate ASC`, ignore les filtres UI. Pas de modale de sélection (à voir si le métier en demande une plus tard). + +## Architecture + +### Backend + +**Dépendance** : `composer require phpoffice/phpspreadsheet` + +**Nouveau resource** : `src/ApiResource/BovineInventoryExport.php` +- `#[ApiResource]` avec une seule opération `Get` : + - `uriTemplate: '/bovines/inventory-export'` + - `output: false` + - `provider: BovineInventoryExportProvider::class` + - `security: "is_granted('ROLE_USER')"` (cohérent avec la page `/inventory`) + - OpenApi tag `Bovines` + +**Nouveau provider** : `src/State/Bovin/BovineInventoryExportProvider.php` +- Injecte `EntityManagerInterface` +- Query Doctrine : `WHERE exitedAt IS NULL ORDER BY birthDate ASC` +- Construit le `Spreadsheet` avec PhpSpreadsheet +- Retourne une `Symfony\Component\HttpFoundation\Response` avec : + - `Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet` + - `Content-Disposition: attachment; filename="inventaire_bovins_YYYY-MM-DD.xlsx"` + - Body = `IOFactory::createWriter($spreadsheet, 'Xlsx')->save('php://output')` capturé via `ob_*` + +### Frontend + +**Page** : `frontend/pages/inventory.vue` +- Nouveau bouton "Exporter Excel" à droite du titre, à côté de "Rafraîchir" +- Style : même que "Rafraîchir" (bg-primary-500, h-[50px], icône `mdi:file-excel-outline`) +- Visible pour tout user authentifié (pas de gate admin) +- Au clic : appelle `useApi().getBlob('bovines/inventory-export')`, crée un blob URL, déclenche un `` synthétique avec le filename retourné par le backend (lu depuis le header `Content-Disposition`) + +## Génération XLSX — détails + +**Fichier** : +- 1 seule feuille `Inventaire` +- Filename : `inventaire_bovins_YYYY-MM-DD.xlsx` (date du jour serveur) + +**En-têtes (ligne 1)** : +- 9 colonnes dans l'ordre : `N° National`, `N° Travail`, `Sexe`, `Né le`, `Age (mois)`, `Race`, `Bâtiment`, `Case`, `Entrée le` +- Style : gras, fond `#f1f5f9` (slate-100), bordure noire fine, alignement centré +- Auto-filter activé sur la plage des en-têtes (Excel ajoute les boutons de filtre natifs) +- Freeze pane : ligne 2 figée + +**Lignes de données (à partir de la ligne 2)** : +- Ordre `birthDate ASC` (plus vieux en haut, NULL à la fin via `NULLS LAST` natif Postgres) +- Largeurs de colonnes : + - N° National : 18 + - N° Travail : 12 + - Sexe : 10 + - Né le : 12 + - Age : 12 + - Race : 12 + - Bâtiment : 30 + - Case : 8 + - Entrée le : 12 + +**Mapping des valeurs** : +- Sexe : `M` → `Mâle`, `F` → `Femelle`, autre / null → vide +- Né le, Entrée le : format `JJ/MM/AAAA`, vide si null +- Age : entier (mois), vide si null +- Bâtiment, Case : valeurs nestées via `bovine.buildingCase.building.label` et `bovine.buildingCase.caseNumber`, vide si null + +**Couleurs des lignes** (basées sur `ageMonths`, mêmes seuils que l'UI) : +| Tranche | Hex | Tailwind | +|--------|-----|----------| +| 24+ mois | `#ddd6fe` | violet-200 | +| 22-24 mois | `#fecaca` | red-200 | +| 20-22 mois | `#fed7aa` | orange-200 | +| < 20 mois ou NULL | `#ffffff` | blanc | + +Le fond est appliqué sur toute la ligne (9 cellules). + +## Flux d'erreur + +- Exception PhpSpreadsheet (création buffer) → propage en 500 standard API Platform +- Pas d'utilisateur (token expiré) → 401 standard via la sécurité + +## Performance + +- 936 lignes × 9 colonnes : génération en mémoire < 1s, fichier < 100 KB +- Pas de pagination, pas de streaming row-by-row (overkill pour ce volume) + +## Tests + +Optionnel ce lot : test PHPUnit du provider qui vérifie que : +- Status 200 +- Content-Type XLSX +- Header `Content-Disposition: attachment; filename=...xlsx` +- Body non vide +Mock simple de l'`EntityManagerInterface` pour retourner 2 bovins fictifs. + +À faire en follow-up si on veut couvrir. + +## Verification manuelle + +1. `make composer-install` (après avoir ajouté la dep) +2. Recharger `/inventory` +3. Clic sur le bouton "Exporter Excel" +4. Vérifier le téléchargement : nom de fichier = `inventaire_bovins_2026-04-24.xlsx` +5. Ouvrir dans Excel/LibreOffice : + - 9 colonnes attendues + - En-tête figé en scrollant + - Auto-filter natif Excel + - Lignes colorées selon âge (violet/rouge/orange) + - Tri par date de naissance croissante + +## Critères d'acceptation + +- [ ] L'export contient 100 % des bovins actifs (count = `SELECT COUNT(*) FROM bovine WHERE exited_at IS NULL`) +- [ ] Le filename inclut la date du jour +- [ ] Les couleurs correspondent aux seuils d'âge +- [ ] L'ordre matche l'UI (`birthDate ASC`) +- [ ] Pas de régression sur les autres endpoints `/api/bovines`