'use client'; import { useState, useEffect, useCallback } from 'react'; import { db, Item } from '@/lib/db'; import { inventoryApi } from '@/lib/api'; import PageShell from '@/components/PageShell'; import Scanner from '@/components/Scanner'; import { toast } from 'react-hot-toast'; import { Package, ChevronRight, ChevronDown, BarChart3, Layers, Plus, Minus, Trash2, X, AlertTriangle, Tag, Edit2, Camera, Layout, Printer, Download, Search } from 'lucide-react'; import { generateBarcode128, getQRCodeURL } from '@/lib/labels'; import { clsx, type ClassValue } from 'clsx'; import { twMerge } from 'tailwind-merge'; function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); } export default function InventoryPage() { const [mounted, setMounted] = useState(false); const [inventory, setInventory] = useState([]); const [stats, setStats] = useState(null); const [expandedCategory, setExpandedCategory] = useState(null); const [searchQuery, setSearchQuery] = useState(''); const [currentUser, setCurrentUser] = useState(null); // Stock Adjustment State const [selectedItem, setSelectedItem] = useState(null); const [adjustQty, setAdjustQty] = useState(1); const [adjustType, setAdjustType] = useState<'ADD' | 'REMOVE' | 'TRASH'>('ADD'); const [trashReason, setTrashReason] = useState('Damaged'); // Item Editing state const [isEditing, setIsEditing] = useState(false); const [editedItem, setEditedItem] = useState>({}); const [categoriesList, setCategoriesList] = useState([]); // Category Editing state const [editingCategory, setEditingCategory] = useState(null); const [catEditedName, setCatEditedName] = useState(''); const [catEditedDesc, setCatEditedDesc] = useState(''); // Scanner state const [showScanner, setShowScanner] = useState(false); const [fieldScanning, setFieldScanning] = useState<{ active: boolean, field: string } | null>(null); // Box Manager State const [showBoxManager, setShowBoxManager] = useState(false); const [selectedBoxLabel, setSelectedBoxLabel] = useState(null); const [boxSearchQuery, setBoxSearchQuery] = useState(''); useEffect(() => { setMounted(true); const savedUser = localStorage.getItem('inventory_user'); if (savedUser) { setCurrentUser(JSON.parse(savedUser)); } loadData(); }, []); const loadData = async () => { // Load local items const cached = await db.items.toArray(); setInventory(cached); try { // Load backend stats const s = await inventoryApi.getStats(); setStats(s); const cats = await inventoryApi.getCategories(); setCategoriesList(cats); // Load fresh items const res = await inventoryApi.getItems(); setInventory(res); // Sync local DB await db.items.clear(); await db.items.bulkPut(res); } catch (err: any) { console.error("Failed to load backend data", err); } }; const handleAdjustStock = async () => { if (!selectedItem) return; const toastId = toast.loading("Processing..."); try { const isOnline = navigator.onLine; const finalAdjustQty = adjustQty; const newQty = selectedItem.quantity + (adjustType === 'ADD' ? adjustQty : -adjustQty); // Local Update await db.items.update(selectedItem.id!, { quantity: newQty }); setInventory(prev => prev.map(i => i.id === selectedItem.id ? { ...i, quantity: newQty } : i)); if (isOnline) { const endpoint = adjustType === 'ADD' ? 'check-in' : (adjustType === 'TRASH' ? 'trash' : 'check-out'); const payload: any = { barcode: selectedItem.barcode, quantity: finalAdjustQty, user_id: currentUser?.id || 1 }; if (adjustType === 'TRASH') payload.reason = trashReason; await inventoryApi.adjustStock(endpoint, payload); toast.success("Inventory updated & synced", { id: toastId }); } else { await db.pendingOperations.add({ type: adjustType === 'ADD' ? 'CHECK_IN' : (adjustType === 'TRASH' ? 'TRASH' : 'CHECK_OUT'), barcode: selectedItem.barcode, quantity: finalAdjustQty, timestamp: Date.now(), synced: 0, uuid: crypto.randomUUID() } as any); toast.success("Saved locally (Offline)", { id: toastId }); } setSelectedItem(null); setAdjustQty(1); } catch (error: any) { console.error("Adjustment failure:", error); toast.error("Error saving operation", { id: toastId }); } }; const handleUpdateItem = async () => { if (!selectedItem) return; try { const updated = { ...selectedItem, ...editedItem }; if (updated.part_number) updated.part_number = updated.part_number.toUpperCase(); await db.items.update(selectedItem.id!, updated); if (navigator.onLine) { await inventoryApi.updateItem(selectedItem.id!, updated); } toast.success("Item updated successfully"); setIsEditing(false); setSelectedItem(updated as Item); await loadData(); } catch (err: any) { console.error(err); toast.error("Failed to update item"); } }; const handleDeleteItem = async () => { if (!selectedItem || !selectedItem.id) return; if (!window.confirm(`Are you sure you want to delete "${selectedItem.name}"?`)) return; try { await db.items.delete(selectedItem.id); if (navigator.onLine) { await inventoryApi.deleteItem(selectedItem.id); } toast.success("Item deleted"); setSelectedItem(null); await loadData(); } catch (err: any) { console.error(err); toast.error("Failed to delete item"); } }; const handleUpdateCategory = async () => { if (!editingCategory) return; try { await inventoryApi.updateCategory(editingCategory.id, { name: catEditedName, description: catEditedDesc }); toast.success("Category updated"); setEditingCategory(null); await loadData(); } catch (err: any) { toast.error("Update failed"); } }; const onOCRMatch = useCallback(async (text: string) => { const cleanText = text.toUpperCase().replace(/[^A-Z0-9\s/+-]/g, ' '); const tokens = cleanText.split(/[\s\n,]+/).filter(t => t.length >= 3); if (fieldScanning?.active && fieldScanning.field === 'box_label') { const label = tokens[0] || cleanText; setEditedItem(prev => ({ ...prev, box_label: label })); setFieldScanning(null); setShowScanner(false); toast.success(`Captured: ${label}`); return; } }, [fieldScanning]); const onScanSuccess = useCallback((barcode: string) => { // Inventory page doesn't do check-in via scanner, it just finds the item const item = inventory.find(i => i.barcode === barcode); if (item) { setSelectedItem(item); setShowScanner(false); } else { toast.error(`Item with barcode ${barcode} not found in catalog`); } }, [inventory]); // Group items by category const categories = Array.from(new Set(inventory.map(i => i.category))); const filteredCategories = categories.filter(c => c.toLowerCase().includes(searchQuery.toLowerCase()) || inventory.some(i => i.category === c && i.name.toLowerCase().includes(searchQuery.toLowerCase())) ); // Extract unique item types and box labels for suggestions const existingTypes = Array.from(new Set(inventory.map(i => i.type).filter(Boolean))).sort() as string[]; const existingBoxes = Array.from(new Set(inventory.map(i => i.box_label).filter(Boolean))).sort() as string[]; if (!mounted) return null; return (
{existingTypes.map(t => {existingBoxes.map(b =>

Inventory Catalog

Enterprise Stock Overview

{/* Stats Dashboard */}

Categories

{stats?.total_categories || categories.length}

Item Types

{stats?.total_items || inventory.length}

Total Boxes

{existingBoxes.length}

{/* Search */}
setSearchQuery(e.target.value)} className="w-full bg-slate-900 border border-slate-800 rounded-2xl py-4 pl-12 pr-4 text-sm focus:border-primary outline-none transition-all" />
🔍
{/* Categorized List (Accordion) */}
{filteredCategories.map(cat => (
setExpandedCategory(expandedCategory === cat ? null : cat)} >

{cat}

{inventory.filter(i => i.category === cat).length} Item types in stock

{expandedCategory === cat ? : }
{expandedCategory === cat && (
{inventory .filter(i => i.category === cat) .filter(i => i.name.toLowerCase().includes(searchQuery.toLowerCase())) .map(item => (
setSelectedItem(item)} className="bg-slate-950/40 border border-slate-800/50 p-4 rounded-2xl flex items-center justify-between hover:border-primary/40 cursor-pointer transition-all active:scale-[0.98]" >

{item.name}

{item.specs}

{item.quantity}

Stock

))}
)}
))} {filteredCategories.length === 0 && (

No results found

)}
{/* Stock Adjustment / Edit Item Overlay */} {selectedItem && (

{isEditing ? "Edit Item" : selectedItem.name} {!isEditing && ( In Stock: {selectedItem.quantity} )}

{!isEditing && ( )} {isEditing && ( )}
{isEditing ? (
setEditedItem({...editedItem, name: e.target.value})} className="w-full bg-slate-950 border border-slate-800 rounded-xl py-3 px-4 text-sm font-bold outline-none text-slate-100 placeholder:text-slate-700" />
setEditedItem({...editedItem, part_number: e.target.value})} className="w-full bg-slate-950 border border-slate-800 rounded-xl py-3 px-4 text-sm font-bold outline-none text-slate-100 placeholder:text-slate-700 uppercase" />
setEditedItem({ ...editedItem, category: e.target.value })} className="w-full bg-slate-950 border border-slate-800 rounded-xl py-3 px-4 text-sm font-bold outline-none text-slate-100 placeholder:text-slate-700" placeholder="e.g. storage" /> {categoriesList.map(c => (
setEditedItem({...editedItem, type: e.target.value})} className="w-full bg-slate-950 border border-slate-800 rounded-xl py-3 px-4 text-sm font-bold outline-none text-slate-100" />
setEditedItem({...editedItem, connector: e.target.value})} className="w-full bg-slate-950 border border-slate-800 rounded-xl py-3 px-4 text-sm font-bold outline-none text-slate-100" placeholder="e.g. LC/UPC" />
setEditedItem({...editedItem, size: e.target.value})} className="w-full bg-slate-950 border border-slate-800 rounded-xl py-3 px-4 text-sm font-bold outline-none text-slate-100" placeholder="e.g. 1.6TB" />
setEditedItem({...editedItem, color: e.target.value})} className="w-full bg-slate-950 border border-slate-800 rounded-xl py-3 px-4 text-sm font-bold outline-none text-slate-100" placeholder="e.g. Blue" />
setEditedItem({...editedItem, box_label: e.target.value})} className="w-full bg-slate-950 border border-slate-800 rounded-xl py-3 pl-4 pr-12 text-sm font-bold outline-none text-slate-100 placeholder:text-slate-700 focus:border-primary transition-colors" placeholder="e.g. Box 5" />