246 lines
9.1 KiB
TypeScript
246 lines
9.1 KiB
TypeScript
/**
|
|
* 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<HydraRef | string>
|
|
}
|
|
|
|
/** 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
|
|
}
|