Optimize validation table

This commit is contained in:
2025-03-08 14:30:11 -05:00
parent 45fa583ce8
commit 31c838197a
5 changed files with 337 additions and 251 deletions

View File

@@ -309,19 +309,37 @@ export default React.memo(ValidationCell, (prev, next) => {
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
);
});

View File

@@ -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 (
<ValidationCell
field={field}
value={value}
onChange={(newValue) => 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)

View File

@@ -193,51 +193,30 @@ const MultiInputCell = <T extends string>({
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 (
<div
@@ -349,35 +328,30 @@ const MultiInputCell = <T extends string>({
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 (
<div

View File

@@ -1,4 +1,4 @@
import { useState, useRef, useCallback } from 'react'
import { useState, useRef, useCallback, useMemo } from 'react'
import { Field } from '../../../../types'
import { Check, ChevronsUpDown } from 'lucide-react'
import { Button } from '@/components/ui/button'
@@ -45,29 +45,36 @@ const SelectCell = <T extends string>({
// Ref for the command list to enable scrolling
const commandListRef = useRef<HTMLDivElement>(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 selectOptions = (options || fieldOptions || []).map(option => ({
label: option.label || String(option.value),
value: String(option.value)
}));
// 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 (selectOptions.length === 0) {
// Add a default empty option if we have none
selectOptions.push({ label: 'No options available', value: '' });
}
if (processedOptions.length === 0) {
// Add a default empty option if we have none
processedOptions.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...';
return processedOptions;
}, [field.fieldType, options]);
// 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 = <T extends string>({
}
}, []);
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) => (
<CommandItem
key={option.value}
value={option.label}
onSelect={() => handleSelect(option.value)}
>
{option.label}
{String(option.value) === String(value) && (
<Check className="ml-auto h-4 w-4" />
)}
</CommandItem>
));
}, [selectOptions, value, handleSelect]);
return (
<Popover open={open} onOpenChange={setOpen}>
@@ -111,21 +134,11 @@ const SelectCell = <T extends string>({
<CommandList
ref={commandListRef}
onWheel={handleWheel}
className="max-h-[200px]"
>
<CommandEmpty>No results found.</CommandEmpty>
<CommandGroup>
{selectOptions.map((option) => (
<CommandItem
key={option.value}
value={option.label}
onSelect={() => handleSelect(option.value)}
>
{option.label}
{String(option.value) === String(value) && (
<Check className="ml-auto h-4 w-4" />
)}
</CommandItem>
))}
{commandItems}
</CommandGroup>
</CommandList>
</Command>

View File

@@ -165,6 +165,7 @@ export const useValidationState = <T extends string>({
const [isValidating] = useState(false)
const [validationErrors, setValidationErrors] = useState<Map<number, Record<string, ErrorType[]>>>(new Map())
const [rowValidationStatus, setRowValidationStatus] = useState<Map<number, 'pending' | 'validating' | 'validated' | 'error'>>(new Map())
const [validatingCells, setValidatingCells] = useState<Set<string>>(new Set())
// Template state
const [templates, setTemplates] = useState<Template[]>([])
@@ -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<string, ErrorType[]> = {};
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<T>);
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;
});
}, [data, fields, validateField]);
// 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]);
// 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<string, any>;
// 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<Record<number, NodeJS.Timeout>>({});
// 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<string, ErrorType[]> = {};
let hasErrors = false;
batchPromises.push(
new Promise<void>(resolve => {
const row = data[rowIndex];
const fieldErrors: Record<string, ErrorType[]> = {};
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<T>;
}
// 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<T>;
}
if (row.ship_restrictions === undefined || row.ship_restrictions === null || row.ship_restrictions === '') {
newData[rowIndex] = {
...newData[rowIndex],
ship_restrictions: '0'
} as RowData<T>;
}
if (row.ship_restrictions === undefined || row.ship_restrictions === null || row.ship_restrictions === '') {
newData[rowIndex] = {
...newData[rowIndex],
ship_restrictions: '0'
} as RowData<T>;
}
// Process price fields to strip dollar signs - use the cleanPriceFields function
const rowAsRecord = row as Record<string, any>;
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;
}
// Process price fields to strip dollar signs - use the cleanPriceFields function
const rowAsRecord = row as Record<string, any>;
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<T>);
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<T>);
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;
}
setRowValidationStatus(prev => {
const newMap = new Map(prev);
initialStatus.forEach((status, rowIndex) => {
newMap.set(rowIndex, status);
});
return newMap;
});
if (hasErrors) {
initialErrors.set(rowIndex, fieldErrors);
initialStatus.set(rowIndex, 'error');
// 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 {
initialStatus.set(rowIndex, 'validated');
// 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();
}
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');
// Schedule UPC validations after basic validation is complete
setTimeout(() => {
runUPCValidation();
}, 100);
}
});
};
// Start processing batches
// Start processing the first batch
processBatch();
};