feat(front) : page répertoire clients + datatable
Pull Request — Quality gate / Backend (PHP CS + PHPUnit) (pull_request) Successful in 1m47s
Pull Request — Quality gate / Frontend (lint + Vitest + build) (pull_request) Successful in 1m15s

- Page /clients (route à plat) : MalioDataTable 6 colonnes (contact, téléphone
  formaté, codes catégories, badges sites), toggle « Voir les archivés » (état
  local), boutons Ajouter (manage) / Exporter (view, download xlsx), clic ligne
  vers le détail, empty state.
- Composable useClientsRepository (wrapper de usePaginatedList) + util
  formatPhoneFR + clé i18n showArchived.
- Contrat back : la liste client:read expose désormais les codes catégories
  (category:read) et les sites agrégés des adresses (site:read + Client::getSites) ;
  jointures anti N+1 dans createListQueryBuilder. Tests back + front.
This commit is contained in:
2026-06-02 11:17:22 +02:00
parent a5af1e6108
commit 9ca9cb1d42
9 changed files with 519 additions and 5 deletions
+34 -3
View File
@@ -15,6 +15,7 @@ use App\Module\Commercial\Infrastructure\Doctrine\DoctrineClientRepository;
use App\Shared\Domain\Attribute\Auditable;
use App\Shared\Domain\Contract\BlamableInterface;
use App\Shared\Domain\Contract\CategoryInterface;
use App\Shared\Domain\Contract\SiteInterface;
use App\Shared\Domain\Contract\TimestampableInterface;
use App\Shared\Domain\Trait\TimestampableBlamableTrait;
use DateTimeImmutable;
@@ -58,7 +59,11 @@ use Symfony\Component\Validator\Constraints as Assert;
operations: [
new GetCollection(
security: "is_granted('commercial.clients.view')",
normalizationContext: ['groups' => ['client:read', 'default:read']],
// La liste embarque les categories (avec leur code, groupe
// category:read) et les sites agreges des adresses (groupe
// site:read) pour alimenter les colonnes « Catégories » et
// « Site(s) » du Repertoire (ERP-62). Cf. getSites() plus bas.
normalizationContext: ['groups' => ['client:read', 'default:read', 'category:read', 'site:read']],
provider: ClientProvider::class,
),
new Get(
@@ -72,13 +77,15 @@ use Symfony\Component\Validator\Constraints as Assert;
'client_contact:read',
'client_address:read',
'client_rib:read',
'category:read',
'site:read',
'default:read',
]],
provider: ClientProvider::class,
),
new Post(
security: "is_granted('commercial.clients.manage')",
normalizationContext: ['groups' => ['client:read', 'default:read']],
normalizationContext: ['groups' => ['client:read', 'default:read', 'category:read', 'site:read']],
denormalizationContext: ['groups' => ['client:write:main']],
processor: ClientProcessor::class,
),
@@ -96,7 +103,7 @@ use Symfony\Component\Validator\Constraints as Assert;
// autoriser/refuser onglet par onglet (RG-1.22 / RG-1.28) : les
// champs accounting exigent accounting.manage, isArchived exige
// archive, le reste (main/information) exige manage.
normalizationContext: ['groups' => ['client:read', 'default:read']],
normalizationContext: ['groups' => ['client:read', 'default:read', 'category:read', 'site:read']],
denormalizationContext: ['groups' => [
'client:write:main',
'client:write:information',
@@ -651,6 +658,30 @@ class Client implements TimestampableInterface, BlamableInterface
return $this;
}
/**
* Sites distincts rattaches a au moins une adresse du client (RG-1.10).
* Le Client ne porte pas de sites en propre : ils vivent sur les adresses.
* Agrege en lecture seule pour la colonne « Site(s) » du Repertoire (badges
* colores) — expose en LISTE via le groupe client:read (les adresses
* completes restent reservees au detail, client:item:read).
*
* @return list<SiteInterface>
*/
#[Groups(['client:read'])]
public function getSites(): array
{
$sites = [];
foreach ($this->addresses as $address) {
foreach ($address->getSites() as $site) {
// Deduplication par identite d'objet : un meme site peut etre
// rattache a plusieurs adresses du client.
$sites[spl_object_id($site)] = $site;
}
}
return array_values($sites);
}
/** @return Collection<int, ClientRib> */
#[Groups(['client:item:read'])]
public function getRibs(): Collection