From f4313d1f3d047364d19c554684c937930eac7495 Mon Sep 17 00:00:00 2001 From: tristan Date: Wed, 3 Jun 2026 13:53:26 +0200 Subject: [PATCH] fix(front) : champ adresse vide apres validation + libelle departement des sites - ClientAddressBlock : la rue courante est toujours reinjectee dans les options de MalioInputAutocomplete (computed, miroir de cityOptions). Sinon, des que la liste de suggestions BAN est vide (remontage apres validation, edition d'une adresse existante), le composant ne resolvait plus la valeur liee et affichait un champ vide alors que la donnee etait bien persistee. Test de montage ajoute. - useClientReferentials : le libelle des sites = numero de departement (2 premiers chiffres du code postal, deja expose par /sites) au lieu du nom. --- .../components/ClientAddressBlock.vue | 19 ++++- .../__tests__/ClientAddressBlock.spec.ts | 76 +++++++++++++++++++ .../composables/useClientReferentials.ts | 6 +- 3 files changed, 98 insertions(+), 3 deletions(-) create mode 100644 frontend/modules/commercial/components/__tests__/ClientAddressBlock.spec.ts diff --git a/frontend/modules/commercial/components/ClientAddressBlock.vue b/frontend/modules/commercial/components/ClientAddressBlock.vue index 3539dbf..d19ad95 100644 --- a/frontend/modules/commercial/components/ClientAddressBlock.vue +++ b/frontend/modules/commercial/components/ClientAddressBlock.vue @@ -201,7 +201,8 @@ const model = computed(() => props.modelValue) const degraded = ref(false) // Villes proposees par la BAN (alimentees a la saisie du code postal). const banCityOptions = ref([]) -const addressOptions = ref([]) +// Adresses proposees par la BAN (alimentees a la saisie d'adresse). +const banAddressOptions = ref([]) // Options ville effectives : on garantit que la ville courante figure toujours // dans la liste, sinon MalioSelect (qui resout le libelle depuis ses options) @@ -214,6 +215,20 @@ const cityOptions = computed(() => { } return banCityOptions.value }) + +// Meme garantie que cityOptions pour le champ Adresse : la rue courante doit +// toujours figurer dans les options, sinon MalioInputAutocomplete (qui resout +// l'affichage depuis ses options) laisse le champ VIDE des que la liste de +// suggestions BAN est vide — typiquement juste apres validation (remontage) ou +// a l'edition d'une adresse existante (1.12), alors que la valeur est bien +// persistee. On reinjecte donc la rue liee si la BAN ne l'a pas (re)proposee. +const addressOptions = computed(() => { + const current = props.modelValue.street + if (current && !banAddressOptions.value.some(o => o.value === current)) { + return [{ value: current, label: current }, ...banAddressOptions.value] + } + return banAddressOptions.value +}) const addressLoading = ref(false) // Conserve les suggestions d'adresse pour retrouver ville/CP au moment du select. let lastAddressSuggestions: AddressSuggestion[] = [] @@ -280,7 +295,7 @@ async function onAddressSearch(query: string): Promise { const postalCode = (model.value.postalCode ?? '').replace(/\D/g, '') || undefined const suggestions = await autocomplete.searchAddress(query, postalCode) lastAddressSuggestions = suggestions - addressOptions.value = suggestions.map(s => ({ value: s.street, label: s.label })) + banAddressOptions.value = suggestions.map(s => ({ value: s.street, label: s.label })) } catch { enterDegraded() diff --git a/frontend/modules/commercial/components/__tests__/ClientAddressBlock.spec.ts b/frontend/modules/commercial/components/__tests__/ClientAddressBlock.spec.ts new file mode 100644 index 0000000..1ec6587 --- /dev/null +++ b/frontend/modules/commercial/components/__tests__/ClientAddressBlock.spec.ts @@ -0,0 +1,76 @@ +import { describe, it, expect, vi } from 'vitest' +import { mount } from '@vue/test-utils' +import { defineComponent, h, ref, computed } from 'vue' +import { emptyAddress } from '~/modules/commercial/types/clientForm' +import ClientAddressBlock from '../ClientAddressBlock.vue' + +// Le composable BAN est mocke : aucun appel reseau, aucune suggestion chargee. +// On reproduit ainsi l'etat « adresse persistee, mais liste de suggestions +// vide » (remontage apres validation / edition d'une adresse existante). +vi.mock('~/shared/composables/useAddressAutocomplete', () => ({ + useAddressAutocomplete: () => ({ + searchCity: vi.fn(), + searchAddress: vi.fn(), + }), +})) + +// Auto-imports Nuxt/Vue utilises sans import explicite par le composant. +vi.stubGlobal('useI18n', () => ({ t: (key: string) => key })) +vi.stubGlobal('ref', ref) +vi.stubGlobal('computed', computed) + +// Stub de MalioInputAutocomplete : expose les `value` des options recues, pour +// verifier que la rue courante figure bien dans la liste (sinon le composant +// Malio ne peut pas resoudre/afficher la valeur liee -> champ vide). +const MalioInputAutocompleteStub = defineComponent({ + name: 'MalioInputAutocomplete', + props: { + modelValue: { type: [String, Number, null], default: undefined }, + options: { type: Array as () => { value: string | number, label: string }[], default: () => [] }, + loading: { type: Boolean, default: false }, + minSearchLength: { type: Number, default: 0 }, + label: { type: String, default: '' }, + readonly: { type: Boolean, default: false }, + }, + emits: ['update:modelValue', 'search', 'select'], + setup(props) { + return () => h('div', { + 'data-testid': 'addr-autocomplete', + 'data-options': JSON.stringify(props.options.map(o => o.value)), + }) + }, +}) + +function mountBlock(street: string | null) { + return mount(ClientAddressBlock, { + props: { + modelValue: { ...emptyAddress(), street }, + title: 'Adresse', + categoryOptions: [], + siteOptions: [], + contactOptions: [], + countryOptions: [], + }, + global: { + stubs: { + MalioButtonIcon: true, + MalioCheckbox: true, + MalioSelect: true, + MalioSelectCheckbox: true, + MalioInputText: true, + MalioInputAutocomplete: MalioInputAutocompleteStub, + }, + }, + }) +} + +describe('ClientAddressBlock — affichage de l\'adresse persistee', () => { + it('inclut la rue courante dans les options de l\'autocomplete meme sans recherche BAN', () => { + const wrapper = mountBlock('8 Boulevard du Port') + + const el = wrapper.find('[data-testid="addr-autocomplete"]') + const values = JSON.parse(el.attributes('data-options') ?? '[]') + + expect(values).toContain('8 Boulevard du Port') + }) +}) diff --git a/frontend/modules/commercial/composables/useClientReferentials.ts b/frontend/modules/commercial/composables/useClientReferentials.ts index 0f5bb8f..6721ee2 100644 --- a/frontend/modules/commercial/composables/useClientReferentials.ts +++ b/frontend/modules/commercial/composables/useClientReferentials.ts @@ -45,6 +45,7 @@ interface CategoryMember extends HydraMember { interface SiteMember extends HydraMember { name: string + postalCode: string } interface ReferentialMember extends HydraMember { @@ -101,7 +102,10 @@ export function useClientReferentials() { fetchAll('/categories') .then((cats) => { categories.value = cats.map(c => ({ value: c['@id'], label: c.name, code: c.code })) }), fetchAll('/sites') - .then((sitesList) => { sites.value = sitesList.map(s => ({ value: s['@id'], label: s.name })) }), + // Libelle = numero de departement (2 premiers chiffres du code + // postal du site), ex: 86100 -> « 86 ». Le code postal est deja + // expose par /sites (groupe site:read) — aucune colonne a ajouter. + .then((sitesList) => { sites.value = sitesList.map(s => ({ value: s['@id'], label: (s.postalCode ?? '').slice(0, 2) })) }), fetchAll('/tva_modes') .then((tva) => { tvaModes.value = tva.map(t => ({ value: t['@id'], label: t.label })) }), fetchAll('/payment_delays')