# 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`