Files
Starseed/frontend/modules/commercial/utils/supplierEdit.ts
T
tristan 18fdf9354f feat(front) : page Modification fournisseur (/suppliers/{id}/edit) (ERP-96)
- page edit.vue : champs pre-remplis depuis GET /suppliers/{id}, PATCH partiel
  INDEPENDANT par onglet (mode strict RG-2.16 : un seul groupe de serialisation
  par appel), pas de formulaire principal masque mais editable via son propre PATCH
- pas de contact inline (ERP-106) ; onglets metier readonly sans manage, Comptabilite
  visible/editable selon accounting.view / accounting.manage (resolveTabEditability)
- collections contacts/adresses/RIB : POST/PATCH par ligne + DELETE differe des
  retraits ; erreurs 422 mappees inline par champ (propertyPath) via useSupplierFormErrors
- supplierEdit : mappers d'hydratation (mapMainDraft/mapInformationDraft/
  mapAccountingFormDraft, + volumeForecast) et resolveTabEditability
- tests Vitest : mappers d'hydratation + gating par role (matrice 2.7)
- miroir de l'ecran Modification client (M1), adapte M2 (addressType/bennes/
  triageProvider/volumeForecast, pas de relation Distributeur/Courtier)
2026-06-11 08:22:17 +02:00

227 lines
9.4 KiB
TypeScript

/**
* Helpers purs des ecrans « Ajouter » / « Modifier » un fournisseur (M2
* Commercial) — miroir de `clientEdit.ts` (M1). Deux responsabilites, toutes deux
* testables unitairement (cf. supplierEdit.spec.ts) :
* 1. Pre-remplissage : mapper le payload `GET /api/suppliers/{id}` (embed +
* scalaires) vers les brouillons « plats » edites par la page de modification.
* 2. Scoping STRICT des payloads PATCH (mode strict RG-2.16 / ERP-74) : chaque
* onglet n'envoie QUE les champs de SON groupe de serialisation, jamais un
* payload mixte (un champ hors-permission = 403 sur l'integralite cote back).
*
* Ces helpers ne touchent ni a l'API ni a l'etat reactif.
*/
import {
ADDRESS_REQUIRED_NON_NULLABLE_KEYS,
MAIN_REQUIRED_NON_NULLABLE_KEYS,
omitEmptyRequired,
RIB_REQUIRED_NON_NULLABLE_KEYS,
} from '~/modules/commercial/utils/supplierFormRules'
import { iriOf, type SupplierDetail } from '~/modules/commercial/utils/supplierConsultation'
import type {
SupplierAddressFormDraft,
SupplierContactFormDraft,
SupplierRibFormDraft,
} from '~/modules/commercial/types/supplierForm'
/** Etat « plat » du bloc principal (groupe supplier:write:main). */
export interface MainFormDraft {
companyName: string | null
/** IRI des categories rattachees (M2M, type FOURNISSEUR). */
categoryIris: string[]
}
/** Etat « plat » de l'onglet Information (groupe supplier:write:information). */
export interface InformationFormDraft {
description: string | null
competitors: string | null
/** Date de creation de l'entreprise au format YYYY-MM-DD (MalioDate). */
foundedAt: string | null
/** Nombre de salaries en chaine (saisie masquee), converti en number au PATCH. */
employeesCount: string | null
revenueAmount: string | null
profitAmount: string | null
directorName: string | null
/** Volume previsionnel (entier >= 0, specifique fournisseur) en chaine. */
volumeForecast: string | null
}
/** Etat « plat » de l'onglet Comptabilite (groupe supplier:write:accounting). */
export interface AccountingFormDraft {
siren: string | null
accountNumber: string | null
nTva: string | null
tvaModeIri: string | null
paymentDelayIri: string | null
paymentTypeIri: string | null
bankIri: string | null
}
/** Permissions de l'utilisateur courant pertinentes pour l'edition d'un fournisseur. */
export interface SupplierEditAbilities {
/** `commercial.suppliers.manage` : bloc principal + onglets metier. */
canManage: boolean
/** `commercial.suppliers.accounting.view` : visibilite de l'onglet Comptabilite. */
canAccountingView: boolean
/** `commercial.suppliers.accounting.manage` : edition de l'onglet Comptabilite. */
canAccountingManage: boolean
}
/** Editabilite resolue par zone d'onglet (deduite des permissions). */
export interface TabEditability {
/** Bloc principal + onglets Information / Contacts / Adresses editables. */
businessEditable: boolean
/** Onglet Comptabilite present (affiche). */
accountingVisible: boolean
/** Onglet Comptabilite editable. */
accountingEditable: boolean
}
// ── Pre-remplissage (GET detail -> brouillons) ──────────────────────────────
/** Mappe le detail fournisseur vers le brouillon du bloc principal. */
export function mapMainDraft(supplier: SupplierDetail): MainFormDraft {
return {
companyName: supplier.companyName ?? null,
categoryIris: (supplier.categories ?? []).map(c => c['@id']),
}
}
/** Mappe le detail fournisseur vers le brouillon de l'onglet Information. */
export function mapInformationDraft(supplier: SupplierDetail): InformationFormDraft {
return {
description: supplier.description ?? null,
competitors: supplier.competitors ?? null,
// MalioDate attend strictement YYYY-MM-DD : on tronque l'ISO datetime.
foundedAt: supplier.foundedAt ? supplier.foundedAt.slice(0, 10) : null,
employeesCount: supplier.employeesCount != null ? String(supplier.employeesCount) : null,
revenueAmount: supplier.revenueAmount ?? null,
profitAmount: supplier.profitAmount ?? null,
directorName: supplier.directorName ?? null,
// Volume previsionnel (entier, specifique fournisseur) en chaine pour la saisie.
volumeForecast: supplier.volumeForecast != null ? String(supplier.volumeForecast) : null,
}
}
/** Mappe les champs comptables du detail vers le brouillon de l'onglet (scalaires + IRI). */
export function mapAccountingFormDraft(supplier: SupplierDetail): AccountingFormDraft {
return {
siren: supplier.siren ?? null,
accountNumber: supplier.accountNumber ?? null,
nTva: supplier.nTva ?? null,
tvaModeIri: iriOf(supplier.tvaMode),
paymentDelayIri: iriOf(supplier.paymentDelay),
paymentTypeIri: iriOf(supplier.paymentType),
bankIri: iriOf(supplier.bank),
}
}
/**
* Resout l'editabilite par zone a partir des permissions (option 1 ERP-74,
* miroir UI du re-gating champ-par-champ du SupplierProcessor) :
* - bloc principal + Information/Contacts/Adresses : editables ssi `manage` ;
* - Comptabilite : visible ssi `accounting.view`, editable ssi `accounting.manage`.
*
* Produit le comportement attendu :
* - Admin : tout editable.
* - Bureau / Commerciale (manage, sans accounting) : metier editable, Compta masquee.
* - Compta (accounting seul, sans manage) : metier readonly, Compta editable.
*/
export function resolveTabEditability(abilities: SupplierEditAbilities): TabEditability {
return {
businessEditable: abilities.canManage,
accountingVisible: abilities.canAccountingView,
accountingEditable: abilities.canAccountingManage,
}
}
// ── Scoping strict des payloads PATCH/POST ──────────────────────────────────
/**
* Payload du bloc principal — groupe supplier:write:main UNIQUEMENT.
* companyName omis si vide -> 422 NotBlank au lieu d'un 400 de type (ERP-119).
*/
export function buildMainPayload(main: MainFormDraft): Record<string, unknown> {
return omitEmptyRequired({
companyName: main.companyName,
categories: main.categoryIris,
}, MAIN_REQUIRED_NON_NULLABLE_KEYS)
}
/** Payload de l'onglet Information — groupe supplier:write:information UNIQUEMENT. */
export function buildInformationPayload(information: InformationFormDraft): Record<string, unknown> {
return {
description: information.description || null,
competitors: information.competitors || null,
foundedAt: information.foundedAt || null,
employeesCount: information.employeesCount ? Number(information.employeesCount) : null,
revenueAmount: information.revenueAmount || null,
profitAmount: information.profitAmount || null,
directorName: information.directorName || null,
volumeForecast: information.volumeForecast ? Number(information.volumeForecast) : null,
}
}
/**
* Payload des scalaires de l'onglet Comptabilite — groupe supplier:write:accounting
* UNIQUEMENT (les RIB passent par la sous-ressource /suppliers/{id}/ribs). La
* banque n'a de sens que pour un Virement (RG-2.07) : forcee a null sinon.
*/
export function buildAccountingPayload(
accounting: AccountingFormDraft,
isBankRequired: boolean,
): Record<string, unknown> {
return {
siren: accounting.siren || null,
accountNumber: accounting.accountNumber || null,
tvaMode: accounting.tvaModeIri,
nTva: accounting.nTva || null,
paymentDelay: accounting.paymentDelayIri,
paymentType: accounting.paymentTypeIri,
bank: isBankRequired ? accounting.bankIri : null,
}
}
/** Payload d'un contact (sous-ressource supplier_contact). */
export function buildContactPayload(contact: SupplierContactFormDraft): Record<string, unknown> {
return {
firstName: contact.firstName || null,
lastName: contact.lastName || null,
jobTitle: contact.jobTitle || null,
phonePrimary: contact.phonePrimary || null,
phoneSecondary: contact.hasSecondaryPhone ? (contact.phoneSecondary || null) : null,
email: contact.email || null,
}
}
/**
* Payload d'une adresse (sous-ressource supplier_address). postalCode / city /
* street omis si vides -> 422 NotBlank (ERP-119). Specifique fournisseur :
* `bennes` (entier, 0 par defaut) + `triageProvider` (booleen). Pas d'email de
* facturation (difference M1).
*/
export function buildAddressPayload(address: SupplierAddressFormDraft): Record<string, unknown> {
return omitEmptyRequired({
addressType: address.addressType,
country: address.country,
postalCode: address.postalCode || null,
city: address.city || null,
street: address.street || null,
streetComplement: address.streetComplement || null,
categories: address.categoryIris,
sites: address.siteIris,
contacts: address.contactIris,
bennes: address.bennes !== null && address.bennes !== '' ? Number(address.bennes) : null,
triageProvider: address.triageProvider,
}, ADDRESS_REQUIRED_NON_NULLABLE_KEYS)
}
/** Payload d'un RIB (sous-ressource supplier_rib). */
export function buildRibPayload(rib: SupplierRibFormDraft): Record<string, unknown> {
return omitEmptyRequired({
label: rib.label,
bic: rib.bic,
iban: rib.iban,
}, RIB_REQUIRED_NON_NULLABLE_KEYS)
}