'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 { useStockAdjustment } from '@/hooks/useStockAdjustment'; import { useSync } from '@/hooks/useSync'; import Scanner from '@/components/Scanner'; import AIOnboarding from '@/components/AIOnboarding'; import PageShell from '@/components/PageShell'; import ItemComparisonModal from '@/components/ItemComparisonModal'; import StockAdjustmentPanel from '@/components/StockAdjustmentPanel'; import NewItemDialog from '@/components/NewItemDialog'; import ScannerSection from '@/components/ScannerSection'; import { toast } from 'react-hot-toast'; import { Package, Camera, Plus, Minus, Trash2, AlertTriangle, X, ChevronRight, Edit2, RefreshCw, 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 [trashReason, setTrashReason] = useState('Damaged'); 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 { syncing, handleSync } = useSync({ isOnline, currentUser, onInventoryUpdate: setInventory }); 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 })); } } }); const { adjustQty, setAdjustQty, adjustType, setAdjustType, handleAdjustStock } = useStockAdjustment({ selectedItem, isOnline, currentUser, onAdjustmentComplete: () => { setSelectedItem(null); }, onInventoryUpdate: setInventory }); 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 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 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 [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'}
setMode(newMode as any)} showScanner={showScanner} onShowScanner={setShowScanner} onScanSuccess={onScanSuccess} onOCRMatch={onOCRMatch} onAddItemClick={() => setShowOnboarding(true)} /> {/* Onboarding Overlay */} {showOnboarding && ( setShowOnboarding(false)} onComplete={handleOnboardingComplete} /> )} {/* Item Comparison Modal (for duplicate Part Numbers) */} {/* Stock Adjustment Panel */} { setSelectedItem(null); setIsEditing(false); }} onEdit={(item) => { setEditedItem(item); setIsEditing(true); }} onEditChange={setEditedItem} onQuantityChange={setAdjustQty} onTypeChange={setAdjustType} onReasonChange={setTrashReason} onShowScanner={(active, field) => { setFieldScanning({ active, field }); setShowScanner(true); }} onAdjustStock={handleAdjustStock} onUpdateItem={handleUpdateItem} onDeleteItem={handleDeleteItem} /> {/* Box Contents Selection Modal */} {boxMatches.length > 0 && !selectedItem && (

Box Contents

Select the item you want to {mode.replace('_', ' ').toLowerCase()}

{boxMatches.map(item => ( ))}
)} {/* Footer Branding */}
); }