- {renderCellContent()}
+ <>
+
+
+
- {/* Show error icon if there are errors and we're not editing */}
- {currentError && !isEditing && (
-
-
+ {isFocused && (
+
+ {errors.map((error, i) => (
+
{error.message}
+ ))}
)}
-
- )
-}
+ >
+ );
+});
-export default ValidationCell
\ No newline at end of file
+// Main ValidationCell component - now with proper memoization
+const ValidationCell = memo(
(props: ValidationCellProps) => {
+ 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 ;
+ }
+
+ // Handle cases where field might be undefined or incomplete
+ if (!field || !field.fieldType) {
+ return (
+
+ Error: Invalid field configuration
+
+ );
+ }
+
+ // 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 (
+
+ );
+ } catch (error) {
+ console.error("Error rendering SelectCell with custom options:", error);
+ return Error rendering field
;
+ }
+ }
+
+ // Standard rendering based on field type
+ try {
+ switch (fieldType) {
+ case 'input':
+ return (
+
+ );
+ case 'multi-input':
+ return (
+
+ );
+ case 'select':
+ case 'multi-select':
+ return (
+
+ );
+ case 'checkbox':
+ return (
+
+ );
+ default:
+ return (
+
+ );
+ }
+ } catch (error) {
+ console.error(`Error rendering cell of type ${fieldType}:`, error);
+ return (
+
+ Error rendering field
+
+ );
+ }
+ }, [
+ fieldType,
+ field,
+ value,
+ onChange,
+ handleStartEdit,
+ handleEndEdit,
+ hasErrors,
+ options,
+ getFieldTypeProp
+ ]);
+
+ return (
+
+ {cellContent}
+
+ {/* Render errors if any exist */}
+ {hasErrors && }
+
+ );
+});
+
+ValidationCell.displayName = 'ValidationCell';
+
+export default ValidationCell;
\ No newline at end of file
diff --git a/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/ValidationContainer.tsx b/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/ValidationContainer.tsx
index 9240f1b..a716116 100644
--- a/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/ValidationContainer.tsx
+++ b/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/ValidationContainer.tsx
@@ -1,4 +1,4 @@
-import React, { useState } from 'react'
+import React, { useState, useEffect, useRef, useCallback, useMemo } from 'react'
import { useValidationState, Props } from '../hooks/useValidationState'
import ValidationTable from './ValidationTable'
import { Button } from '@/components/ui/button'
@@ -38,12 +38,10 @@ const ValidationContainer = ({
const {
data,
filteredData,
- isValidating,
validationErrors,
rowSelection,
setRowSelection,
updateRow,
- hasErrors,
templates,
selectedTemplateId,
applyTemplate,
@@ -51,21 +49,475 @@ const ValidationContainer = ({
getTemplateDisplayText,
filters,
updateFilters,
- setTemplateState,
- templateState,
- saveTemplate,
loadTemplates,
setData,
fields
} = validationState
+ // Add state for tracking product lines and sublines per row
+ const [rowProductLines, setRowProductLines] = useState>({});
+ const [rowSublines, setRowSublines] = useState>({});
+ const [isLoadingLines, setIsLoadingLines] = useState>({});
+ const [isLoadingSublines, setIsLoadingSublines] = useState>({});
+
+ // Add UPC validation state
+ const [isValidatingUpc, setIsValidatingUpc] = useState(false);
+ const [validatingUpcRows, setValidatingUpcRows] = useState>(new Set());
+
+ // Store item numbers in a separate state to avoid updating the main data
+ const [itemNumbers, setItemNumbers] = useState>({});
+
+ // Cache for UPC validation results
+ const processedUpcMapRef = useRef(new Map());
+ 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;
+ 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;
+ 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;
+ 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;
+ if (rowDataAny.supplier) {
+ // Mark this row as being validated
+ setValidatingUpcRows(prev => {
+ const newSet = new Set(prev);
+ newSet.add(rowIndex);
+ return newSet;
+ });
+
+ // Set global validation state
+ setIsValidatingUpc(true);
+
+ await validateUpc(rowIndex, rowDataAny.supplier.toString(), value.toString());
+
+ // Update validation state
+ setValidatingUpcRows(prev => {
+ const newSet = new Set(prev);
+ newSet.delete(rowIndex);
+ if (newSet.size === 0) {
+ setIsValidatingUpc(false);
+ }
+ return newSet;
+ });
+ }
+ }
+ }, [data, filteredData, updateRow, fetchProductLines, fetchSublines, validateUpc, setData]);
+
+ // When data changes, fetch product lines and sublines for rows that have company/line values
+ useEffect(() => {
+ // Skip if there's no data
+ if (!data.length) return;
+
+ // Process each row to set up initial line/subline options
+ data.forEach(row => {
+ const rowId = row.__index;
+ if (!rowId) return; // Skip rows without an index
+
+ // If row has company but no product lines fetched yet, fetch them
+ if (row.company && !rowProductLines[rowId]) {
+ fetchProductLines(rowId, row.company.toString());
+ }
+
+ // If row has line but no sublines fetched yet, fetch them
+ if (row.line && !rowSublines[rowId]) {
+ fetchSublines(rowId, row.line.toString());
+ }
+ });
+ }, [data, rowProductLines, rowSublines, fetchProductLines, fetchSublines]);
+
+ // Validate UPCs on initial data load
+ useEffect(() => {
+ // Skip if there's no data or we've already done the validation
+ if (data.length === 0 || initialUpcValidationDoneRef.current) return;
+
+ // Use a short timeout to allow the UI to render first
+ const timer = setTimeout(() => {
+ validateAllUPCs();
+ }, 100);
+
+ return () => clearTimeout(timer);
+ }, [data, validateAllUPCs]);
+
// Use AI validation hook
const aiValidation = useAiValidation(
data,
setData,
fields,
- validationState.rowHook,
- validationState.tableHook
+ // Create a wrapper function that adapts the rowHook to the expected signature
+ validationState.rowHook ?
+ async (row) => {
+ // Call the original rowHook and return the row itself instead of just Meta
+ await validationState.rowHook(row, 0, data);
+ return row;
+ } :
+ undefined,
+ // Create a wrapper function that adapts the tableHook to the expected signature
+ validationState.tableHook ?
+ async (rows) => {
+ // Call the original tableHook and return the rows themselves
+ await validationState.tableHook(rows);
+ return rows;
+ } :
+ undefined
);
const { translations } = useRsi()
@@ -73,13 +525,17 @@ const ValidationContainer = ({
// State for product search dialog
const [isProductSearchDialogOpen, setIsProductSearchDialogOpen] = useState(false)
- const handleNext = () => {
+ // Handle next button click - memoized
+ const handleNext = useCallback(() => {
+ // Make sure any pending item numbers are applied
+ applyItemNumbersToData();
+
// Call the onNext callback with the validated data
onNext?.(data)
- }
+ }, [onNext, data, applyItemNumbersToData]);
- // Delete selected rows
- const deleteSelectedRows = () => {
+ // Delete selected rows - memoized
+ const deleteSelectedRows = useCallback(() => {
const selectedRowIndexes = Object.keys(rowSelection).map(Number);
const newData = data.filter((_, index) => !selectedRowIndexes.includes(index));
setData(newData);
@@ -89,7 +545,68 @@ const ValidationContainer = ({
? "Row deleted"
: `${selectedRowIndexes.length} rows deleted`
);
- }
+ }, [data, rowSelection, setData, setRowSelection]);
+
+ // Enhanced ValidationTable component that's aware of item numbers
+ const EnhancedValidationTable = useCallback(({
+ data,
+ ...props
+ }: React.ComponentProps>) => {
+ // 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 data={enhancedData} {...props} />;
+ }, [itemNumbers]);
+
+ // Memoize the ValidationTable to prevent unnecessary re-renders
+ const renderValidationTable = useMemo(() => (
+
+ ), [
+ EnhancedValidationTable,
+ filteredData,
+ validationState.fields,
+ enhancedUpdateRow,
+ rowSelection,
+ setRowSelection,
+ validationErrors,
+ isRowValidatingUpc,
+ validatingUpcRows,
+ filters,
+ templates,
+ applyTemplate,
+ getTemplateDisplayText,
+ rowProductLines,
+ rowSublines,
+ isLoadingLines,
+ isLoadingSublines
+ ]);
return (
@@ -151,20 +668,7 @@ const ValidationContainer =
({
-
- data={filteredData}
- fields={validationState.fields}
- updateRow={updateRow}
- rowSelection={rowSelection}
- setRowSelection={setRowSelection}
- validationErrors={validationErrors}
- isValidatingUpc={validationState.isValidatingUpc}
- validatingUpcRows={validationState.validatingUpcRows}
- filters={filters}
- templates={templates}
- applyTemplate={applyTemplate}
- getTemplateDisplayText={getTemplateDisplayText}
- />
+ {renderValidationTable}
@@ -276,8 +780,7 @@ const ValidationContainer = ({
diff --git a/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/ValidationTable.tsx b/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/ValidationTable.tsx
index 54593ea..83b09de 100644
--- a/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/ValidationTable.tsx
+++ b/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/ValidationTable.tsx
@@ -4,22 +4,12 @@ import {
getCoreRowModel,
flexRender,
createColumnHelper,
- RowSelectionState
-} from '@tanstack/react-table'
+ RowSelectionState} from '@tanstack/react-table'
import { Fields, Field } from '../../../types'
import { RowData, Template } from '../hooks/useValidationState'
import { Checkbox } from '@/components/ui/checkbox'
import ValidationCell from './ValidationCell'
-import {
- Table,
- TableBody,
- TableCell,
- TableHead,
- TableHeader,
- TableRow,
-} from "@/components/ui/table"
import { useRsi } from '../../../hooks/useRsi'
-import { Button } from '@/components/ui/button'
import SearchableTemplateSelect from './SearchableTemplateSelect'
// Define a simple Error type locally to avoid import issues
@@ -42,9 +32,105 @@ interface ValidationTableProps {
templates: Template[]
applyTemplate: (templateId: string, rowIndexes: number[]) => void
getTemplateDisplayText: (templateId: string | null) => string
+ rowProductLines?: Record
+ rowSublines?: Record
+ isLoadingLines?: Record
+ isLoadingSublines?: Record
[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
+ 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 (
+
+ );
+ },
+ // 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 (
+
+ template ? getTemplateDisplayText(template) : 'Select template'
+ }
+ />
+ );
+ },
+ // Custom comparison function for the memo
+ (prevProps, nextProps) => {
+ return (
+ prevProps.templateValue === nextProps.templateValue &&
+ prevProps.templates === nextProps.templates
+ );
+ }
+);
+
const ValidationTable = ({
data,
fields,
@@ -53,128 +139,147 @@ const ValidationTable = ({
updateRow,
validationErrors,
isValidatingUpc,
- validatingUpcRows,
filters,
templates,
applyTemplate,
getTemplateDisplayText,
- ...props
-}: ValidationTableProps) => {
+ rowProductLines = {},
+ rowSublines = {},
+ isLoadingLines = {},
+ isLoadingSublines = {}}: ValidationTableProps) => {
const { translations } = useRsi()
const columnHelper = createColumnHelper>()
- // Build table columns
+ // Define columns for the table
const columns = useMemo(() => {
+ // Selection column
const selectionColumn = columnHelper.display({
- id: 'selection',
+ id: 'select',
header: ({ table }) => (
- table.toggleAllRowsSelected(!!value)}
- aria-label="Select all rows"
- />
+
+ table.toggleAllPageRowsSelected(!!value)}
+ aria-label="Select all"
+ />
+
),
cell: ({ row }) => (
- row.toggleSelected(!!value)}
- aria-label="Select row"
- />
+
+ row.toggleSelected(!!value)}
+ aria-label="Select row"
+ />
+
),
- size: 40,
- })
+ size: 50,
+ });
- // Add template column
+ // Template column
const templateColumn = columnHelper.display({
id: 'template',
- header: "Template",
+ header: 'Template',
cell: ({ row }) => {
- try {
- // Only render the component if templates are available
- if (!templates || templates.length === 0) {
- return (
-
- );
- }
-
- return (
- {
- 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 (
-
- );
- }
+ const rowIndex = row.index;
+ return (
+
+ );
},
size: 200,
});
// Create columns for each field
const fieldColumns = fields.map(field => {
+ // Get the field width directly from the field definition
+ // These are exactly the values defined in Import.tsx
+ const fieldWidth = field.width || (
+ field.fieldType.type === "checkbox" ? 80 :
+ field.fieldType.type === "select" ? 150 :
+ field.fieldType.type === "multi-select" ? 200 :
+ (field.fieldType.type === "input" || field.fieldType.type === "multi-input") &&
+ (field.fieldType as any).multiline ? 300 :
+ 150
+ );
+
return columnHelper.accessor(
- (row: RowData) => row[field.key as keyof typeof row] as any,
+ (row: RowData) => row[field.key as keyof typeof row],
{
- id: String(field.key),
+ id: field.key,
header: field.label,
- cell: ({ row, column, getValue }) => {
- const rowIndex = row.index
- const key = column.id as T
- const value = getValue()
- const errors = validationErrors.get(rowIndex)?.[key] || []
-
- // Create a properly typed field object
- const typedField: Field = {
- label: field.label,
- key: field.key as T,
- alternateMatches: field.alternateMatches as string[] | undefined,
- validations: field.validations as any[] | undefined,
- fieldType: field.fieldType,
- example: field.example,
- width: field.width,
- disabled: field.disabled,
- onChange: field.onChange
+ cell: ({ row, column }) => {
+ try {
+ const rowIndex = row.index;
+ const value = row.getValue(column.id);
+ const errors = validationErrors.get(rowIndex)?.[column.id] || [];
+ const rowId = row.original?.__index;
+
+ // Determine if we have custom options for this field
+ let fieldOptions;
+ let isOptionsLoading = false;
+
+ // Handle line field - use company-specific product lines
+ if (field.key === 'line' && rowId && rowProductLines[rowId]) {
+ fieldOptions = rowProductLines[rowId];
+ isOptionsLoading = isLoadingLines[rowId] || false;
+ }
+ // Handle subline field - use line-specific sublines
+ else if (field.key === 'subline' && rowId && rowSublines[rowId]) {
+ fieldOptions = rowSublines[rowId];
+ isOptionsLoading = isLoadingSublines[rowId] || false;
+ }
+
+ // Cast the field type for ValidationCell
+ const typedField = field as Field;
+
+ return (
+
+ );
+ } catch (error) {
+ console.error(`Error rendering cell for column ${column.id}:`, error);
+ return (
+
+ Error rendering cell
+
+ );
}
-
- return (
-
- rowIndex={rowIndex}
- field={typedField}
- value={value}
- onChange={(newValue) => updateRow(rowIndex, key, newValue)}
- errors={errors}
- isValidatingUpc={isValidatingUpc(rowIndex)}
- />
- )
},
- size: (field as any).width || (
- field.fieldType.type === "checkbox" ? 80 :
- field.fieldType.type === "select" ? 150 :
- 200
- ),
+ size: fieldWidth,
}
- )
- })
+ );
+ });
- return [selectionColumn, templateColumn, ...fieldColumns]
- }, [columnHelper, fields, updateRow, validationErrors, isValidatingUpc, templates, applyTemplate, getTemplateDisplayText])
+ return [selectionColumn, templateColumn, ...fieldColumns];
+ }, [
+ columnHelper,
+ fields,
+ templates,
+ applyTemplate,
+ getTemplateDisplayText,
+ rowProductLines,
+ rowSublines,
+ isLoadingLines,
+ isLoadingSublines,
+ validationErrors,
+ isValidatingUpc,
+ updateRow
+ ]);
// Initialize table
const table = useReactTable({
@@ -186,66 +291,84 @@ const ValidationTable = ({
enableRowSelection: true,
onRowSelectionChange: setRowSelection,
getCoreRowModel: getCoreRowModel(),
- })
+ });
- return (
-
-
- {table.getHeaderGroups().map((headerGroup) => (
-
- {headerGroup.headers.map((header) => (
-
- {header.isPlaceholder
- ? null
- : flexRender(
- header.column.columnDef.header,
- header.getContext()
- )}
-
- ))}
-
- ))}
-
-
- {table.getRowModel().rows?.length ? (
- table.getRowModel().rows.map((row) => (
-
- {row.getVisibleCells().map((cell) => (
-
- {flexRender(cell.column.columnDef.cell, cell.getContext())}
-
- ))}
-
- ))
- ) : (
-
-
- {filters?.showErrorsOnly
- ? translations.validationStep.noRowsMessageWhenFiltered || "No rows with errors found."
- : translations.validationStep.noRowsMessage || "No rows found."}
-
-
- )}
-
-
- )
-}
+ // Apply filters to rows if needed
+ const filteredRows = useMemo(() => {
+ let rows = table.getRowModel().rows;
+
+ if (filters?.showErrorsOnly) {
+ rows = rows.filter(row => {
+ const rowIndex = row.index;
+ return validationErrors.has(rowIndex) &&
+ Object.values(validationErrors.get(rowIndex) || {}).some(errors => errors.length > 0);
+ });
+ }
+
+ return rows;
+ }, [table, filters, validationErrors]);
-export default ValidationTable
\ No newline at end of file
+ return (
+
+
+
+
+
+ {table.getHeaderGroups()[0].headers.map((header) => (
+ |
+ {header.isPlaceholder
+ ? null
+ : flexRender(
+ header.column.columnDef.header,
+ header.getContext()
+ )}
+ |
+ ))}
+
+
+
+ {filteredRows.length ? (
+ filteredRows.map((row) => (
+
+ {row.getVisibleCells().map((cell) => (
+ |
+ {flexRender(cell.column.columnDef.cell, cell.getContext())}
+ |
+ ))}
+
+ ))
+ ) : (
+
+ |
+ {filters?.showErrorsOnly
+ ? translations.validationStep.noRowsMessageWhenFiltered || "No rows with errors found."
+ : translations.validationStep.noRowsMessage || "No rows found."}
+ |
+
+ )}
+
+
+
+
+ );
+};
+
+export default ValidationTable;
\ No newline at end of file
diff --git a/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells/InputCell.tsx b/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells/InputCell.tsx
index a8f3551..8d51604 100644
--- a/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells/InputCell.tsx
+++ b/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells/InputCell.tsx
@@ -90,7 +90,6 @@ const InputCell = ({
onChange={(e) => setInputValue(e.target.value)}
onFocus={handleFocus}
onBlur={handleBlur}
- placeholder={field.description}
className={cn(
"min-h-[80px] resize-none",
outlineClass,
@@ -105,7 +104,6 @@ const InputCell = ({
onChange={(e) => setInputValue(e.target.value)}
onFocus={handleFocus}
onBlur={handleBlur}
- placeholder={field.description}
autoFocus
className={cn(
outlineClass,
@@ -121,7 +119,7 @@ const InputCell = ({
hasErrors ? "border-destructive" : "border-input"
)}
>
- {isPrice ? getDisplayValue() : (inputValue || {field.description})}
+ {isPrice ? getDisplayValue() : (inputValue)}
)
)}
diff --git a/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells/MultiInputCell.tsx b/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells/MultiInputCell.tsx
index f1ff24d..a273d1b 100644
--- a/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells/MultiInputCell.tsx
+++ b/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells/MultiInputCell.tsx
@@ -177,7 +177,7 @@ const MultiInputCell = ({
)
})
) : (
- {field.description || "Select options..."}
+ { "Select options..."}
)}