[ERP-62] Page Répertoire clients (datatable + Ajouter / Exporter) (#44)
Auto Tag Develop / tag (push) Successful in 8s
Auto Tag Develop / tag (push) Successful in 8s
## ERP-62 — Page Répertoire clients (datatable + Ajouter / Exporter) Tâche Lesstime #480. **Stacke sur ERP-61** (clés i18n `commercial.clients.*`) — non encore mergé : la diff vers `develop` inclut le commit ERP-61 tant qu'il n'est pas mergé. ### Front - Page `/clients` (route à plat) : `MalioDataTable` 6 colonnes (Nom entreprise / Contact / Téléphone formaté / Email / codes Catégories / badges Site(s)), toggle « Voir les archivés » (état 100 % local), boutons **+ Ajouter** (visible si `commercial.clients.manage`) et **Exporter** (visible si `view`, télécharge `clients/export.xlsx` via `useApi`), clic ligne → `/clients/{id}`, empty state. - Composable `useClientsRepository` = wrapper de `usePaginatedList<Client>({ url: '/clients' })` + toggle `includeArchived` (repasse page 1). - Util `formatPhoneFR` (signature cible à coordonner avec ERP-66 / 1.13) + clé i18n `showArchived`. ### Back — ⚠️ MAJ contrat de sérialisation (incluse dans cette MR) Le `GET /api/clients` n'exposait ni les codes catégories ni les sites en liste (le bloc Lesstime l'affirmait à tort). Corrigé : - `Client` : `category:read` + `site:read` ajoutés aux `normalizationContext` (GetCollection/Get/Post/Patch) + accesseur agrégé `getSites()` (`#[Groups(client:read)]`). - `DoctrineClientRepository::createListQueryBuilder` : jointures + `addSelect` (categories / addresses / sites) anti N+1. - Aucune migration (pure sérialisation). ### Tests - Back : `ClientApiTest` (codes catégories + sites name/color en liste). `make test` ✅ 454. - Front : `useClientsRepository.spec.ts` + `phone.test.ts`. `vitest` ✅ 111. `nuxi typecheck` ✅ (mes fichiers). ### Non couvert Golden path navigateur non joué : dev-nuxt (conteneur) cassé (résolution `@malio/layer-ui/tailwind.config.ts`) + BDD sans clients démo (nécessite `make db-reset`). Aspects front restants traités séparément. --------- Co-authored-by: Matthieu <contact@malio.fr> Reviewed-on: #44 Co-authored-by: tristan <tristan@yuno.malio.fr> Co-committed-by: tristan <tristan@yuno.malio.fr>
This commit was merged in pull request #44.
This commit is contained in:
@@ -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(
|
||||
@@ -86,7 +91,7 @@ use Symfony\Component\Validator\Constraints as Assert;
|
||||
),
|
||||
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,
|
||||
),
|
||||
@@ -104,7 +109,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',
|
||||
@@ -659,6 +664,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);
|
||||
}
|
||||
|
||||
// Embed gate sur le groupe COMPTABLE (et non client:item:read comme contacts/
|
||||
// adresses) : client:read:accounting n'est ajoute au contexte que si l'user a
|
||||
// accounting.view (ClientReadGroupContextBuilder). Resultat : la cle `ribs` est
|
||||
|
||||
Reference in New Issue
Block a user