"use client"; import { useState, useRef, useEffect, useCallback } from "react"; import { ChevronDown, Search, X } from "lucide-react"; import type { AccountGroup } from "@/app/(portal)/po/new/new-po-form"; interface Props { name: string; value: string; onChange: (value: string) => void; groups: AccountGroup[]; placeholder?: string; required?: boolean; /** "default" for the main PO field, "compact" for the per-line-item table cell */ size?: "default" | "compact"; } export function SearchableSelect({ name, value, onChange, groups, placeholder = "Select accounting code…", required, size = "default", }: Props) { const [open, setOpen] = useState(false); const [query, setQuery] = useState(""); const containerRef = useRef(null); const searchRef = useRef(null); // Close on outside click / Escape useEffect(() => { if (!open) return; function handleKey(e: KeyboardEvent) { if (e.key === "Escape") { setOpen(false); setQuery(""); } } function handleClick(e: MouseEvent) { if (containerRef.current && !containerRef.current.contains(e.target as Node)) { setOpen(false); setQuery(""); } } document.addEventListener("keydown", handleKey); document.addEventListener("mousedown", handleClick); return () => { document.removeEventListener("keydown", handleKey); document.removeEventListener("mousedown", handleClick); }; }, [open]); // Auto-focus search input when opened useEffect(() => { if (open) searchRef.current?.focus(); }, [open]); // Find the display label for the current value const selectedItem = groups.flatMap((g) => g.items).find((i) => i.id === value); const selectedLabel = selectedItem ? `${selectedItem.code} — ${selectedItem.name}` : ""; // Filter by query (code or name, case-insensitive) const q = query.trim().toLowerCase(); const filtered = q ? groups .map((g) => ({ ...g, items: g.items.filter( (i) => i.code.toLowerCase().includes(q) || i.name.toLowerCase().includes(q) ), })) .filter((g) => g.items.length > 0) : groups; const handleSelect = useCallback( (id: string) => { onChange(id); setOpen(false); setQuery(""); }, [onChange] ); const handleClear = useCallback( (e: React.MouseEvent) => { e.stopPropagation(); onChange(""); }, [onChange] ); const isCompact = size === "compact"; return (
{/* Hidden input for form submission */} {required && !value && ( /* Invisible trick to trigger native "required" validation on submit */ {}} className="absolute opacity-0 w-0 h-0 pointer-events-none" aria-hidden /> )} {/* Trigger */} {/* Dropdown panel */} {open && (
{/* Search input */}
setQuery(e.target.value)} placeholder="Type code or name…" className="flex-1 text-sm outline-none placeholder:text-neutral-400" /> {query && ( )}
{/* Options */}
{filtered.length === 0 ? (

No codes match "{query}"

) : ( filtered.map((group) => (
{/* Group header */}
{group.group}
{/* Items */} {group.items.map((item) => ( ))}
)) )}
)}
); }