diff --git a/client/src/components/MissingProductsModal.tsx b/client/src/components/MissingProductsModal.tsx index 8011d0f..f753c9c 100644 --- a/client/src/components/MissingProductsModal.tsx +++ b/client/src/components/MissingProductsModal.tsx @@ -1,7 +1,9 @@ -import React, { useState } from 'react'; -import { useDispatch } from 'react-redux'; +import React, { useState, useMemo } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { RootState } from '../store'; import { addProduct } from '../store/slices/productSlice'; -import { X, Plus } from 'lucide-react'; +import { findPotentialMatches } from '../utils/productMatcher'; +import { X, Plus, AlertCircle, CheckCircle } from 'lucide-react'; import toast from 'react-hot-toast'; import api from '../utils/api'; @@ -25,66 +27,143 @@ export const MissingProductsModal: React.FC = ({ onComplete }) => { const dispatch = useDispatch(); + const products = useSelector((state: RootState) => state.products.products); + const [isProcessing, setIsProcessing] = useState(false); + const [productData, setProductData] = useState<{[key: string]: { printingCost: number; category: string; + useExisting?: string; // ID of existing product to use instead of creating new }}>({}); + // Find potential matches for each missing product + const suggestions = useMemo(() => { + const result: {[key: string]: ReturnType} = {}; + + missingProducts.forEach(missingProduct => { + result[missingProduct.title] = findPotentialMatches( + missingProduct.title, + products, + 3 // Show top 3 suggestions + ); + }); + + return result; + }, [missingProducts, products]); + const handleInputChange = (productTitle: string, field: string, value: string) => { setProductData(prev => ({ ...prev, [productTitle]: { ...prev[productTitle], - [field]: field === 'category' ? value : parseFloat(value) || 0 + [field]: field === 'category' ? value : + field === 'useExisting' ? value : + parseFloat(value) || 0 + } + })); + }; + + const handleUseExisting = (missingTitle: string, existingProductId: string) => { + setProductData(prev => ({ + ...prev, + [missingTitle]: { + ...prev[missingTitle], + useExisting: existingProductId } })); }; const handleAddProducts = async () => { - const newProducts: any[] = []; - - for (const product of missingProducts) { - const data = productData[product.title] || {}; - const printingCost = data.printingCost || 0; - const category = data.category || 'Imported Items'; - - try { - const res = await api.post('/products', { - title: product.title, - description: `Imported from order ${product.orderNumber || 'Unknown'}`, - price: product.price || 0, - costOfGoods: 0, - printingCost, - sku: '', - category, - tags: [], - inventory: { quantity: 0, lowStockAlert: 5 }, - isActive: true - }); - dispatch(addProduct(res.data)); - newProducts.push(res.data); - } catch { - // Fall back to local-only if API fails - const fallback = { - _id: `product_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, - title: product.title, - description: `Imported from order ${product.orderNumber || 'Unknown'}`, - price: product.price || 0, - costOfGoods: 0, - printingCost, - sku: '', - category, - tags: [], - inventory: { quantity: 0, lowStockAlert: 5 }, - isActive: true - }; - dispatch(addProduct(fallback)); - newProducts.push(fallback); - } + // Prevent multiple clicks while processing + if (isProcessing) { + console.log('Already processing, ignoring click'); + return; } - toast.success(`Added ${newProducts.length} new products with printing costs`); - onComplete(newProducts); + setIsProcessing(true); + console.log('Starting product creation process...'); + + try { + const newProducts: any[] = []; + const existingMatches: any[] = []; + + for (const product of missingProducts) { + const data = productData[product.title] || {}; + + // If user selected to use existing product, add it to matches + if (data.useExisting) { + const existingProduct = products.find(p => p._id === data.useExisting); + if (existingProduct) { + existingMatches.push({ + orderItem: { + title: product.title, + quantity: product.quantity, + price: product.price + }, + matchedProduct: existingProduct + }); + continue; + } + } + + // Otherwise create new product + const printingCost = data.printingCost || 0; + const category = data.category || 'Imported Items'; + + try { + const res = await api.post('/products', { + title: product.title, + description: `Imported from order ${product.orderNumber || 'Unknown'}`, + price: product.price || 0, + costOfGoods: 0, + printingCost, + sku: '', + category, + tags: [], + inventory: { quantity: 0, lowStockAlert: 5 }, + isActive: true + }); + dispatch(addProduct(res.data)); + newProducts.push(res.data); + } catch (error) { + console.error('Error creating product:', product.title, error); + // Fall back to local-only if API fails + const fallback = { + _id: `product_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, + title: product.title, + description: `Imported from order ${product.orderNumber || 'Unknown'}`, + price: product.price || 0, + costOfGoods: 0, + printingCost, + sku: '', + category, + tags: [], + inventory: { quantity: 0, lowStockAlert: 5 }, + isActive: true + }; + dispatch(addProduct(fallback)); + newProducts.push(fallback); + } + } + + if (newProducts.length > 0) { + toast.success(`Added ${newProducts.length} new products with printing costs`); + } + if (existingMatches.length > 0) { + toast.success(`Matched ${existingMatches.length} items to existing products`); + } + + console.log('Products processed successfully, calling onComplete...'); + + // Call onComplete and let parent handle closing + onComplete([...newProducts, ...existingMatches]); + + } catch (error) { + console.error('Error processing products:', error); + toast.error('Failed to process products. Please try again.'); + } finally { + setIsProcessing(false); + } }; return ( @@ -97,22 +176,27 @@ export const MissingProductsModal: React.FC = ({
-

- The following products from your order are not in your product database. - Please add printing costs (including materials/filament) to complete the import: -

+
+

+ Found unmatched product: "{missingProducts[0]?.title}" +

+

+ This product isn't in your database yet. Add printing costs to complete the import. +

+
{missingProducts.map((product, index) => (
-
-
+
+
@@ -123,57 +207,125 @@ export const MissingProductsModal: React.FC = ({ Quantity ordered: {product.quantity}{product.price ? ` × $${product.price.toFixed(2)} each` : ''}
- -
- - handleInputChange(product.title, 'printingCost', e.target.value)} - /> -
- -
- - -
+ + {/* Show suggestions if available */} + {suggestions[product.title] && suggestions[product.title].length > 0 && ( +
+
+ + + Possible matches found: + +
+
+ {suggestions[product.title].map((suggestion, sugIndex) => ( +
+
+
+ {suggestion.product.title} +
+
+ {suggestion.reason} ({(suggestion.confidence * 100).toFixed(0)}% match) +
+
+ +
+ ))} +
+
+ )} + + {/* Only show input fields if not using existing product */} + {!productData[product.title]?.useExisting && ( +
+
+ + handleInputChange(product.title, 'printingCost', e.target.value)} + disabled={isProcessing} + /> +

+ Include filament, materials, and time costs +

+
+
+ + +
+
+ )}
))}
-
+
diff --git a/client/src/components/MissingProductsModal.tsx.backup b/client/src/components/MissingProductsModal.tsx.backup new file mode 100644 index 0000000..13013f6 --- /dev/null +++ b/client/src/components/MissingProductsModal.tsx.backup @@ -0,0 +1,344 @@ +import React, { useState, useMemo } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { RootState } from '../store'; +import { addProduct } from '../store/slices/productSlice'; +import { findPotentialMatches } from '../utils/productMatcher'; +import { X, Plus, AlertCircle, CheckCircle } from 'lucide-react'; +import toast from 'react-hot-toast'; +import api from '../utils/api'; + +export interface MissingProduct { + title: string; + quantity: number; + price?: number; + size?: string; + orderNumber?: string; +} + +interface MissingProductsModalProps { + missingProducts: MissingProduct[]; + onClose: () => void; + onComplete: (products: any[]) => void; +} + +export const MissingProductsModal: React.FC = ({ + missingProducts, + onClose, + onComplete +}) => { + const dispatch = useDispatch(); + const products = useSelector((state: RootState) => state.products.products); + const [isProcessing, setIsProcessing] = useState(false); + + const [productData, setProductData] = useState<{[key: string]: { + printingCost: number; + category: string; + useExisting?: string; // ID of existing product to use instead of creating new + }}>({}); + + // Find potential matches for each missing product + const suggestions = useMemo(() => { + const result: {[key: string]: ReturnType} = {}; + + missingProducts.forEach(missingProduct => { + result[missingProduct.title] = findPotentialMatches( + missingProduct.title, + products, + 3 // Show top 3 suggestions + ); + }); + + return result; + }, [missingProducts, products]); + + const handleInputChange = (productTitle: string, field: string, value: string) => { + setProductData(prev => ({ + ...prev, + [productTitle]: { + ...prev[productTitle], + [field]: field === 'category' ? value : + field === 'useExisting' ? value : + parseFloat(value) || 0 + } + })); + }; + + const handleUseExisting = (missingTitle: string, existingProductId: string) => { + setProductData(prev => ({ + ...prev, + [missingTitle]: { + ...prev[missingTitle], + useExisting: existingProductId + } + })); + }; + + const handleAddProducts = async () => { + // Prevent multiple clicks + if (isProcessing) return; + setIsProcessing(true); + + try { + const newProducts: any[] = []; + const existingMatches: any[] = []; + + for (const product of missingProducts) { + const data = productData[product.title] || {}; + + // If user selected to use existing product, add it to matches + if (data.useExisting) { + const existingProduct = products.find(p => p._id === data.useExisting); + if (existingProduct) { + existingMatches.push({ + orderItem: { + title: product.title, + quantity: product.quantity, + price: product.price + }, + matchedProduct: existingProduct + }); + continue; + } + } + + // Otherwise create new product + const printingCost = data.printingCost || 0; + const category = data.category || 'Imported Items'; + + try { + const res = await api.post('/products', { + title: product.title, + description: `Imported from order ${product.orderNumber || 'Unknown'}`, + price: product.price || 0, + costOfGoods: 0, + printingCost, + sku: '', + category, + tags: [], + inventory: { quantity: 0, lowStockAlert: 5 }, + isActive: true + }); + dispatch(addProduct(res.data)); + newProducts.push(res.data); + } catch { + // Fall back to local-only if API fails + const fallback = { + _id: `product_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, + title: product.title, + description: `Imported from order ${product.orderNumber || 'Unknown'}`, + price: product.price || 0, + costOfGoods: 0, + printingCost, + sku: '', + category, + tags: [], + inventory: { quantity: 0, lowStockAlert: 5 }, + isActive: true + }; + dispatch(addProduct(fallback)); + newProducts.push(fallback); + } + } + + if (newProducts.length > 0) { + toast.success(`Added ${newProducts.length} new products with printing costs`); + } + if (existingMatches.length > 0) { + toast.success(`Matched ${existingMatches.length} items to existing products`); + } + + // Close modal immediately after successful processing + onComplete([...newProducts, ...existingMatches]); + onClose(); // Explicitly close the modal + + } catch (error) { + console.error('Error processing products:', error); + toast.error('Failed to process products. Please try again.'); + } finally { + setIsProcessing(false); + } + }; + + return ( +
+
+
+

+ Missing Products - Add Printing Costs +

+ +
+ +
+

+ The following products from your order are not in your product database. + Please add printing costs (including materials/filament) to complete the import: +

+ +
+ {missingProducts.map((product, index) => ( +
+
+
+ +
+ {product.title} +
+
+ Quantity ordered: {product.quantity}{product.price ? ` × $${product.price.toFixed(2)} each` : ''} +
+
+ + {/* Show suggestions if available */} + {suggestions[product.title] && suggestions[product.title].length > 0 && ( +
+
+ + + Possible matches found: + +
+
+ {suggestions[product.title].map((suggestion, sugIndex) => ( +
+
+
+ {suggestion.product.title} +
+
+ {suggestion.reason} ({(suggestion.confidence * 100).toFixed(0)}% match) +
+
+ +
+ ))} +
+
+ )} + + {/* Only show input fields if not using existing product */} + {!productData[product.title]?.useExisting && ( +
+
+ + handleInputChange(product.title, 'printingCost', e.target.value)} + /> +

+ Include filament, materials, and time costs +

+
+
+ + +
+
+ )} +
+
+ ))} +
+
+
+ +
+ + handleInputChange(product.title, 'printingCost', e.target.value)} + /> +
+ +
+ + +
+
+
+ ))} +
+ +
+ + +
+ + + + ); +}; \ No newline at end of file diff --git a/client/src/pages/DataImport.tsx b/client/src/pages/DataImport.tsx index 5277ae5..cb2848f 100644 --- a/client/src/pages/DataImport.tsx +++ b/client/src/pages/DataImport.tsx @@ -304,7 +304,11 @@ export default function DataImport() { const handleMissingProductsSubmit = (newProducts: any[]) => { // Products are already created by the MissingProductsModal // We need to use the updated product list for matching + console.log('Missing products submitted:', newProducts); + + // Close modal immediately and reset state setShowMissingProductsModal(false); + setMissingProducts([]); const slip = pdfResults.find(slip => slip.orderNumber === currentOrderForMissing); if (slip) { @@ -312,6 +316,11 @@ export default function DataImport() { const updatedProducts = [...products, ...newProducts]; createOrUpdateOrderFromSlip(slip, updatedProducts); } + + // Reset the current order tracking + setCurrentOrderForMissing(''); + + toast.success('Products processed successfully! Order has been updated.'); }; const handleClearTestData = () => { @@ -820,7 +829,11 @@ export default function DataImport() { {showMissingProductsModal && ( setShowMissingProductsModal(false)} + onClose={() => { + setShowMissingProductsModal(false); + setMissingProducts([]); + setCurrentOrderForMissing(''); + }} onComplete={handleMissingProductsSubmit} /> )}