Files
Starseed/frontend/modules/commercial/utils/clientFormRules.ts
T
tristan 955f9a436f refactor(front) : aligne l'ecran ajouter client sur la maquette (ERP-63)
- Layout maquette : en-tete avec retour, grille 3 colonnes (gap-x-[80px]),
  cartes ombrees pour les onglets, boutons Valider centres, libelles ajustes.
- Telephones du formulaire principal en tableau (1 par defaut, + revele le 2e).
- Information : Description en row-span-2 (alignement corrige via pt-1),
  Nombre de salaries en MalioInputText masque chiffres.
- Adresse : carte ombree, suppression en absolute, sites en cases a cocher
  inline, pays France/Espagne, exclusivite Prospect appliquee au toggle.
- Onglets : icones par onglet (TAB_ICONS) ; Statistiques / Rapports / Echanges
  passent en edit-only (absents a la creation, option includeEditOnlyTabs pour
  la modification).
2026-06-02 18:01:18 +02:00

159 lines
5.5 KiB
TypeScript

/**
* 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
/**
* Onglets affiches uniquement en MODIFICATION (selon le role), jamais a la
* creation : Statistiques / Rapports / Echanges. A rebrancher dans les ecrans
* d'edition (1.11/1.12) via l'option `includeEditOnlyTabs`.
*/
export const CLIENT_FORM_EDIT_ONLY_TABS = ['statistics', 'reports', 'exchanges'] as const
/**
* Construit l'ordre des onglets du formulaire client.
* - L'onglet Comptabilite n'est present que si l'utilisateur a `accounting.view`
* (Bureau / Commerciale ne le voient pas).
* - Les onglets edit-only (Statistiques / Rapports / Echanges) sont exclus par
* defaut (creation) ; passer `includeEditOnlyTabs: true` pour les afficher en
* modification.
* Ordre aligne sur la spec M1 § Ecran « Ajouter un client ».
*/
export function buildClientFormTabKeys(
canAccountingView: boolean,
options: { includeEditOnlyTabs?: boolean } = {},
): string[] {
const keys = ['information', 'contact', 'address', 'transport']
if (canAccountingView) {
keys.push('accounting')
}
if (options.includeEditOnlyTabs) {
keys.push(...CLIENT_FORM_EDIT_ONLY_TABS)
}
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
}