feat: barre de recherche globale dans la navbar admin

Navbar admin :
- Barre de recherche persistante en haut de toutes les pages admin
- Recherche dans tous les index Meilisearch simultanément :
  clients (5), NDD (5), sites (5), contacts (5), revendeurs (3)
- Résultats en dropdown glassmorphism avec icône par type
- Clic sur un résultat → page + tab correspondant :
  Client → /admin/clients/{id}
  NDD → /admin/clients/{id}?tab=ndd
  Site → /admin/clients/{id}?tab=sites
  Contact → /admin/clients/{id}?tab=contacts
  Revendeur → /admin/revendeurs/{id}/edit

DashboardController::globalSearch :
- Route GET /admin/global-search?q=...
- Agrège les résultats de 5 index Meilisearch
- Retourne [{type, label, sub, url}]

app.js :
- Debounce 250ms, min 2 chars
- Badges type (Client, NDD, Site, Contact, Revendeur)
- Fermeture Escape / clic extérieur

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Serreau Jovann
2026-04-04 21:29:36 +02:00
parent f68712bd02
commit c849a31ea1
3 changed files with 111 additions and 4 deletions

View File

@@ -213,4 +213,64 @@ document.addEventListener('DOMContentLoaded', () => {
// Recherche entreprise (page creation client)
initEntrepriseSearch();
// Recherche globale (navbar admin)
const globalInput = document.getElementById('global-search');
const globalResults = document.getElementById('global-search-results');
if (globalInput && globalResults) {
const typeIcons = {
client: '&#128100;',
ndd: '&#127760;',
site: '&#128187;',
contact: '&#128101;',
revendeur: '&#127970;',
};
const typeLabels = {
client: 'Client',
ndd: 'NDD',
site: 'Site',
contact: 'Contact',
revendeur: 'Revendeur',
};
let globalDebounce;
globalInput.addEventListener('input', () => {
clearTimeout(globalDebounce);
const q = globalInput.value.trim();
if (q.length < 2) {
globalResults.classList.add('hidden');
globalResults.innerHTML = '';
return;
}
globalDebounce = setTimeout(async () => {
const resp = await fetch('/admin/global-search?q=' + encodeURIComponent(q));
const hits = await resp.json();
if (hits.length === 0) {
globalResults.innerHTML = '<div class="px-4 py-3 text-xs text-gray-400">Aucun resultat.</div>';
} else {
globalResults.innerHTML = hits.map(h =>
`<a href="${h.url}" class="flex items-center gap-3 px-4 py-2 hover:bg-white/80 border-b border-white/10 transition-colors">
<span class="text-sm">${typeIcons[h.type] || ''}</span>
<div class="min-w-0 flex-1">
<span class="text-xs font-bold block truncate">${h.label}</span>
${h.sub ? `<span class="text-[10px] text-gray-400 block truncate">${h.sub}</span>` : ''}
</div>
<span class="px-1.5 py-0.5 bg-gray-100 text-gray-500 font-bold uppercase text-[8px] rounded shrink-0">${typeLabels[h.type] || h.type}</span>
</a>`
).join('');
}
globalResults.classList.remove('hidden');
}, 250);
});
document.addEventListener('click', (e) => {
/* istanbul ignore next */ if (!globalResults.contains(e.target) && e.target !== globalInput) {
globalResults.classList.add('hidden');
}
});
globalInput.addEventListener('keydown', (e) => {
if (e.key === 'Escape') globalResults.classList.add('hidden');
});
}
});