diff --git a/src/app/api/v1/stock-quick-draft/suppliers/route.ts b/src/app/api/v1/stock-quick-draft/suppliers/route.ts new file mode 100644 index 0000000..cc465a9 --- /dev/null +++ b/src/app/api/v1/stock-quick-draft/suppliers/route.ts @@ -0,0 +1,44 @@ +import { NextResponse } from "next/server"; +import { getAdminPocketBase } from "@/lib/server/pocketbase-admin"; + +/** + * GET /api/v1/stock-quick-draft/suppliers + * + * Lists every supplier sorted by `suppliername`. The Quick-Draft modal + * uses this when the picked stock item has no purchase history and the + * new-SKU path has no history either, to populate a fallback supplier + * dropdown. + * + * Why a dedicated route instead of `GET /api/v1/suppliers`: + * - the agent-API route uses bearer auth (AGENT_API_KEY) that the + * browser doesn't carry, so the modal always 401'd against it. + * - the modal wants a minimal `{ id, name }[]` shape; the agent-API + * returns the raw PB records. + * + * Response: { ok: true, items: { id: string; name: string }[] } + */ +export const runtime = "nodejs"; +export const dynamic = "force-dynamic"; + +export async function GET() { + try { + const pb = await getAdminPocketBase(); + const records = await pb.collection("suppliers").getFullList({ + sort: "suppliername", + }); + const items = records.map((r) => ({ + id: r.id, + name: (r.suppliername as string) ?? "", + })); + return NextResponse.json({ ok: true, items }); + } catch (err) { + return NextResponse.json( + { + ok: false, + reason: "list_failed", + message: err instanceof Error ? err.message : "Failed to list suppliers", + }, + { status: 500 }, + ); + } +} \ No newline at end of file diff --git a/src/components/stock-orders/quick-draft-modal.test.tsx b/src/components/stock-orders/quick-draft-modal.test.tsx index 81c50b6..7e2544e 100644 --- a/src/components/stock-orders/quick-draft-modal.test.tsx +++ b/src/components/stock-orders/quick-draft-modal.test.tsx @@ -536,3 +536,51 @@ describe("QuickDraftModal — footer", () => { expect(onOpenChange).toHaveBeenCalledWith(false); }); }); + +describe("QuickDraftModal — no-history fallback", () => { + beforeEach(() => { + vi.useFakeTimers(); + global.fetch = vi.fn().mockImplementation(async (url: string) => { + if (url.includes("/search")) { + return { + ok: true, + json: async () => ({ ok: true, items: [] }), + }; + } + if (url.includes("/supplier-history")) { + return { + ok: true, + json: async () => ({ ok: true, history: [] }), + }; + } + if (url.includes("/suppliers")) { + return { + ok: true, + json: async () => ({ + ok: true, + items: [ + { id: "sup-a", name: "Acme Tools" }, + { id: "sup-b", name: "BuildPro" }, + ], + }), + }; + } + return { ok: true, json: async () => ({ ok: true, items: [] }) }; + }); + }); + + afterEach(() => { + vi.useRealTimers(); + }); + + it("renders a supplier select when history is empty in new-SKU mode", async () => { + render( {}} />); + fireEvent.click(screen.getByTestId("quick-draft-toggle-new-sku")); + await act(async () => { + await vi.advanceTimersByTimeAsync(0); + }); + expect( + screen.getByTestId("quick-draft-supplier-select"), + ).toBeInTheDocument(); + }); +}); diff --git a/src/components/stock-orders/quick-draft-modal.tsx b/src/components/stock-orders/quick-draft-modal.tsx index afb09a7..849387d 100644 --- a/src/components/stock-orders/quick-draft-modal.tsx +++ b/src/components/stock-orders/quick-draft-modal.tsx @@ -51,6 +51,50 @@ function formatMonthYear(date: Date): string { return date.toLocaleDateString("en-US", { month: "short", year: "numeric" }); } +/** + * Fallback supplier picker used when there is no purchase history for + * the picked stock item (or in new-SKU mode, where there's no item at + * all). Renders a full ` onPick(e.target.value)} + className="w-full rounded-md border bg-background px-3 py-2 text-sm" + disabled={suppliersLoading && allSuppliers.length === 0} + > + + {allSuppliers.map((s) => ( + + ))} + +
+ {suppliersLoading && allSuppliers.length === 0 + ? "Loading suppliers…" + : "No prior history for this item — pick any supplier."} +
+ + ); +} + export function QuickDraftModal({ open, onOpenChange }: QuickDraftModalProps) { const [defaultReference] = useState( () => `Stock order entry — ${formatMonthYear(new Date())}`, @@ -66,6 +110,12 @@ export function QuickDraftModal({ open, onOpenChange }: QuickDraftModalProps) { const [history, setHistory] = useState([]); const [historyLoading, setHistoryLoading] = useState(false); const [supplierChoice, setSupplierChoice] = useState(null); + // Fallback supplier list when there is no purchase history for the + // selected item (or in new-SKU mode, where there's no item yet). + const [allSuppliers, setAllSuppliers] = useState< + { id: string; name: string }[] + >([]); + const [suppliersLoading, setSuppliersLoading] = useState(false); const [qty, setQty] = useState(""); const [cost, setCost] = useState(""); // New-SKU toggle state (named for readability; plan uses isNewSku/newName/newSku) @@ -163,6 +213,49 @@ export function QuickDraftModal({ open, onOpenChange }: QuickDraftModalProps) { }; }, [searchQuery]); + // Fetch the full supplier list whenever we land in a state with no + // history-driven supplier choice (no history rows for the picked + // item, or new-SKU mode where there is no item at all). Only fires + // once per modal session — the list is cached in `allSuppliers`. + useEffect(() => { + // Only fetch the full supplier list when we don't have a + // history-driven supplier choice to show. + if (!selectedItem && !newSkuMode) return; + if (historyLoading) return; // Wait for history fetch to settle first + if (history.length > 0) return; // History already gives us the choices + if (allSuppliers.length > 0) return; // Already cached + + let cancelled = false; + setSuppliersLoading(true); + (async () => { + try { + const res = await fetch("/api/v1/stock-quick-draft/suppliers"); + const data = (await res.json()) as { + ok: boolean; + items?: { id: string; name: string }[]; + message?: string; + }; + if (!cancelled && data.ok && data.items) setAllSuppliers(data.items); + } catch (err) { + if (!cancelled) + toast.error( + err instanceof Error ? err.message : "Failed to load suppliers", + ); + } finally { + if (!cancelled) setSuppliersLoading(false); + } + })(); + return () => { + cancelled = true; + }; + }, [ + selectedItem, + newSkuMode, + historyLoading, + history.length, + allSuppliers.length, + ]); + const onPickItem = async (item: StockItemSearchResult) => { setSelectedItem(item); setSupplierChoice(null); @@ -358,9 +451,12 @@ export function QuickDraftModal({ open, onOpenChange }: QuickDraftModalProps) { )} {!historyLoading && history.length === 0 && ( -
- No prior history for this item. -
+ )} {!historyLoading && history.length > 0 && (
@@ -480,10 +576,12 @@ export function QuickDraftModal({ open, onOpenChange }: QuickDraftModalProps) {
)} {!historyLoading && history.length === 0 && ( -
- Pick a supplier — Task 10 will add a full supplier dropdown - when there's no history. -
+ )}