"use client"; import { useState, useCallback } 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 { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; 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, DialogFooter, } from "@/components/ui/dialog"; import { Label } from "@/components/ui/label"; import { Plus, Search, Pencil, Trash2, AlertTriangle, ExternalLink, PackagePlus, Printer, Box, ArrowRight } from "lucide-react"; import Link from "next/link"; import { useTableSort } from "@/hooks/use-table-sort"; import { SortableTableHead } from "@/components/sortable-table-head"; import { ComponentDialog } from "@/components/component-dialog"; import { AddStockDialog } from "@/components/add-stock-dialog"; import { generateLabelPdf } from "@/lib/generate-label-pdf"; import { toast } from "sonner"; import { formatUnitPrice } from "@/lib/format"; import type { Component, Category, ComponentSpecs } from "@/lib/types"; const fetcher = (url: string) => fetch(url).then((r) => r.json()); export default function ComponentsPage() { const [search, setSearch] = useState(""); const [categoryFilter, setCategoryFilter] = useState("all"); const [lowStockFilter, setLowStockFilter] = useState(false); const [dialogOpen, setDialogOpen] = useState(false); const [editingComponent, setEditingComponent] = useState(null); const [deleteTarget, setDeleteTarget] = useState(null); const [stockTarget, setStockTarget] = useState(null); const [stockDialogOpen, setStockDialogOpen] = useState(false); // Component detail dialog state const [detailDialogOpen, setDetailDialogOpen] = useState(false); const [detailComponent, setDetailComponent] = useState(null); interface ProductUsage { id: number; product_id: number; product_name: string; sku: string; version: string; status: string; quantity: number; reference_designator: string | null; notes: string | null; } const { data: componentUsage = [], isLoading: usageLoading } = useSWR( detailComponent ? `/api/components/${detailComponent.id}/usage` : null, fetcher ); function openDetail(comp: Component) { setDetailComponent(comp); setDetailDialogOpen(true); } // Label printing state const [labelDialogOpen, setLabelDialogOpen] = useState(false); const [labelPartNumber, setLabelPartNumber] = useState(""); const [labelQuantity, setLabelQuantity] = useState(1); const [printingLabel, setPrintingLabel] = useState(false); function openLabelDialog(comp: Component) { setLabelPartNumber(comp.part_number); setLabelQuantity(comp.stock_quantity); 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 (categoryFilter && categoryFilter !== "all") params.set("category", categoryFilter); if (lowStockFilter) params.set("lowStock", "true"); const { data: components = [], mutate } = useSWR( `/api/components?${params.toString()}`, fetcher ); const { data: categories = [] } = useSWR( "/api/categories", fetcher ); const { sorted: sortedComponents, sort, toggleSort } = useTableSort(components); const handleSave = useCallback( async (data: Partial) => { const url = editingComponent ? `/api/components/${editingComponent.id}` : "/api/components"; const method = editingComponent ? "PUT" : "POST"; const res = await fetch(url, { method, headers: { "Content-Type": "application/json" }, body: JSON.stringify(data), }); if (!res.ok) { const err = await res.json(); throw new Error(err.error || "Failed to save"); } toast.success( editingComponent ? "Component updated" : "Component created" ); mutate(); }, [editingComponent, mutate] ); const handleDelete = useCallback(async () => { if (!deleteTarget) return; await fetch(`/api/components/${deleteTarget.id}`, { method: "DELETE" }); toast.success("Component deleted"); setDeleteTarget(null); mutate(); }, [deleteTarget, mutate]); function openCreate() { setEditingComponent(null); setDialogOpen(true); } function openEdit(comp: Component) { setEditingComponent(comp); setDialogOpen(true); } function openAddStock(comp: Component) { setStockTarget(comp); setStockDialogOpen(true); } return (

Components

Manage your electronic component inventory

{/* Filters */}
setSearch(e.target.value)} className="pl-9" />
{/* Components Table */}
Actions {sortedComponents.length === 0 ? ( {search || categoryFilter !== "all" || lowStockFilter ? "No components match your filters." : "No components yet. Click \"Add Component\" to get started."} ) : ( sortedComponents.map((comp) => { const isLowStock = comp.stock_quantity <= comp.min_stock_level && comp.min_stock_level > 0; return ( openDetail(comp)}> {comp.datasheet_url ? ( e.stopPropagation()} > {comp.part_number} ) : ( comp.part_number )} {comp.name} {comp.category_name && ( {comp.category_name} )} {comp.customer_supplied ? ( {comp.customer_name || "Customer"} ) : ( Standard )} {comp.shelf_location || "-"} {formatUnitPrice(Number(comp.unit_price))} {isLowStock && ( )} {comp.stock_quantity} {Number((comp as Record).on_order) > 0 ? ( {Number((comp as Record).on_order)} ) : ( - )} {comp.min_stock_level}
e.stopPropagation()}>
); }) )}
mutate()} onPrintLabel={(partNumber, quantity) => { setLabelPartNumber(partNumber); setLabelQuantity(quantity); setLabelDialogOpen(true); }} /> setDeleteTarget(null)} > Delete Component Are you sure you want to delete{" "} {deleteTarget?.name} ({deleteTarget?.part_number} )? This action cannot be undone and will remove it from all BOMs. Cancel Delete {/* Component Detail Dialog */} {detailComponent && (() => { const rawSpecs = detailComponent.specs; const specs: ComponentSpecs = typeof rawSpecs === "string" ? JSON.parse(rawSpecs) : rawSpecs || {}; const hasSpecs = Object.values(specs).some((v) => v && String(v).trim() !== ""); return ( <> {detailComponent.name} {detailComponent.part_number}
{detailComponent.category_name && ( Category: {detailComponent.category_name} )} {detailComponent.manufacturer && ( Manufacturer: {detailComponent.manufacturer} )} Stock: {detailComponent.stock_quantity} {Number((detailComponent as Record).on_order) > 0 && ( On Order: {Number((detailComponent as Record).on_order)} )} {detailComponent.customer_supplied && ( Customer Supplied: {detailComponent.customer_name || "Unknown"} )}
{/* Specifications */} {hasSpecs && (

Specifications

{specs.manufacturer_id && Manufacturer ID: {specs.manufacturer_id}} {specs.value && Value: {specs.value}} {specs.power && Power: {specs.power}} {specs.voltage && Voltage: {specs.voltage}} {specs.tolerance && Tolerance: {specs.tolerance}} {specs.color && Color: {specs.color}} {specs.no_of_pins && Pins: {specs.no_of_pins}} {specs.no_of_rows && Rows: {specs.no_of_rows}} {specs.connector_type && Type: {specs.connector_type}}
)} {/* Supplier info */} {(detailComponent.supplier || detailComponent.datasheet_url) && (
{detailComponent.supplier && ( Supplier: {detailComponent.supplier} )} {detailComponent.supplier_part_number && ( Supplier Part No.: {detailComponent.supplier_part_number} )} {detailComponent.datasheet_url && ( Supplier URL )}
)}

Used in Products

{componentUsage.length}
{usageLoading ? (
Loading...
) : componentUsage.length === 0 ? (
Not used in any product BOM
) : (
Product SKU Qty per Unit Ref. Designator {componentUsage.map((usage) => (
{usage.product_name} {usage.version && ( v{usage.version} )}
{usage.sku} {Number(usage.quantity)} {usage.reference_designator || "-"} e.stopPropagation()} className="inline-flex items-center text-primary hover:text-primary/80" title="View BOM" >
))}
)}
); })()}
{/* Print Label Dialog */} Print Label

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}

); }