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')