feat(front) : regles metier + types + stub autocomplete creation client (ERP-63)
- clientFormRules : RG-1.05/1.14 (contacts), RG-1.06/07/08/11 (exclusivite adresse + email facturation), RG-1.12/1.13 (banque/RIB selon type de reglement), construction des onglets (gating Comptabilite). 18 tests Vitest. - types/clientForm : brouillons locaux (contact/adresse/RIB) + fabriques. - useAddressAutocomplete : STUB ERP-63 conforme a la signature ERP-66 (mode degrade), a remplacer par l'implementation BAN d'ERP-66. Note : RG-1.04 (Information obligatoire pour la Commerciale) volontairement non miroitee cote front (le payload /api/me ne porte pas le code de role ; Bureau et Commerciale partagent les memes permissions). Le back l'applique de maniere fiable.
This commit is contained in:
@@ -0,0 +1,143 @@
|
||||
/**
|
||||
* Regles metier pures de l'ecran « Ajouter un client » (M1 Commercial).
|
||||
*
|
||||
* Centralisees ici (hors composant) pour rester testables unitairement et
|
||||
* partagees entre la page de creation et les futurs ecrans d'edition (1.11/1.12).
|
||||
* Ces helpers ne touchent ni a l'API ni a l'etat reactif : ils prennent des
|
||||
* brouillons « plats » et retournent des booleens / nouveaux objets.
|
||||
*
|
||||
* Le back reste la source de verite (les RG sont re-validees serveur) ; ces
|
||||
* regles ne servent qu'au feedback UI immediat (gating de boutons, visibilite).
|
||||
*
|
||||
* NOTE RG-1.04 (Information obligatoire pour la Commerciale) : volontairement
|
||||
* NON miroite cote front pour l'instant. Le payload /api/me ne porte pas le code
|
||||
* de role (roles = IRIs opaques) et Bureau partage les memes permissions que
|
||||
* Commerciale : aucun signal fiable pour distinguer le role cote front. Le back
|
||||
* (ClientProcessor, via BusinessRoleAware) applique la regle de maniere fiable ;
|
||||
* a rebrancher ici des qu'un code de role sera expose dans /api/me.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Onglets « coquille » (non encore implementes) : frame vide, passage
|
||||
* automatique a l'onglet suivant (decision Tristan 28/05).
|
||||
*/
|
||||
export const CLIENT_FORM_PLACEHOLDER_TABS = ['transport', 'statistics', 'reports', 'exchanges'] as const
|
||||
|
||||
/**
|
||||
* Construit l'ordre des onglets de l'ecran « Ajouter un client ». L'onglet
|
||||
* Comptabilite n'est present que si l'utilisateur a `accounting.view` — sinon il
|
||||
* est totalement absent (Bureau / Commerciale ne le voient pas). Ordre aligne
|
||||
* sur la spec M1 § Ecran « Ajouter un client ».
|
||||
*/
|
||||
export function buildClientFormTabKeys(canAccountingView: boolean): string[] {
|
||||
const keys = ['information', 'contact', 'address', 'transport']
|
||||
if (canAccountingView) {
|
||||
keys.push('accounting')
|
||||
}
|
||||
keys.push('statistics', 'reports', 'exchanges')
|
||||
return keys
|
||||
}
|
||||
|
||||
/** Sous-ensemble d'un contact necessaire aux regles de nommage (RG-1.05/1.14). */
|
||||
export interface ContactDraft {
|
||||
firstName: string | null
|
||||
lastName: string | null
|
||||
}
|
||||
|
||||
/** Drapeaux d'usage d'une adresse (RG-1.06/07/08/11). */
|
||||
export interface AddressFlagsDraft {
|
||||
isProspect: boolean
|
||||
isDelivery: boolean
|
||||
isBilling: boolean
|
||||
}
|
||||
|
||||
/** Vrai si une chaine porte au moins un caractere non-espace. */
|
||||
function isFilled(value: string | null | undefined): boolean {
|
||||
return value !== null && value !== undefined && value.trim() !== ''
|
||||
}
|
||||
|
||||
/**
|
||||
* RG-1.05 : un contact est valide des qu'il porte un nom OU un prenom.
|
||||
*/
|
||||
export function isContactNamed(contact: ContactDraft): boolean {
|
||||
return isFilled(contact.firstName) || isFilled(contact.lastName)
|
||||
}
|
||||
|
||||
/**
|
||||
* RG-1.14 : l'onglet Contact ne peut etre finalise que s'il reste au moins un
|
||||
* contact nomme (nom ou prenom).
|
||||
*/
|
||||
export function hasAtLeastOneValidContact(contacts: ContactDraft[]): boolean {
|
||||
return contacts.some(isContactNamed)
|
||||
}
|
||||
|
||||
/**
|
||||
* RG-1.06/07/08 : une adresse de prospection est exclusive d'une adresse de
|
||||
* livraison/facturation. Prospect n'est selectionnable que si ni Livraison ni
|
||||
* Facturation ne sont coches.
|
||||
*/
|
||||
export function canSelectProspect(flags: AddressFlagsDraft): boolean {
|
||||
return !flags.isDelivery && !flags.isBilling
|
||||
}
|
||||
|
||||
/**
|
||||
* RG-1.06/07/08 : Livraison et Facturation ne sont selectionnables que si
|
||||
* Prospect n'est pas coche.
|
||||
*/
|
||||
export function canSelectDeliveryOrBilling(flags: AddressFlagsDraft): boolean {
|
||||
return !flags.isProspect
|
||||
}
|
||||
|
||||
/**
|
||||
* Applique l'exclusivite Prospect / (Livraison|Facturation) au changement d'un
|
||||
* drapeau. Cocher Prospect efface Livraison + Facturation ; cocher Livraison ou
|
||||
* Facturation efface Prospect. Decocher n'a aucun effet de bord. Retourne un
|
||||
* nouvel objet (pas de mutation de l'entree).
|
||||
*/
|
||||
export function applyProspectExclusivity(
|
||||
flags: AddressFlagsDraft,
|
||||
field: keyof AddressFlagsDraft,
|
||||
value: boolean,
|
||||
): AddressFlagsDraft {
|
||||
const next: AddressFlagsDraft = { ...flags, [field]: value }
|
||||
|
||||
if (value && field === 'isProspect') {
|
||||
next.isDelivery = false
|
||||
next.isBilling = false
|
||||
}
|
||||
else if (value && (field === 'isDelivery' || field === 'isBilling')) {
|
||||
next.isProspect = false
|
||||
}
|
||||
|
||||
return next
|
||||
}
|
||||
|
||||
/**
|
||||
* RG-1.11 : l'email de facturation n'est visible/obligatoire que si l'adresse
|
||||
* est une adresse de facturation.
|
||||
*/
|
||||
export function isBillingEmailRequired(flags: AddressFlagsDraft): boolean {
|
||||
return flags.isBilling
|
||||
}
|
||||
|
||||
/** Code stable du type de reglement « virement » (cf. PaymentType.code, RG-1.12). */
|
||||
const PAYMENT_TYPE_TRANSFER = 'VIREMENT'
|
||||
|
||||
/** Code stable du type de reglement « lettre de change » (RG-1.13). */
|
||||
const PAYMENT_TYPE_LCR = 'LCR'
|
||||
|
||||
/**
|
||||
* RG-1.12 : la banque est obligatoire lorsque le type de reglement est un
|
||||
* virement.
|
||||
*/
|
||||
export function isBankRequiredForPaymentType(code: string | null | undefined): boolean {
|
||||
return code === PAYMENT_TYPE_TRANSFER
|
||||
}
|
||||
|
||||
/**
|
||||
* RG-1.13 : au moins un RIB complet est obligatoire lorsque le type de reglement
|
||||
* est une LCR.
|
||||
*/
|
||||
export function isRibRequiredForPaymentType(code: string | null | undefined): boolean {
|
||||
return code === PAYMENT_TYPE_LCR
|
||||
}
|
||||
Reference in New Issue
Block a user