- 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>
94 lines
2.7 KiB
PHP
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;
|
|
}
|
|
}
|