feat(directory) : type prestataire, validateurs front, autocomplete adresse BAN
- 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.
This commit is contained in:
@@ -0,0 +1,113 @@
|
||||
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 ?? '',
|
||||
}
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user