refactor(front) : champs anti-parasites via masks maska (filtrage natif, focus/curseur OK) au lieu du sanitizer @update ; email sans masque (ERP-193)
Pull Request — Quality gate / Backend (PHP CS + PHPUnit) (pull_request) Has been cancelled
Pull Request — Quality gate / Frontend (lint + Vitest + build) (pull_request) Has been cancelled

This commit is contained in:
2026-06-19 14:25:26 +02:00
parent e66615d40b
commit c11d7822ce
18 changed files with 229 additions and 290 deletions
@@ -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 @@
<MalioInputText
:model-value="model.streetComplement"
:label="t('commercial.clients.form.address.streetComplement')"
:mask="ADDRESS_MASK"
:readonly="readonly"
:disabled="disabled"
:error="errors?.streetComplement"
@@ -204,7 +206,7 @@ import {
import { useAddressAutocomplete, type AddressSuggestion } from '~/shared/composables/useAddressAutocomplete'
import type { CategoryOption, RefOption } from '~/modules/commercial/composables/useClientReferentials'
import type { AddressFormDraft } from '~/modules/commercial/types/clientForm'
import { sanitizeAddress, sanitizeEmail } from '~/shared/utils/textSanitize'
import { ADDRESS_MASK } from '~/shared/utils/textSanitize'
// Masque code postal FR : 5 chiffres.
const POSTAL_CODE_MASK = '#####'
@@ -300,23 +302,14 @@ 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, emails de facturation = profil email.
const FIELD_SANITIZERS: Partial<Record<keyof AddressFormDraft, (v: string) => 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<K extends keyof AddressFormDraft>(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 « + »). */
@@ -15,6 +15,7 @@
<MalioInputText
:model-value="model.lastName"
:label="t('commercial.clients.form.contact.lastName')"
:mask="PERSON_NAME_MASK"
:readonly="readonly"
:disabled="disabled"
:error="errors?.lastName"
@@ -23,6 +24,7 @@
<MalioInputText
:model-value="model.firstName"
:label="t('commercial.clients.form.contact.firstName')"
:mask="PERSON_NAME_MASK"
:readonly="readonly"
:disabled="disabled"
:error="errors?.firstName"
@@ -35,6 +37,7 @@
<MalioInputText
:model-value="model.jobTitle"
:label="t('commercial.clients.form.contact.jobTitle')"
:mask="FREE_TEXT_MASK"
:readonly="readonly"
:disabled="disabled"
:error="errors?.jobTitle"
@@ -77,7 +80,7 @@
<script setup lang="ts">
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<Record<keyof ContactFormDraft, (v: string) => 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<K extends keyof ContactFormDraft>(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). */
@@ -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 @@
<MalioInputText
:model-value="model.streetComplement"
:label="t('commercial.suppliers.form.address.streetComplement')"
:mask="ADDRESS_MASK"
:readonly="readonly"
:disabled="disabled"
:error="errors?.streetComplement"
@@ -182,7 +185,7 @@
import { useAddressAutocomplete, type AddressSuggestion } from '~/shared/composables/useAddressAutocomplete'
import type { CategoryOption, RefOption } from '~/modules/commercial/composables/useSupplierReferentials'
import type { SupplierAddressFormDraft, SupplierAddressType } from '~/modules/commercial/types/supplierForm'
import { sanitizeAddress } from '~/shared/utils/textSanitize'
import { ADDRESS_MASK } from '~/shared/utils/textSanitize'
// Masque code postal FR : 5 chiffres.
const POSTAL_CODE_MASK = '#####'
@@ -254,21 +257,14 @@ 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. Les autres champs (CP, bennes, selects) ne sont pas filtres ici.
const FIELD_SANITIZERS: Partial<Record<keyof SupplierAddressFormDraft, (v: string) => 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<K extends keyof SupplierAddressFormDraft>(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. */
@@ -14,6 +14,7 @@
<MalioInputText
:model-value="model.lastName"
:label="t('commercial.suppliers.form.contact.lastName')"
:mask="PERSON_NAME_MASK"
:readonly="readonly"
:disabled="disabled"
:error="errors?.lastName"
@@ -22,6 +23,7 @@
<MalioInputText
:model-value="model.firstName"
:label="t('commercial.suppliers.form.contact.firstName')"
:mask="PERSON_NAME_MASK"
:readonly="readonly"
:disabled="disabled"
:error="errors?.firstName"
@@ -34,6 +36,7 @@
<MalioInputText
:model-value="model.jobTitle"
:label="t('commercial.suppliers.form.contact.jobTitle')"
:mask="FREE_TEXT_MASK"
:readonly="readonly"
:disabled="disabled"
:error="errors?.jobTitle"
@@ -76,7 +79,7 @@
<script setup lang="ts">
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<Record<keyof SupplierContactFormDraft, (v: string) => 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<K extends keyof SupplierContactFormDraft>(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). */
@@ -24,8 +24,8 @@
`manage` (ex. Compta). -->
<div class="mt-[48px] grid grid-cols-3 xl:grid-cols-4 gap-x-[44px] gap-y-4">
<MalioInputText
:model-value="main.companyName"
@update:model-value="(v: string) => main.companyName = sanitizeFreeText(v)"
v-model="main.companyName"
:mask="FREE_TEXT_MASK"
:label="t('commercial.clients.form.main.companyName')"
:required="true"
:disabled="businessReadonly"
@@ -106,8 +106,8 @@
:error="informationErrors.errors.description"
/>
<MalioInputText
:model-value="information.competitors"
@update:model-value="(v: string) => information.competitors = sanitizeFreeText(v)"
v-model="information.competitors"
:mask="FREE_TEXT_MASK"
:label="t('commercial.clients.form.information.competitors')"
:disabled="businessReadonly"
:error="informationErrors.errors.competitors"
@@ -141,8 +141,8 @@
@update:model-value="onRevenueAmountInput"
/>
<MalioInputText
:model-value="information.directorName"
@update:model-value="(v: string) => information.directorName = sanitizePersonName(v)"
v-model="information.directorName"
:mask="PERSON_NAME_MASK"
:label="t('commercial.clients.form.information.directorName')"
:disabled="businessReadonly"
:error="informationErrors.errors.directorName"
@@ -254,8 +254,8 @@
:error="accountingErrors.errors.siren"
/>
<MalioInputText
:model-value="accounting.accountNumber"
@update:model-value="(v: string) => accounting.accountNumber = sanitizeCodeAlnum(v)"
v-model="accounting.accountNumber"
:mask="CODE_ALNUM_MASK"
:label="t('commercial.clients.form.accounting.accountNumber')"
:disabled="accountingReadonly"
:required="true"
@@ -272,8 +272,8 @@
@update:model-value="(v: string | number | null) => accounting.tvaModeIri = v === null ? null : String(v)"
/>
<MalioInputText
:model-value="accounting.nTva"
@update:model-value="(v: string) => accounting.nTva = sanitizeCodeAlnum(v)"
v-model="accounting.nTva"
:mask="CODE_ALNUM_MASK"
:label="t('commercial.clients.form.accounting.nTva')"
:disabled="accountingReadonly"
:required="true"
@@ -336,16 +336,16 @@
:error="ribErrors[index]?.label"
/>
<MalioInputText
:model-value="rib.bic"
@update:model-value="(v: string) => rib.bic = sanitizeCodeAlnum(v)"
v-model="rib.bic"
:mask="CODE_ALNUM_MASK"
:label="t('commercial.clients.form.accounting.ribBic')"
:disabled="accountingReadonly"
:required="isRibRequired"
:error="ribErrors[index]?.bic"
/>
<MalioInputText
:model-value="rib.iban"
@update:model-value="(v: string) => rib.iban = sanitizeCodeAlnum(v)"
v-model="rib.iban"
:mask="CODE_ALNUM_MASK"
:label="t('commercial.clients.form.accounting.ribIban')"
:disabled="accountingReadonly"
:required="isRibRequired"
@@ -439,7 +439,7 @@ import {
} from '~/modules/commercial/utils/forms/clientEdit'
import { clampRevenueAmount } from '~/modules/commercial/utils/forms/amountInput'
import { todayIso } from '~/shared/utils/date'
import { sanitizeCodeAlnum, sanitizeFreeText, sanitizePersonName } from '~/shared/utils/textSanitize'
import { CODE_ALNUM_MASK, FREE_TEXT_MASK, PERSON_NAME_MASK } from '~/shared/utils/textSanitize'
import {
buildClientFormTabKeys,
isAddressValid,
@@ -18,8 +18,8 @@
automatiquement sur l'onglet Information. -->
<div class="mt-[48px] grid grid-cols-3 xl:grid-cols-4 gap-x-[44px] gap-y-4">
<MalioInputText
:model-value="main.companyName"
@update:model-value="(v: string) => main.companyName = sanitizeFreeText(v)"
v-model="main.companyName"
:mask="FREE_TEXT_MASK"
:label="t('commercial.clients.form.main.companyName')"
:required="true"
:disabled="mainLocked"
@@ -101,8 +101,8 @@
:error="informationErrors.errors.description"
/>
<MalioInputText
:model-value="information.competitors"
@update:model-value="(v: string) => information.competitors = sanitizeFreeText(v)"
v-model="information.competitors"
:mask="FREE_TEXT_MASK"
:label="t('commercial.clients.form.information.competitors')"
:disabled="isValidated('information')"
:error="informationErrors.errors.competitors"
@@ -136,8 +136,8 @@
@update:model-value="onRevenueAmountInput"
/>
<MalioInputText
:model-value="information.directorName"
@update:model-value="(v: string) => information.directorName = sanitizePersonName(v)"
v-model="information.directorName"
:mask="PERSON_NAME_MASK"
:label="t('commercial.clients.form.information.directorName')"
:disabled="isValidated('information')"
:error="informationErrors.errors.directorName"
@@ -252,8 +252,8 @@
:error="accountingErrors.errors.siren"
/>
<MalioInputText
:model-value="accounting.accountNumber"
@update:model-value="(v: string) => accounting.accountNumber = sanitizeCodeAlnum(v)"
v-model="accounting.accountNumber"
:mask="CODE_ALNUM_MASK"
:label="t('commercial.clients.form.accounting.accountNumber')"
:disabled="accountingReadonly"
:required="true"
@@ -270,8 +270,8 @@
@update:model-value="(v: string | number | null) => accounting.tvaModeIri = v === null ? null : String(v)"
/>
<MalioInputText
:model-value="accounting.nTva"
@update:model-value="(v: string) => accounting.nTva = sanitizeCodeAlnum(v)"
v-model="accounting.nTva"
:mask="CODE_ALNUM_MASK"
:label="t('commercial.clients.form.accounting.nTva')"
:disabled="accountingReadonly"
:required="true"
@@ -335,16 +335,16 @@
:error="ribErrors[index]?.label"
/>
<MalioInputText
:model-value="rib.bic"
@update:model-value="(v: string) => rib.bic = sanitizeCodeAlnum(v)"
v-model="rib.bic"
:mask="CODE_ALNUM_MASK"
:label="t('commercial.clients.form.accounting.ribBic')"
:disabled="accountingReadonly"
:required="isRibRequired"
:error="ribErrors[index]?.bic"
/>
<MalioInputText
:model-value="rib.iban"
@update:model-value="(v: string) => rib.iban = sanitizeCodeAlnum(v)"
v-model="rib.iban"
:mask="CODE_ALNUM_MASK"
:label="t('commercial.clients.form.accounting.ribIban')"
:disabled="accountingReadonly"
:required="isRibRequired"
@@ -423,7 +423,7 @@ import {
} from '~/modules/commercial/utils/forms/clientFormRules'
import { clampRevenueAmount } from '~/modules/commercial/utils/forms/amountInput'
import { todayIso } from '~/shared/utils/date'
import { sanitizeCodeAlnum, sanitizeFreeText, sanitizePersonName } from '~/shared/utils/textSanitize'
import { CODE_ALNUM_MASK, FREE_TEXT_MASK, PERSON_NAME_MASK } from '~/shared/utils/textSanitize'
import {
buildAddressPayload,
buildMainPayload,
@@ -23,12 +23,12 @@
roles sans `manage` (ex. Compta). Pas de contact inline (ERP-106). -->
<div class="mt-[48px] grid grid-cols-3 xl:grid-cols-4 gap-x-[44px] gap-y-4">
<MalioInputText
:model-value="main.companyName"
v-model="main.companyName"
:label="t('commercial.suppliers.form.main.companyName')"
:required="true"
:disabled="businessReadonly"
:error="mainErrors.errors.companyName"
@update:model-value="(v: string) => main.companyName = sanitizeFreeText(v)"
:mask="FREE_TEXT_MASK"
/>
<MalioSelectCheckbox
:model-value="main.categoryIris"
@@ -67,11 +67,11 @@
:error="informationErrors.errors.description"
/>
<MalioInputText
:model-value="information.competitors"
v-model="information.competitors"
:label="t('commercial.suppliers.form.information.competitors')"
:disabled="businessReadonly"
:error="informationErrors.errors.competitors"
@update:model-value="(v: string) => information.competitors = sanitizeFreeText(v)"
:mask="FREE_TEXT_MASK"
/>
<!-- Date de creation jamais dans le futur (ERP-193) : :max plafonne
le calendrier a aujourd'hui et invalide une saisie future. -->
@@ -102,11 +102,11 @@
@update:model-value="onRevenueAmountInput"
/>
<MalioInputText
:model-value="information.directorName"
v-model="information.directorName"
:label="t('commercial.suppliers.form.information.directorName')"
:disabled="businessReadonly"
:error="informationErrors.errors.directorName"
@update:model-value="(v: string) => information.directorName = sanitizePersonName(v)"
:mask="PERSON_NAME_MASK"
/>
<MalioInputAmount
v-model="information.profitAmount"
@@ -223,12 +223,12 @@
:error="accountingErrors.errors.siren"
/>
<MalioInputText
:model-value="accounting.accountNumber"
v-model="accounting.accountNumber"
:label="t('commercial.suppliers.form.accounting.accountNumber')"
:disabled="accountingReadonly"
:required="true"
:error="accountingErrors.errors.accountNumber"
@update:model-value="(v: string) => accounting.accountNumber = sanitizeCodeAlnum(v)"
:mask="CODE_ALNUM_MASK"
/>
<MalioSelect
:model-value="accounting.tvaModeIri"
@@ -241,12 +241,12 @@
@update:model-value="(v: string | number | null) => accounting.tvaModeIri = v === null ? null : String(v)"
/>
<MalioInputText
:model-value="accounting.nTva"
v-model="accounting.nTva"
:label="t('commercial.suppliers.form.accounting.nTva')"
:disabled="accountingReadonly"
:required="true"
:error="accountingErrors.errors.nTva"
@update:model-value="(v: string) => accounting.nTva = sanitizeCodeAlnum(v)"
:mask="CODE_ALNUM_MASK"
/>
<MalioSelect
:model-value="accounting.paymentDelayIri"
@@ -305,20 +305,20 @@
:error="ribErrors[index]?.label"
/>
<MalioInputText
:model-value="rib.bic"
v-model="rib.bic"
:label="t('commercial.suppliers.form.accounting.ribBic')"
:disabled="accountingReadonly"
:required="isRibRequired"
:error="ribErrors[index]?.bic"
@update:model-value="(v: string) => rib.bic = sanitizeCodeAlnum(v)"
:mask="CODE_ALNUM_MASK"
/>
<MalioInputText
:model-value="rib.iban"
v-model="rib.iban"
:label="t('commercial.suppliers.form.accounting.ribIban')"
:disabled="accountingReadonly"
:required="isRibRequired"
:error="ribErrors[index]?.iban"
@update:model-value="(v: string) => rib.iban = sanitizeCodeAlnum(v)"
:mask="CODE_ALNUM_MASK"
/>
</div>
</div>
@@ -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).
@@ -18,12 +18,12 @@
automatiquement sur l'onglet Information. Pas de contact inline (ERP-106). -->
<div class="mt-[48px] grid grid-cols-3 xl:grid-cols-4 gap-x-[44px] gap-y-4">
<MalioInputText
:model-value="main.companyName"
v-model="main.companyName"
:label="t('commercial.suppliers.form.main.companyName')"
:required="true"
:disabled="mainLocked"
:error="mainErrors.errors.companyName"
@update:model-value="(v: string) => main.companyName = sanitizeFreeText(v)"
:mask="FREE_TEXT_MASK"
/>
<MalioSelectCheckbox
:model-value="main.categoryIris"
@@ -61,11 +61,11 @@
:error="informationErrors.errors.description"
/>
<MalioInputText
:model-value="information.competitors"
v-model="information.competitors"
:label="t('commercial.suppliers.form.information.competitors')"
:disabled="isValidated('information')"
:error="informationErrors.errors.competitors"
@update:model-value="(v: string) => information.competitors = sanitizeFreeText(v)"
:mask="FREE_TEXT_MASK"
/>
<!-- Date de creation jamais dans le futur (ERP-193) : :max plafonne
le calendrier a aujourd'hui et invalide une saisie future. -->
@@ -96,11 +96,11 @@
@update:model-value="onRevenueAmountInput"
/>
<MalioInputText
:model-value="information.directorName"
v-model="information.directorName"
:label="t('commercial.suppliers.form.information.directorName')"
:disabled="isValidated('information')"
:error="informationErrors.errors.directorName"
@update:model-value="(v: string) => information.directorName = sanitizePersonName(v)"
:mask="PERSON_NAME_MASK"
/>
<MalioInputAmount
v-model="information.profitAmount"
@@ -217,12 +217,12 @@
:error="accountingErrors.errors.siren"
/>
<MalioInputText
:model-value="accounting.accountNumber"
v-model="accounting.accountNumber"
:label="t('commercial.suppliers.form.accounting.accountNumber')"
:disabled="accountingReadonly"
:required="true"
:error="accountingErrors.errors.accountNumber"
@update:model-value="(v: string) => accounting.accountNumber = sanitizeCodeAlnum(v)"
:mask="CODE_ALNUM_MASK"
/>
<MalioSelect
:model-value="accounting.tvaModeIri"
@@ -235,12 +235,12 @@
@update:model-value="(v: string | number | null) => accounting.tvaModeIri = v === null ? null : String(v)"
/>
<MalioInputText
:model-value="accounting.nTva"
v-model="accounting.nTva"
:label="t('commercial.suppliers.form.accounting.nTva')"
:disabled="accountingReadonly"
:required="true"
:error="accountingErrors.errors.nTva"
@update:model-value="(v: string) => accounting.nTva = sanitizeCodeAlnum(v)"
:mask="CODE_ALNUM_MASK"
/>
<MalioSelect
:model-value="accounting.paymentDelayIri"
@@ -299,20 +299,20 @@
:error="ribErrors[index]?.label"
/>
<MalioInputText
:model-value="rib.bic"
v-model="rib.bic"
:label="t('commercial.suppliers.form.accounting.ribBic')"
:disabled="accountingReadonly"
:required="isRibRequired"
:error="ribErrors[index]?.bic"
@update:model-value="(v: string) => rib.bic = sanitizeCodeAlnum(v)"
:mask="CODE_ALNUM_MASK"
/>
<MalioInputText
:model-value="rib.iban"
v-model="rib.iban"
:label="t('commercial.suppliers.form.accounting.ribIban')"
:disabled="accountingReadonly"
:required="isRibRequired"
:error="ribErrors[index]?.iban"
@update:model-value="(v: string) => rib.iban = sanitizeCodeAlnum(v)"
:mask="CODE_ALNUM_MASK"
/>
</div>
</div>
@@ -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 = '#########'
@@ -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 @@
<MalioInputText
:model-value="model.streetComplement"
:label="t('technique.providers.form.address.streetComplement')"
:mask="ADDRESS_MASK"
:readonly="readonly"
:disabled="disabled"
:error="errors?.streetComplement"
@@ -141,7 +143,7 @@
import { useAddressAutocomplete, type AddressSuggestion } from '~/shared/composables/useAddressAutocomplete'
import type { RefOption } from '~/modules/technique/composables/useProviderReferentials'
import type { ProviderAddressFormDraft } from '~/modules/technique/types/providerForm'
import { sanitizeAddress } from '~/shared/utils/textSanitize'
import { ADDRESS_MASK } from '~/shared/utils/textSanitize'
// Masque code postal FR : 5 chiffres.
const POSTAL_CODE_MASK = '#####'
@@ -206,20 +208,14 @@ 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.
const FIELD_SANITIZERS: Partial<Record<keyof ProviderAddressFormDraft, (v: string) => 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<K extends keyof ProviderAddressFormDraft>(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. */
@@ -14,6 +14,7 @@
<MalioInputText
:model-value="model.lastName"
:label="t('technique.providers.form.contact.lastName')"
:mask="PERSON_NAME_MASK"
:readonly="readonly"
:disabled="disabled"
:error="errors?.lastName"
@@ -22,6 +23,7 @@
<MalioInputText
:model-value="model.firstName"
:label="t('technique.providers.form.contact.firstName')"
:mask="PERSON_NAME_MASK"
:readonly="readonly"
:disabled="disabled"
:error="errors?.firstName"
@@ -34,6 +36,7 @@
<MalioInputText
:model-value="model.jobTitle"
:label="t('technique.providers.form.contact.jobTitle')"
:mask="FREE_TEXT_MASK"
:readonly="readonly"
:disabled="disabled"
:error="errors?.jobTitle"
@@ -77,7 +80,7 @@
<script setup lang="ts">
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<Record<keyof ProviderContactFormDraft, (v: string) => 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<K extends keyof ProviderContactFormDraft>(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). */
@@ -20,12 +20,12 @@
<!-- Bloc principal (pre-rempli, editable si `manage`) -->
<div class="mt-[48px] grid grid-cols-3 xl:grid-cols-4 gap-x-[44px] gap-y-4">
<MalioInputText
:model-value="main.companyName"
v-model="main.companyName"
:label="t('technique.providers.form.main.companyName')"
:mask="FREE_TEXT_MASK"
:required="true"
:disabled="businessReadonly"
:error="mainErrors.errors.companyName"
@update:model-value="(v: string) => main.companyName = sanitizeFreeText(v)"
/>
<MalioSelectCheckbox
:model-value="main.categoryIris"
@@ -147,12 +147,12 @@
:error="accountingErrors.errors.siren"
/>
<MalioInputText
:model-value="accounting.accountNumber"
v-model="accounting.accountNumber"
:label="t('technique.providers.form.accounting.accountNumber')"
:mask="CODE_ALNUM_MASK"
:disabled="accountingReadonly"
:required="true"
:error="accountingErrors.errors.accountNumber"
@update:model-value="(v: string) => accounting.accountNumber = sanitizeCodeAlnum(v)"
/>
<MalioSelect
:model-value="accounting.tvaModeIri"
@@ -165,12 +165,12 @@
@update:model-value="(v: string | number | null) => accounting.tvaModeIri = v === null ? null : String(v)"
/>
<MalioInputText
:model-value="accounting.nTva"
v-model="accounting.nTva"
:label="t('technique.providers.form.accounting.nTva')"
:mask="CODE_ALNUM_MASK"
:disabled="accountingReadonly"
:required="true"
:error="accountingErrors.errors.nTva"
@update:model-value="(v: string) => accounting.nTva = sanitizeCodeAlnum(v)"
/>
<MalioSelect
:model-value="accounting.paymentDelayIri"
@@ -229,20 +229,20 @@
:error="ribErrors[index]?.label"
/>
<MalioInputText
:model-value="rib.bic"
v-model="rib.bic"
:label="t('technique.providers.form.accounting.ribBic')"
:mask="CODE_ALNUM_MASK"
:disabled="accountingReadonly"
:required="true"
:error="ribErrors[index]?.bic"
@update:model-value="(v: string) => rib.bic = sanitizeCodeAlnum(v)"
/>
<MalioInputText
:model-value="rib.iban"
v-model="rib.iban"
:label="t('technique.providers.form.accounting.ribIban')"
:mask="CODE_ALNUM_MASK"
:disabled="accountingReadonly"
:required="true"
:error="ribErrors[index]?.iban"
@update:model-value="(v: string) => rib.iban = sanitizeCodeAlnum(v)"
/>
</div>
</div>
@@ -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 = '#########'
@@ -19,12 +19,12 @@
Selecteur de site present ici (RG-3.03, relation directe). -->
<div class="mt-[48px] grid grid-cols-3 xl:grid-cols-4 gap-x-[44px] gap-y-4">
<MalioInputText
:model-value="main.companyName"
v-model="main.companyName"
:label="t('technique.providers.form.main.companyName')"
:mask="FREE_TEXT_MASK"
:required="true"
:disabled="mainLocked"
:error="mainErrors.errors.companyName"
@update:model-value="(v: string) => main.companyName = sanitizeFreeText(v)"
/>
<MalioSelectCheckbox
:model-value="main.categoryIris"
@@ -146,12 +146,12 @@
:error="accountingErrors.errors.siren"
/>
<MalioInputText
:model-value="accounting.accountNumber"
v-model="accounting.accountNumber"
:label="t('technique.providers.form.accounting.accountNumber')"
:mask="CODE_ALNUM_MASK"
:disabled="accountingReadonly"
:required="true"
:error="accountingErrors.errors.accountNumber"
@update:model-value="(v: string) => accounting.accountNumber = sanitizeCodeAlnum(v)"
/>
<MalioSelect
:model-value="accounting.tvaModeIri"
@@ -164,12 +164,12 @@
@update:model-value="(v: string | number | null) => accounting.tvaModeIri = v === null ? null : String(v)"
/>
<MalioInputText
:model-value="accounting.nTva"
v-model="accounting.nTva"
:label="t('technique.providers.form.accounting.nTva')"
:mask="CODE_ALNUM_MASK"
:disabled="accountingReadonly"
:required="true"
:error="accountingErrors.errors.nTva"
@update:model-value="(v: string) => accounting.nTva = sanitizeCodeAlnum(v)"
/>
<MalioSelect
:model-value="accounting.paymentDelayIri"
@@ -229,20 +229,20 @@
:error="ribErrors[index]?.label"
/>
<MalioInputText
:model-value="rib.bic"
v-model="rib.bic"
:label="t('technique.providers.form.accounting.ribBic')"
:mask="CODE_ALNUM_MASK"
:disabled="accountingReadonly"
:required="true"
:error="ribErrors[index]?.bic"
@update:model-value="(v: string) => rib.bic = sanitizeCodeAlnum(v)"
/>
<MalioInputText
:model-value="rib.iban"
v-model="rib.iban"
:label="t('technique.providers.form.accounting.ribIban')"
:mask="CODE_ALNUM_MASK"
:disabled="accountingReadonly"
:required="true"
:error="ribErrors[index]?.iban"
@update:model-value="(v: string) => rib.iban = sanitizeCodeAlnum(v)"
/>
</div>
</div>
@@ -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 = '#########'
@@ -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 @@
<MalioInputText
:model-value="model.streetComplement"
:label="t('transport.carriers.form.address.streetComplement')"
:mask="ADDRESS_MASK"
:readonly="readonly"
:disabled="disabled"
:error="errors?.streetComplement"
@@ -98,7 +100,7 @@
<script setup lang="ts">
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<Record<keyof CarrierAddressFormDraft, (v: string) => 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<K extends keyof CarrierAddressFormDraft>(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. */
@@ -14,6 +14,7 @@
<MalioInputText
:model-value="model.lastName"
:label="t('transport.carriers.form.contact.lastName')"
:mask="PERSON_NAME_MASK"
:readonly="readonly"
:disabled="disabled"
:error="errors?.lastName"
@@ -22,6 +23,7 @@
<MalioInputText
:model-value="model.firstName"
:label="t('transport.carriers.form.contact.firstName')"
:mask="PERSON_NAME_MASK"
:readonly="readonly"
:disabled="disabled"
:error="errors?.firstName"
@@ -33,6 +35,7 @@
<MalioInputText
:model-value="model.jobTitle"
:label="t('transport.carriers.form.contact.jobTitle')"
:mask="FREE_TEXT_MASK"
:readonly="readonly"
:disabled="disabled"
:error="errors?.jobTitle"
@@ -77,7 +80,7 @@
<script setup lang="ts">
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<Record<keyof CarrierContactFormDraft, (v: string) => 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<K extends keyof CarrierContactFormDraft>(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). */
@@ -19,8 +19,8 @@
<!-- Formulaire principal (éditable, PATCH partiel) -->
<div class="mt-[48px] grid grid-cols-3 xl:grid-cols-4 gap-x-[44px] gap-y-4">
<MalioInputText
:model-value="main.name"
@update:model-value="(v: string) => main.name = sanitizeFreeText(v)"
v-model="main.name"
:mask="FREE_TEXT_MASK"
:label="t('transport.carriers.form.main.name')"
:required="true"
:error="mainErrors.errors.name"
@@ -220,7 +220,7 @@ import { useCarrierForm } from '~/modules/transport/composables/useCarrierForm'
import { useCarrier } from '~/modules/transport/composables/useCarrier'
import type { QualimatCarrierRow } from '~/modules/transport/composables/useQualimatSearch'
import { clampPercent, LIOT_PLATES_MASK, sanitizeDecimal } from '~/modules/transport/utils/forms/numberInput'
import { sanitizeFreeText } from '~/shared/utils/textSanitize'
import { FREE_TEXT_MASK } from '~/shared/utils/textSanitize'
interface SelectOption {
value: string
@@ -19,8 +19,8 @@
seule pour un transporteur QUALIMAT (saisie assistee, onglet Qualimat). -->
<div class="mt-[48px] grid grid-cols-3 xl:grid-cols-4 gap-x-[44px] gap-y-4">
<MalioInputText
:model-value="main.name"
@update:model-value="(v: string) => main.name = sanitizeFreeText(v)"
v-model="main.name"
:mask="FREE_TEXT_MASK"
:label="t('transport.carriers.form.main.name')"
:required="true"
:disabled="mainLocked"
@@ -314,7 +314,7 @@ import CarrierQualimatTab from '~/modules/transport/components/CarrierQualimatTa
import { useCarrierForm } from '~/modules/transport/composables/useCarrierForm'
import type { QualimatCarrierRow } from '~/modules/transport/composables/useQualimatSearch'
import { clampPercent, LIOT_PLATES_MASK, sanitizeDecimal } from '~/modules/transport/utils/forms/numberInput'
import { sanitizeFreeText } from '~/shared/utils/textSanitize'
import { FREE_TEXT_MASK } from '~/shared/utils/textSanitize'
interface SelectOption {
value: string