diff --git a/docs/superpowers/specs/2026-04-29-bovine-entry-exit-design.md b/docs/superpowers/specs/2026-04-29-bovine-entry-exit-design.md new file mode 100644 index 0000000..eeb8ec5 --- /dev/null +++ b/docs/superpowers/specs/2026-04-29-bovine-entry-exit-design.md @@ -0,0 +1,199 @@ +# Entrée / Sortie des bovins — Design + +## Contexte + +Aujourd'hui, l'application gère les **réceptions** (arrivée d'un camion) qui déclarent un nombre de bovins par race (ex : 5 charolais + 3 limousine + 2 autres). Une fois la réception terminée, ces déclarations sont des indicateurs imprécis et il manque l'étape de saisie individuelle des bovins (numéro national, poids, prix…). + +L'objectif est d'introduire un **workflow d'entrée** qui transforme une réception bovins finie en saisies individuelles enrichies via EDNOTIF, et de poser les fondations pour un futur workflow de sortie symétrique. + +Pour ce lot, **les sorties sont hors scope** mais l'écran liste prévoit déjà leur emplacement. + +## Décisions structurantes + +| Décision | Choix | +| --- | --- | +| Distinction "en attente" vs "terminée" | Flag explicite `entryCompleted: bool` sur `Reception` | +| Lien Bovine → Reception | FK 1-N, `Bovine.reception` ManyToOne **nullable** | +| Rendu de l'écran de saisie | UN formulaire (2 lignes) + tableau récap dessous | +| Bâtiment + Case | Choisis **par bovin** dans le formulaire | +| Persistance | Save individuel à chaque "Ajouter" (POST /bovines) | +| Enrichissement EDNOTIF | Au backend via le `BovineProcessor` existant (pas de lookup live) | + +## Modèle de données + +### `Reception` — modification + +Nouveau champ : +- `entryCompleted: bool`, default `false`, non nullable. +- Pertinent uniquement quand `receptionType.code === 'BOVINS'`. Pour les autres types, reste `false` et ignoré côté UI. +- Inclus dans les groupes `reception:read` et `reception:write`. + +Migration : `ALTER TABLE reception ADD COLUMN entry_completed BOOLEAN NOT NULL DEFAULT false`. + +Ajout d'un `BooleanFilter` sur `entryCompleted` dans `#[ApiFilter]`. + +### `Bovine` — modification + +Nouveau champ : +- `reception: Reception` (ManyToOne, **nullable**). +- Inclus dans `bovine:read` et `bovine:write`. + +Migration : `ALTER TABLE bovine ADD COLUMN reception_id INTEGER NULL` + index + FK contrainte. Bovins existants restent à `NULL` — aucune migration de données. + +Ajout d'un `SearchFilter` exact sur `reception` dans `#[ApiFilter]` pour permettre `GET /bovines?reception={id}`. + +### `Reception` — relation inverse pour le compteur + +Pour permettre l'affichage du compteur "bovins saisis" dans la liste sans N+1 : + +- Ajouter `bovines: Collection` côté `Reception` (OneToMany inverse, `mappedBy: 'reception'`, fetch lazy). +- Exposer un getter calculé `getRegisteredBovineCount(): int` dans le groupe `reception:read`. +- L'implémentation côté provider/list peut utiliser un `addSelect('COUNT(b.id) AS bovineCount')` via un `QueryExtension` API Platform si le N+1 devient un problème (à mesurer). + +### Aucune autre entité + +Pas de table de jointure (un bovin entre une seule fois via une réception unique). Pas de nouvelle entité `Entry` (la `Reception` joue ce rôle). Pas d'entité `Exit` pour ce lot — la symétrie sera traitée plus tard. + +## Endpoints API + +Tous les endpoints réutilisent les ressources existantes ; **aucun endpoint custom n'est créé**. + +### Liste des entrées en attente + +`GET /api/receptions?receptionType.code=BOVINS&isValid=true&entryCompleted=false` + +### Validation finale d'une entrée + +`PATCH /api/receptions/{id}` avec `{ entryCompleted: true }`. + +### Création d'un bovin lié + +`POST /api/bovines` (Content-Type `application/ld+json`) avec : +```json +{ + "nationalNumber": "FR1234567890", + "receivedWeight": 368, + "pricePerKg": 5.7, + "arrivalDate": "2026-04-29", + "supplier": "/api/suppliers/12", + "reception": "/api/receptions/45", + "buildingCase": "/api/building_cases/8" +} +``` + +Le `BovineProcessor` enrichit automatiquement (workNumber, birthDate, race auto-créée via `BovineType`). + +**Nettoyage en passant** : le `BovineProcessor` actuel appelle `setBreedCode()` qui n'existe plus (héritage avant la migration vers `BovineType` FK). À corriger pour qu'il fasse `setBovineType()` avec auto-create d'un `BovineType` si la race retournée par EDNOTIF n'existe pas en base. + +### Suppression d'un bovin + +`DELETE /api/bovines/{id}` — sécurité actuelle `ROLE_ADMIN` à abaisser à `ROLE_USER` pour permettre la correction immédiate depuis le tableau. + +## Front-end + +### Home (`pages/index.vue`) + +- Card "CASES" → renommée "ENTRÉE / SORTIE" (multi-ligne `Entrée
Sortie`). +- Lien : `/entry-exit`. +- Icône : `mdi:swap-horizontal-bold` (à finaliser à l'implémentation). + +### Page liste — `pages/entry-exit/index.vue` + +Deux sections empilées : + +**Entrées en attente** +- Composant : `UiDataTable`. +- Filtres serveur : `receptionType.code=BOVINS`, `isValid=true`, `entryCompleted=false`. +- Colonnes : + - Date réception + - Fournisseur (`supplier.name`) + - Total déclaré (calculé côté front : `sum(bovines_types.quantity) + parseInt(bovineDetail ?? '0')`) + - Bovins saisis (depuis `getRegisteredBovineCount` exposé sur Reception) + - Action (rangée cliquable) +- Click row → `/entry-exit/entry/{receptionId}`. + +**Sorties en attente** +- Tableau placeholder vide avec message "À venir". + +### Écran de saisie — `pages/entry-exit/entry/[id].vue` + +**Header** +- Titre : "Entrée bovins #N-BR-XXXX — Fournisseur YYY" +- Sous-titre : "Bovins déclarés : 8 · Bovins saisis : 3" +- Icône retour à gauche. + +**Formulaire (2 lignes)** + +Ligne 1 : Numéro national · Poids à l'arrivée · Date d'arrivée · Vendeur (Supplier select) +Ligne 2 : Prix au kilo · Bâtiment (Building select) · Case (BuildingCase select dépendant du bâtiment) · Bouton **Ajouter** + +**Pré-remplissage** (au chargement et après chaque add) : +- Date d'arrivée = `reception.receptionDate` (date seule, modifiable) +- Vendeur = `reception.supplier` (modifiable) +- Bâtiment = premier de `reception.buildings` si dispo, sinon vide +- Case = vide (à choisir explicitement) +- Numéro national, poids, prix : vides + +**Comportement bouton "Ajouter"** +- Disabled si form invalide (n° national vide, poids ≤ 0, prix ≤ 0, building/case manquants). +- Click → `POST /api/bovines` avec `application/ld+json`. +- Succès → reload du tableau, reset form (en gardant les pré-remplissages), focus sur Numéro national. +- Erreur 409 (doublon n° national) → toast "Ce bovin existe déjà". +- Erreur EDNOTIF → bovin créé sans enrichissement (race/naissance vides), toast warning. + +**Tableau récap (dessous)** + +Colonnes : N° national · N° travail · Race · Sexe · Date naissance · Poids arrivée · Date arrivée · Prix/kg · Prix total · Bâtiment · Case · Action (icône poubelle). + +Source : `GET /api/bovines?reception={id}` au mount + après chaque add/delete. + +Suppression : `DELETE /api/bovines/{id}` avec `window.confirm`. + +**Footer** +- Bouton **Valider l'entrée** (à droite). +- Si `bovins saisis < bovins déclarés` → `window.confirm("Vous n'avez saisi que X/Y bovins. Confirmer la fermeture ?")`. +- Disabled si 0 bovin saisi. +- Click → `PATCH /api/receptions/{id}` avec `{ entryCompleted: true }` → toast succès → redirection `/entry-exit`. + +## Sécurité (rôles) + +| Action | Rôle requis | +| --- | --- | +| Voir la page entrée/sortie | `ROLE_USER` | +| Ajouter un bovin (POST /bovines) | `ROLE_USER` (actuellement `ROLE_ADMIN` — à abaisser, ce flux est métier opérationnel) | +| Supprimer un bovin (DELETE /bovines) | `ROLE_USER` (idem, à abaisser) | +| Valider l'entrée (PATCH receptions) | `ROLE_USER` | + +L'abaissement à `ROLE_USER` sur `Bovine::Post`, `Bovine::Patch` et `Bovine::Delete` est **délibéré** : ce flux fait partie des opérations métier quotidiennes, pas de l'administration. À confirmer pendant l'implémentation. + +## Cas limites + +- **Total saisi > déclaré** : autorisé (les déclarations en réception sont des indicateurs imprécis). +- **Doublon n° national** : la `UniqueConstraint` BDD le rejette → toast. +- **EDNOTIF indisponible** : bovin créé sans enrich, comportement actuel du processor. +- **Réception supprimée pendant la saisie** : impossible côté UI tant qu'on est dans l'écran. Si ça arrive (autre user), les `POST /bovines` suivants échoueront en 404 sur l'IRI reception → toast. +- **Sortie d'un bovin** : non géré dans ce lot. Le futur workflow de sortie viendra basculer `Bovine.exitedAt`. + +## Critères d'acceptation + +- [ ] Migration `entry_completed` sur Reception passe sans erreur. +- [ ] Migration `reception_id` sur Bovine passe sans erreur, bovins existants intacts. +- [ ] Card "CASES" sur home remplacée par "ENTRÉE / SORTIE". +- [ ] `/entry-exit` affiche les entrées en attente et un placeholder sorties. +- [ ] Click sur une entrée → écran saisie avec form pré-rempli. +- [ ] "Ajouter" → bovin créé, ligne au tableau, form reset (pré-remplissages restaurés). +- [ ] Suppression d'une ligne fonctionne avec confirmation. +- [ ] "Valider l'entrée" bascule `entryCompleted` et redirige. +- [ ] Une réception fermée disparaît de la liste. +- [ ] `BovineProcessor` corrigé pour utiliser `setBovineType()` avec auto-create. +- [ ] `make test` passe sans régression. + +## Mode d'implémentation + +Sur ce projet, l'utilisateur souhaite **valider chaque étape du plan** avant exécution. À chaque étape du plan d'implémentation, l'agent doit : + +1. Présenter ce qu'il s'apprête à faire (fichiers, changements). +2. Attendre la validation explicite de l'utilisateur. +3. Exécuter, puis présenter l'étape suivante. + +Cette discipline permet des retours en direct et des ajustements fins en cours de route.