docs : spec export Excel inventaire bovin

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-24 11:23:26 +02:00
parent 097eb39cb0
commit bde59bf9ee

View File

@@ -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 `<a download>` 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`