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> </div>
<div className="space-y-2"> <div className="space-y-2 max-w-[610px]">
<Label htmlFor="categories">Categories</Label> <Label htmlFor="categories">Categories</Label>
<Popover modal={false}> <Popover modal={false}>
<PopoverTrigger asChild className="max-w-[calc(800px-3.5rem)]"> <PopoverTrigger asChild className="w-full">
<Button <Button
variant="outline" variant="outline"
role="combobox" role="combobox"

View File

@@ -19,7 +19,7 @@ import {
import { useQuery } from "@tanstack/react-query" import { useQuery } from "@tanstack/react-query"
import config from "@/config" import config from "@/config"
import { Button } from "@/components/ui/button" 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 { Separator } from "@/components/ui/separator"
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table" import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
import { Badge } from "@/components/ui/badge" import { Badge } from "@/components/ui/badge"
@@ -34,8 +34,6 @@ import {
CommandList, CommandList,
} from "@/components/ui/command" } from "@/components/ui/command"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
import { Switch } from "@/components/ui/switch"
import { Label } from "@/components/ui/label"
export type MatchColumnsProps<T extends string> = { export type MatchColumnsProps<T extends string> = {
data: RawData[] data: RawData[]

View File

@@ -4,7 +4,6 @@ import { UploadStep } from "./UploadStep/UploadStep"
import { SelectHeaderStep } from "./SelectHeaderStep/SelectHeaderStep" import { SelectHeaderStep } from "./SelectHeaderStep/SelectHeaderStep"
import { SelectSheetStep } from "./SelectSheetStep/SelectSheetStep" import { SelectSheetStep } from "./SelectSheetStep/SelectSheetStep"
import { mapWorkbook } from "../utils/mapWorkbook" import { mapWorkbook } from "../utils/mapWorkbook"
import { ValidationStep } from "./ValidationStep/ValidationStep"
import { ValidationStepNew } from "./ValidationStepNew" import { ValidationStepNew } from "./ValidationStepNew"
import { ImageUploadStep } from "./ImageUploadStep/ImageUploadStep" import { ImageUploadStep } from "./ImageUploadStep/ImageUploadStep"
import { addErrorsAndRunHooks } from "./ValidationStep/utils/dataMutations" import { addErrorsAndRunHooks } from "./ValidationStep/utils/dataMutations"
@@ -118,7 +117,17 @@ export const UploadFlow = ({ state, onNext, onBack }: Props) => {
onNext({ type: StepType.selectSheet, workbook }) 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: 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: default:

View File

@@ -79,7 +79,7 @@ export const AiValidationDialogs: React.FC<AiValidationDialogsProps> = ({
<div <div
className="h-full bg-primary transition-all duration-500" className="h-full bg-primary transition-all duration-500"
style={{ style={{
width: `${aiValidationProgress.progressPercent ?? (aiValidationProgress.step / 5) * 100}%`, width: `${aiValidationProgress.progressPercent ?? Math.floor((aiValidationProgress.step / 5) * 100)}%`,
backgroundColor: aiValidationProgress.step === -1 ? 'var(--destructive)' : undefined 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 { Field } from '../../../types'
import { Loader2 } from 'lucide-react' import { Loader2 } from 'lucide-react'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
@@ -7,6 +7,12 @@ import InputCell from './cells/InputCell'
import MultiInputCell from './cells/MultiInputCell' import MultiInputCell from './cells/MultiInputCell'
import SelectCell from './cells/SelectCell' import SelectCell from './cells/SelectCell'
import CheckboxCell from './cells/CheckboxCell' import CheckboxCell from './cells/CheckboxCell'
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip"
// Define an error object type // Define an error object type
type ErrorObject = { type ErrorObject = {
@@ -18,158 +24,249 @@ type ErrorObject = {
/** /**
* ValidationIcon - Renders an appropriate icon based on error level * 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" const iconClasses = "h-4 w-4"
const icon = useMemo(() => {
switch(error.level) { switch(error.level) {
case 'error': case 'error':
return <AlertCircle className={cn(iconClasses, "text-destructive")} /> return <AlertCircle className={cn(iconClasses, "text-destructive")} />;
case 'warning': case 'warning':
return <AlertTriangle className={cn(iconClasses, "text-amber-500")} /> return <AlertTriangle className={cn(iconClasses, "text-amber-500")} />;
case 'info': case 'info':
return <Info className={cn(iconClasses, "text-blue-500")} /> return <Info className={cn(iconClasses, "text-blue-500")} />;
default: default:
return <AlertCircle className={iconClasses} /> 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> { export interface ValidationCellProps<T extends string> {
rowIndex: number
field: Field<T> field: Field<T>
value: any value: any
onChange: (value: any) => void onChange: (value: any) => void
errors: ErrorObject[] errors: ErrorObject[]
isValidatingUpc?: boolean isValidatingUpc?: boolean
isInValidatingRow?: boolean
fieldKey?: string
options?: any[]
isLoading?: boolean
key?: string
} }
const ValidationCell = <T extends string>({ // Memoized loader component
rowIndex, 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>
));
// Memoized error display component
const ErrorDisplay = memo(({ errors, isFocused }: { errors: ErrorObject[], isFocused: boolean }) => {
if (!errors || errors.length === 0) return null;
return (
<>
<div className="absolute right-1 top-1/2 -translate-y-1/2">
<ValidationIcon error={errors[0]} />
</div>
{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>
)}
</>
);
});
// Main ValidationCell component - now with proper memoization
const ValidationCell = memo(<T extends string>(props: ValidationCellProps<T>) => {
const {
field, field,
value, value,
onChange, onChange,
errors, errors,
isValidatingUpc = false isValidatingUpc = false,
}: ValidationCellProps<T>) => { fieldKey,
const [isEditing, setIsEditing] = useState(false) options } = props;
// Get the most severe error // State for showing/hiding error messages
const currentError = errors.length > 0 ? errors[0] : null const [isFocused, setIsFocused] = useState(false);
// Determine if field is disabled // Handlers for edit state
const isFieldDisabled = field.disabled || false const handleStartEdit = useCallback(() => {
setIsFocused(true);
}, []);
// Check if this is a UPC field for validation const handleEndEdit = useCallback(() => {
const isUpcField = field.key === 'upc' || setIsFocused(false);
(field.fieldType as any)?.upcField || }, []);
field.key.toString().toLowerCase().includes('upc')
// Render cell contents based on field type // Check if this cell has errors
const renderCellContent = () => { const hasErrors = errors && errors.length > 0;
// If we're validating UPC, show a spinner
if (isValidatingUpc) { // Show loading state when validating UPC fields
return ( if (isValidatingUpc && (fieldKey === 'upc' || fieldKey === 'barcode' || fieldKey === 'item_number')) {
<div className="flex items-center justify-center w-full min-h-[32px]"> return <LoadingIndicator />;
<Loader2 className="h-4 w-4 animate-spin" />
</div>
)
} }
const fieldType = field.fieldType.type // 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>
);
}
// Handle different field types // 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) { switch (fieldType) {
case 'input': case 'input':
return ( return (
<InputCell<T> <InputCell
field={field} field={field}
value={value} value={value}
onChange={onChange} onChange={onChange}
onStartEdit={() => setIsEditing(true)} onStartEdit={handleStartEdit}
onEndEdit={() => setIsEditing(false)} onEndEdit={handleEndEdit}
hasErrors={errors.length > 0} hasErrors={hasErrors}
isMultiline={(field.fieldType as any).multiline} isMultiline={getFieldTypeProp('multiline', false)}
isPrice={(field.fieldType as any).price} isPrice={getFieldTypeProp('price', false)}
/> />
) );
case 'multi-input': case 'multi-input':
return ( return (
<MultiInputCell<T> <MultiInputCell
field={field} field={field}
value={value} value={value}
onChange={onChange} onChange={onChange}
onStartEdit={() => setIsEditing(true)} onStartEdit={handleStartEdit}
onEndEdit={() => setIsEditing(false)} onEndEdit={handleEndEdit}
hasErrors={errors.length > 0} hasErrors={hasErrors}
separator={(field.fieldType as any).separator || ','} separator={getFieldTypeProp('separator', ',')}
isMultiline={(field.fieldType as any).multiline} isMultiline={getFieldTypeProp('multiline', false)}
isPrice={(field.fieldType as any).price} isPrice={getFieldTypeProp('price', false)}
/> />
) );
case 'select': 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': case 'multi-select':
return ( return (
<MultiInputCell<T> <SelectCell
field={field} field={field}
value={value} value={value}
onChange={onChange} onChange={onChange}
onStartEdit={() => setIsEditing(true)} onStartEdit={handleStartEdit}
onEndEdit={() => setIsEditing(false)} onEndEdit={handleEndEdit}
hasErrors={errors.length > 0} hasErrors={hasErrors}
separator={(field.fieldType as any).separator || ','} options={getFieldTypeProp('options', [])}
options={field.fieldType.type === 'multi-select' ? field.fieldType.options : undefined}
/> />
) );
case 'checkbox': case 'checkbox':
return ( return (
<CheckboxCell<T> <CheckboxCell
field={field} field={field}
value={value} value={value}
onChange={onChange} onChange={onChange}
hasErrors={errors.length > 0} hasErrors={hasErrors}
booleanMatches={(field.fieldType as any).booleanMatches} booleanMatches={getFieldTypeProp('booleanMatches', {})}
/> />
) );
default: default:
return ( return (
<div className="p-2"> <InputCell
{String(value || '')} 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> </div>
) );
}
} }
}, [
fieldType,
field,
value,
onChange,
handleStartEdit,
handleEndEdit,
hasErrors,
options,
getFieldTypeProp
]);
// Main cell rendering
return ( return (
<div className={cn( <div className={cn(
"relative w-full", "relative",
isFieldDisabled ? "opacity-70 pointer-events-none" : "" hasErrors && "space-y-1"
)}> )}>
{renderCellContent()} {cellContent}
{/* Show error icon if there are errors and we're not editing */} {/* Render errors if any exist */}
{currentError && !isEditing && ( {hasErrors && <ErrorDisplay errors={errors} isFocused={isFocused} />}
<div className="absolute right-1 top-1/2 -translate-y-1/2">
<ValidationIcon error={currentError} />
</div> </div>
)} );
</div> });
)
}
export default ValidationCell 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 { useValidationState, Props } from '../hooks/useValidationState'
import ValidationTable from './ValidationTable' import ValidationTable from './ValidationTable'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
@@ -38,12 +38,10 @@ const ValidationContainer = <T extends string>({
const { const {
data, data,
filteredData, filteredData,
isValidating,
validationErrors, validationErrors,
rowSelection, rowSelection,
setRowSelection, setRowSelection,
updateRow, updateRow,
hasErrors,
templates, templates,
selectedTemplateId, selectedTemplateId,
applyTemplate, applyTemplate,
@@ -51,21 +49,475 @@ const ValidationContainer = <T extends string>({
getTemplateDisplayText, getTemplateDisplayText,
filters, filters,
updateFilters, updateFilters,
setTemplateState,
templateState,
saveTemplate,
loadTemplates, loadTemplates,
setData, setData,
fields fields
} = validationState } = 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 // Use AI validation hook
const aiValidation = useAiValidation<T>( const aiValidation = useAiValidation<T>(
data, data,
setData, setData,
fields, fields,
validationState.rowHook, // Create a wrapper function that adapts the rowHook to the expected signature
validationState.tableHook 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>() const { translations } = useRsi<T>()
@@ -73,13 +525,17 @@ const ValidationContainer = <T extends string>({
// State for product search dialog // State for product search dialog
const [isProductSearchDialogOpen, setIsProductSearchDialogOpen] = useState(false) 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 // Call the onNext callback with the validated data
onNext?.(data) onNext?.(data)
} }, [onNext, data, applyItemNumbersToData]);
// Delete selected rows // Delete selected rows - memoized
const deleteSelectedRows = () => { const deleteSelectedRows = useCallback(() => {
const selectedRowIndexes = Object.keys(rowSelection).map(Number); const selectedRowIndexes = Object.keys(rowSelection).map(Number);
const newData = data.filter((_, index) => !selectedRowIndexes.includes(index)); const newData = data.filter((_, index) => !selectedRowIndexes.includes(index));
setData(newData); setData(newData);
@@ -89,7 +545,68 @@ const ValidationContainer = <T extends string>({
? "Row deleted" ? "Row deleted"
: `${selectedRowIndexes.length} rows 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 ( return (
<div className="flex flex-col h-[calc(100vh-10rem)] overflow-hidden"> <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="px-8 pb-6 flex-1 min-h-0">
<div className="rounded-md border h-full flex flex-col overflow-hidden"> <div className="rounded-md border h-full flex flex-col overflow-hidden">
<div className="flex-1 overflow-auto"> <div className="flex-1 overflow-auto">
<ValidationTable<T> {renderValidationTable}
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}
/>
</div> </div>
</div> </div>
</div> </div>
@@ -276,7 +780,6 @@ const ValidationContainer = <T extends string>({
</Button> </Button>
<Button <Button
disabled={hasErrors}
onClick={handleNext} onClick={handleNext}
> >
{translations.validationStep.nextButtonTitle || "Next"} {translations.validationStep.nextButtonTitle || "Next"}

View File

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

View File

@@ -90,7 +90,6 @@ const InputCell = <T extends string>({
onChange={(e) => setInputValue(e.target.value)} onChange={(e) => setInputValue(e.target.value)}
onFocus={handleFocus} onFocus={handleFocus}
onBlur={handleBlur} onBlur={handleBlur}
placeholder={field.description}
className={cn( className={cn(
"min-h-[80px] resize-none", "min-h-[80px] resize-none",
outlineClass, outlineClass,
@@ -105,7 +104,6 @@ const InputCell = <T extends string>({
onChange={(e) => setInputValue(e.target.value)} onChange={(e) => setInputValue(e.target.value)}
onFocus={handleFocus} onFocus={handleFocus}
onBlur={handleBlur} onBlur={handleBlur}
placeholder={field.description}
autoFocus autoFocus
className={cn( className={cn(
outlineClass, outlineClass,
@@ -121,7 +119,7 @@ const InputCell = <T extends string>({
hasErrors ? "border-destructive" : "border-input" hasErrors ? "border-destructive" : "border-input"
)} )}
> >
{isPrice ? getDisplayValue() : (inputValue || <span className="text-muted-foreground">{field.description}</span>)} {isPrice ? getDisplayValue() : (inputValue)}
</div> </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> </div>
<ChevronsUpDown className="h-4 w-4 shrink-0 opacity-50" /> <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 // Get current display value
const displayValue = value ? const displayValue = value ?
selectOptions.find(option => String(option.value) === String(value))?.label || String(value) : selectOptions.find(option => String(option.value) === String(value))?.label || String(value) :
field.description || 'Select...'; 'Select...';
const handleSelect = (selectedValue: string) => { const handleSelect = (selectedValue: string) => {
onChange(selectedValue); onChange(selectedValue);

View File

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

View File

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