feat(front) : util httpExternal + autocomplete adresse BAN (ERP-66) #52
@@ -201,7 +201,8 @@ const model = computed(() => props.modelValue)
|
|||||||
const degraded = ref(false)
|
const degraded = ref(false)
|
||||||
// Villes proposees par la BAN (alimentees a la saisie du code postal).
|
// Villes proposees par la BAN (alimentees a la saisie du code postal).
|
||||||
const banCityOptions = ref<RefOption[]>([])
|
const banCityOptions = ref<RefOption[]>([])
|
||||||
const addressOptions = ref<RefOption[]>([])
|
// Adresses proposees par la BAN (alimentees a la saisie d'adresse).
|
||||||
|
const banAddressOptions = ref<RefOption[]>([])
|
||||||
|
|
||||||
// Options ville effectives : on garantit que la ville courante figure toujours
|
// Options ville effectives : on garantit que la ville courante figure toujours
|
||||||
// dans la liste, sinon MalioSelect (qui resout le libelle depuis ses options)
|
// dans la liste, sinon MalioSelect (qui resout le libelle depuis ses options)
|
||||||
@@ -214,6 +215,20 @@ const cityOptions = computed<RefOption[]>(() => {
|
|||||||
}
|
}
|
||||||
return banCityOptions.value
|
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<RefOption[]>(() => {
|
||||||
|
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)
|
const addressLoading = ref(false)
|
||||||
// Conserve les suggestions d'adresse pour retrouver ville/CP au moment du select.
|
// Conserve les suggestions d'adresse pour retrouver ville/CP au moment du select.
|
||||||
let lastAddressSuggestions: AddressSuggestion[] = []
|
let lastAddressSuggestions: AddressSuggestion[] = []
|
||||||
@@ -280,7 +295,7 @@ async function onAddressSearch(query: string): Promise<void> {
|
|||||||
const postalCode = (model.value.postalCode ?? '').replace(/\D/g, '') || undefined
|
const postalCode = (model.value.postalCode ?? '').replace(/\D/g, '') || undefined
|
||||||
const suggestions = await autocomplete.searchAddress(query, postalCode)
|
const suggestions = await autocomplete.searchAddress(query, postalCode)
|
||||||
lastAddressSuggestions = suggestions
|
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 {
|
catch {
|
||||||
enterDegraded()
|
enterDegraded()
|
||||||
|
|||||||
@@ -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')
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -45,6 +45,7 @@ interface CategoryMember extends HydraMember {
|
|||||||
|
|
||||||
interface SiteMember extends HydraMember {
|
interface SiteMember extends HydraMember {
|
||||||
name: string
|
name: string
|
||||||
|
postalCode: string
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ReferentialMember extends HydraMember {
|
interface ReferentialMember extends HydraMember {
|
||||||
@@ -101,7 +102,10 @@ export function useClientReferentials() {
|
|||||||
fetchAll<CategoryMember>('/categories')
|
fetchAll<CategoryMember>('/categories')
|
||||||
.then((cats) => { categories.value = cats.map(c => ({ value: c['@id'], label: c.name, code: c.code })) }),
|
.then((cats) => { categories.value = cats.map(c => ({ value: c['@id'], label: c.name, code: c.code })) }),
|
||||||
fetchAll<SiteMember>('/sites')
|
fetchAll<SiteMember>('/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<ReferentialMember>('/tva_modes')
|
fetchAll<ReferentialMember>('/tva_modes')
|
||||||
.then((tva) => { tvaModes.value = tva.map(t => ({ value: t['@id'], label: t.label })) }),
|
.then((tva) => { tvaModes.value = tva.map(t => ({ value: t['@id'], label: t.label })) }),
|
||||||
fetchAll<ReferentialMember>('/payment_delays')
|
fetchAll<ReferentialMember>('/payment_delays')
|
||||||
|
|||||||
Reference in New Issue
Block a user