feat(transport) : exclut les courtiers du select clients (filtre excludeCategoryCode)
This commit is contained in:
@@ -316,9 +316,9 @@ const clientOptions = ref<SelectOption[]>([])
|
||||
const supplierOptions = ref<SelectOption[]>([])
|
||||
const siteOptions = ref<SelectOption[]>([])
|
||||
|
||||
async function loadOptions(url: string, target: typeof clientOptions, labelOf: (m: Record<string, unknown>) => string): Promise<void> {
|
||||
async function loadOptions(url: string, target: typeof clientOptions, labelOf: (m: Record<string, unknown>) => string, extraParams: Record<string, string> = {}): Promise<void> {
|
||||
try {
|
||||
const data = await api.get<{ member?: Record<string, unknown>[] }>(url, { pagination: 'false' }, { headers: { Accept: 'application/ld+json' }, toast: false })
|
||||
const data = await api.get<{ member?: Record<string, unknown>[] }>(url, { pagination: 'false', ...extraParams }, { headers: { Accept: 'application/ld+json' }, toast: false })
|
||||
target.value = (data.member ?? []).map(m => ({ value: String(m['@id']), label: labelOf(m) }))
|
||||
}
|
||||
catch {
|
||||
@@ -348,7 +348,8 @@ onMounted(async () => {
|
||||
}
|
||||
}
|
||||
loadCountries().catch(() => {})
|
||||
void loadOptions('/clients', clientOptions, m => String(m.companyName ?? m['@id']))
|
||||
// Exclut les courtiers (catégorie COURTIER) du select clients du module Transport.
|
||||
void loadOptions('/clients', clientOptions, m => String(m.companyName ?? m['@id']), { excludeCategoryCode: 'COURTIER' })
|
||||
void loadOptions('/suppliers', supplierOptions, m => String(m.companyName ?? m['@id']))
|
||||
void loadOptions('/sites', siteOptions, m => String(m.name ?? m['@id']))
|
||||
})
|
||||
|
||||
@@ -439,11 +439,12 @@ async function loadOptions(
|
||||
url: string,
|
||||
target: typeof clientOptions,
|
||||
labelOf: (m: Record<string, unknown>) => string,
|
||||
extraParams: Record<string, string> = {},
|
||||
): Promise<void> {
|
||||
try {
|
||||
const data = await api.get<{ member?: Record<string, unknown>[] }>(
|
||||
url,
|
||||
{ pagination: 'false' },
|
||||
{ pagination: 'false', ...extraParams },
|
||||
{ headers: { Accept: 'application/ld+json' }, toast: false },
|
||||
)
|
||||
target.value = (data.member ?? []).map(m => ({ value: String(m['@id']), label: labelOf(m) }))
|
||||
@@ -455,7 +456,8 @@ async function loadOptions(
|
||||
|
||||
/** Charge les référentiels de l'onglet Prix (non bloquant : selects vides si échec). */
|
||||
function loadPriceReferentials(): void {
|
||||
void loadOptions('/clients', clientOptions, m => String(m.companyName ?? m['@id']))
|
||||
// Exclut les courtiers (catégorie COURTIER) du select clients du module Transport.
|
||||
void loadOptions('/clients', clientOptions, m => String(m.companyName ?? m['@id']), { excludeCategoryCode: 'COURTIER' })
|
||||
void loadOptions('/suppliers', supplierOptions, m => String(m.companyName ?? m['@id']))
|
||||
void loadOptions('/sites', siteOptions, m => String(m.name ?? m['@id']))
|
||||
}
|
||||
|
||||
@@ -28,6 +28,10 @@ interface ClientRepositoryInterface
|
||||
* dont le code est dans la liste (OR — ERP-78). Liste vide = pas de filtre.
|
||||
* - $siteIds : restreint aux clients ayant au moins une adresse rattachee a
|
||||
* l'un des sites donnes (OR — RG-1.10). Liste vide = pas de filtre.
|
||||
* - $excludeCategoryCodes : EXCLUT les clients possedant au moins une
|
||||
* categorie dont le code est dans la liste (NOT IN). Liste vide = pas de
|
||||
* filtre. Utilise par le module Transport pour ecarter les courtiers
|
||||
* (code COURTIER) des selects clients.
|
||||
*
|
||||
* Filtrage centralise ICI (et non dans les providers/controllers) pour que
|
||||
* la liste paginee (ClientProvider) et l'export (ClientExportController)
|
||||
@@ -41,6 +45,7 @@ interface ClientRepositoryInterface
|
||||
*
|
||||
* @param list<string> $categoryCodes
|
||||
* @param list<int> $siteIds
|
||||
* @param list<string> $excludeCategoryCodes
|
||||
*/
|
||||
public function createListQueryBuilder(
|
||||
bool $includeArchived = false,
|
||||
@@ -48,6 +53,7 @@ interface ClientRepositoryInterface
|
||||
array $categoryCodes = [],
|
||||
array $siteIds = [],
|
||||
bool $archivedOnly = false,
|
||||
array $excludeCategoryCodes = [],
|
||||
): QueryBuilder;
|
||||
|
||||
/**
|
||||
|
||||
@@ -25,6 +25,8 @@ use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
||||
* - tri par defaut companyName ASC — RG-1.26 ;
|
||||
* - filtres ?search=... (fuzzy companyName + lastName + email) et
|
||||
* ?categoryCode=<code> (clients ayant >= 1 categorie de ce code — ERP-78) ;
|
||||
* - ?excludeCategoryCode=<code> : EXCLUT les clients ayant >= 1 categorie de ce
|
||||
* code (NOT IN — utilise par le module Transport pour ecarter les courtiers) ;
|
||||
* - pagination obligatoire (convention Starseed ERP-72) : Paginator ORM ;
|
||||
* echappatoire ?pagination=false pour alimenter un <select> sans pagination.
|
||||
*
|
||||
@@ -70,6 +72,10 @@ final class ClientProvider implements ProviderInterface
|
||||
// RG-1.03) OU une liste (?categoryCode[]=A&categoryCode[]=B, drawer multi).
|
||||
$categoryCodes = $this->readStringList($filters['categoryCode'] ?? []);
|
||||
$siteIds = $this->readIntList($filters['siteId'] ?? []);
|
||||
// excludeCategoryCode : EXCLUT les clients ayant ce(s) code(s) de categorie.
|
||||
// Le module Transport l'utilise pour ecarter les courtiers (COURTIER) de
|
||||
// ses selects clients.
|
||||
$excludeCategoryCodes = $this->readStringList($filters['excludeCategoryCode'] ?? []);
|
||||
|
||||
// Filtrage delegue au repository (logique partagee avec l'export XLSX).
|
||||
$qb = $this->repository->createListQueryBuilder(
|
||||
@@ -78,6 +84,7 @@ final class ClientProvider implements ProviderInterface
|
||||
$categoryCodes,
|
||||
$siteIds,
|
||||
$archivedOnly,
|
||||
$excludeCategoryCodes,
|
||||
);
|
||||
|
||||
// Echappatoire ?pagination=false : collection complete sans Paginator
|
||||
|
||||
@@ -37,6 +37,7 @@ class DoctrineClientRepository extends ServiceEntityRepository implements Client
|
||||
array $categoryCodes = [],
|
||||
array $siteIds = [],
|
||||
bool $archivedOnly = false,
|
||||
array $excludeCategoryCodes = [],
|
||||
): QueryBuilder {
|
||||
// SELECTION uniquement (filtres + tri) : pas de fetch-join to-many ici.
|
||||
// L'hydratation des collections affichees (Catégories / Site(s)) est
|
||||
@@ -57,6 +58,7 @@ class DoctrineClientRepository extends ServiceEntityRepository implements Client
|
||||
|
||||
$this->applySearch($qb, $search);
|
||||
$this->applyCategoryCodes($qb, $categoryCodes);
|
||||
$this->applyExcludeCategoryCodes($qb, $excludeCategoryCodes);
|
||||
$this->applySiteIds($qb, $siteIds);
|
||||
|
||||
return $qb;
|
||||
@@ -151,6 +153,35 @@ class DoctrineClientRepository extends ServiceEntityRepository implements Client
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* EXCLUT les clients possedant au moins une categorie dont le code figure
|
||||
* dans la liste (NOT IN). Miroir negatif d'{@see self::applyCategoryCodes()} :
|
||||
* utilise par le module Transport pour ecarter les courtiers (code COURTIER)
|
||||
* des selects clients, sans dependre du nombre de categories d'un client (un
|
||||
* client [COURTIER, DISTRIBUTEUR] est bien exclu). Sous-requete NOT IN pour ne
|
||||
* pas perturber le DISTINCT / ORDER BY principal.
|
||||
*
|
||||
* @param list<string> $excludeCategoryCodes
|
||||
*/
|
||||
private function applyExcludeCategoryCodes(QueryBuilder $qb, array $excludeCategoryCodes): void
|
||||
{
|
||||
$codes = $this->normalizeStringList($excludeCategoryCodes);
|
||||
if ([] === $codes) {
|
||||
return;
|
||||
}
|
||||
|
||||
$sub = $this->getEntityManager()->createQueryBuilder()
|
||||
->select('c3.id')
|
||||
->from(Client::class, 'c3')
|
||||
->join('c3.categories', 'cat3')
|
||||
->where('cat3.code IN (:excludeCategoryCodes)')
|
||||
;
|
||||
|
||||
$qb->andWhere($qb->expr()->notIn('c.id', $sub->getDQL()))
|
||||
->setParameter('excludeCategoryCodes', $codes)
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* Restreint aux clients ayant au moins une adresse rattachee a l'un des
|
||||
* sites donnes (OR — RG-1.10 : les sites vivent sur les adresses, pas sur le
|
||||
|
||||
@@ -355,6 +355,29 @@ final class ClientApiTest extends AbstractCommercialApiTestCase
|
||||
self::assertNotContains('FILTRE SECTEUR CO', $names);
|
||||
}
|
||||
|
||||
/**
|
||||
* Le module Transport ecarte les courtiers de ses selects clients via
|
||||
* ?excludeCategoryCode=COURTIER : tout client portant la categorie COURTIER
|
||||
* est exclu (NOT IN), y compris s'il porte EN PLUS une autre categorie.
|
||||
*/
|
||||
public function testListExcludeCategoryCodeRemovesBrokers(): void
|
||||
{
|
||||
$client = $this->createAdminClient();
|
||||
$this->seedClient('Exclu Distrib Co', false, 'DISTRIBUTEUR');
|
||||
$this->seedClient('Exclu Courtier Co', false, 'COURTIER');
|
||||
// Client multi-categories DISTRIBUTEUR + COURTIER : doit etre exclu malgre
|
||||
// sa categorie DISTRIBUTEUR (l'exclusion porte sur « possede COURTIER »).
|
||||
$mixed = $this->seedClient('Exclu Mixte Co', false, 'DISTRIBUTEUR');
|
||||
$mixed->addCategory($this->createCategory('COURTIER'));
|
||||
$this->getEm()->flush();
|
||||
|
||||
$names = $this->companyNames($client, '/api/clients?pagination=false&excludeCategoryCode=COURTIER');
|
||||
|
||||
self::assertContains('EXCLU DISTRIB CO', $names);
|
||||
self::assertNotContains('EXCLU COURTIER CO', $names);
|
||||
self::assertNotContains('EXCLU MIXTE CO', $names);
|
||||
}
|
||||
|
||||
/**
|
||||
* ERP-62 (drawer) : filtre Sites (?siteId[]=X) — clients ayant >= 1 adresse
|
||||
* rattachee au site donne.
|
||||
|
||||
Reference in New Issue
Block a user