From e43abdafd07f28c720c95412482e97b31fa84368 Mon Sep 17 00:00:00 2001 From: Matt Date: Thu, 2 Apr 2026 14:04:56 -0400 Subject: [PATCH] Image upload tweaks/fixes --- .../steps/ImageUploadStep/ImageUploadStep.tsx | 8 ++++- .../hooks/useBulkImageUpload.ts | 32 ++++++++++------- .../hooks/useProductImageOperations.ts | 36 ++++++++++++++----- 3 files changed, 54 insertions(+), 22 deletions(-) diff --git a/inventory/src/components/product-import/steps/ImageUploadStep/ImageUploadStep.tsx b/inventory/src/components/product-import/steps/ImageUploadStep/ImageUploadStep.tsx index 9e43396..44e68ff 100644 --- a/inventory/src/components/product-import/steps/ImageUploadStep/ImageUploadStep.tsx +++ b/inventory/src/components/product-import/steps/ImageUploadStep/ImageUploadStep.tsx @@ -329,7 +329,13 @@ export const ImageUploadStep = ({ handleAddImageFromUrl(index, urlInputs[index]); } }} - onImageUpload={(files: FileList | File[]) => handleImageUpload(files, index)} + onImageUpload={async (files: FileList | File[]) => { + const { skipped } = await handleImageUpload(files, index); + if (skipped > 0) { + const label = product.name || `Product #${index + 1}`; + toast.info(`Skipped ${skipped} duplicate ${skipped === 1 ? 'image' : 'images'} for ${label}`); + } + }} onDragOver={(e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); diff --git a/inventory/src/components/product-import/steps/ImageUploadStep/hooks/useBulkImageUpload.ts b/inventory/src/components/product-import/steps/ImageUploadStep/hooks/useBulkImageUpload.ts index 89e0c9e..8c04a77 100644 --- a/inventory/src/components/product-import/steps/ImageUploadStep/hooks/useBulkImageUpload.ts +++ b/inventory/src/components/product-import/steps/ImageUploadStep/hooks/useBulkImageUpload.ts @@ -2,7 +2,7 @@ import { useState } from "react"; import { toast } from "sonner"; import { UnassignedImage, Product } from "../types"; -type HandleImageUploadFn = (files: FileList | File[], productIndex: number) => Promise; +type HandleImageUploadFn = (files: FileList | File[], productIndex: number) => Promise<{ uploaded: number; skipped: number }>; interface UseBulkImageUploadProps { data: Product[]; @@ -88,27 +88,31 @@ export const useBulkImageUpload = ({ data, handleImageUpload }: UseBulkImageUplo // Function to handle bulk image upload const handleBulkUpload = async (files: File[]) => { if (!files.length) return; - + setProcessingBulk(true); const unassigned: UnassignedImage[] = []; - + let totalUploaded = 0; + let totalSkipped = 0; + for (const file of files) { // Extract identifiers from filename const identifiers = extractIdentifiers(file.name); let assigned = false; - + // Try to match each identifier for (const identifier of identifiers) { const productIndex = findProductByIdentifier(identifier); - + if (productIndex !== -1) { // Found a match, upload to this product - await handleImageUpload([file], productIndex); + const result = await handleImageUpload([file], productIndex); + totalUploaded += result.uploaded; + totalSkipped += result.skipped; assigned = true; break; } } - + // If no match was found, add to unassigned if (!assigned) { unassigned.push({ @@ -117,15 +121,17 @@ export const useBulkImageUpload = ({ data, handleImageUpload }: UseBulkImageUplo }); } } - + // Update unassigned images setUnassignedImages(prev => [...prev, ...unassigned]); setProcessingBulk(false); - - // Show summary toast - const assignedCount = files.length - unassigned.length; - if (assignedCount > 0) { - toast.success(`Auto-assigned ${assignedCount} ${assignedCount === 1 ? 'image' : 'images'} to products`); + + // Show summary toasts + if (totalUploaded > 0) { + toast.success(`Auto-assigned ${totalUploaded} ${totalUploaded === 1 ? 'image' : 'images'} to products`); + } + if (totalSkipped > 0) { + toast.info(`Skipped ${totalSkipped} ${totalSkipped === 1 ? 'image' : 'images'} already uploaded for their products`); } if (unassigned.length > 0) { toast.warning(`Could not auto-assign ${unassigned.length} ${unassigned.length === 1 ? 'image' : 'images'}`); diff --git a/inventory/src/components/product-import/steps/ImageUploadStep/hooks/useProductImageOperations.ts b/inventory/src/components/product-import/steps/ImageUploadStep/hooks/useProductImageOperations.ts index ff64cbb..03944ef 100644 --- a/inventory/src/components/product-import/steps/ImageUploadStep/hooks/useProductImageOperations.ts +++ b/inventory/src/components/product-import/steps/ImageUploadStep/hooks/useProductImageOperations.ts @@ -144,20 +144,37 @@ export const useProductImageOperations = ({ product.product_images = [product.product_images].filter(Boolean); } - // Only add if the URL doesn't already exist + // Only add if the URL doesn't already exist (spread to avoid mutating frozen Immer arrays) if (!product.product_images.includes(imageUrl)) { - product.product_images.push(imageUrl); + product.product_images = [...product.product_images, imageUrl]; } return newData; }; // Function to handle image upload - update product data - const handleImageUpload = async (files: FileList | File[], productIndex: number) => { - if (!files || files.length === 0) return; - + const handleImageUpload = async (files: FileList | File[], productIndex: number): Promise<{ uploaded: number; skipped: number }> => { + if (!files || files.length === 0) return { uploaded: 0, skipped: 0 }; + + let uploaded = 0; + let skipped = 0; + for (let i = 0; i < files.length; i++) { const file = files[i]; + + // Check for exact duplicate (same filename, size, and type already uploaded for this product) + const isDuplicate = productImages.some( + img => img.productIndex === productIndex && + !img.loading && + img.imageUrl && + img.fileName === file.name && + img.metadata?.size === file.size + ); + if (isDuplicate) { + skipped++; + continue; + } + const imageId = `image-${productIndex}-${Date.now()}-${i}`; const productLabel = data[productIndex].name || `Product #${productIndex + 1}`; @@ -305,17 +322,20 @@ export const useProductImageOperations = ({ // Update the product data with the new image URL addImageToProduct(productIndex, result.imageUrl); - + + uploaded++; toast.success(`Image uploaded for ${productLabel}`); } catch (error) { console.error('Upload error:', error); - + // Remove the failed image from our state setProductImages(prev => prev.filter(img => img.id !== imageId)); - + toast.error(`Failed to upload image: ${error instanceof Error ? error.message : 'Unknown error'}`); } } + + return { uploaded, skipped }; }; // Function to remove an image - update to work with product_images