216 lines
8.3 KiB
TypeScript
216 lines
8.3 KiB
TypeScript
/**
|
|
* Regles metier pures de l'ecran « Ajouter un fournisseur » (M2 Commercial).
|
|
*
|
|
* Miroir de `clientFormRules.ts` (M1), centralisees ici (hors composant) pour
|
|
* rester testables unitairement et partagees entre la creation et les ecrans
|
|
* d'edition/consultation (95/96). Ces helpers ne touchent ni a l'API ni a l'etat
|
|
* reactif : ils prennent des brouillons « plats » et retournent des booleens.
|
|
*
|
|
* Le back reste la source de verite (les RG sont re-validees serveur, mode
|
|
* strict) ; ces regles ne servent qu'au feedback UI immediat (gating de boutons).
|
|
*
|
|
* Differences M2 vs M1 :
|
|
* - Adresse via enum `addressType` (PROSPECT/DEPART/RENDU, RG-2.09) — pas de
|
|
* drapeaux ni d'exclusivite a gerer cote front (le radio est exclusif par nature).
|
|
* - Pas d'email de facturation, pas de relation Distributeur/Courtier.
|
|
*/
|
|
|
|
import type { SupplierAddressType } from '~/modules/commercial/types/supplierForm'
|
|
|
|
/**
|
|
* Onglets « coquille » (non encore implementes) : frame vide, passage
|
|
* automatique a l'onglet suivant (aligne M1).
|
|
*/
|
|
export const SUPPLIER_FORM_PLACEHOLDER_TABS = ['transport', 'statistics', 'reports', 'exchanges'] as const
|
|
|
|
/**
|
|
* Onglets affiches uniquement en MODIFICATION/CONSULTATION (jamais a la
|
|
* creation) : Statistiques / Rapports / Echanges. A rebrancher dans les ecrans
|
|
* 95/96 via l'option `includeEditOnlyTabs`.
|
|
*/
|
|
export const SUPPLIER_FORM_EDIT_ONLY_TABS = ['statistics', 'reports', 'exchanges'] as const
|
|
|
|
/**
|
|
* Construit l'ordre des onglets du formulaire fournisseur.
|
|
* - L'onglet Comptabilite n'est present que si l'utilisateur a `accounting.view`
|
|
* (Bureau / Commerciale ne le voient pas).
|
|
* - Les onglets edit-only sont exclus par defaut (creation) ; passer
|
|
* `includeEditOnlyTabs: true` pour les afficher en modification/consultation.
|
|
* Ordre aligne sur la spec M2 § Ecran « Ajouter un fournisseur » (barre 5 onglets).
|
|
*/
|
|
export function buildSupplierFormTabKeys(
|
|
canAccountingView: boolean,
|
|
options: { includeEditOnlyTabs?: boolean } = {},
|
|
): string[] {
|
|
const keys = ['information', 'contacts', 'addresses', 'transport']
|
|
if (canAccountingView) {
|
|
keys.push('accounting')
|
|
}
|
|
if (options.includeEditOnlyTabs) {
|
|
keys.push(...SUPPLIER_FORM_EDIT_ONLY_TABS)
|
|
}
|
|
return keys
|
|
}
|
|
|
|
/**
|
|
* Dernier onglet REMPLISSABLE d'un jeu d'onglets : le dernier qui n'est pas un
|
|
* placeholder. Role-aware sans regle ad hoc — il suffit de lui passer les
|
|
* `tabKeys` deja filtres par permission. Sa validation marque la fin de l'ajout.
|
|
*/
|
|
export function lastFillableTabKey(tabKeys: string[]): string | undefined {
|
|
return [...tabKeys].reverse().find(
|
|
key => !(SUPPLIER_FORM_PLACEHOLDER_TABS as readonly string[]).includes(key),
|
|
)
|
|
}
|
|
|
|
/** Sous-ensemble d'un contact necessaire aux regles de nommage (RG-2.04/2.13). */
|
|
export interface ContactDraft {
|
|
firstName: string | null
|
|
lastName: string | null
|
|
}
|
|
|
|
/** 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-2.04 : 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-2.13 : l'onglet Contacts 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)
|
|
}
|
|
|
|
/**
|
|
* Primitive reutilisable : vrai si TOUTES les valeurs fournies sont vides. Sert a
|
|
* detecter un bloc de collection totalement vide (amorce non remplie). Un bloc qui
|
|
* porte la moindre donnee n'est PAS « blank » : il doit etre soumis pour declencher
|
|
* sa 422 inline plutot que d'etre saute silencieusement.
|
|
*/
|
|
export function isBlankRow(values: (string | null | undefined)[]): boolean {
|
|
return values.every(value => !isFilled(value))
|
|
}
|
|
|
|
/** Champs saisissables d'un bloc contact (pour detecter un bloc totalement vide). */
|
|
export interface ContactFillableDraft extends ContactDraft {
|
|
jobTitle: string | null
|
|
phonePrimary: string | null
|
|
phoneSecondary: string | null
|
|
email: string | null
|
|
}
|
|
|
|
/**
|
|
* Vrai si AUCUN champ saisissable du bloc contact n'est rempli. Distingue un bloc
|
|
* d'amorce vide (a ignorer au submit) d'un bloc partiellement rempli sans nom
|
|
* (email/telephone/fonction seul) : ce dernier doit etre soumis pour declencher la
|
|
* 422 RG-2.04 affichee inline.
|
|
*/
|
|
export function isContactBlank(contact: ContactFillableDraft): boolean {
|
|
return isBlankRow([
|
|
contact.firstName,
|
|
contact.lastName,
|
|
contact.jobTitle,
|
|
contact.phonePrimary,
|
|
contact.phoneSecondary,
|
|
contact.email,
|
|
])
|
|
}
|
|
|
|
/** Champs saisissables d'un bloc RIB (pour detecter un bloc totalement vide). */
|
|
export interface RibFillableDraft {
|
|
label: string | null
|
|
bic: string | null
|
|
iban: string | null
|
|
}
|
|
|
|
/**
|
|
* Vrai si AUCUN champ du bloc RIB n'est rempli. Un RIB partiellement rempli (ex.
|
|
* IBAN seul) n'est PAS « blank » : il doit etre soumis pour declencher les 422
|
|
* NotBlank inline plutot que d'etre saute silencieusement.
|
|
*/
|
|
export function isRibBlank(rib: RibFillableDraft): boolean {
|
|
return isBlankRow([rib.label, rib.bic, rib.iban])
|
|
}
|
|
|
|
/**
|
|
* RG-2.08 : un RIB est complet quand ses trois champs sont remplis (label, BIC,
|
|
* IBAN). Predicat partage entre le gating du bouton « + RIB » et la validation de
|
|
* l'onglet (au moins un RIB complet si reglement LCR).
|
|
*/
|
|
export function isRibComplete(rib: RibFillableDraft): boolean {
|
|
return isFilled(rib.label) && isFilled(rib.bic) && isFilled(rib.iban)
|
|
}
|
|
|
|
/**
|
|
* Sous-ensemble d'une adresse necessaire a sa validite par-bloc : type (enum),
|
|
* sites et categories rattaches.
|
|
*/
|
|
export interface AddressValidityDraft {
|
|
addressType: SupplierAddressType | null
|
|
categoryIris: string[]
|
|
siteIris: string[]
|
|
}
|
|
|
|
/**
|
|
* Validite par-bloc d'une adresse : type renseigne (RG-2.09), >= 1 site (RG-2.06)
|
|
* et >= 1 categorie (RG-2.10). Predicat partage entre le gating du bouton
|
|
* « + Adresse » (le dernier bloc doit etre valide avant d'en ajouter un autre) et
|
|
* la validation de l'onglet (toutes les adresses valides). Pas d'email de
|
|
* facturation cote fournisseur (difference M1).
|
|
*/
|
|
export function isAddressValid(address: AddressValidityDraft): boolean {
|
|
return address.addressType !== null
|
|
&& address.siteIris.length >= 1
|
|
&& address.categoryIris.length >= 1
|
|
}
|
|
|
|
/** Code stable du type de reglement « virement » (RG-2.07). */
|
|
const PAYMENT_TYPE_TRANSFER = 'VIREMENT'
|
|
|
|
/** Code stable du type de reglement « lettre de change » (RG-2.08). */
|
|
const PAYMENT_TYPE_LCR = 'LCR'
|
|
|
|
/** RG-2.07 : 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-2.08 : 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
|
|
}
|
|
|
|
// ── Champs requis adosses a une colonne NON-nullable (ERP-119) ───────────────
|
|
// Memes contraintes qu'au M1 : un champ requis (NotBlank) porte par une colonne
|
|
// Doctrine NON nullable rejette `null` en 400 de TYPE avant le Validator. Parade :
|
|
// OMETTRE la cle du payload quand elle est vide -> le back produit une 422 NotBlank
|
|
// avec propertyPath, mappee en rouge sous le champ.
|
|
export const MAIN_REQUIRED_NON_NULLABLE_KEYS = ['companyName'] as const
|
|
export const ADDRESS_REQUIRED_NON_NULLABLE_KEYS = ['postalCode', 'city', 'street'] as const
|
|
export const RIB_REQUIRED_NON_NULLABLE_KEYS = ['label', 'bic', 'iban'] as const
|
|
|
|
/**
|
|
* Retire d'un payload d'ecriture les cles requises laissees vides (null / '' /
|
|
* undefined), pour laisser le back produire une 422 NotBlank par champ plutot
|
|
* qu'un 400 de type sur une colonne non-nullable. Mute et retourne le payload.
|
|
*/
|
|
export function omitEmptyRequired<T extends Record<string, unknown>>(
|
|
payload: T,
|
|
requiredKeys: readonly string[],
|
|
): T {
|
|
for (const key of requiredKeys) {
|
|
const value = payload[key]
|
|
if (value === null || value === undefined || value === '') {
|
|
delete payload[key]
|
|
}
|
|
}
|
|
|
|
return payload
|
|
}
|