import { useEffect, useRef, useState } from 'react'; import { useNavigate } from 'react-router'; import { faMagnifyingGlass, faUser, faCalendar } from '@fortawesome/pro-duotone-svg-icons'; import { faIcon } from '@/lib/icon-wrapper'; import { Input } from '@/components/base/input/input'; import { Badge } from '@/components/base/badges/badges'; import { apiClient } from '@/lib/api-client'; import { formatShortDate } from '@/lib/format'; import { cx } from '@/utils/cx'; const SearchIcon = faIcon(faMagnifyingGlass); const UserIcon = faIcon(faUser); const CalendarIcon = faIcon(faCalendar); type SearchResultType = 'lead' | 'patient' | 'appointment'; type SearchResult = { id: string; type: SearchResultType; title: string; subtitle: string; phone?: string; }; type GlobalSearchProps = { onSelectResult?: (result: SearchResult) => void; }; const TYPE_ICONS: Record> = { lead: UserIcon, patient: UserIcon, appointment: CalendarIcon, }; const TYPE_BADGE_COLORS: Record = { lead: 'brand', patient: 'success', appointment: 'blue', }; const TYPE_LABELS: Record = { lead: 'Lead', patient: 'Patient', appointment: 'Appointment', }; export const GlobalSearch = ({ onSelectResult }: GlobalSearchProps) => { const [query, setQuery] = useState(''); const [results, setResults] = useState([]); const [isOpen, setIsOpen] = useState(false); const [isSearching, setIsSearching] = useState(false); const [highlightedIndex, setHighlightedIndex] = useState(-1); const containerRef = useRef(null); const debounceRef = useRef | null>(null); const navigate = useNavigate(); useEffect(() => { if (debounceRef.current) clearTimeout(debounceRef.current); if (query.trim().length < 2) { setResults([]); setIsOpen(false); setIsSearching(false); return; } setIsSearching(true); debounceRef.current = setTimeout(async () => { try { const data = await apiClient.get<{ leads: Array; patients: Array; appointments: Array; }>(`/api/search?q=${encodeURIComponent(query)}`, { silent: true }); const searchResults: SearchResult[] = []; for (const l of data.leads ?? []) { const name = l.contactName ? `${l.contactName.firstName} ${l.contactName.lastName}`.trim() : l.name; searchResults.push({ id: l.id, type: 'lead', title: name || 'Unknown', subtitle: [l.contactPhone?.primaryPhoneNumber, l.source, l.interestedService].filter(Boolean).join(' · '), phone: l.contactPhone?.primaryPhoneNumber, }); } for (const p of data.patients ?? []) { const name = p.fullName ? `${p.fullName.firstName} ${p.fullName.lastName}`.trim() : p.name; searchResults.push({ id: p.id, type: 'patient', title: name || 'Unknown', subtitle: p.phones?.primaryPhoneNumber ?? '', phone: p.phones?.primaryPhoneNumber, }); } for (const a of data.appointments ?? []) { const date = a.scheduledAt ? formatShortDate(a.scheduledAt) : ''; searchResults.push({ id: a.id, type: 'appointment', title: a.doctorName ?? 'Appointment', subtitle: [a.department, date, a.status].filter(Boolean).join(' · '), }); } setResults(searchResults); setIsOpen(true); } catch { setResults([]); } finally { setIsSearching(false); setHighlightedIndex(-1); } }, 300); return () => { if (debounceRef.current) clearTimeout(debounceRef.current); }; }, [query]); // Close on outside click useEffect(() => { const handleClickOutside = (event: MouseEvent) => { if (containerRef.current && !containerRef.current.contains(event.target as Node)) { setIsOpen(false); } }; document.addEventListener('mousedown', handleClickOutside); return () => document.removeEventListener('mousedown', handleClickOutside); }, []); const handleSelect = (result: SearchResult) => { setIsOpen(false); setQuery(''); if (onSelectResult) { onSelectResult(result); } else { navigate(`/patient/${result.id}`); } }; const handleKeyDown = (event: React.KeyboardEvent) => { if (!isOpen || results.length === 0) return; if (event.key === 'ArrowDown') { event.preventDefault(); setHighlightedIndex((prev) => (prev < results.length - 1 ? prev + 1 : 0)); } else if (event.key === 'ArrowUp') { event.preventDefault(); setHighlightedIndex((prev) => (prev > 0 ? prev - 1 : results.length - 1)); } else if (event.key === 'Enter' && highlightedIndex >= 0) { event.preventDefault(); handleSelect(results[highlightedIndex]); } else if (event.key === 'Escape') { setIsOpen(false); } }; // Group results by type const groupedResults: Record = {}; for (const result of results) { const groupLabel = `${TYPE_LABELS[result.type]}s`; if (!groupedResults[groupLabel]) { groupedResults[groupLabel] = []; } groupedResults[groupLabel].push(result); } let flatIndex = -1; return (
setQuery(value)} onFocus={() => { if (results.length > 0) setIsOpen(true); }} shortcut="/" /> {isOpen && (
{isSearching && (
)} {!isSearching && results.length === 0 && (

No results

Try a different search term

)} {!isSearching && results.length > 0 && Object.entries(groupedResults).map(([groupLabel, groupResults]) => (
{groupLabel}
{groupResults.map((result) => { flatIndex++; const currentIndex = flatIndex; const Icon = TYPE_ICONS[result.type]; return ( ); })}
))}
)}
); };