feat : Reception.validatedAt + statut entrées + mode consultation

- Backend : champ Reception.validatedAt (timestamp) avec PreUpdate + helpers
  isFullyConfirmed/tryValidate ; sync EDNOTIF déclenche tryValidate sur
  les receptions impactées ; expose Supplier.name dans le groupe bovine:read.
- Migration : ajout colonne validated_at sans backfill (les receptions
  remontent en attente jusqu'au prochain sync).
- Front /entry-exit : remplace Historique par 'Entrées validées' (filtre
  exists[validatedAt]=true), ajoute filtres et colonne Statut sur les
  deux tableaux, retire Fournisseur, layout 2x2 (entrées + sorties).
- Front /entry-exit/entry/{id} : mode consultation quand entryCompleted=true
  (formulaire + actions masqués, colonne EDNOTIF par bovin) ; ajoute
  colonnes Vendeur et Cause dans le récap.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-30 16:31:16 +02:00
parent 476502c91c
commit 8f88abab46
9 changed files with 400 additions and 240 deletions

View File

@@ -118,6 +118,7 @@ class Bovine
#[ORM\ManyToOne]
#[Groups(['bovine:read', 'bovine:write', 'building_case:read'])]
#[ApiProperty(readableLink: true)]
private ?Supplier $supplier = null;
#[ORM\Column(length: 50, nullable: true)]

View File

@@ -6,6 +6,7 @@ namespace App\Entity;
use ApiPlatform\Doctrine\Orm\Filter\BooleanFilter;
use ApiPlatform\Doctrine\Orm\Filter\DateFilter;
use ApiPlatform\Doctrine\Orm\Filter\ExistsFilter;
use ApiPlatform\Doctrine\Orm\Filter\SearchFilter;
use ApiPlatform\Metadata\ApiFilter;
use ApiPlatform\Metadata\ApiProperty;
@@ -32,6 +33,7 @@ use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer;
#[ORM\HasLifecycleCallbacks]
#[ORM\Table(name: 'reception')]
#[ApiFilter(BooleanFilter::class, properties: ['isValid', 'entryCompleted'])]
#[ApiFilter(ExistsFilter::class, properties: ['validatedAt'])]
#[ApiFilter(SearchFilter::class, properties: [
'identificationNumber' => 'ipartial',
'supplier.name' => 'ipartial',
@@ -115,6 +117,10 @@ class Reception
#[Groups(['reception:read', 'reception:write', 'reception-bovine:read'])]
private bool $entryCompleted = false;
#[ORM\Column(type: 'datetime_immutable', nullable: true)]
#[Groups(['reception:read', 'reception-bovine:read'])]
private ?DateTimeImmutable $validatedAt = null;
#[ORM\Column(name: 'date_reception', type: 'datetime_immutable')]
#[Groups(['reception:read', 'reception:write', 'reception-bovine:read'])]
#[Context(
@@ -295,6 +301,45 @@ class Reception
return $this;
}
public function getValidatedAt(): ?DateTimeImmutable
{
return $this->validatedAt;
}
public function setValidatedAt(?DateTimeImmutable $validatedAt): self
{
$this->validatedAt = $validatedAt;
return $this;
}
public function isFullyConfirmed(): bool
{
if ($this->bovines->isEmpty()) {
return false;
}
foreach ($this->bovines as $bovine) {
if (null === $bovine->getEdnotifConfirmedAt()) {
return false;
}
}
return true;
}
public function tryValidate(): void
{
if ($this->entryCompleted && null === $this->validatedAt && $this->isFullyConfirmed()) {
$this->validatedAt = new DateTimeImmutable();
}
}
#[ORM\PreUpdate]
public function onPreUpdate(): void
{
$this->tryValidate();
}
#[Groups(['reception:read'])]
public function getRegisteredBovineCount(): int
{

View File

@@ -54,11 +54,11 @@ class Supplier
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
#[Groups(['supplier:read', 'reception:read'])]
#[Groups(['supplier:read', 'reception:read', 'bovine:read'])]
private ?int $id = null;
#[ORM\Column(length: 180)]
#[Groups(['supplier:read', 'reception:read', 'supplier:write'])]
#[Groups(['supplier:read', 'reception:read', 'supplier:write', 'bovine:read'])]
private string $name = '';
#[ORM\Column(length: 180, nullable: true)]

View File

@@ -52,7 +52,8 @@ final class BovineSyncInventoryProcessor implements ProcessorInterface
$existingByNationalNumber[$bovine->getNationalNumber()] = $bovine;
}
$seen = [];
$seen = [];
$impactedReceptions = [];
foreach ($inventory->animals as $animal) {
$nationalNumber = $animal->identification?->bovin?->nationalNumber;
if (null === $nationalNumber || '' === $nationalNumber) {
@@ -77,9 +78,17 @@ final class BovineSyncInventoryProcessor implements ProcessorInterface
// voit ce bovin remonter dans l'inventaire.
if (null === $bovine->getEdnotifConfirmedAt()) {
$bovine->setEdnotifConfirmedAt(new DateTimeImmutable());
$reception = $bovine->getReception();
if (null !== $reception) {
$impactedReceptions[$reception->getId()] = $reception;
}
}
}
foreach ($impactedReceptions as $reception) {
$reception->tryValidate();
}
$now = new DateTimeImmutable();
foreach ($existingByNationalNumber as $nationalNumber => $bovine) {
if (isset($seen[$nationalNumber])) {