feat: add side-by-side item comparison for duplicate Part Numbers
New Component: ItemComparisonModal.tsx - Shows existing vs new item side-by-side - Highlights fields that are different (in yellow) - Options to Update item or Skip (local-only save) - Shows existing item ID and comparison details Backend Changes: - Updated error message to say 'Part Number' not 'barcode' - 409 response includes existing item data for comparison - Clear, user-friendly conflict messaging Frontend Changes: - New state for comparison modal (newItem, existingItem, existingId) - handleOnboardingComplete() shows modal on 409 conflict - handleComparisonUpdate() calls updateItem() API - handleComparisonSkip() saves locally without syncing - Better error handling distinguishes 409 from other failures Workflow: 1. User imports item with Part Number that already exists 2. System shows comparison modal 3. User can: - Update (merges new data into existing) - Skip (saves locally, doesn't sync to cloud)
This commit is contained in:
@@ -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<any | null>(null);
|
||||
const [categories, setCategories] = useState<any[]>([]);
|
||||
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) */}
|
||||
<ItemComparisonModal
|
||||
show={comparisonModal.show}
|
||||
existingItem={comparisonModal.existingItem}
|
||||
newItem={comparisonModal.newItem}
|
||||
onUpdate={handleComparisonUpdate}
|
||||
onSkip={handleComparisonSkip}
|
||||
loading={comparisonLoading}
|
||||
/>
|
||||
|
||||
{/* Stock Adjustment Overlay */}
|
||||
{selectedItem && (
|
||||
<div className="fixed inset-0 z-50 flex items-end sm:items-center justify-center p-4 bg-slate-950/80 animate-in fade-in duration-200">
|
||||
|
||||
Reference in New Issue
Block a user