From 08a17f91b3c704dd2c106d095f18a1055f80dd73 Mon Sep 17 00:00:00 2001 From: tristan Date: Tue, 28 Apr 2026 07:25:31 +0000 Subject: [PATCH] feat: ajout du prix au kilo et de l'age moyen bovin + feed bovin via xlsx (!50) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit | Numéro du ticket | Titre du ticket | |------------------|-----------------| | | | ## Description de la PR ## Modification du .env ## Check list - [ ] Pas de régression - [x] TU/TI/TF rédigée - [ ] TU/TI/TF OK - [ ] CHANGELOG modifié Reviewed-on: https://gitea.malio.fr/MALIO-DEV/Ferme/pulls/50 Co-authored-by: tristan Co-committed-by: tristan --- CLAUDE.md | 2 +- README.md | 62 +++++ frontend/composables/useBovineColumns.ts | 83 +++++++ frontend/pages/infrastructure/case.vue | 105 ++++++--- frontend/pages/inventory.vue | 72 +++--- frontend/services/dto/bovine-data.ts | 13 +- frontend/utils/bovine-age.ts | 8 + migrations/Version20260424132554.php | 31 +++ migrations/Version20260427154952.php | 35 +++ migrations/Version20260428061801.php | 35 +++ migrations/Version20260428065800.php | 50 ++++ src/ApiResource/BovineInventoryExport.php | 2 +- src/Command/FeedBovinePricesCommand.php | 215 ++++++++++++++++++ src/Command/SeedCommand.php | 3 + src/DataFixtures/ReferenceFixtures.php | 3 + src/Entity/Bovine.php | 84 +++++-- src/Entity/BovineType.php | 4 +- src/Entity/Building.php | 17 ++ src/Entity/Reception.php | 12 +- src/Entity/Shipment.php | 12 +- src/Repository/BovineRepository.php | 53 +++++ .../Bovin/BovineInventoryExportProvider.php | 14 +- .../Bovin/BovineInventoryStatsProvider.php | 27 +-- .../Bovin/BovineSyncInventoryProcessor.php | 39 +++- .../BuildingCaseWeightsReportProvider.php | 2 +- 25 files changed, 857 insertions(+), 126 deletions(-) create mode 100644 frontend/composables/useBovineColumns.ts create mode 100644 migrations/Version20260424132554.php create mode 100644 migrations/Version20260427154952.php create mode 100644 migrations/Version20260428061801.php create mode 100644 migrations/Version20260428065800.php create mode 100644 src/Command/FeedBovinePricesCommand.php create mode 100644 src/Repository/BovineRepository.php diff --git a/CLAUDE.md b/CLAUDE.md index 902346a..a61e7ff 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -82,7 +82,7 @@ frontend/ - Code en anglais ; "pont-bascule" est un terme métier conservé tel quel. - Les opérations API Platform sont définies directement sur les entités Doctrine. -- Pas de classes Repository custom : utiliser `EntityManagerInterface` avec les repos par défaut. +- Repository custom autorisé dès qu'on a une requête métier non-triviale (agrégations, jointures spécifiques, filtres multiples). Toujours via DQL/QueryBuilder, **jamais de SQL brut** (pas de `Connection::executeQuery`, `fetchAssociative`, etc.). Les CRUD basiques restent sur le repo Doctrine par défaut via `EntityManagerInterface`. - `config/reference.php` est auto-généré — ne pas modifier à la main. - Endpoints toujours au pluriel (convention API Platform). - Ne jamais créer de GET qui crée des ressources : utiliser POST + PATCH. diff --git a/README.md b/README.md index 790a0af..b594f72 100644 --- a/README.md +++ b/README.md @@ -178,3 +178,65 @@ Pour suivre les logs en temps réel : - tail -f var/log/dev.log - tail -f var/log/prod.log + +## Feed des prix bovins + +Une commande Symfony permet de mettre à jour le **poids à l'arrivée**, le **prix au kilo** et le **fournisseur** des bovins existants à partir d'un fichier XLSX. La commande **ne crée jamais de nouveau bovin** : elle complète seulement ceux déjà présents en BDD. + +### Format du fichier XLSX attendu + +Pas de ligne d'en-tête, 4 colonnes dans cet ordre : + +| Colonne | Champ | Format | Exemple | +|---------|-------|--------|---------| +| A | Numéro national | Avec ou sans préfixe `FR ` (insensible casse) | `FR 7979580026` ou `7979580026` | +| B | Fournisseur | Texte libre, casse ignorée | `TERRENA` | +| C | Poids à l'arrivée (kg) | Entier | `368` | +| D | Prix au kilo | Décimal | `5.7` | +| E | Code bâtiment (optionnel) | `B1`, `B2`, `B3`, `ZT` (casse ignorée) | `B2` | + +### Comportement + +- **Numéro national** : le préfixe `FR` (avec ou sans espace) est retiré s'il est présent. Sinon la valeur est utilisée telle quelle. +- **Bovin introuvable** en BDD → ligne ignorée, log warning à la fin avec aperçu. +- **Fournisseur introuvable** en BDD → le bovin est mis à jour quand même avec `supplier = null`, log warning. +- **Bâtiment** (colonne E) : recherché par `code` (insensible casse). Set uniquement si le bovin n'a pas déjà une `buildingCase` assignée (la case prime sur le bâtiment direct côté affichage). Si code introuvable → log warning, champ non set. +- **Cellules `weight` / `price` vides ou non numériques** → champ non modifié. +- La commande est **idempotente** : peut être relancée sans effet de bord. + +### Lancement en dev + +Copie le fichier dans la racine du projet (mappée dans le container sous `/var/www/html`), puis : + +```bash +# Simulation sans écriture en BDD +docker compose exec php bin/console app:feed-bovine-prices /var/www/html/feed_bovin.xlsx --dry-run + +# Persistance effective +docker compose exec php bin/console app:feed-bovine-prices /var/www/html/feed_bovin.xlsx +``` + +### Lancement en prod + +```bash +# 1. Envoyer le fichier sur le serveur +scp feed_bovin.xlsx ferme-prod:/tmp/ + +# 2. SSH sur le serveur et lancer la commande dans le dossier de l'app +ssh ferme-prod +cd /var/www/ferme +php bin/console app:feed-bovine-prices /tmp/feed_bovin.xlsx --dry-run # vérification +php bin/console app:feed-bovine-prices /tmp/feed_bovin.xlsx # exécution +rm /tmp/feed_bovin.xlsx # nettoyage +``` + +### Sortie attendue + +À la fin, un tableau récapitule : + +- Lignes totales lues +- Bovins mis à jour +- Bovins introuvables (avec aperçu des 10 premiers numéros) +- Lignes invalides (numéro national vide) +- Fournisseurs introuvables (avec liste et compte par nom) +- Bâtiments introuvables (avec liste des codes inconnus) diff --git a/frontend/composables/useBovineColumns.ts b/frontend/composables/useBovineColumns.ts new file mode 100644 index 0000000..a4866cd --- /dev/null +++ b/frontend/composables/useBovineColumns.ts @@ -0,0 +1,83 @@ +import { computed } from 'vue' +import { useAuthStore } from '~/stores/auth' + +export interface BovineColumn { + key: string + label: string + width?: string +} + +export interface UseBovineColumnsOptions { + /** + * 'inventory' (par défaut) : colonnes complètes incluant Bâtiment + Case. + * 'case' : pas de Bâtiment ni Case (déjà dans le titre de la page), + * largeurs élargies pour combler l'espace. + */ + variant?: 'inventory' | 'case' +} + +/** + * Définition partagée des colonnes des tableaux bovins (inventory + case). + * Variants distincts pour chaque écran et chaque rôle (admin/user) afin de + * pouvoir ajuster les largeurs indépendamment. + */ +export const useBovineColumns = (options: UseBovineColumnsOptions = {}) => { + const auth = useAuthStore() + + const adminColumnsInventory: BovineColumn[] = [ + { key: 'nationalNumber', label: 'N° National', width: '80px' }, + { key: 'workNumber', label: 'N° Travail', width: '60px' }, + { key: 'sex', label: 'Sexe', width: '70px' }, + { key: 'birthDate', label: 'Né le', width: '72px' }, + { key: 'age', label: 'Age', width: '110px' }, + { key: 'bovineType.label', label: 'Race', width: '90px' }, + { key: 'buildingCase.building.label', label: 'Bâtiment', width: '1fr' }, + { key: 'buildingCase.caseNumber', label: 'Case', width: '42px' }, + { key: 'arrivalDate', label: 'Entrée le', width: '90px' }, + { key: 'pricePerKg', label: 'Prix/kg', width: '65px' }, + { key: 'finalPrice', label: 'Prix total', width: '80px' } + ] + + const userColumnsInventory: BovineColumn[] = [ + { key: 'nationalNumber', label: 'N° National', width: '80px' }, + { key: 'workNumber', label: 'N° Travail', width: '60px' }, + { key: 'sex', label: 'Sexe', width: '70px' }, + { key: 'birthDate', label: 'Né le', width: '72px' }, + { key: 'age', label: 'Age', width: '110px' }, + { key: 'bovineType.label', label: 'Race', width: '1fr' }, + { key: 'buildingCase.building.label', label: 'Bâtiment', width: '120px' }, + { key: 'buildingCase.caseNumber', label: 'Case', width: '42px' }, + { key: 'arrivalDate', label: 'Entrée le', width: '90px' } + ] + + const adminColumnsCase: BovineColumn[] = [ + { key: 'nationalNumber', label: 'N° National', width: '110px' }, + { key: 'workNumber', label: 'N° Travail', width: '85px' }, + { key: 'sex', label: 'Sexe', width: '90px' }, + { key: 'birthDate', label: 'Né le', width: '100px' }, + { key: 'age', label: 'Age', width: '90px' }, + { key: 'bovineType.label', label: 'Race', width: '1fr' }, + { key: 'arrivalDate', label: 'Entrée le', width: '110px' }, + { key: 'pricePerKg', label: 'Prix/kg', width: '85px' }, + { key: 'finalPrice', label: 'Prix total', width: '105px' } + ] + + const userColumnsCase: BovineColumn[] = [ + { key: 'nationalNumber', label: 'N° National', width: '130px' }, + { key: 'workNumber', label: 'N° Travail', width: '100px' }, + { key: 'sex', label: 'Sexe', width: '110px' }, + { key: 'birthDate', label: 'Né le', width: '140px' }, + { key: 'age', label: 'Age', width: '130px' }, + { key: 'bovineType.label', label: 'Race', width: '1fr' }, + { key: 'arrivalDate', label: 'Entrée le', width: '170px' } + ] + + const columns = computed(() => { + if (options.variant === 'case') { + return auth.isAdmin ? adminColumnsCase : userColumnsCase + } + return auth.isAdmin ? adminColumnsInventory : userColumnsInventory + }) + + return { columns } +} diff --git a/frontend/pages/infrastructure/case.vue b/frontend/pages/infrastructure/case.vue index 18df4b2..16cb1d1 100644 --- a/frontend/pages/infrastructure/case.vue +++ b/frontend/pages/infrastructure/case.vue @@ -33,7 +33,22 @@ -
+
+
+ {{ stats.over24 }} + ≥ 24 mois +
+
+ {{ stats.between22And24 }} + 22 – 24 mois +
+
+ {{ stats.between20And22 }} + 20 – 22 mois +
+
+ +
- -