ec952896ba
Auto Tag Develop / tag (push) Successful in 10s
## Objectif Retirer le bloc « contact principal » (Nom, Prénom, Téléphone, Téléphone 2, Email) des trois écrans Client — **création**, **consultation**, **modification** — ainsi que des types, mappeurs, validations et clés i18n associés. La saisie des contacts passe désormais exclusivement par l'onglet **Contacts** (`ClientContactBlock`, inchangé). Dépend du ticket **1/3 (back)** : l'API ne renvoie/n'accepte plus ces 5 champs sur `client`. Contexte : `docs/specs/M1-clients/refonte-contact/README.md`. ## Changements - **`pages/clients/new.vue`** : bloc principal réduit à Nom entreprise / Catégories / Relation / Triage. Suppression de `main.firstName/lastName/email`, `mainPhones`, `addMainPhone()`, `prefillFirstContact()`. `isMainValid` ne dépend plus que de `companyName` + ≥ 1 catégorie + relation valide. Payload POST et `ClientResponse` nettoyés. - **`pages/clients/[id]/edit.vue`** : mêmes champs retirés, `isMainValid` simplifié. - **`pages/clients/[id]/index.vue`** : affichage lecture seule des 5 champs retiré. - **`utils/clientEdit.ts`** : `MainFormDraft`, `mapMainDraft()`, `buildMainPayload()` débarrassés des 5 champs + `hasSecondaryPhone`. - **`utils/clientConsultation.ts`** : `ClientDetail` débarrassé des champs inline (`ContactRead` conservé). - **`i18n/locales/fr.json`** : clés `form.main.firstName/lastName/email/phonePrimary/phoneSecondary/addPhone` supprimées. `form.contact.*` conservé. - **Tests** : `clientEdit.spec.ts` ajusté (factory, `MAIN_KEYS`, assertions `mapMainDraft`, test téléphone secondaire obsolète retiré). ## Vérifications - `make nuxt-test` : suites `clientEdit` / `clientConsultation` / `clientFormRules` vertes. Les 2 échecs restants (`useClientReferentials.spec.ts`, libellé de site) sont **pré-existants** sur `develop` (confirmé par `git stash`), sans rapport avec ce ticket. - `eslint` sur les fichiers touchés : OK, aucun import/variable mort. - Zéro référence orpheline aux clés `form.main.*` supprimées ; JSON i18n valide. ## Reste à faire - Golden path navigateur (création → consultation → modification sans bloc inline) à valider manuellement. Reviewed-on: #57 Co-authored-by: tristan <tristan@yuno.malio.fr> Co-committed-by: tristan <tristan@yuno.malio.fr>
248 lines
10 KiB
TypeScript
248 lines
10 KiB
TypeScript
/**
|
|
* Helpers purs de l'ecran « Modification client » (M1 Commercial, 1.12).
|
|
*
|
|
* Deux responsabilites, toutes deux testables unitairement (cf. clientEdit.spec.ts) :
|
|
* 1. Pre-remplissage : mapper le payload `GET /api/clients/{id}` (embed
|
|
* contacts/adresses/ribs + scalaires) vers les brouillons « plats » edites
|
|
* par la page et les blocs reutilisables (mappers contacts/adresses/ribs/
|
|
* comptabilite reutilises depuis clientConsultation).
|
|
* 2. Scoping STRICT des payloads PATCH (mode strict RG-1.28 / 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.
|
|
*
|
|
* NOTE RG-1.04 (Information obligatoire pour la Commerciale) : volontairement NON
|
|
* miroitee cote front (cf. clientFormRules.ts) — /api/me n'expose pas le code de
|
|
* role et Bureau partage les permissions de Commerciale. Le back l'applique de
|
|
* maniere fiable (422) ; on laisse remonter ce 422 en toast.
|
|
*/
|
|
|
|
import {
|
|
iriOf,
|
|
relationOf,
|
|
type ClientDetail,
|
|
} from '~/modules/commercial/utils/clientConsultation'
|
|
import type { AddressFormDraft, ContactFormDraft, RibFormDraft } from '~/modules/commercial/types/clientForm'
|
|
|
|
/**
|
|
* Etat « plat » du bloc principal (groupe client:write:main). Distinct des
|
|
* brouillons Contact : ces champs vivent sur le Client lui-meme (companyName,
|
|
* categories, relation, triage), pas sur une sous-ressource ClientContact. Les
|
|
* coordonnees de contact (nom, prenom, telephones, email) ne sont plus portees
|
|
* par le Client : elles vivent exclusivement dans l'onglet Contacts.
|
|
*/
|
|
export interface MainFormDraft {
|
|
companyName: string | null
|
|
/** IRI des categories rattachees (M2M). */
|
|
categoryIris: string[]
|
|
relationType: 'distributeur' | 'courtier' | null
|
|
distributorIri: string | null
|
|
brokerIri: string | null
|
|
triageService: boolean
|
|
}
|
|
|
|
/** Etat « plat » de l'onglet Information (groupe client: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
|
|
}
|
|
|
|
/** Etat « plat » de l'onglet Comptabilite (groupe client: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 client. */
|
|
export interface ClientEditAbilities {
|
|
/** `commercial.clients.manage` : bloc principal + onglets metier. */
|
|
canManage: boolean
|
|
/** `commercial.clients.accounting.view` : visibilite de l'onglet Comptabilite. */
|
|
canAccountingView: boolean
|
|
/** `commercial.clients.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 / Contact / Adresse editables. */
|
|
businessEditable: boolean
|
|
/** Onglet Comptabilite present (affiche). */
|
|
accountingVisible: boolean
|
|
/** Onglet Comptabilite editable. */
|
|
accountingEditable: boolean
|
|
}
|
|
|
|
// ── Pre-remplissage (GET detail -> brouillons) ──────────────────────────────
|
|
|
|
/**
|
|
* Mappe le detail client vers le brouillon du bloc principal. La relation
|
|
* Distributeur/Courtier est resolue par exclusivite (RG-1.03) et son IRI extrait
|
|
* de l'embed.
|
|
*/
|
|
export function mapMainDraft(client: ClientDetail): MainFormDraft {
|
|
const relation = relationOf(client)
|
|
|
|
return {
|
|
companyName: client.companyName ?? null,
|
|
categoryIris: (client.categories ?? []).map(c => c['@id']),
|
|
relationType: relation.type,
|
|
distributorIri: iriOf(client.distributor),
|
|
brokerIri: iriOf(client.broker),
|
|
triageService: client.triageService === true,
|
|
}
|
|
}
|
|
|
|
/** Mappe le detail client vers le brouillon de l'onglet Information. */
|
|
export function mapInformationDraft(client: ClientDetail): InformationFormDraft {
|
|
return {
|
|
description: client.description ?? null,
|
|
competitors: client.competitors ?? null,
|
|
// MalioDate attend strictement YYYY-MM-DD : on tronque l'ISO datetime.
|
|
foundedAt: client.foundedAt ? client.foundedAt.slice(0, 10) : null,
|
|
employeesCount: client.employeesCount != null ? String(client.employeesCount) : null,
|
|
revenueAmount: client.revenueAmount ?? null,
|
|
profitAmount: client.profitAmount ?? null,
|
|
directorName: client.directorName ?? null,
|
|
}
|
|
}
|
|
|
|
/** Mappe les champs comptables du detail vers le brouillon de l'onglet (scalaires + IRI). */
|
|
export function mapAccountingFormDraft(client: ClientDetail): AccountingFormDraft {
|
|
return {
|
|
siren: client.siren ?? null,
|
|
accountNumber: client.accountNumber ?? null,
|
|
nTva: client.nTva ?? null,
|
|
tvaModeIri: iriOf(client.tvaMode),
|
|
paymentDelayIri: iriOf(client.paymentDelay),
|
|
paymentTypeIri: iriOf(client.paymentType),
|
|
bankIri: iriOf(client.bank),
|
|
}
|
|
}
|
|
|
|
// ── Scoping strict des payloads PATCH ────────────────────────────────────────
|
|
|
|
/**
|
|
* Payload du bloc principal — groupe client:write:main UNIQUEMENT. La relation
|
|
* Distributeur/Courtier est mutuellement exclusive (RG-1.03) : on ne renseigne
|
|
* que la FK correspondant au type choisi, l'autre est forcee a null.
|
|
*/
|
|
export function buildMainPayload(main: MainFormDraft): Record<string, unknown> {
|
|
return {
|
|
companyName: main.companyName,
|
|
categories: main.categoryIris,
|
|
distributor: main.relationType === 'distributeur' ? main.distributorIri : null,
|
|
broker: main.relationType === 'courtier' ? main.brokerIri : null,
|
|
triageService: main.triageService,
|
|
}
|
|
}
|
|
|
|
/** Payload de l'onglet Information — groupe client: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,
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Payload des scalaires de l'onglet Comptabilite — groupe client:write:accounting
|
|
* UNIQUEMENT (les RIB passent par la sous-ressource /clients/{id}/ribs). La banque
|
|
* n'a de sens que pour un Virement (RG-1.12) : 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 client_contact). */
|
|
export function buildContactPayload(contact: ContactFormDraft): 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 client_address). */
|
|
export function buildAddressPayload(
|
|
address: AddressFormDraft,
|
|
isBillingEmailRequired: boolean,
|
|
): Record<string, unknown> {
|
|
return {
|
|
isProspect: address.isProspect,
|
|
isDelivery: address.isDelivery,
|
|
isBilling: address.isBilling,
|
|
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,
|
|
billingEmail: isBillingEmailRequired ? (address.billingEmail || null) : null,
|
|
}
|
|
}
|
|
|
|
/** Payload d'un RIB (sous-ressource client_rib). */
|
|
export function buildRibPayload(rib: RibFormDraft): Record<string, unknown> {
|
|
return {
|
|
label: rib.label,
|
|
bic: rib.bic,
|
|
iban: rib.iban,
|
|
}
|
|
}
|
|
|
|
// ── Gating par permission ────────────────────────────────────────────────────
|
|
|
|
/**
|
|
* Resout l'editabilite par zone a partir des permissions (option 1 ERP-74,
|
|
* miroir UI du re-gating champ-par-champ du ClientProcessor) :
|
|
* - bloc principal + Information/Contact/Adresse : 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: ClientEditAbilities): TabEditability {
|
|
return {
|
|
businessEditable: abilities.canManage,
|
|
accountingVisible: abilities.canAccountingView,
|
|
accountingEditable: abilities.canAccountingManage,
|
|
}
|
|
}
|