Files
Ferme/src/Command/BackfillBovineMovementsCommand.php
tristan de76a77120 feat(bovine) : suivi des mouvements internes (bâtiment/case)
- Entité BovineMovement (bovine, buildingCase|building, enteredAt, leftAt) + relation OneToMany sur Bovine ordonnée DESC
- Endpoint POST /api/bovine_movements via BovineMovementProcessor : ferme le mouvement courant, ouvre le nouveau, synchronise bovine.buildingCase
- Commande idempotente app:backfill-bovine-movements pour initialiser l'historique des bovins existants
- Onglet Mouvement de la page Vie du bovin : form 3 colonnes (style admin) + UiDataTable avec filtres header (Bâtiment, Case actifs ; Du/Au/Durée désactivés)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 14:45:35 +02:00

94 lines
2.7 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Command;
use App\Entity\Bovine;
use App\Entity\BovineMovement;
use DateTimeImmutable;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use function count;
#[AsCommand(
name: 'app:backfill-bovine-movements',
description: 'Crée un mouvement initial pour chaque bovin ayant une case ou un bâtiment mais aucun mouvement enregistré.'
)]
class BackfillBovineMovementsCommand extends Command
{
private const FLUSH_EVERY = 100;
public function __construct(
private readonly EntityManagerInterface $entityManager,
) {
parent::__construct();
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
$bovines = $this->entityManager->createQueryBuilder()
->select('b')
->from(Bovine::class, 'b')
->where('b.buildingCase IS NOT NULL OR b.building IS NOT NULL')
->andWhere('NOT EXISTS (SELECT 1 FROM '.BovineMovement::class.' m WHERE m.bovine = b)')
->getQuery()
->getResult()
;
$total = count($bovines);
if (0 === $total) {
$io->success('Aucun bovin à backfiller.');
return Command::SUCCESS;
}
$io->info(sprintf('%d bovin(s) à backfiller.', $total));
$now = new DateTimeImmutable();
$created = 0;
$fallback = 0;
foreach ($bovines as $i => $bovine) {
$movement = new BovineMovement();
$movement->setBovine($bovine);
if (null !== $bovine->getBuildingCase()) {
$movement->setBuildingCase($bovine->getBuildingCase());
} else {
$movement->setBuilding($bovine->getBuilding());
}
$enteredAt = $bovine->getArrivalDate();
if (null === $enteredAt) {
$enteredAt = $now;
++$fallback;
}
$movement->setEnteredAt($enteredAt);
$this->entityManager->persist($movement);
++$created;
if (0 === ($i + 1) % self::FLUSH_EVERY) {
$this->entityManager->flush();
}
}
$this->entityManager->flush();
$io->success(sprintf('%d mouvement(s) créé(s).', $created));
if ($fallback > 0) {
$io->warning(sprintf("%d bovin(s) sans date d'arrivée → enteredAt = maintenant.", $fallback));
}
return Command::SUCCESS;
}
}