4369c71706
Entité WeighingTicket - Entité métier complète (#[Auditable], TimestampableBlamableTrait, relations ORM Client/Supplier/Site) + contrat de sérialisation à 3 maillons (weighing_ticket:read / :item:read + contextes par opération). - Getters calculés displayDate et plateFreeFormat (#[SerializedName]), sécurité view/manage, pas de Delete/archive. - Validation #[Assert\*] messages FR + #[Assert\Callback] RG-5.03 (->atPath()), libellé i18n audit.entity.logistique_weighingticket. - Repository : interface Domain + DoctrineWeighingTicketRepository (recherche + tri number DESC, deletedAt IS NULL). Dette site.code - Site.code mappé VARCHAR(8) (groupes read/write), dérivation auto au PrePersist (2 premiers chiffres du CP), UniqueConstraint uq_site_code. - Migration Version20260617160000 : ALTER COLUMN code SET NOT NULL + COMMENT. - Fixtures (codes 86/17/82) et SiteApiTest ajustés. Câblage - doctrine.yaml : mapping ORM du module Logistique (absent du scaffold ERP-181). - ColumnCommentsCatalog : site.code + table weighing_ticket. Specs M5 versionnées (spec-back / spec-front / prompts).
74 lines
2.4 KiB
PHP
74 lines
2.4 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Module\Logistique\Infrastructure\Doctrine;
|
|
|
|
use App\Module\Logistique\Domain\Entity\WeighingTicket;
|
|
use App\Module\Logistique\Domain\Repository\WeighingTicketRepositoryInterface;
|
|
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
|
use Doctrine\ORM\QueryBuilder;
|
|
use Doctrine\Persistence\ManagerRegistry;
|
|
|
|
/**
|
|
* @extends ServiceEntityRepository<WeighingTicket>
|
|
*/
|
|
class DoctrineWeighingTicketRepository extends ServiceEntityRepository implements WeighingTicketRepositoryInterface
|
|
{
|
|
public function __construct(ManagerRegistry $registry)
|
|
{
|
|
parent::__construct($registry, WeighingTicket::class);
|
|
}
|
|
|
|
public function findById(int $id): ?WeighingTicket
|
|
{
|
|
return $this->find($id);
|
|
}
|
|
|
|
public function save(WeighingTicket $ticket): void
|
|
{
|
|
$this->getEntityManager()->persist($ticket);
|
|
$this->getEntityManager()->flush();
|
|
}
|
|
|
|
public function createListQueryBuilder(?string $search = null): QueryBuilder
|
|
{
|
|
// Left-join des contreparties pour la recherche par nom (sans cartesien
|
|
// dangereux : ManyToOne). Le cloisonnement par site courant est ajoute
|
|
// par le SiteScopedQueryExtension (§ 2.3). Tri par defaut number DESC.
|
|
$qb = $this->createQueryBuilder('wt')
|
|
->leftJoin('wt.client', 'c')
|
|
->leftJoin('wt.supplier', 's')
|
|
->andWhere('wt.deletedAt IS NULL')
|
|
->orderBy('wt.number', 'DESC')
|
|
;
|
|
|
|
$this->applySearch($qb, $search);
|
|
|
|
return $qb;
|
|
}
|
|
|
|
/**
|
|
* Recherche fuzzy insensible a la casse sur le numero, le nom du client /
|
|
* fournisseur, le libelle « Autre » et l'immatriculation (§ 4.1).
|
|
* Metacaracteres LIKE (%, _, \) echappes pour rester litteraux.
|
|
*/
|
|
private function applySearch(QueryBuilder $qb, ?string $search): void
|
|
{
|
|
if (null === $search || '' === trim($search)) {
|
|
return;
|
|
}
|
|
|
|
$escaped = str_replace(['\\', '%', '_'], ['\\\\', '\%', '\_'], trim($search));
|
|
$pattern = '%'.mb_strtolower($escaped, 'UTF-8').'%';
|
|
|
|
$qb->andWhere(
|
|
'LOWER(wt.number) LIKE :search '
|
|
.'OR LOWER(c.companyName) LIKE :search '
|
|
.'OR LOWER(s.companyName) LIKE :search '
|
|
.'OR LOWER(wt.otherLabel) LIKE :search '
|
|
.'OR LOWER(wt.immatriculation) LIKE :search',
|
|
)->setParameter('search', $pattern);
|
|
}
|
|
}
|