'use client'; import { useState, useEffect, useCallback } from 'react'; import { db, Item } from '@/lib/db'; import { inventoryApi } from '@/lib/api'; import { fetchAndCacheItems, syncOfflineOperations } from '@/lib/sync'; import Scanner from '@/components/Scanner'; import AIOnboarding from '@/components/AIOnboarding'; import PageShell from '@/components/PageShell'; import { toast } from 'react-hot-toast'; import { Package, Camera, Plus, Minus, Trash2, AlertTriangle, X, ChevronRight, Edit2, RefreshCw, Sparkles, Smartphone, ArrowDownCircle, ArrowUpCircle, Search } from 'lucide-react'; import { generateBarcode128, getQRCodeURL } from '@/lib/labels'; import { clsx, type ClassValue } from 'clsx'; import { twMerge } from 'tailwind-merge'; import axios from 'axios'; import versionData from '../VERSION.json'; interface User { id: number; username: string; role: string; } function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); } export default function Home() { const [mounted, setMounted] = useState(false); const [isOnline, setIsOnline] = useState(true); const [mode, setMode] = useState<'CHECK_IN' | 'CHECK_OUT' | 'TRASH'>('CHECK_OUT'); const [showScanner, setShowScanner] = useState(false); const [showOnboarding, setShowOnboarding] = useState(false); const [selectedBoxLabel, setSelectedBoxLabel] = useState(null); const [selectedItem, setSelectedItem] = useState(null); const [boxMatches, setBoxMatches] = useState([]); const [isEditing, setIsEditing] = useState(false); const [isScannerReady, setIsScannerReady] = useState(false); const [editedItem, setEditedItem] = useState>({}); const [adjustQty, setAdjustQty] = useState(1); const [adjustType, setAdjustType] = useState<'ADD' | 'REMOVE' | 'TRASH'>('ADD'); const [trashReason, setTrashReason] = useState('Damaged'); const [lastScanned, setLastScanned] = useState(null); const [inventory, setInventory] = useState([]); const [syncing, setSyncing] = useState(false); const [currentUser, setCurrentUser] = useState(null); const [categories, setCategories] = useState([]); const [fieldScanning, setFieldScanning] = useState<{ active: boolean, field: string } | null>(null); useEffect(() => { if (!localStorage.getItem('inventory_token')) { window.location.href = '/login'; return; } setMounted(true); setIsOnline(navigator.onLine); const savedUser = localStorage.getItem('inventory_user'); if (savedUser) { setCurrentUser(JSON.parse(savedUser)); } // Initial data fetch inventoryApi.getCategories().then(c => setCategories(c)).catch(() => { }); loadInventory(); preloadOCR(); const handleOnline = () => { setIsOnline(true); handleSync(); // Auto-sync pending ops when back online }; const handleOffline = () => setIsOnline(false); window.addEventListener('online', handleOnline); window.addEventListener('offline', handleOffline); // Active polling for network status AND periodic background refresh (30s) const interval = setInterval(() => { setIsOnline(navigator.onLine); if (navigator.onLine) { loadInventory(); // Keep other devices in sync } }, 30000); return () => { window.removeEventListener('online', handleOnline); window.removeEventListener('offline', handleOffline); clearInterval(interval); }; }, []); const loadInventory = async () => { const cached = await db.items.toArray(); setInventory(cached); if (navigator.onLine) { const fresh = await fetchAndCacheItems(); setInventory(fresh); } }; const preloadOCR = async () => { try { // Import the library only when needed to keep bundle small const { createWorker } = await import('tesseract.js'); const worker = await createWorker('eng'); await worker.terminate(); setIsScannerReady(true); } catch (e) { console.warn("OCR Preload failed - will retry on demand", e); } }; const handleOnboardingComplete = async (itemData: any) => { try { if (itemData.part_number) { itemData.part_number = itemData.part_number.toLowerCase(); } // 1. Add to local DB cache await db.items.add(itemData); // 2. If online, try to push to backend immediately if (isOnline) { await inventoryApi.createItem(1, itemData); toast.success("Item saved to cloud catalog!"); } else { toast.success("Item saved locally. Will sync when online."); } setShowOnboarding(false); await loadInventory(); } catch (error) { console.error(error); toast.error("Failed to save item"); } }; const handleSync = useCallback(async () => { if (!isOnline || !currentUser) return; setSyncing(true); try { const result = await syncOfflineOperations(currentUser.id); if (result.success > 0) { toast.success(`Synced ${result.success} operations!`); } await loadInventory(); } catch (error) { console.error("Sync failed", error); } finally { setSyncing(false); } }, [isOnline, currentUser]); const onScanSuccess = useCallback(async (barcode: string) => { setLastScanned(barcode); setShowScanner(false); const normalizedBarcode = barcode.toLowerCase(); const item = await db.items.where('barcode').equals(barcode) .or('part_number').equals(normalizedBarcode).first(); if (!item) { toast.error(`Item ${barcode} not found in catalog.`); return; } await db.pendingOperations.add({ type: mode, barcode: item.barcode, quantity: 1, timestamp: Date.now(), synced: 0, uuid: crypto.randomUUID() }); const newQty = mode === 'CHECK_IN' ? item.quantity + 1 : item.quantity - 1; await db.items.update(item.id!, { quantity: newQty }); setInventory(prev => prev.map(i => i.id === item.id ? { ...i, quantity: newQty } : i)); toast.success(`${mode === 'CHECK_IN' ? 'Checked in' : 'Checked out'} ${item.name}`); if (isOnline) { handleSync(); } }, [mode, isOnline, handleSync]); const onOCRMatch = useCallback(async (text: string) => { // 1. Clean and normalize const cleanText = text.toLowerCase().replace(/[^a-z0-9\s/+-]/g, ' '); // Garbage Filter: Ignore noisy strings (measurements like 0.11, dates like 2024-07-25) // We only keep tokens that are at least 3 chars AND not just decimals const tokens = cleanText.split(/[\s\n,]+/) .filter(t => t.length >= 3) .filter(t => !/^\d+\.\d+$/.test(t)) // Filter out decimals like 0.11 or 0.12 .filter(t => !/^\d{2,4}-\d{2}-\d{2}$/.test(t)); // Filter out dates if (tokens.length === 0) return; // [NEW] Targeted Field Scan Logic if (fieldScanning?.active) { if (fieldScanning.field === 'box_label') { const potentialLabel = tokens[0] || cleanText; setEditedItem(prev => ({ ...prev, box_label: potentialLabel })); setFieldScanning(null); toast.success(`Captured: ${potentialLabel}`); return; } } // Toast only potentially useful scans toast(`Scanning: ${cleanText.substring(0, 30)}...`, { icon: '🔍', duration: 1500, id: 'ocr-scan' }); // [BOX SCANNING LOGIC] Prioritize checking if this is a known box const possibleBoxItems = inventory.filter(item => { if (!item.box_label) return false; const boxText = item.box_label.toLowerCase().replace(/[^a-z0-9\s/+-]/g, ' '); // Exact or Partial include match for box label if (cleanText.includes(boxText)) return true; // Strong token matching (for words >= 4 chars) const boxTokens = boxText.split(/[\s/+-]/).filter(t => t.length >= 4); const matchedTokens = boxTokens.filter(bt => tokens.includes(bt)); return matchedTokens.length >= 2 || (boxTokens.length === 1 && matchedTokens.length === 1); }); if (possibleBoxItems.length === 1) { toast.success(`Box identified: 1 item found`, { duration: 3000, id: 'ocr-success' }); setSelectedItem(possibleBoxItems[0]); setAdjustType(mode === 'CHECK_IN' ? 'ADD' : 'REMOVE'); setShowScanner(false); return; } else if (possibleBoxItems.length > 1) { toast.success(`Box identified: ${possibleBoxItems.length} items found`, { duration: 3000, id: 'ocr-success' }); setBoxMatches(possibleBoxItems); setShowScanner(false); return; } // [INDIVIDUAL ITEM MATCHING] Fallback to classic specific identification let bestMatch = null; let maxMatchScore = 0; for (const item of inventory) { let score = 0; const pn = (item.part_number || '').toLowerCase(); const sn = (item.serial_number || '').toLowerCase(); const name = item.name.toLowerCase(); const category = item.category.toLowerCase(); const ocrKey = (item.ocr_text || '').toLowerCase().replace(/[^a-z0-9\s/+-]/g, ' '); // Priority 0: Exact OCR Key match (Heuristic provided by AI) if (ocrKey && cleanText.includes(ocrKey)) score += 1000; // Priority 1: Serial Number (Absolute match) if (sn && cleanText.includes(sn)) score += 500; // Priority 2: Part Number (High confidence) if (pn && cleanText.includes(pn)) score += 200; // Priority 3: Token based matching for PN (LC/UPC etc) if (pn) { const pnTokens = pn.split(/[\s/+-]/).filter(t => t.length >= 3); pnTokens.forEach(t => { if (cleanText.includes(t)) score += 50; }); } // Priority 4: Name & Category const nameTokens = name.split(/[\s/+-]/).filter(t => t.length >= 3); nameTokens.forEach(t => { if (cleanText.includes(t)) score += 10; }); if (category && cleanText.includes(category)) score += 20; if (score > maxMatchScore) { maxMatchScore = score; bestMatch = item; } } // Threshold: Need a significant score to match automatically if (bestMatch && maxMatchScore >= 40) { toast.success(`Matched: ${bestMatch.name}`, { duration: 3000, id: 'ocr-success' }); setSelectedItem(bestMatch); setAdjustType(mode === 'CHECK_IN' ? 'ADD' : 'REMOVE'); setShowScanner(false); } }, [mode, inventory]); const handleUpdateItem = async () => { if (!selectedItem) return; try { const updated = { ...selectedItem, ...editedItem }; // Normalize PN if (updated.part_number) updated.part_number = updated.part_number.toLowerCase(); await db.items.update(selectedItem.id!, updated); if (isOnline) { await inventoryApi.updateItem(selectedItem.id!, updated); } toast.success("Item updated successfully"); setIsEditing(false); setSelectedItem(updated as Item); await loadInventory(); } 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}" completely from the catalog?`)) return; try { await db.items.delete(selectedItem.id); if (isOnline) { await inventoryApi.deleteItem(selectedItem.id); } toast.success("Item deleted from catalog"); setSelectedItem(null); await loadInventory(); } catch (err: any) { console.error(err); toast.error("Failed to delete item"); } }; const handleAdjustStock = async () => { if (!selectedItem) return; const toastId = toast.loading("Processing..."); try { const finalAdjustQty = adjustQty; const newQty = selectedItem.quantity + (adjustType === 'ADD' ? adjustQty : -adjustQty); // 1. Create a unique ID for this operation to prevent double-counting on server const operationId = crypto.randomUUID(); // 2. Queue local operation const opType = adjustType === 'ADD' ? 'CHECK_IN' : (adjustType === 'TRASH' ? 'TRASH' : 'CHECK_OUT'); await db.pendingOperations.add({ type: opType as any, barcode: selectedItem.barcode, quantity: finalAdjustQty, timestamp: Date.now(), synced: 0, // We add this to our DB even if it doesn't have it yet, Dexie handles it uuid: operationId } as any); // 3. Update local UI & DB await db.items.update(selectedItem.id!, { quantity: newQty }); setInventory(prev => prev.map(i => i.id === selectedItem.id ? { ...i, quantity: newQty } : i)); // 4. Trigger Sync if (isOnline) { await handleSync(); toast.success("Inventory updated & synced", { id: toastId }); } else { 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 [searchQuery, setSearchQuery] = useState(''); const filteredInventory = inventory.filter(item => { const query = searchQuery.toLowerCase(); return ( item.name.toLowerCase().includes(query) || item.category.toLowerCase().includes(query) || (item.description?.toLowerCase().includes(query) ?? false) || (item.connector?.toLowerCase().includes(query) ?? false) || (item.size?.toLowerCase().includes(query) ?? false) || (item.part_number?.toLowerCase().includes(query) ?? false) || (item.color?.toLowerCase().includes(query) ?? false) || (item.box_label?.toLowerCase().includes(query) ?? false) || (item.ocr_text?.toLowerCase().includes(query) ?? false) ); }); // 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 (
{/* Search datalists for autocomplete */} {existingTypes.map(t => {existingBoxes.map(b => {/* Header */}
TFM aInventory

Inventory Control

Check-in, Check-out & Trash Operations

{isScannerReady && (
Scanner: OK
)}
Sync: {isOnline ? 'Active' : 'Offline'}
{/* Mode Switcher */}
{[ { id: 'CHECK_IN', label: 'Check In', icon: ArrowDownCircle }, { id: 'CHECK_OUT', label: 'Check Out', icon: ArrowUpCircle }, { id: 'TRASH', label: 'Trash', icon: Trash2 } ].map((m) => ( ))}
{/* Scanner Section */}
{showScanner ? (

scanning...

) : (
)}
{/* Onboarding Overlay */} {showOnboarding && ( setShowOnboarding(false)} onComplete={handleOnboardingComplete} /> )} {/* Stock Adjustment Overlay */} {selectedItem && (

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

{!isEditing && ( )} {isEditing && ( )}
{isEditing ? (