More validate step changes/fixes
This commit is contained in:
@@ -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"
|
||||||
|
|||||||
@@ -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[]
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|
||||||
switch(error.level) {
|
const icon = useMemo(() => {
|
||||||
case 'error':
|
switch(error.level) {
|
||||||
return <AlertCircle className={cn(iconClasses, "text-destructive")} />
|
case 'error':
|
||||||
case 'warning':
|
return <AlertCircle className={cn(iconClasses, "text-destructive")} />;
|
||||||
return <AlertTriangle className={cn(iconClasses, "text-amber-500")} />
|
case 'warning':
|
||||||
case 'info':
|
return <AlertTriangle className={cn(iconClasses, "text-amber-500")} />;
|
||||||
return <Info className={cn(iconClasses, "text-blue-500")} />
|
case 'info':
|
||||||
default:
|
return <Info className={cn(iconClasses, "text-blue-500")} />;
|
||||||
return <AlertCircle className={iconClasses} />
|
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> {
|
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(() => (
|
||||||
field,
|
<div className="flex items-center justify-center h-9 rounded-md border border-input bg-gray-50 px-3">
|
||||||
value,
|
<Loader2 className="h-4 w-4 animate-spin text-muted-foreground" />
|
||||||
onChange,
|
<span className="ml-2 text-sm text-muted-foreground">Validating...</span>
|
||||||
errors,
|
</div>
|
||||||
isValidatingUpc = false
|
));
|
||||||
}: ValidationCellProps<T>) => {
|
|
||||||
const [isEditing, setIsEditing] = useState(false)
|
|
||||||
|
|
||||||
// Get the most severe error
|
|
||||||
const currentError = errors.length > 0 ? errors[0] : null
|
|
||||||
|
|
||||||
// Determine if field is disabled
|
// Memoized error display component
|
||||||
const isFieldDisabled = field.disabled || false
|
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 (
|
return (
|
||||||
<div className={cn(
|
<>
|
||||||
"relative w-full",
|
<div className="absolute right-1 top-1/2 -translate-y-1/2">
|
||||||
isFieldDisabled ? "opacity-70 pointer-events-none" : ""
|
<ValidationIcon error={errors[0]} />
|
||||||
)}>
|
</div>
|
||||||
{renderCellContent()}
|
|
||||||
|
|
||||||
{/* Show error icon if there are errors and we're not editing */}
|
{isFocused && (
|
||||||
{currentError && !isEditing && (
|
<div className="text-xs text-destructive p-1 mt-1 bg-destructive/5 rounded-sm">
|
||||||
<div className="absolute right-1 top-1/2 -translate-y-1/2">
|
{errors.map((error, i) => (
|
||||||
<ValidationIcon error={currentError} />
|
<div key={i} className="py-0.5">{error.message}</div>
|
||||||
|
))}
|
||||||
</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;
|
||||||
@@ -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,8 +780,7 @@ const ValidationContainer = <T extends string>({
|
|||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
disabled={hasErrors}
|
onClick={handleNext}
|
||||||
onClick={handleNext}
|
|
||||||
>
|
>
|
||||||
{translations.validationStep.nextButtonTitle || "Next"}
|
{translations.validationStep.nextButtonTitle || "Next"}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -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 }) => (
|
||||||
<Checkbox
|
<div className="flex justify-center">
|
||||||
checked={table.getIsAllRowsSelected()}
|
<Checkbox
|
||||||
onCheckedChange={(value) => table.toggleAllRowsSelected(!!value)}
|
checked={table.getIsAllPageRowsSelected()}
|
||||||
aria-label="Select all rows"
|
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
|
||||||
/>
|
aria-label="Select all"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
),
|
),
|
||||||
cell: ({ row }) => (
|
cell: ({ row }) => (
|
||||||
<Checkbox
|
<div className="flex justify-center">
|
||||||
checked={row.getIsSelected()}
|
<Checkbox
|
||||||
onCheckedChange={(value) => row.toggleSelected(!!value)}
|
checked={row.getIsSelected()}
|
||||||
aria-label="Select row"
|
onCheckedChange={(value) => row.toggleSelected(!!value)}
|
||||||
/>
|
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
|
return (
|
||||||
if (!templates || templates.length === 0) {
|
<MemoizedTemplateCell
|
||||||
return (
|
rowIndex={rowIndex}
|
||||||
<Button variant="outline" className="w-full justify-between" disabled>
|
templateValue={row.original.__template || null}
|
||||||
Loading templates...
|
templates={templates}
|
||||||
</Button>
|
applyTemplate={applyTemplate}
|
||||||
);
|
getTemplateDisplayText={getTemplateDisplayText}
|
||||||
}
|
/>
|
||||||
|
);
|
||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
size: 200,
|
size: 200,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Create columns for each field
|
// Create columns for each field
|
||||||
const fieldColumns = fields.map(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(
|
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,
|
header: field.label,
|
||||||
cell: ({ row, column, getValue }) => {
|
cell: ({ row, column }) => {
|
||||||
const rowIndex = row.index
|
try {
|
||||||
const key = column.id as T
|
const rowIndex = row.index;
|
||||||
const value = getValue()
|
const value = row.getValue(column.id);
|
||||||
const errors = validationErrors.get(rowIndex)?.[key] || []
|
const errors = validationErrors.get(rowIndex)?.[column.id] || [];
|
||||||
|
const rowId = row.original?.__index;
|
||||||
// Create a properly typed field object
|
|
||||||
const typedField: Field<T> = {
|
// Determine if we have custom options for this field
|
||||||
label: field.label,
|
let fieldOptions;
|
||||||
key: field.key as T,
|
let isOptionsLoading = false;
|
||||||
alternateMatches: field.alternateMatches as string[] | undefined,
|
|
||||||
validations: field.validations as any[] | undefined,
|
// Handle line field - use company-specific product lines
|
||||||
fieldType: field.fieldType,
|
if (field.key === 'line' && rowId && rowProductLines[rowId]) {
|
||||||
example: field.example,
|
fieldOptions = rowProductLines[rowId];
|
||||||
width: field.width,
|
isOptionsLoading = isLoadingLines[rowId] || false;
|
||||||
disabled: field.disabled,
|
}
|
||||||
onChange: field.onChange
|
// 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 || (
|
size: fieldWidth,
|
||||||
field.fieldType.type === "checkbox" ? 80 :
|
|
||||||
field.fieldType.type === "select" ? 150 :
|
|
||||||
200
|
|
||||||
),
|
|
||||||
}
|
}
|
||||||
)
|
);
|
||||||
})
|
});
|
||||||
|
|
||||||
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,66 +291,84 @@ const ValidationTable = <T extends string>({
|
|||||||
enableRowSelection: true,
|
enableRowSelection: true,
|
||||||
onRowSelectionChange: setRowSelection,
|
onRowSelectionChange: setRowSelection,
|
||||||
getCoreRowModel: getCoreRowModel(),
|
getCoreRowModel: getCoreRowModel(),
|
||||||
})
|
});
|
||||||
|
|
||||||
return (
|
// Apply filters to rows if needed
|
||||||
<Table>
|
const filteredRows = useMemo(() => {
|
||||||
<TableHeader className="sticky top-0 bg-muted z-10">
|
let rows = table.getRowModel().rows;
|
||||||
{table.getHeaderGroups().map((headerGroup) => (
|
|
||||||
<TableRow key={headerGroup.id}>
|
if (filters?.showErrorsOnly) {
|
||||||
{headerGroup.headers.map((header) => (
|
rows = rows.filter(row => {
|
||||||
<TableHead
|
const rowIndex = row.index;
|
||||||
key={header.id}
|
return validationErrors.has(rowIndex) &&
|
||||||
style={{
|
Object.values(validationErrors.get(rowIndex) || {}).some(errors => errors.length > 0);
|
||||||
width: header.getSize(),
|
});
|
||||||
minWidth: header.getSize(),
|
}
|
||||||
}}
|
|
||||||
>
|
return rows;
|
||||||
{header.isPlaceholder
|
}, [table, filters, validationErrors]);
|
||||||
? 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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
||||||
@@ -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>
|
||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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" />
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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" }],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user