fix : retours métier ERP-193 (4 répertoires) (#139)
Auto Tag Develop / tag (push) Successful in 11s

Lot de retours métier **ERP-193** (« Fix tous les retours starseed »), transverse aux 4 répertoires (clients, fournisseurs, prestataires, transporteurs).

## Contenu

- **Pagination** : défaut à 25 items/page sur les 4 répertoires.
- **Libellé** : colonne « Dernière activité » → « Dernière modification ».
- **Consultation** : masquage des onglets vides (coquilles « à venir » + onglets de données sans donnée).
- **Chiffre d'affaires** : plafonné à 999 999 999 999,99 (clamp front + `Assert\LessThanOrEqual` back).
- **Date de création** : interdiction des dates futures (`:max` MalioDate + `Assert\LessThanOrEqual('today')` back).
- **Caractères spéciaux** : blocage des caractères parasites (`²³§~#|…`) dans les champs texte via une allow-list par profil (nom de personne / texte libre / adresse / code alphanumérique) — filtrage front à la frappe + `Assert\Regex` back autoritaire. Email/IBAN/BIC/TVA conservent leurs validateurs de format.
- **UI** : champs en consultation et onglets validés grisés (`readonly` → `disabled`).
- **UI** : boutons « Archiver » en rouge (variant `danger`).

## Tests

- Back : nouveaux tests RG (plafond CA, dates futures, caractères spéciaux) + garde-fou contraintes — suite complète verte (813 tests).
- Front : nouveaux tests unitaires (sanitizers, helpers date/montant) — 615 tests verts, eslint clean.

---------

Co-authored-by: THOLOT DECHENE Matthieu <matthieu@yuno.malio.fr>
Reviewed-on: #139
Co-authored-by: tristan <tristan@yuno.malio.fr>
Co-committed-by: tristan <tristan@yuno.malio.fr>
This commit was merged in pull request #139.
This commit is contained in:
2026-06-22 09:40:40 +00:00
committed by Autin
parent 6c938756cc
commit 5e15c1f69f
93 changed files with 2791 additions and 803 deletions
@@ -6,6 +6,7 @@
icon="mdi:arrow-left-bold"
icon-size="24"
variant="ghost"
:title="t('technique.providers.edit.back')"
v-bind="{ ariaLabel: t('technique.providers.edit.back') }"
@click="goBack"
/>
@@ -22,8 +23,9 @@
<MalioInputText
v-model="main.companyName"
:label="t('technique.providers.form.main.companyName')"
:mask="FREE_TEXT_MASK"
:required="true"
:readonly="businessReadonly"
:disabled="businessReadonly"
:error="mainErrors.errors.companyName"
/>
<MalioSelectCheckbox
@@ -31,7 +33,7 @@
:options="referentials.categories.value"
:label="t('technique.providers.form.main.categories')"
:display-tag="true"
:readonly="businessReadonly"
:disabled="businessReadonly"
:required="true"
:error="mainErrors.errors.categories"
@update:model-value="(v: (string | number)[]) => main.categoryIris = v.map(String)"
@@ -41,7 +43,7 @@
:options="referentials.sites.value"
:label="t('technique.providers.form.main.sites')"
:display-tag="true"
:readonly="businessReadonly"
:disabled="businessReadonly"
:required="true"
:error="mainErrors.errors.sites"
@update:model-value="(v: (string | number)[]) => main.siteIris = v.map(String)"
@@ -71,7 +73,7 @@
:key="index"
:model-value="contact"
:removable="isRowRemovable(contacts, index)"
:readonly="businessReadonly"
:disabled="businessReadonly"
:errors="contactErrors[index]"
@update:model-value="(v) => contacts[index] = v"
@remove="askRemoveContact(index)"
@@ -102,12 +104,11 @@
v-for="(address, index) in addresses"
:key="index"
:model-value="address"
:category-options="referentials.categories.value"
:site-options="referentials.sites.value"
:contact-options="contactOptions"
:country-options="countryOptions"
:removable="isRowRemovable(addresses, index)"
:readonly="businessReadonly"
:disabled="businessReadonly"
:errors="addressErrors[index]"
@update:model-value="(v) => addresses[index] = v"
@remove="askRemoveAddress(index)"
@@ -141,14 +142,15 @@
v-model="accounting.siren"
:label="t('technique.providers.form.accounting.siren')"
:mask="SIREN_MASK"
:readonly="accountingReadonly"
:disabled="accountingReadonly"
:required="true"
:error="accountingErrors.errors.siren"
/>
<MalioInputText
v-model="accounting.accountNumber"
:label="t('technique.providers.form.accounting.accountNumber')"
:readonly="accountingReadonly"
:mask="CODE_ALNUM_MASK"
:disabled="accountingReadonly"
:required="true"
:error="accountingErrors.errors.accountNumber"
/>
@@ -156,7 +158,7 @@
:model-value="accounting.tvaModeIri"
:options="referentials.tvaModes.value"
:label="t('technique.providers.form.accounting.tvaMode')"
:readonly="accountingReadonly"
:disabled="accountingReadonly"
empty-option-label=""
:required="true"
:error="accountingErrors.errors.tvaMode"
@@ -165,7 +167,8 @@
<MalioInputText
v-model="accounting.nTva"
:label="t('technique.providers.form.accounting.nTva')"
:readonly="accountingReadonly"
:mask="CODE_ALNUM_MASK"
:disabled="accountingReadonly"
:required="true"
:error="accountingErrors.errors.nTva"
/>
@@ -173,7 +176,7 @@
:model-value="accounting.paymentDelayIri"
:options="referentials.paymentDelays.value"
:label="t('technique.providers.form.accounting.paymentDelay')"
:readonly="accountingReadonly"
:disabled="accountingReadonly"
empty-option-label=""
:required="true"
:error="accountingErrors.errors.paymentDelay"
@@ -183,7 +186,7 @@
:model-value="accounting.paymentTypeIri"
:options="referentials.paymentTypes.value"
:label="t('technique.providers.form.accounting.paymentType')"
:readonly="accountingReadonly"
:disabled="accountingReadonly"
empty-option-label=""
:required="true"
:error="accountingErrors.errors.paymentType"
@@ -194,7 +197,7 @@
:model-value="accounting.bankIri"
:options="referentials.banks.value"
:label="t('technique.providers.form.accounting.bank')"
:readonly="accountingReadonly"
:disabled="accountingReadonly"
empty-option-label=""
:required="true"
:error="accountingErrors.errors.bank"
@@ -221,21 +224,23 @@
<MalioInputText
v-model="rib.label"
:label="t('technique.providers.form.accounting.ribLabel')"
:readonly="accountingReadonly"
:disabled="accountingReadonly"
:required="true"
:error="ribErrors[index]?.label"
/>
<MalioInputText
v-model="rib.bic"
:label="t('technique.providers.form.accounting.ribBic')"
:readonly="accountingReadonly"
:mask="CODE_ALNUM_MASK"
:disabled="accountingReadonly"
:required="true"
:error="ribErrors[index]?.bic"
/>
<MalioInputText
v-model="rib.iban"
:label="t('technique.providers.form.accounting.ribIban')"
:readonly="accountingReadonly"
:mask="CODE_ALNUM_MASK"
:disabled="accountingReadonly"
:required="true"
:error="ribErrors[index]?.iban"
/>
@@ -313,6 +318,7 @@ import {
} from '~/modules/technique/types/providerForm'
import { extractApiErrorMessage } from '~/shared/utils/api'
import { isRowRemovable } from '~/shared/utils/collectionRow'
import { CODE_ALNUM_MASK, FREE_TEXT_MASK } from '~/shared/utils/textSanitize'
// Masque SIREN : 9 chiffres (la normalisation finale reste serveur).
const SIREN_MASK = '#########'
@@ -6,6 +6,7 @@
icon="mdi:arrow-left-bold"
icon-size="24"
variant="ghost"
:title="t('technique.providers.consultation.back')"
v-bind="{ ariaLabel: t('technique.providers.consultation.back') }"
@click="goBack"
/>
@@ -22,7 +23,7 @@
/>
<MalioButton
v-if="showArchive"
variant="secondary"
variant="danger"
icon-name="mdi:archive-arrow-down-outline"
icon-position="left"
:label="t('technique.providers.action.archive')"
@@ -47,28 +48,32 @@
<!-- Bloc principal (lecture seule) -->
<div class="mt-[48px] grid grid-cols-3 xl:grid-cols-4 gap-x-[44px] gap-y-4">
<MalioInputText
v-if="isFilled(provider.companyName)"
:model-value="provider.companyName"
:label="t('technique.providers.form.main.companyName')"
readonly
disabled
/>
<MalioSelectCheckbox
v-if="isFilled(mainCategoryIris)"
:model-value="mainCategoryIris"
:options="mainCategoryOptions"
:label="t('technique.providers.form.main.categories')"
:display-tag="true"
readonly
disabled
/>
<MalioSelectCheckbox
v-if="isFilled(mainSiteIris)"
:model-value="mainSiteIris"
:options="mainSiteOptions"
:label="t('technique.providers.form.main.sites')"
:display-tag="true"
readonly
disabled
/>
</div>
<!-- Onglets (navigation libre, tout en lecture seule) -->
<MalioTabList v-model="activeTab" :tabs="tabs" :max-visible-tabs="5" :max-width="1100" class="mt-[60px]">
<!-- ERP-193 : barre masquee s'il ne reste aucun onglet non vide. -->
<MalioTabList v-if="visibleTabKeys.length" v-model="activeTab" :tabs="tabs" :max-visible-tabs="5" :max-width="1100" class="mt-[60px]">
<!-- Onglet Contacts -->
<template #contacts>
<div class="mt-12 flex flex-col gap-6">
@@ -76,7 +81,8 @@
v-for="(contact, index) in contacts"
:key="index"
:model-value="contact"
readonly
disabled
hide-empty
/>
</div>
</template>
@@ -88,31 +94,29 @@
v-for="(view, index) in addressViews"
:key="index"
:model-value="view.draft"
:category-options="view.categoryOptions"
:site-options="view.siteOptions"
:contact-options="contactOptions"
:country-options="countryOptionsFor(view.draft.country)"
readonly
disabled
hide-empty
/>
</div>
</template>
<!-- Onglets placeholder « A venir » (comme les autres modules). -->
<template #reports><ComingSoonPlaceholder /></template>
<template #exchanges><ComingSoonPlaceholder /></template>
<!-- ERP-193 : les onglets « a venir » (Rapports / Echanges) ne sont
plus rendus en consultation (masquage des onglets vides). -->
<!-- Onglet Comptabilite (present uniquement si accounting.view). -->
<template v-if="canAccountingView" #accounting>
<div class="mt-12 flex flex-col gap-6">
<div class="bg-white py-4 pl-[28px] pr-[60px] shadow-[0_4px_4px_0_rgba(0,0,0,0.25)]">
<div class="grid grid-cols-4 gap-x-[44px] gap-y-4">
<MalioInputText :model-value="accounting.siren" :label="t('technique.providers.form.accounting.siren')" readonly />
<MalioInputText :model-value="accounting.accountNumber" :label="t('technique.providers.form.accounting.accountNumber')" readonly />
<MalioSelect :model-value="accounting.tvaModeIri" :options="tvaModeOptions" :label="t('technique.providers.form.accounting.tvaMode')" readonly empty-option-label="" />
<MalioInputText :model-value="accounting.nTva" :label="t('technique.providers.form.accounting.nTva')" readonly />
<MalioSelect :model-value="accounting.paymentDelayIri" :options="paymentDelayOptions" :label="t('technique.providers.form.accounting.paymentDelay')" readonly empty-option-label="" />
<MalioSelect :model-value="accounting.paymentTypeIri" :options="paymentTypeOptions" :label="t('technique.providers.form.accounting.paymentType')" readonly empty-option-label="" />
<MalioSelect v-if="isBankRequired" :model-value="accounting.bankIri" :options="bankOptions" :label="t('technique.providers.form.accounting.bank')" readonly empty-option-label="" />
<MalioInputText v-if="isFilled(accounting.siren)" :model-value="accounting.siren" :label="t('technique.providers.form.accounting.siren')" disabled />
<MalioInputText v-if="isFilled(accounting.accountNumber)" :model-value="accounting.accountNumber" :label="t('technique.providers.form.accounting.accountNumber')" disabled />
<MalioSelect v-if="isFilled(accounting.tvaModeIri)" :model-value="accounting.tvaModeIri" :options="tvaModeOptions" :label="t('technique.providers.form.accounting.tvaMode')" disabled empty-option-label="" />
<MalioInputText v-if="isFilled(accounting.nTva)" :model-value="accounting.nTva" :label="t('technique.providers.form.accounting.nTva')" disabled />
<MalioSelect v-if="isFilled(accounting.paymentDelayIri)" :model-value="accounting.paymentDelayIri" :options="paymentDelayOptions" :label="t('technique.providers.form.accounting.paymentDelay')" disabled empty-option-label="" />
<MalioSelect v-if="isFilled(accounting.paymentTypeIri)" :model-value="accounting.paymentTypeIri" :options="paymentTypeOptions" :label="t('technique.providers.form.accounting.paymentType')" disabled empty-option-label="" />
<MalioSelect v-if="isBankRequired && isFilled(accounting.bankIri)" :model-value="accounting.bankIri" :options="bankOptions" :label="t('technique.providers.form.accounting.bank')" disabled empty-option-label="" />
</div>
</div>
@@ -123,9 +127,9 @@
class="bg-white py-4 pl-[28px] pr-[60px] shadow-[0_4px_4px_0_rgba(0,0,0,0.25)]"
>
<div class="grid grid-cols-4 gap-x-[44px] gap-y-4">
<MalioInputText :model-value="rib.label" :label="t('technique.providers.form.accounting.ribLabel')" readonly />
<MalioInputText :model-value="rib.bic" :label="t('technique.providers.form.accounting.ribBic')" readonly />
<MalioInputText :model-value="rib.iban" :label="t('technique.providers.form.accounting.ribIban')" readonly />
<MalioInputText v-if="isFilled(rib.label)" :model-value="rib.label" :label="t('technique.providers.form.accounting.ribLabel')" disabled />
<MalioInputText v-if="isFilled(rib.bic)" :model-value="rib.bic" :label="t('technique.providers.form.accounting.ribBic')" disabled />
<MalioInputText v-if="isFilled(rib.iban)" :model-value="rib.iban" :label="t('technique.providers.form.accounting.ribIban')" disabled />
</div>
</div>
</div>
@@ -158,7 +162,7 @@
</template>
<script setup lang="ts">
import { computed, onMounted, reactive, ref } from 'vue'
import { computed, onMounted, reactive, ref, watch } from 'vue'
import { useProvider } from '~/modules/technique/composables/useProvider'
import {
canEditProvider,
@@ -170,6 +174,7 @@ import {
mapContactToDraft,
mapRibToDraft,
paymentTypeCodeOf,
providerConsultationVisibleTabs,
referentialOptionOf,
showArchiveAction,
showRestoreAction,
@@ -177,6 +182,7 @@ import {
} from '~/modules/technique/utils/forms/providerDetail'
import { isBankRequiredForPaymentType, isRibRequiredForPaymentType } from '~/modules/technique/utils/forms/providerAccounting'
import { emptyProviderAddress, emptyProviderContact } from '~/modules/technique/types/providerForm'
import { isFilled } from '~/shared/utils/consultationDisplay'
const { t } = useI18n()
const route = useRoute()
@@ -197,7 +203,6 @@ const headerTitle = computed(() => provider.value?.companyName || t('technique.p
useHead({ title: t('technique.providers.consultation.title') })
// ── Onglets (ordre spec : Contacts · Adresse · Rapports · Échanges · Comptabilité) ──
const activeTab = ref('contacts')
const TAB_ICONS: Record<string, string> = {
contacts: 'mdi:account-box-plus-outline',
address: 'mdi:map-marker-outline',
@@ -205,11 +210,27 @@ const TAB_ICONS: Record<string, string> = {
exchanges: 'mdi:swap-horizontal',
accounting: 'mdi:bank-circle-outline',
}
const tabs = computed(() => {
const keys = ['contacts', 'address', 'reports', 'exchanges']
if (canAccountingView.value) keys.push('accounting')
return keys.map(key => ({ key, label: t(`technique.providers.tab.${key}`), icon: TAB_ICONS[key] }))
})
// ERP-193 (retour metier) : on masque les coquilles (Rapports / Echanges) ET
// tout onglet de donnees vide. La liste depend donc du payload charge.
const visibleTabKeys = computed(() => providerConsultationVisibleTabs(provider.value, {
canAccountingView: canAccountingView.value,
}))
const tabs = computed(() => visibleTabKeys.value.map(
key => ({ key, label: t(`technique.providers.tab.${key}`), icon: TAB_ICONS[key] }),
))
// Onglet initial : vide tant que le prestataire n'est pas charge, puis premier
// onglet visible. Un watcher recale si l'onglet courant disparait.
const activeTab = ref('')
watch(visibleTabKeys, (keys) => {
if (keys.length === 0) {
activeTab.value = ''
return
}
if (!keys.includes(activeTab.value)) {
activeTab.value = keys[0]
}
}, { immediate: true })
// ── Donnees mappees depuis la SEULE reponse detail ─────────────────────────────
const mainCategoryIris = computed(() => irisOf(provider.value?.categories))
@@ -226,16 +247,15 @@ const contacts = computed(() => {
// Contacts rattachables (pour resoudre les libelles des contacts lies aux adresses).
const contactOptions = computed(() => contactOptionsOf(provider.value?.contacts))
// Vue par adresse : brouillon + options propres a l'adresse (sites/categories embarques).
// Vue par adresse : brouillon + options propres a l'adresse (sites embarques).
const addressViews = computed(() => {
const views = (provider.value?.addresses ?? []).map(address => ({
draft: mapAddressToDraft(address),
siteOptions: siteOptionsOf(address.sites),
categoryOptions: categoryOptionsOf(address.categories),
}))
return views.length > 0
? views
: [{ draft: emptyProviderAddress(), siteOptions: [], categoryOptions: [] }]
: [{ draft: emptyProviderAddress(), siteOptions: [] }]
})
/** Pays : une seule option (la valeur courante), suffisant pour l'affichage readonly. */
@@ -44,7 +44,7 @@
@update:page="goToPage"
@update:per-page="setItemsPerPage"
>
<!-- Categories : libelles (name) separes par une virgule. -->
<!-- Categories : libelles (name) separes par une virgule, aligne sur le client (ERP-193). -->
<template #cell-categories="{ item }">
{{ formatCategories(item) }}
</template>
@@ -210,7 +210,7 @@ const columns = [
{ key: 'lastActivity', label: t('technique.providers.column.lastActivity') },
]
/** Libelles des categories du prestataire, separes par une virgule (name). */
/** Libelles (name) des categories du prestataire, separes par une virgule (aligne sur le client, ERP-193). */
function formatCategories(item: Record<string, unknown>): string {
const categories = (item.categories as Provider['categories']) ?? []
return categories.map(c => c.name).join(', ')
@@ -6,6 +6,7 @@
icon="mdi:arrow-left-bold"
icon-size="24"
variant="ghost"
:title="t('technique.providers.form.back')"
v-bind="{ ariaLabel: t('technique.providers.form.back') }"
@click="goBack"
/>
@@ -21,8 +22,9 @@
<MalioInputText
v-model="main.companyName"
:label="t('technique.providers.form.main.companyName')"
:mask="FREE_TEXT_MASK"
:required="true"
:readonly="mainLocked"
:disabled="mainLocked"
:error="mainErrors.errors.companyName"
/>
<MalioSelectCheckbox
@@ -30,7 +32,7 @@
:options="referentials.categories.value"
:label="t('technique.providers.form.main.categories')"
:display-tag="true"
:readonly="mainLocked"
:disabled="mainLocked"
:required="true"
:error="mainErrors.errors.categories"
@update:model-value="(v: (string | number)[]) => main.categoryIris = v.map(String)"
@@ -40,7 +42,7 @@
:options="referentials.sites.value"
:label="t('technique.providers.form.main.sites')"
:display-tag="true"
:readonly="mainLocked"
:disabled="mainLocked"
:required="true"
:error="mainErrors.errors.sites"
@update:model-value="(v: (string | number)[]) => main.siteIris = v.map(String)"
@@ -72,12 +74,16 @@
:key="index"
:model-value="contact"
:removable="isRowRemovable(contacts, index)"
:readonly="isValidated('contact')"
:disabled="isValidated('contact')"
:errors="contactErrors[index]"
@update:model-value="(v) => contacts[index] = v"
@remove="askRemoveContact(index)"
/>
<div v-if="!isValidated('contact')" class="flex justify-center gap-6">
<!-- Masque tant que le prestataire n'est pas cree : Contact etant
l'onglet actif par defaut, ses actions (Ajouter / Valider) ne
doivent pas apparaitre a cote du Valider du formulaire principal
(ERP-193). -->
<div v-if="!isValidated('contact') && providerId !== null" class="flex justify-center gap-6">
<MalioButton
variant="secondary"
icon-name="mdi:add-bold"
@@ -89,7 +95,7 @@
<MalioButton
variant="primary"
:label="t('technique.providers.form.submit')"
:disabled="tabSubmitting || providerId === null"
:disabled="tabSubmitting"
@click="onSubmitContacts"
/>
</div>
@@ -102,12 +108,11 @@
v-for="(address, index) in addresses"
:key="index"
:model-value="address"
:category-options="referentials.categories.value"
:site-options="referentials.sites.value"
:contact-options="contactOptions"
:country-options="countryOptions"
:removable="isRowRemovable(addresses, index)"
:readonly="isValidated('address')"
:disabled="isValidated('address')"
:errors="addressErrors[index]"
@update:model-value="(v) => addresses[index] = v"
@remove="askRemoveAddress(index)"
@@ -140,14 +145,15 @@
v-model="accounting.siren"
:label="t('technique.providers.form.accounting.siren')"
:mask="SIREN_MASK"
:readonly="accountingReadonly"
:disabled="accountingReadonly"
:required="true"
:error="accountingErrors.errors.siren"
/>
<MalioInputText
v-model="accounting.accountNumber"
:label="t('technique.providers.form.accounting.accountNumber')"
:readonly="accountingReadonly"
:mask="CODE_ALNUM_MASK"
:disabled="accountingReadonly"
:required="true"
:error="accountingErrors.errors.accountNumber"
/>
@@ -155,7 +161,7 @@
:model-value="accounting.tvaModeIri"
:options="referentials.tvaModes.value"
:label="t('technique.providers.form.accounting.tvaMode')"
:readonly="accountingReadonly"
:disabled="accountingReadonly"
empty-option-label=""
:required="true"
:error="accountingErrors.errors.tvaMode"
@@ -164,7 +170,8 @@
<MalioInputText
v-model="accounting.nTva"
:label="t('technique.providers.form.accounting.nTva')"
:readonly="accountingReadonly"
:mask="CODE_ALNUM_MASK"
:disabled="accountingReadonly"
:required="true"
:error="accountingErrors.errors.nTva"
/>
@@ -172,7 +179,7 @@
:model-value="accounting.paymentDelayIri"
:options="referentials.paymentDelays.value"
:label="t('technique.providers.form.accounting.paymentDelay')"
:readonly="accountingReadonly"
:disabled="accountingReadonly"
empty-option-label=""
:required="true"
:error="accountingErrors.errors.paymentDelay"
@@ -182,7 +189,7 @@
:model-value="accounting.paymentTypeIri"
:options="referentials.paymentTypes.value"
:label="t('technique.providers.form.accounting.paymentType')"
:readonly="accountingReadonly"
:disabled="accountingReadonly"
empty-option-label=""
:required="true"
:error="accountingErrors.errors.paymentType"
@@ -194,7 +201,7 @@
:model-value="accounting.bankIri"
:options="referentials.banks.value"
:label="t('technique.providers.form.accounting.bank')"
:readonly="accountingReadonly"
:disabled="accountingReadonly"
empty-option-label=""
:required="true"
:error="accountingErrors.errors.bank"
@@ -221,21 +228,23 @@
<MalioInputText
v-model="rib.label"
:label="t('technique.providers.form.accounting.ribLabel')"
:readonly="accountingReadonly"
:disabled="accountingReadonly"
:required="true"
:error="ribErrors[index]?.label"
/>
<MalioInputText
v-model="rib.bic"
:label="t('technique.providers.form.accounting.ribBic')"
:readonly="accountingReadonly"
:mask="CODE_ALNUM_MASK"
:disabled="accountingReadonly"
:required="true"
:error="ribErrors[index]?.bic"
/>
<MalioInputText
v-model="rib.iban"
:label="t('technique.providers.form.accounting.ribIban')"
:readonly="accountingReadonly"
:mask="CODE_ALNUM_MASK"
:disabled="accountingReadonly"
:required="true"
:error="ribErrors[index]?.iban"
/>
@@ -297,6 +306,7 @@ import {
} from '~/modules/technique/utils/forms/providerAccounting'
import { extractApiErrorMessage } from '~/shared/utils/api'
import { isRowRemovable } from '~/shared/utils/collectionRow'
import { CODE_ALNUM_MASK, FREE_TEXT_MASK } from '~/shared/utils/textSanitize'
// Masque SIREN : 9 chiffres (la normalisation finale reste serveur).
const SIREN_MASK = '#########'