fix : retours métier ERP-193 (4 répertoires) (#139)
Auto Tag Develop / tag (push) Successful in 11s
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:
@@ -2,7 +2,7 @@
|
||||
<div class="relative grid grid-cols-4 gap-x-[44px] gap-y-4 bg-white py-4 pl-[28px] pr-[60px] shadow-[0_4px_4px_0_rgba(0,0,0,0.25)]">
|
||||
<!-- Suppression : modal de confirmation cote parent. -->
|
||||
<MalioButtonIcon
|
||||
v-if="removable && !readonly"
|
||||
v-if="removable && !readonly && !disabled"
|
||||
icon="mdi:delete-outline"
|
||||
variant="ghost"
|
||||
button-class="absolute top-3 right-3"
|
||||
@@ -12,91 +12,91 @@
|
||||
|
||||
<!-- Sites Starseed : multiselect a tags (>= 1 obligatoire, RG-3.05). -->
|
||||
<MalioSelectCheckbox
|
||||
v-if="!hideEmpty || isFilled(model.siteIris)"
|
||||
:model-value="model.siteIris"
|
||||
:options="siteOptions"
|
||||
:label="t('technique.providers.form.address.sites')"
|
||||
:display-tag="true"
|
||||
:readonly="readonly"
|
||||
:required="true"
|
||||
:disabled="disabled"
|
||||
:required="!readonly && !disabled"
|
||||
:error="errors?.sites"
|
||||
@update:model-value="(v: (string | number)[]) => update('siteIris', v.map(String))"
|
||||
/>
|
||||
|
||||
<!-- Categories de type PRESTATAIRE (>= 1 obligatoire, RG-3.09). -->
|
||||
<MalioSelectCheckbox
|
||||
:model-value="model.categoryIris"
|
||||
:options="categoryOptions"
|
||||
:label="t('technique.providers.form.address.categories')"
|
||||
:display-tag="true"
|
||||
:readonly="readonly"
|
||||
:required="true"
|
||||
:error="errors?.categories"
|
||||
@update:model-value="(v: (string | number)[]) => update('categoryIris', v.map(String))"
|
||||
/>
|
||||
|
||||
<!-- Contacts rattaches (M2M, facultatif) : alimente par l'onglet Contact. -->
|
||||
<MalioSelectCheckbox
|
||||
v-if="!hideEmpty || isFilled(model.contactIris)"
|
||||
:model-value="model.contactIris"
|
||||
:options="contactOptions"
|
||||
:label="t('technique.providers.form.address.contacts')"
|
||||
:display-tag="true"
|
||||
:readonly="readonly"
|
||||
:disabled="disabled"
|
||||
@update:model-value="(v: (string | number)[]) => update('contactIris', v.map(String))"
|
||||
/>
|
||||
|
||||
<MalioSelect
|
||||
v-if="!hideEmpty || isFilled(model.country)"
|
||||
:model-value="model.country"
|
||||
:options="countryOptions"
|
||||
:label="t('technique.providers.form.address.country')"
|
||||
:readonly="readonly"
|
||||
:required="true"
|
||||
:disabled="disabled"
|
||||
:required="!readonly && !disabled"
|
||||
@update:model-value="(v: string | number | null) => update('country', String(v ?? 'France'))"
|
||||
/>
|
||||
|
||||
<MalioInputText
|
||||
v-if="!hideEmpty || isFilled(model.postalCode)"
|
||||
:model-value="model.postalCode"
|
||||
:label="t('technique.providers.form.address.postalCode')"
|
||||
:mask="POSTAL_CODE_MASK"
|
||||
:readonly="readonly"
|
||||
:required="true"
|
||||
:disabled="disabled"
|
||||
:required="!readonly && !disabled"
|
||||
:error="errors?.postalCode"
|
||||
@update:model-value="onPostalCodeChange"
|
||||
/>
|
||||
|
||||
<!-- Ville : MalioSelect alimente par le code postal (BAN). Saisie libre si BAN indispo. -->
|
||||
<MalioSelect
|
||||
v-if="!degraded"
|
||||
v-if="!degraded && (!hideEmpty || isFilled(model.city))"
|
||||
:model-value="model.city"
|
||||
:options="cityOptions"
|
||||
:label="t('technique.providers.form.address.city')"
|
||||
:readonly="readonly"
|
||||
:disabled="disabled"
|
||||
empty-option-label=""
|
||||
:required="true"
|
||||
:required="!readonly && !disabled"
|
||||
:error="errors?.city"
|
||||
@update:model-value="(v: string | number | null) => update('city', v === null ? null : String(v))"
|
||||
@update:model-value="onCityChange"
|
||||
/>
|
||||
<MalioInputText
|
||||
v-else
|
||||
v-else-if="degraded && (!hideEmpty || isFilled(model.city))"
|
||||
:model-value="model.city"
|
||||
:label="t('technique.providers.form.address.city')"
|
||||
:mask="ADDRESS_MASK"
|
||||
:readonly="readonly"
|
||||
:required="true"
|
||||
:disabled="disabled"
|
||||
:required="!readonly && !disabled"
|
||||
:error="errors?.city"
|
||||
@update:model-value="(v: string) => update('city', v)"
|
||||
/>
|
||||
|
||||
<!-- Adresse (BAN) sur 2 colonnes + Adresse complementaire. allow-create : le
|
||||
texte saisi est conserve si la BAN ne propose rien (saisie manuelle). -->
|
||||
<div class="col-span-2">
|
||||
<div v-if="!hideEmpty || isFilled(model.street)" class="col-span-2">
|
||||
<MalioInputAutocomplete
|
||||
v-if="!readonly"
|
||||
v-if="!readonly && !disabled"
|
||||
:model-value="model.street"
|
||||
:options="addressOptions"
|
||||
:loading="addressLoading"
|
||||
:min-search-length="3"
|
||||
:label="t('technique.providers.form.address.street')"
|
||||
:readonly="readonly"
|
||||
:required="true"
|
||||
:disabled="disabled"
|
||||
:required="!readonly && !disabled"
|
||||
:error="errors?.street"
|
||||
:allow-create="true"
|
||||
:no-results-text="t('technique.providers.form.address.streetNotFound')"
|
||||
@@ -109,17 +109,20 @@
|
||||
:model-value="model.street"
|
||||
:label="t('technique.providers.form.address.street')"
|
||||
:readonly="readonly"
|
||||
:required="true"
|
||||
:disabled="disabled"
|
||||
:required="!readonly && !disabled"
|
||||
:error="errors?.street"
|
||||
@update:model-value="(v: string) => update('street', v)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="col-span-1">
|
||||
<div v-if="!hideEmpty || isFilled(model.streetComplement)" class="col-span-1">
|
||||
<MalioInputText
|
||||
:model-value="model.streetComplement"
|
||||
:label="t('technique.providers.form.address.streetComplement')"
|
||||
:mask="ADDRESS_MASK"
|
||||
:readonly="readonly"
|
||||
:disabled="disabled"
|
||||
:error="errors?.streetComplement"
|
||||
@update:model-value="(v: string) => update('streetComplement', v)"
|
||||
/>
|
||||
@@ -131,6 +134,8 @@
|
||||
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 { ADDRESS_MASK } from '~/shared/utils/textSanitize'
|
||||
import { isFilled } from '~/shared/utils/consultationDisplay'
|
||||
|
||||
// Masque code postal FR : 5 chiffres.
|
||||
const POSTAL_CODE_MASK = '#####'
|
||||
@@ -138,8 +143,6 @@ const POSTAL_CODE_MASK = '#####'
|
||||
const props = defineProps<{
|
||||
/** Brouillon de l'adresse (v-model). */
|
||||
modelValue: ProviderAddressFormDraft
|
||||
/** Categories autorisees sur une adresse (type PRESTATAIRE). */
|
||||
categoryOptions: RefOption[]
|
||||
/** Sites Starseed disponibles. */
|
||||
siteOptions: RefOption[]
|
||||
/** Contacts deja saisis, rattachables a l'adresse. */
|
||||
@@ -148,6 +151,10 @@ const props = defineProps<{
|
||||
countryOptions: RefOption[]
|
||||
removable?: boolean
|
||||
readonly?: boolean
|
||||
/** Bloc desactive (champs grises, consultation — distinct de readonly). */
|
||||
disabled?: boolean
|
||||
/** Consultation : masque les champs non remplis (ERP-193). */
|
||||
hideEmpty?: boolean
|
||||
/** Erreurs serveur 422 de cette ligne, indexees par champ (ERP-101). */
|
||||
errors?: Record<string, string>
|
||||
}>()
|
||||
@@ -193,11 +200,37 @@ const addressLoading = ref(false)
|
||||
// Conserve les suggestions d'adresse pour retrouver ville/CP au moment du select.
|
||||
let lastAddressSuggestions: AddressSuggestion[] = []
|
||||
|
||||
// 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). */
|
||||
function update<K extends keyof ProviderAddressFormDraft>(field: K, value: ProviderAddressFormDraft[K]): void {
|
||||
emit('update:modelValue', { ...props.modelValue, [field]: value })
|
||||
}
|
||||
|
||||
/**
|
||||
* Selection d'une ville (select assiste BAN) → vide adresse + complement, devenus
|
||||
* incoherents avec la nouvelle ville. Ne reagit qu'a un vrai changement de valeur.
|
||||
* En mode degrade (saisie libre), la ville reste un simple `update` (pas de reset
|
||||
* a chaque frappe).
|
||||
*/
|
||||
function onCityChange(value: string | number | null): void {
|
||||
const next = value === null ? null : String(value)
|
||||
if (next === (props.modelValue.city ?? null)) {
|
||||
return
|
||||
}
|
||||
banAddressOptions.value = []
|
||||
lastAddressSuggestions = []
|
||||
emit('update:modelValue', {
|
||||
...props.modelValue,
|
||||
city: next,
|
||||
street: null,
|
||||
streetComplement: null,
|
||||
})
|
||||
}
|
||||
|
||||
/** Previent le parent (toast unique) que l'autocompletion est indisponible. */
|
||||
function notifyUnavailable(): void {
|
||||
if (!unavailableNotified) {
|
||||
@@ -208,9 +241,27 @@ function notifyUnavailable(): void {
|
||||
|
||||
/** Saisie du code postal → met a jour le champ + interroge la BAN pour la ville (RG-3.06). */
|
||||
async function onPostalCodeChange(value: string): Promise<void> {
|
||||
update('postalCode', value)
|
||||
|
||||
const digits = (value ?? '').replace(/\D/g, '')
|
||||
const previousDigits = (props.modelValue.postalCode ?? '').replace(/\D/g, '')
|
||||
|
||||
// CP complet (5 chiffres) et reellement modifie → ville, adresse et complement
|
||||
// deviennent incoherents avec le nouveau code postal : on les vide pour forcer
|
||||
// une re-saisie coherente (on n'efface pas pendant une correction partielle).
|
||||
if (digits.length === 5 && digits !== previousDigits) {
|
||||
banAddressOptions.value = []
|
||||
lastAddressSuggestions = []
|
||||
emit('update:modelValue', {
|
||||
...props.modelValue,
|
||||
postalCode: value,
|
||||
city: null,
|
||||
street: null,
|
||||
streetComplement: null,
|
||||
})
|
||||
}
|
||||
else {
|
||||
update('postalCode', value)
|
||||
}
|
||||
|
||||
if (digits.length < 5) {
|
||||
return
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user