From b45e2d3a95152bb26f9b7a719c3509ad63a5bf29 Mon Sep 17 00:00:00 2001 From: tristan Date: Fri, 10 Apr 2026 10:29:16 +0200 Subject: [PATCH] =?UTF-8?q?feat=20:=20=C3=A9cran=20d'ajout=20bovin=20+=20f?= =?UTF-8?q?eed=20bovin=20+=20fix=20pes=C3=A9es=20exp=C3=A9ditions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/composables/steps/useWeighingStep.ts | 1 + frontend/composables/useWeighing.ts | 6 +- frontend/config/shipment.config.ts | 8 +- frontend/pages/infrastructure/bovine.vue | 180 ++++++++++++++++++ frontend/pages/infrastructure/building.vue | 60 +++--- frontend/pages/infrastructure/case.vue | 123 +++++++++++- frontend/pages/shipment/[[id]].vue | 12 +- frontend/pages/shipment/update/[[id]].vue | 22 +-- frontend/services/bovine.ts | 13 ++ frontend/services/dto/bovine-data.ts | 2 + frontend/services/dto/building-case-data.ts | 12 +- .../services/dto/building-case-status-data.ts | 6 - frontend/services/statut.ts | 23 --- migrations/Version20260410065020.php | 35 ++++ migrations/Version20260410074533.php | 35 ++++ migrations/Version20260410081839.php | 35 ++++ migrations/Version20260410082723.php | 31 +++ src/Command/EnrichBovinesCommand.php | 99 ++++++++++ src/Command/SeedCommand.php | 40 +--- .../BuildingInfrastructureFixtures.php | 62 +----- src/Entity/Bovine.php | 76 +++++++- src/Entity/Building.php | 6 +- src/Entity/BuildingCase.php | 34 ++-- src/Entity/Statut.php | 138 -------------- src/State/BovineProcessor.php | 68 +++++++ .../BuildingCaseWeightsReportProvider.php | 45 +---- templates/case_weights_report.html.twig | 19 +- 27 files changed, 788 insertions(+), 403 deletions(-) create mode 100644 frontend/pages/infrastructure/bovine.vue delete mode 100644 frontend/services/dto/building-case-status-data.ts delete mode 100644 frontend/services/statut.ts create mode 100644 migrations/Version20260410065020.php create mode 100644 migrations/Version20260410074533.php create mode 100644 migrations/Version20260410081839.php create mode 100644 migrations/Version20260410082723.php create mode 100644 src/Command/EnrichBovinesCommand.php delete mode 100644 src/Entity/Statut.php create mode 100644 src/State/BovineProcessor.php diff --git a/frontend/composables/steps/useWeighingStep.ts b/frontend/composables/steps/useWeighingStep.ts index 916f019..60be3ef 100644 --- a/frontend/composables/steps/useWeighingStep.ts +++ b/frontend/composables/steps/useWeighingStep.ts @@ -37,6 +37,7 @@ export const useWeighingStep = (options: UseWeighingStepOptions) => { entityName: options.entityName, apiResource: options.apiResource, titleLabel: options.titleLabel, + isFinal: options.isFinal, getWeightFromScale: options.getWeightFromScale, updateEntity: options.updateEntity, loadEntity: options.loadEntity diff --git a/frontend/composables/useWeighing.ts b/frontend/composables/useWeighing.ts index 2b92130..d6dac48 100644 --- a/frontend/composables/useWeighing.ts +++ b/frontend/composables/useWeighing.ts @@ -12,6 +12,7 @@ export interface UseWeighingOptions { entityName: 'reception' | 'shipment' apiResource: string titleLabel: string + isFinal?: boolean getWeightFromScale: () => Promise updateEntity: (id: number, payload: any) => Promise loadEntity?: (id: number) => Promise @@ -23,6 +24,7 @@ export const useWeighing = ({ entityName, apiResource, titleLabel, + isFinal = false, getWeightFromScale, updateEntity, loadEntity @@ -77,7 +79,7 @@ export const useWeighing = ({ }) } - const nextStep = mode === 'tare' + const nextStep = isFinal ? entity.value.currentStep : entity.value.currentStep + 1 await updateEntity(entity.value.id, { @@ -152,7 +154,7 @@ export const useWeighingShipment = ({ entity: shipment, entityName: 'shipment', apiResource: 'shipments', - titleLabel: modeShipment === 'gross' ? 'Pesée à vide' : 'Pesée à plein', + titleLabel: modeShipment === 'gross' ? 'Pesée à plein' : 'Pesée à vide', getWeightFromScale: async () => { const { getWeightShipment } = await import('~/services/shipment') return getWeightShipment() diff --git a/frontend/config/shipment.config.ts b/frontend/config/shipment.config.ts index 4244e71..388467a 100644 --- a/frontend/config/shipment.config.ts +++ b/frontend/config/shipment.config.ts @@ -5,13 +5,13 @@ export const shipmentConfig: WorkflowConfig = { apiResource: 'shipments', steps: [ { label: 'Expédition' }, - { label: 'Pesée à vide', weighingMode: 'gross' }, + { label: 'Pesée à vide', weighingMode: 'tare' }, { label: 'Chargement' }, - { label: 'Pesée à plein', weighingMode: 'tare', isFinal: true } + { label: 'Pesée à plein', weighingMode: 'gross', isFinal: true } ], weighingLabels: { - gross: 'Pesée à vide', - tare: 'Pesée à plein' + gross: 'Pesée à plein', + tare: 'Pesée à vide' }, buildReceiptFilename: (entity: WorkflowEntity) => { const ship = entity as any diff --git a/frontend/pages/infrastructure/bovine.vue b/frontend/pages/infrastructure/bovine.vue new file mode 100644 index 0000000..8962c48 --- /dev/null +++ b/frontend/pages/infrastructure/bovine.vue @@ -0,0 +1,180 @@ + + + diff --git a/frontend/pages/infrastructure/building.vue b/frontend/pages/infrastructure/building.vue index 77e73c4..16651c5 100644 --- a/frontend/pages/infrastructure/building.vue +++ b/frontend/pages/infrastructure/building.vue @@ -36,7 +36,7 @@ v-for="cell in entry.cells" :key="cell.key" class="relative text-white flex h-[50px] items-center justify-center border-y-[3px] border-y-black bg-white hover:opacity-85 focus-visible:outline-none" - :class="[cell.sideBorderClass, activeLegendStatutId !== null && cell.caseStatusId !== activeLegendStatutId ? 'opacity-35 hover:opacity-70' : '']" + :class="[cell.sideBorderClass, activeLegendLabel !== null && cell.caseStatusLabel !== activeLegendLabel ? 'opacity-35 hover:opacity-70' : '']" :style="[cell.spanStyle, cell.sideBorderStyle]" :to="cell.caseId ? `/infrastructure/case?id=${cell.caseId}` : '/infrastructure/case'" :title="cell.caseStatusLabel ?? undefined" @@ -58,25 +58,19 @@
- -
+
- + {{ statut.label }}
@@ -90,33 +84,35 @@ import type {BuildingData} from "~/services/dto/building-data" import type {BuildingLayoutData} from "~/services/dto/building-layout-data" import type {BuildingCasePositionData} from "~/services/dto/building-case-position-data" -import type {BuildingCaseStatusData} from "~/services/dto/building-case-status-data" import {getBuildingList} from "~/services/building" -import {getStatutList} from "~/services/statut" definePageMeta({layout: "default"}) const router = useRouter() // Données brutes chargées depuis l'API const buildingList = ref([]) -const statutLegend = ref([]) +const statutLegend = [ + { label: 'Libre', couleur: '#A3B18A' }, + { label: 'Occupé', couleur: '#3A506B' }, + { label: 'Malade', couleur: '#E07A5F' }, +] // Statut actuellement survolé dans la légende (pour filtrage visuel) -const activeLegendStatutId = ref(null) +const activeLegendLabel = ref(null) // Modèle de vue prêt pour le template (layout + cellules + styles de grille) const buildingLayouts = computed(() => - buildingList.value.map((building) => { - // On affiche uniquement le premier layout du bâtiment - const layout = building.layouts?.[0] ?? null - const view = layout ? buildLayoutView(layout) : null - return {building, layout, cells: view?.cells ?? [], gridStyle: view?.gridStyle ?? {}} - }) + buildingList.value + .filter((building) => building.layouts && building.layouts.length > 0) + .map((building) => { + const layout = building.layouts![0] + const view = buildLayoutView(layout) + return {building, layout, cells: view?.cells ?? [], gridStyle: view?.gridStyle ?? {}} + }) ) type GridCell = { key: string caseId: number | null display: string - caseStatusId: number | null caseStatusLabel: string | null // Couleur de fond de la case (dépend du statut) caseStyle?: Record @@ -130,7 +126,8 @@ type GridCell = { contentInsetClass: string } // Type intermédiaire : garde des infos utiles au calcul des bordures, retirées ensuite -type GridCellDraft = Omit & { x: number; columnSpan: number } +type GridCellDraft = Omit & { x: number; columnSpan: number} + // Nettoie la couleur de statut pour éviter les chaînes vides / espaces const normalizeCaseStatusColor = (value: string | null | undefined): string | null => { @@ -181,7 +178,6 @@ const buildLayoutView = (layout: BuildingLayoutData): { // Métadonnées utiles au rendu / navigation / légende const caseId = (position.buildingCase?.id ?? null) as number | null const caseNumber = (position.buildingCase?.caseNumber ?? null) as number | null - const caseStatusId = position.buildingCase?.statut?.id ?? null const caseStatusLabel = position.buildingCase?.statut?.label ?? null const statusColor = normalizeCaseStatusColor(position.buildingCase?.statut?.couleur) @@ -191,7 +187,6 @@ const buildLayoutView = (layout: BuildingLayoutData): { columnSpan, caseId, display: caseNumber !== null ? String(caseNumber) : "Case", - caseStatusId, caseStatusLabel, caseStyle: statusColor ? {backgroundColor: statusColor} : undefined, // Exemple : "14 / span 1" => commence en colonne 14 et occupe 1 colonne @@ -230,13 +225,6 @@ const buildLayoutView = (layout: BuildingLayoutData): { } onMounted(async () => { - // Chargement initial des bâtiments et de la légende des statuts - const buildings = await getBuildingList() - const statuts = await getStatutList() - buildingList.value = buildings - // Tri alphabétique FR pour une légende stable - statutLegend.value = [...statuts].sort((a, b) => - (a.label ?? "").localeCompare(b.label ?? "", "fr", {sensitivity: "base"}) - ) + buildingList.value = await getBuildingList() }) diff --git a/frontend/pages/infrastructure/case.vue b/frontend/pages/infrastructure/case.vue index 3c359a7..cf444d1 100644 --- a/frontend/pages/infrastructure/case.vue +++ b/frontend/pages/infrastructure/case.vue @@ -1,21 +1,118 @@ diff --git a/frontend/pages/shipment/[[id]].vue b/frontend/pages/shipment/[[id]].vue index 36d7323..03194a6 100644 --- a/frontend/pages/shipment/[[id]].vue +++ b/frontend/pages/shipment/[[id]].vue @@ -18,11 +18,11 @@

- pesée à plein -

-

+ pesée à plein +

submitted.value && (tareWeight.value.weight === null || tareWeight.value.weighedAt === null || tareWeight.value.dsd === null) ) -const activeTab = ref<'weightsEmpty' | 'weights'>('weights') +const activeTab = ref<'weightsEmpty' | 'weights'>('weightsEmpty') const grossWeight = ref(createEmptyWeightEntry('gross')) const tareWeight = ref(createEmptyWeightEntry('tare')) const formIsLoading = ref(false) diff --git a/frontend/services/bovine.ts b/frontend/services/bovine.ts index 1524991..7a13a8b 100644 --- a/frontend/services/bovine.ts +++ b/frontend/services/bovine.ts @@ -27,3 +27,16 @@ export async function createBovines(nationalNumbers: string[]): Promise<{ create return { created, errors } } + +export async function getBovine(id: number) { + const api = useApi() + return api.get(`bovines/${id}`) +} + +export async function updateBovine(id: number, payload: BovinePayload) { + const api = useApi() + return api.patch(`bovines/${id}`, payload, { + toastErrorKey: 'errors.bovine.update', + toastSuccessKey: 'success.bovine.update' + }) +} diff --git a/frontend/services/dto/bovine-data.ts b/frontend/services/dto/bovine-data.ts index ddc736f..58bb736 100644 --- a/frontend/services/dto/bovine-data.ts +++ b/frontend/services/dto/bovine-data.ts @@ -4,6 +4,7 @@ export interface BovineData { receivedWeight: number | null arrivalDate: string | null buildingCase: string | null + supplier: string | null } export type BovinePayload = { @@ -11,4 +12,5 @@ export type BovinePayload = { receivedWeight?: number | null arrivalDate?: string | null buildingCase?: string | null + supplier?: string | null } diff --git a/frontend/services/dto/building-case-data.ts b/frontend/services/dto/building-case-data.ts index 4d718c3..08aa5be 100644 --- a/frontend/services/dto/building-case-data.ts +++ b/frontend/services/dto/building-case-data.ts @@ -1,9 +1,17 @@ -import type { BuildingCaseStatusData } from '~/services/dto/building-case-status-data' +import type { BovineData } from '~/services/dto/bovine-data' + +export interface BuildingSummary { + id: number + label: string + code: string +} export interface BuildingCaseData { id: number caseNumber: number | null code: string | null capacity: number | null - statut?: BuildingCaseStatusData | null + statut?: { label: string; couleur: string } | null + building?: BuildingSummary | null + bovines: BovineData[] } diff --git a/frontend/services/dto/building-case-status-data.ts b/frontend/services/dto/building-case-status-data.ts deleted file mode 100644 index 187a412..0000000 --- a/frontend/services/dto/building-case-status-data.ts +++ /dev/null @@ -1,6 +0,0 @@ -export interface BuildingCaseStatusData { - id: number - label: string | null - code: string | null - couleur: string | null -} diff --git a/frontend/services/statut.ts b/frontend/services/statut.ts deleted file mode 100644 index 3a7622c..0000000 --- a/frontend/services/statut.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { useApi } from '~/composables/useApi' -import type { BuildingCaseStatusData } from '~/services/dto/building-case-status-data' - -export type StatutListResponse = - | BuildingCaseStatusData[] - | { 'hydra:member'?: BuildingCaseStatusData[] } - -export async function getStatutList(): Promise { - const api = useApi() - const response = await api.get('statuts', {}, { - toastErrorKey: 'errors.http.get' - }) - - if (Array.isArray(response)) { - return response - } - - if (response && typeof response === 'object' && Array.isArray(response['hydra:member'])) { - return response['hydra:member'] - } - - return [] -} diff --git a/migrations/Version20260410065020.php b/migrations/Version20260410065020.php new file mode 100644 index 0000000..07afd8c --- /dev/null +++ b/migrations/Version20260410065020.php @@ -0,0 +1,35 @@ +addSql('ALTER TABLE bovine ADD supplier_id INT DEFAULT NULL'); + $this->addSql('ALTER TABLE bovine ADD CONSTRAINT FK_2068337F2ADD6D8C FOREIGN KEY (supplier_id) REFERENCES supplier (id)'); + $this->addSql('CREATE INDEX IDX_2068337F2ADD6D8C ON bovine (supplier_id)'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('ALTER TABLE bovine DROP CONSTRAINT FK_2068337F2ADD6D8C'); + $this->addSql('DROP INDEX IDX_2068337F2ADD6D8C'); + $this->addSql('ALTER TABLE bovine DROP supplier_id'); + } +} diff --git a/migrations/Version20260410074533.php b/migrations/Version20260410074533.php new file mode 100644 index 0000000..c48ecc0 --- /dev/null +++ b/migrations/Version20260410074533.php @@ -0,0 +1,35 @@ +addSql('ALTER TABLE bovine ADD work_number VARCHAR(50) DEFAULT NULL'); + $this->addSql('ALTER TABLE bovine ADD birth_date DATE DEFAULT NULL'); + $this->addSql('ALTER TABLE bovine ADD breed_code VARCHAR(20) DEFAULT NULL'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('ALTER TABLE bovine DROP work_number'); + $this->addSql('ALTER TABLE bovine DROP birth_date'); + $this->addSql('ALTER TABLE bovine DROP breed_code'); + } +} diff --git a/migrations/Version20260410081839.php b/migrations/Version20260410081839.php new file mode 100644 index 0000000..348e2bc --- /dev/null +++ b/migrations/Version20260410081839.php @@ -0,0 +1,35 @@ +addSql('ALTER TABLE building_case DROP CONSTRAINT fk_de2cee50f6203804'); + $this->addSql('DROP INDEX idx_de2cee50f6203804'); + $this->addSql('ALTER TABLE building_case DROP statut_id'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('ALTER TABLE building_case ADD statut_id INT DEFAULT NULL'); + $this->addSql('ALTER TABLE building_case ADD CONSTRAINT fk_de2cee50f6203804 FOREIGN KEY (statut_id) REFERENCES statut (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('CREATE INDEX idx_de2cee50f6203804 ON building_case (statut_id)'); + } +} diff --git a/migrations/Version20260410082723.php b/migrations/Version20260410082723.php new file mode 100644 index 0000000..51be61e --- /dev/null +++ b/migrations/Version20260410082723.php @@ -0,0 +1,31 @@ +addSql('DROP TABLE statut'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('CREATE TABLE statut (id INT GENERATED BY DEFAULT AS IDENTITY NOT NULL, label VARCHAR(255) NOT NULL, code VARCHAR(255) NOT NULL, color VARCHAR(255) NOT NULL, PRIMARY KEY (id))'); + } +} diff --git a/src/Command/EnrichBovinesCommand.php b/src/Command/EnrichBovinesCommand.php new file mode 100644 index 0000000..b179ed2 --- /dev/null +++ b/src/Command/EnrichBovinesCommand.php @@ -0,0 +1,99 @@ +entityManager->getRepository(Bovine::class)->findBy(['workNumber' => null]); + + if (0 === count($bovines)) { + $io->success('Tous les bovins sont déjà enrichis.'); + + return Command::SUCCESS; + } + + $io->info(sprintf('%d bovin(s) à enrichir.', count($bovines))); + + $enriched = 0; + $failed = 0; + + foreach ($bovines as $bovine) { + try { + $animalFile = $this->bovinApi->getAnimalFile( + nationalNumber: $bovine->getNationalNumber(), + countryCode: 'FR', + ); + $identification = $animalFile->identification; + + if (null === $identification) { + $io->warning(sprintf(' %s — pas d\'identification retournée.', $bovine->getNationalNumber())); + ++$failed; + + continue; + } + + $bovine->setWorkNumber($identification->workNumber); + $bovine->setBirthDate($identification->birthDate?->date); + $bovine->setBreedCode($this->normalizeBreedCode($identification->breedType)); + + ++$enriched; + $io->text(sprintf(' ✓ %s → n° travail %s', $bovine->getNationalNumber(), $identification->workNumber ?? '—')); + } catch (Throwable $e) { + ++$failed; + $io->warning(sprintf(' %s — erreur : %s', $bovine->getNationalNumber(), $e->getMessage())); + } + } + + $this->entityManager->flush(); + + $io->success(sprintf('%d enrichi(s), %d échoué(s).', $enriched, $failed)); + + return Command::SUCCESS; + } + + private function normalizeBreedCode(mixed $breedType): ?string + { + if (null === $breedType) { + return null; + } + + if (is_numeric($breedType)) { + return (string) $breedType; + } + + if (is_string($breedType) && preg_match('/\d+/', $breedType, $matches)) { + return $matches[0]; + } + + return null; + } +} diff --git a/src/Command/SeedCommand.php b/src/Command/SeedCommand.php index 0c25b59..ef56831 100644 --- a/src/Command/SeedCommand.php +++ b/src/Command/SeedCommand.php @@ -18,7 +18,6 @@ use App\Entity\MerchandiseType; use App\Entity\PelletType; use App\Entity\ReceptionType; use App\Entity\ShipmentType; -use App\Entity\Statut; use App\Entity\Supplier; use App\Entity\Truck; use App\Entity\Vehicle; @@ -230,24 +229,6 @@ class SeedCommand extends Command private function seedBuildingInfrastructure(): void { - $statusByCode = []; - $statusRows = [ - ['label' => 'Libre', 'code' => 'LB', 'color' => '#A3B18A'], - ['label' => 'Occupé', 'code' => 'OC', 'color' => '#3A506B'], - ['label' => 'Malade', 'code' => 'ML', 'color' => '#E07A5F'], - ]; - foreach ($statusRows as $statusRow) { - /** @var Statut $status */ - $status = $this->upsertByCode(Statut::class, $statusRow['code'], static function (Statut $entity) use ($statusRow) { - $entity - ->setLabel($statusRow['label']) - ->setCode($statusRow['code']) - ->setColor($statusRow['color']) - ; - }); - $statusByCode[$statusRow['code']] = $status; - } - $buildingRepo = $this->entityManager->getRepository(Building::class); $layoutByBuildingCode = []; $layoutRows = [ @@ -274,25 +255,15 @@ class SeedCommand extends Command } $caseRows = [ - ['buildingCode' => 'B1', 'from' => 1, 'to' => 12, 'status' => 'LB'], - ['buildingCode' => 'B1', 'from' => 13, 'to' => 24, 'status' => 'OC'], - ['buildingCode' => 'B1', 'from' => 25, 'to' => 32, 'status' => 'ML'], - ['buildingCode' => 'B1', 'from' => 33, 'to' => 44, 'status' => 'LB'], - ['buildingCode' => 'B2', 'from' => 1, 'to' => 10, 'status' => 'OC'], - ['buildingCode' => 'B2', 'from' => 11, 'to' => 22, 'status' => 'LB'], - ['buildingCode' => 'B2', 'from' => 23, 'to' => 30, 'status' => 'ML'], - ['buildingCode' => 'B2', 'from' => 31, 'to' => 44, 'status' => 'OC'], - ['buildingCode' => 'B3', 'from' => 1, 'to' => 8, 'status' => 'ML'], - ['buildingCode' => 'B3', 'from' => 9, 'to' => 20, 'status' => 'LB'], - ['buildingCode' => 'B3', 'from' => 21, 'to' => 34, 'status' => 'OC'], - ['buildingCode' => 'B3', 'from' => 35, 'to' => 44, 'status' => 'ML'], + ['buildingCode' => 'B1', 'from' => 1, 'to' => 44], + ['buildingCode' => 'B2', 'from' => 1, 'to' => 44], + ['buildingCode' => 'B3', 'from' => 1, 'to' => 44], ]; $caseByCode = []; foreach ($caseRows as $caseRow) { $building = $buildingRepo->findOneBy(['code' => $caseRow['buildingCode']]); - $status = $statusByCode[$caseRow['status']] ?? null; - if (!$building instanceof Building || !$status instanceof Statut) { + if (!$building instanceof Building) { continue; } @@ -300,13 +271,12 @@ class SeedCommand extends Command $code = sprintf('%s-C%d', $caseRow['buildingCode'], $caseNumber); /** @var BuildingCase $buildingCase */ - $buildingCase = $this->upsertByCode(BuildingCase::class, $code, static function (BuildingCase $entity) use ($code, $caseNumber, $building, $status) { + $buildingCase = $this->upsertByCode(BuildingCase::class, $code, static function (BuildingCase $entity) use ($code, $caseNumber, $building) { $entity ->setCode($code) ->setCaseNumber($caseNumber) ->setCapacity(15) ->setIdBuilding($building) - ->setStatut($status) ; }); $caseByCode[$code] = $buildingCase; diff --git a/src/DataFixtures/BuildingInfrastructureFixtures.php b/src/DataFixtures/BuildingInfrastructureFixtures.php index f95e873..463bbaf 100644 --- a/src/DataFixtures/BuildingInfrastructureFixtures.php +++ b/src/DataFixtures/BuildingInfrastructureFixtures.php @@ -8,7 +8,6 @@ use App\Entity\Building; use App\Entity\BuildingCase; use App\Entity\BuildingCasePosition; use App\Entity\BuildingLayout; -use App\Entity\Statut; use Doctrine\Bundle\FixturesBundle\Fixture; use Doctrine\Common\DataFixtures\DependentFixtureInterface; use Doctrine\Persistence\ObjectManager; @@ -18,10 +17,9 @@ class BuildingInfrastructureFixtures extends Fixture implements DependentFixture { public function load(ObjectManager $manager): void { - $statuts = $this->loadStatuts($manager); $buildings = $this->getBuildingsByCode($manager, ['B1', 'B2', 'B3']); $layouts = $this->loadLayouts($manager, $buildings); - $cases = $this->loadBuildingCases($manager, $buildings, $statuts); + $cases = $this->loadBuildingCases($manager, $buildings); $this->loadCasePositions($manager, $layouts, $cases); $manager->flush(); @@ -34,38 +32,6 @@ class BuildingInfrastructureFixtures extends Fixture implements DependentFixture ]; } - /** - * @return array - */ - private function loadStatuts(ObjectManager $manager): array - { - $repo = $manager->getRepository(Statut::class); - - $data = [ - ['label' => 'Libre', 'code' => 'LB', 'color' => '#A3B18A'], - ['label' => 'Occupé', 'code' => 'OC', 'color' => '#3A506B'], - ['label' => 'Malade', 'code' => 'ML', 'color' => '#E07A5F'], - ]; - - $result = []; - foreach ($data as $row) { - /** @var null|Statut $statut */ - $statut = $repo->findOneBy(['code' => $row['code']]); - if (!$statut instanceof Statut) { - $statut = new Statut() - ->setLabel($row['label']) - ->setCode($row['code']) - ->setColor($row['color']) - ; - $manager->persist($statut); - } - - $result[$row['code']] = $statut; - } - - return $result; - } - /** * @param list $codes * @@ -126,34 +92,21 @@ class BuildingInfrastructureFixtures extends Fixture implements DependentFixture /** * @param array $buildings - * @param array $statuts * * @return array */ - private function loadBuildingCases(ObjectManager $manager, array $buildings, array $statuts): array + private function loadBuildingCases(ObjectManager $manager, array $buildings): array { $repo = $manager->getRepository(BuildingCase::class); - $statusRanges = [ - // B1 - ['buildingCode' => 'B1', 'from' => 1, 'to' => 12, 'statut' => 'LB'], - ['buildingCode' => 'B1', 'from' => 13, 'to' => 24, 'statut' => 'OC'], - ['buildingCode' => 'B1', 'from' => 25, 'to' => 32, 'statut' => 'ML'], - ['buildingCode' => 'B1', 'from' => 33, 'to' => 44, 'statut' => 'LB'], - // B2 - ['buildingCode' => 'B2', 'from' => 1, 'to' => 10, 'statut' => 'OC'], - ['buildingCode' => 'B2', 'from' => 11, 'to' => 22, 'statut' => 'LB'], - ['buildingCode' => 'B2', 'from' => 23, 'to' => 30, 'statut' => 'ML'], - ['buildingCode' => 'B2', 'from' => 31, 'to' => 44, 'statut' => 'OC'], - // B3 - ['buildingCode' => 'B3', 'from' => 1, 'to' => 8, 'statut' => 'ML'], - ['buildingCode' => 'B3', 'from' => 9, 'to' => 20, 'statut' => 'LB'], - ['buildingCode' => 'B3', 'from' => 21, 'to' => 34, 'statut' => 'OC'], - ['buildingCode' => 'B3', 'from' => 35, 'to' => 44, 'statut' => 'ML'], + $caseRanges = [ + ['buildingCode' => 'B1', 'from' => 1, 'to' => 44], + ['buildingCode' => 'B2', 'from' => 1, 'to' => 44], + ['buildingCode' => 'B3', 'from' => 1, 'to' => 44], ]; $result = []; - foreach ($statusRanges as $range) { + foreach ($caseRanges as $range) { for ($caseNumber = $range['from']; $caseNumber <= $range['to']; ++$caseNumber) { $code = sprintf('%s-C%d', $range['buildingCode'], $caseNumber); @@ -169,7 +122,6 @@ class BuildingInfrastructureFixtures extends Fixture implements DependentFixture ->setCode($code) ->setCapacity(15) ->setIdBuilding($buildings[$range['buildingCode']]) - ->setStatut($statuts[$range['statut']]) ; $manager->persist($buildingCase); } diff --git a/src/Entity/Bovine.php b/src/Entity/Bovine.php index b3c8310..29930d8 100644 --- a/src/Entity/Bovine.php +++ b/src/Entity/Bovine.php @@ -9,6 +9,7 @@ use ApiPlatform\Metadata\Get; use ApiPlatform\Metadata\GetCollection; use ApiPlatform\Metadata\Patch; use ApiPlatform\Metadata\Post; +use App\State\BovineProcessor; use DateTimeImmutable; use Doctrine\ORM\Mapping as ORM; use Symfony\Component\Serializer\Attribute\Context; @@ -31,12 +32,14 @@ use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer; normalizationContext: ['groups' => ['bovine:read']], denormalizationContext: ['groups' => ['bovine:write']], security: "is_granted('ROLE_ADMIN')", + processor: BovineProcessor::class, ), new Patch( requirements: ['id' => '\d+'], normalizationContext: ['groups' => ['bovine:read']], denormalizationContext: ['groups' => ['bovine:write']], security: "is_granted('ROLE_ADMIN')", + processor: BovineProcessor::class, ), ], security: "is_granted('ROLE_USER')", @@ -46,19 +49,19 @@ class Bovine #[ORM\Id] #[ORM\GeneratedValue] #[ORM\Column] - #[Groups(['bovine:read'])] + #[Groups(['bovine:read', 'building_case:read'])] private ?int $id = null; #[ORM\Column(length: 50)] - #[Groups(['bovine:read', 'bovine:write'])] + #[Groups(['bovine:read', 'bovine:write', 'building_case:read'])] private string $nationalNumber = ''; #[ORM\Column(nullable: true)] - #[Groups(['bovine:read', 'bovine:write'])] + #[Groups(['bovine:read', 'bovine:write', 'building_case:read'])] private ?int $receivedWeight = null; #[ORM\Column(type: 'date_immutable', nullable: true)] - #[Groups(['bovine:read', 'bovine:write'])] + #[Groups(['bovine:read', 'bovine:write', 'building_case:read'])] #[Context([DateTimeNormalizer::FORMAT_KEY => 'Y-m-d'])] private ?DateTimeImmutable $arrivalDate = null; @@ -66,6 +69,23 @@ class Bovine #[Groups(['bovine:read', 'bovine:write'])] private ?BuildingCase $buildingCase = null; + #[ORM\ManyToOne] + #[Groups(['bovine:read', 'bovine:write', 'building_case:read'])] + private ?Supplier $supplier = null; + + #[ORM\Column(length: 50, nullable: true)] + #[Groups(['bovine:read', 'building_case:read'])] + private ?string $workNumber = null; + + #[ORM\Column(type: 'date_immutable', nullable: true)] + #[Groups(['bovine:read', 'building_case:read'])] + #[Context([DateTimeNormalizer::FORMAT_KEY => 'Y-m-d'])] + private ?DateTimeImmutable $birthDate = null; + + #[ORM\Column(length: 20, nullable: true)] + #[Groups(['bovine:read', 'building_case:read'])] + private ?string $breedCode = null; + public function getId(): ?int { return $this->id; @@ -118,4 +138,52 @@ class Bovine return $this; } + + public function getSupplier(): ?Supplier + { + return $this->supplier; + } + + public function setSupplier(?Supplier $supplier): static + { + $this->supplier = $supplier; + + return $this; + } + + public function getWorkNumber(): ?string + { + return $this->workNumber; + } + + public function setWorkNumber(?string $workNumber): static + { + $this->workNumber = $workNumber; + + return $this; + } + + public function getBirthDate(): ?DateTimeImmutable + { + return $this->birthDate; + } + + public function setBirthDate(?DateTimeImmutable $birthDate): static + { + $this->birthDate = $birthDate; + + return $this; + } + + public function getBreedCode(): ?string + { + return $this->breedCode; + } + + public function setBreedCode(?string $breedCode): static + { + $this->breedCode = $breedCode; + + return $this; + } } diff --git a/src/Entity/Building.php b/src/Entity/Building.php index d1d37eb..c857aba 100644 --- a/src/Entity/Building.php +++ b/src/Entity/Building.php @@ -32,15 +32,15 @@ class Building #[ORM\Id] #[ORM\GeneratedValue] #[ORM\Column] - #[Groups(['building:read', 'reception:read'])] + #[Groups(['building:read', 'building:summary', 'reception:read'])] private ?int $id = null; #[ORM\Column(length: 120)] - #[Groups(['building:read', 'reception:read'])] + #[Groups(['building:read', 'building:summary', 'reception:read'])] private string $label = ''; #[ORM\Column(length: 50)] - #[Groups(['building:read', 'reception:read'])] + #[Groups(['building:read', 'building:summary', 'reception:read'])] private string $code = ''; /** diff --git a/src/Entity/BuildingCase.php b/src/Entity/BuildingCase.php index 0dd4f85..66f7bf5 100644 --- a/src/Entity/BuildingCase.php +++ b/src/Entity/BuildingCase.php @@ -19,7 +19,7 @@ use Symfony\Component\Serializer\Attribute\SerializedName; operations: [ new Get( requirements: ['id' => '\d+'], - normalizationContext: ['groups' => ['building:read']], + normalizationContext: ['groups' => ['building_case:read', 'building:summary']], ), new Get( uriTemplate: '/building_cases/{id}/weights-report', @@ -39,20 +39,20 @@ class BuildingCase #[ORM\Id] #[ORM\GeneratedValue] #[ORM\Column] - #[Groups(['building:read'])] + #[Groups(['building:read', 'building_case:read'])] private ?int $id = null; #[ORM\Column] - #[Groups(['building:read'])] + #[Groups(['building:read', 'building_case:read'])] #[SerializedName('caseNumber')] private ?int $case_number = null; #[ORM\Column(length: 255)] - #[Groups(['building:read'])] + #[Groups(['building:read', 'building_case:read'])] private ?string $code = null; #[ORM\Column] - #[Groups(['building:read'])] + #[Groups(['building:read', 'building_case:read'])] private ?int $capacity = null; /** @@ -62,16 +62,15 @@ class BuildingCase private Collection $id_case_position; #[ORM\ManyToOne(inversedBy: 'buildingCases')] + #[Groups(['building_case:read'])] + #[SerializedName('building')] private ?Building $id_building = null; - #[ORM\ManyToOne(inversedBy: 'id_case')] - #[Groups(['building:read'])] - private ?Statut $statut = null; - /** * @var Collection */ #[ORM\OneToMany(targetEntity: Bovine::class, mappedBy: 'buildingCase')] + #[Groups(['building_case:read'])] private Collection $bovines; public function __construct() @@ -170,16 +169,17 @@ class BuildingCase return $this; } - public function getStatut(): ?Statut + /** + * @return array{label: string, couleur: string} + */ + #[Groups(['building:read', 'building_case:read'])] + public function getStatut(): array { - return $this->statut; - } + if ($this->bovines->count() > 0) { + return ['label' => 'Occupé', 'couleur' => '#3A506B']; + } - public function setStatut(?Statut $statut): static - { - $this->statut = $statut; - - return $this; + return ['label' => 'Libre', 'couleur' => '#A3B18A']; } /** diff --git a/src/Entity/Statut.php b/src/Entity/Statut.php deleted file mode 100644 index c32d60f..0000000 --- a/src/Entity/Statut.php +++ /dev/null @@ -1,138 +0,0 @@ - '\d+'], - normalizationContext: ['groups' => ['building:read']], - ), - new GetCollection( - normalizationContext: ['groups' => ['building:read']], - ), - ], - security: "is_granted('ROLE_USER')", -)] -class Statut -{ - #[ORM\Id] - #[ORM\GeneratedValue] - #[ORM\Column] - #[Groups(['building:read'])] - private ?int $id = null; - - #[ORM\Column(length: 255)] - #[Groups(['building:read'])] - private ?string $label = null; - - #[ORM\Column(length: 255)] - #[Groups(['building:read'])] - private ?string $code = null; - - #[ORM\Column(length: 255)] - #[Groups(['building:read'])] - #[SerializedName('couleur')] - private ?string $color = null; - - /** - * @var Collection - */ - #[ORM\OneToMany(targetEntity: BuildingCase::class, mappedBy: 'statut')] - private Collection $id_case; - - public function __construct() - { - $this->id_case = new ArrayCollection(); - } - - public function getId(): ?int - { - return $this->id; - } - - public function setId(int $id): static - { - $this->id = $id; - - return $this; - } - - public function getLabel(): ?string - { - return $this->label; - } - - public function setLabel(string $label): static - { - $this->label = $label; - - return $this; - } - - public function getCode(): ?string - { - return $this->code; - } - - public function setCode(string $code): static - { - $this->code = $code; - - return $this; - } - - public function getColor(): ?string - { - return $this->color; - } - - public function setColor(string $color): static - { - $this->color = $color; - - return $this; - } - - /** - * @return Collection - */ - public function getIdCase(): Collection - { - return $this->id_case; - } - - public function addIdCase(BuildingCase $idCase): static - { - if (!$this->id_case->contains($idCase)) { - $this->id_case->add($idCase); - $idCase->setStatut($this); - } - - return $this; - } - - public function removeIdCase(BuildingCase $idCase): static - { - if ($this->id_case->removeElement($idCase)) { - // set the owning side to null (unless already changed) - if ($idCase->getStatut() === $this) { - $idCase->setStatut(null); - } - } - - return $this; - } -} diff --git a/src/State/BovineProcessor.php b/src/State/BovineProcessor.php new file mode 100644 index 0000000..f6bd3d5 --- /dev/null +++ b/src/State/BovineProcessor.php @@ -0,0 +1,68 @@ +getNationalNumber()) { + $this->enrichFromEdnotif($data); + } + + return $this->persistProcessor->process($data, $operation, $uriVariables, $context); + } + + private function enrichFromEdnotif(Bovine $bovine): void + { + try { + $animalFile = $this->bovinApi->getAnimalFile( + nationalNumber: $bovine->getNationalNumber(), + countryCode: 'FR', + ); + + $identification = $animalFile->identification; + if (null === $identification) { + return; + } + + $bovine->setWorkNumber($identification->workNumber); + $bovine->setBirthDate($identification->birthDate?->date); + $bovine->setBreedCode($this->normalizeBreedCode($identification->breedType)); + } catch (Throwable) { + // External service unavailable — persist bovine without enrichment. + } + } + + private function normalizeBreedCode(mixed $breedType): ?string + { + if (null === $breedType) { + return null; + } + + if (is_numeric($breedType)) { + return (string) $breedType; + } + + if (is_string($breedType) && preg_match('/\d+/', $breedType, $matches)) { + return $matches[0]; + } + + return null; + } +} diff --git a/src/State/BuildingCaseWeightsReportProvider.php b/src/State/BuildingCaseWeightsReportProvider.php index 45584b0..92ed3c2 100644 --- a/src/State/BuildingCaseWeightsReportProvider.php +++ b/src/State/BuildingCaseWeightsReportProvider.php @@ -11,10 +11,8 @@ use App\Entity\BuildingCase; use DateTimeImmutable; use Doctrine\ORM\EntityManagerInterface; use Dompdf\Dompdf; -use Malio\EdnotifBundle\Bovin\Api\BovinApiInterface; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; -use Throwable; use Twig\Environment; use Twig\Error\LoaderError; use Twig\Error\RuntimeError; @@ -40,7 +38,6 @@ final readonly class BuildingCaseWeightsReportProvider implements ProviderInterf public function __construct( private Environment $twig, private EntityManagerInterface $entityManager, - private BovinApiInterface $bovinApi, ) {} /** @@ -68,24 +65,9 @@ final readonly class BuildingCaseWeightsReportProvider implements ProviderInterf continue; } - $workNumber = null; - $birthDate = null; - $breedCode = null; - - try { - $animalFileDto = $this->bovinApi->getAnimalFile( - nationalNumber: $bovine->getNationalNumber(), - countryCode: 'FR', - ); - - $workNumber = $animalFileDto->identification?->workNumber; - $birthDate = $animalFileDto->identification?->birthDate?->date?->format('d/m/y'); - $breedCode = $this->normalizeBreedCode($animalFileDto->identification?->breedType); - if (null === $headerBreedCode && null !== $breedCode) { - $headerBreedCode = $breedCode; - } - } catch (Throwable) { - // Keep row data even if external identification service is unavailable. + $breedCode = $bovine->getBreedCode(); + if (null === $headerBreedCode && null !== $breedCode) { + $headerBreedCode = $breedCode; } $arrivalDate = $bovine->getArrivalDate(); @@ -101,8 +83,8 @@ final readonly class BuildingCaseWeightsReportProvider implements ProviderInterf $rows[] = [ 'nationalNumber' => $bovine->getNationalNumber(), - 'workNumber' => $workNumber, - 'birthDate' => $birthDate, + 'workNumber' => $bovine->getWorkNumber(), + 'birthDate' => $bovine->getBirthDate()?->format('d/m/y'), 'receivedWeight' => $bovine->getReceivedWeight(), 'arrivalDate' => $bovine->getArrivalDate()?->format('d/m/Y'), 'projectedWeights' => $projectedWeights, @@ -131,23 +113,6 @@ final readonly class BuildingCaseWeightsReportProvider implements ProviderInterf ]); } - private function normalizeBreedCode(mixed $breedType): ?string - { - if (null === $breedType) { - return null; - } - - if (is_numeric($breedType)) { - return (string) $breedType; - } - - if (is_string($breedType) && preg_match('/\d+/', $breedType, $matches)) { - return $matches[0]; - } - - return null; - } - private function resolveDailyGainKg(?string $breedCode): float { return 1.3; diff --git a/templates/case_weights_report.html.twig b/templates/case_weights_report.html.twig index 6ee9c8e..17dbd29 100644 --- a/templates/case_weights_report.html.twig +++ b/templates/case_weights_report.html.twig @@ -265,23 +265,15 @@ TABLEAU PRINCIPAL ========================= --> - - - - - {% for month in monthHeaders %} - - {% endfor %} - - - - - + + + + {% for month in monthHeaders|default([]) %} - + {% endfor %} @@ -315,6 +307,7 @@ {% set baseWeight = row ? (row.receivedWeight ?? null) : null %} +
N° de
travail
Poids
(kg)
Date de
naissance
N° de
travail
N° de
travail
Poids
(kg)
Date de
naissance
{{ month.name }}{{ month.name }}
{{ row ? (row.workNumber ?? '') : '' }} {{ baseWeight ?? '' }}