diff --git a/frontend/i18n/locales/fr.json b/frontend/i18n/locales/fr.json
index 3205c85..cc62b73 100644
--- a/frontend/i18n/locales/fr.json
+++ b/frontend/i18n/locales/fr.json
@@ -71,7 +71,7 @@
"companyName": "Nom",
"categories": "Catégories",
"sites": "Site",
- "lastActivity": "Dernière activité"
+ "lastActivity": "Dernière modification"
},
"filters": {
"title": "Filtres",
@@ -125,7 +125,7 @@
},
"edit": {
"title": "Modifier le fournisseur",
- "back": "Retour au répertoire",
+ "back": "Retour à la consultation",
"loading": "Chargement du fournisseur…",
"notFound": "Fournisseur introuvable.",
"save": "Enregistrer"
@@ -215,7 +215,7 @@
"companyName": "Nom",
"categories": "Catégories",
"sites": "Site",
- "lastActivity": "Dernière activité"
+ "lastActivity": "Dernière modification"
},
"filters": {
"title": "Filtres",
@@ -268,7 +268,7 @@
},
"edit": {
"title": "Modifier le client",
- "back": "Retour au répertoire",
+ "back": "Retour à la consultation",
"loading": "Chargement du client…",
"notFound": "Client introuvable.",
"save": "Enregistrer"
@@ -385,7 +385,7 @@
"companyName": "Nom",
"categories": "Catégories",
"sites": "Site",
- "lastActivity": "Dernière activité"
+ "lastActivity": "Dernière modification"
},
"filters": {
"title": "Filtres",
@@ -420,7 +420,7 @@
},
"edit": {
"title": "Modifier le prestataire",
- "back": "Retour à la fiche",
+ "back": "Retour à la consultation",
"loading": "Chargement…",
"notFound": "Prestataire introuvable.",
"save": "Enregistrer"
@@ -453,7 +453,6 @@
},
"address": {
"sites": "Sites",
- "categories": "Catégorie",
"contacts": "Contact(s) rattaché(s)",
"country": "Pays",
"postalCode": "Code postal",
@@ -509,7 +508,7 @@
"name": "Nom",
"certification": "Certification",
"validityDate": "Date de validité",
- "lastActivity": "Dernière activité"
+ "lastActivity": "Dernière modification"
},
"certification": {
"QUALIMAT": "QUALIMAT",
@@ -558,8 +557,8 @@
"message": "Ce transporteur réapparaîtra dans le répertoire actif. Confirmer la restauration ?"
},
"price": {
- "group": "Type de transport",
- "carrier": "Transporteurs",
+ "group": "Transport",
+ "carrier": "Fournisseurs / Clients",
"aproOrSite": "Adresse sites",
"delivery": "Adresse livraisons",
"forfait": "Forfait (€)",
@@ -790,7 +789,8 @@
"auth": {
"logout": "Deconnexion reussie"
},
- "title": "Succès"
+ "title": "Succès",
+ "deleted": "Suppression effectuée"
},
"admin": {
"roles": {
diff --git a/frontend/modules/commercial/components/ClientAddressBlock.vue b/frontend/modules/commercial/components/ClientAddressBlock.vue
index ff2c574..8e5886a 100644
--- a/frontend/modules/commercial/components/ClientAddressBlock.vue
+++ b/frontend/modules/commercial/components/ClientAddressBlock.vue
@@ -2,7 +2,7 @@
@@ -33,17 +34,21 @@
:label="t('commercial.clients.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))"
/>
+
update('contactIris', v.map(String))"
/>
@@ -55,8 +60,9 @@
v-if="isBillingEmailRequired(model)"
:model-value="model.billingEmail"
:label="t('commercial.clients.form.address.billingEmail')"
- :required="true"
+ :required="!readonly && !disabled"
:readonly="readonly"
+ :disabled="disabled"
:lowercase="true"
:error="errors?.billingEmail"
:addable="!model.hasSecondaryBillingEmail && !readonly"
@@ -64,13 +70,17 @@
@update:model-value="(v: string) => update('billingEmail', v)"
@add="revealSecondaryBillingEmail"
/>
-
+
+
update('billingEmailSecondary', v)"
@@ -82,7 +92,8 @@
:label="t('commercial.clients.form.address.categories')"
:display-tag="true"
:readonly="readonly"
- :required="true"
+ :disabled="disabled"
+ :required="!readonly && !disabled"
:error="errors?.categories"
@update:model-value="(v: (string | number)[]) => update('categoryIris', v.map(String))"
/>
@@ -92,7 +103,8 @@
:options="countryOptions"
:label="t('commercial.clients.form.address.country')"
:readonly="readonly"
- :required="true"
+ :disabled="disabled"
+ :required="!readonly && !disabled"
@update:model-value="(v: string | number | null) => update('country', String(v ?? 'France'))"
/>
@@ -101,7 +113,8 @@
:label="t('commercial.clients.form.address.postalCode')"
:mask="POSTAL_CODE_MASK"
:readonly="readonly"
- :required="true"
+ :disabled="disabled"
+ :required="!readonly && !disabled"
:error="errors?.postalCode"
@update:model-value="onPostalCodeChange"
/>
@@ -115,17 +128,20 @@
:options="cityOptions"
:label="t('commercial.clients.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"
/>
update('city', v)"
/>
@@ -142,14 +158,15 @@
blur/Entree (saisie manuelle) — sinon il serait efface. La ville reste
pilotee par le code postal ; choisir une suggestion remplit rue+ville+CP. -->
update('street', v)"
/>
-
+
update('streetComplement', v)"
/>
@@ -191,6 +211,8 @@ 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 { ADDRESS_MASK } from '~/shared/utils/textSanitize'
+import { isFilled } from '~/shared/utils/consultationDisplay'
// Masque code postal FR : 5 chiffres.
const POSTAL_CODE_MASK = '#####'
@@ -209,6 +231,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
}>()
@@ -284,11 +310,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) ; les emails de facturation valident leur format (Assert\Email).
+
/** Emet un nouveau brouillon avec le champ modifie (immutabilite). */
function update(field: K, value: AddressFormDraft[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,
+ })
+}
+
/** Revele le 2e champ email de facturation (clic sur le « + »). */
function revealSecondaryBillingEmail(): void {
emit('update:modelValue', { ...props.modelValue, hasSecondaryBillingEmail: true })
@@ -304,9 +356,27 @@ function notifyUnavailable(): void {
/** Saisie du code postal → met a jour le champ + interroge la BAN pour la ville. */
async function onPostalCodeChange(value: string): Promise {
- 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
}
diff --git a/frontend/modules/commercial/components/ClientContactBlock.vue b/frontend/modules/commercial/components/ClientContactBlock.vue
index e4284c7..f2e466f 100644
--- a/frontend/modules/commercial/components/ClientContactBlock.vue
+++ b/frontend/modules/commercial/components/ClientContactBlock.vue
@@ -4,7 +4,7 @@
non supprimable (1er bloc obligatoire RG-1.14) ou en lecture seule.
ariaLabel via v-bind objet (prop camelCase ; aria-* serait un attribut HTML). -->
update('lastName', v)"
/>
update('firstName', v)"
/>
-
+
update('jobTitle', v)"
/>
update('email', v)"
/>
update('phoneSecondary', v)"
/>
@@ -71,6 +84,8 @@