"use client"; import { Search, X } from "lucide-react"; import { useEffect, useRef, useState, type ReactNode } from "react"; type LookupProps = { placeholder?: string; onSearch: (query: string) => Promise; renderResult: (item: T) => ReactNode; onSelect: (item: T) => void; minChars?: number; }; // Generic async search-select used by the position lookup (hire wizard), // manager/superior lookup (create position), and employee multi-select // (reorg workbench) in later phases. export function Lookup({ placeholder = "Suchen…", onSearch, renderResult, onSelect, minChars = 2 }: LookupProps) { const [query, setQuery] = useState(""); const [results, setResults] = useState([]); const [open, setOpen] = useState(false); // Derived from "have we finished searching for the current query yet", // rather than a separate state flag flipped synchronously at the top of // the effect below — the effect only ever sets state from the async // search's own completion callback now. const [lastSearchedQuery, setLastSearchedQuery] = useState(null); const containerRef = useRef(null); const tooShort = query.trim().length < minChars; const loading = !tooShort && lastSearchedQuery !== query.trim(); useEffect(() => { if (tooShort) return; let cancelled = false; const timeout = setTimeout(() => { onSearch(query.trim()).then((res) => { if (cancelled) return; setResults(res); setOpen(true); setLastSearchedQuery(query.trim()); }); }, 200); return () => { cancelled = true; clearTimeout(timeout); }; }, [query, minChars, onSearch, tooShort]); // Derived rather than reset via effect: once the query drops below // minChars, hide the dropdown and any stale results immediately without // needing a synchronous setState inside the effect above. const showDropdown = open && !tooShort; const visibleResults = tooShort ? [] : results; useEffect(() => { function onClickOutside(e: MouseEvent) { if (containerRef.current && !containerRef.current.contains(e.target as Node)) { setOpen(false); } } document.addEventListener("mousedown", onClickOutside); return () => document.removeEventListener("mousedown", onClickOutside); }, []); return (
setQuery(e.target.value)} placeholder={placeholder} className="w-full text-sm outline-none placeholder:text-ink-muted" /> {query && ( )}
{showDropdown && (
{loading &&
Suche…
} {!loading && visibleResults.length === 0 &&
Keine Treffer
} {!loading && visibleResults.map((item, i) => ( ))}
)}
); }