f9c881c771
Les roles metier (bureau / compta / commerciale) prenaient un 403 sur GET /api/categories et GET /api/sites : la security des GetCollection/Get exigeait catalog.categories.view / sites.view, permissions reservees a l'administration du Catalogue et des Sites. Or ces referentiels sont transverses (selects de creation/filtre client) : creation de client cassee et filtres vides pour ces roles. Correctif back (Option C — permission de lecture-referentiel dediee) : - Nouvelles permissions catalog.categories.read_ref et sites.read_ref, distinctes de .view (pas d'item sidebar admin) et de .manage. Chaque permission appartient a son propre module -> aucun couplage inter-module (regle ABSOLUE n°1) et reutilisable tel quel par M2 Fournisseurs. - Security lecture (liste + item) elargie : view OR read_ref sur Category et Site. - Matrice RBAC § 2.7 (RbacSeeder) : read_ref attache a bureau / compta / commerciale. Usine reste sans acces. Durcissement front (resilience, requis dans tous les cas) : - useClientReferentials.loadCommon passe de Promise.all a Promise.allSettled avec affectation isolee par referentiel : l'echec d'un endpoint ne vide que SON select, plus la totalite du formulaire. Tests : - ClientRBACMatrixTest : les roles metier listent /categories et /sites (200), usine reste a 403. - SitesModuleTest : set de permissions porte a 4 codes. - useClientReferentials.spec : resilience d'un referentiel en echec. Miroirs E2E (personas.ts / SeedE2ECommand) non touches : read_ref n'ajoute aucun lien sidebar, le persona user-full lit deja via .view, et aucun persona ne modelise un role metier seul ; pas de nouveau test E2E (regle n°7).
154 lines
5.5 KiB
TypeScript
154 lines
5.5 KiB
TypeScript
import { ref } from 'vue'
|
|
|
|
/**
|
|
* Charge les referentiels (listes courtes) alimentant les selects de l'ecran
|
|
* « Ajouter un client » : categories, sites, modes de TVA, delais et types de
|
|
* reglement, banques, et les listes distributeurs / courtiers.
|
|
*
|
|
* Toutes les collections sont recuperees en entier via l'echappatoire prevue
|
|
* `?pagination=false` (referentiels de quelques dizaines d'entrees max), avec
|
|
* l'en-tete `Accept: application/ld+json` impose par API Platform 4 pour obtenir
|
|
* l'enveloppe Hydra (`member`). Les valeurs d'option sont les IRI Hydra (`@id`)
|
|
* pour pouvoir etre renvoyees telles quelles dans les payloads POST/PATCH
|
|
* (relations ManyToOne / ManyToMany).
|
|
*
|
|
* Etat 100 % local a l'instance (refs) — aucune persistance URL.
|
|
*/
|
|
|
|
/** Option generique au format attendu par MalioSelect / MalioSelectCheckbox ({ label, value }). */
|
|
export interface RefOption {
|
|
value: string
|
|
label: string
|
|
}
|
|
|
|
/** Option de type de reglement enrichie de son code stable (RG-1.12 / RG-1.13). */
|
|
export interface PaymentTypeOption extends RefOption {
|
|
code: string
|
|
}
|
|
|
|
/** Option de categorie enrichie de son code stable (filtrage RG-1.29 cote adresse). */
|
|
export interface CategoryOption extends RefOption {
|
|
code: string
|
|
}
|
|
|
|
/** Option de client (distributeur / courtier) — value = IRI du client lie. */
|
|
export type ClientOption = RefOption
|
|
|
|
interface HydraMember {
|
|
'@id': string
|
|
}
|
|
|
|
interface CategoryMember extends HydraMember {
|
|
code: string
|
|
name: string
|
|
}
|
|
|
|
interface SiteMember extends HydraMember {
|
|
name: string
|
|
}
|
|
|
|
interface ReferentialMember extends HydraMember {
|
|
code: string
|
|
label: string
|
|
}
|
|
|
|
interface ClientMember extends HydraMember {
|
|
companyName: string
|
|
}
|
|
|
|
const LD_JSON_HEADERS = { Accept: 'application/ld+json' }
|
|
|
|
export function useClientReferentials() {
|
|
const api = useApi()
|
|
|
|
const categories = ref<CategoryOption[]>([])
|
|
const sites = ref<RefOption[]>([])
|
|
const tvaModes = ref<RefOption[]>([])
|
|
const paymentDelays = ref<RefOption[]>([])
|
|
const paymentTypes = ref<PaymentTypeOption[]>([])
|
|
const banks = ref<RefOption[]>([])
|
|
const distributors = ref<ClientOption[]>([])
|
|
const brokers = ref<ClientOption[]>([])
|
|
|
|
/** Recupere une collection complete (pagination desactivee) en Hydra. */
|
|
async function fetchAll<T extends HydraMember>(
|
|
url: string,
|
|
query: Record<string, string | string[]> = {},
|
|
): Promise<T[]> {
|
|
const res = await api.get<{ member?: T[] }>(
|
|
url,
|
|
{ pagination: 'false', ...query },
|
|
{ headers: LD_JSON_HEADERS, toast: false },
|
|
)
|
|
return res.member ?? []
|
|
}
|
|
|
|
/**
|
|
* Charge en parallele les referentiels communs (hors distributeurs/courtiers,
|
|
* charges a la demande selon la relation choisie). Les selects compta ne sont
|
|
* pertinents que si l'utilisateur a acces a l'onglet, mais le cout est
|
|
* negligeable et simplifie l'orchestration.
|
|
*
|
|
* Resilience (ERP-102) : chaque referentiel est charge et affecte
|
|
* independamment via `Promise.allSettled`. Si UN endpoint echoue (ex: 403,
|
|
* coupure reseau), seul SON select reste vide — les autres sont peuples
|
|
* normalement. Un `Promise.all` rejetterait au premier echec et viderait la
|
|
* TOTALITE des selects, rendant le formulaire de creation client inutilisable.
|
|
* `loadCommon` ne rejette donc jamais.
|
|
*/
|
|
async function loadCommon(): Promise<void> {
|
|
await Promise.allSettled([
|
|
fetchAll<CategoryMember>('/categories').then(cats => {
|
|
categories.value = cats.map(c => ({ value: c['@id'], label: c.name, code: c.code }))
|
|
}),
|
|
fetchAll<SiteMember>('/sites').then(sitesList => {
|
|
sites.value = sitesList.map(s => ({ value: s['@id'], label: s.name }))
|
|
}),
|
|
fetchAll<ReferentialMember>('/tva_modes').then(tva => {
|
|
tvaModes.value = tva.map(t => ({ value: t['@id'], label: t.label }))
|
|
}),
|
|
fetchAll<ReferentialMember>('/payment_delays').then(delays => {
|
|
paymentDelays.value = delays.map(d => ({ value: d['@id'], label: d.label }))
|
|
}),
|
|
fetchAll<ReferentialMember>('/payment_types').then(types => {
|
|
paymentTypes.value = types.map(t => ({ value: t['@id'], label: t.label, code: t.code }))
|
|
}),
|
|
fetchAll<ReferentialMember>('/banks').then(banksList => {
|
|
banks.value = banksList.map(b => ({ value: b['@id'], label: b.label }))
|
|
}),
|
|
])
|
|
}
|
|
|
|
/** Liste des clients pouvant etre choisis comme distributeur (code DISTRIBUTEUR). */
|
|
async function loadDistributors(): Promise<void> {
|
|
if (distributors.value.length > 0) {
|
|
return
|
|
}
|
|
const clients = await fetchAll<ClientMember>('/clients', { categoryCode: 'DISTRIBUTEUR' })
|
|
distributors.value = clients.map(c => ({ value: c['@id'], label: c.companyName }))
|
|
}
|
|
|
|
/** Liste des clients pouvant etre choisis comme courtier (code COURTIER). */
|
|
async function loadBrokers(): Promise<void> {
|
|
if (brokers.value.length > 0) {
|
|
return
|
|
}
|
|
const clients = await fetchAll<ClientMember>('/clients', { categoryCode: 'COURTIER' })
|
|
brokers.value = clients.map(c => ({ value: c['@id'], label: c.companyName }))
|
|
}
|
|
|
|
return {
|
|
categories,
|
|
sites,
|
|
tvaModes,
|
|
paymentDelays,
|
|
paymentTypes,
|
|
banks,
|
|
distributors,
|
|
brokers,
|
|
loadCommon,
|
|
loadDistributors,
|
|
loadBrokers,
|
|
}
|
|
}
|