More validate step changes/fixes

This commit is contained in:
2025-03-04 23:51:40 -05:00
parent 7a43428e76
commit 05bac73c45
12 changed files with 1098 additions and 361 deletions

View File

@@ -1427,10 +1427,10 @@ export function ProductSearchDialog({ isOpen, onClose, onTemplateCreated }: Prod
</div>
</div>
<div className="space-y-2">
<div className="space-y-2 max-w-[610px]">
<Label htmlFor="categories">Categories</Label>
<Popover modal={false}>
<PopoverTrigger asChild className="max-w-[calc(800px-3.5rem)]">
<PopoverTrigger asChild className="w-full">
<Button
variant="outline"
role="combobox"

View File

@@ -19,7 +19,7 @@ import {
import { useQuery } from "@tanstack/react-query"
import config from "@/config"
import { Button } from "@/components/ui/button"
import { CheckCircle2, AlertCircle, EyeIcon, EyeOffIcon, ArrowRightIcon, XIcon, FileSpreadsheetIcon, LinkIcon, FileIcon, CheckIcon, ChevronsUpDown } from "lucide-react"
import { CheckCircle2, AlertCircle, EyeIcon, EyeOffIcon, ArrowRightIcon, XIcon, FileSpreadsheetIcon, LinkIcon, CheckIcon, ChevronsUpDown } from "lucide-react"
import { Separator } from "@/components/ui/separator"
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
import { Badge } from "@/components/ui/badge"
@@ -34,8 +34,6 @@ import {
CommandList,
} from "@/components/ui/command"
import { cn } from "@/lib/utils"
import { Switch } from "@/components/ui/switch"
import { Label } from "@/components/ui/label"
export type MatchColumnsProps<T extends string> = {
data: RawData[]

View File

@@ -4,7 +4,6 @@ import { UploadStep } from "./UploadStep/UploadStep"
import { SelectHeaderStep } from "./SelectHeaderStep/SelectHeaderStep"
import { SelectSheetStep } from "./SelectSheetStep/SelectSheetStep"
import { mapWorkbook } from "../utils/mapWorkbook"
import { ValidationStep } from "./ValidationStep/ValidationStep"
import { ValidationStepNew } from "./ValidationStepNew"
import { ImageUploadStep } from "./ImageUploadStep/ImageUploadStep"
import { addErrorsAndRunHooks } from "./ValidationStep/utils/dataMutations"
@@ -118,7 +117,17 @@ export const UploadFlow = ({ state, onNext, onBack }: Props) => {
onNext({ type: StepType.selectSheet, workbook })
}
}}
setInitialState={onNext}
setInitialState={(state) => {
// Ensure the state has the correct type
if (state.type === StepType.validateData) {
onNext({
type: StepType.validateData,
data: state.data,
isFromScratch: state.isFromScratch,
globalSelections: undefined
});
}
}}
/>
)
case StepType.selectSheet:
@@ -238,7 +247,15 @@ export const UploadFlow = ({ state, onNext, onBack }: Props) => {
})
}
}}
onSubmit={onSubmit}
onSubmit={(data, file) => {
// Create a Result object from the array data
const result = {
validData: data,
invalidData: [],
all: data
};
onSubmit(result, file);
}}
/>
)
default:

View File

@@ -79,7 +79,7 @@ export const AiValidationDialogs: React.FC<AiValidationDialogsProps> = ({
<div
className="h-full bg-primary transition-all duration-500"
style={{
width: `${aiValidationProgress.progressPercent ?? (aiValidationProgress.step / 5) * 100}%`,
width: `${aiValidationProgress.progressPercent ?? Math.floor((aiValidationProgress.step / 5) * 100)}%`,
backgroundColor: aiValidationProgress.step === -1 ? 'var(--destructive)' : undefined
}}
/>

View File

@@ -1,4 +1,4 @@
import React, { useState, useCallback } from 'react'
import { useState, useCallback, useMemo, memo } from 'react'
import { Field } from '../../../types'
import { Loader2 } from 'lucide-react'
import { cn } from '@/lib/utils'
@@ -7,6 +7,12 @@ import InputCell from './cells/InputCell'
import MultiInputCell from './cells/MultiInputCell'
import SelectCell from './cells/SelectCell'
import CheckboxCell from './cells/CheckboxCell'
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip"
// Define an error object type
type ErrorObject = {
@@ -18,158 +24,249 @@ type ErrorObject = {
/**
* ValidationIcon - Renders an appropriate icon based on error level
*/
const ValidationIcon = ({ error }: { error: ErrorObject }) => {
const ValidationIcon = memo(({ error }: { error: ErrorObject }) => {
const iconClasses = "h-4 w-4"
switch(error.level) {
case 'error':
return <AlertCircle className={cn(iconClasses, "text-destructive")} />
case 'warning':
return <AlertTriangle className={cn(iconClasses, "text-amber-500")} />
case 'info':
return <Info className={cn(iconClasses, "text-blue-500")} />
default:
return <AlertCircle className={iconClasses} />
}
}
const icon = useMemo(() => {
switch(error.level) {
case 'error':
return <AlertCircle className={cn(iconClasses, "text-destructive")} />;
case 'warning':
return <AlertTriangle className={cn(iconClasses, "text-amber-500")} />;
case 'info':
return <Info className={cn(iconClasses, "text-blue-500")} />;
default:
return <AlertCircle className={cn(iconClasses, "text-muted-foreground")} />;
}
}, [error.level, iconClasses]);
return (
<TooltipProvider>
<Tooltip delayDuration={200}>
<TooltipTrigger asChild>
<div className="cursor-help">{icon}</div>
</TooltipTrigger>
<TooltipContent className="max-w-[300px] text-wrap break-words">
<p>{error.message}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
);
});
export interface ValidationCellProps<T extends string> {
rowIndex: number
field: Field<T>
value: any
onChange: (value: any) => void
errors: ErrorObject[]
isValidatingUpc?: boolean
isInValidatingRow?: boolean
fieldKey?: string
options?: any[]
isLoading?: boolean
key?: string
}
const ValidationCell = <T extends string>({
rowIndex,
field,
value,
onChange,
errors,
isValidatingUpc = false
}: ValidationCellProps<T>) => {
const [isEditing, setIsEditing] = useState(false)
// Get the most severe error
const currentError = errors.length > 0 ? errors[0] : null
// Memoized loader component
const LoadingIndicator = memo(() => (
<div className="flex items-center justify-center h-9 rounded-md border border-input bg-gray-50 px-3">
<Loader2 className="h-4 w-4 animate-spin text-muted-foreground" />
<span className="ml-2 text-sm text-muted-foreground">Validating...</span>
</div>
));
// Determine if field is disabled
const isFieldDisabled = field.disabled || false
// Memoized error display component
const ErrorDisplay = memo(({ errors, isFocused }: { errors: ErrorObject[], isFocused: boolean }) => {
if (!errors || errors.length === 0) return null;
// Check if this is a UPC field for validation
const isUpcField = field.key === 'upc' ||
(field.fieldType as any)?.upcField ||
field.key.toString().toLowerCase().includes('upc')
// Render cell contents based on field type
const renderCellContent = () => {
// If we're validating UPC, show a spinner
if (isValidatingUpc) {
return (
<div className="flex items-center justify-center w-full min-h-[32px]">
<Loader2 className="h-4 w-4 animate-spin" />
</div>
)
}
const fieldType = field.fieldType.type
// Handle different field types
switch (fieldType) {
case 'input':
return (
<InputCell<T>
field={field}
value={value}
onChange={onChange}
onStartEdit={() => setIsEditing(true)}
onEndEdit={() => setIsEditing(false)}
hasErrors={errors.length > 0}
isMultiline={(field.fieldType as any).multiline}
isPrice={(field.fieldType as any).price}
/>
)
case 'multi-input':
return (
<MultiInputCell<T>
field={field}
value={value}
onChange={onChange}
onStartEdit={() => setIsEditing(true)}
onEndEdit={() => setIsEditing(false)}
hasErrors={errors.length > 0}
separator={(field.fieldType as any).separator || ','}
isMultiline={(field.fieldType as any).multiline}
isPrice={(field.fieldType as any).price}
/>
)
case 'select':
return (
<SelectCell<T>
field={field}
value={value}
onChange={onChange}
onStartEdit={() => setIsEditing(true)}
onEndEdit={() => setIsEditing(false)}
hasErrors={errors.length > 0}
options={field.fieldType.type === 'select' ? field.fieldType.options : undefined}
/>
)
case 'multi-select':
return (
<MultiInputCell<T>
field={field}
value={value}
onChange={onChange}
onStartEdit={() => setIsEditing(true)}
onEndEdit={() => setIsEditing(false)}
hasErrors={errors.length > 0}
separator={(field.fieldType as any).separator || ','}
options={field.fieldType.type === 'multi-select' ? field.fieldType.options : undefined}
/>
)
case 'checkbox':
return (
<CheckboxCell<T>
field={field}
value={value}
onChange={onChange}
hasErrors={errors.length > 0}
booleanMatches={(field.fieldType as any).booleanMatches}
/>
)
default:
return (
<div className="p-2">
{String(value || '')}
</div>
)
}
}
// Main cell rendering
return (
<div className={cn(
"relative w-full",
isFieldDisabled ? "opacity-70 pointer-events-none" : ""
)}>
{renderCellContent()}
<>
<div className="absolute right-1 top-1/2 -translate-y-1/2">
<ValidationIcon error={errors[0]} />
</div>
{/* Show error icon if there are errors and we're not editing */}
{currentError && !isEditing && (
<div className="absolute right-1 top-1/2 -translate-y-1/2">
<ValidationIcon error={currentError} />
{isFocused && (
<div className="text-xs text-destructive p-1 mt-1 bg-destructive/5 rounded-sm">
{errors.map((error, i) => (
<div key={i} className="py-0.5">{error.message}</div>
))}
</div>
)}
</div>
)
}
</>
);
});
export default ValidationCell
// Main ValidationCell component - now with proper memoization
const ValidationCell = memo(<T extends string>(props: ValidationCellProps<T>) => {
const {
field,
value,
onChange,
errors,
isValidatingUpc = false,
fieldKey,
options } = props;
// State for showing/hiding error messages
const [isFocused, setIsFocused] = useState(false);
// Handlers for edit state
const handleStartEdit = useCallback(() => {
setIsFocused(true);
}, []);
const handleEndEdit = useCallback(() => {
setIsFocused(false);
}, []);
// Check if this cell has errors
const hasErrors = errors && errors.length > 0;
// Show loading state when validating UPC fields
if (isValidatingUpc && (fieldKey === 'upc' || fieldKey === 'barcode' || fieldKey === 'item_number')) {
return <LoadingIndicator />;
}
// Handle cases where field might be undefined or incomplete
if (!field || !field.fieldType) {
return (
<div className="p-2 text-sm text-muted-foreground">
Error: Invalid field configuration
</div>
);
}
// Get the field type safely
const fieldType = field.fieldType.type || 'input';
// Helper for safely accessing fieldType properties
const getFieldTypeProp = (propName: string, defaultValue: any = undefined) => {
if (!field.fieldType) return defaultValue;
return (field.fieldType as any)[propName] !== undefined ?
(field.fieldType as any)[propName] :
defaultValue;
};
// Memoize the cell content to prevent unnecessary re-renders
const cellContent = useMemo(() => {
// Handle custom options for select fields first
if ((fieldType === 'select' || fieldType === 'multi-select') && options && options.length > 0) {
try {
return (
<SelectCell
field={field}
value={value}
onChange={onChange}
onStartEdit={handleStartEdit}
onEndEdit={handleEndEdit}
hasErrors={hasErrors}
options={options}
/>
);
} catch (error) {
console.error("Error rendering SelectCell with custom options:", error);
return <div className="p-2 text-destructive">Error rendering field</div>;
}
}
// Standard rendering based on field type
try {
switch (fieldType) {
case 'input':
return (
<InputCell
field={field}
value={value}
onChange={onChange}
onStartEdit={handleStartEdit}
onEndEdit={handleEndEdit}
hasErrors={hasErrors}
isMultiline={getFieldTypeProp('multiline', false)}
isPrice={getFieldTypeProp('price', false)}
/>
);
case 'multi-input':
return (
<MultiInputCell
field={field}
value={value}
onChange={onChange}
onStartEdit={handleStartEdit}
onEndEdit={handleEndEdit}
hasErrors={hasErrors}
separator={getFieldTypeProp('separator', ',')}
isMultiline={getFieldTypeProp('multiline', false)}
isPrice={getFieldTypeProp('price', false)}
/>
);
case 'select':
case 'multi-select':
return (
<SelectCell
field={field}
value={value}
onChange={onChange}
onStartEdit={handleStartEdit}
onEndEdit={handleEndEdit}
hasErrors={hasErrors}
options={getFieldTypeProp('options', [])}
/>
);
case 'checkbox':
return (
<CheckboxCell
field={field}
value={value}
onChange={onChange}
hasErrors={hasErrors}
booleanMatches={getFieldTypeProp('booleanMatches', {})}
/>
);
default:
return (
<InputCell
field={field}
value={value}
onChange={onChange}
onStartEdit={handleStartEdit}
onEndEdit={handleEndEdit}
hasErrors={hasErrors}
/>
);
}
} catch (error) {
console.error(`Error rendering cell of type ${fieldType}:`, error);
return (
<div className="p-2 text-destructive">
Error rendering field
</div>
);
}
}, [
fieldType,
field,
value,
onChange,
handleStartEdit,
handleEndEdit,
hasErrors,
options,
getFieldTypeProp
]);
return (
<div className={cn(
"relative",
hasErrors && "space-y-1"
)}>
{cellContent}
{/* Render errors if any exist */}
{hasErrors && <ErrorDisplay errors={errors} isFocused={isFocused} />}
</div>
);
});
ValidationCell.displayName = 'ValidationCell';
export default ValidationCell;

View File

@@ -1,4 +1,4 @@
import React, { useState } from 'react'
import React, { useState, useEffect, useRef, useCallback, useMemo } from 'react'
import { useValidationState, Props } from '../hooks/useValidationState'
import ValidationTable from './ValidationTable'
import { Button } from '@/components/ui/button'
@@ -38,12 +38,10 @@ const ValidationContainer = <T extends string>({
const {
data,
filteredData,
isValidating,
validationErrors,
rowSelection,
setRowSelection,
updateRow,
hasErrors,
templates,
selectedTemplateId,
applyTemplate,
@@ -51,21 +49,475 @@ const ValidationContainer = <T extends string>({
getTemplateDisplayText,
filters,
updateFilters,
setTemplateState,
templateState,
saveTemplate,
loadTemplates,
setData,
fields
} = validationState
// Add state for tracking product lines and sublines per row
const [rowProductLines, setRowProductLines] = useState<Record<string, any[]>>({});
const [rowSublines, setRowSublines] = useState<Record<string, any[]>>({});
const [isLoadingLines, setIsLoadingLines] = useState<Record<string, boolean>>({});
const [isLoadingSublines, setIsLoadingSublines] = useState<Record<string, boolean>>({});
// Add UPC validation state
const [isValidatingUpc, setIsValidatingUpc] = useState(false);
const [validatingUpcRows, setValidatingUpcRows] = useState<Set<number>>(new Set());
// Store item numbers in a separate state to avoid updating the main data
const [itemNumbers, setItemNumbers] = useState<Record<number, string>>({});
// Cache for UPC validation results
const processedUpcMapRef = useRef(new Map<string, string>());
const initialUpcValidationDoneRef = useRef(false);
// Function to check if a specific row is being validated - memoized
const isRowValidatingUpc = useCallback((rowIndex: number): boolean => {
return validatingUpcRows.has(rowIndex);
}, [validatingUpcRows]);
// Apply all pending updates to the data state
const applyItemNumbersToData = useCallback(() => {
if (Object.keys(itemNumbers).length === 0) return;
setData(prevData => {
const newData = [...prevData];
// Apply all item numbers without changing other data
Object.entries(itemNumbers).forEach(([indexStr, itemNumber]) => {
const index = parseInt(indexStr);
if (index >= 0 && index < newData.length) {
// Only update the item_number field and leave everything else unchanged
newData[index] = {
...newData[index],
item_number: itemNumber
};
}
});
return newData;
});
// Clear the item numbers state after applying
setItemNumbers({});
}, [setData, itemNumbers]);
// Function to fetch product lines for a specific company - memoized
const fetchProductLines = useCallback(async (rowIndex: string | number, companyId: string) => {
try {
// Only fetch if we have a valid company ID
if (!companyId) return;
// Set loading state for this row
setIsLoadingLines(prev => ({ ...prev, [rowIndex]: true }));
// Fetch product lines from API
const response = await fetch(`/api/import/product-lines/${companyId}`);
if (!response.ok) {
throw new Error(`Failed to fetch product lines: ${response.status}`);
}
const productLines = await response.json();
// Store the product lines for this specific row
setRowProductLines(prev => ({ ...prev, [rowIndex]: productLines }));
return productLines;
} catch (error) {
console.error('Error fetching product lines:', error);
} finally {
// Clear loading state
setIsLoadingLines(prev => ({ ...prev, [rowIndex]: false }));
}
}, []);
// Function to fetch sublines for a specific line - memoized
const fetchSublines = useCallback(async (rowIndex: string | number, lineId: string) => {
try {
// Only fetch if we have a valid line ID
if (!lineId) return;
// Set loading state for this row
setIsLoadingSublines(prev => ({ ...prev, [rowIndex]: true }));
// Fetch sublines from API
const response = await fetch(`/api/import/sublines/${lineId}`);
if (!response.ok) {
throw new Error(`Failed to fetch sublines: ${response.status}`);
}
const sublines = await response.json();
// Store the sublines for this specific row
setRowSublines(prev => ({ ...prev, [rowIndex]: sublines }));
return sublines;
} catch (error) {
console.error('Error fetching sublines:', error);
} finally {
// Clear loading state
setIsLoadingSublines(prev => ({ ...prev, [rowIndex]: false }));
}
}, []);
// Function to validate UPC with the API - memoized
const validateUpc = useCallback(async (rowIndex: number, supplierId: string, upcValue: string): Promise<{ success: boolean, itemNumber?: string }> => {
try {
// Skip if either value is missing
if (!supplierId || !upcValue) {
return { success: false };
}
// Check if we've already validated this UPC/supplier combination
const cacheKey = `${supplierId}-${upcValue}`;
if (processedUpcMapRef.current.has(cacheKey)) {
const cachedItemNumber = processedUpcMapRef.current.get(cacheKey);
if (cachedItemNumber) {
// Just update the item numbers state, not the main data
setItemNumbers(prev => ({
...prev,
[rowIndex]: cachedItemNumber
}));
return { success: true, itemNumber: cachedItemNumber };
}
return { success: false };
}
// Make API call to validate UPC
const response = await fetch(`/api/import/check-upc-and-generate-sku?upc=${encodeURIComponent(upcValue)}&supplierId=${encodeURIComponent(supplierId)}`);
// Process the response
if (response.status === 409) {
// UPC already exists - show validation error
const errorData = await response.json();
// Update the validation errors in the main data
// This is necessary for errors to display correctly
setData(prevData => {
const newData = [...prevData];
const rowToUpdate = newData.find((_, idx) => idx === rowIndex);
if (rowToUpdate) {
const fieldKey = 'upc' in rowToUpdate ? 'upc' : 'barcode';
// Only update the errors field
newData[rowIndex] = {
...rowToUpdate,
__errors: {
...(rowToUpdate.__errors || {}),
[fieldKey]: {
level: 'error',
message: `UPC already exists (${errorData.existingItemNumber})`
}
}
};
}
return newData;
});
return { success: false };
} else if (response.ok) {
// Successful validation - update item number
const responseData = await response.json();
if (responseData.success && responseData.itemNumber) {
// Store in cache
processedUpcMapRef.current.set(cacheKey, responseData.itemNumber);
// Update the item numbers state, not the main data
setItemNumbers(prev => ({
...prev,
[rowIndex]: responseData.itemNumber
}));
// Clear any UPC errors if they exist (this requires updating the main data)
setData(prevData => {
const newData = [...prevData];
const rowToUpdate = newData.find((_, idx) => idx === rowIndex);
if (rowToUpdate && rowToUpdate.__errors) {
const updatedErrors = { ...rowToUpdate.__errors };
delete updatedErrors.upc;
delete updatedErrors.barcode;
// Only update if errors need to be cleared
if (Object.keys(updatedErrors).length !== Object.keys(rowToUpdate.__errors).length) {
newData[rowIndex] = {
...rowToUpdate,
__errors: Object.keys(updatedErrors).length > 0 ? updatedErrors : undefined
};
return newData;
}
}
return prevData; // Return unchanged if no error updates needed
});
return { success: true, itemNumber: responseData.itemNumber };
}
}
return { success: false };
} catch (error) {
console.error(`Error validating UPC for row ${rowIndex}:`, error);
return { success: false };
}
}, [data, setData]);
// Apply item numbers when validation is complete
useEffect(() => {
if (!isValidatingUpc && Object.keys(itemNumbers).length > 0) {
// Only update the main data state once all validation is complete
applyItemNumbersToData();
}
}, [isValidatingUpc, itemNumbers, applyItemNumbersToData]);
// Optimized batch validation function - memoized
const validateAllUPCs = useCallback(async () => {
// Skip if we've already done the initial validation
if (initialUpcValidationDoneRef.current) {
return;
}
// Mark that we've done the initial validation
initialUpcValidationDoneRef.current = true;
console.log('Starting UPC validation...');
// Set validation state
setIsValidatingUpc(true);
// Find all rows that have both supplier and UPC/barcode
const rowsToValidate = data
.map((row, index) => ({ row, index }))
.filter(({ row }) => {
const rowAny = row as Record<string, any>;
const hasSupplier = rowAny.supplier;
const hasUpc = rowAny.upc || rowAny.barcode;
return hasSupplier && hasUpc;
});
const totalRows = rowsToValidate.length;
console.log(`Found ${totalRows} rows with both supplier and UPC`);
if (totalRows === 0) {
setIsValidatingUpc(false);
return;
}
// Mark all rows as being validated
setValidatingUpcRows(new Set(rowsToValidate.map(({ index }) => index)));
// Process the rows in batches for better performance
const BATCH_SIZE = 10;
try {
for (let i = 0; i < rowsToValidate.length; i += BATCH_SIZE) {
const batch = rowsToValidate.slice(i, Math.min(i + BATCH_SIZE, rowsToValidate.length));
// Process this batch in parallel
await Promise.all(
batch.map(async ({ row, index }) => {
try {
const rowAny = row as Record<string, any>;
const supplierId = rowAny.supplier.toString();
const upcValue = (rowAny.upc || rowAny.barcode).toString();
// Validate the UPC
await validateUpc(index, supplierId, upcValue);
// Remove this row from the validating set
setValidatingUpcRows(prev => {
const newSet = new Set(prev);
newSet.delete(index);
return newSet;
});
} catch (error) {
console.error(`Error processing row ${index}:`, error);
}
})
);
}
} catch (error) {
console.error('Error in batch validation:', error);
} finally {
// Reset validation state
setIsValidatingUpc(false);
setValidatingUpcRows(new Set());
console.log('Completed UPC validation');
}
}, [data, validateUpc]);
// Enhanced updateRow function - memoized
const enhancedUpdateRow = useCallback(async (rowIndex: number, fieldKey: T, value: any) => {
// Update the main data state
updateRow(rowIndex, fieldKey, value);
// Now handle any additional logic for specific fields
const rowData = filteredData[rowIndex];
// If updating company field, fetch product lines
if (fieldKey === 'company' && value) {
// Clear any existing line/subline values for this row if company changes
const originalIndex = data.findIndex(item => item.__index === rowData?.__index);
if (originalIndex !== -1) {
// Update the data to clear line and subline
setData(prevData => {
const newData = [...prevData];
newData[originalIndex] = {
...newData[originalIndex],
line: undefined,
subline: undefined
};
return newData;
});
}
// Fetch product lines for the new company if rowData has __index
if (rowData && rowData.__index) {
await fetchProductLines(rowData.__index, value.toString());
}
// If company field is being updated AND there's a UPC value, validate UPC
if (rowData) {
const rowDataAny = rowData as Record<string, any>;
if (rowDataAny.upc || rowDataAny.barcode) {
const upcValue = rowDataAny.upc || rowDataAny.barcode;
// Mark this row as being validated
setValidatingUpcRows(prev => {
const newSet = new Set(prev);
newSet.add(rowIndex);
return newSet;
});
// Set global validation state
setIsValidatingUpc(true);
await validateUpc(rowIndex, value.toString(), upcValue.toString());
// Update validation state
setValidatingUpcRows(prev => {
const newSet = new Set(prev);
newSet.delete(rowIndex);
if (newSet.size === 0) {
setIsValidatingUpc(false);
}
return newSet;
});
}
}
}
// If updating line field, fetch sublines
if (fieldKey === 'line' && value) {
// Clear any existing subline value for this row
const originalIndex = data.findIndex(item => item.__index === rowData?.__index);
if (originalIndex !== -1) {
// Update the data to clear subline only
setData(prevData => {
const newData = [...prevData];
newData[originalIndex] = {
...newData[originalIndex],
subline: undefined
};
return newData;
});
}
// Fetch sublines for the new line if rowData has __index
if (rowData && rowData.__index) {
await fetchSublines(rowData.__index, value.toString());
}
}
// If updating UPC/barcode field AND there's a supplier value, validate UPC
if ((fieldKey === 'upc' || fieldKey === 'barcode') && value && rowData) {
const rowDataAny = rowData as Record<string, any>;
if (rowDataAny.supplier) {
// Mark this row as being validated
setValidatingUpcRows(prev => {
const newSet = new Set(prev);
newSet.add(rowIndex);
return newSet;
});
// Set global validation state
setIsValidatingUpc(true);
await validateUpc(rowIndex, rowDataAny.supplier.toString(), value.toString());
// Update validation state
setValidatingUpcRows(prev => {
const newSet = new Set(prev);
newSet.delete(rowIndex);
if (newSet.size === 0) {
setIsValidatingUpc(false);
}
return newSet;
});
}
}
}, [data, filteredData, updateRow, fetchProductLines, fetchSublines, validateUpc, setData]);
// When data changes, fetch product lines and sublines for rows that have company/line values
useEffect(() => {
// Skip if there's no data
if (!data.length) return;
// Process each row to set up initial line/subline options
data.forEach(row => {
const rowId = row.__index;
if (!rowId) return; // Skip rows without an index
// If row has company but no product lines fetched yet, fetch them
if (row.company && !rowProductLines[rowId]) {
fetchProductLines(rowId, row.company.toString());
}
// If row has line but no sublines fetched yet, fetch them
if (row.line && !rowSublines[rowId]) {
fetchSublines(rowId, row.line.toString());
}
});
}, [data, rowProductLines, rowSublines, fetchProductLines, fetchSublines]);
// Validate UPCs on initial data load
useEffect(() => {
// Skip if there's no data or we've already done the validation
if (data.length === 0 || initialUpcValidationDoneRef.current) return;
// Use a short timeout to allow the UI to render first
const timer = setTimeout(() => {
validateAllUPCs();
}, 100);
return () => clearTimeout(timer);
}, [data, validateAllUPCs]);
// Use AI validation hook
const aiValidation = useAiValidation<T>(
data,
setData,
fields,
validationState.rowHook,
validationState.tableHook
// Create a wrapper function that adapts the rowHook to the expected signature
validationState.rowHook ?
async (row) => {
// Call the original rowHook and return the row itself instead of just Meta
await validationState.rowHook(row, 0, data);
return row;
} :
undefined,
// Create a wrapper function that adapts the tableHook to the expected signature
validationState.tableHook ?
async (rows) => {
// Call the original tableHook and return the rows themselves
await validationState.tableHook(rows);
return rows;
} :
undefined
);
const { translations } = useRsi<T>()
@@ -73,13 +525,17 @@ const ValidationContainer = <T extends string>({
// State for product search dialog
const [isProductSearchDialogOpen, setIsProductSearchDialogOpen] = useState(false)
const handleNext = () => {
// Handle next button click - memoized
const handleNext = useCallback(() => {
// Make sure any pending item numbers are applied
applyItemNumbersToData();
// Call the onNext callback with the validated data
onNext?.(data)
}
}, [onNext, data, applyItemNumbersToData]);
// Delete selected rows
const deleteSelectedRows = () => {
// Delete selected rows - memoized
const deleteSelectedRows = useCallback(() => {
const selectedRowIndexes = Object.keys(rowSelection).map(Number);
const newData = data.filter((_, index) => !selectedRowIndexes.includes(index));
setData(newData);
@@ -89,7 +545,68 @@ const ValidationContainer = <T extends string>({
? "Row deleted"
: `${selectedRowIndexes.length} rows deleted`
);
}
}, [data, rowSelection, setData, setRowSelection]);
// Enhanced ValidationTable component that's aware of item numbers
const EnhancedValidationTable = useCallback(({
data,
...props
}: React.ComponentProps<typeof ValidationTable<T>>) => {
// Merge the item numbers with the data for display purposes only
const enhancedData = useMemo(() => {
if (Object.keys(itemNumbers).length === 0) return data;
// Create a new array with the item numbers merged in
return data.map((row, index) => {
if (itemNumbers[index]) {
return { ...row, item_number: itemNumbers[index] };
}
return row;
});
}, [data]);
return <ValidationTable<T> data={enhancedData} {...props} />;
}, [itemNumbers]);
// Memoize the ValidationTable to prevent unnecessary re-renders
const renderValidationTable = useMemo(() => (
<EnhancedValidationTable
data={filteredData}
fields={validationState.fields}
updateRow={enhancedUpdateRow}
rowSelection={rowSelection}
setRowSelection={setRowSelection}
validationErrors={validationErrors}
isValidatingUpc={isRowValidatingUpc}
validatingUpcRows={Array.from(validatingUpcRows)}
filters={filters}
templates={templates}
applyTemplate={applyTemplate}
getTemplateDisplayText={getTemplateDisplayText}
rowProductLines={rowProductLines}
rowSublines={rowSublines}
isLoadingLines={isLoadingLines}
isLoadingSublines={isLoadingSublines}
/>
), [
EnhancedValidationTable,
filteredData,
validationState.fields,
enhancedUpdateRow,
rowSelection,
setRowSelection,
validationErrors,
isRowValidatingUpc,
validatingUpcRows,
filters,
templates,
applyTemplate,
getTemplateDisplayText,
rowProductLines,
rowSublines,
isLoadingLines,
isLoadingSublines
]);
return (
<div className="flex flex-col h-[calc(100vh-10rem)] overflow-hidden">
@@ -151,20 +668,7 @@ const ValidationContainer = <T extends string>({
<div className="px-8 pb-6 flex-1 min-h-0">
<div className="rounded-md border h-full flex flex-col overflow-hidden">
<div className="flex-1 overflow-auto">
<ValidationTable<T>
data={filteredData}
fields={validationState.fields}
updateRow={updateRow}
rowSelection={rowSelection}
setRowSelection={setRowSelection}
validationErrors={validationErrors}
isValidatingUpc={validationState.isValidatingUpc}
validatingUpcRows={validationState.validatingUpcRows}
filters={filters}
templates={templates}
applyTemplate={applyTemplate}
getTemplateDisplayText={getTemplateDisplayText}
/>
{renderValidationTable}
</div>
</div>
</div>
@@ -276,8 +780,7 @@ const ValidationContainer = <T extends string>({
</Button>
<Button
disabled={hasErrors}
onClick={handleNext}
onClick={handleNext}
>
{translations.validationStep.nextButtonTitle || "Next"}
</Button>

View File

@@ -4,22 +4,12 @@ import {
getCoreRowModel,
flexRender,
createColumnHelper,
RowSelectionState
} from '@tanstack/react-table'
RowSelectionState} from '@tanstack/react-table'
import { Fields, Field } from '../../../types'
import { RowData, Template } from '../hooks/useValidationState'
import { Checkbox } from '@/components/ui/checkbox'
import ValidationCell from './ValidationCell'
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table"
import { useRsi } from '../../../hooks/useRsi'
import { Button } from '@/components/ui/button'
import SearchableTemplateSelect from './SearchableTemplateSelect'
// Define a simple Error type locally to avoid import issues
@@ -42,9 +32,105 @@ interface ValidationTableProps<T extends string> {
templates: Template[]
applyTemplate: (templateId: string, rowIndexes: number[]) => void
getTemplateDisplayText: (templateId: string | null) => string
rowProductLines?: Record<string, any[]>
rowSublines?: Record<string, any[]>
isLoadingLines?: Record<string, boolean>
isLoadingSublines?: Record<string, boolean>
[key: string]: any
}
// Memoized cell component to prevent unnecessary re-renders
const MemoizedCell = React.memo(
({
rowIndex,
field,
value,
errors,
isValidatingUpc,
fieldOptions,
isOptionsLoading,
updateRow,
columnId
}: {
rowIndex: number
field: Field<any>
value: any
errors: ErrorType[]
isValidatingUpc: (rowIndex: number) => boolean
fieldOptions?: any[]
isOptionsLoading?: boolean
updateRow: (rowIndex: number, key: any, value: any) => void
columnId: string
}) => {
const handleChange = (newValue: any) => {
updateRow(rowIndex, columnId, newValue);
};
return (
<ValidationCell
value={value}
field={field}
onChange={handleChange}
errors={errors || []}
isValidatingUpc={isValidatingUpc(rowIndex)}
fieldKey={columnId}
options={fieldOptions}
isLoading={isOptionsLoading}
/>
);
},
// Custom comparison function for the memo
(prevProps, nextProps) => {
// Re-render only if any of these props changed
return (
prevProps.value === nextProps.value &&
prevProps.errors === nextProps.errors &&
prevProps.fieldOptions === nextProps.fieldOptions &&
prevProps.isOptionsLoading === nextProps.isOptionsLoading &&
prevProps.isValidatingUpc(prevProps.rowIndex) === nextProps.isValidatingUpc(nextProps.rowIndex)
);
}
);
// Memoized template cell component
const MemoizedTemplateCell = React.memo(
({
rowIndex,
templateValue,
templates,
applyTemplate,
getTemplateDisplayText
}: {
rowIndex: number
templateValue: string | null
templates: Template[]
applyTemplate: (templateId: string, rowIndexes: number[]) => void
getTemplateDisplayText: (templateId: string | null) => string
}) => {
const handleTemplateChange = (value: string) => {
applyTemplate(value, [rowIndex]);
};
return (
<SearchableTemplateSelect
templates={templates}
value={templateValue || ''}
onValueChange={handleTemplateChange}
getTemplateDisplayText={(template) =>
template ? getTemplateDisplayText(template) : 'Select template'
}
/>
);
},
// Custom comparison function for the memo
(prevProps, nextProps) => {
return (
prevProps.templateValue === nextProps.templateValue &&
prevProps.templates === nextProps.templates
);
}
);
const ValidationTable = <T extends string>({
data,
fields,
@@ -53,128 +139,147 @@ const ValidationTable = <T extends string>({
updateRow,
validationErrors,
isValidatingUpc,
validatingUpcRows,
filters,
templates,
applyTemplate,
getTemplateDisplayText,
...props
}: ValidationTableProps<T>) => {
rowProductLines = {},
rowSublines = {},
isLoadingLines = {},
isLoadingSublines = {}}: ValidationTableProps<T>) => {
const { translations } = useRsi<T>()
const columnHelper = createColumnHelper<RowData<T>>()
// Build table columns
// Define columns for the table
const columns = useMemo(() => {
// Selection column
const selectionColumn = columnHelper.display({
id: 'selection',
id: 'select',
header: ({ table }) => (
<Checkbox
checked={table.getIsAllRowsSelected()}
onCheckedChange={(value) => table.toggleAllRowsSelected(!!value)}
aria-label="Select all rows"
/>
<div className="flex justify-center">
<Checkbox
checked={table.getIsAllPageRowsSelected()}
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
aria-label="Select all"
/>
</div>
),
cell: ({ row }) => (
<Checkbox
checked={row.getIsSelected()}
onCheckedChange={(value) => row.toggleSelected(!!value)}
aria-label="Select row"
/>
<div className="flex justify-center">
<Checkbox
checked={row.getIsSelected()}
onCheckedChange={(value) => row.toggleSelected(!!value)}
aria-label="Select row"
/>
</div>
),
size: 40,
})
size: 50,
});
// Add template column
// Template column
const templateColumn = columnHelper.display({
id: 'template',
header: "Template",
header: 'Template',
cell: ({ row }) => {
try {
// Only render the component if templates are available
if (!templates || templates.length === 0) {
return (
<Button variant="outline" className="w-full justify-between" disabled>
Loading templates...
</Button>
);
}
return (
<SearchableTemplateSelect
templates={templates}
value={row.original.__template || ""}
onValueChange={(value: string) => {
try {
// Apply template to this row
applyTemplate(value, [row.index]);
} catch (error) {
console.error("Error applying template in cell:", error);
}
}}
getTemplateDisplayText={getTemplateDisplayText}
defaultBrand={row.original.company as string || ""}
/>
);
} catch (error) {
console.error("Error rendering template cell:", error);
return (
<Button variant="outline" className="w-full text-destructive">
Error loading templates
</Button>
);
}
const rowIndex = row.index;
return (
<MemoizedTemplateCell
rowIndex={rowIndex}
templateValue={row.original.__template || null}
templates={templates}
applyTemplate={applyTemplate}
getTemplateDisplayText={getTemplateDisplayText}
/>
);
},
size: 200,
});
// Create columns for each field
const fieldColumns = fields.map(field => {
// Get the field width directly from the field definition
// These are exactly the values defined in Import.tsx
const fieldWidth = field.width || (
field.fieldType.type === "checkbox" ? 80 :
field.fieldType.type === "select" ? 150 :
field.fieldType.type === "multi-select" ? 200 :
(field.fieldType.type === "input" || field.fieldType.type === "multi-input") &&
(field.fieldType as any).multiline ? 300 :
150
);
return columnHelper.accessor(
(row: RowData<T>) => row[field.key as keyof typeof row] as any,
(row: RowData<T>) => row[field.key as keyof typeof row],
{
id: String(field.key),
id: field.key,
header: field.label,
cell: ({ row, column, getValue }) => {
const rowIndex = row.index
const key = column.id as T
const value = getValue()
const errors = validationErrors.get(rowIndex)?.[key] || []
// Create a properly typed field object
const typedField: Field<T> = {
label: field.label,
key: field.key as T,
alternateMatches: field.alternateMatches as string[] | undefined,
validations: field.validations as any[] | undefined,
fieldType: field.fieldType,
example: field.example,
width: field.width,
disabled: field.disabled,
onChange: field.onChange
cell: ({ row, column }) => {
try {
const rowIndex = row.index;
const value = row.getValue(column.id);
const errors = validationErrors.get(rowIndex)?.[column.id] || [];
const rowId = row.original?.__index;
// Determine if we have custom options for this field
let fieldOptions;
let isOptionsLoading = false;
// Handle line field - use company-specific product lines
if (field.key === 'line' && rowId && rowProductLines[rowId]) {
fieldOptions = rowProductLines[rowId];
isOptionsLoading = isLoadingLines[rowId] || false;
}
// Handle subline field - use line-specific sublines
else if (field.key === 'subline' && rowId && rowSublines[rowId]) {
fieldOptions = rowSublines[rowId];
isOptionsLoading = isLoadingSublines[rowId] || false;
}
// Cast the field type for ValidationCell
const typedField = field as Field<string>;
return (
<MemoizedCell
rowIndex={rowIndex}
field={typedField}
value={value}
errors={errors}
isValidatingUpc={isValidatingUpc}
fieldOptions={fieldOptions}
isOptionsLoading={isOptionsLoading}
updateRow={updateRow}
columnId={column.id}
/>
);
} catch (error) {
console.error(`Error rendering cell for column ${column.id}:`, error);
return (
<div className="p-2 text-destructive text-sm">
Error rendering cell
</div>
);
}
return (
<ValidationCell<T>
rowIndex={rowIndex}
field={typedField}
value={value}
onChange={(newValue) => updateRow(rowIndex, key, newValue)}
errors={errors}
isValidatingUpc={isValidatingUpc(rowIndex)}
/>
)
},
size: (field as any).width || (
field.fieldType.type === "checkbox" ? 80 :
field.fieldType.type === "select" ? 150 :
200
),
size: fieldWidth,
}
)
})
);
});
return [selectionColumn, templateColumn, ...fieldColumns]
}, [columnHelper, fields, updateRow, validationErrors, isValidatingUpc, templates, applyTemplate, getTemplateDisplayText])
return [selectionColumn, templateColumn, ...fieldColumns];
}, [
columnHelper,
fields,
templates,
applyTemplate,
getTemplateDisplayText,
rowProductLines,
rowSublines,
isLoadingLines,
isLoadingSublines,
validationErrors,
isValidatingUpc,
updateRow
]);
// Initialize table
const table = useReactTable({
@@ -186,66 +291,84 @@ const ValidationTable = <T extends string>({
enableRowSelection: true,
onRowSelectionChange: setRowSelection,
getCoreRowModel: getCoreRowModel(),
})
});
return (
<Table>
<TableHeader className="sticky top-0 bg-muted z-10">
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => (
<TableHead
key={header.id}
style={{
width: header.getSize(),
minWidth: header.getSize(),
}}
>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext()
)}
</TableHead>
))}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => (
<TableRow
key={row.id}
data-state={row.getIsSelected() && "selected"}
className={validatingUpcRows.includes(row.index) ? "opacity-70" : ""}
>
{row.getVisibleCells().map((cell) => (
<TableCell
key={cell.id}
className="p-2 align-top"
style={{
width: cell.column.getSize(),
minWidth: cell.column.getSize(),
}}
>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell colSpan={columns.length} className="h-24 text-center">
{filters?.showErrorsOnly
? translations.validationStep.noRowsMessageWhenFiltered || "No rows with errors found."
: translations.validationStep.noRowsMessage || "No rows found."}
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
)
}
// Apply filters to rows if needed
const filteredRows = useMemo(() => {
let rows = table.getRowModel().rows;
if (filters?.showErrorsOnly) {
rows = rows.filter(row => {
const rowIndex = row.index;
return validationErrors.has(rowIndex) &&
Object.values(validationErrors.get(rowIndex) || {}).some(errors => errors.length > 0);
});
}
return rows;
}, [table, filters, validationErrors]);
export default ValidationTable
return (
<div className="h-full flex flex-col">
<div className="overflow-auto flex-1">
<table className="w-full border-separate border-spacing-0" style={{ tableLayout: 'fixed' }}>
<thead className="sticky top-0 z-10 bg-background border-b h-5">
<tr>
{table.getHeaderGroups()[0].headers.map((header) => (
<th
key={header.id}
style={{
width: `${header.getSize()}px`,
minWidth: `${header.getSize()}px`,
}}
className="h-10 py-3 px-3 text-left text-muted-foreground font-medium text-sm bg-muted"
>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext()
)}
</th>
))}
</tr>
</thead>
<tbody>
{filteredRows.length ? (
filteredRows.map((row) => (
<tr
key={row.id}
data-state={row.getIsSelected() ? "selected" : undefined}
className={validationErrors.has(row.index) ? "bg-red-50/30" : ""}
>
{row.getVisibleCells().map((cell) => (
<td
key={cell.id}
style={{
width: `${cell.column.getSize()}px`,
minWidth: `${cell.column.getSize()}px`,
}}
className="p-1 align-middle border-b border-muted"
>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</td>
))}
</tr>
))
) : (
<tr>
<td colSpan={columns.length} className="h-24 text-center">
{filters?.showErrorsOnly
? translations.validationStep.noRowsMessageWhenFiltered || "No rows with errors found."
: translations.validationStep.noRowsMessage || "No rows found."}
</td>
</tr>
)}
</tbody>
</table>
</div>
</div>
);
};
export default ValidationTable;

View File

@@ -90,7 +90,6 @@ const InputCell = <T extends string>({
onChange={(e) => setInputValue(e.target.value)}
onFocus={handleFocus}
onBlur={handleBlur}
placeholder={field.description}
className={cn(
"min-h-[80px] resize-none",
outlineClass,
@@ -105,7 +104,6 @@ const InputCell = <T extends string>({
onChange={(e) => setInputValue(e.target.value)}
onFocus={handleFocus}
onBlur={handleBlur}
placeholder={field.description}
autoFocus
className={cn(
outlineClass,
@@ -121,7 +119,7 @@ const InputCell = <T extends string>({
hasErrors ? "border-destructive" : "border-input"
)}
>
{isPrice ? getDisplayValue() : (inputValue || <span className="text-muted-foreground">{field.description}</span>)}
{isPrice ? getDisplayValue() : (inputValue)}
</div>
)
)}

View File

@@ -177,7 +177,7 @@ const MultiInputCell = <T extends string>({
)
})
) : (
<span className="text-muted-foreground">{field.description || "Select options..."}</span>
<span className="text-muted-foreground">{ "Select options..."}</span>
)}
</div>
<ChevronsUpDown className="h-4 w-4 shrink-0 opacity-50" />

View File

@@ -65,7 +65,7 @@ const SelectCell = <T extends string>({
// Get current display value
const displayValue = value ?
selectOptions.find(option => String(option.value) === String(value))?.label || String(value) :
field.description || 'Select...';
'Select...';
const handleSelect = (selectedValue: string) => {
onChange(selectedValue);

View File

@@ -1,9 +1,10 @@
import * as XLSX from "xlsx"
import type { RawData } from "../types"
export const mapWorkbook = (workbook: XLSX.WorkBook): RawData[] => {
const firstSheetName = workbook.SheetNames[0]
const worksheet = workbook.Sheets[firstSheetName]
export const mapWorkbook = (workbook: XLSX.WorkBook, sheetName?: string): RawData[] => {
// Use the provided sheetName or default to the first sheet
const sheetToUse = sheetName || workbook.SheetNames[0]
const worksheet = workbook.Sheets[sheetToUse]
const data = XLSX.utils.sheet_to_json<string[]>(worksheet, {
header: 1,

View File

@@ -31,7 +31,7 @@ const BASE_IMPORT_FIELDS = [
type: "select",
options: [], // Will be populated from API
},
width: 200,
width: 220,
validations: [{ rule: "required", errorMessage: "Required", level: "error" }],
},
{
@@ -43,7 +43,7 @@ const BASE_IMPORT_FIELDS = [
type: "select",
options: [], // Will be populated dynamically based on company selection
},
width: 180,
width: 220,
validations: [{ rule: "required", errorMessage: "Required", level: "error" }],
},
{
@@ -54,7 +54,7 @@ const BASE_IMPORT_FIELDS = [
type: "select",
options: [], // Will be populated dynamically based on line selection
},
width: 180,
width: 220,
},
{
label: "UPC",
@@ -86,7 +86,7 @@ const BASE_IMPORT_FIELDS = [
description: "Supplier's product identifier",
alternateMatches: ["sku", "item#", "mfg item #", "item", "supplier #"],
fieldType: { type: "input" },
width: 180,
width: 130,
validations: [
{ rule: "required", errorMessage: "Required", level: "error" },
{ rule: "unique", errorMessage: "Must be unique", level: "error" },
@@ -98,7 +98,7 @@ const BASE_IMPORT_FIELDS = [
description: "Internal notions number",
alternateMatches: ["notions #","nmc"],
fieldType: { type: "input" },
width: 110,
width: 100,
validations: [
{ rule: "required", errorMessage: "Required", level: "error" },
{ rule: "unique", errorMessage: "Must be unique", level: "error" },
@@ -133,12 +133,12 @@ const BASE_IMPORT_FIELDS = [
],
},
{
label: "Qty Per Unit",
label: "Min Qty",
key: "qty_per_unit",
description: "Quantity of items per individual unit",
alternateMatches: ["inner pack", "inner", "min qty", "unit qty", "min. order qty", "supplier qty/unit"],
fieldType: { type: "input" },
width: 90,
width: 80,
validations: [
{ rule: "required", errorMessage: "Required", level: "error" },
{ rule: "regex", value: "^[0-9]+$", errorMessage: "Must be a number", level: "error" },
@@ -165,7 +165,7 @@ const BASE_IMPORT_FIELDS = [
description: "Number of units per case",
alternateMatches: ["mc qty","case qty","case pack","box ct"],
fieldType: { type: "input" },
width: 50,
width: 100,
validations: [
{ rule: "regex", value: "^[0-9]+$", errorMessage: "Must be a number", level: "error" },
],
@@ -178,7 +178,7 @@ const BASE_IMPORT_FIELDS = [
type: "select",
options: [], // Will be populated from API
},
width: 180,
width: 200,
validations: [{ rule: "required", errorMessage: "Required", level: "error" }],
},
{
@@ -189,7 +189,7 @@ const BASE_IMPORT_FIELDS = [
type: "select",
options: [], // Will be populated from API
},
width: 180,
width: 200,
},
{
label: "ETA Date",
@@ -256,12 +256,12 @@ const BASE_IMPORT_FIELDS = [
validations: [{ rule: "required", errorMessage: "Required", level: "error" }],
},
{
label: "Country Of Origin",
label: "COO",
key: "coo",
description: "2-letter country code (ISO)",
alternateMatches: ["coo", "country of origin"],
fieldType: { type: "input" },
width: 100,
width: 70,
validations: [
{ rule: "regex", value: "^[A-Z]{2}$", errorMessage: "Must be 2 letters", level: "error" },
],
@@ -296,7 +296,7 @@ const BASE_IMPORT_FIELDS = [
type: "input",
multiline: true
},
width: 400,
width: 500,
validations: [{ rule: "required", errorMessage: "Required", level: "error" }],
},
{