'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 { toast, Toaster } from 'react-hot-toast'; import { Package, Plus, Minus, Trash2, AlertTriangle, X, History, LayoutGrid, Wifi, WifiOff, ChevronRight, Edit2, RefreshCw, CloudOff, Sparkles, Smartphone, CheckCircle2, User, Settings, Lock, Shield, Key, LogOut, UserPlus, Tag } from 'lucide-react'; import { clsx, type ClassValue } from 'clsx'; import { twMerge } from 'tailwind-merge'; import axios from 'axios'; 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 [selectedItem, setSelectedItem] = useState(null); 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 [showLogs, setShowLogs] = useState(false); const [auditLogs, setAuditLogs] = useState([]); const [users, setUsers] = useState([]); const [currentUser, setCurrentUser] = useState(null); const [showUserSelect, setShowUserSelect] = useState(false); const [showSettings, setShowSettings] = useState(false); const [selectedUserForLogin, setSelectedUserForLogin] = useState(null); const [loginPassword, setLoginPassword] = useState(''); const [categories, setCategories] = useState([]); useEffect(() => { setMounted(true); const savedUser = localStorage.getItem('inventory_user'); if (savedUser) { setCurrentUser(JSON.parse(savedUser)); } else { setShowUserSelect(true); } // Initial users and categories fetch inventoryApi.getUsers().then(u => setUsers(u)).catch(() => {}); inventoryApi.getCategories().then(c => setCategories(c)).catch(() => {}); }, []); const handleSelectUser = async (user: any) => { // If user has a password (Admin always has one), prompt for it if (user.username === 'Admin' || user.id > 1) { // Assume others might need it too setSelectedUserForLogin(user); setLoginPassword(''); return; } // Auto-login for passwordless users (if any) setCurrentUser(user); localStorage.setItem('inventory_user', JSON.stringify(user)); setShowUserSelect(false); toast.success(`Welcome back, ${user.username}`); }; const handleLogin = async () => { if (!selectedUserForLogin) return; try { const user = await inventoryApi.login({ username: selectedUserForLogin.username, password: loginPassword }); setCurrentUser(user); localStorage.setItem('inventory_user', JSON.stringify(user)); setShowUserSelect(false); setSelectedUserForLogin(null); setLoginPassword(''); toast.success(`Authenticated: ${user.username}`); } catch (error) { toast.error("Invalid password"); } }; const fetchLogs = async () => { if (!isOnline) return; try { const logs = await inventoryApi.getAuditLogs(30); setAuditLogs(logs); } catch (err) { console.error(err); } }; const toggleLogs = async () => { if (!showLogs) { await fetchLogs(); } setShowLogs(!showLogs); }; useEffect(() => { setMounted(true); setIsOnline(navigator.onLine); 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); loadInventory(); preloadOCR(); 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); console.log("OCR Engine Pre-warmed & Ready Offline"); } 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.toUpperCase(); } // 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 upperBarcode = barcode.toUpperCase(); const item = await db.items.where('barcode').equals(barcode) .or('part_number').equals(upperBarcode).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.toUpperCase().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; // Toast only potentially useful scans toast(`Scanning: ${cleanText.substring(0, 30)}...`, { icon: '🔍', duration: 1500, id: 'ocr-scan' }); let bestMatch = null; let maxMatchScore = 0; for (const item of inventory) { let score = 0; const pn = (item.part_number || '').toUpperCase(); const sn = (item.serial_number || '').toUpperCase(); const name = item.name.toUpperCase(); const category = item.category.toUpperCase(); // 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.toUpperCase(); 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) { 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) { 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.specs?.toLowerCase().includes(query) ?? false) || (item.part_number?.toLowerCase().includes(query) ?? false) || (item.color?.toLowerCase().includes(query) ?? false) ); }); if (!mounted) return null; return (
{/* Header */}
TFM aInventory

TFM aInventory

VERSION 1.1.0

{isScannerReady && (
OFFLINE SCAN: OK
)}
SERVER SYNC: {isOnline ? 'OK' : 'NO'}
{/* Mode Switcher */}
{[ { id: 'CHECK_IN', label: 'Check In' }, { id: 'CHECK_OUT', label: 'Check Out' }, { id: 'TRASH', label: 'Trash/Waste' } ].map((m) => ( ))}
{/* Scanner Section */}
{showScanner ? (

Scanning...

) : (

Tap to start scanning

Scan barcodes for routine {mode.toLowerCase().replace('_', ' ')}

)}
{/* Inventory List */}

Catalog Items

setSearchQuery(e.target.value)} className="w-full bg-slate-900 border border-slate-800 rounded-xl py-2 pl-3 pr-10 text-sm focus:border-primary outline-none transition-all" />
🔍
{filteredInventory.map(item => (
{ setSelectedItem(item); setAdjustType('ADD'); }} className="bg-slate-900/40 border border-slate-800/50 p-3 rounded-[1.5rem] flex items-center justify-between group hover:border-primary/40 hover:bg-slate-900/60 active:scale-[0.98] transition-all cursor-pointer shadow-sm" >
{item.category} {item.type && ( {item.type} )} {item.color && (
{item.color} )}

{item.name}

{item.specs}

PN: {item.part_number || 'N/A'}

{item.quantity}

Stock

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

No results for "{searchQuery}"

Try searching by category, specs (e.g. OM4) or color.

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

Edit Metadata

) : (

{selectedItem.name}

)}
{!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 outline-none text-slate-100" />
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-mono outline-none text-slate-100" placeholder="e.g. OM4-TURQ-2M" />
setEditedItem({...editedItem, type: e.target.value})} className="w-full bg-slate-950 border border-slate-800 rounded-xl py-3 px-4 text-sm outline-none text-slate-100" placeholder="e.g. SFP+" />
setEditedItem({...editedItem, specs: e.target.value})} className="w-full bg-slate-950 border border-slate-800 rounded-xl py-3 px-4 text-sm outline-none text-slate-100" />
) : ( <>
{[ { id: 'ADD', label: 'Buy More', icon: Plus, color: 'text-primary' }, { id: 'REMOVE', label: 'Subtract', icon: Minus, color: 'text-amber-500' }, { id: 'TRASH', label: 'Discard', icon: Trash2, color: 'text-red-500' } ].map((t) => ( ))}
{adjustQty}
{adjustType === 'TRASH' && (
Waste Declaration
)}
)}
)} {/* Audit Logs Overlay */} {showLogs && (

Audit History

Live transaction log from cloud

{auditLogs.length === 0 ? (

No transactions found

) : ( auditLogs.map((log) => (

{log.action}

by {log.username || 'System'}
{inventory.find(i => i.id === log.target_item_id)?.name || `Item #${log.target_item_id}`}
{(log.details || log.timestamp) && (

{new Date(log.timestamp).toLocaleString()}

{log.details && ( {log.details} )}
)}

0 ? "text-green-400" : "text-rose-400" )}> {log.quantity_change > 0 ? '+' : ''}{log.quantity_change}

)) )}
)} {/* Footer Branding */}

Powered by TFM Group Software

v1.1.0 • 2026-04-10 • BUILD: dev-84140

{/* Bottom Nav */}
{/* Central Add Button */} {currentUser?.role === 'admin' && ( )}
{/* Settings Drawer (User Management) */} {showSettings && (

System Admin

User Management

{users.map(u => (

{u.username}

{u.role}

{u.username !== 'Admin' && ( )}
))}

Category Groups

{categories.map(cat => (

{cat.name}

{cat.description || 'No description'}

))}

Logout

Exit current session and return to identity check.

)} {/* User Selection Overlay */} {showUserSelect && (

Identity Check

Select operator profile to continue

{!selectedUserForLogin ? ( users.map(user => ( )) ) : (

Logging in as

{selectedUserForLogin.username}

setLoginPassword(e.target.value)} onKeyDown={(e) => e.key === 'Enter' && handleLogin()} className="w-full bg-slate-800/50 border border-slate-800 focus:border-primary rounded-2xl py-4 pl-12 pr-4 text-white focus:outline-none transition-all" placeholder="••••••••" />
)}
{users.length === 0 && (
Initializing users...
)}
)}
); }