"use client"; import { useState, useRef, useEffect, useLayoutEffect, useCallback } from "react"; import { createPortal } from "react-dom"; 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); const [portalStyle, setPortalStyle] = useState({}); const [mounted, setMounted] = useState(false); useEffect(() => { setMounted(true); }, []); // Recalculate portal position on open, scroll, and resize so the panel tracks // the trigger even when the page (or any ancestor) is scrolled. const updatePortalPos = useCallback(() => { if (!containerRef.current) return; const rect = containerRef.current.getBoundingClientRect(); const PANEL_WIDTH = 420; const left = Math.max(8, rect.right - PANEL_WIDTH); setPortalStyle({ position: "fixed", top: rect.bottom + 4, left, width: PANEL_WIDTH, zIndex: 9999, }); }, []); useLayoutEffect(() => { if (!open) return; updatePortalPos(); }, [open, updatePortalPos]); useEffect(() => { if (!open) return; // capture:true catches scroll on any ancestor, not just window window.addEventListener("scroll", updatePortalPos, true); window.addEventListener("resize", updatePortalPos); return () => { window.removeEventListener("scroll", updatePortalPos, true); window.removeEventListener("resize", updatePortalPos); }; }, [open, updatePortalPos]); // 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]); const selectedItem = groups.flatMap((g) => g.items).find((i) => i.id === value); const selectedLabel = selectedItem ? `${selectedItem.code} — ${selectedItem.name}` : ""; 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"; const dropdownPanel = (
{/* Search input */}
setQuery(e.target.value)} placeholder="Type code or name…" className="flex-1 text-sm outline-none placeholder:text-neutral-400" /> {query && ( )}
{/* Options list */}
{filtered.length === 0 ? (

No codes match “{query}”

) : ( filtered.map((group) => { // Strip top-level breadcrumb — show only the sub-category part const groupLabel = group.group.includes("›") ? group.group.split("›").pop()!.trim() : group.group; return (
{groupLabel}
{group.items.map((item) => ( ))}
); }) )}
); return (
{required && !value && ( {}} className="absolute opacity-0 w-0 h-0 pointer-events-none" aria-hidden /> )} {/* Trigger button — same appearance for both sizes */} {/* Dropdown: compact uses a portal (fixed) to escape overflow-x-auto containers */} {open && ( isCompact && mounted ? createPortal(dropdownPanel, document.body) : dropdownPanel )}
); }