feat(fournisseurs) : pagination serveur + search multi-champs (name/email/telephone) + filtre catégorie + tri
Some checks failed
Auto Tag Develop / tag (push) Has been cancelled
Some checks failed
Auto Tag Develop / tag (push) Has been cancelled
Backend
- Nouveau ConstructeurSearchFilter : LIKE insensible casse sur name/email + LEFT JOIN telephones.numero, accessible via ?search=
- Constructeur entity : ApiFilter ConstructeurSearchFilter, SearchFilter (categories.id exact), OrderFilter (name, email, createdAt)
- paginationMaximumItemsPerPage 200 -> 2000 (pour ConstructeurSelect et MachineDetail qui chargent l'ensemble en cache)
Frontend
- useConstructeurs : nouvelle fonction fetchConstructeursPage({ page, itemsPerPage, search, categoryId, orderField, orderDirection }) renvoyant { items, totalItems, totalPages, currentPage }
- constructeurs.vue : suppression du filtre/tri client, état page/perPage/totalItems/totalPages, watchers sur search/filter/sort qui reset page=1 et rechargent, prop pagination du DataTable câblée, recharge après create/update/delete
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -4,6 +4,9 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use ApiPlatform\Doctrine\Orm\Filter\OrderFilter;
|
||||
use ApiPlatform\Doctrine\Orm\Filter\SearchFilter;
|
||||
use ApiPlatform\Metadata\ApiFilter;
|
||||
use ApiPlatform\Metadata\ApiResource;
|
||||
use ApiPlatform\Metadata\Delete;
|
||||
use ApiPlatform\Metadata\Get;
|
||||
@@ -12,6 +15,7 @@ use ApiPlatform\Metadata\Patch;
|
||||
use ApiPlatform\Metadata\Post;
|
||||
use ApiPlatform\Metadata\Put;
|
||||
use App\Entity\Trait\CuidEntityTrait;
|
||||
use App\Filter\ConstructeurSearchFilter;
|
||||
use App\Repository\ConstructeurRepository;
|
||||
use DateTimeImmutable;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
@@ -26,6 +30,9 @@ use Symfony\Component\Validator\Constraints as Assert;
|
||||
#[ORM\Entity(repositoryClass: ConstructeurRepository::class)]
|
||||
#[ORM\Table(name: 'constructeurs')]
|
||||
#[ORM\HasLifecycleCallbacks]
|
||||
#[ApiFilter(ConstructeurSearchFilter::class)]
|
||||
#[ApiFilter(SearchFilter::class, properties: ['categories.id' => 'exact'])]
|
||||
#[ApiFilter(OrderFilter::class, properties: ['name', 'email', 'createdAt'])]
|
||||
#[ApiResource(
|
||||
description: 'Fournisseurs et constructeurs. Référentiel partagé entre les machines, pièces, composants et produits pour identifier les fabricants et distributeurs.',
|
||||
operations: [
|
||||
@@ -37,7 +44,7 @@ use Symfony\Component\Validator\Constraints as Assert;
|
||||
new Delete(security: "is_granted('ROLE_GESTIONNAIRE')"),
|
||||
],
|
||||
paginationClientItemsPerPage: true,
|
||||
paginationMaximumItemsPerPage: 200,
|
||||
paginationMaximumItemsPerPage: 2000,
|
||||
normalizationContext: ['groups' => ['constructeur:read']],
|
||||
denormalizationContext: ['groups' => ['constructeur:write']]
|
||||
)]
|
||||
|
||||
59
src/Filter/ConstructeurSearchFilter.php
Normal file
59
src/Filter/ConstructeurSearchFilter.php
Normal file
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Filter;
|
||||
|
||||
use ApiPlatform\Doctrine\Orm\Filter\AbstractFilter;
|
||||
use ApiPlatform\Doctrine\Orm\Util\QueryNameGeneratorInterface;
|
||||
use ApiPlatform\Metadata\Operation;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
|
||||
/**
|
||||
* Search filter pour Constructeur : LIKE insensible à la casse sur name, email
|
||||
* + LEFT JOIN sur la collection telephones pour matcher aussi sur telephone.numero.
|
||||
* Param query : ?search=...
|
||||
*/
|
||||
final class ConstructeurSearchFilter extends AbstractFilter
|
||||
{
|
||||
public function getDescription(string $resourceClass): array
|
||||
{
|
||||
return [
|
||||
'search' => [
|
||||
'property' => null,
|
||||
'type' => 'string',
|
||||
'required' => false,
|
||||
'description' => 'Recherche dans le nom, l\'email et les numéros de téléphone du fournisseur.',
|
||||
'openapi' => [
|
||||
'allowEmptyValue' => true,
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
protected function filterProperty(string $property, mixed $value, QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, ?Operation $operation = null, array $context = []): void
|
||||
{
|
||||
if ('search' !== $property || !is_string($value) || '' === trim($value)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$alias = $queryBuilder->getRootAliases()[0];
|
||||
$telAlias = $queryNameGenerator->generateJoinAlias('telephones');
|
||||
$paramName = $queryNameGenerator->generateParameterName('search');
|
||||
$likePattern = '%'.mb_strtolower(trim($value)).'%';
|
||||
|
||||
$queryBuilder
|
||||
->leftJoin(sprintf('%s.telephones', $alias), $telAlias)
|
||||
->andWhere(sprintf(
|
||||
'LOWER(%1$s.name) LIKE :%4$s OR LOWER(%1$s.email) LIKE :%4$s OR LOWER(%2$s.numero) LIKE :%4$s',
|
||||
$alias,
|
||||
$telAlias,
|
||||
'',
|
||||
$paramName,
|
||||
))
|
||||
->setParameter($paramName, $likePattern)
|
||||
;
|
||||
|
||||
$queryBuilder->groupBy(sprintf('%s.id', $alias));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user