[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>
This commit was merged in pull request #49.
This commit is contained in:
2026-06-03 09:57:14 +00:00
committed by Autin
parent 276f242b10
commit 6fee9f6bd6
7 changed files with 1239 additions and 5 deletions
@@ -0,0 +1,70 @@
import { ref } from 'vue'
import type { ClientDetail } from '~/modules/commercial/utils/clientConsultation'
/**
* Chargement et actions d'archivage d'un client unique (ecran « Consultation
* client », 1.11). Lit le detail embarque via `GET /api/clients/{id}` (contacts /
* adresses / ribs sous `client:item:read` / `client:read:accounting`) et expose
* les bascules d'archivage (PATCH `isArchived` SEUL — tout autre champ => 422).
*
* L'en-tete `Accept: application/ld+json` est impose pour obtenir le payload
* Hydra complet (sans lui, API Platform 4 renvoie une representation reduite).
*
* Etat 100 % local a l'instance (refs) — aucune persistance URL. Les erreurs
* d'archivage/restauration (notamment le 409 RG-1.23 : homonyme actif a la
* restauration) sont PROPAGEES a l'appelant, qui decide du toast a afficher.
*/
export function useClient(id: number | string) {
const api = useApi()
const client = ref<ClientDetail | null>(null)
const loading = ref(false)
const error = ref(false)
/** Recupere le detail complet (embed contacts/adresses/ribs + comptabilite). */
function fetchDetail(): Promise<ClientDetail> {
return api.get<ClientDetail>(
`/clients/${id}`,
{},
{ headers: { Accept: 'application/ld+json' }, toast: false },
)
}
/** Charge le detail du client. En cas d'echec : `error = true`, `client = null`. */
async function load(): Promise<void> {
loading.value = true
error.value = false
try {
client.value = await fetchDetail()
}
catch {
error.value = true
client.value = null
}
finally {
loading.value = false
}
}
/**
* Bascule l'archivage (PATCH `isArchived` SEUL — tout autre champ => 422),
* puis RECHARGE le detail complet : la reponse du PATCH ne porte que le groupe
* `client:read` (ni l'embed contacts/adresses/ribs ni les libelles des
* referentiels comptables), un simple merge laisserait l'affichage incoherent.
* Toute erreur (notamment le 409 d'homonyme actif a la restauration, RG-1.23)
* est propagee a l'appelant AVANT le rechargement.
*/
async function setArchived(isArchived: boolean): Promise<void> {
await api.patch(`/clients/${id}`, { isArchived }, { toast: false })
client.value = await fetchDetail()
}
return {
client,
loading,
error,
load,
archive: () => setArchived(true),
restore: () => setArchived(false),
}
}