200 lines
9.3 KiB
Markdown
200 lines
9.3 KiB
Markdown
# 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<Bovine>` 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<br>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.
|