feat: recherche entreprise SIRET/SIREN via API data.gouv.fr (proxy PHP)
Backend (ClientsController::entrepriseSearch) : - Route GET /admin/clients/entreprise-search?q=... - Proxy PHP vers https://recherche-entreprises.api.gouv.fr/search (pas d'appel API direct depuis le JS) - Retourne JSON avec results[], total_results - Gestion erreur avec 502 si API indisponible Frontend (assets/modules/entreprise-search.js) : - Module JS séparé, pas de script inline (CSP compatible) - Modal glassmorphism avec champ recherche et liste résultats - Chaque résultat affiche : nom, SIREN, SIRET, adresse, dirigeant, statut - Au clic sur un résultat, auto-remplissage du formulaire : raisonSociale, siret, numTva (calcul clé TVA), address, zipCode, city + firstName/lastName du dirigeant si les champs sont vides - Fermeture modal via overlay, bouton X, ou Escape Template : - Bouton "Rechercher SIRET / SIREN" à côté du bouton Retour - Modal HTML avec header glass-dark, champ recherche, zone résultats scrollable Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
113
assets/modules/entreprise-search.js
Normal file
113
assets/modules/entreprise-search.js
Normal file
@@ -0,0 +1,113 @@
|
||||
/**
|
||||
* Recherche entreprise via API data.gouv.fr
|
||||
* Auto-remplissage du formulaire de création client
|
||||
*/
|
||||
const API_URL = '/admin/clients/entreprise-search'
|
||||
|
||||
const computeTva = (siren) => {
|
||||
if (!siren) return ''
|
||||
const key = (12 + 3 * (parseInt(siren, 10) % 97)) % 97
|
||||
return 'FR' + String(key).padStart(2, '0') + siren
|
||||
}
|
||||
|
||||
const capitalize = (s) => s ? s.charAt(0).toUpperCase() + s.slice(1).toLowerCase() : ''
|
||||
|
||||
const fillField = (id, value) => {
|
||||
const el = document.getElementById(id)
|
||||
if (el) el.value = value
|
||||
}
|
||||
|
||||
const fillFieldIfEmpty = (id, value) => {
|
||||
const el = document.getElementById(id)
|
||||
if (el && !el.value && value) el.value = value
|
||||
}
|
||||
|
||||
const renderResult = (e, onSelect) => {
|
||||
const s = e.siege || {}
|
||||
const d = (e.dirigeants && e.dirigeants[0]) || {}
|
||||
const actif = e.etat_administratif === 'A'
|
||||
const addr = [s.numero_voie, s.type_voie, s.libelle_voie].filter(Boolean).join(' ')
|
||||
|
||||
const div = document.createElement('div')
|
||||
div.className = 'glass p-4 cursor-pointer hover:bg-white/80 transition-all'
|
||||
div.innerHTML = `
|
||||
<div class="flex items-start justify-between gap-3">
|
||||
<div class="min-w-0">
|
||||
<div class="font-bold text-sm">${e.nom_complet || e.nom_raison_sociale || 'N/A'}</div>
|
||||
<div class="text-xs text-gray-500 mt-1">
|
||||
SIREN <span class="font-mono font-bold">${e.siren || '?'}</span>
|
||||
${s.siret ? ' — SIRET <span class="font-mono font-bold">' + s.siret + '</span>' : ''}
|
||||
</div>
|
||||
<div class="text-xs text-gray-400 mt-1">${s.geo_adresse || s.adresse || ''}</div>
|
||||
${d.nom ? '<div class="text-xs text-gray-400 mt-1">Dirigeant : ' + (d.prenoms || '') + ' ' + d.nom + '</div>' : ''}
|
||||
</div>
|
||||
<span class="shrink-0 px-2 py-1 text-[10px] font-bold uppercase ${actif ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'}" style="border-radius: 6px;">
|
||||
${actif ? 'Actif' : 'Ferme'}
|
||||
</span>
|
||||
</div>`
|
||||
|
||||
div.addEventListener('click', () => {
|
||||
fillField('raisonSociale', e.nom_raison_sociale || e.nom_complet || '')
|
||||
fillField('siret', s.siret || '')
|
||||
fillField('numTva', computeTva(e.siren))
|
||||
fillField('address', addr)
|
||||
fillField('zipCode', s.code_postal || '')
|
||||
fillField('city', s.libelle_commune || '')
|
||||
|
||||
const prenom = (d.prenoms || '').split(' ')[0]
|
||||
fillFieldIfEmpty('firstName', capitalize(prenom))
|
||||
fillFieldIfEmpty('lastName', capitalize(d.nom))
|
||||
|
||||
onSelect()
|
||||
})
|
||||
|
||||
return div
|
||||
}
|
||||
|
||||
export function initEntrepriseSearch() {
|
||||
const modal = document.getElementById('modal-entreprise')
|
||||
const overlay = document.getElementById('modal-overlay')
|
||||
const closeBtn = document.getElementById('modal-close')
|
||||
const openBtn = document.getElementById('btn-search-entreprise')
|
||||
const input = document.getElementById('search-entreprise-input')
|
||||
const searchBtn = document.getElementById('search-entreprise-btn')
|
||||
const resultsEl = document.getElementById('search-entreprise-results')
|
||||
const statusEl = document.getElementById('search-entreprise-status')
|
||||
|
||||
if (!modal || !openBtn) return
|
||||
|
||||
const openModal = () => { modal.classList.remove('hidden'); input.focus() }
|
||||
const closeModal = () => modal.classList.add('hidden')
|
||||
|
||||
openBtn.addEventListener('click', openModal)
|
||||
closeBtn.addEventListener('click', closeModal)
|
||||
overlay.addEventListener('click', closeModal)
|
||||
document.addEventListener('keydown', (e) => { if (e.key === 'Escape') closeModal() })
|
||||
|
||||
const doSearch = async () => {
|
||||
const q = input.value.trim()
|
||||
if (q.length < 2) return
|
||||
|
||||
resultsEl.innerHTML = ''
|
||||
statusEl.textContent = 'Recherche en cours...'
|
||||
statusEl.classList.remove('hidden')
|
||||
|
||||
try {
|
||||
const resp = await fetch(API_URL + '?q=' + encodeURIComponent(q) + '&page=1&per_page=10')
|
||||
const data = await resp.json()
|
||||
|
||||
if (!data.results || data.results.length === 0) {
|
||||
statusEl.textContent = 'Aucun resultat trouve.'
|
||||
return
|
||||
}
|
||||
|
||||
statusEl.textContent = data.total_results + ' resultat(s) - cliquez pour selectionner'
|
||||
data.results.forEach(e => resultsEl.appendChild(renderResult(e, closeModal)))
|
||||
} catch (err) {
|
||||
statusEl.textContent = 'Erreur : ' + err.message
|
||||
}
|
||||
}
|
||||
|
||||
searchBtn.addEventListener('click', doSearch)
|
||||
input.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); doSearch() } })
|
||||
}
|
||||
Reference in New Issue
Block a user