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:
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user