/** * 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 }