From b96a9f412a2d32b827b7b3fad1f7553d3ac899d6 Mon Sep 17 00:00:00 2001 From: Matt Date: Wed, 26 Feb 2025 11:24:05 -0500 Subject: [PATCH] Improve AI validate revert visuals, fix some regressions --- inventory-server/src/routes/ai-validation.js | 86 +++-- .../steps/ValidationStep/ValidationStep.tsx | 364 +++++++++++------- inventory/src/pages/Import.tsx | 9 +- 3 files changed, 296 insertions(+), 163 deletions(-) diff --git a/inventory-server/src/routes/ai-validation.js b/inventory-server/src/routes/ai-validation.js index f0726a2..58bddac 100644 --- a/inventory-server/src/routes/ai-validation.js +++ b/inventory-server/src/routes/ai-validation.js @@ -111,23 +111,44 @@ router.post("/debug", async (req, res) => { } try { - const avgTimeResults = await pool.query( - `SELECT AVG(duration_seconds) as avg_duration, - COUNT(*) as sample_count - FROM ai_validation_performance - WHERE prompt_length BETWEEN $1 * 0.8 AND $1 * 1.2`, - [debugResponse.promptLength] + // Instead of looking for similar prompt lengths, calculate an average processing rate + const rateResults = await pool.query( + `SELECT + AVG(duration_seconds / prompt_length) as avg_rate_per_char, + COUNT(*) as sample_count, + AVG(duration_seconds) as avg_duration + FROM ai_validation_performance` ); // Add estimated time to the response - if (avgTimeResults.rows && avgTimeResults.rows[0]) { + if (rateResults.rows && rateResults.rows[0] && rateResults.rows[0].avg_rate_per_char) { + // Calculate estimated time based on the rate and current prompt length + const rate = rateResults.rows[0].avg_rate_per_char; + const estimatedSeconds = Math.max(15, Math.round(rate * debugResponse.promptLength)); + debugResponse.estimatedProcessingTime = { - seconds: avgTimeResults.rows[0].avg_duration || null, - sampleCount: avgTimeResults.rows[0].sample_count || 0 + seconds: estimatedSeconds, + sampleCount: rateResults.rows[0].sample_count || 0, + avgRate: rate, + calculationMethod: "rate-based" }; - console.log("📊 Retrieved processing time estimate:", debugResponse.estimatedProcessingTime); + console.log("📊 Calculated time estimate using rate-based method:", { + rate: rate, + promptLength: debugResponse.promptLength, + estimatedSeconds: estimatedSeconds, + sampleCount: rateResults.rows[0].sample_count + }); } else { - console.log("📊 No processing time estimates available for prompt length:", debugResponse.promptLength); + // Fallback: Calculate a simple estimate based on prompt length (1 second per 1000 characters) + const estimatedSeconds = Math.max(15, Math.round(debugResponse.promptLength / 1000)); + console.log("📊 No rate data available, using fallback calculation"); + debugResponse.estimatedProcessingTime = { + seconds: estimatedSeconds, + sampleCount: 0, + isEstimate: true, + calculationMethod: "fallback" + }; + console.log("📊 Fallback time estimate:", debugResponse.estimatedProcessingTime); } } catch (queryError) { console.error("⚠️ Failed to query performance metrics:", queryError); @@ -812,29 +833,38 @@ router.post("/validate", async (req, res) => { // Insert performance data into the local PostgreSQL database await pool.query( `INSERT INTO ai_validation_performance - (prompt_length, product_count, start_time, end_time) - VALUES ($1, $2, $3, $4)`, - [promptLength, products.length, startTime, endTime] + (prompt_length, product_count, start_time, end_time, duration_seconds) + VALUES ($1, $2, $3, $4, $5)`, + [ + promptLength, + products.length, + startTime, + endTime, + (endTime - startTime) / 1000 + ] ); console.log("📊 Performance metrics inserted into database"); // Query for average processing time based on similar prompt lengths try { - const avgTimeResults = await pool.query( - `SELECT AVG(duration_seconds) as avg_duration, - COUNT(*) as sample_count - FROM ai_validation_performance - WHERE prompt_length BETWEEN $1 * 0.8 AND $1 * 1.2`, - [promptLength] + const rateResults = await pool.query( + `SELECT + AVG(duration_seconds / prompt_length) as avg_rate_per_char, + COUNT(*) as sample_count, + AVG(duration_seconds) as avg_duration + FROM ai_validation_performance` ); - if (avgTimeResults.rows && avgTimeResults.rows[0]) { - performanceMetrics.avgDuration = avgTimeResults.rows[0].avg_duration; - performanceMetrics.sampleCount = avgTimeResults.rows[0].sample_count; + if (rateResults.rows && rateResults.rows[0] && rateResults.rows[0].avg_rate_per_char) { + const rate = rateResults.rows[0].avg_rate_per_char; + performanceMetrics.avgRate = rate; + performanceMetrics.estimatedSeconds = Math.round(rate * promptLength); + performanceMetrics.sampleCount = rateResults.rows[0].sample_count; + performanceMetrics.calculationMethod = "rate-based"; } - console.log("📊 Performance metrics retrieved:", performanceMetrics); + console.log("📊 Performance metrics with rate calculation:", performanceMetrics); } catch (queryError) { console.error("⚠️ Failed to query performance metrics:", queryError); } @@ -854,7 +884,13 @@ router.post("/validate", async (req, res) => { res.json({ success: true, changeDetails: changeDetails, - performanceMetrics, + performanceMetrics: performanceMetrics || { + // Fallback: calculate a simple estimate + promptLength: promptLength, + processingTimeSeconds: Math.max(15, Math.round(promptLength / 1000)), + isEstimate: true, + productCount: products.length + }, ...aiResponse, }); } catch (parseError) { diff --git a/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStep/ValidationStep.tsx b/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStep/ValidationStep.tsx index 7c6c1d6..9505779 100644 --- a/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStep/ValidationStep.tsx +++ b/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStep/ValidationStep.tsx @@ -76,6 +76,8 @@ import { } from "@/components/ui/select" import type { GlobalSelections } from "../MatchColumnsStep/MatchColumnsStep" import { useQuery } from "@tanstack/react-query" +import { Badge } from "@/components/ui/badge" +import { CheckIcon } from "lucide-react" // Template interface interface Template { @@ -179,40 +181,29 @@ const EditableCell = memo(({ value, onChange, error, field, productLines, sublin // Never disable line field if it already has a value if (value) return false; - // The line field should be enabled if: - // 1. We have a company selected (even if product lines are still loading) - // 2. We have product lines available - return !((field.fieldType.type === 'select' || field.fieldType.type === 'multi-select') && - field.fieldType.options?.length) && !productLines?.length; + // The line field should be enabled if we have a company selected or product lines available + const hasCompanySelected = (field.fieldType.type === 'select' || field.fieldType.type === 'multi-select') && + field.fieldType.options && field.fieldType.options.length > 0; + const hasFetchedProductLines = productLines && productLines.length > 0; + + return !(hasCompanySelected || hasFetchedProductLines); } if (field.key === 'subline') { // Never disable subline field if it already has a value if (value) return false; - // The subline field should be enabled if: - // 1. We have a line selected (even if sublines are still loading) - // 2. We have sublines available - return !((field.fieldType.type === 'select' || field.fieldType.type === 'multi-select') && - field.fieldType.options?.length) && !sublines?.length; + // The subline field should be enabled if we have a line selected or sublines available + const hasLineSelected = (field.fieldType.type === 'select' || field.fieldType.type === 'multi-select') && + field.fieldType.options && field.fieldType.options.length > 0; + const hasFetchedSublines = sublines && sublines.length > 0; + + return !(hasLineSelected || hasFetchedSublines); } // For other fields, use the disabled property return field.disabled; }, [field.key, field.disabled, field.fieldType, productLines, sublines, value]); - // For debugging - useEffect(() => { - if (field.key === 'subline') { - console.log('Subline field state:', { - disabled: field.disabled, - isFieldDisabled, - value, - options: field.fieldType.type === 'select' ? field.fieldType.options : [], - sublines, - hasSublines: sublines && sublines.length > 0 - }); - } - }, [field, value, sublines, isFieldDisabled]); const handleWheel = useCallback((e: React.WheelEvent) => { const commandList = e.currentTarget; @@ -288,12 +279,6 @@ const EditableCell = memo(({ value, onChange, error, field, productLines, sublin if (fieldType.type === "select") { // For line and subline fields, ensure we're using the latest options if (field.key === 'line') { - // Log current state for debugging - console.log('Getting display value for line:', { - value, - productLines: productLines?.length ?? 0, - options: fieldType.options?.length ?? 0 - }); // First try to find in productLines if available if (productLines?.length) { @@ -313,7 +298,6 @@ const EditableCell = memo(({ value, onChange, error, field, productLines, sublin return fallbackOptionLine.label; } } - console.log('Unable to find display value for line:', value); return value; } if (field.key === 'subline') { @@ -1141,6 +1125,9 @@ export const ValidationStep = ({ }: Props) => { const { translations, fields, onClose, onSubmit, rowHook, tableHook, allowInvalidSubmit } = useRsi(); const { toast } = useToast(); + + // Track which changes have been reverted + const [revertedChanges, setRevertedChanges] = useState>(new Set()); // Fetch product lines when company is selected const { data: productLines } = useQuery({ @@ -1245,28 +1232,47 @@ export const ValidationStep = ({ const fieldsWithUpdatedOptions = useMemo(() => { return Array.from(fields as ReadonlyFields).map(field => { if (field.key === 'line') { + // Check if we have product lines available + const hasProductLines = productLines && productLines.length > 0; + + // For line field, ensure we have the proper options return { ...field, fieldType: { ...field.fieldType, - options: productLines || (field.fieldType.type === 'select' ? field.fieldType.options : []), + // Use fetched product lines if available, otherwise keep existing options + options: hasProductLines + ? productLines + : (field.fieldType.type === 'select' || field.fieldType.type === 'multi-select') + ? field.fieldType.options + : [] }, - // Only disable if no company is selected or if product lines failed to load - // when a company is selected - disabled: !globalSelections?.company || (globalSelections?.company && productLines !== undefined && productLines.length === 0) + // The line field should only be disabled if no company is selected AND no product lines available + disabled: !globalSelections?.company && !hasProductLines } as Field; } + if (field.key === 'subline') { + // Check if we have sublines available + const hasSublines = sublines && sublines.length > 0; + + // For subline field, ensure we have the proper options return { ...field, fieldType: { ...field.fieldType, - options: sublines || (field.fieldType.type === 'select' ? field.fieldType.options : []), + // Use fetched sublines if available, otherwise keep existing options + options: hasSublines + ? sublines + : (field.fieldType.type === 'select' || field.fieldType.type === 'multi-select') + ? field.fieldType.options + : [] }, - // Enable subline field if we have a global line selection or if we have sublines available - disabled: !globalSelections?.line && (!sublines || sublines.length === 0) + // The subline field should only be disabled if no line is selected AND no sublines available + disabled: !globalSelections?.line && !hasSublines } as Field; } + return field; }); }, [fields, productLines, sublines, globalSelections?.company, globalSelections?.line]); @@ -1653,6 +1659,9 @@ export const ValidationStep = ({ // Store the original data before any changes const originalDataCopy = [...data]; + // Reset reverted changes when starting a new validation + setRevertedChanges(new Set()); + setIsAiValidating(true); const startTime = new Date(); setAiValidationProgress({ @@ -1677,12 +1686,28 @@ export const ValidationStep = ({ setAiValidationProgress(prev => { // Calculate progress percentage let progressPercent = 0; - if (prev.estimatedSeconds && prev.estimatedSeconds > 0) { - // Cap at 99% if we exceed estimated time but aren't done yet - progressPercent = Math.min(99, Math.floor((elapsedSeconds / prev.estimatedSeconds) * 100)); + + // Log current state for debugging + console.log('Timer update - Current state:', { + step: prev.step, + hasEstimatedTime: !!prev.estimatedSeconds, + estimatedSeconds: prev.estimatedSeconds, + elapsedSeconds + }); + + // Progress is based on step, not just time + if (prev.step === 5) { // Completed step + progressPercent = 100; + } else if (prev.estimatedSeconds && prev.estimatedSeconds > 0) { + // Cap at 95% if we exceed estimated time but aren't done yet + progressPercent = Math.min(95, Math.floor((elapsedSeconds / prev.estimatedSeconds) * 100)); + console.log('Using time-based progress:', progressPercent); } else { - // If no estimate, use step-based progress (25% per step), also capped at 99% - progressPercent = Math.min(99, (prev.step * 25) + Math.min(24, Math.floor((elapsedSeconds % 10) / 10 * 25))); + // If no estimate, use step-based progress, each step is 20% progress plus some time-based progress within step + const baseProgress = (prev.step - 1) * 20; + const stepProgress = Math.min(20, Math.floor((elapsedSeconds % 30) / 30 * 20)); + progressPercent = Math.min(95, baseProgress + stepProgress); + console.log('Using step-based progress:', progressPercent); } // Extract the base status message without any time information @@ -1696,7 +1721,7 @@ export const ValidationStep = ({ status: baseStatus }; }); - }, 1000); + }, 1000) as unknown as NodeJS.Timeout; // Clean the data to ensure we only send what's needed const cleanedData = data.map(item => { @@ -1719,13 +1744,30 @@ export const ValidationStep = ({ if (debugResponse.ok) { const debugData = await debugResponse.json(); + console.log('Debug response details:', { + hasEstimatedTime: !!debugData.estimatedProcessingTime, + estimatedTimeSeconds: debugData.estimatedProcessingTime?.seconds, + calculationMethod: debugData.estimatedProcessingTime?.calculationMethod || 'unknown', + avgRate: debugData.estimatedProcessingTime?.avgRate, + promptLength: debugData.promptLength, + fullResponse: debugData + }); if (debugData.estimatedProcessingTime?.seconds) { - setAiValidationProgress(prev => ({ - ...prev, - estimatedSeconds: debugData.estimatedProcessingTime.seconds, - promptLength: debugData.promptLength - })); + console.log('Setting estimated time:', debugData.estimatedProcessingTime.seconds); + setAiValidationProgress(prev => { + const newState = { + ...prev, + estimatedSeconds: debugData.estimatedProcessingTime.seconds, + promptLength: debugData.promptLength + }; + console.log('New progress state with time:', newState); + return newState; + }); + } else { + console.log('No estimated time in debug response'); } + } else { + console.error('Debug response not OK:', debugResponse.status); } } catch (estimateError) { console.error('Error getting time estimate:', estimateError); @@ -1736,7 +1778,9 @@ export const ValidationStep = ({ setAiValidationProgress(prev => ({ ...prev, status: "Sending data to AI service...", - step: 2 + step: 2, + estimatedSeconds: prev.estimatedSeconds, + promptLength: prev.promptLength })); const response = await fetch(`${config.apiUrl}/ai-validation/validate`, { @@ -1772,7 +1816,9 @@ export const ValidationStep = ({ status: "Processing AI response...", step: 3, // Update with actual metrics from the server - estimatedSeconds: result.performanceMetrics.processingTimeSeconds || prev.estimatedSeconds, + estimatedSeconds: result.performanceMetrics.estimatedSeconds || + result.performanceMetrics.processingTimeSeconds || + prev.estimatedSeconds, promptLength: result.performanceMetrics.promptLength || prev.promptLength, progressPercent: 75 // 75% complete when we're processing the AI response })); @@ -1789,18 +1835,25 @@ export const ValidationStep = ({ ...prev, status: "Applying corrections...", step: 4, - progressPercent: 90 // 90% complete when applying corrections + progressPercent: 90, // 90% complete when applying corrections + estimatedSeconds: prev.estimatedSeconds, + promptLength: prev.promptLength })); // Update the data with AI suggestions if (result.correctedData && Array.isArray(result.correctedData)) { // Process data to properly handle comma-separated values for multi-select fields - const processedData = result.correctedData.map((corrected: any) => { - const processed = { ...corrected }; + const processedData = result.correctedData.map((corrected: any, index: number) => { + // Start with original data to preserve __index and other metadata + const original = data[index] || {}; + const processed = { ...original, ...corrected }; // Process each field Object.keys(processed).forEach(key => { + if (key.startsWith('__')) return; // Skip metadata fields + const fieldConfig = fields.find(f => f.key === key); + if (!fieldConfig) return; // Skip if no matching field found // Handle multi-select fields (comma-separated values) if (fieldConfig?.fieldType.type === 'multi-select' && typeof processed[key] === 'string') { @@ -1809,7 +1862,6 @@ export const ValidationStep = ({ } // For select and multi-select fields, ensure we're working with IDs - // We don't convert IDs to display names here because we want to preserve IDs in the data if (fieldConfig?.fieldType.type === 'select' || fieldConfig?.fieldType.type === 'multi-select') { const options = fieldConfig.fieldType.options || []; @@ -1820,7 +1872,10 @@ export const ValidationStep = ({ if (!isAlreadyId) { // Try to find the option by label - const matchingOption = options.find(opt => opt.label === processed[key]); + const matchingOption = options.find(opt => + opt.label.toLowerCase() === processed[key].toLowerCase() || + String(opt.label) === String(processed[key]) + ); if (matchingOption) { // Convert label to ID const originalValue = processed[key]; @@ -1833,12 +1888,17 @@ export const ValidationStep = ({ // Handle array of values (multi-select) if (Array.isArray(processed[key])) { processed[key] = processed[key].map((val: string | number) => { + if (val === null || val === undefined) return val; + // Check if the value is already an ID const isAlreadyId = options.some(opt => String(opt.value) === String(val)); if (!isAlreadyId) { - // Try to find the option by label - const matchingOption = options.find(opt => opt.label === val); + // Try to find the option by label (case insensitive) + const matchingOption = options.find(opt => + typeof val === 'string' && typeof opt.label === 'string' && + opt.label.toLowerCase() === val.toLowerCase() + ); if (matchingOption) { // Convert label to ID return matchingOption.value; @@ -1853,16 +1913,9 @@ export const ValidationStep = ({ return processed; }); - // Preserve the __index and __errors from the original data - const newData = processedData.map((item: any, idx: number) => ({ - ...item, - __index: data[idx]?.__index, - __errors: data[idx]?.__errors, - })); - console.log('About to update data with AI corrections:', { originalDataSample: data.slice(0, 2), - newDataSample: newData.slice(0, 2), + processedDataSample: processedData.slice(0, 2), correctionCount: result.changes?.length || 0 }); @@ -1873,7 +1926,7 @@ export const ValidationStep = ({ // Validate the data with the hooks const validatedData = await addErrorsAndRunHooks( - newData, + processedData, currentFields, rowHook, tableHook @@ -1917,7 +1970,7 @@ export const ValidationStep = ({ } catch (error) { console.error('Error validating AI corrections:', error); // Fall back to basic update without validation - setData(newData); + setData(processedData); // Still show the result dialog even if validation failed setAiValidationDetails({ @@ -1933,7 +1986,9 @@ export const ValidationStep = ({ setAiValidationProgress(prev => ({ ...prev, status: "Validation complete!", - step: 5 + step: 5, + estimatedSeconds: prev.estimatedSeconds, + promptLength: prev.promptLength })); setTimeout(() => { @@ -1949,7 +2004,9 @@ export const ValidationStep = ({ setAiValidationProgress(prev => ({ ...prev, status: "Validation failed", - step: -1 + step: -1, + estimatedSeconds: prev.estimatedSeconds, + promptLength: prev.promptLength })); } finally { // Clear the interval when we're done (success or error) @@ -1962,7 +2019,10 @@ export const ValidationStep = ({ // Only set to 100% when actually complete (or in error state) setAiValidationProgress(prev => ({ ...prev, - progressPercent: prev.step === -1 ? prev.progressPercent : 100 // Only show 100% if successful completion + progressPercent: prev.step === -1 ? prev.progressPercent : 100, // Only show 100% if successful completion + estimatedSeconds: prev.estimatedSeconds, + promptLength: prev.promptLength, + elapsedSeconds: prev.elapsedSeconds })); } }; @@ -2083,17 +2143,20 @@ export const ValidationStep = ({ return; } - const currentProduct = data[productIndex]; - if (!currentProduct) { - console.error(`Cannot revert: current product at index ${productIndex} not found`); + const currentDataIndex = data.findIndex(d => d.__index === originalProduct.__index); + if (currentDataIndex === -1) { + console.error(`Cannot revert: current product with __index ${originalProduct.__index} not found`); return; } - + const currentProduct = data[currentDataIndex]; + // Get the original value in a type-safe way const originalValue = fieldKey in originalProduct ? (originalProduct as Record)[fieldKey] : undefined; - console.log(`Reverting change to field "${fieldKey}" for product at index ${productIndex}`, { + console.log(`Reverting change to field "${fieldKey}" for product at index ${currentDataIndex}`, { + originalIndex: productIndex, + currentIndex: currentDataIndex, original: originalValue, current: fieldKey in currentProduct ? (currentProduct as Record)[fieldKey] : undefined }); @@ -2101,25 +2164,43 @@ export const ValidationStep = ({ // Create a new data array with the reverted field const newData = [...data]; + // Get the field configuration + // Create a new product object with the reverted field - newData[productIndex] = { - ...newData[productIndex], + newData[currentDataIndex] = { + ...newData[currentDataIndex], [fieldKey]: originalValue - } as Data & ExtendedMeta; // Cast to ensure type safety + }; // Update the data state setData(newData); // Re-validate to update error states - updateData(newData, [productIndex]); + updateData(newData, [currentDataIndex]); + + // Add to the set of reverted changes + const revertKey = `${productIndex}:${fieldKey}`; + setRevertedChanges(prev => { + const newSet = new Set(prev); + newSet.add(revertKey); + return newSet; + }); // Show success notification toast({ title: "Change reverted", description: `Reverted the change to "${fieldKey}"`, + variant: "default", // Make sure it's visible + duration: 3000, // Show for 3 seconds }); }; + // Function to check if a change has been reverted + const isChangeReverted = (productIndex: number, fieldKey: string): boolean => { + const revertKey = `${productIndex}:${fieldKey}`; + return revertedChanges.has(revertKey); + }; + return (
({

{aiValidationProgress.status}

- {aiValidationProgress.estimatedSeconds && aiValidationProgress.elapsedSeconds !== undefined && aiValidationProgress.step > 0 && aiValidationProgress.step < 5 && ( -
- {(() => { - // Calculate time remaining using the elapsed seconds - const elapsedSeconds = aiValidationProgress.elapsedSeconds; - const totalEstimatedSeconds = aiValidationProgress.estimatedSeconds; - const remainingSeconds = Math.max(0, totalEstimatedSeconds - elapsedSeconds); - - // Format time remaining - if (remainingSeconds < 60) { - return `Approximately ${Math.round(remainingSeconds)} seconds remaining`; - } else { - const minutes = Math.floor(remainingSeconds / 60); - const seconds = Math.round(remainingSeconds % 60); - return `Approximately ${minutes}m ${seconds}s remaining`; - } - })()} - {aiValidationProgress.promptLength && ( -

- Prompt length: {aiValidationProgress.promptLength.toLocaleString()} characters -

- )} -
- )} + {(() => { + + + return aiValidationProgress.estimatedSeconds && + aiValidationProgress.elapsedSeconds !== undefined && + aiValidationProgress.step > 0 && + aiValidationProgress.step < 5 && ( +
+ {(() => { + // Calculate time remaining using the elapsed seconds + const elapsedSeconds = aiValidationProgress.elapsedSeconds; + const totalEstimatedSeconds = aiValidationProgress.estimatedSeconds; + const remainingSeconds = Math.max(0, totalEstimatedSeconds - elapsedSeconds); + + // Format time remaining + if (remainingSeconds < 60) { + return `Approximately ${Math.round(remainingSeconds)} seconds remaining`; + } else { + const minutes = Math.floor(remainingSeconds / 60); + const seconds = Math.round(remainingSeconds % 60); + return `Approximately ${minutes}m ${seconds}s remaining`; + } + })()} + {aiValidationProgress.promptLength && ( +

+ Prompt length: {aiValidationProgress.promptLength.toLocaleString()} characters +

+ )} +
+ ); + })()}
@@ -2261,12 +2349,23 @@ export const ValidationStep = ({ {product.changes.map((change, j) => { const field = fields.find(f => f.key === change.field); + const isReverted = isChangeReverted(product.productIndex, change.field); + return ( - - {field?.label || change.field} + + + {field?.label || change.field} + {isReverted && ( + + Reverted + + )} +
-
{getFieldDisplayValue(change.field, change.original)}
+
+ {getFieldDisplayValue(change.field, change.original)} +
{/* Show raw value if it's an ID */} {change.original && typeof change.original === 'string' && !isNaN(Number(change.original)) && @@ -2277,7 +2376,9 @@ export const ValidationStep = ({
-
{getFieldDisplayValue(change.field, change.corrected)}
+
+ {getFieldDisplayValue(change.field, change.corrected)} +
{/* Show raw value if it's an ID */} {change.corrected && typeof change.corrected === 'string' && !isNaN(Number(change.corrected)) && @@ -2288,33 +2389,28 @@ export const ValidationStep = ({
- + {isReverted ? ( + + ) : ( + + )}
diff --git a/inventory/src/pages/Import.tsx b/inventory/src/pages/Import.tsx index a52edc5..a33b711 100644 --- a/inventory/src/pages/Import.tsx +++ b/inventory/src/pages/Import.tsx @@ -27,7 +27,7 @@ const BASE_IMPORT_FIELDS = [ label: "UPC", key: "upc", description: "Universal Product Code/Barcode", - alternateMatches: ["barcode", "bar code", "jan", "ean"], + alternateMatches: ["barcode", "bar code", "jan", "ean", "upc code"], fieldType: { type: "input" }, width: 140, validations: [ @@ -52,7 +52,7 @@ const BASE_IMPORT_FIELDS = [ label: "Notions #", key: "notions_no", description: "Internal notions number", - alternateMatches: ["notions #"], + alternateMatches: ["notions #","nmc"], fieldType: { type: "input" }, width: 110, validations: [ @@ -65,7 +65,7 @@ const BASE_IMPORT_FIELDS = [ label: "Name", key: "name", description: "Product name/title", - alternateMatches: ["sku description"], + alternateMatches: ["sku description","product name"], fieldType: { type: "input" }, width: 500, validations: [ @@ -137,7 +137,7 @@ const BASE_IMPORT_FIELDS = [ label: "Case Pack", key: "case_qty", description: "Number of units per case", - alternateMatches: ["mc qty","case qty","case pack"], + alternateMatches: ["mc qty","case qty","case pack","box ct"], fieldType: { type: "input" }, width: 50, validations: [ @@ -170,6 +170,7 @@ const BASE_IMPORT_FIELDS = [ label: "Line", key: "line", description: "Product line", + alternateMatches: ["collection"], fieldType: { type: "select", options: [], // Will be populated dynamically based on company selection