'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 { useScanner } from '@/hooks/useScanner'; import Scanner from '@/components/Scanner'; import AIOnboarding from '@/components/AIOnboarding'; import PageShell from '@/components/PageShell'; import ItemComparisonModal from '@/components/ItemComparisonModal'; 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 [inventory, setInventory] = useState([]); 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 [editedItem, setEditedItem] = useState>({}); const [adjustQty, setAdjustQty] = useState(1); const [adjustType, setAdjustType] = useState<'ADD' | 'REMOVE' | 'TRASH'>('ADD'); const [trashReason, setTrashReason] = useState('Damaged'); const [syncing, setSyncing] = useState(false); const [currentUser, setCurrentUser] = useState(null); const [categories, setCategories] = useState([]); const [comparisonModal, setComparisonModal] = useState<{ show: boolean, newItem: any, existingItem: any, existingId: number | null }>({ show: false, newItem: null, existingItem: null, existingId: null }); const [comparisonLoading, setComparisonLoading] = useState(false); const { mode, setMode, showScanner, setShowScanner, lastScanned, isScannerReady, fieldScanning, setFieldScanning, onScanSuccess, onOCRMatch, } = useScanner({ inventory, isOnline, onSync: handleSync, onMatchFound: (item, adjustType) => { setSelectedItem(item); setAdjustType(adjustType); }, onMultipleMatches: (items) => { setBoxMatches(items); }, onFieldCapture: (field, value) => { if (field === 'box_label') { setEditedItem(prev => ({ ...prev, box_label: value })); } } }); 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(); const handleOnline = () => { setIsOnline(true); }; 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 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 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 && currentUser) { try { await inventoryApi.createItem(currentUser.id, itemData); toast.success("Item saved to cloud catalog!"); setShowOnboarding(false); await loadInventory(); } catch (err: any) { const status = err.response?.status; const responseData = err.response?.data; if (status === 409 && responseData?.detail) { // Show comparison modal for duplicate part number const detail = responseData.detail; setComparisonModal({ show: true, newItem: itemData, existingItem: detail.existing_item, existingId: detail.existing_id }); return; // Don't close onboarding, user will decide } else { console.error("Cloud sync failed:", err.message); toast.error("Item saved locally. Cloud sync failed."); setShowOnboarding(false); await loadInventory(); } } } else if (!isOnline) { toast.success("Item saved locally. Will sync when online."); setShowOnboarding(false); await loadInventory(); } else { toast("Please log in to save to cloud.", { icon: '⚠️' }); } } catch (error) { console.error(error); toast.error("Failed to save item"); } }; const handleComparisonUpdate = async () => { if (!comparisonModal.existingId) return; setComparisonLoading(true); try { // Update existing item await inventoryApi.updateItem(comparisonModal.existingId, comparisonModal.newItem); toast.success("Item updated successfully!"); setComparisonModal({ show: false, newItem: null, existingItem: null, existingId: null }); setShowOnboarding(false); await loadInventory(); } catch (err: any) { console.error("Update failed:", err.message); toast.error("Failed to update item"); } finally { setComparisonLoading(false); } }; const handleComparisonSkip = () => { // Item was already added to local DB, just close modal and onboarding setComparisonModal({ show: false, newItem: null, existingItem: null, existingId: null }); setShowOnboarding(false); toast("Local copy saved. Not synced to cloud.", { icon: '💾' }); loadInventory(); }; 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 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

TFM aInventory

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} /> )} {/* Item Comparison Modal (for duplicate Part Numbers) */} {/* Stock Adjustment Overlay */} {selectedItem && (

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

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