From c11d7822cea054f006daa61a3a3efdd0f44ed064 Mon Sep 17 00:00:00 2001 From: tristan Date: Fri, 19 Jun 2026 14:25:26 +0200 Subject: [PATCH] refactor(front) : champs anti-parasites via masks maska (filtrage natif, focus/curseur OK) au lieu du sanitizer @update ; email sans masque (ERP-193) --- .../components/ClientAddressBlock.vue | 25 +++--- .../components/ClientContactBlock.vue | 24 +++--- .../components/SupplierAddressBlock.vue | 24 +++--- .../components/SupplierContactBlock.vue | 24 +++--- .../commercial/pages/clients/[id]/edit.vue | 30 ++++---- .../modules/commercial/pages/clients/new.vue | 30 ++++---- .../commercial/pages/suppliers/[id]/edit.vue | 30 ++++---- .../commercial/pages/suppliers/new.vue | 30 ++++---- .../components/ProviderAddressBlock.vue | 22 +++--- .../components/ProviderContactBlock.vue | 24 +++--- .../technique/pages/providers/[id]/edit.vue | 22 +++--- .../modules/technique/pages/providers/new.vue | 22 +++--- .../components/CarrierAddressBlock.vue | 22 ++---- .../components/CarrierContactBlock.vue | 24 +++--- .../transport/pages/carriers/[id]/edit.vue | 6 +- .../modules/transport/pages/carriers/new.vue | 6 +- .../utils/__tests__/textSanitize.test.ts | 77 +++++++++---------- frontend/shared/utils/textSanitize.ts | 77 ++++++++----------- 18 files changed, 229 insertions(+), 290 deletions(-) diff --git a/frontend/modules/commercial/components/ClientAddressBlock.vue b/frontend/modules/commercial/components/ClientAddressBlock.vue index c1b6b0b..8d8296f 100644 --- a/frontend/modules/commercial/components/ClientAddressBlock.vue +++ b/frontend/modules/commercial/components/ClientAddressBlock.vue @@ -133,6 +133,7 @@ v-else :model-value="model.city" :label="t('commercial.clients.form.address.city')" + :mask="ADDRESS_MASK" :readonly="readonly" :disabled="disabled" :required="true" @@ -184,6 +185,7 @@ string>> = { - street: sanitizeAddress, - streetComplement: sanitizeAddress, - city: sanitizeAddress, - billingEmail: sanitizeEmail, - billingEmailSecondary: sanitizeEmail, -} +// Filtrage des caracteres parasites : porte par le mask ADDRESS_MASK (maska) sur +// les champs texte editables (complement, ville en mode degrade). La voie en +// autocomplete (BAN) et la ville en select ne sont pas masquees (le back valide +// via Assert\Regex) ; les emails de facturation valident leur format (Assert\Email). -/** Emet un nouveau brouillon avec le champ modifie (immutabilite), sanitise si besoin. */ +/** Emet un nouveau brouillon avec le champ modifie (immutabilite). */ function update(field: K, value: AddressFormDraft[K]): void { - const sanitizer = FIELD_SANITIZERS[field] - const next = (sanitizer && typeof value === 'string') - ? (sanitizer(value) as AddressFormDraft[K]) - : value - emit('update:modelValue', { ...props.modelValue, [field]: next }) + emit('update:modelValue', { ...props.modelValue, [field]: value }) } /** Revele le 2e champ email de facturation (clic sur le « + »). */ diff --git a/frontend/modules/commercial/components/ClientContactBlock.vue b/frontend/modules/commercial/components/ClientContactBlock.vue index 13c0915..cf67be4 100644 --- a/frontend/modules/commercial/components/ClientContactBlock.vue +++ b/frontend/modules/commercial/components/ClientContactBlock.vue @@ -15,6 +15,7 @@ import type { ContactFormDraft } from '~/modules/commercial/types/clientForm' -import { sanitizeEmail, sanitizeFreeText, sanitizePersonName } from '~/shared/utils/textSanitize' +import { FREE_TEXT_MASK, PERSON_NAME_MASK } from '~/shared/utils/textSanitize' // Masque telephone FR : 5 groupes de 2 chiffres (la normalisation finale reste // serveur, cf. formatPhoneFR re-applique a la valeur renvoyee). @@ -108,22 +111,13 @@ const { t } = useI18n() // Alias local pour la lisibilite du template. const model = computed(() => props.modelValue) -// Filtres de saisie par champ (ERP-193) : on retire les caracteres parasites a la -// frappe. Noms = profil personne, fonction = texte libre, email = profil email. -const FIELD_SANITIZERS: Partial string>> = { - lastName: sanitizePersonName, - firstName: sanitizePersonName, - jobTitle: sanitizeFreeText, - email: sanitizeEmail, -} +// Filtrage des caracteres parasites : porte par les masks maska sur les champs +// (PERSON_NAME_MASK / FREE_TEXT_MASK), filtrage natif au focus/curseur. L'email n'a +// pas de mask (ERP-101 : validation de format via Assert\Email + erreur inline). -/** Emet un nouveau brouillon avec le champ modifie (immutabilite), sanitise si besoin. */ +/** Emet un nouveau brouillon avec le champ modifie (immutabilite). */ function update(field: K, value: ContactFormDraft[K]): void { - const sanitizer = FIELD_SANITIZERS[field] - const next = (sanitizer && typeof value === 'string') - ? (sanitizer(value) as ContactFormDraft[K]) - : value - emit('update:modelValue', { ...props.modelValue, [field]: next }) + emit('update:modelValue', { ...props.modelValue, [field]: value }) } /** Revele le 2e numero (RG-1.02/1.20 : max 1 secondaire, le « + » disparait). */ diff --git a/frontend/modules/commercial/components/SupplierAddressBlock.vue b/frontend/modules/commercial/components/SupplierAddressBlock.vue index 2bdf72a..a109c46 100644 --- a/frontend/modules/commercial/components/SupplierAddressBlock.vue +++ b/frontend/modules/commercial/components/SupplierAddressBlock.vue @@ -104,6 +104,7 @@ v-else :model-value="model.city" :label="t('commercial.suppliers.form.address.city')" + :mask="ADDRESS_MASK" :readonly="readonly" :disabled="disabled" :required="true" @@ -135,6 +136,7 @@ v-else :model-value="model.street" :label="t('commercial.suppliers.form.address.street')" + :mask="ADDRESS_MASK" :readonly="readonly" :disabled="disabled" :required="true" @@ -147,6 +149,7 @@ string>> = { - street: sanitizeAddress, - streetComplement: sanitizeAddress, - city: sanitizeAddress, -} +// Filtrage des caracteres parasites : porte par le mask ADDRESS_MASK (maska) sur +// les champs texte editables (complement, ville en mode degrade, voie en repli). La +// voie en autocomplete (BAN) et la ville en select ne sont pas masquees (le back +// valide via Assert\Regex). -/** Emet un nouveau brouillon avec le champ modifie (immutabilite), sanitise si besoin. */ +/** Emet un nouveau brouillon avec le champ modifie (immutabilite). */ function update(field: K, value: SupplierAddressFormDraft[K]): void { - const sanitizer = FIELD_SANITIZERS[field] - const next = (sanitizer && typeof value === 'string') - ? (sanitizer(value) as SupplierAddressFormDraft[K]) - : value - emit('update:modelValue', { ...props.modelValue, [field]: next }) + emit('update:modelValue', { ...props.modelValue, [field]: value }) } /** Previent le parent (toast unique) que l'autocompletion est indisponible. */ diff --git a/frontend/modules/commercial/components/SupplierContactBlock.vue b/frontend/modules/commercial/components/SupplierContactBlock.vue index 22633e0..5f5b0c7 100644 --- a/frontend/modules/commercial/components/SupplierContactBlock.vue +++ b/frontend/modules/commercial/components/SupplierContactBlock.vue @@ -14,6 +14,7 @@ import type { SupplierContactFormDraft } from '~/modules/commercial/types/supplierForm' -import { sanitizeEmail, sanitizeFreeText, sanitizePersonName } from '~/shared/utils/textSanitize' +import { FREE_TEXT_MASK, PERSON_NAME_MASK } from '~/shared/utils/textSanitize' // Masque telephone FR : 5 groupes de 2 chiffres (la normalisation finale reste serveur). const PHONE_MASK = '## ## ## ## ##' @@ -106,22 +109,13 @@ const { t } = useI18n() // Alias local pour la lisibilite du template. const model = computed(() => props.modelValue) -// Filtres de saisie par champ (ERP-193) : on retire les caracteres parasites a la -// frappe. Noms = profil personne, fonction = texte libre, email = profil email. -const FIELD_SANITIZERS: Partial string>> = { - lastName: sanitizePersonName, - firstName: sanitizePersonName, - jobTitle: sanitizeFreeText, - email: sanitizeEmail, -} +// Filtrage des caracteres parasites : porte par les masks maska sur les champs +// (PERSON_NAME_MASK / FREE_TEXT_MASK), filtrage natif au focus/curseur. L'email n'a +// pas de mask (ERP-101 : validation de format via Assert\Email + erreur inline). -/** Emet un nouveau brouillon avec le champ modifie (immutabilite), sanitise si besoin. */ +/** Emet un nouveau brouillon avec le champ modifie (immutabilite). */ function update(field: K, value: SupplierContactFormDraft[K]): void { - const sanitizer = FIELD_SANITIZERS[field] - const next = (sanitizer && typeof value === 'string') - ? (sanitizer(value) as SupplierContactFormDraft[K]) - : value - emit('update:modelValue', { ...props.modelValue, [field]: next }) + emit('update:modelValue', { ...props.modelValue, [field]: value }) } /** Revele le 2e numero (max 1 secondaire, le « + » disparait). */ diff --git a/frontend/modules/commercial/pages/clients/[id]/edit.vue b/frontend/modules/commercial/pages/clients/[id]/edit.vue index 77475e3..4506f0d 100644 --- a/frontend/modules/commercial/pages/clients/[id]/edit.vue +++ b/frontend/modules/commercial/pages/clients/[id]/edit.vue @@ -24,8 +24,8 @@ `manage` (ex. Compta). -->
@@ -102,11 +102,11 @@ @update:model-value="onRevenueAmountInput" />
@@ -428,7 +428,7 @@ import { } from '~/modules/commercial/types/supplierForm' import { extractApiErrorMessage } from '~/shared/utils/api' import { isRowRemovable, removeCollectionRow } from '~/shared/utils/collectionRow' -import { sanitizeCodeAlnum, sanitizeFreeText, sanitizePersonName } from '~/shared/utils/textSanitize' +import { CODE_ALNUM_MASK, FREE_TEXT_MASK, PERSON_NAME_MASK } from '~/shared/utils/textSanitize' import { readHistoryTab } from '~/shared/utils/historyTab' // Masques de saisie (la normalisation finale reste serveur). diff --git a/frontend/modules/commercial/pages/suppliers/new.vue b/frontend/modules/commercial/pages/suppliers/new.vue index 1ec109d..cb482d4 100644 --- a/frontend/modules/commercial/pages/suppliers/new.vue +++ b/frontend/modules/commercial/pages/suppliers/new.vue @@ -18,12 +18,12 @@ automatiquement sur l'onglet Information. Pas de contact inline (ERP-106). -->
@@ -96,11 +96,11 @@ @update:model-value="onRevenueAmountInput" />
@@ -401,7 +401,7 @@ import { } from '~/modules/commercial/types/supplierForm' import { extractApiErrorMessage } from '~/shared/utils/api' import { isRowRemovable } from '~/shared/utils/collectionRow' -import { sanitizeCodeAlnum, sanitizeFreeText, sanitizePersonName } from '~/shared/utils/textSanitize' +import { CODE_ALNUM_MASK, FREE_TEXT_MASK, PERSON_NAME_MASK } from '~/shared/utils/textSanitize' // Masques de saisie (la normalisation finale reste serveur). const SIREN_MASK = '#########' diff --git a/frontend/modules/technique/components/ProviderAddressBlock.vue b/frontend/modules/technique/components/ProviderAddressBlock.vue index 311f956..4c60904 100644 --- a/frontend/modules/technique/components/ProviderAddressBlock.vue +++ b/frontend/modules/technique/components/ProviderAddressBlock.vue @@ -85,6 +85,7 @@ v-else :model-value="model.city" :label="t('technique.providers.form.address.city')" + :mask="ADDRESS_MASK" :readonly="readonly" :disabled="disabled" :required="true" @@ -128,6 +129,7 @@ string>> = { - street: sanitizeAddress, - streetComplement: sanitizeAddress, - city: sanitizeAddress, -} +// Filtrage des caracteres parasites : porte par le mask ADDRESS_MASK (maska) sur +// les champs texte editables (complement, ville en mode degrade). La voie en +// autocomplete (BAN) et la ville en select ne sont pas masquees (le back valide +// via Assert\Regex). -/** Emet un nouveau brouillon avec le champ modifie (immutabilite), sanitise si besoin. */ +/** Emet un nouveau brouillon avec le champ modifie (immutabilite). */ function update(field: K, value: ProviderAddressFormDraft[K]): void { - const sanitizer = FIELD_SANITIZERS[field] - const next = (sanitizer && typeof value === 'string') - ? (sanitizer(value) as ProviderAddressFormDraft[K]) - : value - emit('update:modelValue', { ...props.modelValue, [field]: next }) + emit('update:modelValue', { ...props.modelValue, [field]: value }) } /** Previent le parent (toast unique) que l'autocompletion est indisponible. */ diff --git a/frontend/modules/technique/components/ProviderContactBlock.vue b/frontend/modules/technique/components/ProviderContactBlock.vue index 6fc23fb..2b092f3 100644 --- a/frontend/modules/technique/components/ProviderContactBlock.vue +++ b/frontend/modules/technique/components/ProviderContactBlock.vue @@ -14,6 +14,7 @@ import type { ProviderContactFormDraft } from '~/modules/technique/types/providerForm' -import { sanitizeEmail, sanitizeFreeText, sanitizePersonName } from '~/shared/utils/textSanitize' +import { FREE_TEXT_MASK, PERSON_NAME_MASK } from '~/shared/utils/textSanitize' // Masque telephone FR : 5 groupes de 2 chiffres (la normalisation finale reste serveur). const PHONE_MASK = '## ## ## ## ##' @@ -105,22 +108,13 @@ const { t } = useI18n() // Alias local pour la lisibilite du template. const model = computed(() => props.modelValue) -// Filtres de saisie par champ (ERP-193) : on retire les caracteres parasites a la -// frappe. Noms = profil personne, fonction = texte libre, email = profil email. -const FIELD_SANITIZERS: Partial string>> = { - lastName: sanitizePersonName, - firstName: sanitizePersonName, - jobTitle: sanitizeFreeText, - email: sanitizeEmail, -} +// Filtrage des caracteres parasites : porte par les masks maska sur les champs +// (PERSON_NAME_MASK / FREE_TEXT_MASK), filtrage natif au focus/curseur. L'email n'a +// pas de mask (ERP-101 : validation de format via Assert\Email + erreur inline). -/** Emet un nouveau brouillon avec le champ modifie (immutabilite), sanitise si besoin. */ +/** Emet un nouveau brouillon avec le champ modifie (immutabilite). */ function update(field: K, value: ProviderContactFormDraft[K]): void { - const sanitizer = FIELD_SANITIZERS[field] - const next = (sanitizer && typeof value === 'string') - ? (sanitizer(value) as ProviderContactFormDraft[K]) - : value - emit('update:modelValue', { ...props.modelValue, [field]: next }) + emit('update:modelValue', { ...props.modelValue, [field]: value }) } /** Revele le 2e numero (max 1 secondaire, le « + » disparait). */ diff --git a/frontend/modules/technique/pages/providers/[id]/edit.vue b/frontend/modules/technique/pages/providers/[id]/edit.vue index 2602371..bd88b96 100644 --- a/frontend/modules/technique/pages/providers/[id]/edit.vue +++ b/frontend/modules/technique/pages/providers/[id]/edit.vue @@ -20,12 +20,12 @@
@@ -318,7 +318,7 @@ import { } from '~/modules/technique/types/providerForm' import { extractApiErrorMessage } from '~/shared/utils/api' import { isRowRemovable } from '~/shared/utils/collectionRow' -import { sanitizeCodeAlnum, sanitizeFreeText } from '~/shared/utils/textSanitize' +import { CODE_ALNUM_MASK, FREE_TEXT_MASK } from '~/shared/utils/textSanitize' // Masque SIREN : 9 chiffres (la normalisation finale reste serveur). const SIREN_MASK = '#########' diff --git a/frontend/modules/technique/pages/providers/new.vue b/frontend/modules/technique/pages/providers/new.vue index 69ad7ee..4088215 100644 --- a/frontend/modules/technique/pages/providers/new.vue +++ b/frontend/modules/technique/pages/providers/new.vue @@ -19,12 +19,12 @@ Selecteur de site present ici (RG-3.03, relation directe). -->
@@ -302,7 +302,7 @@ import { } from '~/modules/technique/utils/forms/providerAccounting' import { extractApiErrorMessage } from '~/shared/utils/api' import { isRowRemovable } from '~/shared/utils/collectionRow' -import { sanitizeCodeAlnum, sanitizeFreeText } from '~/shared/utils/textSanitize' +import { CODE_ALNUM_MASK, FREE_TEXT_MASK } from '~/shared/utils/textSanitize' // Masque SIREN : 9 chiffres (la normalisation finale reste serveur). const SIREN_MASK = '#########' diff --git a/frontend/modules/transport/components/CarrierAddressBlock.vue b/frontend/modules/transport/components/CarrierAddressBlock.vue index 3cae419..b25bfe8 100644 --- a/frontend/modules/transport/components/CarrierAddressBlock.vue +++ b/frontend/modules/transport/components/CarrierAddressBlock.vue @@ -42,6 +42,7 @@ v-else :model-value="model.city" :label="t('transport.carriers.form.address.city')" + :mask="ADDRESS_MASK" :readonly="readonly" :disabled="disabled" :required="true" @@ -87,6 +88,7 @@ import { useAddressAutocomplete, type AddressSuggestion } from '~/shared/composables/useAddressAutocomplete' import type { CarrierAddressFormDraft } from '~/modules/transport/types/carrierForm' -import { sanitizeAddress } from '~/shared/utils/textSanitize' +import { ADDRESS_MASK } from '~/shared/utils/textSanitize' interface RefOption { value: string @@ -160,21 +162,13 @@ const addressLoading = ref(false) // Conserve les suggestions d'adresse pour retrouver ville/CP au moment du select. let lastAddressSuggestions: AddressSuggestion[] = [] -// Filtres de saisie par champ (ERP-193) : voie / complement / ville = profil -// adresse. Le code postal (masque numerique) n'est pas filtre ici. -const FIELD_SANITIZERS: Partial string>> = { - street: sanitizeAddress, - streetComplement: sanitizeAddress, - city: sanitizeAddress, -} +// Filtrage des caracteres parasites : porte par le mask ADDRESS_MASK (maska) sur les +// champs texte editables (complement, ville en mode degrade). La voie en autocomplete +// (BAN) et la ville en select ne sont pas masquees (le back valide via Assert\Regex). -/** Emet un nouveau brouillon avec le champ modifie (immutabilite), sanitise si besoin. */ +/** Emet un nouveau brouillon avec le champ modifie (immutabilite). */ function update(field: K, value: CarrierAddressFormDraft[K]): void { - const sanitizer = FIELD_SANITIZERS[field] - const next = (sanitizer && typeof value === 'string') - ? (sanitizer(value) as CarrierAddressFormDraft[K]) - : value - emit('update:modelValue', { ...props.modelValue, [field]: next }) + emit('update:modelValue', { ...props.modelValue, [field]: value }) } /** Previent le parent (toast unique) que l'autocompletion est indisponible. */ diff --git a/frontend/modules/transport/components/CarrierContactBlock.vue b/frontend/modules/transport/components/CarrierContactBlock.vue index cf6f189..143166f 100644 --- a/frontend/modules/transport/components/CarrierContactBlock.vue +++ b/frontend/modules/transport/components/CarrierContactBlock.vue @@ -14,6 +14,7 @@ import type { CarrierContactFormDraft } from '~/modules/transport/types/carrierForm' -import { sanitizeEmail, sanitizeFreeText, sanitizePersonName } from '~/shared/utils/textSanitize' +import { FREE_TEXT_MASK, PERSON_NAME_MASK } from '~/shared/utils/textSanitize' // Masque téléphone FR : 5 groupes de 2 chiffres (la normalisation finale reste serveur). const PHONE_MASK = '## ## ## ## ##' @@ -105,22 +108,13 @@ const { t } = useI18n() // Alias local pour la lisibilité du template. const model = computed(() => props.modelValue) -// Filtres de saisie par champ (ERP-193) : on retire les caractères parasites à la -// frappe. Noms = profil personne, fonction = texte libre, email = profil email. -const FIELD_SANITIZERS: Partial string>> = { - lastName: sanitizePersonName, - firstName: sanitizePersonName, - jobTitle: sanitizeFreeText, - email: sanitizeEmail, -} +// Filtrage des caractères parasites : porté par les masks maska sur les champs +// (PERSON_NAME_MASK / FREE_TEXT_MASK), filtrage natif au focus/curseur. L'email n'a +// pas de mask (ERP-101 : validation de format via Assert\Email + erreur inline). -/** Émet un nouveau brouillon avec le champ modifié (immutabilité), sanitisé si besoin. */ +/** Émet un nouveau brouillon avec le champ modifié (immutabilité). */ function update(field: K, value: CarrierContactFormDraft[K]): void { - const sanitizer = FIELD_SANITIZERS[field] - const next = (sanitizer && typeof value === 'string') - ? (sanitizer(value) as CarrierContactFormDraft[K]) - : value - emit('update:modelValue', { ...props.modelValue, [field]: next }) + emit('update:modelValue', { ...props.modelValue, [field]: value }) } /** Révèle le 2e numéro (max 1 secondaire, le « + » disparaît). */ diff --git a/frontend/modules/transport/pages/carriers/[id]/edit.vue b/frontend/modules/transport/pages/carriers/[id]/edit.vue index b36a921..4f8405f 100644 --- a/frontend/modules/transport/pages/carriers/[id]/edit.vue +++ b/frontend/modules/transport/pages/carriers/[id]/edit.vue @@ -19,8 +19,8 @@
{ +/** Reproduit le traitement maska au runtime (MaskInput) : preProcess puis masked. */ +function apply(mask: MaskInputOptions, value: string): string { + const pre = mask.preProcess ? mask.preProcess(value) : value + return new Mask(mask).masked(pre) +} + +describe('PERSON_NAME_MASK', () => { it('garde lettres accentuees, espace, apostrophe, tiret, point', () => { - expect(sanitizePersonName('Jean-Pierre')).toBe('Jean-Pierre') - expect(sanitizePersonName('O’Brien')).toBe('O’Brien') - expect(sanitizePersonName("D'Angelo")).toBe("D'Angelo") - expect(sanitizePersonName('Saint-Étienne J.')).toBe('Saint-Étienne J.') + expect(apply(PERSON_NAME_MASK, 'Jean-Pierre')).toBe('Jean-Pierre') + expect(apply(PERSON_NAME_MASK, 'O’Brien')).toBe('O’Brien') + expect(apply(PERSON_NAME_MASK, "D'Angelo")).toBe("D'Angelo") + expect(apply(PERSON_NAME_MASK, 'Saint-Étienne J.')).toBe('Saint-Étienne J.') }) - it('retire chiffres et caracteres parasites', () => { - expect(sanitizePersonName('Dupont²³')).toBe('Dupont') - expect(sanitizePersonName('Jean§&#~|')).toBe('Jean') - expect(sanitizePersonName('Marie123')).toBe('Marie') + it('retire chiffres et caracteres parasites (ou qu\'ils soient)', () => { + expect(apply(PERSON_NAME_MASK, 'Dupont²³')).toBe('Dupont') + expect(apply(PERSON_NAME_MASK, 'Jean§&#~|')).toBe('Jean') + expect(apply(PERSON_NAME_MASK, 'Ma§rie123')).toBe('Marie') // parasite AU MILIEU }) }) -describe('sanitizeFreeText', () => { - it('garde &, /, parentheses, degre, chiffres (raison sociale / fonction)', () => { - expect(sanitizeFreeText('Dupont & Fils')).toBe('Dupont & Fils') - expect(sanitizeFreeText('Resp. Achats/Ventes')).toBe('Resp. Achats/Ventes') - expect(sanitizeFreeText('SARL Léon (Pôle n°2)')).toBe('SARL Léon (Pôle n°2)') +describe('FREE_TEXT_MASK', () => { + it('garde &, /, parentheses, degre, chiffres', () => { + expect(apply(FREE_TEXT_MASK, 'Dupont & Fils')).toBe('Dupont & Fils') + expect(apply(FREE_TEXT_MASK, 'Resp. Achats/Ventes')).toBe('Resp. Achats/Ventes') + expect(apply(FREE_TEXT_MASK, 'SARL Léon (Pôle n°2)')).toBe('SARL Léon (Pôle n°2)') }) it('retire les parasites ²³§~#|', () => { - expect(sanitizeFreeText('ACME²³§')).toBe('ACME') - expect(sanitizeFreeText('Test~#|<>{}')).toBe('Test') + expect(apply(FREE_TEXT_MASK, 'ACME²³§')).toBe('ACME') + expect(apply(FREE_TEXT_MASK, 'Te~#|st<>{}')).toBe('Test') }) }) -describe('sanitizeAddress', () => { +describe('ADDRESS_MASK', () => { it('garde chiffres, virgule, point, apostrophe, slash, degre, tiret', () => { - expect(sanitizeAddress('12 bis, rue de l’Église')).toBe('12 bis, rue de l’Église') - expect(sanitizeAddress('Bât. n°3 - Zone A/B')).toBe('Bât. n°3 - Zone A/B') + expect(apply(ADDRESS_MASK, '12 bis, rue de l’Église')).toBe('12 bis, rue de l’Église') + expect(apply(ADDRESS_MASK, 'Bât. n°3 - Zone A/B')).toBe('Bât. n°3 - Zone A/B') }) it('retire les parasites', () => { - expect(sanitizeAddress('5 rue X²³§&')).toBe('5 rue X') + expect(apply(ADDRESS_MASK, '5 rue X²³§&')).toBe('5 rue X') }) }) -describe('sanitizeEmail', () => { - it('garde les caracteres email valides', () => { - expect(sanitizeEmail('jean.dupont+pro@acme-corp.fr')).toBe('jean.dupont+pro@acme-corp.fr') - }) - - it('retire espaces et parasites', () => { - expect(sanitizeEmail('jean §² dupont@acme.fr')).toBe('jeandupont@acme.fr') - expect(sanitizeEmail('a&b#c@x.fr')).toBe('abc@x.fr') - }) -}) - -describe('sanitizeCodeAlnum', () => { +describe('CODE_ALNUM_MASK', () => { it('force la majuscule et ne garde que A-Z 0-9', () => { - expect(sanitizeCodeAlnum('411dupont')).toBe('411DUPONT') - expect(sanitizeCodeAlnum('FR 12 345')).toBe('FR12345') - expect(sanitizeCodeAlnum('4-11.000§')).toBe('411000') + expect(apply(CODE_ALNUM_MASK, '411dupont')).toBe('411DUPONT') + expect(apply(CODE_ALNUM_MASK, 'FR 12 345')).toBe('FR12345') + expect(apply(CODE_ALNUM_MASK, '4-11.000§')).toBe('411000') }) it('chaine vide reste vide', () => { - expect(sanitizeCodeAlnum('')).toBe('') + expect(apply(CODE_ALNUM_MASK, '')).toBe('') }) }) diff --git a/frontend/shared/utils/textSanitize.ts b/frontend/shared/utils/textSanitize.ts index a225302..b7c170b 100644 --- a/frontend/shared/utils/textSanitize.ts +++ b/frontend/shared/utils/textSanitize.ts @@ -1,58 +1,47 @@ /** - * Filtres de saisie texte (retour metier ERP-193) : on retire a la frappe / au - * collage les caracteres parasites (« ²³§~#| … ») des champs texte libres. + * Masks de saisie texte (retour metier ERP-193) : filtrage NATIF (maska) des + * caracteres parasites (« ²³§~#| … ») dans les champs texte libres. maska gere le + * focus et le curseur (contrairement a un nettoyage manuel sur @update qui laissait + * le caractere affiche jusqu'a la frappe suivante). * * Miroir FRONT des patterns back `App\Shared\Domain\Validation\TextInputPattern` * (allow-list par famille de champ). Le back reste l'autorite (Assert\Regex → - * 422 inline via useFormErrors) ; ces fonctions ne font que le confort de saisie. - * Purs / testables. + * 422 inline via useFormErrors) ; ces masks ne font que le confort de saisie. * - * IMPORTANT : garder les classes de caracteres STRICTEMENT alignees sur le back - * (toute divergence = soit un caractere bloque au front mais accepte au back, soit - * l'inverse → 422 surprise). + * IMPORTANT : garder les classes de caracteres STRICTEMENT alignees sur le back. + * + * L'EMAIL n'a PAS de mask (decision ERP-101 : un email n'a pas de structure fixe, + * on valide le FORMAT via Assert\Email + erreur inline, jamais via un masque). */ +import type { MaskInputOptions } from 'maska' /** - * Noms de personnes (Nom, Prenom, Dirigeant) : lettres (accents), espace, - * apostrophe droite/courbe, tiret, point. + * Construit un mask maska « jeu de caracteres autorise, longueur libre » : + * - `preProcess` retire d'abord TOUT caractere hors charset, OU QU'IL SOIT (un + * masque positionnel seul s'arreterait au 1er caractere invalide car le token + * `multiple` est glouton) ; + * - le token `P` (`multiple`) laisse ensuite passer le reste, sans limite de longueur. + * + * @param pattern classe des caracteres AUTORISES (1 caractere, sans flag global) + * @param strip negation de `pattern`, flag global (retire les interdits) + * @param upper force la majuscule (codes : n° compte / TVA / IBAN / BIC) */ -export function sanitizePersonName(value: string): string { - return value.replace(/[^\p{L}\p{M} '’.-]/gu, '') +function charsetMask(pattern: RegExp, strip: RegExp, upper = false): MaskInputOptions { + return { + mask: 'P', + tokens: { P: { pattern, multiple: true } }, + preProcess: (v: string) => (upper ? v.toUpperCase() : v).replace(strip, ''), + } } -/** - * Texte societe / libre (Raison sociale, Concurrents, Fonction) : nom + chiffres, - * virgule, esperluette, slash, parentheses, degre. - */ -export function sanitizeFreeText(value: string): string { - // 0-9 (et pas \p{N}) : \p{N} engloberait les exposants ² ³ — justement parasites. - return value.replace(/[^\p{L}\p{M}0-9 '’.,&/()°-]/gu, '') -} +/** Noms de personnes (Nom, Prenom, Dirigeant) : lettres (accents), espace, apostrophe, tiret, point. */ +export const PERSON_NAME_MASK = charsetMask(/[\p{L}\p{M} '’.-]/u, /[^\p{L}\p{M} '’.-]/gu) -/** - * Adresse (voie, complement, ville) : lettres, chiffres, espace, apostrophe, - * point, virgule, slash, degre, tiret. - */ -export function sanitizeAddress(value: string): string { - // 0-9 (et pas \p{N}) : evite de laisser passer les exposants ² ³. - return value.replace(/[^\p{L}\p{M}0-9 '’.,/°-]/gu, '') -} +/** Texte societe / libre (Raison sociale, Concurrents, Fonction) : + chiffres, virgule, &, /, parentheses, degre. */ +export const FREE_TEXT_MASK = charsetMask(/[\p{L}\p{M}0-9 '’.,&/()°-]/u, /[^\p{L}\p{M}0-9 '’.,&/()°-]/gu) -/** - * Codes alphanumeriques majuscules (N° de compte comptable, N° de TVA, IBAN, BIC) : - * uniquement A-Z et 0-9, majuscule forcee. - */ -export function sanitizeCodeAlnum(value: string): string { - return value.toUpperCase().replace(/[^A-Z0-9]/g, '') -} +/** Adresse (voie, complement, ville) : lettres, chiffres, espace, apostrophe, point, virgule, slash, degre, tiret. */ +export const ADDRESS_MASK = charsetMask(/[\p{L}\p{M}0-9 '’.,/°-]/u, /[^\p{L}\p{M}0-9 '’.,/°-]/gu) -/** - * Email : retire espaces et caracteres impossibles dans une adresse, en gardant - * le jeu de caracteres email valides (lettres, chiffres, @ . _ % + - '). La - * validation de FORMAT reste au back (Assert\Email) ; ici on bloque juste les - * parasites (« ²³§~#| … ») a la frappe. La normalisation lowercase est portee par - * MalioInputEmail (prop `lowercase`), on ne la duplique pas. - */ -export function sanitizeEmail(value: string): string { - return value.replace(/[^A-Za-z0-9@._%+'-]/g, '') -} +/** Codes alphanumeriques majuscules (N° de compte, N° de TVA, IBAN, BIC) : A-Z et 0-9, majuscule forcee. */ +export const CODE_ALNUM_MASK = charsetMask(/[A-Z0-9]/, /[^A-Z0-9]/g, true)