/** * Helpers purs des ecrans Consultation / Modification prestataire (M3 Technique, * ERP-145) — miroir SIMPLIFIE de `supplierConsultation.ts` (M2). Mappent le payload * `GET /api/providers/{id}` (relations embarquees, cf. groupes `provider:item:read` * + `provider:read:accounting`) vers les brouillons « plats » partages avec * `ProviderContactBlock` / `ProviderAddressBlock` et l'onglet Comptabilite. * * Ne touchent ni a l'API ni a l'etat reactif (testables unitairement). * * Rappels de contrat back (JSON reel fige — ERP-139, spec-back § 4.0.bis) : * - categories / sites du prestataire et des adresses : OBJETS embarques (avec @id) ; * - refs comptables (tvaMode/paymentDelay/paymentType/bank) : OBJETS embarques * `{@id, id, label, (code pour paymentType)}` ; * - champs nuls OMIS (skip_null_values) → toujours lire avec `?? null` ; * - champs comptables + `ribs` TOTALEMENT ABSENTS sans permission accounting.view. * * Differences M2 : pas de type d'adresse / bennes / triage, pas d'onglet Information. */ import { formatPhoneFR } from '~/shared/utils/phone' import type { ProviderAccountingDraft, ProviderAddressFormDraft, ProviderContactFormDraft, ProviderRibFormDraft, } from '~/modules/technique/types/providerForm' import type { RefOption } from '~/modules/technique/composables/useProviderReferentials' /** Reference Hydra embarquee minimale (@id toujours present). */ export interface HydraRef { '@id': string [key: string]: unknown } /** Une relation peut etre embarquee (objet), un IRI nu (chaine) ou absente. */ export type Relation = HydraRef | string | null | undefined /** Site embarque (groupe site:read). */ export interface SiteRead extends HydraRef { name?: string postalCode?: string color?: string } /** Categorie embarquee (groupe category:read). */ export interface CategoryRead extends HydraRef { code?: string name?: string } /** Contact embarque (groupe provider:item:read). */ export interface ContactRead extends HydraRef { id: number firstName?: string | null lastName?: string | null jobTitle?: string | null phonePrimary?: string | null phoneSecondary?: string | null email?: string | null } /** Adresse embarquee (groupe provider:item:read) — version simplifiee M3. */ export interface AddressRead extends HydraRef { id: number country?: string | null postalCode?: string | null city?: string | null street?: string | null streetComplement?: string | null sites?: SiteRead[] categories?: CategoryRead[] // L'embed M2M des contacts d'adresse peut etre un objet (partiel) ou un IRI nu. contacts?: Array } /** RIB embarque (groupe provider:read:accounting, present ssi accounting.view). */ export interface RibRead extends HydraRef { id: number label?: string | null bic?: string | null iban?: string | null } /** * Detail d'un prestataire (`GET /api/providers/{id}`). Tous les champs sont * optionnels : skip_null_values + gating accounting peuvent omettre n'importe * quelle cle. */ export interface ProviderDetail extends HydraRef { id: number companyName?: string | null isArchived?: boolean categories?: CategoryRead[] sites?: SiteRead[] contacts?: ContactRead[] addresses?: AddressRead[] ribs?: RibRead[] // Onglet Comptabilite (present ssi accounting.view) siren?: string | null accountNumber?: string | null nTva?: string | null tvaMode?: Relation paymentDelay?: Relation paymentType?: Relation bank?: Relation } /** Extrait l'IRI d'une relation (objet embarque, IRI nu, ou null si absente). */ export function iriOf(relation: Relation): string | null { if (relation === null || relation === undefined) { return null } if (typeof relation === 'string') { return relation } return relation['@id'] ?? null } /** IRI des elements d'une collection embarquee (categories / sites du prestataire). */ export function irisOf(items: HydraRef[] | undefined): string[] { return (items ?? []).map(i => i['@id']) } /** Mappe un contact embarque vers un brouillon (telephones formates XX XX XX XX XX). */ export function mapContactToDraft(contact: ContactRead): ProviderContactFormDraft { const phoneSecondary = contact.phoneSecondary ?? null return { id: contact.id, iri: contact['@id'] ?? null, firstName: contact.firstName ?? null, lastName: contact.lastName ?? null, jobTitle: contact.jobTitle ?? null, phonePrimary: contact.phonePrimary ? formatPhoneFR(contact.phonePrimary) : null, phoneSecondary: phoneSecondary ? formatPhoneFR(phoneSecondary) : null, email: contact.email ?? null, hasSecondaryPhone: phoneSecondary !== null && phoneSecondary !== '', } } /** Mappe une adresse embarquee vers un brouillon (IRI extraits des sous-collections). */ export function mapAddressToDraft(address: AddressRead): ProviderAddressFormDraft { return { id: address.id, country: address.country ?? 'France', postalCode: address.postalCode ?? null, city: address.city ?? null, street: address.street ?? null, streetComplement: address.streetComplement ?? null, categoryIris: (address.categories ?? []).map(c => c['@id']), siteIris: (address.sites ?? []).map(s => s['@id']), contactIris: (address.contacts ?? []).map(c => (typeof c === 'string' ? c : c['@id'])), } } /** Mappe un RIB embarque vers un brouillon. */ export function mapRibToDraft(rib: RibRead): ProviderRibFormDraft { return { id: rib.id, label: rib.label ?? null, bic: rib.bic ?? null, iban: rib.iban ?? null, } } /** Mappe les champs comptables (scalaires + IRI des referentiels embarques). */ export function mapAccountingDraft(provider: ProviderDetail): ProviderAccountingDraft { return { siren: provider.siren ?? null, accountNumber: provider.accountNumber ?? null, nTva: provider.nTva ?? null, tvaModeIri: iriOf(provider.tvaMode), paymentDelayIri: iriOf(provider.paymentDelay), paymentTypeIri: iriOf(provider.paymentType), bankIri: iriOf(provider.bank), } } /** * Options de categories (value=IRI, label=nom) construites depuis l'embed. * Source role-independante : evite de dependre de `GET /categories` (403 possible * pour un role metier), qui laisserait les libelles vides en consultation. */ export function categoryOptionsOf(categories: CategoryRead[] | undefined): RefOption[] { return (categories ?? []).map(c => ({ value: c['@id'], label: c.name ?? c.code ?? c['@id'], })) } /** Options de sites (value=IRI, label=nom) construites depuis un embed. */ export function siteOptionsOf(sites: SiteRead[] | undefined): RefOption[] { return (sites ?? []).map(s => ({ value: s['@id'], label: s.name ?? s['@id'] })) } /** Options de contacts (value=IRI, label=nom complet ou email) depuis l'embed prestataire. */ export function contactOptionsOf(contacts: ContactRead[] | undefined): RefOption[] { return (contacts ?? []).map(c => ({ value: c['@id'], label: [c.firstName, c.lastName].filter(Boolean).join(' ') || (c.email ?? c['@id']), })) } /** * Liste a une seule option (ou vide) construite depuis un referentiel embarque * (TvaMode / PaymentDelay / PaymentType / Bank) pour alimenter un MalioSelect en * lecture seule. Le libelle vient de l'embed, jamais d'un GET de referentiel — * l'affichage reste correct quel que soit le role. */ export function referentialOptionOf(relation: Relation): RefOption[] { if (!relation || typeof relation === 'string') { return [] } const label = (relation.label as string | undefined) ?? (relation.name as string | undefined) ?? relation['@id'] return [{ value: relation['@id'], label }] } /** Code metier d'un referentiel embarque (PaymentType.code = 'LCR' / 'VIREMENT'), ou null. */ export function paymentTypeCodeOf(relation: Relation): string | null { if (!relation || typeof relation === 'string') { return null } return (relation.code as string | undefined) ?? null } /** * Bouton « Modifier » : visible si l'utilisateur peut editer au moins un onglet — * `manage` (onglets metier) OU `accounting.manage` (le role Compta doit pouvoir * ouvrir l'edition pour son onglet Comptabilite). Le readonly fin par onglet est * gere sur l'ecran d'edition. */ export function canEditProvider(canAny: (codes: string[]) => boolean): boolean { return canAny(['technique.providers.manage', 'technique.providers.accounting.manage']) } /** Bouton « Archiver » : permission archive ET prestataire encore actif (Admin seul). */ export function showArchiveAction(can: (code: string) => boolean, isArchived: boolean): boolean { return can('technique.providers.archive') && !isArchived } /** Bouton « Restaurer » : permission archive ET prestataire deja archive (Admin seul). */ export function showRestoreAction(can: (code: string) => boolean, isArchived: boolean): boolean { return can('technique.providers.archive') && isArchived }