diff --git a/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/ValidationCell.tsx b/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/ValidationCell.tsx index 4ff2275..c8a22f5 100644 --- a/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/ValidationCell.tsx +++ b/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/ValidationCell.tsx @@ -308,20 +308,38 @@ export default React.memo(ValidationCell, (prev, next) => { // Deep comparison of errors const prevErrorsStr = JSON.stringify(prev.errors); const nextErrorsStr = JSON.stringify(next.errors); - - // For item number fields, compare itemNumber + + // Deep comparison of options + const prevOptionsStr = JSON.stringify(prev.options); + const nextOptionsStr = JSON.stringify(next.options); + + // For validating cells, always re-render + if (prev.isValidating !== next.isValidating) { + return false; + } + + // For item numbers, check if the item number changed if (prev.fieldKey === 'item_number') { return ( prev.value === next.value && prev.itemNumber === next.itemNumber && - prev.isValidating === next.isValidating + prevErrorsStr === nextErrorsStr ); } - - // For all other fields, compare core props + + // For select and multi-select fields, check if options changed + if (prev.field.fieldType.type === 'select' || prev.field.fieldType.type === 'multi-select') { + return ( + prev.value === next.value && + prevErrorsStr === nextErrorsStr && + prevOptionsStr === nextOptionsStr + ); + } + + // For all other fields, check if value or errors changed return ( prev.value === next.value && prevErrorsStr === nextErrorsStr && - JSON.stringify(prev.options) === JSON.stringify(next.options) + prev.width === next.width ); }); \ No newline at end of file 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 2b06598..caf1f8e 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 @@ -1,4 +1,4 @@ -import React, { useMemo, useRef, useEffect, useLayoutEffect } from 'react' +import React, { useMemo, useRef, useEffect, useLayoutEffect, useCallback } from 'react' import { useReactTable, getCoreRowModel, @@ -78,15 +78,25 @@ const MemoizedCell = React.memo(({ const rowErrors = validationErrors.get(rowIndex) || {}; const fieldErrors = rowErrors[String(field.key)] || []; const isValidating = validatingCells.has(`${rowIndex}-${field.key}`); - const options = field.fieldType.type === 'select' || field.fieldType.type === 'multi-select' - ? Array.from(field.fieldType.options || []) - : []; + + // Only compute options when needed for select/multi-select fields + const options = useMemo(() => { + if (field.fieldType.type === 'select' || field.fieldType.type === 'multi-select') { + return Array.from((field.fieldType as any).options || []); + } + return []; + }, [field.fieldType]); + + // Memoize the onChange handler to prevent unnecessary re-renders + const handleChange = useCallback((newValue: any) => { + updateRow(rowIndex, field.key, newValue); + }, [updateRow, rowIndex, field.key]); return ( updateRow(rowIndex, field.key, newValue)} + onChange={handleChange} errors={fieldErrors} isValidating={isValidating} fieldKey={String(field.key)} @@ -128,6 +138,17 @@ const MemoizedCell = React.memo(({ const prevErrors = prev.validationErrors.get(prev.rowIndex)?.[fieldKey]; const nextErrors = next.validationErrors.get(next.rowIndex)?.[fieldKey]; + // For select/multi-select fields, also check if options changed + if (prev.field.fieldType.type === 'select' || prev.field.fieldType.type === 'multi-select') { + const prevOptions = (prev.field.fieldType as any).options; + const nextOptions = (next.field.fieldType as any).options; + + // If options length changed, we need to re-render + if (prevOptions?.length !== nextOptions?.length) { + return false; + } + } + return ( prev.value === next.value && JSON.stringify(prevErrors) === JSON.stringify(nextErrors) 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 daa1333..89d9550 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 @@ -193,51 +193,30 @@ const MultiInputCell = ({ if (containerRef.current) { const container = containerRef.current; - // Force direct style properties using the DOM API - container.style.setProperty('width', `${cellWidth}px`, 'important'); - container.style.setProperty('min-width', `${cellWidth}px`, 'important'); - container.style.setProperty('max-width', `${cellWidth}px`, 'important'); - container.style.setProperty('box-sizing', 'border-box', 'important'); - container.style.setProperty('display', 'inline-block', 'important'); - container.style.setProperty('flex', '0 0 auto', 'important'); + // Force direct style properties using the DOM API - simplified approach + container.style.width = `${cellWidth}px`; + container.style.minWidth = `${cellWidth}px`; + container.style.maxWidth = `${cellWidth}px`; // Apply to the button element as well const button = container.querySelector('button'); if (button) { // Cast to HTMLElement to access style property const htmlButton = button as HTMLElement; - htmlButton.style.setProperty('width', `${cellWidth}px`, 'important'); - htmlButton.style.setProperty('min-width', `${cellWidth}px`, 'important'); - htmlButton.style.setProperty('max-width', `${cellWidth}px`, 'important'); - - // Make sure flex layout is enforced - const buttonContent = button.querySelector('div'); - if (buttonContent && buttonContent instanceof HTMLElement) { - buttonContent.style.setProperty('display', 'flex', 'important'); - buttonContent.style.setProperty('align-items', 'center', 'important'); - buttonContent.style.setProperty('justify-content', 'space-between', 'important'); - buttonContent.style.setProperty('width', '100%', 'important'); - buttonContent.style.setProperty('overflow', 'hidden', 'important'); - } - - // Find the chevron icon and ensure it's not wrapping - const chevron = button.querySelector('svg'); - if (chevron && chevron instanceof SVGElement) { - chevron.style.cssText = 'flex-shrink: 0 !important; margin-left: auto !important;'; - } + htmlButton.style.width = `${cellWidth}px`; + htmlButton.style.minWidth = `${cellWidth}px`; + htmlButton.style.maxWidth = `${cellWidth}px`; } } }, [cellWidth]); - // Create a key-value map for inline styles with fixed width - const fixedWidth = { + // Create a key-value map for inline styles with fixed width - simplified + const fixedWidth = useMemo(() => ({ width: `${cellWidth}px`, minWidth: `${cellWidth}px`, maxWidth: `${cellWidth}px`, boxSizing: 'border-box' as const, - display: 'inline-block', - flex: '0 0 auto' - }; + }), [cellWidth]); return (
({ if (containerRef.current) { const container = containerRef.current; - // Force direct style properties using the DOM API - container.style.setProperty('width', `${cellWidth}px`, 'important'); - container.style.setProperty('min-width', `${cellWidth}px`, 'important'); - container.style.setProperty('max-width', `${cellWidth}px`, 'important'); - container.style.setProperty('box-sizing', 'border-box', 'important'); - container.style.setProperty('display', 'inline-block', 'important'); - container.style.setProperty('flex', '0 0 auto', 'important'); + // Force direct style properties using the DOM API - simplified approach + container.style.width = `${cellWidth}px`; + container.style.minWidth = `${cellWidth}px`; + container.style.maxWidth = `${cellWidth}px`; - // Apply to the input or div element as well - const input = container.querySelector('textarea, div'); - if (input) { + // Apply to the button element as well + const button = container.querySelector('button'); + if (button) { // Cast to HTMLElement to access style property - const htmlElement = input as HTMLElement; - htmlElement.style.setProperty('width', `${cellWidth}px`, 'important'); - htmlElement.style.setProperty('min-width', `${cellWidth}px`, 'important'); - htmlElement.style.setProperty('max-width', `${cellWidth}px`, 'important'); + const htmlButton = button as HTMLElement; + htmlButton.style.width = `${cellWidth}px`; + htmlButton.style.minWidth = `${cellWidth}px`; + htmlButton.style.maxWidth = `${cellWidth}px`; } } }, [cellWidth]); - // Create a key-value map for inline styles with fixed width - const fixedWidth = { + // Create a key-value map for inline styles with fixed width - simplified + const fixedWidth = useMemo(() => ({ width: `${cellWidth}px`, minWidth: `${cellWidth}px`, maxWidth: `${cellWidth}px`, boxSizing: 'border-box' as const, - display: 'inline-block', - flex: '0 0 auto' - }; + }), [cellWidth]); return (
({ // Ref for the command list to enable scrolling const commandListRef = useRef(null) - // Ensure we always have an array of options with the correct shape - const fieldType = field.fieldType; - const fieldOptions = fieldType && - (fieldType.type === 'select' || fieldType.type === 'multi-select') && - fieldType.options ? - fieldType.options : - []; + // Memoize options processing to avoid recalculation on every render + const selectOptions = useMemo(() => { + // Ensure we always have an array of options with the correct shape + const fieldType = field.fieldType; + const fieldOptions = fieldType && + (fieldType.type === 'select' || fieldType.type === 'multi-select') && + (fieldType as any).options ? + (fieldType as any).options : + []; + + // Always ensure selectOptions is a valid array with at least a default option + const processedOptions = (options || fieldOptions || []).map((option: any) => ({ + label: option.label || String(option.value), + value: String(option.value) + })); + + if (processedOptions.length === 0) { + // Add a default empty option if we have none + processedOptions.push({ label: 'No options available', value: '' }); + } + + return processedOptions; + }, [field.fieldType, options]); - // Always ensure selectOptions is a valid array with at least a default option - const selectOptions = (options || fieldOptions || []).map(option => ({ - label: option.label || String(option.value), - value: String(option.value) - })); - - if (selectOptions.length === 0) { - // Add a default empty option if we have none - selectOptions.push({ label: 'No options available', value: '' }); - } - - // Get current display value - const displayValue = value ? - selectOptions.find(option => String(option.value) === String(value))?.label || String(value) : - 'Select...'; + // Memoize display value to avoid recalculation on every render + const displayValue = useMemo(() => { + return value ? + selectOptions.find((option: SelectOption) => String(option.value) === String(value))?.label || String(value) : + 'Select...'; + }, [value, selectOptions]); // Handle wheel scroll in dropdown const handleWheel = useCallback((e: React.WheelEvent) => { @@ -77,11 +84,27 @@ const SelectCell = ({ } }, []); - const handleSelect = (selectedValue: string) => { + const handleSelect = useCallback((selectedValue: string) => { onChange(selectedValue); setOpen(false); if (onEndEdit) onEndEdit(); - }; + }, [onChange, onEndEdit]); + + // Memoize the command items to avoid recreating them on every render + const commandItems = useMemo(() => { + return selectOptions.map((option: SelectOption) => ( + handleSelect(option.value)} + > + {option.label} + {String(option.value) === String(value) && ( + + )} + + )); + }, [selectOptions, value, handleSelect]); return ( @@ -111,21 +134,11 @@ const SelectCell = ({ No results found. - {selectOptions.map((option) => ( - handleSelect(option.value)} - > - {option.label} - {String(option.value) === String(value) && ( - - )} - - ))} + {commandItems} diff --git a/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useValidationState.tsx b/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useValidationState.tsx index 381caf5..d6c87a9 100644 --- a/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useValidationState.tsx +++ b/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useValidationState.tsx @@ -165,6 +165,7 @@ export const useValidationState = ({ const [isValidating] = useState(false) const [validationErrors, setValidationErrors] = useState>>(new Map()) const [rowValidationStatus, setRowValidationStatus] = useState>(new Map()) + const [validatingCells, setValidatingCells] = useState>(new Set()) // Template state const [templates, setTemplates] = useState([]) @@ -737,31 +738,36 @@ useEffect(() => { // Validate a single row const validateRow = useCallback((rowIndex: number) => { - // Skip if row doesn't exist or if we're in the middle of applying a template - if (rowIndex < 0 || rowIndex >= data.length) return; + // Skip if row doesn't exist + if (!data[rowIndex]) return; - // Skip validation if we're applying a template - if (isApplyingTemplateRef.current) { - return true; - } + // Mark row as validating + setRowValidationStatus(prev => { + const updated = new Map(prev); + updated.set(rowIndex, 'validating'); + return updated; + }); + // Get the row data const row = data[rowIndex]; const fieldErrors: Record = {}; let hasErrors = false; - // Ensure values are trimmed for proper validation - const cleanedRow = { ...row }; - Object.entries(cleanedRow).forEach(([key, value]) => { - if (typeof value === 'string') { - (cleanedRow as any)[key] = value.trim(); - } - }); - - // Validate each field + // Use a more efficient approach - only validate fields that need validation + // This includes required fields and fields with values fields.forEach(field => { if (field.disabled) return; + const key = String(field.key); - const value = cleanedRow[key as keyof typeof cleanedRow]; + const value = row[key as keyof typeof row]; + + // Skip validation for empty non-required fields + const isRequired = field.validations?.some(v => v.rule === 'required'); + if (!isRequired && (value === undefined || value === null || value === '')) { + return; + } + + // Validate the field const errors = validateField(value, field as Field); if (errors.length > 0) { fieldErrors[key] = errors; @@ -769,8 +775,8 @@ useEffect(() => { } }); - // Special validation for supplier and company - check existence and non-emptiness - if (!cleanedRow.supplier || (typeof cleanedRow.supplier === 'string' && cleanedRow.supplier.trim() === '')) { + // Special validation for supplier and company + if (!row.supplier) { fieldErrors['supplier'] = [{ message: 'Supplier is required', level: 'error', @@ -778,8 +784,7 @@ useEffect(() => { }]; hasErrors = true; } - - if (!cleanedRow.company || (typeof cleanedRow.company === 'string' && cleanedRow.company.trim() === '')) { + if (!row.company) { fieldErrors['company'] = [{ message: 'Company is required', level: 'error', @@ -788,64 +793,61 @@ useEffect(() => { hasErrors = true; } - // Update validation state + // Update validation errors for this row setValidationErrors(prev => { - const newErrors = new Map(prev); - if (hasErrors) { - newErrors.set(rowIndex, fieldErrors); - } else { - newErrors.delete(rowIndex); - } - return newErrors; + const updated = new Map(prev); + updated.set(rowIndex, fieldErrors); + return updated; }); + // Update row validation status setRowValidationStatus(prev => { - const newStatus = new Map(prev); - newStatus.set(rowIndex, hasErrors ? 'error' : 'validated'); - return newStatus; + const updated = new Map(prev); + updated.set(rowIndex, hasErrors ? 'error' : 'validated'); + return updated; }); - - // Update the row data with errors - setData(prev => { - const newData = [...prev]; - newData[rowIndex] = { - ...newData[rowIndex], - __errors: hasErrors ? fieldErrors : undefined - }; - return newData; - }); - - return !hasErrors; - }, [data, fields, validateField, setValidationErrors, setRowValidationStatus, setData]); + }, [data, fields, validateField]); - // Update a single row's field value + // Update a row's field value const updateRow = useCallback((rowIndex: number, key: T, value: any) => { - // Process value before updating data - let processedValue = value; - - // Strip dollar signs from price fields - if ((key === 'msrp' || key === 'cost_each') && typeof value === 'string') { - processedValue = value.replace(/[$,]/g, ''); - - // Also ensure it's a valid number - const numValue = parseFloat(processedValue); - if (!isNaN(numValue)) { - processedValue = numValue.toFixed(2); - } - } - // Save current scroll position const scrollPosition = { left: window.scrollX, top: window.scrollY }; - // Update the data immediately for UI responsiveness + // Track the cell as validating + setValidatingCells(prev => { + const newSet = new Set(prev); + newSet.add(`${rowIndex}-${key}`); + return newSet; + }); + + // Process the value based on field type + const field = fields.find(f => f.key === key); + let processedValue = value; + + // Special handling for price fields + if (field && + typeof field.fieldType === 'object' && + (field.fieldType.type === 'input' || field.fieldType.type === 'multi-input') && + 'price' in field.fieldType && + field.fieldType.price === true && + typeof value === 'string') { + // Remove $ and commas + processedValue = value.replace(/[$,\s]/g, '').trim(); + + // Convert to number if possible + const numValue = parseFloat(processedValue); + if (!isNaN(numValue)) { + processedValue = numValue.toFixed(2); + } + } + + // Update the data state setData(prevData => { const newData = [...prevData]; - - // Create a copy of the row to avoid reference issues - const updatedRow = { ...newData[rowIndex] }; + const updatedRow = { ...newData[rowIndex] } as Record; // Update the field value updatedRow[key] = processedValue; @@ -869,12 +871,21 @@ useEffect(() => { window.scrollTo(scrollPosition.left, scrollPosition.top); }); - // Validate the row after the update - setTimeout(() => { + // Use debounced validation to avoid excessive validation calls + const shouldValidateUpc = (key === 'upc' || key === 'supplier'); + + // Clear any existing timeout for this row + if (validationTimeoutsRef.current[rowIndex]) { + clearTimeout(validationTimeoutsRef.current[rowIndex]); + } + + // Set a new timeout for validation + validationTimeoutsRef.current[rowIndex] = setTimeout(() => { + // Validate the row validateRow(rowIndex); - // Trigger UPC validation if applicable - if ((key === 'upc' || key === 'supplier') && data[rowIndex]) { + // Trigger UPC validation if applicable, but only if both fields are present + if (shouldValidateUpc && data[rowIndex]) { const row = data[rowIndex]; const upcValue = key === 'upc' ? processedValue : row.upc; const supplierValue = key === 'supplier' ? processedValue : row.supplier; @@ -883,8 +894,28 @@ useEffect(() => { validateUpc(rowIndex, String(supplierValue), String(upcValue)); } } - }, 50); - }, [data, validateRow, validateUpc, setData, setRowValidationStatus, cleanPriceFields]); + + // Remove the cell from validating state + setValidatingCells(prev => { + const newSet = new Set(prev); + newSet.delete(`${rowIndex}-${key}`); + return newSet; + }); + }, 300); // Increase debounce time to reduce validation frequency + }, [data, validateRow, validateUpc, setData, setRowValidationStatus, cleanPriceFields, fields]); + + // Add this at the top of the component, after other useRef declarations + const validationTimeoutsRef = useRef>({}); + + // Clean up timeouts on unmount + useEffect(() => { + return () => { + // Clear all validation timeouts + Object.values(validationTimeoutsRef.current).forEach(timeout => { + clearTimeout(timeout); + }); + }; + }, []); // Save a new template const saveTemplate = useCallback(async (name: string, type: string) => { @@ -1286,104 +1317,133 @@ useEffect(() => { console.log(`Validating ${data.length} rows`); // Process in batches to avoid blocking the UI - const BATCH_SIZE = 50; + const BATCH_SIZE = 100; // Increase batch size for better performance let currentBatch = 0; + let totalBatches = Math.ceil(data.length / BATCH_SIZE); const processBatch = () => { const startIdx = currentBatch * BATCH_SIZE; const endIdx = Math.min(startIdx + BATCH_SIZE, data.length); + // Create a batch of validation promises + const batchPromises = []; + for (let rowIndex = startIdx; rowIndex < endIdx; rowIndex++) { - const row = data[rowIndex]; - const fieldErrors: Record = {}; - let hasErrors = false; + batchPromises.push( + new Promise(resolve => { + const row = data[rowIndex]; + const fieldErrors: Record = {}; + let hasErrors = false; - // Set default values for tax_cat and ship_restrictions if not already set - if (row.tax_cat === undefined || row.tax_cat === null || row.tax_cat === '') { - newData[rowIndex] = { - ...newData[rowIndex], - tax_cat: '0' - } as RowData; - } - - if (row.ship_restrictions === undefined || row.ship_restrictions === null || row.ship_restrictions === '') { - newData[rowIndex] = { - ...newData[rowIndex], - ship_restrictions: '0' - } as RowData; - } - - // Process price fields to strip dollar signs - use the cleanPriceFields function - const rowAsRecord = row as Record; - if ((typeof rowAsRecord.msrp === 'string' && rowAsRecord.msrp.includes('$')) || - (typeof rowAsRecord.cost_each === 'string' && rowAsRecord.cost_each.includes('$'))) { - // Clean just this row - const cleanedRow = cleanPriceFields([row])[0]; - newData[rowIndex] = cleanedRow; - } + // Set default values for tax_cat and ship_restrictions if not already set + if (row.tax_cat === undefined || row.tax_cat === null || row.tax_cat === '') { + newData[rowIndex] = { + ...newData[rowIndex], + tax_cat: '0' + } as RowData; + } + + if (row.ship_restrictions === undefined || row.ship_restrictions === null || row.ship_restrictions === '') { + newData[rowIndex] = { + ...newData[rowIndex], + ship_restrictions: '0' + } as RowData; + } + + // Process price fields to strip dollar signs - use the cleanPriceFields function + const rowAsRecord = row as Record; + if ((typeof rowAsRecord.msrp === 'string' && rowAsRecord.msrp.includes('$')) || + (typeof rowAsRecord.cost_each === 'string' && rowAsRecord.cost_each.includes('$'))) { + // Clean just this row + const cleanedRow = cleanPriceFields([row])[0]; + newData[rowIndex] = cleanedRow; + } - fields.forEach(field => { - if (field.disabled) return; - const key = String(field.key); - const value = row[key as keyof typeof row]; - const errors = validateField(value, field as Field); - if (errors.length > 0) { - fieldErrors[key] = errors; - hasErrors = true; - } + // Only validate required fields and fields with values + fields.forEach(field => { + if (field.disabled) return; + const key = String(field.key); + const value = row[key as keyof typeof row]; + + // Skip validation for empty non-required fields + const isRequired = field.validations?.some(v => v.rule === 'required'); + if (!isRequired && (value === undefined || value === null || value === '')) { + return; + } + + const errors = validateField(value, field as Field); + if (errors.length > 0) { + fieldErrors[key] = errors; + hasErrors = true; + } + }); + + // Special validation for supplier and company + if (!row.supplier) { + fieldErrors['supplier'] = [{ + message: 'Supplier is required', + level: 'error', + source: 'required' + }]; + hasErrors = true; + } + if (!row.company) { + fieldErrors['company'] = [{ + message: 'Company is required', + level: 'error', + source: 'required' + }]; + hasErrors = true; + } + + // Update validation errors for this row + initialErrors.set(rowIndex, fieldErrors); + + // Update row validation status + initialStatus.set(rowIndex, hasErrors ? 'error' : 'validated'); + + resolve(); + }) + ); + } + + // Process all promises in the batch + Promise.all(batchPromises).then(() => { + // Update state for this batch + setValidationErrors(prev => { + const newMap = new Map(prev); + initialErrors.forEach((errors, rowIndex) => { + newMap.set(rowIndex, errors); + }); + return newMap; }); - // Special validation for supplier and company - if (!row.supplier) { - fieldErrors['supplier'] = [{ - message: 'Supplier is required', - level: 'error', - source: 'required' - }]; - hasErrors = true; - } - if (!row.company) { - fieldErrors['company'] = [{ - message: 'Company is required', - level: 'error', - source: 'required' - }]; - hasErrors = true; - } - - if (hasErrors) { - initialErrors.set(rowIndex, fieldErrors); - initialStatus.set(rowIndex, 'error'); - } else { - initialStatus.set(rowIndex, 'validated'); - } - - newData[rowIndex] = { - ...newData[rowIndex], - __errors: Object.keys(fieldErrors).length > 0 ? fieldErrors : undefined - }; - } - - currentBatch++; - - // If there are more batches to process, schedule the next one - if (endIdx < data.length) { - setTimeout(processBatch, 0); - } else { - // All batches processed, update state - setData(newData); - setRowValidationStatus(initialStatus); - setValidationErrors(initialErrors); - console.log('Basic field validation complete'); + setRowValidationStatus(prev => { + const newMap = new Map(prev); + initialStatus.forEach((status, rowIndex) => { + newMap.set(rowIndex, status); + }); + return newMap; + }); - // Schedule UPC validations after basic validation is complete - setTimeout(() => { - runUPCValidation(); - }, 100); - } + // Move to the next batch or finish + currentBatch++; + if (currentBatch < totalBatches) { + // Schedule the next batch with a small delay to allow UI updates + setTimeout(processBatch, 10); + } else { + // All batches processed, update the data + setData(newData); + console.log('Basic validation complete'); + initialValidationDoneRef.current = true; + + // Run item number uniqueness validation after basic validation + validateItemNumberUniqueness(); + } + }); }; - // Start processing batches + // Start processing the first batch processBatch(); };