"use client"; import { useState, useMemo } from "react"; import useSWR from "swr"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Badge } from "@/components/ui/badge"; import { Label } from "@/components/ui/label"; import { Card, CardContent, CardHeader, CardTitle, } from "@/components/ui/card"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, } from "@/components/ui/dialog"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/components/ui/table"; import { Search, History, ShoppingCart, ArrowUpCircle, ArrowDownCircle, RotateCcw, Euro, Package, TrendingUp, Download, ChevronLeft, ChevronRight, Printer, ExternalLink, FileDown, } from "lucide-react"; import { useTableSort } from "@/hooks/use-table-sort"; import { SortableTableHead } from "@/components/sortable-table-head"; import { formatCurrency } from "@/lib/format"; import { generateLabelPdf } from "@/lib/generate-label-pdf"; import { toast } from "sonner"; import type { StockTransaction } from "@/lib/types"; const fetcher = (url: string) => fetch(url).then((r) => r.json()); const PAGE_SIZE = 25; const typeConfig: Record< string, { label: string; color: string; icon: typeof ShoppingCart } > = { purchase: { label: "Purchase", color: "bg-accent text-accent-foreground", icon: ShoppingCart }, adjustment: { label: "Adjustment", color: "bg-primary text-primary-foreground", icon: ArrowUpCircle }, return: { label: "Return", color: "bg-warning text-warning-foreground", icon: RotateCcw }, consumption: { label: "Consumption", color: "bg-destructive text-destructive-foreground", icon: ArrowDownCircle }, }; export default function StockHistoryPage() { const [search, setSearch] = useState(""); const [typeFilter, setTypeFilter] = useState("all"); const [page, setPage] = useState(0); // PO detail dialog state const [poDialogOpen, setPoDialogOpen] = useState(false); const [poDetail, setPoDetail] = useState | null>(null); const [poLoading, setPoLoading] = useState(false); async function handleOpenPo(poNumber: string) { setPoLoading(true); setPoDialogOpen(true); try { // Find PO by number from list const listRes = await fetch("/api/supplier-pos"); const allPos = await listRes.json(); const match = (Array.isArray(allPos) ? allPos : []).find( (p: Record) => p.po_number === poNumber ); if (!match) { toast.error("Purchase order not found"); setPoDialogOpen(false); return; } // Fetch full detail const detailRes = await fetch(`/api/supplier-pos/${match.id}`); const detail = await detailRes.json(); setPoDetail(detail); } catch { toast.error("Failed to load purchase order"); setPoDialogOpen(false); } finally { setPoLoading(false); } } async function handleDownloadPoPdf() { if (!poDetail) return; try { const { generateSupplierPoPdf } = await import("@/lib/generate-supplier-po-pdf"); const doc = generateSupplierPoPdf({ po_number: String(poDetail.po_number), date: String(poDetail.created_at), status: String(poDetail.status), supplier_name: String(poDetail.supplier_name), supplier_code: String(poDetail.supplier_code), supplier_email: poDetail.supplier_email as string | null, supplier_phone: poDetail.supplier_phone as string | null, supplier_address: poDetail.supplier_address as string | null, supplier_contact: poDetail.supplier_contact as string | null, supplier_country: poDetail.supplier_country as string | null, supplier_currency: poDetail.supplier_currency as string | null, shipping_cost: Number(poDetail.shipping_cost) || 0, notes: poDetail.notes as string | null, items: (Array.isArray(poDetail.items) ? poDetail.items : []).map((item: Record) => ({ part_number: String(item.part_number || ""), component_name: String(item.component_name || ""), supplier_part_number: String(item.supplier_part_number || ""), quantity: Number(item.quantity), unit_price: Number(item.unit_price), })), }); doc.save(`${poDetail.po_number}.pdf`); } catch { toast.error("Failed to generate PO PDF"); } } // Label printing state const [labelDialogOpen, setLabelDialogOpen] = useState(false); const [labelPartNumber, setLabelPartNumber] = useState(""); const [labelQuantity, setLabelQuantity] = useState(1); const [printingLabel, setPrintingLabel] = useState(false); function openLabelDialog(partNumber: string, defaultQty: number) { setLabelPartNumber(partNumber); setLabelQuantity(defaultQty); setLabelDialogOpen(true); } async function handlePrintLabel() { if (!labelPartNumber) return; setPrintingLabel(true); try { const doc = await generateLabelPdf({ partNumber: labelPartNumber, quantity: labelQuantity, }); doc.save(`label-${labelPartNumber}.pdf`); toast.success("Label PDF generated"); setLabelDialogOpen(false); } catch { toast.error("Failed to generate label"); } finally { setPrintingLabel(false); } } const params = new URLSearchParams(); if (search) params.set("search", search); if (typeFilter && typeFilter !== "all") params.set("type", typeFilter); params.set("limit", String(PAGE_SIZE)); params.set("offset", String(page * PAGE_SIZE)); const { data } = useSWR<{ transactions: StockTransaction[]; total: number; }>(`/api/stock?${params.toString()}`, fetcher, { keepPreviousData: true, }); const transactions = data?.transactions || []; const total = data?.total || 0; const { sorted: sortedTransactions, sort, toggleSort } = useTableSort(transactions); const totalPages = Math.ceil(total / PAGE_SIZE); // Summary stats const stats = useMemo(() => { if (!transactions.length) return { totalTransactions: total, purchaseCount: 0, totalSpent: 0, avgOrderSize: 0, }; const purchases = transactions.filter( (t) => t.transaction_type === "purchase" ); const totalSpent = purchases.reduce( (sum, t) => sum + Number(t.total_cost || 0), 0 ); return { totalTransactions: total, purchaseCount: purchases.length, totalSpent, avgOrderSize: purchases.length > 0 ? totalSpent / purchases.length : 0, }; }, [transactions, total]); function handleExportCSV() { if (!transactions.length) return; const headers = [ "Date", "Type", "Part Number", "Component", "Category", "Quantity", "Unit Cost", "Total Cost", "Supplier", "Purchase Order", "Notes", ]; const rows = transactions.map((t) => [ new Date(t.created_at).toLocaleString(), t.transaction_type, t.part_number || "", `"${t.component_name || ""}"`, t.category_name || "", t.quantity, t.unit_cost ?? "", t.total_cost ?? "", t.supplier || "", t.supplier_order_ref || "", `"${(t.notes || "").replace(/"/g, '""')}"`, ]); const csv = [headers, ...rows].map((r) => r.join(",")).join("\n"); const blob = new Blob([csv], { type: "text/csv" }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = `stock-history-${new Date().toISOString().slice(0, 10)}.csv`; a.click(); URL.revokeObjectURL(url); } // Reset page when filters change function handleSearchChange(val: string) { setSearch(val); setPage(0); } function handleTypeChange(val: string) { setTypeFilter(val); setPage(0); } return (

Purchase History

Track all stock transactions, purchases, and inventory changes

{/* Stats Cards */}
Total Transactions

{stats.totalTransactions.toLocaleString()}

Purchases (This Page)

{stats.purchaseCount}

Total Spent (This Page)

{formatCurrency(stats.totalSpent)}

Avg Purchase Cost

{formatCurrency(stats.avgOrderSize)}

{/* Filters */}
handleSearchChange(e.target.value)} className="pl-9" />
{/* Transactions Table */}
{sortedTransactions.length === 0 ? ( {search || typeFilter !== "all" ? "No transactions match your filters." : "No stock transactions yet. Use the \"Add Stock\" button on a component to record purchases."} ) : ( sortedTransactions.map((txn) => { const config = typeConfig[txn.transaction_type] || typeConfig.purchase; const IconComponent = config.icon; return ( {new Date(txn.created_at).toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric", })}
{new Date(txn.created_at).toLocaleTimeString("en-US", { hour: "2-digit", minute: "2-digit", })}
{config.label} {txn.part_number || "-"} {txn.component_name || "-"} {txn.transaction_type === "consumption" ? "-" : "+"} {Number(txn.quantity).toLocaleString()} {txn.supplier || "-"}
{txn.supplier_order_ref ? ( ) : ( - )}
{txn.part_number && ( )}
); }) )}
{/* Pagination */} {totalPages > 1 && (

Showing {page * PAGE_SIZE + 1}- {Math.min((page + 1) * PAGE_SIZE, total)} of{" "} {total.toLocaleString()} transactions

Page {page + 1} of {totalPages}
)} {/* PO Detail Dialog */} {poLoading ? (

Loading purchase order...

) : poDetail ? ( <> {String(poDetail.po_number)} {String(poDetail.status || "").replace(/_/g, " ")}

Supplier: {String(poDetail.supplier_name)} {" | "} {new Date(String(poDetail.created_at)).toLocaleDateString("en-GB", { day: "2-digit", month: "2-digit", year: "numeric" })}

Part Number Component Qty Unit Price Total Received {(Array.isArray(poDetail.items) ? poDetail.items : []).map((item: Record, idx: number) => ( {String(item.part_number || "")} {String(item.component_name || "")} {Number(item.quantity)} {formatCurrency(Number(item.unit_price))} {formatCurrency(Number(item.quantity) * Number(item.unit_price))} {Number(item.received_quantity)}/{Number(item.quantity)} ))}
{poDetail.notes && (
Notes: {String(poDetail.notes)}
)} ) : null}
{/* Print Label Dialog */} Print Label
{/* Label preview */}

Code:

{labelPartNumber}

Quantity:

{labelQuantity}

setLabelQuantity(Math.max(1, Number(e.target.value) || 1))} className="font-mono" />

Label size: 49mm x 12mm. The QR code will contain: {labelPartNumber},{labelQuantity}

); }