"use client"; import { useState, useEffect, useCallback, useRef } from "react"; import useSWR from "swr"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Textarea } from "@/components/ui/textarea"; import { Badge } from "@/components/ui/badge"; import { Card, CardContent, CardHeader, CardTitle, } from "@/components/ui/card"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/components/ui/table"; import { Dialog, DialogContent, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from "@/components/ui/alert-dialog"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { useTableSort } from "@/hooks/use-table-sort"; import { SortableTableHead } from "@/components/sortable-table-head"; import { Building2, Plus, Search, FileDown, Trash2, Eye, Package, X, AlertTriangle, Cpu, } from "lucide-react"; import { toast } from "sonner"; import type { Supplier, SupplierPackingList, Component as ComponentType } from "@/lib/types"; const fetcher = (url: string) => fetch(url).then((r) => r.json()); interface SupplierPackingItem { id: number; component_id: number; quantity: number; part_number?: string; component_name?: string; used_in_products?: string; } interface LineItem { component_id: number; component_name: string; part_number: string; quantity: number; } export default function SupplierPackingTab() { const { data: supplierPackingLists, mutate } = useSWR( "/api/supplier-packing-lists", fetcher ); const { data: suppliers } = useSWR("/api/suppliers", fetcher); const [search, setSearch] = useState(""); const [createOpen, setCreateOpen] = useState(false); const [viewTarget, setViewTarget] = useState(null); const [viewData, setViewData] = useState< (SupplierPackingList & { items: SupplierPackingItem[] }) | null >(null); const [deleteTarget, setDeleteTarget] = useState(null); // Create form state const [selectedSupplierId, setSelectedSupplierId] = useState(""); const [packingNumber, setPackingNumber] = useState(""); const [lineItems, setLineItems] = useState([]); const [scanStatus, setScanStatus] = useState(null); const [notes, setNotes] = useState(""); const [creating, setCreating] = useState(false); const scanBufferRef = useRef(""); const scanTimerRef = useRef | null>(null); const scanInputRef = useRef(null); // Generate packing number preview useEffect(() => { if (!selectedSupplierId || !suppliers) { setPackingNumber(""); return; } const supplier = suppliers.find((s) => String(s.id) === selectedSupplierId); if (!supplier) return; const supplierName = supplier.name .replace(/[^a-zA-Z0-9]/g, "") .toUpperCase(); const now = new Date(); const yyyy = now.getFullYear(); const mm = String(now.getMonth() + 1).padStart(2, "0"); const dd = String(now.getDate()).padStart(2, "0"); const datePrefix = `PL-EACH-${yyyy}_${mm}_${dd}`; const existing = (supplierPackingLists || []).filter( (pl) => pl.packing_number.startsWith(datePrefix) && pl.packing_number.endsWith(`-${supplierName}`) ).length; const revision = String(existing + 1).padStart(2, "0"); setPackingNumber(`${datePrefix}-${revision}-${supplierName}`); }, [selectedSupplierId, suppliers, supplierPackingLists]); // Fetch detail for view useEffect(() => { if (!viewTarget) { setViewData(null); return; } fetch(`/api/supplier-packing-lists/${viewTarget}`) .then((r) => r.json()) .then(setViewData); }, [viewTarget]); function resetCreateForm() { setSelectedSupplierId(""); setLineItems([]); scanBufferRef.current = ""; setScanStatus(null); setNotes(""); setCreating(false); } // Process the scan buffer: parse tab-separated QR data and look up the component const processScanBuffer = useCallback(async () => { const raw = scanBufferRef.current; scanBufferRef.current = ""; if (scanInputRef.current) scanInputRef.current.value = ""; const parts = raw.split("\t"); if (parts.length < 3) { setScanStatus("Incomplete scan - expected 3 tab-separated fields"); return; } const partNumber = parts[1]?.trim(); const qty = Number(parts[2]?.trim()); if (!partNumber || !qty || qty <= 0) { setScanStatus("Invalid scan data"); return; } try { const res = await fetch(`/api/components?search=${encodeURIComponent(partNumber)}`); const data = await res.json(); const comp = Array.isArray(data) ? data.find((c: ComponentType) => c.part_number === partNumber) : null; if (!comp) { setScanStatus(`Component "${partNumber}" not found`); toast.error(`Component "${partNumber}" not found in database`); return; } setLineItems((prev) => [ ...prev, { component_id: comp.id, component_name: comp.name, part_number: comp.part_number, quantity: qty, }, ]); setScanStatus(`Added ${partNumber} x${qty}`); toast.success(`Added ${comp.part_number} - ${comp.name} (x${qty})`); } catch { setScanStatus("Error looking up component"); toast.error("Error looking up component"); } // Re-focus the scan input for the next scan scanInputRef.current?.focus(); }, [lineItems]); // Handle all keydown events: build buffer from all keys, prevent Tab from moving focus function handleScanKeyDown(e: React.KeyboardEvent) { if (e.key === "Tab") { e.preventDefault(); scanBufferRef.current += "\t"; } else if (e.key === "Enter") { e.preventDefault(); if (scanTimerRef.current) clearTimeout(scanTimerRef.current); processScanBuffer(); return; } else if (e.key === "Backspace") { scanBufferRef.current = scanBufferRef.current.slice(0, -1); } else if (e.key.length === 1) { // Regular printable character scanBufferRef.current += e.key; } else { return; // Ignore other special keys } // Update the visible input to show non-tab content for user feedback if (scanInputRef.current) { scanInputRef.current.value = scanBufferRef.current.replace(/\t/g, " | "); } // Debounce: process 200ms after last keystroke (scanners type fast) if (scanTimerRef.current) clearTimeout(scanTimerRef.current); scanTimerRef.current = setTimeout(() => { processScanBuffer(); }, 200); // Prevent default for all keys so the input doesn't double-type if (e.key.length === 1) e.preventDefault(); } function removeLineItem(componentId: number) { setLineItems((prev) => prev.filter((li) => li.component_id !== componentId)); } async function handleCreate() { if (!selectedSupplierId || !lineItems.length) { toast.error("Select a supplier and add at least one component"); return; } setCreating(true); try { const res = await fetch("/api/supplier-packing-lists", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ supplier_id: Number(selectedSupplierId), items: lineItems.map((li) => ({ component_id: li.component_id, quantity: li.quantity, })), notes: notes || null, }), }); if (!res.ok) { const err = await res.json(); throw new Error(err.error || "Failed to create supplier packing list"); } toast.success("Supplier packing list created"); mutate(); setCreateOpen(false); resetCreateForm(); } catch (err) { toast.error( err instanceof Error ? err.message : "Error creating supplier packing list" ); } finally { setCreating(false); } } async function handleDelete() { if (!deleteTarget) return; try { const res = await fetch(`/api/supplier-packing-lists/${deleteTarget.id}`, { method: "DELETE", }); if (!res.ok) { const err = await res.json(); throw new Error(err.error || "Failed to delete"); } toast.success("Supplier packing list deleted"); mutate(); } catch (err) { toast.error(err instanceof Error ? err.message : "Error deleting"); } finally { setDeleteTarget(null); } } const handleDownloadPdf = useCallback(async (plId: number) => { try { const res = await fetch(`/api/supplier-packing-lists/${plId}`); const data = await res.json(); const { generateSupplierPackingPdf } = await import( "@/lib/generate-supplier-packing-pdf" ); const doc = generateSupplierPackingPdf(data); doc.save(`${data.packing_number}.pdf`); } catch { toast.error("Failed to generate PDF"); } }, []); const filtered = (supplierPackingLists || []).filter((pl) => { if (!search) return true; const s = search.toLowerCase(); return ( pl.packing_number.toLowerCase().includes(s) || (pl.supplier_name || "").toLowerCase().includes(s) ); }); const { sorted: sortedFiltered, sort, toggleSort } = useTableSort(filtered); const totalItems = filtered.reduce( (s, pl) => s + Number(pl.item_count || 0), 0 ); const totalQty = filtered.reduce( (s, pl) => s + Number(pl.total_quantity || 0), 0 ); return ( <>
{/* Summary Cards */}

{filtered.length}

Supplier Packing Lists

{totalItems}

Total Component Lines

{totalQty}

Total Units

{/* Search */}
setSearch(e.target.value)} />
{/* Table */} Packing Lists Actions {sortedFiltered.length === 0 ? ( No supplier packing lists found ) : ( sortedFiltered.map((pl) => ( {pl.packing_number}
{pl.supplier_name} ({pl.supplier_code})
{Number(pl.item_count || 0)} {Number(pl.total_quantity || 0)} {new Date(pl.created_at).toLocaleDateString("en-GB")}
)) )}
{/* Create Dialog */} { setCreateOpen(open); if (!open) resetCreateForm(); }} > New Supplier Packing List
{/* Packing Number (auto-generated) */}

Auto-generated: PL-EACH-YYYY_MM_DD-revision-SUPPLIER

{/* Select Supplier */}
{/* Scan Components */} {selectedSupplierId && (
e.preventDefault()} />

Format: Component PartNumber Quantity (tab-separated via QR scan)

{scanStatus && (

{scanStatus}

)} {lineItems.length > 0 && (
# Part Number Name Qty {lineItems.map((li, i) => ( {i + 1} {li.part_number} {li.component_name} {li.quantity} ))}
)} {lineItems.length > 0 && (
{lineItems.length} component(s),{" "} {lineItems.reduce((s, li) => s + li.quantity, 0)} total units
)}
)} {/* Notes */}