'use client'; import { useState, useEffect, useCallback } from 'react'; import { db, Item } from '@/lib/db'; import { inventoryApi, getBackendUrl } from '@/lib/api'; import PageShell from '@/components/PageShell'; import Scanner from '@/components/Scanner'; import StatCard from '@/components/StatCard'; import InventoryTable from '@/components/InventoryTable'; import FilterBar from '@/components/FilterBar'; import SearchModal from '@/components/inventory/SearchModal'; import QuantityAdjustmentModal from '@/components/inventory/QuantityAdjustmentModal'; import { useInventoryFilter } from '@/hooks/useInventoryFilter'; import { toast } from 'react-hot-toast'; import { Package, BarChart3, Layers, Plus, Minus, Trash2, X, AlertTriangle, Tag, Edit2, Camera, Layout, Printer, Download, Box, 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 [currentUser, setCurrentUser] = useState(null); const [backendUrl, setBackendUrl] = useState(''); const { searchQuery, setSearchQuery, expandedCategory, setExpandedCategory, boxSearchQuery, setBoxSearchQuery, categories, filteredCategories, getFilteredItems, getFilteredBoxes } = useInventoryFilter(inventory); // 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); // Search Modal State const [showSearchModal, setShowSearchModal] = useState(false); const [selectedSearchItem, setSelectedSearchItem] = useState(null); const [showQuantityModal, setShowQuantityModal] = useState(false); useEffect(() => { setMounted(true); const savedUser = localStorage.getItem('inventory_user'); if (savedUser) { setCurrentUser(JSON.parse(savedUser)); } getBackendUrl().then(setBackendUrl); 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.trim().length === 0) { throw new Error("Part number cannot be empty"); } 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]); const handleSearchItemSelect = (item: Item) => { setSelectedSearchItem(item); setShowQuantityModal(true); }; const handleQuantityModalClose = () => { setShowQuantityModal(false); setSelectedSearchItem(null); }; // 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 */}
{/* Search */} {/* Inventory Table */} { const categoryObj = categoriesList.find(c => c.name === cat); if (categoryObj) { setEditingCategory(categoryObj); setCatEditedName(categoryObj.name); setCatEditedDesc(categoryObj.description || ''); } }} categoriesList={categoriesList} backendUrl={backendUrl} />
{/* 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-black border border-[#222222] focus:border-primary px-3 py-2 rounded-none" />
setEditedItem({...editedItem, part_number: e.target.value})} className="w-full bg-black border border-[#222222] focus:border-primary px-3 py-2 rounded-none" />
setEditedItem({ ...editedItem, category: e.target.value })} className="w-full bg-black border border-[#222222] focus:border-primary px-3 py-2 rounded-none" placeholder="e.g. Storage" /> {categoriesList.map(c => (
setEditedItem({...editedItem, type: e.target.value})} className="w-full bg-black border border-[#222222] focus:border-primary px-3 py-2 rounded-none" />
setEditedItem({...editedItem, connector: e.target.value})} className="w-full bg-black border border-[#222222] focus:border-primary px-3 py-2 rounded-none" placeholder="e.g. LC/UPC" />
setEditedItem({...editedItem, size: e.target.value})} className="w-full bg-black border border-[#222222] focus:border-primary px-3 py-2 rounded-none" placeholder="e.g. 1.6TB" />
setEditedItem({...editedItem, color: e.target.value})} className="w-full bg-black border border-[#222222] focus:border-primary px-3 py-2 rounded-none" placeholder="e.g. Blue" />
setEditedItem({...editedItem, box_label: e.target.value})} className="w-full pr-12 bg-black border border-[#222222] focus:border-primary px-3 py-2 rounded-none" placeholder="e.g. Box 5" />