diff --git a/inventory-server/src/routes/import.js b/inventory-server/src/routes/import.js index f75d74d..f4b295d 100644 --- a/inventory-server/src/routes/import.js +++ b/inventory-server/src/routes/import.js @@ -899,6 +899,100 @@ router.get('/search-products', async (req, res) => { } }); +// Endpoint to check UPC and generate item number +router.get('/check-upc-and-generate-sku', async (req, res) => { + const { upc, supplierId } = req.query; + + if (!upc || !supplierId) { + return res.status(400).json({ error: 'UPC and supplier ID are required' }); + } + + try { + const { connection } = await getDbConnection(); + + // Step 1: Check if the UPC already exists + const [upcCheck] = await connection.query( + 'SELECT pid, itemnumber FROM products WHERE upc = ? LIMIT 1', + [upc] + ); + + if (upcCheck.length > 0) { + return res.status(409).json({ + error: 'UPC already exists', + existingProductId: upcCheck[0].pid, + existingItemNumber: upcCheck[0].itemnumber + }); + } + + // Step 2: Generate item number - supplierId-last6DigitsOfUPC minus last digit + let itemNumber = ''; + const upcStr = String(upc); + + // Extract the last 6 digits of the UPC, removing the last digit (checksum) + // So we get 5 digits from positions: length-7 to length-2 + if (upcStr.length >= 7) { + const lastSixMinusOne = upcStr.substring(upcStr.length - 7, upcStr.length - 1); + itemNumber = `${supplierId}-${lastSixMinusOne}`; + } else if (upcStr.length >= 6) { + // If UPC is shorter, use as many digits as possible + const digitsToUse = upcStr.substring(0, upcStr.length - 1); + itemNumber = `${supplierId}-${digitsToUse}`; + } else { + // Very short UPC, just use the whole thing + itemNumber = `${supplierId}-${upcStr}`; + } + + // Step 3: Check if the generated item number exists + const [itemNumberCheck] = await connection.query( + 'SELECT pid FROM products WHERE itemnumber = ? LIMIT 1', + [itemNumber] + ); + + // Step 4: If the item number exists, modify it to use the last 5 digits of the UPC + if (itemNumberCheck.length > 0) { + console.log(`Item number ${itemNumber} already exists, using alternative format`); + + if (upcStr.length >= 5) { + // Use the last 5 digits (including the checksum) + const lastFive = upcStr.substring(upcStr.length - 5); + itemNumber = `${supplierId}-${lastFive}`; + + // Check again if this new item number also exists + const [altItemNumberCheck] = await connection.query( + 'SELECT pid FROM products WHERE itemnumber = ? LIMIT 1', + [itemNumber] + ); + + if (altItemNumberCheck.length > 0) { + // If even the alternative format exists, add a timestamp suffix for uniqueness + const timestamp = Date.now().toString().substring(8, 13); // Get last 5 digits of timestamp + itemNumber = `${supplierId}-${timestamp}`; + console.log(`Alternative item number also exists, using timestamp: ${itemNumber}`); + } + } else { + // For very short UPCs, add a timestamp + const timestamp = Date.now().toString().substring(8, 13); // Get last 5 digits of timestamp + itemNumber = `${supplierId}-${timestamp}`; + } + } + + // Return the generated item number + res.json({ + success: true, + itemNumber, + upc, + supplierId + }); + + } catch (error) { + console.error('Error checking UPC and generating item number:', error); + res.status(500).json({ + error: 'Failed to check UPC and generate item number', + details: error.message + }); + } +}); + // Get product categories for a specific product router.get('/product-categories/:pid', async (req, res) => { try { diff --git a/inventory/src/components/products/ProductSearchDialog.tsx b/inventory/src/components/products/ProductSearchDialog.tsx index f49f8c8..13b20f7 100644 --- a/inventory/src/components/products/ProductSearchDialog.tsx +++ b/inventory/src/components/products/ProductSearchDialog.tsx @@ -269,10 +269,6 @@ export function ProductSearchDialog({ isOpen, onClose, onTemplateCreated }: Prod }; const handleProductSelect = (product: Product) => { - console.log('Selected product supplier data:', { - vendor: product.vendor, - vendor_reference: product.vendor_reference - }); // Ensure all values are of the correct type setFormData({ @@ -303,17 +299,13 @@ export function ProductSearchDialog({ isOpen, onClose, onTemplateCreated }: Prod // Try to find the supplier ID from the vendor name if (product.vendor && fieldOptions) { - console.log('Available suppliers:', fieldOptions.suppliers); - console.log('Looking for supplier match for vendor:', product.vendor); - - // First try exact match - let supplierOption = fieldOptions.suppliers.find( + let supplierOption = fieldOptions.suppliers.find( supplier => supplier.label.toLowerCase() === product.vendor.toLowerCase() ); // If no exact match, try partial match if (!supplierOption) { - console.log('No exact match found, trying partial match'); + supplierOption = fieldOptions.suppliers.find( supplier => supplier.label.toLowerCase().includes(product.vendor.toLowerCase()) || product.vendor.toLowerCase().includes(supplier.label.toLowerCase()) @@ -325,11 +317,7 @@ export function ProductSearchDialog({ isOpen, onClose, onTemplateCreated }: Prod ...prev, supplier: supplierOption.value })); - console.log('Found supplier match:', { - vendorName: product.vendor, - matchedSupplier: supplierOption.label, - supplierId: supplierOption.value - }); + } else { console.log('No supplier match found for vendor:', product.vendor); } @@ -337,7 +325,7 @@ export function ProductSearchDialog({ isOpen, onClose, onTemplateCreated }: Prod // Fetch product categories if (product.pid) { - console.log('Fetching categories for product ID:', product.pid); + fetchProductCategories(product.pid); } @@ -348,7 +336,7 @@ export function ProductSearchDialog({ isOpen, onClose, onTemplateCreated }: Prod const fetchProductCategories = async (productId: number) => { try { const response = await axios.get(`/api/import/product-categories/${productId}`); - console.log('Product categories:', response.data); + if (response.data && Array.isArray(response.data)) { // Filter out categories with type 20 (themes) and type 21 (subthemes) @@ -356,7 +344,6 @@ export function ProductSearchDialog({ isOpen, onClose, onTemplateCreated }: Prod category.type !== 20 && category.type !== 21 ); - console.log('Filtered categories (excluding themes):', filteredCategories); // Extract category IDs and update form data const categoryIds = filteredCategories.map((category: any) => category.value); @@ -422,28 +409,14 @@ export function ProductSearchDialog({ isOpen, onClose, onTemplateCreated }: Prod // Log supplier information for debugging if (formData.supplier) { - const supplierOption = fieldOptions?.suppliers.find( - supplier => supplier.value === formData.supplier - ); - console.log('Submitting supplier:', { - id: formData.supplier, - name: supplierOption?.label || 'Unknown', - allSuppliers: fieldOptions?.suppliers.map(s => ({ id: s.value, name: s.label })) - }); + } else { console.log('No supplier selected for submission'); } // Log categories information for debugging if (formData.categories && formData.categories.length > 0) { - const categoryOptions = formData.categories.map(catId => { - const category = fieldOptions?.categories.find(c => c.value === catId); - return { - id: catId, - name: category?.label || 'Unknown Category' - }; - }); - console.log('Submitting categories:', categoryOptions); + } else { console.log('No categories selected for submission'); } @@ -471,11 +444,9 @@ export function ProductSearchDialog({ isOpen, onClose, onTemplateCreated }: Prod ship_restrictions: formData.ship_restrictions || null }; - console.log('Sending template data:', dataToSend); const response = await axios.post('/api/templates', dataToSend); - console.log('Template creation response:', response); if (response.status >= 200 && response.status < 300) { toast.success('Template created successfully'); @@ -1040,9 +1011,7 @@ export function ProductSearchDialog({ isOpen, onClose, onTemplateCreated }: Prod const filteredCategories = fieldOptions.categories.filter( category => category.type && validCategoryTypes.includes(category.type) ); - - console.log('Filtered categories for dropdown:', filteredCategories.length); - + return [...filteredCategories].sort((a, b) => { const aSelected = selected.has(a.value); const bSelected = selected.has(b.value); @@ -1171,10 +1140,7 @@ export function ProductSearchDialog({ isOpen, onClose, onTemplateCreated }: Prod onSelect={() => { // Make sure we're setting the ID (value), not the label handleSelectChange('supplier', supplier.value); - console.log('Selected supplier from dropdown:', { - label: supplier.label, - value: supplier.value - }); + }} > void; error?: { level: string; message: string }; - field: Field; + field: Field & { + handleUpcValidation?: (upcValue: string) => Promise; + }; productLines?: SelectOption[]; sublines?: SelectOption[]; } @@ -173,12 +175,19 @@ const EditableCell = memo(({ value, onChange, error, field, productLines, sublin const [inputValue, setInputValue] = useState(value ?? "") const [searchQuery, setSearchQuery] = useState("") const [localValues, setLocalValues] = useState([]) + const [isProcessingUpc, setIsProcessingUpc] = useState(false) + const { toast } = useToast() // Determine if the field should be disabled based on its key and context const isFieldDisabled = useMemo(() => { // If the field is already disabled by the parent component, respect that if (field.disabled) return true; + // Never disable item number fields with values + if ((field.key === 'sku' || field.key === 'itemnumber' || field.key === 'item_number') && value) { + return false; + } + // Special handling for line and subline fields if (field.key === 'line') { // Never disable line field if it already has a value @@ -214,12 +223,16 @@ const EditableCell = memo(({ value, onChange, error, field, productLines, sublin e.stopPropagation(); }, []); - // Update input value when external value changes and we're not editing + // Update input value when value changes - use useEffect to ensure synchronization useEffect(() => { - if (!isEditing) { - setInputValue(value ?? "") + // Always update input value when value prop changes + setInputValue(value ?? "") + + // Log updates for item number/SKU fields to help with debugging + if (field.key === 'sku' || field.key === 'itemnumber' || field.key === 'item_number') { + console.log(`EditableCell ${field.key} value updated to "${value}"`); } - }, [value, isEditing]) + }, [value, field.key]) // Keep localValues in sync with value for multi-select useEffect(() => { @@ -349,6 +362,14 @@ const EditableCell = memo(({ value, onChange, error, field, productLines, sublin if (fieldType.type === "multi-input" && Array.isArray(value)) { return value.join(", "); } + + // Special handling for SKU/item number fields to make them more visible + if (field.key === 'sku' || field.key === 'itemnumber' || field.key === 'item_number') { + console.log(`Displaying SKU/item number value: "${value}"`); + // Format it nicely for display + return value ? `${value}` : ""; + } + return value; } @@ -364,6 +385,11 @@ const EditableCell = memo(({ value, onChange, error, field, productLines, sublin newValue = formatPrice(newValue) } + // Log commits for UPC and SKU/item number fields + if (field.key === 'upc' || field.key === 'barcode' || field.key === 'sku' || field.key === 'itemnumber' || field.key === 'item_number') { + console.log(`Committing ${field.key} value: "${newValue}"`); + } + // Always commit the value onChange(newValue) @@ -371,9 +397,75 @@ const EditableCell = memo(({ value, onChange, error, field, productLines, sublin return !validateRegex(newValue) } - const handleBlur = () => { - validateAndCommit(inputValue) - setIsEditing(false) + const handleBlur = async () => { + // Special handling for UPC field + if (field.key === 'upc' || field.key === 'barcode') { + try { + // Skip if input value is empty + if (!inputValue.trim()) { + validateAndCommit(inputValue); + setIsEditing(false); + return; + } + + console.log(`UPC blur handler - saving UPC value: ${inputValue}`); + + // Store the current UPC value to ensure it's not lost + // IMPORTANT: Don't commit the UPC value yet using validateAndCommit + // We'll commit both UPC and item number together + const currentUpcValue = inputValue; + + // Then call the UPC validation function to generate item number + if (field.handleUpcValidation) { + setIsProcessingUpc(true); + + try { + console.log(`Cell blur - calling handleUpcValidation for ${currentUpcValue}`); + // Call the UPC validation function + const result = await field.handleUpcValidation(currentUpcValue); + console.log('UPC validation result in handleBlur:', result); + + // We won't need to re-set UPC since it's handled in the validation function now + + if (result && result.error) { + // If there was an error, now we need to commit the UPC + validateAndCommit(currentUpcValue); + + toast({ + title: "UPC Validation Error", + description: result.message || "Error validating UPC", + variant: "destructive", + }); + } + } catch (error) { + console.error('Error in UPC validation:', error); + // Ensure UPC is still saved even if there was an error + validateAndCommit(currentUpcValue); + + toast({ + title: "UPC Validation Error", + description: error instanceof Error ? error.message : "Error processing UPC", + variant: "destructive", + }); + } finally { + setIsProcessingUpc(false); + } + } else { + // If no validation function, just commit the UPC normally + validateAndCommit(currentUpcValue); + } + } catch (error) { + console.error('Error in UPC blur handler:', error); + // Make sure to commit any changes if there was an error + validateAndCommit(inputValue); + } + } else { + // Normal validation for other fields + validateAndCommit(inputValue); + } + + // Exit editing mode after processing + setIsEditing(false); } if (isEditing) { @@ -626,6 +718,11 @@ const EditableCell = memo(({ value, onChange, error, field, productLines, sublin } /> {currentError && } + {isProcessingUpc && ( +
+ +
+ )} ) default: @@ -662,6 +759,11 @@ const EditableCell = memo(({ value, onChange, error, field, productLines, sublin } /> {currentError && } + {isProcessingUpc && ( +
+ +
+ )} ) } @@ -684,7 +786,9 @@ const EditableCell = memo(({ value, onChange, error, field, productLines, sublin field.fieldType.multiline && "max-h-[100px] overflow-y-auto", currentError ? "border-destructive" : "border-input", field.fieldType.type === "checkbox" ? "flex items-center" : "flex items-center justify-between", - isFieldDisabled && "opacity-50 cursor-not-allowed bg-muted" + isFieldDisabled && "opacity-50 cursor-not-allowed bg-muted", + // Highlight fields that are updated programmatically for better visibility + (field.key === 'sku' || field.key === 'itemnumber' || field.key === 'item_number') && value && "border-input" )} > {((field.fieldType.type === "input" || field.fieldType.type === "multi-input") && field.fieldType.multiline) ? ( @@ -709,7 +813,11 @@ const EditableCell = memo(({ value, onChange, error, field, productLines, sublin )} ) : ( -
+
+ {/* Simplify to just display the value like other fields */} {value ? getDisplayValue(value, field.fieldType) : ""}
)} @@ -717,8 +825,36 @@ const EditableCell = memo(({ value, onChange, error, field, productLines, sublin )} {currentError && } + {isProcessingUpc && ( +
+ +
+ )}
) +}, (prevProps, nextProps) => { + // Custom comparison for memo - force re-render when value changes for sku/itemnumber fields + const isSKUField = prevProps.field.key === 'sku' || prevProps.field.key === 'itemnumber' || prevProps.field.key === 'item_number'; + const isUPCField = prevProps.field.key === 'upc' || prevProps.field.key === 'barcode'; + + // For SKU/item number fields, always re-render when value changes + if (isSKUField && prevProps.value !== nextProps.value) { + console.log(`EditableCell re-rendering for ${prevProps.field.key} - value changed from "${prevProps.value}" to "${nextProps.value}"`); + return false; // Return false to trigger re-render + } + + // For UPC fields, also always re-render when value changes + if (isUPCField && prevProps.value !== nextProps.value) { + console.log(`EditableCell re-rendering for ${prevProps.field.key} - value changed from "${prevProps.value}" to "${nextProps.value}"`); + return false; // Return false to trigger re-render + } + + // For other fields, use standard equality check + return ( + prevProps.value === nextProps.value && + prevProps.error === nextProps.error && + prevProps.field === nextProps.field + ); }) // Update type guard to be more specific @@ -733,8 +869,10 @@ function getMultiInputSeparator(fieldType: FieldType): string { return ","; } -function isPriceField(fieldType: FieldType): fieldType is (InputFieldType | MultiInputFieldType) & { price: true } { - return (fieldType.type === "input" || fieldType.type === "multi-input") && 'price' in fieldType && fieldType.price === true; +function isPriceField(fieldType: FieldType): boolean { + return (fieldType.type === "input" || fieldType.type === "multi-input") && + 'price' in fieldType && + fieldType.price === true; } // Wrap ColumnHeader with memo so that it re-renders only when its props change @@ -1124,9 +1262,7 @@ function useTemplates( }); // Log the raw response for debugging - console.log('Template save response status:', response.status); const responseData = await response.json(); - console.log('Template save response:', responseData); if (!response.ok) { throw new Error(responseData.error || responseData.details || "Failed to save template"); @@ -1255,19 +1391,9 @@ const SearchableTemplateSelect = memo(({ // Log props for debugging useEffect(() => { - console.log('SearchableTemplateSelect props:', { - templatesCount: templates?.length || 0, - value, - hasGetTemplateDisplayText: !!getTemplateDisplayText, - placeholder, - className, - triggerClassName - }); // Check if templates are valid if (templates && templates.length > 0) { - const firstTemplate = templates[0]; - console.log('First template:', firstTemplate); setIsTemplatesReady(true); } else { setIsTemplatesReady(false); @@ -1908,44 +2034,344 @@ export const ValidationStep = ({ const updateData = useCallback( async (rows: (Data & ExtendedMeta)[], indexes?: number[]) => { - setData(rows); + // Create new array references to ensure React state updates properly + const newRows = rows.map(row => ({...row})); + + // Set the initial data update + setData(newRows); // Use fieldsWithUpdatedOptions to ensure we have the latest field definitions with proper options const currentFields = fieldsWithUpdatedOptions as unknown as Fields; - if (rowHook?.constructor.name === "AsyncFunction" || tableHook?.constructor.name === "AsyncFunction") { - const updatedData = await addErrorsAndRunHooks(rows, currentFields, rowHook, tableHook, indexes); - setData(updatedData as (Data & ExtendedMeta)[]); - } else { - const result = await addErrorsAndRunHooks(rows, currentFields, rowHook, tableHook, indexes); - setData(result as (Data & ExtendedMeta)[]); + try { + if (rowHook?.constructor.name === "AsyncFunction" || tableHook?.constructor.name === "AsyncFunction") { + const updatedData = await addErrorsAndRunHooks(newRows, currentFields, rowHook, tableHook, indexes); + // Create a new array to force React to recognize the state change + setData(updatedData.map(row => ({...row})) as (Data & ExtendedMeta)[]); + } else { + const result = await addErrorsAndRunHooks(newRows, currentFields, rowHook, tableHook, indexes); + // Create a new array to force React to recognize the state change + setData(result.map(row => ({...row})) as (Data & ExtendedMeta)[]); + } + console.log('Data updated successfully with', newRows.length, 'rows'); + } catch (error) { + console.error('Error in updateData:', error); + // Still update the data even if hooks fail + setData(newRows); } }, [rowHook, tableHook, fieldsWithUpdatedOptions], ); - const updateRows = useCallback( - (rowIndex: number, columnId: string, value: any) => { + // Reference to store hook timeouts to prevent race conditions + const hookTimeoutRef = useRef(null); + + // Handle UPC validation and item number generation + const handleUpcValidation = useCallback(async (upcValue: string, fieldKey: string) => { + try { + if (!upcValue || !upcValue.trim()) return null; + + // Get the cell element to find the row + const cell = document.getElementById(`cell-${fieldKey}`); + if (!cell) { + console.error('Could not find cell element'); + return { error: true, message: 'Could not determine row context' }; + } + + // Find the row in the DOM + const rowElement = cell.closest('tr'); + if (!rowElement) { + console.error('Could not find row element'); + return { error: true, message: 'Could not determine row context' }; + } + + // Get the row index + const rowIndex = rowElement ? Array.from(rowElement.parentElement?.children || []).indexOf(rowElement) : -1; + if (rowIndex === -1) { + console.error('Could not determine row index'); + return { error: true, message: 'Could not determine row context' }; + } + + // Get the actual row data from filteredData const row = filteredData[rowIndex]; - if (!row) return; + if (!row) { + console.error('Could not find row data'); + return { error: true, message: 'Could not find row data' }; + } + + // Type assertion to access dynamic properties + const rowData = row as Record; + + // We need to have a supplier ID to generate an item number + if (!rowData.supplier) { + console.log('No supplier selected, skipping item number generation'); + return { error: true, message: 'Please select a supplier before validating UPC' }; + } + + // Get the supplier ID from the row + const supplierId = rowData.supplier; + console.log(`Using supplier ID ${supplierId} for UPC validation`); + + // Call the API to check the UPC and generate an item number + console.log(`Calling API with UPC=${upcValue} and supplierId=${supplierId}`); + const response = await fetch(`${config.apiUrl}/import/check-upc-and-generate-sku?upc=${encodeURIComponent(upcValue)}&supplierId=${encodeURIComponent(supplierId)}`); + + // Try to parse the response data even if it's an error + let responseData; + try { + responseData = await response.json(); + console.log('API response:', responseData); + } catch (parseError) { + console.error('Error parsing response:', parseError); + return { error: true, message: 'Failed to validate UPC: Invalid response format' }; + } + + if (!response.ok) { + // Handle 409 error (UPC already exists) + if (response.status === 409) { + console.log(`UPC ${upcValue} already exists - ${responseData.message}`); + const error = { + level: 'error', + message: `UPC already exists for product ID ${responseData.existingProductId} with item number ${responseData.existingItemNumber}` + }; + + return { error: true, message: error.message }; + } + + return { error: true, message: responseData.message || 'Failed to validate UPC' }; + } + + // Success - got an item number + if (responseData && responseData.success && responseData.itemNumber) { + // Figure out which field to use for the item number (itemnumber or sku) + const itemNumberField = fieldsWithUpdatedOptions.find(f => f.key === 'itemnumber'); + const itemNumberKey = itemNumberField ? 'itemnumber' : 'sku'; + + console.log(`Generated item number: ${responseData.itemNumber}, will update field ${itemNumberKey}`); + + // Return a simple object with the field to update and the new value + return { + fieldKey: itemNumberKey, + value: responseData.itemNumber, + rowIndex, + success: true + }; + } + + return { error: true, message: 'No item number returned from API' }; + } catch (error) { + console.error('Error during UPC validation:', error); + return { error: true, message: String(error) }; + } + }, [fieldsWithUpdatedOptions, filteredData]); + + const updateRows = useCallback( + (rowIndex: number, fieldKey: string, value: any) => { + // Get the current row based on the filteredData + const row = filteredData[rowIndex]; + if (!row) { + console.error(`Tried to update row ${rowIndex} but it doesn't exist`); + return; + } + + // Find the original row's index in the full data array + const originalIndex = data.findIndex(item => item.__index === row.__index); + if (originalIndex === -1) { + console.error(`Couldn't find original index for row ${rowIndex}`); + return; + } - const originalIndex = data.findIndex(r => r.__index === row.__index); - if (originalIndex === -1) return; + // Log the update for debugging, especially for important fields + const isKeyField = fieldKey === 'sku' || fieldKey === 'itemnumber' || fieldKey === 'item_number' || fieldKey === 'upc' || fieldKey === 'barcode' || fieldKey === 'supplier'; + if (isKeyField) { + console.log(`updateRows: setting ${fieldKey} = "${value}" for row ${rowIndex} (original index: ${originalIndex})`); + } - const newData = [...data]; - const updatedRow = { - ...row, - [columnId]: value, - }; - newData[originalIndex] = updatedRow; - - // Update immediately first + // Special handling for item number fields (sku or itemnumber) to ensure UPC/barcode is preserved + const isItemNumberField = fieldKey === 'sku' || fieldKey === 'itemnumber' || fieldKey === 'item_number'; + const isUpcField = fieldKey === 'upc' || fieldKey === 'barcode'; + + // Special handling for UPC updates to ensure we preserve and keep the item number + if (isUpcField) { + // Get the current item number values from all possible fields + const typedItem = data[originalIndex] as Record; + + // Create a list of all item number fields and their values + const itemNumberFields: Array<{field: string, value: any}> = []; + + if (typedItem.item_number !== undefined) { + itemNumberFields.push({ field: 'item_number', value: typedItem.item_number }); + } + if (typedItem.itemnumber !== undefined) { + itemNumberFields.push({ field: 'itemnumber', value: typedItem.itemnumber }); + } + if (typedItem.sku !== undefined) { + itemNumberFields.push({ field: 'sku', value: typedItem.sku }); + } + + // Log all found item number fields + if (itemNumberFields.length > 0) { + console.log(`Found item number fields to preserve during UPC update:`, itemNumberFields); + } + + // Create a new data array with the UPC updated but preserving item numbers + const newData = data.map((item, idx) => { + if (idx === originalIndex) { + // Start with the original item + const updatedItem = { ...item, [fieldKey]: value }; + + // Ensure all item number fields are preserved + itemNumberFields.forEach(field => { + if (field.value) { + console.log(`Preserving ${field.field} = "${field.value}" during UPC update`); + (updatedItem as Record)[field.field] = field.value; + } + }); + + return updatedItem; + } + return item; + }); + + // Update the state + setData(newData); + + // Run validation hooks + hookTimeoutRef.current = setTimeout(() => { + console.log(`Running validation hooks after UPC update with preserved item numbers`); + updateData(newData, [originalIndex]) + .catch(err => console.error(`Error in validation:`, err)); + }, 100); + + return; + } + + // Similarly for item number fields, preserve the UPC value + if (isItemNumberField) { + // Check for UPC fields to preserve + const typedItem = data[originalIndex] as Record; + + // Create a new data array with the item number updated but preserving UPC + const newData = data.map((item, idx) => { + if (idx === originalIndex) { + // Create a new item with the updated field + const updatedItem = { ...item, [fieldKey]: value }; + + // Preserve the UPC field + if (typedItem.upc !== undefined) { + console.log(`Preserving upc = "${typedItem.upc}" during item number update`); + (updatedItem as Record).upc = typedItem.upc; + } + if (typedItem.barcode !== undefined) { + console.log(`Preserving barcode = "${typedItem.barcode}" during item number update`); + (updatedItem as Record).barcode = typedItem.barcode; + } + + return updatedItem; + } + return item; + }); + + // Update the state + setData(newData); + + // Run validation hooks + hookTimeoutRef.current = setTimeout(() => { + console.log(`Running validation hooks after item number update with preserved UPC`); + updateData(newData, [originalIndex]) + .catch(err => console.error(`Error in validation:`, err)); + }, 100); + + return; + } + + // For other fields, use the original logic + const newData = data.map((item, idx) => { + if (idx === originalIndex) { + return { ...item, [fieldKey]: value }; + } + return item; + }); + + // Update the state immediately for fast UI feedback setData(newData); - // Then run the async validation - updateData(newData, [originalIndex]); + // Clear any existing hook timeout to prevent race conditions + if (hookTimeoutRef.current) { + clearTimeout(hookTimeoutRef.current); + } + + // Run the hooks asynchronously with a short delay to allow the UI to update first + hookTimeoutRef.current = setTimeout(() => { + console.log(`Running validation hooks after updating ${fieldKey}`); + updateData(newData, [originalIndex]) + .then(() => { + console.log(`Validation completed for ${fieldKey} update`); + + // For item number fields, add another check to verify it's still set after validation + if (isItemNumberField) { + setTimeout(() => { + const currentRow = data.find(item => item.__index === newData[originalIndex].__index); + const typedCurrentRow = currentRow as Record; + + if (currentRow) { + console.log(`Verification check: item number field ${fieldKey} = "${typedCurrentRow[fieldKey]}"`); + + // If the item number was lost, update it again + if (!typedCurrentRow[fieldKey] && value) { + console.log(`Item number was lost, re-updating ${fieldKey} = "${value}"`); + updateRows(rowIndex, fieldKey, value); + } + } + }, 100); + } + }) + .catch(err => console.error(`Error in validation after ${fieldKey} update:`, err)); + }, 50); }, - [data, filteredData, updateData], + [filteredData, data, updateData], + ); + + // Add function to handle SKU generation results from UPC validation + const handleSkuGeneration = useCallback( + (_upcField: string, result: { fieldKey: string, value: string, rowIndex: number }) => { + // Find the corresponding row in the filtered data + const row = filteredData[result.rowIndex]; + if (!row) return; + + // Find the original index in the full data array + const originalIndex = data.findIndex(r => r.__index === row.__index); + if (originalIndex === -1) return; + + // If the row exists, update the SKU field with the generated value + console.log(`Updating ${result.fieldKey} in row ${originalIndex} with value ${result.value}`); + + // Call updateRows to update the item number field + updateRows(result.rowIndex, result.fieldKey, result.value); + + // Add an additional safeguard for UPC preservation + // Get the UPC field key + const typedRow = row as Record; + let upcFieldKey = null; + let upcValue = null; + + if (typedRow.upc) { + upcFieldKey = 'upc'; + upcValue = typedRow.upc; + } else if (typedRow.barcode) { + upcFieldKey = 'barcode'; + upcValue = typedRow.barcode; + } + + // If we found a UPC field, ensure it's preserved + if (upcFieldKey && upcValue) { + console.log(`Also ensuring UPC field "${upcFieldKey}" still has value "${upcValue}"`); + setTimeout(() => { + updateRows(result.rowIndex, upcFieldKey, upcValue); + }, 100); + } + }, + [data, filteredData, updateRows] ); const copyValueDown = useCallback((key: T, label: string) => { @@ -2045,7 +2471,295 @@ export const ValidationStep = ({ }, size: 200, }, - ...(Array.from(fields as ReadonlyFields).map((field): ColumnDef & ExtendedMeta> => ({ + ...(Array.from(fields as ReadonlyFields).map((field): ColumnDef & ExtendedMeta> => { + // Function to handle UPC validation and item number generation + const handleUpcValidation = async (upcValue: string, fieldKey: string) => { + try { + if (!upcValue || !upcValue.trim()) return; + + // Get the cell element to find the row + const cell = document.getElementById(`cell-${fieldKey}`); + if (!cell) { + console.error('Could not find cell element'); + return; + } + + // Find the row in the DOM + const rowElement = cell.closest('tr'); + if (!rowElement) { + console.error('Could not find row element'); + return; + } + + // Get the row index + const rowIndex = rowElement ? Array.from(rowElement.parentElement?.children || []).indexOf(rowElement) : -1; + if (rowIndex === -1) { + console.error('Could not determine row index'); + return; + } + + // Get the actual row data from filteredData + const row = filteredData[rowIndex]; + if (!row) { + console.error('Could not find row data'); + return; + } + + // Type assertion to access dynamic properties + const rowData = row as Record; + + // Find the supplier field in the fields array to get its raw value + const supplierField = Array.from(fields).find(f => f.key === 'supplier'); + if (!supplierField) { + console.error('Could not find supplier field definition'); + return; + } + + // We need to have a supplier ID to generate an item number + if (!rowData.supplier) { + console.log('No supplier selected, skipping item number generation'); + return; + } + + // If there's already an item number, don't overwrite it + // Check for all three possible item number field names in order of preference + const hasItemNumber = rowData.hasOwnProperty('item_number') ? + rowData.item_number : rowData.hasOwnProperty('itemnumber') ? + rowData.itemnumber : rowData.hasOwnProperty('sku') ? rowData.sku : null; + + if (hasItemNumber && String(hasItemNumber).trim()) { + console.log('Item number already exists, skipping generation'); + return; + } + + // Get the raw supplier value from the data model + const supplierId = rowData.supplier; + + // Find the actual supplier ID if what we have is a display name + let supplierIdToUse = supplierId; + + // Check if this is a select field and if we need to translate from name to ID + if (supplierField && supplierField.fieldType.type === 'select') { + const supplierOptions = (supplierField.fieldType as SelectFieldType).options; + // First check if supplierId is already an ID (matched with an option.value) + const isAlreadyId = supplierOptions.some(opt => opt.value === supplierId); + + if (!isAlreadyId) { + // If not an ID, find the option with matching display name and use its value (ID) + const option = supplierOptions.find(opt => + opt.label.toLowerCase() === String(supplierId).toLowerCase() + ); + if (option) { + supplierIdToUse = option.value; + console.log(`Converted supplier name "${supplierId}" to ID "${supplierIdToUse}"`); + } + } + } + + // Create a unique key for this UPC validation to check if we've already processed it + const validationKey = `${upcValue}-${supplierIdToUse}`; + + // Check if we already processed this exact validation + if (cell && cell.dataset.lastValidation === validationKey) { + console.log('Skipping duplicate UPC validation'); + return; + } + + console.log(`Validating UPC ${upcValue} for supplier ID ${supplierIdToUse} in row ${rowIndex}`); + + // Call the API to check the UPC and generate an item number - now with the correct supplier ID + const response = await fetch(`${config.apiUrl}/import/check-upc-and-generate-sku?upc=${encodeURIComponent(upcValue)}&supplierId=${encodeURIComponent(supplierIdToUse)}`); + + // Try to parse the response data even if it's an error + let data; + try { + data = await response.json(); + } catch (parseError) { + console.error('Error parsing response:', parseError); + throw new Error('Failed to validate UPC: Invalid response format'); + } + + // Store this validation in the cell's dataset to prevent duplicate processing + if (cell) { + cell.dataset.lastValidation = validationKey; + } + + if (!response.ok) { + // If UPC already exists, show an error + if (response.status === 409) { + const fieldError = { + level: 'error', + message: `UPC already exists for product ID ${data.existingProductId} with item number ${data.existingItemNumber}` + }; + + toast({ + title: "UPC Validation Error", + description: fieldError.message, + variant: "destructive", + }); + + // Return a specific error object for the caller to handle + return { + error: true, + errorType: 'upc_exists', + message: fieldError.message, + field: 'upc', + level: 'error' + }; + } else { + throw new Error(data.error || 'Failed to validate UPC'); + } + } else if (data.success && data.itemNumber) { + console.log(`Generated item number: ${data.itemNumber}`); + + // Determine the field key to update (checking all three possible field names) + let itemNumberKey = 'item_number'; // default to the most likely field name + + // Check if we have item_number field + if ('item_number' in rowData) { + itemNumberKey = 'item_number'; + } + // Otherwise check if we have itemnumber field + else if ('itemnumber' in rowData) { + itemNumberKey = 'itemnumber'; + } + // Finally fall back to sku if neither is present + else if ('sku' in rowData) { + itemNumberKey = 'sku'; + } + + console.log(`Using field "${itemNumberKey}" for the generated item number`); + + // Return the generated item number with the row index + return { + fieldKey: itemNumberKey, + value: data.itemNumber, + rowIndex + }; + } + } catch (error) { + console.error('Error validating UPC:', error); + return { + error: true, + message: error instanceof Error ? error.message : 'Error validating UPC' + }; + } + }; + + // Create an enhanced field with additional onChange handler for UPC/barcode field + const enhancedField = { + ...field, + onChange: field.key === 'upc' || field.key === 'barcode' + ? (value: any, result?: any) => { + // Store the UPC value to ensure it's preserved through state updates + const upcValue = value; + console.log(`UPC onChange handler called with value: "${upcValue}"`); + + // If the validation resulted in an error, mark the UPC field as invalid + if (result && result.error) { + if (result.errorType === 'upc_exists') { + // Get the row index using the DOM + const upcCell = document.getElementById(`cell-${field.key}`); + if (upcCell) { + const row = upcCell.closest('tr'); + const rowIndex = row ? Array.from(row.parentElement?.children || []).indexOf(row) : -1; + + if (rowIndex >= 0) { + // Update the row with the error + const errorObject = { + level: result.level || 'error', + message: result.message + }; + + // Get the original row from filteredData + const dataRow = filteredData[rowIndex]; + if (!dataRow) return; + + // Find the original index of this row in the full data array + const originalIndex = data.findIndex(r => r.__index === dataRow.__index); + if (originalIndex === -1) return; + + // Create updated data with the error + const newData = [...data]; + newData[originalIndex] = { + ...newData[originalIndex], + __errors: { + ...(newData[originalIndex].__errors || {}), + [field.key]: errorObject + } + }; + + // Ensure the UPC value is still set in the data + newData[originalIndex][field.key] = upcValue; + + // Update the data to show the error + updateData(newData, [originalIndex]); + } + } + } + } + // If the result contains a valid item number, update that field + else if (result && !result.error && result.fieldKey && result.value !== undefined) { + // Check if result has rowIndex for direct update + if (result.rowIndex !== undefined) { + // Update the item number field directly + updateRows(result.rowIndex, result.fieldKey, result.value); + + // IMPORTANT: Re-update the UPC value to ensure it's not lost + setTimeout(() => { + console.log(`Re-updating UPC field "${field.key}" with value "${upcValue}"`); + updateRows(result.rowIndex, field.key, upcValue); + }, 100); + } else if (result.success && result.itemNumber) { + // We need to find the row and update it by searching for this UPC cell + const upcCell = document.getElementById(`cell-${field.key}`); + if (upcCell) { + const row = upcCell.closest('tr'); + const rowIndex = row ? Array.from(row.parentElement?.children || []).indexOf(row) : -1; + + if (rowIndex >= 0) { + // Determine the item number field key (checking all three possible field names) + let itemNumberKey; + if ('item_number' in data[0]) { + itemNumberKey = 'item_number'; + } else if ('itemnumber' in data[0]) { + itemNumberKey = 'itemnumber'; + } else { + itemNumberKey = 'sku'; + } + + console.log(`Using field "${itemNumberKey}" for item number`); + + // First update the UPC to make sure it's preserved + updateRows(rowIndex, field.key, upcValue); + + // Then update the item number field + console.log(`Updating ${itemNumberKey} at row ${rowIndex} to ${result.itemNumber}`); + updateRows(rowIndex, itemNumberKey, result.itemNumber); + + // Update the UPC again to make sure it wasn't lost + setTimeout(() => { + console.log(`Final UPC update for "${field.key}" with value "${upcValue}"`); + updateRows(rowIndex, field.key, upcValue); + }, 150); + } + } + } + } else { + // For normal UPC updates without validation results + // Make sure the call to updateRows correctly sets the UPC value + console.log(`Normal UPC update - setting ${field.key} = "${upcValue}"`); + } + + // Call the original onChange if it exists + if (field.onChange) { + field.onChange(value); + } + } + : field.onChange + }; + + return { accessorKey: field.key, header: () => (
@@ -2057,30 +2771,141 @@ export const ValidationStep = ({
), cell: ({ row, column }) => { - const value = row.getValue(column.id) - const error = row.original.__errors?.[column.id] - const rowIndex = row.index + const value = row.getValue(column.id); + const error = row.original.__errors?.[column.id]; + const rowIndex = row.index; + + // Special logging for item number fields to track their values + if (column.id === 'item_number' || column.id === 'sku' || column.id === 'itemnumber') { + console.log(`Rendering item number cell for field "${column.id}" with value: "${value}" in row ${rowIndex}`); + } + + // Create the props for our cell + const cellProps = { + value, + onChange: (newValue: any) => updateRows(rowIndex, column.id, newValue), + error, + field: { + ...enhancedField as Field, + handleUpcValidation: field.key === 'upc' || field.key === 'barcode' + ? async (upcValue: string) => { + try { + // Get the current supplier ID from the row + const supplierValue = row.getValue('supplier'); + console.log(`UPC validation called for "${upcValue}" with supplier "${supplierValue}" in row ${rowIndex}`); + + if (!supplierValue) { + console.log('No supplier selected, cannot validate UPC'); + return { + error: true, + message: "Please select a supplier before validating UPC" + }; + } + + // Store the current UPC value to make sure it's preserved + const originalUpcValue = upcValue; + + // Make the API call with both UPC and supplier + const result = await handleUpcValidation(upcValue, field.key); + + // Log the result for debugging + console.log('UPC validation API result:', result); + + // If we get a valid item number, update the appropriate field + if (result && !result.error && result.fieldKey && result.value) { + console.log(`UPC validation generated item number: ${result.value} for field ${result.fieldKey}`); + + // CRITICAL CHANGE: Create a special update function for this special case to avoid field loss + // This will update both the item number and UPC in a single operation + const updateBothItemNumberAndUpc = () => { + // Get current row data to preserve all values + const rowData = filteredData[rowIndex]; + if (!rowData) return; + + // Find the original row's index in the full data array + const originalIndex = data.findIndex(item => item.__index === rowData.__index); + if (originalIndex === -1) return; + + // Create a new data array with both fields updated + const newData = data.map((item, idx) => { + if (idx === originalIndex) { + // Create a copy with both the item number and UPC set + const updatedItem = { + ...item, + [result.fieldKey]: result.value, // Set item number + [field.key]: originalUpcValue // Set UPC + }; + + // Log the update + console.log(`Special update: Setting both ${result.fieldKey}="${result.value}" and ${field.key}="${originalUpcValue}"`); + + return updatedItem; + } + return item; + }); + + // Update the state with both fields set + setData(newData); + + // Run the hooks to validate the data + setTimeout(() => { + console.log(`Running validation hooks after dual-field update`); + updateData(newData, [originalIndex]) + .then(() => console.log(`Validation completed after dual-field update`)) + .catch(err => console.error(`Error in validation after dual-field update:`, err)); + }, 100); + }; + + // Use the special update function + updateBothItemNumberAndUpc(); + + // Return success status with the item number + return { + success: true, + itemNumber: result.value, + message: `Generated item number: ${result.value}` + }; + } + + // If there was an error, return it but don't touch the UPC value + if (result && result.error) { + console.log('UPC validation returned error:', result.message); + return result; + } + + return { success: false, message: 'No valid item number returned' }; + } catch (error) { + console.error('Error in UPC validation:', error); + return { error: true, message: String(error) }; + } + } + : undefined + }, + productLines, + sublines + }; + + // Use a more reliable key for item number and UPC fields + const isSkuField = field.key === 'sku' || field.key === 'itemnumber'; + const isUpcField = field.key === 'upc' || field.key === 'barcode'; + const cellKey = `${field.key}-${rowIndex}-${value}-${(isSkuField || isUpcField) ? Date.now() : ''}`; return ( - updateRows(rowIndex, column.id, newValue)} - error={error} - field={field as Field} - productLines={productLines} - sublines={sublines} - /> - ) +
+ +
+ ); }, size: (field as any).width || ( field.fieldType.type === "checkbox" ? 80 : field.fieldType.type === "select" ? 150 : 200 ), - }))) + } + })) ] return baseColumns - }, [fields, updateRows, data, copyValueDown, templates, applyTemplate, productLines, sublines, getTemplateDisplayText]) + }, [fields, updateRows, data, copyValueDown, templates, applyTemplate, productLines, sublines, getTemplateDisplayText, handleSkuGeneration, filteredData, updateData, toast]) const table = useReactTable({ data: filteredData, @@ -2861,6 +3686,203 @@ export const ValidationStep = ({ } }, [toast]); + // Add effect to validate UPCs and generate item numbers on initial load + // Use a ref to track whether initial validation has been performed + const initialValidationPerformedRef = useRef(false); + const lastDataLengthRef = useRef(0); + + useEffect(() => { + // Skip if no data or if fields aren't loaded + if (!data.length || !fieldsWithUpdatedOptions.length) return; + + // Skip if we've already done the initial validation and data length hasn't changed + // This prevents the cycle of data updates triggering more validations + if (initialValidationPerformedRef.current && data.length === lastDataLengthRef.current) { + return; + } + + // Update the ref values + lastDataLengthRef.current = data.length; + + console.log('Running initial UPC validation...'); + + // Process each row that has a UPC and supplier but no item number + const processRows = async () => { + // Create a copy of data to track changes + let newData = [...data]; + let hasChanges = false; + + // Create a mapping of already seen UPCs to avoid duplicates + const processedUpcMap = new Map(); + + // Track which rows were updated for debugging + const updatedRows: number[] = []; + + // Generate promises for all rows that need processing + const rowPromises = data.map(async (row, index) => { + // Cast row to any to work around TypeScript errors + const typedRow = row as any; + + // Check if this row has a UPC and supplier but no item number + const hasUpc = typedRow.upc || typedRow.barcode; + const hasSupplier = typedRow.supplier; + const hasItemNumber = typedRow.itemnumber || typedRow.sku; + + if (hasUpc && hasSupplier && !hasItemNumber) { + try { + // Use the UPC field (either upc or barcode) + const upcValue = typedRow.upc || typedRow.barcode; + const supplierValue = typedRow.supplier; + + // Skip if either value is missing + if (!upcValue || !supplierValue) return null; + + // Find the supplier field to get its options + const supplierField = Array.from(fields).find(f => f.key === 'supplier'); + + // Find the actual supplier ID if what we have is a display name + let supplierIdToUse = supplierValue; + + // Check if this is a select field and if we need to translate from name to ID + if (supplierField && supplierField.fieldType.type === 'select') { + const supplierOptions = (supplierField.fieldType as SelectFieldType).options; + // First check if supplier is already an ID (matched with an option.value) + const isAlreadyId = supplierOptions.some(opt => opt.value === supplierValue); + + if (!isAlreadyId) { + // If not an ID, find the option with matching display name and use its value (ID) + const option = supplierOptions.find(opt => + opt.label.toLowerCase() === String(supplierValue).toLowerCase() + ); + if (option) { + supplierIdToUse = option.value; + console.log(`Initial validation: Converted supplier name "${supplierValue}" to ID "${supplierIdToUse}"`); + } + } + } + + // Check if we already processed this UPC + const upcSupplierKey = `${upcValue}-${supplierIdToUse}`; + if (processedUpcMap.has(upcSupplierKey)) { + console.log(`Skipping duplicate UPC validation for ${upcSupplierKey}`); + return null; + } + + // Mark this UPC as processed + processedUpcMap.set(upcSupplierKey, true); + + console.log(`Validating UPC ${upcValue} for supplier ID ${supplierIdToUse} in row ${index}`); + + // Call the API to check UPC and generate item number + const response = await fetch( + `${config.apiUrl}/import/check-upc-and-generate-sku?upc=${encodeURIComponent(upcValue)}&supplierId=${encodeURIComponent(supplierIdToUse)}` + ); + + const responseData = await response.json(); + + if (!response.ok) { + // Handle 409 error (UPC already exists) + if (response.status === 409) { + console.log(`UPC ${upcValue} already exists - marking as error`); + + // Add error to the UPC field + const upcFieldKey = typedRow.upc ? 'upc' : 'barcode'; + + // Create error object + const error = { + level: 'error', + message: `UPC already exists for product ID ${responseData.existingProductId} with item number ${responseData.existingItemNumber}` + }; + + // Update row with error + newData[index] = { + ...newData[index], + __errors: { + ...(newData[index].__errors || {}), + [upcFieldKey]: error + } + }; + + hasChanges = true; + return null; + } + throw new Error('Failed to validate UPC'); + } + + // Get the generated item number + if (responseData.success && responseData.itemNumber) { + // Determine item number field key + const itemNumberKey = Object.keys(fieldsWithUpdatedOptions.find(f => f.key === 'itemnumber') ? { itemnumber: true } : { sku: true })[0]; + + console.log(`Generated item number ${responseData.itemNumber} for row ${index}, setting in field ${itemNumberKey}`); + + // Update the row with the generated item number + newData[index] = { + ...newData[index], + [itemNumberKey]: responseData.itemNumber + }; + + console.log(`Setting ${itemNumberKey} = "${responseData.itemNumber}" for row ${index}`); + updatedRows.push(index); + + hasChanges = true; + return { rowIndex: index, fieldKey: itemNumberKey, value: responseData.itemNumber }; + } + } catch (error) { + console.error(`Error processing row ${index}:`, error); + } + } + return null; + }); + + // Wait for all promises to resolve + const results = await Promise.all(rowPromises); + + // Update data if there were any changes + if (hasChanges) { + console.log(`Updating data with generated item numbers for rows: ${updatedRows.join(', ')}`); + + // Log the item numbers before update for debugging + updatedRows.forEach(rowIndex => { + const itemNumberKey = Object.keys(fieldsWithUpdatedOptions.find(f => f.key === 'itemnumber') ? { itemnumber: true } : { sku: true })[0]; + console.log(`Before update: Row ${rowIndex} ${itemNumberKey} = "${(newData[rowIndex] as Record)[itemNumberKey]}"`); + }); + + // Use a try-catch to handle any potential errors during update + try { + // Manually update the data state to ensure the changes are reflected + setData([...newData]); // This makes a shallow copy to trigger state update + + console.log('Successfully updated data with item numbers and UPC errors'); + + // Mark that we've performed initial validation + initialValidationPerformedRef.current = true; + + // Then run the async validation separately + setTimeout(() => { + updateData([...newData]) + .then(() => console.log('Async validation completed after item number update')) + .catch(err => console.error('Error in async validation:', err)); + }, 100); + + // Return the validation results for display updates + return results.filter(Boolean); + } catch (updateError) { + console.error('Error updating data:', updateError); + } + } else { + console.log('No changes needed from initial UPC validation'); + // Mark that we've performed initial validation even if no changes were made + initialValidationPerformedRef.current = true; + } + return []; + }; + + // Run the processing asynchronously without blocking page load + setTimeout(() => processRows(), 500); + + }, [data, fieldsWithUpdatedOptions, updateData, fields]); + return (
diff --git a/inventory/src/lib/react-spreadsheet-import/src/types.ts b/inventory/src/lib/react-spreadsheet-import/src/types.ts index cf6de29..c53c4dc 100644 --- a/inventory/src/lib/react-spreadsheet-import/src/types.ts +++ b/inventory/src/lib/react-spreadsheet-import/src/types.ts @@ -65,7 +65,7 @@ export type Data = { // Data model RSI uses for spreadsheet imports export type Fields = DeepReadonly[]> -export type Field = { +export type Field = { // UI-facing field label label: string // Field's unique identifier @@ -75,14 +75,14 @@ export type Field = { // Alternate labels used for fields' auto-matching, e.g. "fname" -> "firstName" alternateMatches?: string[] // Validations used for field entries - validations?: Validation[] + validations?: ValidationConfig[] // Field entry component fieldType: FieldType // UI-facing values shown to user as field examples pre-upload phase example?: string width?: number disabled?: boolean - onChange?: (value: string) => void + onChange?: (value: any, additionalData?: any) => void } export type FieldType = diff --git a/inventory/src/pages/Import.tsx b/inventory/src/pages/Import.tsx index 86a991d..ad781d5 100644 --- a/inventory/src/pages/Import.tsx +++ b/inventory/src/pages/Import.tsx @@ -23,6 +23,39 @@ const BASE_IMPORT_FIELDS = [ width: 220, validations: [{ rule: "required" as const, errorMessage: "Required", level: "error" as ErrorLevel }], }, + { + label: "Company", + key: "company", + description: "Company/Brand name", + fieldType: { + type: "select", + options: [], // Will be populated from API + }, + width: 200, + validations: [{ rule: "required", errorMessage: "Required", level: "error" }], + }, + { + label: "Line", + key: "line", + description: "Product line", + alternateMatches: ["collection"], + fieldType: { + type: "select", + options: [], // Will be populated dynamically based on company selection + }, + width: 180, + validations: [{ rule: "required", errorMessage: "Required", level: "error" }], + }, + { + label: "Sub Line", + key: "subline", + description: "Product sub-line", + fieldType: { + type: "select", + options: [], // Will be populated dynamically based on line selection + }, + width: 180, + }, { label: "UPC", key: "upc", @@ -78,7 +111,7 @@ const BASE_IMPORT_FIELDS = [ key: "item_number", description: "Internal item reference number", fieldType: { type: "input" }, - width: 120, + width: 130, validations: [ { rule: "required", errorMessage: "Required", level: "error" }, { rule: "unique", errorMessage: "Must be unique", level: "error" }, @@ -148,39 +181,6 @@ const BASE_IMPORT_FIELDS = [ width: 180, validations: [{ rule: "required", errorMessage: "Required", level: "error" }], }, - { - label: "Company", - key: "company", - description: "Company/Brand name", - fieldType: { - type: "select", - options: [], // Will be populated from API - }, - width: 200, - validations: [{ rule: "required", errorMessage: "Required", level: "error" }], - }, - { - label: "Line", - key: "line", - description: "Product line", - alternateMatches: ["collection"], - fieldType: { - type: "select", - options: [], // Will be populated dynamically based on company selection - }, - width: 180, - validations: [{ rule: "required", errorMessage: "Required", level: "error" }], - }, - { - label: "Sub Line", - key: "subline", - description: "Product sub-line", - fieldType: { - type: "select", - options: [], // Will be populated dynamically based on line selection - }, - width: 180, - }, { label: "Artist", key: "artist",