diff --git a/backend/routers/items.py b/backend/routers/items.py index 20815d37..3e13f8f1 100644 --- a/backend/routers/items.py +++ b/backend/routers/items.py @@ -97,13 +97,18 @@ def create_item( current_user: auth.TokenData = Depends(auth.get_current_user) ): """[C-01] Create item — only for authenticated users. [M-02] user_id from token.""" - # [DUPLICATE CHECK] Prevent duplicate barcodes (part numbers) + # [DUPLICATE CHECK] Prevent duplicate part numbers if item.barcode: existing = db.query(models.Item).filter(models.Item.barcode == item.barcode).first() if existing: + # Return existing item data for user comparison raise HTTPException( status_code=409, - detail=f"Item with barcode '{item.barcode}' already exists (ID: {existing.id}). Update it instead or use a different barcode." + detail={ + "message": f"Item with Part Number '{item.barcode}' already exists in inventory.", + "existing_id": existing.id, + "existing_item": schemas.Item.model_validate(existing).model_dump() + } ) # [AUTO-PERSIST] Create Category/Color if not exists diff --git a/frontend/app/page.tsx b/frontend/app/page.tsx index 1ee39f23..91e12aa2 100644 --- a/frontend/app/page.tsx +++ b/frontend/app/page.tsx @@ -7,6 +7,7 @@ import { fetchAndCacheItems, syncOfflineOperations } from '@/lib/sync'; 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, @@ -90,6 +91,8 @@ export default function Home() { const [currentUser, setCurrentUser] = useState(null); const [categories, setCategories] = useState([]); const [fieldScanning, setFieldScanning] = useState<{ active: boolean, field: string } | null>(null); + 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); useEffect(() => { if (!localStorage.getItem('inventory_token')) { @@ -169,31 +172,68 @@ export default function Home() { 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 detail = err.response?.data?.detail || err.message; - console.error("Cloud sync failed:", detail); + const responseData = err.response?.data; - if (status === 409) { - toast.error(`Item already exists. ${detail}`); + 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: '⚠️' }); } - - setShowOnboarding(false); - await loadInventory(); } 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); @@ -616,6 +656,16 @@ export default function Home() { /> )} + {/* Item Comparison Modal (for duplicate Part Numbers) */} + + {/* Stock Adjustment Overlay */} {selectedItem && (
diff --git a/frontend/components/ItemComparisonModal.tsx b/frontend/components/ItemComparisonModal.tsx new file mode 100644 index 00000000..9e98163b --- /dev/null +++ b/frontend/components/ItemComparisonModal.tsx @@ -0,0 +1,123 @@ +'use client'; + +import { AlertTriangle, X, RefreshCw, SkipForward } from 'lucide-react'; + +interface ItemComparisonModalProps { + show: boolean; + existingItem: any; + newItem: any; + onUpdate: () => Promise; + onSkip: () => void; + loading?: boolean; +} + +export default function ItemComparisonModal({ + show, + existingItem, + newItem, + onUpdate, + onSkip, + loading = false +}: ItemComparisonModalProps) { + if (!show) return null; + + const fields = [ + { key: 'name', label: 'Name' }, + { key: 'type', label: 'Type' }, + { key: 'description', label: 'Description' }, + { key: 'category', label: 'Category' }, + { key: 'connector', label: 'Connector' }, + { key: 'size', label: 'Size' }, + { key: 'color', label: 'Color' }, + { key: 'part_number', label: 'Part Number' }, + { key: 'ocr_text', label: 'OCR Key' }, + ]; + + const isDifferent = (key: string) => { + const existing = String(existingItem?.[key] || '').trim(); + const newVal = String(newItem?.[key] || '').trim(); + return existing !== newVal && newVal !== ''; + }; + + const hasChanges = fields.some(f => isDifferent(f.key)); + + return ( +
+
+
+ +
+

Part Number Already Exists

+

Compare the versions below

+
+
+ + {/* Comparison Table */} +
+
+
Field
+
Current (ID: {existingItem?.id})
+
New (Import)
+
+ + {fields.map(field => { + const existing = String(existingItem?.[field.key] || '').trim() || '—'; + const newVal = String(newItem?.[field.key] || '').trim() || '—'; + const different = isDifferent(field.key); + + return ( +
+
{field.label}
+
+ {existing} +
+
+ {newVal} +
+
+ ); + })} +
+ + {!hasChanges && ( +
+

✓ Items are identical. No update needed.

+
+ )} + + {/* Action Buttons */} +
+ + {hasChanges && ( + + )} +
+
+
+ ); +}