"use client"; import { useEffect, useMemo, useRef, useState } from "react"; import { useQuery } from "@tanstack/react-query"; import { Loader2, Search, X } from "lucide-react"; import { accountingApi } from "@/lib/accounting-api"; import { useTenantId } from "@/hooks/useTenantId"; import { formatMoney } from "@/lib/utils"; import { cn } from "@/lib/utils"; import { Button } from "@/components/ds/Button"; export type ComboboxOption = { id: string; label: string; subLabel?: string; meta?: Record; }; function useDebounced(value: string, ms = 250) { const [v, setV] = useState(value); useEffect(() => { const t = setTimeout(() => setV(value), ms); return () => clearTimeout(t); }, [value, ms]); return v; } export function EntityCombobox({ valueLabel, placeholder, options, loading, onQueryChange, onSelect, onClear, emptyText = "موردی یافت نشد", className, }: { valueLabel?: string; placeholder: string; options: ComboboxOption[]; loading?: boolean; onQueryChange: (q: string) => void; onSelect: (opt: ComboboxOption) => void; onClear?: () => void; emptyText?: string; className?: string; }) { const [open, setOpen] = useState(false); const [q, setQ] = useState(""); const rootRef = useRef(null); useEffect(() => { const onDoc = (e: MouseEvent) => { if (!rootRef.current?.contains(e.target as Node)) setOpen(false); }; document.addEventListener("mousedown", onDoc); return () => document.removeEventListener("mousedown", onDoc); }, []); return (
{ setOpen(true); onQueryChange(q); }} onChange={(e) => { setQ(e.target.value); setOpen(true); onQueryChange(e.target.value); }} /> {loading ? : null} {valueLabel && onClear ? ( ) : null}
{open ? (
{options.length === 0 && !loading ? (

{emptyText}

) : ( options.map((opt) => ( )) )}
) : null}
); } export function PartyCombobox({ module, valueName, onChange, placeholder, }: { /** sales → customers, purchase → suppliers, else both */ module?: string; valueName?: string; onChange: (party: { id: string; name: string; kind: "customer" | "supplier" } | null) => void; placeholder?: string; }) { const { tenantId } = useTenantId(); const [q, setQ] = useState(""); const dq = useDebounced(q); const kind: "customer" | "supplier" | "both" = module === "sales" ? "customer" : module === "purchase" ? "supplier" : "both"; const customersQ = useQuery({ queryKey: ["accounting", tenantId, "parties", "customers"], queryFn: () => accountingApi.customers.list(tenantId!), enabled: !!tenantId && (kind === "customer" || kind === "both"), staleTime: 30_000, }); const suppliersQ = useQuery({ queryKey: ["accounting", tenantId, "parties", "suppliers"], queryFn: () => accountingApi.suppliers.list(tenantId!), enabled: !!tenantId && (kind === "supplier" || kind === "both"), staleTime: 30_000, }); const options = useMemo(() => { const needle = dq.trim().toLowerCase(); const rows: ComboboxOption[] = []; if (kind === "customer" || kind === "both") { for (const c of customersQ.data ?? []) { const label = `${c.code} — ${c.name}`; if (!needle || label.toLowerCase().includes(needle) || c.name.toLowerCase().includes(needle)) { rows.push({ id: `customer:${c.id}`, label: c.name, subLabel: `مشتری · ${c.code} · مانده ${formatMoney(c.outstanding_balance)}`, meta: { kind: "customer", id: c.id, name: c.name }, }); } } } if (kind === "supplier" || kind === "both") { for (const s of suppliersQ.data ?? []) { const label = `${s.code} — ${s.name}`; if (!needle || label.toLowerCase().includes(needle) || s.name.toLowerCase().includes(needle)) { rows.push({ id: `supplier:${s.id}`, label: s.name, subLabel: `تأمین‌کننده · ${s.code} · مانده ${formatMoney(s.outstanding_balance)}`, meta: { kind: "supplier", id: s.id, name: s.name }, }); } } } return rows.slice(0, 40); }, [customersQ.data, suppliersQ.data, dq, kind]); const loading = customersQ.isFetching || suppliersQ.isFetching; return ( { const kind = (opt.meta?.kind || "customer") as "customer" | "supplier"; onChange({ id: opt.meta!.id, name: opt.meta!.name, kind }); }} onClear={() => onChange(null)} /> ); } export function ItemCombobox({ valueLabel, onSelect, onClear, }: { valueLabel?: string; onSelect: (item: { id: string; code: string; name: string; unit: string; unit_cost: string; }) => void; onClear?: () => void; }) { const { tenantId } = useTenantId(); const [q, setQ] = useState(""); const dq = useDebounced(q); const itemsQ = useQuery({ queryKey: ["accounting", tenantId, "ops-items"], queryFn: () => accountingApi.ops.listItems(tenantId!), enabled: !!tenantId, staleTime: 30_000, }); const options = useMemo(() => { const needle = dq.trim().toLowerCase(); return (itemsQ.data ?? []) .filter((i) => i.is_active !== false) .filter((i) => { if (!needle) return true; return ( i.name.toLowerCase().includes(needle) || i.code.toLowerCase().includes(needle) ); }) .slice(0, 40) .map((i) => ({ id: i.id, label: `${i.code} — ${i.name}`, subLabel: `${i.unit} · موجودی ${i.quantity_on_hand} · فی ${formatMoney(i.unit_cost)}`, meta: { id: i.id, code: i.code, name: i.name, unit: i.unit, unit_cost: String(i.unit_cost ?? "0"), }, })); }, [itemsQ.data, dq]); return ( onSelect({ id: opt.meta!.id, code: opt.meta!.code, name: opt.meta!.name, unit: opt.meta!.unit, unit_cost: opt.meta!.unit_cost, }) } onClear={onClear} /> ); }