import { useEffect, useMemo, useRef, useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { SEARCH_MIN_CHARS } from '@siop/shared'; import { useGlobalSearch } from '@/api/recherche'; import { StatutOT } from '@/components/chips-ot'; import { IcoRecherche } from '@/components/nav-icons'; interface Resultat { cle: string; route: string; contenu: React.ReactNode; } /** La recherche de la topbar, enfin active (recette R3) : ⌘K pour saisir, * résultats groupés (OT / ascenseurs / sites), déjà filtrés par la matrice * côté API — flèches + Entrée pour naviguer. */ export function RechercheGlobale() { const navigate = useNavigate(); const [saisie, setSaisie] = useState(''); const [q, setQ] = useState(''); const [ouverte, setOuverte] = useState(false); const [actif, setActif] = useState(0); const champ = useRef(null); // Débounce : on n'interroge l'API que 200 ms après la dernière frappe. useEffect(() => { const t = setTimeout(() => setQ(saisie), 200); return () => clearTimeout(t); }, [saisie]); // ⌘K / Ctrl+K : focus depuis n'importe où. useEffect(() => { const surTouche = (e: KeyboardEvent) => { if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === 'k') { e.preventDefault(); champ.current?.focus(); champ.current?.select(); } }; window.addEventListener('keydown', surTouche); return () => window.removeEventListener('keydown', surTouche); }, []); const { data } = useGlobalSearch(q); const resultats = useMemo(() => { if (!data) return []; return [ ...data.workOrders.map((w) => ({ cle: `ot-${w.id}`, route: `/ot/${w.id}`, contenu: ( <> {w.reference} {w.title} ), })), ...data.assets.map((a) => ({ cle: `asc-${a.id}`, route: `/ascenseurs/${a.id}`, contenu: ( <> {a.reference} {a.brand} {a.model ? ` ${a.model}` : ''} {a.siteName} ), })), ...data.sites.map((s) => ({ cle: `site-${s.id}`, route: `/sites/${s.id}`, contenu: ( <> {s.name} {s.city ?? ''} ), })), ]; }, [data]); const groupes = useMemo(() => { if (!data) return []; const g: { libelle: string; debut: number; nombre: number }[] = []; if (data.workOrders.length) g.push({ libelle: 'Ordres de travail', debut: 0, nombre: data.workOrders.length }); if (data.assets.length) g.push({ libelle: 'Ascenseurs', debut: data.workOrders.length, nombre: data.assets.length }); if (data.sites.length) { g.push({ libelle: 'Sites', debut: data.workOrders.length + data.assets.length, nombre: data.sites.length, }); } return g; }, [data]); const visible = ouverte && q.trim().length >= SEARCH_MIN_CHARS; const aller = (r: Resultat | undefined) => { if (!r) return; setOuverte(false); setSaisie(''); navigate(r.route); }; return (
{ setSaisie(e.target.value); setOuverte(true); setActif(0); }} onFocus={() => setOuverte(true)} onBlur={() => setOuverte(false)} onKeyDown={(e) => { if (e.key === 'Escape') { setOuverte(false); champ.current?.blur(); } if (!visible || !resultats.length) return; if (e.key === 'ArrowDown') { e.preventDefault(); setActif((a) => Math.min(a + 1, resultats.length - 1)); } if (e.key === 'ArrowUp') { e.preventDefault(); setActif((a) => Math.max(a - 1, 0)); } if (e.key === 'Enter') { e.preventDefault(); aller(resultats[actif]); } }} /> ⌘K {visible ? (
{groupes.map((g) => (
{g.libelle}
{resultats.slice(g.debut, g.debut + g.nombre).map((r, i) => ( ))}
))} {!resultats.length ? (
Aucun résultat pour « {q.trim()} »
) : null}
) : null}
); }