Commit Graph

3 Commits

Author SHA1 Message Date
matthieu 1888b70623 fix(rbac) : référentiels /categories et /sites lisibles par les rôles métier (ERP-102) (#53)
Auto Tag Develop / tag (push) Successful in 11s
## Contexte

ERP-102 — Découvert pendant ERP-64. Connecté avec un rôle **métier** (bureau / compta / commerciale), `GET /api/categories` et `GET /api/sites` renvoient **403**, alors que `/tva_modes`, `/payment_delays`, `/payment_types`, `/banks` renvoient 200.

Conséquences : page **Création client** inutilisable (le `Promise.all` rejetait → **tous** les selects vides) et **filtres Catégories/Sites vides** au répertoire.

## Cause

La `security` des `GetCollection`/`Get` de `Category` et `Site` exigeait `catalog.categories.view` / `sites.view` — permissions d'**administration** du Catalogue / des Sites. Or ces référentiels sont **transverses** : tout rôle qui gère un tiers doit pouvoir les lire.

## Correctif back — Option C (permission de lecture-référentiel dédiée)

Choix d'archi retenu parmi les 3 du ticket :

- **Pourquoi pas A** (`... or is_granted('commercial.clients.view')`) : coupler `Category`/`Site` à une permission **Commercial** viole l'esprit de la règle ABSOLUE n°1 et ne scale pas (M2 Fournisseurs devrait rajouter un OR).
- **Pourquoi pas B** (donner `.view` aux rôles métier) : `.view` = accès admin → items sidebar admin Catégories/Sites exposés à une commerciale.
- **C** : nouvelle permission `catalog.categories.read_ref` / `sites.read_ref`, distincte de `.view` (pas d'item sidebar) et de `.manage`. Chaque permission appartient à **son** module → isolement inter-module préservé, **réutilisable tel quel par M2 Fournisseurs**. C'est la « permission référentiel lisible » que le ticket pointe lui-même.

Détail :
- `CatalogModule` / `SitesModule` : déclaration des deux permissions `read_ref`.
- `Category` / `Site` : security lecture (liste + item) = `view OR read_ref`.
- `RbacSeeder` (matrice § 2.7) : `read_ref` attaché à bureau / compta / commerciale ; usine reste sans accès.

## Durcissement front (résilience — requis dans tous les cas)

`useClientReferentials.loadCommon` : `Promise.all` → **`Promise.allSettled`** avec affectation isolée par référentiel. L'échec d'un endpoint ne vide plus que **son** select, plus la totalité du formulaire.

## Tests (TDD)

- `ClientRBACMatrixTest::testBusinessRolesCanReadCategoriesAndSitesReferentials` — bureau/compta/commerciale listent `/categories` et `/sites` (200), usine reste 403.
- `SitesModuleTest` — set de permissions porté à 4 codes.
- `useClientReferentials.spec` (Vitest) — un référentiel en échec ne vide que son select.

## Vérifications

- `make test` (back) : **467/467** ✓
- `make nuxt-test` (front) : **131/131** ✓
- `make php-cs-fixer` : conforme ✓

## Note miroirs RBAC

`config/sidebar.php` / `personas.ts` / `SeedE2ECommand.php` **non touchés** : `read_ref` n'ajoute aucun item sidebar, le persona E2E `user-full` lit déjà via `.view`, et aucun persona ne modélise un rôle métier seul. Pas de nouveau test E2E (règle n°7 : bug attrapé avant prod). La source de vérité de la matrice (`RbacSeeder`) est mise à jour et couverte par `ClientRBACMatrixTest`.

Closes ERP-102.

---------

Co-authored-by: Matthieu <contact@malio.fr>
Reviewed-on: #53
Co-authored-by: THOLOT DECHENE Matthieu <matthieu@yuno.malio.fr>
Co-committed-by: THOLOT DECHENE Matthieu <matthieu@yuno.malio.fr>
2026-06-03 12:56:46 +00:00
tristan 6fee9f6bd6 [ERP-64] Page Consultation client (lecture seule + Modifier / Archiver) (#49)
Auto Tag Develop / tag (push) Successful in 9s
## ERP-64 — Page Consultation client (lecture seule)

Route **`/clients/[id]`** : consultation client en lecture seule, porte vers Modification + actions Archiver / Restaurer.

### Périmètre (front uniquement)
- **`useClient(id)`** : charge le détail (embed contacts / adresses / ribs), `archive()` / `restore()` via `PATCH { isArchived }` **seul**, puis **refetch complet** (la réponse du PATCH ne porte pas l'embed). Le **409** de conflit d'homonyme à la restauration (RG-1.23) est propagé → toast dédié.
- **Page** : formulaire principal + **8 onglets** readonly en **navigation libre** (4 actifs + 4 placeholders). Onglet **Comptabilité** visible **uniquement avec `accounting.view`**.
- **Boutons** : **Modifier** si `manage` OU `accounting.manage` ; **Archiver** si `archive` et client actif ; **Restaurer** si `archive` et client archivé.
- Téléphones affichés formatés `XX XX XX XX XX`.
- Réutilise `ClientContactBlock` / `ClientAddressBlock` / `TabPlaceholderBlank` (ERP-63) en mode `readonly`.

### Libellés issus de l'embed (role-independant)
`GET /api/categories` et `/api/sites` renvoient **403 pour les rôles métier non-admin**. La page lit donc tous les libellés (catégories, sites, référentiels comptables) **directement dans le payload embarqué** — affichage correct pour tous les rôles, sans dépendre d'un `GET` de référentiel.

### Correctifs `ClientAddressBlock` (lecture seule)
- la **ville** courante est toujours présente dans les options (sinon `MalioSelect` n'affiche rien) ;
- la **rue** s'affiche en champ texte readonly (`MalioInputAutocomplete` ne réaffiche pas sa valeur liée).

### Pas de changement back
L'embed `GET /api/clients/{id}` (contacts/adresses/ribs + sites + codes catégories, gating `accounting.view`, 409 restauration) **était déjà livré par ERP-62 (#44)** — vérifié sur l'API réelle et couvert par `ClientApiTest::testGetDetailEmbedsSubCollections`, `ClientReadGroupContextBuilderTest`, `ClientArchiveTest::testRestoreConflictReturns409`.

### Tests
- Vitest : **+29 tests** (mapping payload→brouillons, options embed, permissions, archive/restore/409). Suite complète **158 OK**.
- `nuxi typecheck` : 0 erreur sur les fichiers ajoutés.
- Golden path navigateur (admin + commerciale) : readonly complet, onglet Compta + RIBs selon `accounting.view`, boutons selon rôle, bascule Archiver ↔ Restaurer.

### ⚠️ À investiguer (hors périmètre)
Le 403 sur `/categories` et `/sites` impacte aussi `useClientReferentials.loadCommon()` (un `Promise.all` qui rejette en entier) → potentiellement le **formulaire de création ERP-63 cassé pour la Commerciale** (impossible de choisir catégories/sites). À confirmer dans un ticket dédié.

Reviewed-on: #49
Co-authored-by: tristan <tristan@yuno.malio.fr>
Co-committed-by: tristan <tristan@yuno.malio.fr>
2026-06-03 09:57:14 +00:00
tristan ee1521384e [ERP-62] Page Répertoire clients (datatable + Ajouter / Exporter) (#44)
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>
2026-06-02 14:16:29 +00:00