feat(commercial) : géolocalisation des adresses Tiers (lat/lng + géocodage BAN + pin ajustable) (ERP-122)
Ajoute la géolocalisation aux adresses Client et Fournisseur, socle de la tournée commerciale (M6 field-sales). Back : - migration : latitude/longitude NUMERIC(10,7), geo_manual BOOLEAN, geocoded_at TIMESTAMPTZ sur client_address et supplier_address (+ COMMENT ON COLUMN FR) - GeolocatableAddressInterface (Shared/Domain/Contract) implémenté par les deux entités ; bornes WGS84 validées (Range -90/90, -180/180, messages FR) - GeocoderInterface + BanGeocoder (api-adresse.data.gouv.fr), branché via AddressGeocoder dans les processors ; géocodage auto au create/update - RG-6.08 : geo_manual=true fige les coordonnées (pas de réécriture auto) - symfony/http-client passe en dépendance de production Front : - AddressGeoPin (Leaflet + OSM) : marqueur déplaçable -> PATCH lat/lng + geoManual=true, bouton Re-géocoder, badges « à géolocaliser » / « pin manuel » - intégration dans les blocs adresse Client et Fournisseur Tests : PHPUnit (géocodage create, non-réécriture RG-6.08, mapping BAN, bornes) + Vitest (drag du pin, badges, re-géocodage).
This commit is contained in:
@@ -49,6 +49,14 @@
|
||||
"commercial": {
|
||||
"title": "Commercial",
|
||||
"welcome": "Module Commercial",
|
||||
"geo": {
|
||||
"title": "Position géographique",
|
||||
"toGeolocate": "À géolocaliser",
|
||||
"manualPin": "Pin ajusté manuellement",
|
||||
"dragHint": "Déplacez le marqueur pour ajuster la position exacte (lieu-dit, entrée de site...).",
|
||||
"regeocode": "Re-géocoder depuis l'adresse",
|
||||
"regeocodeFailed": "Adresse introuvable — position inchangée."
|
||||
},
|
||||
"suppliers": {
|
||||
"title": "Répertoire fournisseurs",
|
||||
"add": "Ajouter",
|
||||
|
||||
@@ -0,0 +1,216 @@
|
||||
<template>
|
||||
<div data-testid="geo-pin">
|
||||
<div class="mb-1 flex items-center gap-2">
|
||||
<span class="text-sm font-medium text-gray-700">{{ t('commercial.geo.title') }}</span>
|
||||
<!-- Badge « a geolocaliser » : adresse valide mais sans coordonnees
|
||||
(spec M6 § 3.2 — exclue du calcul de tournee, RG-6.05). -->
|
||||
<span
|
||||
v-if="!hasCoords"
|
||||
class="inline-flex items-center rounded-full bg-yellow-100 px-2 py-0.5 text-xs font-medium text-yellow-800"
|
||||
data-testid="geo-badge-missing"
|
||||
>
|
||||
{{ t('commercial.geo.toGeolocate') }}
|
||||
</span>
|
||||
<!-- Pin fige a la main (RG-6.08) : informatif. -->
|
||||
<span
|
||||
v-else-if="geoManual"
|
||||
class="inline-flex items-center rounded-full bg-blue-100 px-2 py-0.5 text-xs font-medium text-blue-800"
|
||||
data-testid="geo-badge-manual"
|
||||
>
|
||||
{{ t('commercial.geo.manualPin') }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Mini-carte Leaflet (exception documentee a @malio/layer-ui : carte
|
||||
interactive, type non couvert par la lib — cf. frontend.md
|
||||
§ Composants formulaires). TODO : migrer si la lib couvre un jour
|
||||
les cartes. -->
|
||||
<div
|
||||
v-if="hasCoords"
|
||||
ref="mapEl"
|
||||
class="h-48 w-full rounded border border-gray-200"
|
||||
data-testid="geo-map"
|
||||
/>
|
||||
<p v-if="hasCoords && !readonly" class="mt-1 text-xs text-gray-500">
|
||||
{{ t('commercial.geo.dragHint') }}
|
||||
</p>
|
||||
|
||||
<div v-if="!readonly" class="mt-2 flex items-center gap-4">
|
||||
<MalioButton
|
||||
variant="secondary"
|
||||
:label="t('commercial.geo.regeocode')"
|
||||
:disabled="regeocoding || !canRegeocode"
|
||||
data-testid="geo-regeocode"
|
||||
@click="regeocode"
|
||||
/>
|
||||
<span v-if="regeocodeFailed" class="text-xs text-red-600" data-testid="geo-regeocode-failed">
|
||||
{{ t('commercial.geo.regeocodeFailed') }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { Map as LeafletMap, Marker } from 'leaflet'
|
||||
import { useAddressAutocomplete } from '~/shared/composables/useAddressAutocomplete'
|
||||
|
||||
/**
|
||||
* Mini-carte d'ajustement du pin d'une adresse Tiers (M6.1, spec § 8.3).
|
||||
*
|
||||
* - Marqueur deplacable : au drag, emet les coordonnees corrigees avec
|
||||
* geoManual = true (RG-6.08 : le geocodage auto ne reecrira plus). Le parent
|
||||
* met a jour le brouillon ; la persistance suit le submit du formulaire
|
||||
* (POST/PATCH de l'adresse), comme tous les champs du bloc.
|
||||
* - « Re-geocoder depuis l'adresse » : previsualise la position BAN cote front
|
||||
* et emet geoManual = false — au save, le back (BanGeocoder) refait autorite
|
||||
* et pose geocodedAt.
|
||||
* - Sans coordonnees : pas de carte, badge « a geolocaliser ».
|
||||
*/
|
||||
const props = defineProps<{
|
||||
/** Latitude WGS84 (chaine decimale) ou null si non geolocalisee. */
|
||||
latitude: string | null
|
||||
/** Longitude WGS84 (chaine decimale) ou null si non geolocalisee. */
|
||||
longitude: string | null
|
||||
/** RG-6.08 : pin deja corrige a la main. */
|
||||
geoManual: boolean
|
||||
/** Adresse postale a re-geocoder (« rue, code postal ville »). */
|
||||
geocodeQuery: string | null
|
||||
readonly?: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
/** Nouveau positionnement du pin (drag manuel ou re-geocodage previsualise). */
|
||||
'update:coords': [value: { latitude: string, longitude: string, geoManual: boolean }]
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
const autocomplete = useAddressAutocomplete()
|
||||
|
||||
const mapEl = ref<HTMLElement | null>(null)
|
||||
const regeocoding = ref(false)
|
||||
const regeocodeFailed = ref(false)
|
||||
|
||||
const hasCoords = computed(() =>
|
||||
props.latitude !== null && props.latitude !== ''
|
||||
&& props.longitude !== null && props.longitude !== '',
|
||||
)
|
||||
|
||||
const canRegeocode = computed(() => (props.geocodeQuery ?? '').trim().length >= 3)
|
||||
|
||||
// Instances Leaflet (hors reactivite Vue : un proxy sur la Map casse Leaflet).
|
||||
let map: LeafletMap | null = null
|
||||
let marker: Marker | null = null
|
||||
|
||||
/** Zoom d'affichage du pin (niveau rue). */
|
||||
const PIN_ZOOM = 16
|
||||
|
||||
/**
|
||||
* Monte la carte Leaflet dans le conteneur (import dynamique : la lib n'est
|
||||
* chargee que si l'adresse a des coordonnees).
|
||||
*/
|
||||
async function ensureMap(): Promise<void> {
|
||||
if (map !== null || mapEl.value === null || !hasCoords.value) {
|
||||
return
|
||||
}
|
||||
|
||||
const mod = await import('leaflet')
|
||||
const L = mod.default ?? mod
|
||||
await import('leaflet/dist/leaflet.css')
|
||||
|
||||
// Le conteneur peut avoir disparu pendant le chargement async (v-if).
|
||||
if (mapEl.value === null) {
|
||||
return
|
||||
}
|
||||
|
||||
const position: [number, number] = [Number(props.latitude), Number(props.longitude)]
|
||||
|
||||
map = L.map(mapEl.value, { scrollWheelZoom: false }).setView(position, PIN_ZOOM)
|
||||
L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>',
|
||||
maxZoom: 19,
|
||||
}).addTo(map)
|
||||
|
||||
// divIcon SVG inline : evite les assets PNG de Leaflet (chemins casses par
|
||||
// le bundler Vite sans configuration dediee).
|
||||
const icon = L.divIcon({
|
||||
className: '',
|
||||
html: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="28" height="40" fill="#2563eb" stroke="#1e40af" stroke-width="0.5"><path d="M12 0C7 0 3 4 3 9c0 6.6 9 15 9 15s9-8.4 9-15c0-5-4-9-9-9zm0 12.5A3.5 3.5 0 1 1 12 5.5a3.5 3.5 0 0 1 0 7z"/></svg>',
|
||||
iconSize: [28, 40],
|
||||
iconAnchor: [14, 40],
|
||||
})
|
||||
|
||||
marker = L.marker(position, { icon, draggable: !props.readonly }).addTo(map)
|
||||
marker.on('dragend', onMarkerDragEnd)
|
||||
}
|
||||
|
||||
/** Drag du pin -> coordonnees corrigees + geoManual (RG-6.08). */
|
||||
function onMarkerDragEnd(): void {
|
||||
if (marker === null) {
|
||||
return
|
||||
}
|
||||
const position = marker.getLatLng()
|
||||
emit('update:coords', {
|
||||
latitude: position.lat.toFixed(7),
|
||||
longitude: position.lng.toFixed(7),
|
||||
geoManual: true,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* « Re-geocoder depuis l'adresse » : previsualisation BAN cote front. Emet
|
||||
* geoManual = false — le geocodage serveur refait autorite au save.
|
||||
*/
|
||||
async function regeocode(): Promise<void> {
|
||||
regeocodeFailed.value = false
|
||||
const query = (props.geocodeQuery ?? '').trim()
|
||||
if (query.length < 3) {
|
||||
regeocodeFailed.value = true
|
||||
return
|
||||
}
|
||||
|
||||
regeocoding.value = true
|
||||
try {
|
||||
const coords = await autocomplete.geocode(query)
|
||||
if (coords === null) {
|
||||
regeocodeFailed.value = true
|
||||
return
|
||||
}
|
||||
emit('update:coords', { ...coords, geoManual: false })
|
||||
}
|
||||
catch {
|
||||
// BAN indisponible : position inchangee, message inline.
|
||||
regeocodeFailed.value = true
|
||||
}
|
||||
finally {
|
||||
regeocoding.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// Coordonnees modifiees par le parent (drag deja applique, re-geocodage,
|
||||
// rechargement) : recale le marqueur, ou monte la carte si elle n'existe pas
|
||||
// encore (premieres coordonnees d'une adresse « a geolocaliser »).
|
||||
watch(
|
||||
() => [props.latitude, props.longitude] as const,
|
||||
async () => {
|
||||
if (!hasCoords.value) {
|
||||
return
|
||||
}
|
||||
if (map === null) {
|
||||
await nextTick()
|
||||
await ensureMap()
|
||||
return
|
||||
}
|
||||
const position: [number, number] = [Number(props.latitude), Number(props.longitude)]
|
||||
marker?.setLatLng(position)
|
||||
map.panTo(position)
|
||||
},
|
||||
)
|
||||
|
||||
onMounted(ensureMap)
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
map?.remove()
|
||||
map = null
|
||||
marker = null
|
||||
})
|
||||
</script>
|
||||
@@ -178,6 +178,19 @@
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Pin geographique de l'adresse (M6.1, spec § 8.3) : mini-carte avec
|
||||
marqueur ajustable, persiste au submit comme le reste du bloc. -->
|
||||
<div class="col-span-4">
|
||||
<AddressGeoPin
|
||||
:latitude="model.latitude"
|
||||
:longitude="model.longitude"
|
||||
:geo-manual="model.geoManual"
|
||||
:geocode-query="geocodeQuery"
|
||||
:readonly="readonly"
|
||||
@update:coords="onCoordsUpdate"
|
||||
/>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -289,6 +302,24 @@ function update<K extends keyof AddressFormDraft>(field: K, value: AddressFormDr
|
||||
emit('update:modelValue', { ...props.modelValue, [field]: value })
|
||||
}
|
||||
|
||||
// Adresse postale a re-geocoder (« rue, code postal ville ») — miroir du
|
||||
// getDisplayLabel() serveur (le complement bruite le geocodage, exclu).
|
||||
const geocodeQuery = computed<string | null>(() => {
|
||||
const locality = [model.value.postalCode, model.value.city].filter(Boolean).join(' ')
|
||||
const parts = [model.value.street, locality].filter(part => part && String(part).trim() !== '')
|
||||
return parts.length > 0 ? parts.join(', ') : null
|
||||
})
|
||||
|
||||
/** Pin deplace / re-geocode : repercute coordonnees + drapeau manuel (RG-6.08). */
|
||||
function onCoordsUpdate(coords: { latitude: string, longitude: string, geoManual: boolean }): void {
|
||||
emit('update:modelValue', {
|
||||
...props.modelValue,
|
||||
latitude: coords.latitude,
|
||||
longitude: coords.longitude,
|
||||
geoManual: coords.geoManual,
|
||||
})
|
||||
}
|
||||
|
||||
/** Revele le 2e champ email de facturation (clic sur le « + »). */
|
||||
function revealSecondaryBillingEmail(): void {
|
||||
emit('update:modelValue', { ...props.modelValue, hasSecondaryBillingEmail: true })
|
||||
|
||||
@@ -162,6 +162,19 @@
|
||||
:readonly="readonly"
|
||||
@update:model-value="(v: boolean) => update('triageProvider', v)"
|
||||
/>
|
||||
|
||||
<!-- Pin geographique de l'adresse (M6.1, spec § 8.3) : mini-carte avec
|
||||
marqueur ajustable, persiste au submit comme le reste du bloc. -->
|
||||
<div class="col-span-4">
|
||||
<AddressGeoPin
|
||||
:latitude="model.latitude"
|
||||
:longitude="model.longitude"
|
||||
:geo-manual="model.geoManual"
|
||||
:geocode-query="geocodeQuery"
|
||||
:readonly="readonly"
|
||||
@update:coords="onCoordsUpdate"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -243,6 +256,24 @@ function update<K extends keyof SupplierAddressFormDraft>(field: K, value: Suppl
|
||||
emit('update:modelValue', { ...props.modelValue, [field]: value })
|
||||
}
|
||||
|
||||
// Adresse postale a re-geocoder (« rue, code postal ville ») — miroir du
|
||||
// getDisplayLabel() serveur (le complement bruite le geocodage, exclu).
|
||||
const geocodeQuery = computed<string | null>(() => {
|
||||
const locality = [model.value.postalCode, model.value.city].filter(Boolean).join(' ')
|
||||
const parts = [model.value.street, locality].filter(part => part && String(part).trim() !== '')
|
||||
return parts.length > 0 ? parts.join(', ') : null
|
||||
})
|
||||
|
||||
/** Pin deplace / re-geocode : repercute coordonnees + drapeau manuel (RG-6.08). */
|
||||
function onCoordsUpdate(coords: { latitude: string, longitude: string, geoManual: boolean }): void {
|
||||
emit('update:modelValue', {
|
||||
...props.modelValue,
|
||||
latitude: coords.latitude,
|
||||
longitude: coords.longitude,
|
||||
geoManual: coords.geoManual,
|
||||
})
|
||||
}
|
||||
|
||||
/** Previent le parent (toast unique) que l'autocompletion est indisponible. */
|
||||
function notifyUnavailable(): void {
|
||||
if (!unavailableNotified) {
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||
import { mount, flushPromises } from '@vue/test-utils'
|
||||
import { ref, computed, watch, nextTick, onMounted, onBeforeUnmount } from 'vue'
|
||||
import AddressGeoPin from '../AddressGeoPin.vue'
|
||||
|
||||
// Mock Leaflet (hoisted) : capture le handler `dragend` et pilote la position
|
||||
// renvoyee par getLatLng — permet de simuler un drag du marqueur sans DOM reel.
|
||||
const leafletState = vi.hoisted(() => ({
|
||||
dragendHandler: null as (() => void) | null,
|
||||
markerPosition: { lat: 0, lng: 0 },
|
||||
}))
|
||||
|
||||
vi.mock('leaflet', () => {
|
||||
const marker = {
|
||||
addTo: vi.fn().mockReturnThis(),
|
||||
on: vi.fn((event: string, handler: () => void) => {
|
||||
if (event === 'dragend') {
|
||||
leafletState.dragendHandler = handler
|
||||
}
|
||||
}),
|
||||
getLatLng: vi.fn(() => leafletState.markerPosition),
|
||||
setLatLng: vi.fn(),
|
||||
}
|
||||
const map = {
|
||||
setView: vi.fn().mockReturnThis(),
|
||||
panTo: vi.fn(),
|
||||
remove: vi.fn(),
|
||||
}
|
||||
const L = {
|
||||
map: vi.fn(() => map),
|
||||
tileLayer: vi.fn(() => ({ addTo: vi.fn() })),
|
||||
divIcon: vi.fn(() => ({})),
|
||||
marker: vi.fn(() => marker),
|
||||
}
|
||||
return { default: L, ...L }
|
||||
})
|
||||
vi.mock('leaflet/dist/leaflet.css', () => ({ default: {} }))
|
||||
|
||||
// Mock controlable du geocodage BAN (bouton « Re-geocoder »).
|
||||
const { geocodeMock } = vi.hoisted(() => ({ geocodeMock: vi.fn() }))
|
||||
vi.mock('~/shared/composables/useAddressAutocomplete', () => ({
|
||||
useAddressAutocomplete: () => ({ geocode: geocodeMock }),
|
||||
}))
|
||||
|
||||
// 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)
|
||||
vi.stubGlobal('watch', watch)
|
||||
vi.stubGlobal('nextTick', nextTick)
|
||||
vi.stubGlobal('onMounted', onMounted)
|
||||
vi.stubGlobal('onBeforeUnmount', onBeforeUnmount)
|
||||
|
||||
interface PinProps {
|
||||
latitude?: string | null
|
||||
longitude?: string | null
|
||||
geoManual?: boolean
|
||||
geocodeQuery?: string | null
|
||||
readonly?: boolean
|
||||
}
|
||||
|
||||
function mountPin(props: PinProps = {}) {
|
||||
return mount(AddressGeoPin, {
|
||||
props: {
|
||||
latitude: null,
|
||||
longitude: null,
|
||||
geoManual: false,
|
||||
geocodeQuery: '1 rue du Test, 86100 Châtellerault',
|
||||
...props,
|
||||
},
|
||||
global: {
|
||||
stubs: { MalioButton: true },
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
leafletState.dragendHandler = null
|
||||
geocodeMock.mockReset()
|
||||
})
|
||||
|
||||
describe('AddressGeoPin — adresse sans coordonnees', () => {
|
||||
it('affiche le badge « a geolocaliser » et aucune carte', () => {
|
||||
const wrapper = mountPin()
|
||||
|
||||
expect(wrapper.find('[data-testid="geo-badge-missing"]').exists()).toBe(true)
|
||||
expect(wrapper.find('[data-testid="geo-map"]').exists()).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('AddressGeoPin — drag du marqueur (RG-6.08)', () => {
|
||||
it('emet les coordonnees corrigees avec geoManual=true au dragend', async () => {
|
||||
const wrapper = mountPin({ latitude: '46.5802596', longitude: '0.3404333' })
|
||||
await flushPromises() // import dynamique de Leaflet + montage carte
|
||||
|
||||
expect(leafletState.dragendHandler).not.toBeNull()
|
||||
|
||||
// L'utilisateur depose le pin ailleurs (lieu-dit mal geocode).
|
||||
leafletState.markerPosition = { lat: 48.1234567, lng: -1.6543217 }
|
||||
leafletState.dragendHandler?.()
|
||||
|
||||
const emitted = wrapper.emitted('update:coords')
|
||||
expect(emitted).toHaveLength(1)
|
||||
expect(emitted?.[0]?.[0]).toEqual({
|
||||
latitude: '48.1234567',
|
||||
longitude: '-1.6543217',
|
||||
geoManual: true,
|
||||
})
|
||||
})
|
||||
|
||||
it('affiche le badge « pin manuel » quand geoManual est vrai', () => {
|
||||
const wrapper = mountPin({ latitude: '46.58', longitude: '0.34', geoManual: true })
|
||||
|
||||
expect(wrapper.find('[data-testid="geo-badge-manual"]').exists()).toBe(true)
|
||||
expect(wrapper.find('[data-testid="geo-badge-missing"]').exists()).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('AddressGeoPin — re-geocodage depuis l\'adresse', () => {
|
||||
it('emet la position BAN avec geoManual=false (le back refera autorite au save)', async () => {
|
||||
geocodeMock.mockResolvedValueOnce({ latitude: '46.5802596', longitude: '0.3404333' })
|
||||
const wrapper = mountPin()
|
||||
|
||||
await wrapper.find('[data-testid="geo-regeocode"]').trigger('click')
|
||||
await flushPromises()
|
||||
|
||||
expect(geocodeMock).toHaveBeenCalledWith('1 rue du Test, 86100 Châtellerault')
|
||||
expect(wrapper.emitted('update:coords')?.[0]?.[0]).toEqual({
|
||||
latitude: '46.5802596',
|
||||
longitude: '0.3404333',
|
||||
geoManual: false,
|
||||
})
|
||||
})
|
||||
|
||||
it('signale l\'echec sans emettre quand la BAN ne trouve rien', async () => {
|
||||
geocodeMock.mockResolvedValueOnce(null)
|
||||
const wrapper = mountPin()
|
||||
|
||||
await wrapper.find('[data-testid="geo-regeocode"]').trigger('click')
|
||||
await flushPromises()
|
||||
|
||||
expect(wrapper.emitted('update:coords')).toBeUndefined()
|
||||
expect(wrapper.find('[data-testid="geo-regeocode-failed"]').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('masque le bouton en lecture seule', () => {
|
||||
const wrapper = mountPin({ readonly: true })
|
||||
|
||||
expect(wrapper.find('[data-testid="geo-regeocode"]').exists()).toBe(false)
|
||||
})
|
||||
})
|
||||
@@ -65,6 +65,8 @@ function mountBlock(street: string | null) {
|
||||
MalioSelectCheckbox: true,
|
||||
MalioInputText: true,
|
||||
MalioInputAutocomplete: MalioInputAutocompleteStub,
|
||||
// Pin geographique (M6.1) : teste dans AddressGeoPin.spec.ts.
|
||||
AddressGeoPin: true,
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -130,6 +132,8 @@ describe('ClientAddressBlock — mapping erreur par champ (ERP-101)', () => {
|
||||
MalioSelectCheckbox: true,
|
||||
MalioInputAutocomplete: MalioInputAutocompleteStub,
|
||||
MalioInputText: MalioInputTextProbe,
|
||||
// Pin geographique (M6.1) : teste dans AddressGeoPin.spec.ts.
|
||||
AddressGeoPin: true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
@@ -51,6 +51,12 @@ export interface AddressFormDraft {
|
||||
billingEmailSecondary: string | null
|
||||
/** Drapeau UI : 2e champ email revele (comme hasSecondaryPhone). */
|
||||
hasSecondaryBillingEmail: boolean
|
||||
/** Latitude WGS84 (chaine decimale NUMERIC(10,7)), null si non geolocalisee (M6.1). */
|
||||
latitude: string | null
|
||||
/** Longitude WGS84 (chaine decimale NUMERIC(10,7)), null si non geolocalisee (M6.1). */
|
||||
longitude: string | null
|
||||
/** RG-6.08 : pin corrige a la main — le geocodage auto ne reecrit plus. */
|
||||
geoManual: boolean
|
||||
}
|
||||
|
||||
/** Un RIB du client (onglet Comptabilite). */
|
||||
@@ -96,6 +102,9 @@ export function emptyAddress(): AddressFormDraft {
|
||||
billingEmail: null,
|
||||
billingEmailSecondary: null,
|
||||
hasSecondaryBillingEmail: false,
|
||||
latitude: null,
|
||||
longitude: null,
|
||||
geoManual: false,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -55,6 +55,12 @@ export interface SupplierAddressFormDraft {
|
||||
bennes: string | null
|
||||
/** Prestation de triage (specifique fournisseur, portee par l'adresse — RG). */
|
||||
triageProvider: boolean
|
||||
/** Latitude WGS84 (chaine decimale NUMERIC(10,7)), null si non geolocalisee (M6.1). */
|
||||
latitude: string | null
|
||||
/** Longitude WGS84 (chaine decimale NUMERIC(10,7)), null si non geolocalisee (M6.1). */
|
||||
longitude: string | null
|
||||
/** RG-6.08 : pin corrige a la main — le geocodage auto ne reecrit plus. */
|
||||
geoManual: boolean
|
||||
}
|
||||
|
||||
/** Un RIB du fournisseur (onglet Comptabilite). */
|
||||
@@ -95,6 +101,9 @@ export function emptyAddress(): SupplierAddressFormDraft {
|
||||
contactIris: [],
|
||||
bennes: '0',
|
||||
triageProvider: false,
|
||||
latitude: null,
|
||||
longitude: null,
|
||||
geoManual: false,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -69,6 +69,10 @@ export interface AddressRead extends HydraRef {
|
||||
isBilling?: boolean
|
||||
isBroker?: boolean
|
||||
isDistributor?: boolean
|
||||
/** Geolocalisation (M6.1) : chaines decimales NUMERIC(10,7) cote API. */
|
||||
latitude?: string | null
|
||||
longitude?: string | null
|
||||
geoManual?: boolean
|
||||
sites?: SiteRead[]
|
||||
categories?: CategoryRead[]
|
||||
// L'embed M2M des contacts d'adresse peut etre un objet (partiel) ou un IRI nu.
|
||||
@@ -225,6 +229,9 @@ export function mapAddressToDraft(address: AddressRead): AddressFormDraft {
|
||||
billingEmail: address.billingEmail ?? null,
|
||||
billingEmailSecondary: address.billingEmailSecondary ?? null,
|
||||
hasSecondaryBillingEmail: (address.billingEmailSecondary ?? null) !== null && address.billingEmailSecondary !== '',
|
||||
latitude: address.latitude ?? null,
|
||||
longitude: address.longitude ?? null,
|
||||
geoManual: address.geoManual === true,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -254,6 +254,11 @@ export function buildAddressPayload(
|
||||
contacts: address.contactIris,
|
||||
billingEmail: isBillingEmailRequired ? (address.billingEmail || null) : null,
|
||||
billingEmailSecondary: isBillingEmailRequired && address.hasSecondaryBillingEmail ? (address.billingEmailSecondary || null) : null,
|
||||
// Geolocalisation (M6.1) : pin manuel persiste avec geoManual=true ;
|
||||
// geoManual=false laisse le back regeocoder depuis l'adresse postale.
|
||||
latitude: address.latitude || null,
|
||||
longitude: address.longitude || null,
|
||||
geoManual: address.geoManual,
|
||||
}, ADDRESS_REQUIRED_NON_NULLABLE_KEYS, options)
|
||||
}
|
||||
|
||||
|
||||
@@ -76,6 +76,10 @@ export interface AddressRead extends HydraRef {
|
||||
streetComplement?: string | null
|
||||
bennes?: number | null
|
||||
triageProvider?: boolean
|
||||
/** Geolocalisation (M6.1) : chaines decimales NUMERIC(10,7) cote API. */
|
||||
latitude?: string | null
|
||||
longitude?: string | null
|
||||
geoManual?: boolean
|
||||
sites?: SiteRead[]
|
||||
categories?: CategoryRead[]
|
||||
// L'embed M2M des contacts d'adresse peut etre un objet (partiel) ou un IRI nu.
|
||||
@@ -200,6 +204,9 @@ export function mapAddressToDraft(address: AddressRead): SupplierAddressFormDraf
|
||||
contactIris: (address.contacts ?? []).map(c => (typeof c === 'string' ? c : c['@id'])),
|
||||
bennes: address.bennes != null ? String(address.bennes) : '0',
|
||||
triageProvider: address.triageProvider ?? false,
|
||||
latitude: address.latitude ?? null,
|
||||
longitude: address.longitude ?? null,
|
||||
geoManual: address.geoManual === true,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -237,6 +237,11 @@ export function buildAddressPayload(address: SupplierAddressFormDraft, options:
|
||||
contacts: address.contactIris,
|
||||
bennes: address.bennes !== null && address.bennes !== '' ? Number(address.bennes) : null,
|
||||
triageProvider: address.triageProvider,
|
||||
// Geolocalisation (M6.1) : pin manuel persiste avec geoManual=true ;
|
||||
// geoManual=false laisse le back regeocoder depuis l'adresse postale.
|
||||
latitude: address.latitude || null,
|
||||
longitude: address.longitude || null,
|
||||
geoManual: address.geoManual,
|
||||
}, ADDRESS_REQUIRED_NON_NULLABLE_KEYS, options)
|
||||
}
|
||||
|
||||
|
||||
Generated
+56
-22
@@ -12,6 +12,8 @@
|
||||
"@nuxtjs/i18n": "^10.2.3",
|
||||
"@nuxtjs/tailwindcss": "^6.14.0",
|
||||
"@pinia/nuxt": "^0.11.3",
|
||||
"@types/leaflet": "^1.9.21",
|
||||
"leaflet": "^1.9.4",
|
||||
"nuxt": "^4.3.1",
|
||||
"nuxt-toast": "^1.4.0",
|
||||
"pinia": "^3.0.4",
|
||||
@@ -85,6 +87,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz",
|
||||
"integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.29.0",
|
||||
"@babel/generator": "^7.29.0",
|
||||
@@ -582,27 +585,6 @@
|
||||
"integrity": "sha512-/B8YJGPzaYq1NbsQmwgP8EZqg40NpTw4ZB3suuI0TplbxKHeK94jeaawLmVhCv+YwUnOpiWEz9U6SeThku/8JQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@emnapi/core": {
|
||||
"version": "1.11.0",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.0.tgz",
|
||||
"integrity": "sha512-l9Oo58x0HOP5znGzVhYW9U3e5wVuA4LAZU2AGezTmkhO1CgQRFDhDg4nneHsu/t3WniXg9QrG2nIXL/ZS8ln8Q==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@emnapi/wasi-threads": "1.2.2",
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@emnapi/runtime": {
|
||||
"version": "1.11.0",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.0.tgz",
|
||||
"integrity": "sha512-55coeOFKHv1ywEcUXJtWU5f+Jr/W5tZDvZig8DLKSwUN1JpROQ4rk/SNOQiFWmaR/VKF4zuFyW1B8JduOSv6Pg==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@emnapi/wasi-threads": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz",
|
||||
@@ -1303,6 +1285,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.6.tgz",
|
||||
"integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@floating-ui/core": "^1.7.5",
|
||||
"@floating-ui/utils": "^0.2.11"
|
||||
@@ -2221,6 +2204,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@nuxt/kit/-/kit-4.4.2.tgz",
|
||||
"integrity": "sha512-5+IPRNX2CjkBhuWUwz0hBuLqiaJPRoKzQ+SvcdrQDbAyE+VDeFt74VpSFr5/R0ujrK4b+XnSHUJWdS72w6hsog==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"c12": "^3.3.3",
|
||||
"consola": "^3.4.2",
|
||||
@@ -2323,6 +2307,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@nuxt/schema/-/schema-4.4.2.tgz",
|
||||
"integrity": "sha512-/q6C7Qhiricgi+PKR7ovBnJlKTL0memCbA1CzRT+itCW/oeYzUfeMdQ35mGntlBoyRPNrMXbzuSUhfDbSCU57w==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@vue/shared": "^3.5.30",
|
||||
"defu": "^6.1.4",
|
||||
@@ -4638,6 +4623,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/core/-/core-3.23.2.tgz",
|
||||
"integrity": "sha512-yjv2N7gaQMbIVfsSZHBMscLoybgetcTraXsSMrELAerl/jfRipg5S1dBXMFvgRy8Kh48+TGoH+5nqshxdOEGoQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
@@ -4886,6 +4872,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-list/-/extension-list-3.23.2.tgz",
|
||||
"integrity": "sha512-tRbbjpOPrY4ApIHtn3ctnKIhkkioewMsZa5gJzqVB47LJFNyzLXLo/aID4sJRKTIMi1wd1fA9TiBKPe6KqczPA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
@@ -4991,6 +4978,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-text-style/-/extension-text-style-3.23.2.tgz",
|
||||
"integrity": "sha512-K2o1gMwn09nrd5ewftSy08U6LMC1cW3Cmml5+vHT9P/VeMtYwkbNg+9Mt1uFh7VfAZmlkj8d3u7RYqfl8xMVJA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
@@ -5017,6 +5005,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extensions/-/extensions-3.23.2.tgz",
|
||||
"integrity": "sha512-kRHQ3nSbAfkFdxj9FtDdr4hpREndGgWFA6ZEAwlLeGUxf8QYTpuF9zb2yxdBPBlTc5+JsbPcskNt+u1PazGKYw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
@@ -5031,6 +5020,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-3.23.2.tgz",
|
||||
"integrity": "sha512-1kvsBqGNu2ZJ0P/lkxN0pAMqSyUcpkMIzE4xwGUIyAiD0pZV6dr+OCMwGWOTLllSyrn91xI5K7OLk3pYeCPKqA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"prosemirror-changeset": "^2.3.0",
|
||||
"prosemirror-commands": "^1.6.2",
|
||||
@@ -5140,12 +5130,27 @@
|
||||
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/geojson": {
|
||||
"version": "7946.0.16",
|
||||
"resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz",
|
||||
"integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/json-schema": {
|
||||
"version": "7.0.15",
|
||||
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
|
||||
"integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/leaflet": {
|
||||
"version": "1.9.21",
|
||||
"resolved": "https://registry.npmjs.org/@types/leaflet/-/leaflet-1.9.21.tgz",
|
||||
"integrity": "sha512-TbAd9DaPGSnzp6QvtYngntMZgcRk+igFELwR2N99XZn7RXUdKgsXMR+28bUO0rPsWp8MIu/f47luLIQuSLYv/w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/geojson": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/linkify-it": {
|
||||
"version": "3.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-3.0.5.tgz",
|
||||
@@ -5174,6 +5179,7 @@
|
||||
"integrity": "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"undici-types": "~7.19.0"
|
||||
}
|
||||
@@ -5236,6 +5242,7 @@
|
||||
"integrity": "sha512-/Zb/xaIDfxeJnvishjGdcR4jmr7S+bda8PKNhRGdljDM+elXhlvN0FyPSsMnLmJUrVG9aPO6dof80wjMawsASg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.58.2",
|
||||
"@typescript-eslint/types": "8.58.2",
|
||||
@@ -6015,6 +6022,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.32.tgz",
|
||||
"integrity": "sha512-8UYUYo71cP/0YHMO814TRZlPuUUw3oifHuMR7Wp9SNoRSrxRQnhMLNlCeaODNn6kNTJsjFoQ/kqIj4qGvya4Xg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/parser": "^7.29.2",
|
||||
"@vue/compiler-core": "3.5.32",
|
||||
@@ -6258,6 +6266,7 @@
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz",
|
||||
"integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
@@ -6645,6 +6654,7 @@
|
||||
"resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.8.2.tgz",
|
||||
"integrity": "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"peerDependencies": {
|
||||
"bare-abort-controller": "*"
|
||||
},
|
||||
@@ -6842,6 +6852,7 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"baseline-browser-mapping": "^2.10.12",
|
||||
"caniuse-lite": "^1.0.30001782",
|
||||
@@ -6956,6 +6967,7 @@
|
||||
"resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz",
|
||||
"integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
@@ -7150,7 +7162,8 @@
|
||||
"version": "0.2.2",
|
||||
"resolved": "https://registry.npmjs.org/citty/-/citty-0.2.2.tgz",
|
||||
"integrity": "sha512-+6vJA3L98yv+IdfKGZHBNiGW5KHn22e/JwID0Strsz8h4S/csAu/OuICwxrg44k5MRiZHWIo8XXuJgQTriRP4w==",
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/clean-regexp": {
|
||||
"version": "1.0.0",
|
||||
@@ -8203,6 +8216,7 @@
|
||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.4.tgz",
|
||||
"integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.8.0",
|
||||
"@eslint-community/regexpp": "^4.12.1",
|
||||
@@ -9361,6 +9375,7 @@
|
||||
"integrity": "sha512-GZZ9mKe8r646NUAf/zemnGbjYh4Bt8/MqASJY+pSm5ZDtc3YQox+4gsLI7yi1hba6o+eCsGxpHn5+iEVn31/FQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/node": ">=20.0.0",
|
||||
"@types/whatwg-mimetype": "^3.0.2",
|
||||
@@ -10532,6 +10547,12 @@
|
||||
"safe-buffer": "~5.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/leaflet": {
|
||||
"version": "1.9.4",
|
||||
"resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz",
|
||||
"integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==",
|
||||
"license": "BSD-2-Clause"
|
||||
},
|
||||
"node_modules/levn": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
|
||||
@@ -11807,6 +11828,7 @@
|
||||
"resolved": "https://registry.npmjs.org/nuxt/-/nuxt-4.4.2.tgz",
|
||||
"integrity": "sha512-iWVFpr/YEqVU/CenqIHMnIkvb2HE/9f+q8oxZ+pj2et+60NljGRClCgnmbvGPdmNFE0F1bEhoBCYfqbDOCim3Q==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@dxup/nuxt": "^0.4.0",
|
||||
"@nuxt/cli": "^3.34.0",
|
||||
@@ -12865,6 +12887,7 @@
|
||||
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
|
||||
"integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"deep-is": "^0.1.3",
|
||||
"fast-levenshtein": "^2.0.6",
|
||||
@@ -12922,6 +12945,7 @@
|
||||
"resolved": "https://registry.npmjs.org/oxc-parser/-/oxc-parser-0.112.0.tgz",
|
||||
"integrity": "sha512-7rQ3QdJwobMQLMZwQaPuPYMEF2fDRZwf51lZ//V+bA37nejjKW5ifMHbbCwvA889Y4RLhT+/wLJpPRhAoBaZYw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@oxc-project/types": "^0.112.0"
|
||||
},
|
||||
@@ -13188,6 +13212,7 @@
|
||||
"resolved": "https://registry.npmjs.org/pinia/-/pinia-3.0.4.tgz",
|
||||
"integrity": "sha512-l7pqLUFTI/+ESXn6k3nu30ZIzW5E2WZF/LaHJEpoq6ElcLD+wduZoB2kBN19du6K/4FDpPMazY2wJr+IndBtQw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@vue/devtools-api": "^7.7.7"
|
||||
},
|
||||
@@ -13313,6 +13338,7 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.11",
|
||||
"picocolors": "^1.1.1",
|
||||
@@ -13856,6 +13882,7 @@
|
||||
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz",
|
||||
"integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"cssesc": "^3.0.0",
|
||||
"util-deprecate": "^1.0.2"
|
||||
@@ -14646,6 +14673,7 @@
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.1.tgz",
|
||||
"integrity": "sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/estree": "1.0.8"
|
||||
},
|
||||
@@ -15549,6 +15577,7 @@
|
||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz",
|
||||
"integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@alloc/quick-lru": "^5.2.0",
|
||||
"arg": "^5.0.2",
|
||||
@@ -16228,6 +16257,7 @@
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"napi-postinstall": "^0.3.0"
|
||||
},
|
||||
@@ -16494,6 +16524,7 @@
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.3.2.tgz",
|
||||
"integrity": "sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.27.0",
|
||||
"fdir": "^6.5.0",
|
||||
@@ -17412,6 +17443,7 @@
|
||||
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.32.tgz",
|
||||
"integrity": "sha512-vM4z4Q9tTafVfMAK7IVzmxg34rSzTFMyIe0UUEijUCkn9+23lj0WRfA83dg7eQZIUlgOSGrkViIaCfqSAUXsMw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@vue/compiler-dom": "3.5.32",
|
||||
"@vue/compiler-sfc": "3.5.32",
|
||||
@@ -17456,6 +17488,7 @@
|
||||
"integrity": "sha512-Vxi9pJdbN3ZnVGLODVtZ7y4Y2kzAAE2Cm0CZ3ZDRvydVYxZ6VrnBhLikBsRS+dpwj4Jv4UCv21PTEwF5rQ9WXg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"debug": "^4.4.0",
|
||||
"eslint-scope": "^8.2.0 || ^9.0.0",
|
||||
@@ -17492,6 +17525,7 @@
|
||||
"resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-11.3.1.tgz",
|
||||
"integrity": "sha512-azq8fhVnCwJAw0iXW7i44h9P+Bj+snNuevBAaJ9bxn0I3YVsRU3deVFPNnTfZ2uxVJefGp83JUmL68ddCPw5Pw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@intlify/core-base": "11.3.1",
|
||||
"@intlify/devtools-types": "11.3.1",
|
||||
|
||||
@@ -22,6 +22,8 @@
|
||||
"@nuxtjs/i18n": "^10.2.3",
|
||||
"@nuxtjs/tailwindcss": "^6.14.0",
|
||||
"@pinia/nuxt": "^0.11.3",
|
||||
"@types/leaflet": "^1.9.21",
|
||||
"leaflet": "^1.9.4",
|
||||
"nuxt": "^4.3.1",
|
||||
"nuxt-toast": "^1.4.0",
|
||||
"pinia": "^3.0.4",
|
||||
|
||||
@@ -31,9 +31,21 @@ export interface AddressSuggestion {
|
||||
city: string
|
||||
}
|
||||
|
||||
/** Coordonnees WGS84 d'une adresse geocodee (chaines decimales, format API). */
|
||||
export interface GeocodedCoordinates {
|
||||
latitude: string
|
||||
longitude: string
|
||||
}
|
||||
|
||||
export interface AddressAutocomplete {
|
||||
searchCity(postalCode: string): Promise<CitySuggestion[]>
|
||||
searchAddress(query: string, postalCode?: string): Promise<AddressSuggestion[]>
|
||||
/**
|
||||
* Geocode une adresse complete en coordonnees (M6.1) — previsualisation du
|
||||
* pin cote front uniquement : la valeur persistee reste celle du geocodage
|
||||
* serveur (BanGeocoder) au save. `null` si la BAN ne trouve rien.
|
||||
*/
|
||||
geocode(query: string): Promise<GeocodedCoordinates | null>
|
||||
}
|
||||
|
||||
/** Erreur signalant que le service d'autocompletion BAN n'est pas disponible. */
|
||||
@@ -57,7 +69,11 @@ interface BanFeatureProperties {
|
||||
|
||||
/** Reponse GeoJSON FeatureCollection de la BAN. */
|
||||
interface BanResponse {
|
||||
features?: { properties?: BanFeatureProperties }[]
|
||||
features?: {
|
||||
properties?: BanFeatureProperties
|
||||
/** GeoJSON : coordinates = [longitude, latitude]. */
|
||||
geometry?: { coordinates?: [number, number] }
|
||||
}[]
|
||||
}
|
||||
|
||||
export function useAddressAutocomplete(): AddressAutocomplete {
|
||||
@@ -113,5 +129,32 @@ export function useAddressAutocomplete(): AddressAutocomplete {
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
async geocode(query: string): Promise<GeocodedCoordinates | null> {
|
||||
if (query.trim().length < 3) {
|
||||
return null
|
||||
}
|
||||
|
||||
let res: BanResponse
|
||||
try {
|
||||
res = await httpExternal<BanResponse>(BAN_SEARCH_URL, {
|
||||
query: { q: query, limit: '1' },
|
||||
})
|
||||
}
|
||||
catch {
|
||||
throw new AddressAutocompleteUnavailableError()
|
||||
}
|
||||
|
||||
const coordinates = res.features?.[0]?.geometry?.coordinates
|
||||
if (!coordinates || coordinates.length < 2) {
|
||||
return null
|
||||
}
|
||||
|
||||
// GeoJSON = [longitude, latitude] ; 7 decimales = format NUMERIC(10,7).
|
||||
return {
|
||||
latitude: coordinates[1].toFixed(7),
|
||||
longitude: coordinates[0].toFixed(7),
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user