"use client"; import React, { useState } from "react"; import useSWR from "swr"; import { Card, CardContent, } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Badge } from "@/components/ui/badge"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/components/ui/table"; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from "@/components/ui/alert-dialog"; import { Dialog, DialogContent, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { Tabs, TabsContent, TabsList, TabsTrigger, } from "@/components/ui/tabs"; import { Plus, Search, Pencil, Trash2, Building2, FileDown, ChevronDown, ChevronRight, ShoppingCart, Eye, History, Package, Upload, } from "lucide-react"; import { useTableSort } from "@/hooks/use-table-sort"; import { SortableTableHead } from "@/components/sortable-table-head"; import { SupplierDialog } from "@/components/supplier-dialog"; import { toast } from "sonner"; import { formatCurrency } from "@/lib/format"; import type { Supplier } from "@/lib/types"; const fetcher = (url: string) => fetch(url).then((r) => r.json()); type PoItem = { id: number; part_number: string; component_name: string; supplier_part_number: string | null; quantity: number; unit_price: number; received_quantity: number; }; type SupplierPo = { id: number; po_number: string; status: string; item_count: number; total_value: number; total_received: number; total_ordered: number; created_at: string; items?: PoItem[]; supplier_name?: string; supplier_code?: string; supplier_email?: string; supplier_phone?: string; supplier_address?: string; supplier_contact?: string; notes?: string; }; const STATUS_LABELS: Record = { draft: "Draft", ordered: "Ordered", partially_received: "Partial", received: "Received", cancelled: "Cancelled", }; const STATUS_VARIANTS: Record = { draft: "outline", ordered: "secondary", partially_received: "default", received: "default", cancelled: "destructive", }; const STATUS_ORDER = ["draft", "ordered", "partially_received", "received"]; function getAllowedNextStatuses(currentStatus: string): string[] { if (currentStatus === "cancelled") return []; const currentIdx = STATUS_ORDER.indexOf(currentStatus); if (currentIdx < 0) return STATUS_ORDER; const next = STATUS_ORDER.slice(currentIdx + 1); // Allow cancellation only from "ordered" status if (currentStatus === "ordered") { next.push("cancelled"); } return next; } export default function SuppliersPage() { const [search, setSearch] = useState(""); const [dialogOpen, setDialogOpen] = useState(false); const [editingSupplier, setEditingSupplier] = useState(null); const [deleteTarget, setDeleteTarget] = useState(null); const [expandedSupplier, setExpandedSupplier] = useState(null); const [activePos, setActivePos] = useState([]); const [historyPos, setHistoryPos] = useState([]); const [loadingPos, setLoadingPos] = useState(false); const [poTab, setPoTab] = useState("active"); const [viewPo, setViewPo] = useState(null); const [editingReceivedQty, setEditingReceivedQty] = useState>({}); const [savingReceived, setSavingReceived] = useState(false); const [deletePoTarget, setDeletePoTarget] = useState(null); const [shippingCost, setShippingCost] = useState(0); // Draft PO editing state type DraftItem = { component_id: number; part_number: string; component_name: string; supplier_part_number: string; quantity: number; unit_price: number }; const [draftItems, setDraftItems] = useState([]); const [savingDraft, setSavingDraft] = useState(false); const [addItemOpen, setAddItemOpen] = useState(false); const [addItemComponents, setAddItemComponents] = useState[]>([]); const [addItemSearch, setAddItemSearch] = useState(""); const [loadingAddItems, setLoadingAddItems] = useState(false); const [uploadingCsv, setUploadingCsv] = useState(false); // Manual PO creation state const [createPoOpen, setCreatePoOpen] = useState(false); const [createPoSupplier, setCreatePoSupplier] = useState(null); const [supplierComponents, setSupplierComponents] = useState[]>([]); const [loadingComponents, setLoadingComponents] = useState(false); const [poLineItems, setPoLineItems] = useState>({}); // component_id -> quantity const [poLinePrices, setPoLinePrices] = useState>({}); // component_id -> unit_price override const [creatingPo, setCreatingPo] = useState(false); const [componentSearch, setComponentSearch] = useState(""); const { data: rawSuppliers = [], mutate } = useSWR( `/api/suppliers?search=${encodeURIComponent(search)}`, fetcher ); const suppliers = Array.isArray(rawSuppliers) ? rawSuppliers : []; const { sorted: sortedSuppliers, sort, toggleSort } = useTableSort(suppliers); function openEdit(supplier: Supplier) { setEditingSupplier(supplier); setDialogOpen(true); } function openCreate() { setEditingSupplier(null); setDialogOpen(true); } async function handleDelete() { if (!deleteTarget) return; try { const res = await fetch(`/api/suppliers/${deleteTarget.id}`, { method: "DELETE" }); if (!res.ok) { const err = await res.json(); throw new Error(err.error || "Failed to delete"); } toast.success("Supplier deleted"); mutate(); } catch (err) { toast.error(err instanceof Error ? err.message : "Failed to delete supplier"); } finally { setDeleteTarget(null); } } async function fetchPosForSupplier(supplierId: number) { setLoadingPos(true); try { const [activeRes, historyRes] = await Promise.all([ fetch(`/api/supplier-pos?supplier_id=${supplierId}&status=active`), fetch(`/api/supplier-pos?supplier_id=${supplierId}&status=received`), ]); const activeData = await activeRes.json(); const historyData = await historyRes.json(); setActivePos(Array.isArray(activeData) ? activeData : []); setHistoryPos(Array.isArray(historyData) ? historyData : []); } catch { setActivePos([]); setHistoryPos([]); } finally { setLoadingPos(false); } } async function toggleExpandSupplier(supplierId: number) { if (expandedSupplier === supplierId) { setExpandedSupplier(null); return; } setExpandedSupplier(supplierId); setPoTab("active"); await fetchPosForSupplier(supplierId); } async function handleViewPo(id: number) { try { const res = await fetch(`/api/supplier-pos/${id}`); const data: SupplierPo = await res.json(); setViewPo(data); // Init received qty editing state const qtyMap: Record = {}; (data.items || []).forEach((item) => { qtyMap[item.id] = Number(item.received_quantity) || 0; }); setEditingReceivedQty(qtyMap); setShippingCost(Number(data.shipping_cost) || 0); // Init draft editing state if (data.status === "draft") { setDraftItems((data.items || []).map((item) => ({ component_id: Number((item as Record).component_id) || 0, part_number: item.part_number, component_name: item.component_name, supplier_part_number: item.supplier_part_number || "", quantity: Number(item.quantity), unit_price: Number(item.unit_price), }))); } setAddItemOpen(false); } catch { toast.error("Failed to load PO details"); } } async function handleDownloadPoCsv(id: number) { try { const res = await fetch(`/api/supplier-pos/${id}`); const data = await res.json(); const items = (data.items || []) as PoItem[]; const headers = ["Part Number", "Component", "Supplier Ref", "Quantity", "Unit Price", "Total"]; const rows = items.map((item) => [ item.part_number, item.component_name, item.supplier_part_number || "", Number(item.quantity), Number(item.unit_price).toFixed(2), (Number(item.quantity) * Number(item.unit_price)).toFixed(2), ]); const total = items.reduce((s, i) => s + Number(i.quantity) * Number(i.unit_price), 0); rows.push(["", "", "", "", "Total", total.toFixed(2)]); const csvContent = [headers, ...rows] .map((row) => row.map((cell) => `"${String(cell).replace(/"/g, '""')}"`).join(",")) .join("\n"); const blob = new Blob([csvContent], { type: "text/csv;charset=utf-8;" }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = `${data.po_number}.csv`; a.click(); URL.revokeObjectURL(url); } catch { toast.error("Failed to generate CSV"); } } async function handleDownloadPoPdf(id: number) { try { const res = await fetch(`/api/supplier-pos/${id}`); const data = await res.json(); const { generateSupplierPoPdf } = await import("@/lib/generate-supplier-po-pdf"); const doc = generateSupplierPoPdf({ po_number: data.po_number, date: data.created_at, status: data.status, supplier_name: data.supplier_name, supplier_code: data.supplier_code, supplier_email: data.supplier_email, supplier_phone: data.supplier_phone, supplier_address: data.supplier_address, supplier_contact: data.supplier_contact, supplier_country: data.supplier_country, supplier_currency: data.supplier_currency, shipping_cost: Number(data.shipping_cost) || 0, notes: data.notes, items: (data.items || []).map((item: PoItem) => ({ part_number: item.part_number, component_name: item.component_name, supplier_part_number: item.supplier_part_number, quantity: Number(item.quantity), unit_price: Number(item.unit_price), })), }); doc.save(`${data.po_number}.pdf`); } catch { toast.error("Failed to generate PO PDF"); } } async function handlePoStatusChange(poId: number, newStatus: string) { try { const res = await fetch(`/api/supplier-pos/${poId}`, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: newStatus }), }); if (!res.ok) { const errData = await res.json().catch(() => ({})); throw new Error(errData.error || "Failed to update"); } await res.json(); toast.success(`PO status updated to ${STATUS_LABELS[newStatus] || newStatus}`); // Refresh the expanded supplier's PO lists if (expandedSupplier) { await fetchPosForSupplier(expandedSupplier); } // Close detail dialog on cancellation since PO moves to history if (newStatus === "cancelled") { setViewPo(null); return; } // Only open the detail dialog for partially_received so user can enter received quantities if (newStatus === "partially_received") { const freshRes = await fetch(`/api/supplier-pos/${poId}`); const freshPo: SupplierPo = await freshRes.json(); setViewPo(freshPo); const qtyMap: Record = {}; (freshPo.items || []).forEach((item) => { qtyMap[item.id] = Number(item.received_quantity) || 0; }); setEditingReceivedQty(qtyMap); } else if (viewPo && viewPo.id === poId) { // If the dialog is already open, refresh it const freshRes = await fetch(`/api/supplier-pos/${poId}`); const freshPo: SupplierPo = await freshRes.json(); setViewPo(freshPo); } } catch (err) { toast.error(err instanceof Error ? err.message : "Failed to update PO status"); } } async function handleSaveReceivedQuantities() { if (!viewPo) return; setSavingReceived(true); try { const receivedItems = Object.entries(editingReceivedQty).map(([id, qty]) => ({ id: Number(id), received_quantity: qty, })); // Determine new status based on received quantities const items = viewPo.items || []; const allFullyReceived = items.every((item) => { const receivedQty = editingReceivedQty[item.id] ?? 0; return receivedQty >= Number(item.quantity); }); const someReceived = items.some((item) => { const receivedQty = editingReceivedQty[item.id] ?? 0; return receivedQty > 0; }); let newStatus: string | undefined = undefined; if (allFullyReceived && viewPo.status !== "received") { newStatus = "received"; } else if (someReceived && viewPo.status !== "partially_received" && viewPo.status !== "received") { newStatus = "partially_received"; } const res = await fetch(`/api/supplier-pos/${viewPo.id}`, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ ...(newStatus ? { status: newStatus } : {}), received_items: receivedItems, }), }); if (!res.ok) throw new Error("Failed to save"); toast.success("Received quantities saved"); // Re-fetch full PO to get updated items const freshRes = await fetch(`/api/supplier-pos/${viewPo.id}`); const freshPo: SupplierPo = await freshRes.json(); setViewPo(freshPo); const qtyMap: Record = {}; (freshPo.items || []).forEach((item) => { qtyMap[item.id] = Number(item.received_quantity) || 0; }); setEditingReceivedQty(qtyMap); if (expandedSupplier) { await fetchPosForSupplier(expandedSupplier); } } catch { toast.error("Failed to save received quantities"); } finally { setSavingReceived(false); } } async function handleDeletePo() { if (!deletePoTarget) return; try { const res = await fetch(`/api/supplier-pos/${deletePoTarget.id}`, { method: "DELETE" }); if (!res.ok) { const err = await res.json(); throw new Error(err.error || "Failed to delete"); } toast.success(`PO ${deletePoTarget.po_number} deleted`); if (expandedSupplier) { await fetchPosForSupplier(expandedSupplier); } } catch (err) { toast.error(err instanceof Error ? err.message : "Failed to delete PO"); } finally { setDeletePoTarget(null); } } async function handleSaveDraftItems() { if (!viewPo || viewPo.status !== "draft") return; if (draftItems.length === 0) { toast.error("A PO must have at least one item"); return; } setSavingDraft(true); try { const res = await fetch(`/api/supplier-pos/${viewPo.id}`, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ draft_items: draftItems, shipping_cost: shippingCost }), }); if (!res.ok) throw new Error("Failed to save"); toast.success("PO items updated"); const freshRes = await fetch(`/api/supplier-pos/${viewPo.id}`); const freshPo: SupplierPo = await freshRes.json(); setViewPo(freshPo); setDraftItems((freshPo.items || []).map((item) => ({ component_id: Number((item as Record).component_id) || 0, part_number: item.part_number, component_name: item.component_name, supplier_part_number: item.supplier_part_number || "", quantity: Number(item.quantity), unit_price: Number(item.unit_price), }))); if (expandedSupplier) await fetchPosForSupplier(expandedSupplier); } catch { toast.error("Failed to save PO items"); } finally { setSavingDraft(false); } } async function loadAddItemComponents() { if (!viewPo) return; setAddItemOpen(true); setAddItemSearch(""); setLoadingAddItems(true); try { const res = await fetch(`/api/supplier-pos/${viewPo.id}`); const poData = await res.json(); const supplierName = poData.supplier_name || ""; const compRes = await fetch(`/api/components?supplier_name=${encodeURIComponent(supplierName)}`); const data = await compRes.json(); setAddItemComponents(Array.isArray(data) ? data : []); } catch { setAddItemComponents([]); } finally { setLoadingAddItems(false); } } // Handle LCSC CSV upload for draft POs async function handleCsvUpload(e: React.ChangeEvent) { const file = e.target.files?.[0]; if (!file || !viewPo || viewPo.status !== "draft") return; setUploadingCsv(true); try { const text = await file.text(); const lines = text.split("\n").map(line => line.trim()).filter(Boolean); if (lines.length < 2) { toast.error("CSV file is empty or has no data rows"); return; } // Parse header to find column indices const header = lines[0].split(",").map(h => h.trim().toLowerCase()); const lcscPartIdx = header.findIndex(h => h.includes("lcsc part")); const customerNoIdx = header.findIndex(h => h.includes("customer no")); const quantityIdx = header.findIndex(h => h.includes("quantity")); const unitPriceIdx = header.findIndex(h => h.includes("unit price")); if (lcscPartIdx === -1 || customerNoIdx === -1 || quantityIdx === -1 || unitPriceIdx === -1) { toast.error("CSV must have columns: LCSC Part Number, Customer NO., Quantity, Unit Price"); return; } // Parse data rows const csvItems: { supplierRef: string; partNumber: string; quantity: number; unitPrice: number }[] = []; for (let i = 1; i < lines.length; i++) { // Handle CSV with potential commas in quoted fields const row = lines[i].match(/(".*?"|[^",\s]+)(?=\s*,|\s*$)/g); if (!row) continue; const cleanValue = (val: string) => val?.replace(/^"|"$/g, "").trim() || ""; const supplierRef = cleanValue(row[lcscPartIdx] || ""); const partNumber = cleanValue(row[customerNoIdx] || ""); const quantityStr = cleanValue(row[quantityIdx] || "0"); const unitPriceStr = cleanValue(row[unitPriceIdx] || "0").replace(/[^\d.,]/g, "").replace(",", "."); if (!supplierRef && !partNumber) continue; csvItems.push({ supplierRef, partNumber, quantity: parseInt(quantityStr, 10) || 0, unitPrice: parseFloat(unitPriceStr) || 0, }); } if (csvItems.length === 0) { toast.error("No valid items found in CSV"); return; } // Update draft items with CSV data // Customer NO. from CSV is used to match against Part Number in PO // Only Supplier Ref (LCSC Part Number), Quantity, and Unit Price are updated let updatedCount = 0; const updatedDraftItems = draftItems.map(item => { // Match by Customer NO. (from CSV) against Part Number (in PO) const csvMatch = csvItems.find( csv => csv.partNumber.toLowerCase() === item.part_number.toLowerCase() ); if (csvMatch) { updatedCount++; return { ...item, // Part number stays unchanged - only update supplier ref, quantity, and price supplier_part_number: csvMatch.supplierRef || item.supplier_part_number, quantity: csvMatch.quantity || item.quantity, unit_price: csvMatch.unitPrice || item.unit_price, }; } return item; }); if (updatedCount === 0) { toast.error("No matching items found between CSV and PO. Check that Part Numbers match."); return; } setDraftItems(updatedDraftItems); toast.success(`Updated ${updatedCount} item(s) from CSV. Review and save when ready.`); } catch (err) { toast.error(err instanceof Error ? err.message : "Failed to process CSV"); } finally { setUploadingCsv(false); // Reset file input e.target.value = ""; } } function addComponentToDraft(comp: Record) { const cid = Number(comp.id); if (draftItems.some((d) => d.component_id === cid)) { toast.error("Component already in PO"); return; } setDraftItems((prev) => [...prev, { component_id: cid, part_number: (comp.part_number as string) || "", component_name: (comp.name as string) || "", supplier_part_number: (comp.supplier_part_number as string) || "", quantity: 1, unit_price: Number(comp.unit_price) || 0, }]); toast.success(`Added ${comp.name}`); } async function openCreatePo(supplier: Supplier) { setCreatePoSupplier(supplier); setPoLineItems({}); setPoLinePrices({}); setComponentSearch(""); setCreatePoOpen(true); setLoadingComponents(true); try { const res = await fetch(`/api/components?supplier_name=${encodeURIComponent(supplier.name)}`); const data = await res.json(); setSupplierComponents(Array.isArray(data) ? data : []); } catch { setSupplierComponents([]); toast.error("Failed to load components"); } finally { setLoadingComponents(false); } } function updatePoLineQty(componentId: number, qty: number) { setPoLineItems((prev) => { const next = { ...prev }; if (qty <= 0) { delete next[componentId]; } else { next[componentId] = qty; } return next; }); } async function handleCreateManualPo() { if (!createPoSupplier || Object.keys(poLineItems).length === 0) return; setCreatingPo(true); try { const items = Object.entries(poLineItems) .filter(([, qty]) => qty > 0) .map(([cid, qty]) => { const comp = supplierComponents.find((c) => Number(c.id) === Number(cid)); return { component_id: Number(cid), part_number: comp?.part_number || "", component_name: comp?.name || "", supplier_part_number: comp?.supplier_part_number || "", quantity: qty, unit_price: poLinePrices[Number(cid)] ?? Number(comp?.unit_price) ?? 0, }; }); if (items.length === 0) { toast.error("Add at least one component with a quantity"); return; } const res = await fetch("/api/supplier-pos", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ orders: [{ supplier_id: createPoSupplier.id, supplier_name: createPoSupplier.name, items, }], }), }); if (!res.ok) throw new Error("Failed to create PO"); toast.success("Purchase order created"); setCreatePoOpen(false); setCreatePoSupplier(null); setPoLineItems({}); if (expandedSupplier === createPoSupplier.id) { await fetchPosForSupplier(createPoSupplier.id); } } catch { toast.error("Failed to create purchase order"); } finally { setCreatingPo(false); } } function renderPoTable(pos: SupplierPo[], isHistory: boolean) { if (loadingPos) { return

Loading purchase orders...

; } if (pos.length === 0) { return (

{isHistory ? "No received purchase orders yet." : "No active purchase orders for this supplier."}

); } return (
PO Number Status Items Total Value {!isHistory && Received} Created Actions {pos.map((po) => ( {po.po_number} e.stopPropagation()}> {isHistory ? ( {STATUS_LABELS[po.status] || po.status} ) : (() => { const nextStatuses = getAllowedNextStatuses(po.status); return nextStatuses.length === 0 ? ( {STATUS_LABELS[po.status] || po.status} ) : ( ); })()} {String(po.item_count)} {formatCurrency(Number(po.total_value))} {!isHistory && ( {Number(po.total_received)}/{Number(po.total_ordered)} units )} {new Date(po.created_at).toLocaleDateString("en-GB", { day: "2-digit", month: "2-digit", year: "numeric", })}
{po.status === "draft" && ( )}
))}
); } const viewPoItems = (viewPo?.items || []) as PoItem[]; const viewPoIsActive = viewPo?.status !== "received"; return (

Suppliers

Manage component suppliers and contact information

Total Suppliers

{suppliers.length}

setSearch(e.target.value)} className="pl-10" />
Actions {sortedSuppliers.length === 0 && ( No suppliers found. Click "Add Supplier" to create one. )} {sortedSuppliers.map((supplier) => { const isExpanded = expandedSupplier === supplier.id; return ( toggleExpandSupplier(supplier.id)}>
{isExpanded ? : } {supplier.code}
{supplier.name} {supplier.contact_person || "-"} {supplier.country || "-"} {supplier.currency || "EUR"} {supplier.email || "-"} {supplier.phone || "-"} {supplier.address || "-"}
e.stopPropagation()}>
{isExpanded && (
Purchase Orders
Active ({activePos.length}) History ({historyPos.length})
{renderPoTable(activePos, false)} {renderPoTable(historyPos, true)}
)}
); })}
mutate()} /> {/* PO Detail Dialog */} { if (!open) setViewPo(null); }}> {viewPo?.po_number}
Supplier: {viewPo?.supplier_name} {(() => { const currentStatus = viewPo?.status || "draft"; const nextStatuses = getAllowedNextStatuses(currentStatus); return nextStatuses.length === 0 ? ( {STATUS_LABELS[currentStatus] || currentStatus} ) : ( ); })()} {viewPo ? new Date(viewPo.created_at).toLocaleDateString("en-GB", { day: "2-digit", month: "2-digit", year: "numeric" }) : ""}
{/* Draft mode: editable items table */} {viewPo?.status === "draft" ? ( <> {/* CSV Upload Section for Draft POs */}

Import from LCSC CSV

Upload CSV from LCSC to update Supplier Ref, Quantity, and Unit Price. PO will be finalized after import.

# Part Number Component Supplier Ref Unit Price Qty Total {draftItems.length === 0 && ( No items. Add components below. )} {draftItems.map((item, idx) => ( {idx + 1} {item.part_number} {item.component_name} { setDraftItems((prev) => prev.map((d, i) => i === idx ? { ...d, supplier_part_number: e.target.value } : d)); }} /> { const val = Math.max(0, Number(e.target.value) || 0); setDraftItems((prev) => prev.map((d, i) => i === idx ? { ...d, unit_price: val } : d)); }} /> { const val = Math.max(1, Number(e.target.value) || 1); setDraftItems((prev) => prev.map((d, i) => i === idx ? { ...d, quantity: val } : d)); }} /> {formatCurrency(item.quantity * item.unit_price)} ))}
{/* Add component to draft */} {!addItemOpen ? ( ) : (
Add Component
{loadingAddItems ? (

Loading components...

) : ( <>
setAddItemSearch(e.target.value)} className="pl-9 h-8 text-sm" />
Part Number Component Unit Price {(() => { const searchLower = addItemSearch.toLowerCase(); const filtered = addItemComponents.filter((c) => { if (!addItemSearch) return true; return ( (c.part_number as string || "").toLowerCase().includes(searchLower) || (c.name as string || "").toLowerCase().includes(searchLower) ); }); if (filtered.length === 0) { return ( {addItemSearch ? "No matching components" : "No components from this supplier"} ); } return filtered.map((comp) => { const cid = Number(comp.id); const alreadyAdded = draftItems.some((d) => d.component_id === cid); return ( {comp.part_number as string} {comp.name as string} {formatCurrency(Number(comp.unit_price))} ); }); })()}
)}
)}
setShippingCost(Number(e.target.value) || 0)} />
Total: {formatCurrency(draftItems.reduce((sum, item) => sum + item.quantity * item.unit_price, 0) + shippingCost)}
) : ( <> {/* Non-draft mode: read-only items with received qty editing for active POs */}
# Part Number Component Supplier Ref Ordered {viewPoIsActive && Received} {viewPoIsActive && Pending} Unit Price Total {viewPoItems.map((item, idx) => { const ordered = Number(item.quantity); const received = editingReceivedQty[item.id] ?? Number(item.received_quantity) ?? 0; const pending = Math.max(0, ordered - received); return ( {idx + 1} {item.part_number} {item.component_name} {item.supplier_part_number || "-"} {ordered.toLocaleString()} {viewPoIsActive && ( { const val = Math.min(ordered, Math.max(0, Number(e.target.value) || 0)); setEditingReceivedQty((prev) => ({ ...prev, [item.id]: val })); }} /> )} {viewPoIsActive && ( 0 ? "text-destructive font-semibold" : "text-muted-foreground"}`}> {pending.toLocaleString()} )} {formatCurrency(Number(item.unit_price))} {formatCurrency(ordered * Number(item.unit_price))} ); })}
{shippingCost > 0 && (
Shipping Cost: {formatCurrency(shippingCost)}
)}
{viewPoIsActive ? ( ) :
} {viewPoItems.length > 0 && (
Total: {formatCurrency(viewPoItems.reduce((sum, item) => sum + Number(item.quantity) * Number(item.unit_price), 0) + shippingCost)}
)}
)}
setDeleteTarget(null)}> Delete Supplier Are you sure you want to delete "{deleteTarget?.name}"? This action cannot be undone. Cancel Delete {/* Delete PO Confirmation */} setDeletePoTarget(null)}> Delete Purchase Order Are you sure you want to delete PO "{deletePoTarget?.po_number}"? This action cannot be undone. Cancel Delete {/* Manual PO Creation Dialog */} { if (!open) { setCreatePoOpen(false); setCreatePoSupplier(null); } }}> Create Purchase Order
Supplier: {createPoSupplier?.name}
{loadingComponents ? (

Loading components...

) : supplierComponents.length === 0 ? (

No components found for this supplier. Components must have their Supplier field set to "{createPoSupplier?.name}".

) : (() => { const searchLower = componentSearch.toLowerCase(); const selectedComponents = supplierComponents.filter((c) => poLineItems[Number(c.id)] > 0); const filteredComponents = supplierComponents.filter((comp) => { if (!componentSearch) return true; return ( (comp.part_number as string || "").toLowerCase().includes(searchLower) || (comp.name as string || "").toLowerCase().includes(searchLower) || (comp.supplier_part_number as string || "").toLowerCase().includes(searchLower) ); }); const selectedCount = selectedComponents.length; const totalCost = Object.entries(poLineItems).reduce((sum, [cid, qty]) => { const comp = supplierComponents.find((c) => Number(c.id) === Number(cid)); const price = poLinePrices[Number(cid)] ?? Number(comp?.unit_price) ?? 0; return sum + qty * price; }, 0); return ( <> {/* Search and summary bar */}
setComponentSearch(e.target.value)} className="pl-9 h-9 text-sm" />
{filteredComponents.length} of {supplierComponents.length} components
{/* Selected items summary (always visible above the scrollable area) */} {selectedCount > 0 && (
{selectedCount} component(s) selected {formatCurrency(totalCost)}
)} {/* Scrollable table */}
Part Number Component Supplier Part No. Unit Price In Stock Qty to Order {filteredComponents.length === 0 && ( No components match "{componentSearch}" )} {filteredComponents.map((comp) => { const cid = Number(comp.id); const qty = poLineItems[cid] || 0; return ( 0 ? "bg-primary/5" : ""}> {comp.part_number as string} {comp.name as string} {(comp.supplier_part_number as string) || "-"} { const val = Math.max(0, Number(e.target.value) || 0); setPoLinePrices((prev) => ({ ...prev, [cid]: val })); }} /> {Number(comp.stock_quantity).toLocaleString()} updatePoLineQty(cid, Number(e.target.value) || 0)} /> ); })}
{/* Footer actions */}
); })()}
); }