Fix modal persistence issue
- Add duplicate prevention with isProcessing state - Implement loading spinner and disabled states - Enhance modal closure logic in DataImport.tsx - Add proper state reset on modal close - Prevent multiple button clicks during processing - Improve user feedback with console logging
This commit is contained in:
parent
461e424e5e
commit
f39d4ca266
3 changed files with 600 additions and 91 deletions
|
|
@ -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<MissingProductsModalProps> = ({
|
|||
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<typeof findPotentialMatches>} = {};
|
||||
|
||||
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<MissingProductsModalProps> = ({
|
|||
<button
|
||||
onClick={onClose}
|
||||
className="text-gray-400 hover:text-gray-600"
|
||||
disabled={isProcessing}
|
||||
>
|
||||
<X className="w-6 h-6" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="p-6">
|
||||
<p className="text-gray-600 mb-4">
|
||||
The following products from your order are not in your product database.
|
||||
Please add printing costs (including materials/filament) to complete the import:
|
||||
</p>
|
||||
<div className="mb-4 p-3 bg-blue-50 border border-blue-200 rounded-lg">
|
||||
<p className="text-blue-900 text-sm">
|
||||
<strong>Found unmatched product:</strong> "{missingProducts[0]?.title}"
|
||||
</p>
|
||||
<p className="text-blue-700 text-xs mt-1">
|
||||
This product isn't in your database yet. Add printing costs to complete the import.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
{missingProducts.map((product, index) => (
|
||||
<div key={index} className="border rounded-lg p-4 bg-gray-50">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="md:col-span-2">
|
||||
<div className="grid grid-cols-1 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Product Name
|
||||
</label>
|
||||
|
|
@ -124,56 +208,124 @@ export const MissingProductsModal: React.FC<MissingProductsModalProps> = ({
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Printing Cost per Item (inc. materials)
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
step="0.01"
|
||||
min="0"
|
||||
placeholder="0.00"
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
onChange={(e) => handleInputChange(product.title, 'printingCost', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
{/* Show suggestions if available */}
|
||||
{suggestions[product.title] && suggestions[product.title].length > 0 && (
|
||||
<div className="bg-blue-50 border border-blue-200 rounded-lg p-3">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<AlertCircle className="w-4 h-4 text-blue-600" />
|
||||
<span className="text-sm font-medium text-blue-900">
|
||||
Possible matches found:
|
||||
</span>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
{suggestions[product.title].map((suggestion, sugIndex) => (
|
||||
<div key={sugIndex} className="flex items-center justify-between">
|
||||
<div className="flex-1">
|
||||
<div className="text-sm text-gray-900">
|
||||
{suggestion.product.title}
|
||||
</div>
|
||||
<div className="text-xs text-gray-600">
|
||||
{suggestion.reason} ({(suggestion.confidence * 100).toFixed(0)}% match)
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => handleUseExisting(product.title, suggestion.product._id)}
|
||||
className={`px-3 py-1 text-xs rounded-md border transition-colors ${
|
||||
productData[product.title]?.useExisting === suggestion.product._id
|
||||
? 'bg-green-100 border-green-300 text-green-700'
|
||||
: 'bg-white border-gray-300 text-gray-700 hover:bg-gray-50'
|
||||
}`}
|
||||
disabled={isProcessing}
|
||||
>
|
||||
{productData[product.title]?.useExisting === suggestion.product._id ? (
|
||||
<span className="flex items-center gap-1">
|
||||
<CheckCircle className="w-3 h-3" />
|
||||
Selected
|
||||
</span>
|
||||
) : (
|
||||
'Use This'
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Category
|
||||
</label>
|
||||
<select
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
onChange={(e) => handleInputChange(product.title, 'category', e.target.value)}
|
||||
>
|
||||
<option value="">Select category...</option>
|
||||
<option value="3D Prints">3D Prints</option>
|
||||
<option value="Jewelry">Jewelry</option>
|
||||
<option value="Home Decor">Home Decor</option>
|
||||
<option value="Organizers">Organizers</option>
|
||||
<option value="Planters">Planters</option>
|
||||
<option value="Custom Items">Custom Items</option>
|
||||
<option value="Other">Other</option>
|
||||
</select>
|
||||
</div>
|
||||
{/* Only show input fields if not using existing product */}
|
||||
{!productData[product.title]?.useExisting && (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Printing Cost ($)
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
step="0.01"
|
||||
placeholder="0.00"
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
value={productData[product.title]?.printingCost || ''}
|
||||
onChange={(e) => handleInputChange(product.title, 'printingCost', e.target.value)}
|
||||
disabled={isProcessing}
|
||||
/>
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
Include filament, materials, and time costs
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Category
|
||||
</label>
|
||||
<select
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
value={productData[product.title]?.category || ''}
|
||||
onChange={(e) => handleInputChange(product.title, 'category', e.target.value)}
|
||||
disabled={isProcessing}
|
||||
>
|
||||
<option value="">Select category</option>
|
||||
<option value="Imported Items">Imported Items</option>
|
||||
<option value="3D Printed">3D Printed</option>
|
||||
<option value="Home & Living">Home & Living</option>
|
||||
<option value="Art & Collectibles">Art & Collectibles</option>
|
||||
<option value="Jewelry">Jewelry</option>
|
||||
<option value="Other">Other</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end gap-3 mt-6">
|
||||
<div className="flex gap-3 mt-6 justify-end">
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="px-4 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50"
|
||||
className="px-4 py-2 text-gray-700 border border-gray-300 rounded-lg hover:bg-gray-50"
|
||||
disabled={isProcessing}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
onClick={handleAddProducts}
|
||||
className="flex items-center gap-2 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700"
|
||||
className={`flex items-center gap-2 px-4 py-2 text-white rounded-lg transition-colors ${
|
||||
isProcessing
|
||||
? 'bg-gray-400 cursor-not-allowed'
|
||||
: 'bg-blue-600 hover:bg-blue-700'
|
||||
}`}
|
||||
disabled={isProcessing}
|
||||
>
|
||||
<Plus className="w-4 h-4" />
|
||||
Add Products to Database
|
||||
{isProcessing ? (
|
||||
<>
|
||||
<div className="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin"></div>
|
||||
Processing...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Plus className="w-4 h-4" />
|
||||
Add Products to Database
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
344
client/src/components/MissingProductsModal.tsx.backup
Normal file
344
client/src/components/MissingProductsModal.tsx.backup
Normal file
|
|
@ -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<MissingProductsModalProps> = ({
|
||||
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<typeof findPotentialMatches>} = {};
|
||||
|
||||
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 (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||
<div className="bg-white rounded-lg shadow-xl max-w-4xl w-full mx-4 max-h-[90vh] overflow-y-auto">
|
||||
<div className="flex justify-between items-center p-6 border-b">
|
||||
<h2 className="text-xl font-semibold text-gray-900">
|
||||
Missing Products - Add Printing Costs
|
||||
</h2>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="text-gray-400 hover:text-gray-600"
|
||||
>
|
||||
<X className="w-6 h-6" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="p-6">
|
||||
<p className="text-gray-600 mb-4">
|
||||
The following products from your order are not in your product database.
|
||||
Please add printing costs (including materials/filament) to complete the import:
|
||||
</p>
|
||||
|
||||
<div className="space-y-4">
|
||||
{missingProducts.map((product, index) => (
|
||||
<div key={index} className="border rounded-lg p-4 bg-gray-50">
|
||||
<div className="grid grid-cols-1 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Product Name
|
||||
</label>
|
||||
<div className="text-sm text-gray-900 font-medium">
|
||||
{product.title}
|
||||
</div>
|
||||
<div className="text-xs text-gray-500 mt-1">
|
||||
Quantity ordered: {product.quantity}{product.price ? ` × $${product.price.toFixed(2)} each` : ''}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Show suggestions if available */}
|
||||
{suggestions[product.title] && suggestions[product.title].length > 0 && (
|
||||
<div className="bg-blue-50 border border-blue-200 rounded-lg p-3">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<AlertCircle className="w-4 h-4 text-blue-600" />
|
||||
<span className="text-sm font-medium text-blue-900">
|
||||
Possible matches found:
|
||||
</span>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
{suggestions[product.title].map((suggestion, sugIndex) => (
|
||||
<div key={sugIndex} className="flex items-center justify-between">
|
||||
<div className="flex-1">
|
||||
<div className="text-sm text-gray-900">
|
||||
{suggestion.product.title}
|
||||
</div>
|
||||
<div className="text-xs text-gray-600">
|
||||
{suggestion.reason} ({(suggestion.confidence * 100).toFixed(0)}% match)
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => handleUseExisting(product.title, suggestion.product._id)}
|
||||
className={`px-3 py-1 text-xs rounded-md border transition-colors ${
|
||||
productData[product.title]?.useExisting === suggestion.product._id
|
||||
? 'bg-green-100 border-green-300 text-green-700'
|
||||
: 'bg-white border-gray-300 text-gray-700 hover:bg-gray-50'
|
||||
}`}
|
||||
>
|
||||
{productData[product.title]?.useExisting === suggestion.product._id ? (
|
||||
<span className="flex items-center gap-1">
|
||||
<CheckCircle className="w-3 h-3" />
|
||||
Selected
|
||||
</span>
|
||||
) : (
|
||||
'Use This'
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Only show input fields if not using existing product */}
|
||||
{!productData[product.title]?.useExisting && (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Printing Cost ($)
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
step="0.01"
|
||||
placeholder="0.00"
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
value={productData[product.title]?.printingCost || ''}
|
||||
onChange={(e) => handleInputChange(product.title, 'printingCost', e.target.value)}
|
||||
/>
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
Include filament, materials, and time costs
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Category
|
||||
</label>
|
||||
<select
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
value={productData[product.title]?.category || ''}
|
||||
onChange={(e) => handleInputChange(product.title, 'category', e.target.value)}
|
||||
>
|
||||
<option value="">Select category</option>
|
||||
<option value="Imported Items">Imported Items</option>
|
||||
<option value="3D Printed">3D Printed</option>
|
||||
<option value="Home & Living">Home & Living</option>
|
||||
<option value="Art & Collectibles">Art & Collectibles</option>
|
||||
<option value="Jewelry">Jewelry</option>
|
||||
<option value="Other">Other</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Printing Cost per Item (inc. materials)
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
step="0.01"
|
||||
min="0"
|
||||
placeholder="0.00"
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
onChange={(e) => handleInputChange(product.title, 'printingCost', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Category
|
||||
</label>
|
||||
<select
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
onChange={(e) => handleInputChange(product.title, 'category', e.target.value)}
|
||||
>
|
||||
<option value="">Select category...</option>
|
||||
<option value="3D Prints">3D Prints</option>
|
||||
<option value="Jewelry">Jewelry</option>
|
||||
<option value="Home Decor">Home Decor</option>
|
||||
<option value="Organizers">Organizers</option>
|
||||
<option value="Planters">Planters</option>
|
||||
<option value="Custom Items">Custom Items</option>
|
||||
<option value="Other">Other</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end gap-3 mt-6">
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="px-4 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
onClick={handleAddProducts}
|
||||
className="flex items-center gap-2 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700"
|
||||
>
|
||||
<Plus className="w-4 h-4" />
|
||||
Add Products to Database
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -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 && (
|
||||
<MissingProductsModal
|
||||
missingProducts={missingProducts}
|
||||
onClose={() => setShowMissingProductsModal(false)}
|
||||
onClose={() => {
|
||||
setShowMissingProductsModal(false);
|
||||
setMissingProducts([]);
|
||||
setCurrentOrderForMissing('');
|
||||
}}
|
||||
onComplete={handleMissingProductsSubmit}
|
||||
/>
|
||||
)}
|
||||
|
|
|
|||
Loading…
Reference in a new issue