fix(front) : reset des champs adresse dependants au changement de CP et de ville (4 blocs adresse) (ERP-193)

Changement du code postal (5 chiffres et different) → vide ville, adresse et
complement. Selection d'une nouvelle ville → vide adresse et complement. Applique
aux 4 blocs adresse (client, fournisseur, prestataire, transporteur).
This commit is contained in:
2026-06-22 08:35:19 +02:00
parent 38ad7906ba
commit bb5421663f
5 changed files with 344 additions and 12 deletions
@@ -127,7 +127,7 @@
empty-option-label=""
: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
@@ -312,6 +312,27 @@ function update<K extends keyof AddressFormDraft>(field: K, value: AddressFormDr
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 })
@@ -327,9 +348,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<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
}
@@ -98,7 +98,7 @@
empty-option-label=""
: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
@@ -267,6 +267,27 @@ function update<K extends keyof SupplierAddressFormDraft>(field: K, value: Suppl
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) {
@@ -277,9 +298,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<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
}
@@ -171,6 +171,182 @@ describe('ClientAddressBlock — mapping erreur par champ (ERP-101)', () => {
})
})
/**
* Stub MalioInputText emetteur : re-expose `label` et relaie `update:model-value`,
* pour piloter le champ Code postal et observer le brouillon emis.
*/
const MalioInputTextEmitter = defineComponent({
name: 'MalioInputTextEmitter',
props: {
modelValue: { type: [String, Number, null], default: undefined },
label: { type: String, default: '' },
},
emits: ['update:modelValue'],
setup(props) {
return () => h('div', { 'data-testid': 'addr-input', 'data-label': props.label })
},
})
describe('ClientAddressBlock — changement de code postal vide les champs dependants (ERP-193)', () => {
beforeEach(() => {
searchCityMock.mockReset()
searchCityMock.mockResolvedValue([])
})
function mountFilled() {
return mount(ClientAddressBlock, {
props: {
modelValue: {
...emptyAddress(),
postalCode: '75001',
city: 'Paris',
street: '8 Boulevard du Port',
streetComplement: 'Bat A',
},
title: 'Adresse',
categoryOptions: [],
siteOptions: [],
contactOptions: [],
countryOptions: [],
},
global: {
stubs: {
MalioButtonIcon: true,
MalioCheckbox: true,
MalioSelect: true,
MalioSelectCheckbox: true,
MalioInputAutocomplete: MalioInputAutocompleteStub,
MalioInputText: MalioInputTextEmitter,
},
},
})
}
function postalCodeField(wrapper: ReturnType<typeof mountFilled>) {
return wrapper.findAllComponents(MalioInputTextEmitter).find(
c => c.props('label') === 'commercial.clients.form.address.postalCode',
)
}
it('vide ville, adresse et complement quand le CP complet change', async () => {
const wrapper = mountFilled()
postalCodeField(wrapper)!.vm.$emit('update:modelValue', '33000')
await flushPromises()
const last = wrapper.emitted('update:modelValue')?.at(-1)?.[0] as Record<string, unknown>
expect(last.postalCode).toBe('33000')
expect(last.city).toBeNull()
expect(last.street).toBeNull()
expect(last.streetComplement).toBeNull()
})
it('ne vide pas les champs si le CP reste incomplet (< 5 chiffres)', async () => {
const wrapper = mountFilled()
postalCodeField(wrapper)!.vm.$emit('update:modelValue', '7500')
await flushPromises()
const last = wrapper.emitted('update:modelValue')?.at(-1)?.[0] as Record<string, unknown>
expect(last.postalCode).toBe('7500')
expect(last.city).toBe('Paris')
expect(last.street).toBe('8 Boulevard du Port')
expect(last.streetComplement).toBe('Bat A')
})
it('ne vide pas les champs si le CP complet est identique', async () => {
const wrapper = mountFilled()
postalCodeField(wrapper)!.vm.$emit('update:modelValue', '75001')
await flushPromises()
const last = wrapper.emitted('update:modelValue')?.at(-1)?.[0] as Record<string, unknown>
expect(last.city).toBe('Paris')
expect(last.street).toBe('8 Boulevard du Port')
expect(last.streetComplement).toBe('Bat A')
})
})
/**
* Stub MalioSelect emetteur : re-expose `label` et relaie `update:model-value`,
* pour piloter le select Ville et observer le brouillon emis.
*/
const MalioSelectEmitter = defineComponent({
name: 'MalioSelectEmitter',
props: {
modelValue: { type: [String, Number, null], default: undefined },
label: { type: String, default: '' },
},
emits: ['update:modelValue'],
setup(props) {
return () => h('div', { 'data-testid': 'addr-select', 'data-label': props.label })
},
})
describe('ClientAddressBlock — changement de ville vide adresse + complement (ERP-193)', () => {
beforeEach(() => {
searchCityMock.mockReset()
searchCityMock.mockResolvedValue([])
})
function mountFilled() {
return mount(ClientAddressBlock, {
props: {
modelValue: {
...emptyAddress(),
postalCode: '75001',
city: 'Paris',
street: '8 Boulevard du Port',
streetComplement: 'Bat A',
},
title: 'Adresse',
categoryOptions: [],
siteOptions: [],
contactOptions: [],
countryOptions: [],
},
global: {
stubs: {
MalioButtonIcon: true,
MalioCheckbox: true,
MalioSelectCheckbox: true,
MalioInputText: true,
MalioInputAutocomplete: MalioInputAutocompleteStub,
MalioSelect: MalioSelectEmitter,
},
},
})
}
function cityField(wrapper: ReturnType<typeof mountFilled>) {
return wrapper.findAllComponents(MalioSelectEmitter).find(
c => c.props('label') === 'commercial.clients.form.address.city',
)
}
it('vide adresse et complement quand la ville change', async () => {
const wrapper = mountFilled()
cityField(wrapper)!.vm.$emit('update:modelValue', 'Lyon')
await flushPromises()
const last = wrapper.emitted('update:modelValue')?.at(-1)?.[0] as Record<string, unknown>
expect(last.city).toBe('Lyon')
expect(last.street).toBeNull()
expect(last.streetComplement).toBeNull()
})
it('ne vide pas si la ville selectionnee est identique', async () => {
const wrapper = mountFilled()
cityField(wrapper)!.vm.$emit('update:modelValue', 'Paris')
await flushPromises()
// Aucun nouvel emit (valeur inchangee) → l'adresse reste intacte.
expect(wrapper.emitted('update:modelValue')).toBeUndefined()
})
})
describe('ClientAddressBlock — recherche adresse robuste (erreur BAN)', () => {
beforeEach(() => {
searchAddressMock.mockReset()
@@ -79,7 +79,7 @@
empty-option-label=""
: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
@@ -218,6 +218,27 @@ function update<K extends keyof ProviderAddressFormDraft>(field: K, value: Provi
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) {
@@ -228,9 +249,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
}
@@ -36,7 +36,7 @@
empty-option-label=""
: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
@@ -171,6 +171,27 @@ function update<K extends keyof CarrierAddressFormDraft>(field: K, value: Carrie
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) {
@@ -181,9 +202,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<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
}