5764d8f472
- Prestataire : entité/repo + ressource API Platform (RBAC directory.providers.*), ownership prestataire sur contacts/adresses/comptes-rendus (CHECK XOR à 3), DTO/service/drawer/fiche détail + onglet dédié dans le répertoire. - Prospect : société uniquement (suppression du champ name, company requis) ; migration de backfill, conversion prospect→client et MCP adaptés. - Champ site web sur client/prospect/prestataire (entités, DTO, onglet Information, MCP). - Validateurs front email / téléphone FR (0549200910) / URL sur Information et Contacts, enregistrement bloqué tant qu'un champ est invalide. - Autocomplete adresse branché sur la Base Adresse Nationale (api-adresse.data.gouv.fr) avec mode dégradé en saisie libre. - Administration : retrait de l'onglet Clients.
114 lines
4.0 KiB
TypeScript
114 lines
4.0 KiB
TypeScript
import { httpExternal } from '~/utils/httpExternal'
|
|
|
|
// Autocomplétion d'adresse branchée sur la Base Adresse Nationale (BAN),
|
|
// `api-adresse.data.gouv.fr` — service public français, gratuit, CORS ouvert.
|
|
//
|
|
// Appel HTTP DIRECT depuis le front (pas de proxy back) : la BAN est un domaine
|
|
// externe, sans cookie de session ni enveloppe Hydra → on passe par
|
|
// `httpExternal` et NON `useApi()`.
|
|
//
|
|
// Contrat :
|
|
// searchCity(postalCode) -> liste { city, postalCode }
|
|
// searchAddress(query, cp?) -> liste { label, street, postalCode, city }
|
|
// En cas d'erreur/timeout, la méthode THROW une
|
|
// AddressAutocompleteUnavailableError. Le composant consommateur catch,
|
|
// avertit l'utilisateur et bascule en saisie libre.
|
|
|
|
/** URL de l'endpoint de recherche BAN. */
|
|
const BAN_SEARCH_URL = 'https://api-adresse.data.gouv.fr/search/'
|
|
|
|
/** Une suggestion de ville renvoyée à partir d'un code postal. */
|
|
export interface CitySuggestion {
|
|
city: string
|
|
postalCode: string
|
|
}
|
|
|
|
/** Une suggestion d'adresse complète (saisie assistée du champ « Rue »). */
|
|
export interface AddressSuggestion {
|
|
label: string
|
|
street: string
|
|
postalCode: string
|
|
city: string
|
|
}
|
|
|
|
export interface AddressAutocomplete {
|
|
searchCity(postalCode: string): Promise<CitySuggestion[]>
|
|
searchAddress(query: string, postalCode?: string): Promise<AddressSuggestion[]>
|
|
}
|
|
|
|
/** Erreur signalant que le service d'autocomplétion BAN n'est pas disponible. */
|
|
export class AddressAutocompleteUnavailableError extends Error {
|
|
constructor() {
|
|
super('Address autocomplete (BAN) is not available.')
|
|
this.name = 'AddressAutocompleteUnavailableError'
|
|
}
|
|
}
|
|
|
|
/** Propriétés d'une « feature » GeoJSON renvoyée par la BAN (champs utilisés). */
|
|
interface BanFeatureProperties {
|
|
label?: string
|
|
name?: string
|
|
street?: string
|
|
postcode?: string
|
|
city?: string
|
|
}
|
|
|
|
/** Réponse GeoJSON FeatureCollection de la BAN. */
|
|
interface BanResponse {
|
|
features?: { properties?: BanFeatureProperties }[]
|
|
}
|
|
|
|
export function useAddressAutocomplete(): AddressAutocomplete {
|
|
return {
|
|
async searchCity(postalCode: string): Promise<CitySuggestion[]> {
|
|
let res: BanResponse
|
|
try {
|
|
res = await httpExternal<BanResponse>(BAN_SEARCH_URL, {
|
|
query: { q: postalCode, type: 'municipality' },
|
|
})
|
|
}
|
|
catch {
|
|
throw new AddressAutocompleteUnavailableError()
|
|
}
|
|
|
|
return (res.features ?? []).map((feature) => {
|
|
const props = feature.properties ?? {}
|
|
return {
|
|
city: props.city ?? props.name ?? '',
|
|
postalCode: props.postcode ?? '',
|
|
}
|
|
})
|
|
},
|
|
|
|
async searchAddress(query: string, postalCode?: string): Promise<AddressSuggestion[]> {
|
|
// Pas de `type=housenumber` ici : sans filtre, la BAN classe rues +
|
|
// numéros par pertinence (comportement d'autocomplétion attendu).
|
|
// On n'ajoute `postcode` que s'il est fourni (sinon recherche large).
|
|
const banQuery: Record<string, string> = { q: query }
|
|
if (postalCode) {
|
|
banQuery.postcode = postalCode
|
|
}
|
|
|
|
let res: BanResponse
|
|
try {
|
|
res = await httpExternal<BanResponse>(BAN_SEARCH_URL, { query: banQuery })
|
|
}
|
|
catch {
|
|
throw new AddressAutocompleteUnavailableError()
|
|
}
|
|
|
|
return (res.features ?? []).map((feature) => {
|
|
const props = feature.properties ?? {}
|
|
return {
|
|
label: props.label ?? '',
|
|
// `name` porte la ligne d'adresse complète (numéro + voie) ;
|
|
// `street` ne contient que la voie. On privilégie `name`.
|
|
street: props.name ?? props.street ?? '',
|
|
postalCode: props.postcode ?? '',
|
|
city: props.city ?? '',
|
|
}
|
|
})
|
|
},
|
|
}
|
|
}
|