docs : spec export Excel inventaire bovin
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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`
|
||||||
Reference in New Issue
Block a user