'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 { 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 [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'}
{/* 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 ? (