Fix issues with validation errors showing and problems with concurrent editing, improve scroll position saving
This commit is contained in:
@@ -1188,7 +1188,6 @@ export const MatchColumnsStep = React.memo(<T extends string>({
|
|||||||
})) as unknown as Fields<T>
|
})) as unknown as Fields<T>
|
||||||
|
|
||||||
const unmatched = findUnmatchedRequiredFields(typedFields, columns);
|
const unmatched = findUnmatchedRequiredFields(typedFields, columns);
|
||||||
console.log("Unmatched required fields:", unmatched);
|
|
||||||
return unmatched;
|
return unmatched;
|
||||||
}, [fields, columns])
|
}, [fields, columns])
|
||||||
|
|
||||||
@@ -1200,7 +1199,6 @@ export const MatchColumnsStep = React.memo(<T extends string>({
|
|||||||
// Type assertion to handle the DeepReadonly<T> vs string type mismatch
|
// Type assertion to handle the DeepReadonly<T> vs string type mismatch
|
||||||
return !unmatchedRequiredFields.includes(key as any);
|
return !unmatchedRequiredFields.includes(key as any);
|
||||||
});
|
});
|
||||||
console.log("Matched required fields:", matched);
|
|
||||||
return matched;
|
return matched;
|
||||||
}, [requiredFields, unmatchedRequiredFields]);
|
}, [requiredFields, unmatchedRequiredFields]);
|
||||||
|
|
||||||
|
|||||||
@@ -100,8 +100,6 @@ const BaseCellContent = React.memo(({
|
|||||||
(field.fieldType.type === 'input' || field.fieldType.type === 'multi-input') &&
|
(field.fieldType.type === 'input' || field.fieldType.type === 'multi-input') &&
|
||||||
field.fieldType.price === true;
|
field.fieldType.price === true;
|
||||||
|
|
||||||
console.log(`BaseCellContent: field.key=${field.key}, fieldType=${fieldType}, disabled=${field.disabled}, options=`, options);
|
|
||||||
|
|
||||||
if (fieldType === 'select') {
|
if (fieldType === 'select') {
|
||||||
return (
|
return (
|
||||||
<SelectCell
|
<SelectCell
|
||||||
@@ -175,20 +173,17 @@ export interface ValidationCellProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Add efficient error message extraction function
|
// Add efficient error message extraction function
|
||||||
const getErrorMessage = (error: ErrorObject): string => error.message;
|
|
||||||
|
|
||||||
// Add a utility function to process errors with appropriate caching
|
// Highly optimized error processing function with fast paths for common cases
|
||||||
function processErrors(value: any, errors: ErrorObject[]): {
|
function processErrors(value: any, errors: ErrorObject[]): {
|
||||||
filteredErrors: ErrorObject[];
|
|
||||||
hasError: boolean;
|
hasError: boolean;
|
||||||
isRequiredButEmpty: boolean;
|
isRequiredButEmpty: boolean;
|
||||||
shouldShowErrorIcon: boolean;
|
shouldShowErrorIcon: boolean;
|
||||||
errorMessages: string;
|
errorMessages: string;
|
||||||
} {
|
} {
|
||||||
// Fast path - if no errors, return immediately
|
// Fast path - if no errors or empty error array, return immediately
|
||||||
if (!errors || errors.length === 0) {
|
if (!errors || errors.length === 0) {
|
||||||
return {
|
return {
|
||||||
filteredErrors: [],
|
|
||||||
hasError: false,
|
hasError: false,
|
||||||
isRequiredButEmpty: false,
|
isRequiredButEmpty: false,
|
||||||
shouldShowErrorIcon: false,
|
shouldShowErrorIcon: false,
|
||||||
@@ -196,45 +191,37 @@ function processErrors(value: any, errors: ErrorObject[]): {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use the shared isEmpty function instead of defining a local one
|
// Use the shared isEmpty function for value checking
|
||||||
const valueIsEmpty = isEmpty(value);
|
const valueIsEmpty = isEmpty(value);
|
||||||
|
|
||||||
// If not empty, filter out required errors
|
// Fast path for the most common case - required field with empty value
|
||||||
// Create a new array only if we need to filter (avoid unnecessary allocations)
|
if (valueIsEmpty && errors.length === 1 && errors[0].type === ErrorType.Required) {
|
||||||
let filteredErrors: ErrorObject[];
|
return {
|
||||||
let hasRequiredError = false;
|
hasError: true,
|
||||||
|
isRequiredButEmpty: true,
|
||||||
if (valueIsEmpty) {
|
shouldShowErrorIcon: false,
|
||||||
// For empty values, check if there are required errors
|
errorMessages: ''
|
||||||
hasRequiredError = errors.some(error => error.type === ErrorType.Required);
|
};
|
||||||
filteredErrors = errors;
|
|
||||||
} else {
|
|
||||||
// For non-empty values, filter out required errors
|
|
||||||
filteredErrors = errors.filter(error => error.type !== ErrorType.Required);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine if any actual errors exist after filtering
|
// For non-empty values with errors, we need to show error icons
|
||||||
const hasError = filteredErrors.length > 0 && filteredErrors.some(error =>
|
const hasError = errors.some(error => error.level === 'error' || error.level === 'warning');
|
||||||
error.level === 'error' || error.level === 'warning'
|
|
||||||
);
|
|
||||||
|
|
||||||
// Check if field is required but empty
|
// For empty values with required errors, show only a border
|
||||||
const isRequiredButEmpty = valueIsEmpty && hasRequiredError;
|
const isRequiredButEmpty = valueIsEmpty && errors.some(error => error.type === ErrorType.Required);
|
||||||
|
|
||||||
// Only show error icons for non-empty fields with actual errors
|
// Show error icons for non-empty fields with errors, or for empty fields with non-required errors
|
||||||
const shouldShowErrorIcon = hasError && (!valueIsEmpty || !hasRequiredError);
|
const shouldShowErrorIcon = hasError && (!valueIsEmpty || !errors.every(error => error.type === ErrorType.Required));
|
||||||
|
|
||||||
// Get error messages for the tooltip - only if we need to show icon
|
// Only compute error messages if we're going to show an icon
|
||||||
let errorMessages = '';
|
const errorMessages = shouldShowErrorIcon
|
||||||
if (shouldShowErrorIcon) {
|
? errors
|
||||||
errorMessages = filteredErrors
|
.filter(e => e.level === 'error' || e.level === 'warning')
|
||||||
.filter(e => e.level === 'error' || e.level === 'warning')
|
.map(e => e.message)
|
||||||
.map(getErrorMessage)
|
.join('\n')
|
||||||
.join('\n');
|
: '';
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
filteredErrors,
|
|
||||||
hasError,
|
hasError,
|
||||||
isRequiredButEmpty,
|
isRequiredButEmpty,
|
||||||
shouldShowErrorIcon,
|
shouldShowErrorIcon,
|
||||||
@@ -242,7 +229,7 @@ function processErrors(value: any, errors: ErrorObject[]): {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper function to compare error arrays efficiently
|
// Helper function to compare error arrays efficiently with a hash-based approach
|
||||||
function compareErrorArrays(prevErrors: ErrorObject[], nextErrors: ErrorObject[]): boolean {
|
function compareErrorArrays(prevErrors: ErrorObject[], nextErrors: ErrorObject[]): boolean {
|
||||||
// Fast path for referential equality
|
// Fast path for referential equality
|
||||||
if (prevErrors === nextErrors) return true;
|
if (prevErrors === nextErrors) return true;
|
||||||
@@ -251,15 +238,21 @@ function compareErrorArrays(prevErrors: ErrorObject[], nextErrors: ErrorObject[]
|
|||||||
if (!prevErrors || !nextErrors) return prevErrors === nextErrors;
|
if (!prevErrors || !nextErrors) return prevErrors === nextErrors;
|
||||||
if (prevErrors.length !== nextErrors.length) return false;
|
if (prevErrors.length !== nextErrors.length) return false;
|
||||||
|
|
||||||
// Check if errors are equivalent
|
// Generate simple hash from error properties
|
||||||
return prevErrors.every((error, index) => {
|
const getErrorHash = (error: ErrorObject): string => {
|
||||||
const nextError = nextErrors[index];
|
return `${error.message}|${error.level}|${error.type || ''}`;
|
||||||
return (
|
};
|
||||||
error.message === nextError.message &&
|
|
||||||
error.level === nextError.level &&
|
// Compare using hashes
|
||||||
error.type === nextError.type
|
const prevHashes = prevErrors.map(getErrorHash);
|
||||||
);
|
const nextHashes = nextErrors.map(getErrorHash);
|
||||||
});
|
|
||||||
|
// Sort hashes to ensure consistent order
|
||||||
|
prevHashes.sort();
|
||||||
|
nextHashes.sort();
|
||||||
|
|
||||||
|
// Compare sorted hash arrays
|
||||||
|
return prevHashes.join(',') === nextHashes.join(',');
|
||||||
}
|
}
|
||||||
|
|
||||||
const ValidationCell = React.memo(({
|
const ValidationCell = React.memo(({
|
||||||
@@ -300,15 +293,18 @@ const ValidationCell = React.memo(({
|
|||||||
// Add state for hover on target row
|
// Add state for hover on target row
|
||||||
const [isTargetRowHovered, setIsTargetRowHovered] = React.useState(false);
|
const [isTargetRowHovered, setIsTargetRowHovered] = React.useState(false);
|
||||||
|
|
||||||
|
// Force isValidating to be a boolean
|
||||||
|
const isLoading = isValidating === true;
|
||||||
|
|
||||||
// Handle copy down button click
|
// Handle copy down button click
|
||||||
const handleCopyDownClick = () => {
|
const handleCopyDownClick = React.useCallback(() => {
|
||||||
if (copyDown && totalRows > rowIndex + 1) {
|
if (copyDown && totalRows > rowIndex + 1) {
|
||||||
// Enter copy down mode
|
// Enter copy down mode
|
||||||
copyDownContext.setIsInCopyDownMode(true);
|
copyDownContext.setIsInCopyDownMode(true);
|
||||||
copyDownContext.setSourceRowIndex(rowIndex);
|
copyDownContext.setSourceRowIndex(rowIndex);
|
||||||
copyDownContext.setSourceFieldKey(fieldKey);
|
copyDownContext.setSourceFieldKey(fieldKey);
|
||||||
}
|
}
|
||||||
};
|
}, [copyDown, copyDownContext, fieldKey, rowIndex, totalRows]);
|
||||||
|
|
||||||
// Check if this cell is in a row that can be a target for copy down
|
// Check if this cell is in a row that can be a target for copy down
|
||||||
const isInTargetRow = copyDownContext.isInCopyDownMode &&
|
const isInTargetRow = copyDownContext.isInCopyDownMode &&
|
||||||
@@ -319,7 +315,7 @@ const ValidationCell = React.memo(({
|
|||||||
const isSelectedTarget = isInTargetRow && rowIndex <= (copyDownContext.targetRowIndex || 0);
|
const isSelectedTarget = isInTargetRow && rowIndex <= (copyDownContext.targetRowIndex || 0);
|
||||||
|
|
||||||
// Handle click on a potential target cell
|
// Handle click on a potential target cell
|
||||||
const handleTargetCellClick = () => {
|
const handleTargetCellClick = React.useCallback(() => {
|
||||||
if (isInTargetRow && copyDownContext.sourceRowIndex !== null && copyDownContext.sourceFieldKey !== null) {
|
if (isInTargetRow && copyDownContext.sourceRowIndex !== null && copyDownContext.sourceFieldKey !== null) {
|
||||||
copyDownContext.handleCopyDownComplete(
|
copyDownContext.handleCopyDownComplete(
|
||||||
copyDownContext.sourceRowIndex,
|
copyDownContext.sourceRowIndex,
|
||||||
@@ -327,22 +323,35 @@ const ValidationCell = React.memo(({
|
|||||||
rowIndex
|
rowIndex
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
}, [copyDownContext, isInTargetRow, rowIndex]);
|
||||||
|
|
||||||
|
// Memoize the cell style objects to avoid recreating them on every render
|
||||||
|
const cellStyle = React.useMemo(() => ({
|
||||||
|
width: `${width}px`,
|
||||||
|
minWidth: `${width}px`,
|
||||||
|
maxWidth: `${width}px`,
|
||||||
|
boxSizing: 'border-box' as const,
|
||||||
|
cursor: isInTargetRow ? 'pointer' : undefined,
|
||||||
|
...(isSourceCell ? { backgroundColor: '#dbeafe', borderRadius: '0.375rem', padding: 0, boxShadow: '0 0 0 2px #3b82f6' } :
|
||||||
|
isSelectedTarget ? { backgroundColor: '#bfdbfe', borderRadius: '0.375rem', padding: 0 } :
|
||||||
|
isInTargetRow && isTargetRowHovered ? { backgroundColor: '#dbeafe', borderRadius: '0.375rem', padding: 0, cursor: 'pointer' } :
|
||||||
|
isInTargetRow ? { borderRadius: '0.375rem', padding: 0, cursor: 'pointer' } : {})
|
||||||
|
}), [width, isInTargetRow, isSourceCell, isSelectedTarget, isTargetRowHovered]);
|
||||||
|
|
||||||
|
// Memoize the cell class name to prevent re-calculating on every render
|
||||||
|
const cellClassName = React.useMemo(() => {
|
||||||
|
if (isSourceCell || isSelectedTarget || isInTargetRow) {
|
||||||
|
return isSourceCell ? '!bg-blue-100 !border-blue-500 !rounded-md' :
|
||||||
|
isSelectedTarget ? '!bg-blue-200 !border-blue-200 !rounded-md' :
|
||||||
|
isInTargetRow ? 'hover:!bg-blue-100 !border-blue-200 !rounded-md' : '';
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}, [isSourceCell, isSelectedTarget, isInTargetRow]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TableCell
|
<TableCell
|
||||||
className="p-1 group relative"
|
className="p-1 group relative"
|
||||||
style={{
|
style={cellStyle}
|
||||||
width: `${width}px`,
|
|
||||||
minWidth: `${width}px`,
|
|
||||||
maxWidth: `${width}px`,
|
|
||||||
boxSizing: 'border-box',
|
|
||||||
cursor: isInTargetRow ? 'pointer' : undefined,
|
|
||||||
...(isSourceCell ? { backgroundColor: '#dbeafe', borderRadius: '0.375rem', padding: 0, boxShadow: '0 0 0 2px #3b82f6' } :
|
|
||||||
isSelectedTarget ? { backgroundColor: '#bfdbfe', borderRadius: '0.375rem', padding: 0 } :
|
|
||||||
isInTargetRow && isTargetRowHovered ? { backgroundColor: '#dbeafe', borderRadius: '0.375rem', padding: 0, cursor: 'pointer' } :
|
|
||||||
isInTargetRow ? { borderRadius: '0.375rem', padding: 0, cursor: 'pointer' } : {})
|
|
||||||
}}
|
|
||||||
onClick={isInTargetRow ? handleTargetCellClick : undefined}
|
onClick={isInTargetRow ? handleTargetCellClick : undefined}
|
||||||
onMouseEnter={isInTargetRow ? () => setIsTargetRowHovered(true) : undefined}
|
onMouseEnter={isInTargetRow ? () => setIsTargetRowHovered(true) : undefined}
|
||||||
onMouseLeave={isInTargetRow ? () => setIsTargetRowHovered(false) : undefined}
|
onMouseLeave={isInTargetRow ? () => setIsTargetRowHovered(false) : undefined}
|
||||||
@@ -401,25 +410,20 @@ const ValidationCell = React.memo(({
|
|||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{isValidating ? (
|
{isLoading ? (
|
||||||
<div className={`flex items-center justify-center gap-2 border ${hasError || isRequiredButEmpty ? 'border-red-500' : 'border-input'} rounded-sm px-2 py-1.5`}>
|
<div className={`flex items-center justify-center gap-2 border ${hasError || isRequiredButEmpty ? 'border-red-500' : 'border-input'} rounded-sm px-2 py-1.5`}>
|
||||||
<Loader2 className="h-4 w-4 animate-spin text-blue-500" />
|
<Loader2 className="h-4 w-4 animate-spin text-blue-500" />
|
||||||
<span>Loading...</span>
|
<span>Loading...</span>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className={`truncate overflow-hidden ${isCopyDownHovered && !copyDownContext.isInCopyDownMode ? 'bg-blue-50/50' : ''}`}>
|
<div className={`truncate overflow-hidden ${isCopyDownHovered && !copyDownContext.isInCopyDownMode ? 'bg-blue-50/50' : ''}`}>
|
||||||
{(() => { console.log(`ValidationCell: fieldKey=${fieldKey}, options=`, options); return null; })()}
|
|
||||||
<BaseCellContent
|
<BaseCellContent
|
||||||
field={field}
|
field={field}
|
||||||
value={displayValue}
|
value={displayValue}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
hasErrors={hasError || isRequiredButEmpty}
|
hasErrors={hasError || isRequiredButEmpty}
|
||||||
options={options}
|
options={options}
|
||||||
className={isSourceCell || isSelectedTarget || isInTargetRow ? `${
|
className={cellClassName}
|
||||||
isSourceCell ? '!bg-blue-100 !border-blue-500 !rounded-md' :
|
|
||||||
isSelectedTarget ? '!bg-blue-200 !border-blue-200 !rounded-md' :
|
|
||||||
isInTargetRow ? 'hover:!bg-blue-100 !border-blue-200 !rounded-md' : ''
|
|
||||||
}` : ''}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -427,32 +431,48 @@ const ValidationCell = React.memo(({
|
|||||||
</TableCell>
|
</TableCell>
|
||||||
);
|
);
|
||||||
}, (prevProps, nextProps) => {
|
}, (prevProps, nextProps) => {
|
||||||
// Deep compare the most important props to avoid unnecessary re-renders
|
// Fast path: if all props are the same object
|
||||||
const valueEqual = prevProps.value === nextProps.value;
|
if (prevProps === nextProps) return true;
|
||||||
const isValidatingEqual = prevProps.isValidating === nextProps.isValidating;
|
|
||||||
const fieldEqual = prevProps.field === nextProps.field;
|
|
||||||
const itemNumberEqual = prevProps.itemNumber === nextProps.itemNumber;
|
|
||||||
|
|
||||||
// Use enhanced error comparison
|
// Optimize the memo comparison function, checking most impactful props first
|
||||||
const errorsEqual = compareErrorArrays(prevProps.errors, nextProps.errors);
|
// Check isValidating first as it's most likely to change frequently
|
||||||
|
if (prevProps.isValidating !== nextProps.isValidating) return false;
|
||||||
|
|
||||||
// Shallow options comparison with length check
|
// Then check value changes
|
||||||
const optionsEqual =
|
if (prevProps.value !== nextProps.value) return false;
|
||||||
prevProps.options === nextProps.options ||
|
|
||||||
(Array.isArray(prevProps.options) &&
|
|
||||||
Array.isArray(nextProps.options) &&
|
|
||||||
prevProps.options.length === nextProps.options.length &&
|
|
||||||
prevProps.options.every((opt, idx) => {
|
|
||||||
// Handle safely when options might be undefined
|
|
||||||
const nextOptions = nextProps.options || [];
|
|
||||||
return opt === nextOptions[idx];
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Skip comparison of props that rarely change
|
// Item number is related to validation state
|
||||||
// (rowIndex, width, copyDown, totalRows)
|
if (prevProps.itemNumber !== nextProps.itemNumber) return false;
|
||||||
|
|
||||||
return valueEqual && isValidatingEqual && fieldEqual && errorsEqual &&
|
// Check errors with our optimized comparison function
|
||||||
optionsEqual && itemNumberEqual;
|
if (!compareErrorArrays(prevProps.errors, nextProps.errors)) return false;
|
||||||
|
|
||||||
|
// Check field identity
|
||||||
|
if (prevProps.field !== nextProps.field) return false;
|
||||||
|
|
||||||
|
// Shallow options comparison - only if field type is select or multi-select
|
||||||
|
if (prevProps.field.fieldType?.type === 'select' || prevProps.field.fieldType?.type === 'multi-select') {
|
||||||
|
const optionsEqual = prevProps.options === nextProps.options ||
|
||||||
|
(Array.isArray(prevProps.options) &&
|
||||||
|
Array.isArray(nextProps.options) &&
|
||||||
|
prevProps.options.length === nextProps.options.length &&
|
||||||
|
prevProps.options.every((opt, idx) => {
|
||||||
|
const nextOptions = nextProps.options || [];
|
||||||
|
return opt === nextOptions[idx];
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (!optionsEqual) return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check copy down context changes
|
||||||
|
const copyDownContextChanged =
|
||||||
|
prevProps.rowIndex !== nextProps.rowIndex ||
|
||||||
|
prevProps.fieldKey !== nextProps.fieldKey;
|
||||||
|
|
||||||
|
if (copyDownContextChanged) return false;
|
||||||
|
|
||||||
|
// All essential props are the same - we can skip re-rendering
|
||||||
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
ValidationCell.displayName = 'ValidationCell';
|
ValidationCell.displayName = 'ValidationCell';
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import SearchableTemplateSelect from './SearchableTemplateSelect'
|
|||||||
import { useAiValidation } from '../hooks/useAiValidation'
|
import { useAiValidation } from '../hooks/useAiValidation'
|
||||||
import { AiValidationDialogs } from './AiValidationDialogs'
|
import { AiValidationDialogs } from './AiValidationDialogs'
|
||||||
import { Fields } from '../../../types'
|
import { Fields } from '../../../types'
|
||||||
|
import { ErrorType, ValidationError, ErrorSources } from '../../../types'
|
||||||
import { SearchProductTemplateDialog } from '@/components/templates/SearchProductTemplateDialog'
|
import { SearchProductTemplateDialog } from '@/components/templates/SearchProductTemplateDialog'
|
||||||
import { TemplateForm } from '@/components/templates/TemplateForm'
|
import { TemplateForm } from '@/components/templates/TemplateForm'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
@@ -16,6 +17,7 @@ import { RowSelectionState } from '@tanstack/react-table'
|
|||||||
import { useUpcValidation } from '../hooks/useUpcValidation'
|
import { useUpcValidation } from '../hooks/useUpcValidation'
|
||||||
import { useProductLinesFetching } from '../hooks/useProductLinesFetching'
|
import { useProductLinesFetching } from '../hooks/useProductLinesFetching'
|
||||||
import UpcValidationTableAdapter from './UpcValidationTableAdapter'
|
import UpcValidationTableAdapter from './UpcValidationTableAdapter'
|
||||||
|
import { clearAllUniquenessCaches } from '../hooks/useValidation'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ValidationContainer component - the main wrapper for the validation step
|
* ValidationContainer component - the main wrapper for the validation step
|
||||||
@@ -58,7 +60,7 @@ const ValidationContainer = <T extends string>({
|
|||||||
loadTemplates,
|
loadTemplates,
|
||||||
setData,
|
setData,
|
||||||
fields,
|
fields,
|
||||||
isLoadingTemplates } = validationState
|
isLoadingTemplates } = validationState
|
||||||
|
|
||||||
// Use product lines fetching hook
|
// Use product lines fetching hook
|
||||||
const {
|
const {
|
||||||
@@ -114,6 +116,58 @@ const ValidationContainer = <T extends string>({
|
|||||||
const [templateFormInitialData, setTemplateFormInitialData] = useState<any>(null)
|
const [templateFormInitialData, setTemplateFormInitialData] = useState<any>(null)
|
||||||
const [fieldOptions, setFieldOptions] = useState<any>(null)
|
const [fieldOptions, setFieldOptions] = useState<any>(null)
|
||||||
|
|
||||||
|
// Track fields that need revalidation due to value changes
|
||||||
|
const [fieldsToRevalidate, setFieldsToRevalidate] = useState<Set<number>>(new Set());
|
||||||
|
const [fieldsToRevalidateMap, setFieldsToRevalidateMap] = useState<{[rowIndex: number]: string[]}>({});
|
||||||
|
|
||||||
|
// Function to mark a row for revalidation
|
||||||
|
const markRowForRevalidation = useCallback((rowIndex: number, fieldKey?: string) => {
|
||||||
|
setFieldsToRevalidate(prev => {
|
||||||
|
const newSet = new Set(prev);
|
||||||
|
newSet.add(rowIndex);
|
||||||
|
return newSet;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Also track which specific field needs to be revalidated
|
||||||
|
if (fieldKey) {
|
||||||
|
setFieldsToRevalidateMap(prev => {
|
||||||
|
const newMap = { ...prev };
|
||||||
|
if (!newMap[rowIndex]) {
|
||||||
|
newMap[rowIndex] = [];
|
||||||
|
}
|
||||||
|
if (!newMap[rowIndex].includes(fieldKey)) {
|
||||||
|
newMap[rowIndex] = [...newMap[rowIndex], fieldKey];
|
||||||
|
}
|
||||||
|
return newMap;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Add a ref to track the last validation time
|
||||||
|
const lastValidationTime = useRef(0);
|
||||||
|
|
||||||
|
// Trigger revalidation only for specifically marked fields
|
||||||
|
useEffect(() => {
|
||||||
|
if (fieldsToRevalidate.size === 0) return;
|
||||||
|
|
||||||
|
// Revalidate the marked rows
|
||||||
|
const rowsToRevalidate = Array.from(fieldsToRevalidate);
|
||||||
|
|
||||||
|
// Clear the revalidation set
|
||||||
|
setFieldsToRevalidate(new Set());
|
||||||
|
|
||||||
|
// Get the fields map for revalidation
|
||||||
|
const fieldsMap = { ...fieldsToRevalidateMap };
|
||||||
|
|
||||||
|
// Clear the fields map
|
||||||
|
setFieldsToRevalidateMap({});
|
||||||
|
|
||||||
|
console.log(`Validating ${rowsToRevalidate.length} rows with specific fields`);
|
||||||
|
|
||||||
|
// Revalidate each row with specific fields information
|
||||||
|
validationState.revalidateRows(rowsToRevalidate, fieldsMap);
|
||||||
|
}, [fieldsToRevalidate, validationState, fieldsToRevalidateMap]);
|
||||||
|
|
||||||
// Function to fetch field options for template form
|
// Function to fetch field options for template form
|
||||||
const fetchFieldOptions = useCallback(async () => {
|
const fetchFieldOptions = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
@@ -246,14 +300,105 @@ const ValidationContainer = <T extends string>({
|
|||||||
}
|
}
|
||||||
}, [prepareRowDataForTemplateForm, fetchFieldOptions]);
|
}, [prepareRowDataForTemplateForm, fetchFieldOptions]);
|
||||||
|
|
||||||
|
// Create a function to validate uniqueness if validateUniqueItemNumbers is not available
|
||||||
|
const validateUniqueValues = useCallback(() => {
|
||||||
|
// Check if validateUniqueItemNumbers exists on validationState using safer method
|
||||||
|
if ('validateUniqueItemNumbers' in validationState &&
|
||||||
|
typeof (validationState as any).validateUniqueItemNumbers === 'function') {
|
||||||
|
(validationState as any).validateUniqueItemNumbers();
|
||||||
|
} else {
|
||||||
|
// Otherwise fall back to revalidating all rows
|
||||||
|
validationState.revalidateRows(Array.from(Array(data.length).keys()));
|
||||||
|
}
|
||||||
|
}, [validationState, data.length]);
|
||||||
|
|
||||||
|
// Apply item numbers to data and trigger revalidation for uniqueness
|
||||||
|
const applyItemNumbersAndValidate = useCallback(() => {
|
||||||
|
// Clear uniqueness validation caches to ensure fresh validation
|
||||||
|
clearAllUniquenessCaches();
|
||||||
|
|
||||||
|
upcValidation.applyItemNumbersToData((updatedRowIds) => {
|
||||||
|
console.log(`Revalidating item numbers for ${updatedRowIds.length} rows`);
|
||||||
|
|
||||||
|
// Force clearing all uniqueness errors for item_number and upc fields first
|
||||||
|
const newValidationErrors = new Map(validationErrors);
|
||||||
|
|
||||||
|
// Clear uniqueness errors for all rows that had their item numbers updated
|
||||||
|
updatedRowIds.forEach(rowIndex => {
|
||||||
|
const rowErrors = newValidationErrors.get(rowIndex);
|
||||||
|
if (rowErrors) {
|
||||||
|
// Create a copy of row errors without uniqueness errors for item_number/upc
|
||||||
|
const filteredErrors: Record<string, ValidationError[]> = { ...rowErrors };
|
||||||
|
let hasChanges = false;
|
||||||
|
|
||||||
|
// Clear item_number errors if they exist and are uniqueness errors
|
||||||
|
if (filteredErrors.item_number &&
|
||||||
|
filteredErrors.item_number.some(e => e.type === ErrorType.Unique)) {
|
||||||
|
delete filteredErrors.item_number;
|
||||||
|
hasChanges = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also clear upc/barcode errors if they exist and are uniqueness errors
|
||||||
|
if (filteredErrors.upc &&
|
||||||
|
filteredErrors.upc.some(e => e.type === ErrorType.Unique)) {
|
||||||
|
delete filteredErrors.upc;
|
||||||
|
hasChanges = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filteredErrors.barcode &&
|
||||||
|
filteredErrors.barcode.some(e => e.type === ErrorType.Unique)) {
|
||||||
|
delete filteredErrors.barcode;
|
||||||
|
hasChanges = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the map or remove the row entry if no errors remain
|
||||||
|
if (hasChanges) {
|
||||||
|
if (Object.keys(filteredErrors).length > 0) {
|
||||||
|
newValidationErrors.set(rowIndex, filteredErrors);
|
||||||
|
} else {
|
||||||
|
newValidationErrors.delete(rowIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Call the revalidateRows function directly with affected rows
|
||||||
|
validationState.revalidateRows(updatedRowIds);
|
||||||
|
|
||||||
|
// Immediately run full uniqueness validation across all rows if available
|
||||||
|
// This is crucial to properly identify new uniqueness issues
|
||||||
|
setTimeout(() => {
|
||||||
|
validateUniqueValues();
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
// Mark all updated rows for revalidation
|
||||||
|
updatedRowIds.forEach(rowIndex => {
|
||||||
|
markRowForRevalidation(rowIndex, 'item_number');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}, [upcValidation.applyItemNumbersToData, markRowForRevalidation, clearAllUniquenessCaches, validationErrors, validationState.revalidateRows, validateUniqueValues]);
|
||||||
|
|
||||||
// Handle next button click - memoized
|
// Handle next button click - memoized
|
||||||
const handleNext = useCallback(() => {
|
const handleNext = useCallback(() => {
|
||||||
// Make sure any pending item numbers are applied
|
// Make sure any pending item numbers are applied
|
||||||
upcValidation.applyItemNumbersToData();
|
upcValidation.applyItemNumbersToData(updatedRowIds => {
|
||||||
|
// Mark updated rows for revalidation
|
||||||
|
updatedRowIds.forEach(rowIndex => {
|
||||||
|
markRowForRevalidation(rowIndex, 'item_number');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Small delay to ensure all validations complete before proceeding
|
||||||
|
setTimeout(() => {
|
||||||
|
// Call the onNext callback with the validated data
|
||||||
|
onNext?.(data);
|
||||||
|
}, 100);
|
||||||
|
});
|
||||||
|
|
||||||
// Call the onNext callback with the validated data
|
// If no item numbers to apply, just proceed
|
||||||
onNext?.(data)
|
if (upcValidation.validatingRows.size === 0) {
|
||||||
}, [onNext, data, upcValidation.applyItemNumbersToData]);
|
onNext?.(data);
|
||||||
|
}
|
||||||
|
}, [onNext, data, upcValidation, markRowForRevalidation]);
|
||||||
|
|
||||||
const deleteSelectedRows = useCallback(() => {
|
const deleteSelectedRows = useCallback(() => {
|
||||||
// Get selected row keys (which may be UUIDs)
|
// Get selected row keys (which may be UUIDs)
|
||||||
@@ -331,9 +476,73 @@ const ValidationContainer = <T extends string>({
|
|||||||
setRowSelection(newSelection);
|
setRowSelection(newSelection);
|
||||||
}, [setRowSelection]);
|
}, [setRowSelection]);
|
||||||
|
|
||||||
|
// Add scroll container ref at the container level
|
||||||
|
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const lastScrollPosition = useRef({ left: 0, top: 0 });
|
||||||
|
const isScrolling = useRef(false);
|
||||||
|
|
||||||
|
// Track if we're currently validating a UPC
|
||||||
|
const isValidatingUpcRef = useRef(false);
|
||||||
|
|
||||||
|
// Track last UPC update to prevent conflicting changes
|
||||||
|
const lastUpcUpdate = useRef({
|
||||||
|
rowIndex: -1,
|
||||||
|
supplier: "",
|
||||||
|
upc: ""
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add these ref declarations here, at component level
|
||||||
|
const lastCompanyFetchTime = useRef<Record<string, number>>({});
|
||||||
|
const lastLineFetchTime = useRef<Record<string, number>>({});
|
||||||
|
|
||||||
|
// Memoize scroll handlers - simplified to avoid performance issues
|
||||||
|
const handleScroll = useCallback((event: React.UIEvent<HTMLDivElement> | Event) => {
|
||||||
|
// Store scroll position directly without conditions
|
||||||
|
const target = event.currentTarget as HTMLDivElement;
|
||||||
|
lastScrollPosition.current = {
|
||||||
|
left: target.scrollLeft,
|
||||||
|
top: target.scrollTop
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Add scroll event listener
|
||||||
|
useEffect(() => {
|
||||||
|
const container = scrollContainerRef.current;
|
||||||
|
if (container) {
|
||||||
|
// Convert React event handler to native event handler
|
||||||
|
const nativeHandler = ((evt: Event) => {
|
||||||
|
handleScroll(evt);
|
||||||
|
}) as EventListener;
|
||||||
|
|
||||||
|
container.addEventListener('scroll', nativeHandler, { passive: true });
|
||||||
|
return () => container.removeEventListener('scroll', nativeHandler);
|
||||||
|
}
|
||||||
|
}, [handleScroll]);
|
||||||
|
|
||||||
|
// Use a ref to track if we need to restore scroll position
|
||||||
|
const needScrollRestore = useRef(false);
|
||||||
|
|
||||||
|
// Set flag when data changes
|
||||||
|
useEffect(() => {
|
||||||
|
needScrollRestore.current = true;
|
||||||
|
// Only restore scroll on layout effects to avoid triggering rerenders
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Use layout effect for DOM manipulations
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
if (!needScrollRestore.current) return;
|
||||||
|
|
||||||
|
const container = scrollContainerRef.current;
|
||||||
|
if (container && (lastScrollPosition.current.left > 0 || lastScrollPosition.current.top > 0)) {
|
||||||
|
container.scrollLeft = lastScrollPosition.current.left;
|
||||||
|
container.scrollTop = lastScrollPosition.current.top;
|
||||||
|
needScrollRestore.current = false;
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Ensure manual edits to item numbers persist with minimal changes to validation logic
|
||||||
const handleUpdateRow = useCallback(async (rowIndex: number, key: T, value: any) => {
|
const handleUpdateRow = useCallback(async (rowIndex: number, key: T, value: any) => {
|
||||||
// Process value before updating data
|
// Process value before updating data
|
||||||
console.log(`enhancedUpdateRow called: rowIndex=${rowIndex}, fieldKey=${key}, value=`, value);
|
|
||||||
let processedValue = value;
|
let processedValue = value;
|
||||||
|
|
||||||
// Strip dollar signs from price fields
|
// Strip dollar signs from price fields
|
||||||
@@ -347,158 +556,272 @@ const ValidationContainer = <T extends string>({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save current scroll position
|
// Find the row in the data
|
||||||
const scrollPosition = {
|
|
||||||
left: window.scrollX,
|
|
||||||
top: window.scrollY
|
|
||||||
};
|
|
||||||
|
|
||||||
// Find the original index in the data array
|
|
||||||
const rowData = filteredData[rowIndex];
|
const rowData = filteredData[rowIndex];
|
||||||
const originalIndex = data.findIndex(item => item.__index === rowData?.__index);
|
if (!rowData) {
|
||||||
|
console.error(`No row data found for index ${rowIndex}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (originalIndex === -1) {
|
// Use __index to find the actual row in the full data array
|
||||||
// If we can't find the original row, just do a simple update
|
const rowId = rowData.__index;
|
||||||
updateRow(rowIndex, key, processedValue);
|
const originalIndex = data.findIndex(item => item.__index === rowId);
|
||||||
} else {
|
|
||||||
// Update the data directly
|
// Detect if this is a direct item_number edit
|
||||||
|
const isItemNumberEdit = key === 'item_number' as T;
|
||||||
|
|
||||||
|
// For item_number edits, we need special handling to ensure they persist
|
||||||
|
if (isItemNumberEdit) {
|
||||||
|
console.log(`Manual edit to item_number: ${value}`);
|
||||||
|
|
||||||
|
// First, update data immediately to ensure edit takes effect
|
||||||
setData(prevData => {
|
setData(prevData => {
|
||||||
const newData = [...prevData];
|
const newData = [...prevData];
|
||||||
const updatedRow = {
|
if (originalIndex >= 0 && originalIndex < newData.length) {
|
||||||
|
newData[originalIndex] = {
|
||||||
|
...newData[originalIndex],
|
||||||
|
[key]: processedValue
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return newData;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Mark for revalidation after a delay to ensure data update completes first
|
||||||
|
setTimeout(() => {
|
||||||
|
markRowForRevalidation(rowIndex, key as string);
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
// Return early to prevent double-updating
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For all other fields, use standard approach
|
||||||
|
// Always use setData for updating - immediate update for better UX
|
||||||
|
const updatedRow = { ...rowData, [key]: processedValue };
|
||||||
|
|
||||||
|
// Mark this row for revalidation to clear any existing errors
|
||||||
|
markRowForRevalidation(rowIndex, key as string);
|
||||||
|
|
||||||
|
// Update the data immediately to show the change
|
||||||
|
setData(prevData => {
|
||||||
|
const newData = [...prevData];
|
||||||
|
if (originalIndex >= 0 && originalIndex < newData.length) {
|
||||||
|
// Create a new row object with the updated field
|
||||||
|
newData[originalIndex] = {
|
||||||
...newData[originalIndex],
|
...newData[originalIndex],
|
||||||
[key]: processedValue
|
[key]: processedValue
|
||||||
};
|
};
|
||||||
|
}
|
||||||
newData[originalIndex] = updatedRow;
|
return newData;
|
||||||
return newData;
|
});
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Restore scroll position after update
|
// Secondary effects - using a timeout to ensure UI updates first
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
window.scrollTo(scrollPosition.left, scrollPosition.top);
|
// Handle company change - clear line/subline and fetch product lines
|
||||||
}, 0);
|
if (key === 'company' && value) {
|
||||||
|
console.log(`Company changed to ${value} for row ${rowIndex}, updating lines and sublines`);
|
||||||
// Now handle any additional logic for specific fields
|
|
||||||
if (key === 'company' && value) {
|
|
||||||
// Clear any existing line/subline values for this row if company changes
|
|
||||||
if (originalIndex !== -1) {
|
|
||||||
setData(prevData => {
|
|
||||||
const newData = [...prevData];
|
|
||||||
newData[originalIndex] = {
|
|
||||||
...newData[originalIndex],
|
|
||||||
line: undefined,
|
|
||||||
subline: undefined
|
|
||||||
};
|
|
||||||
return newData;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use cached product lines if available, otherwise fetch
|
|
||||||
if (rowData && rowData.__index) {
|
|
||||||
const companyId = value.toString();
|
|
||||||
if (rowProductLines[companyId]) {
|
|
||||||
// Use cached data
|
|
||||||
console.log(`Using cached product lines for company ${companyId}`);
|
|
||||||
} else {
|
|
||||||
// Fetch product lines for the new company
|
|
||||||
if (value !== undefined) {
|
|
||||||
await fetchProductLines(rowData.__index as string, companyId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If updating supplier field AND there's a UPC value, validate UPC
|
|
||||||
if (key === 'supplier' && value && rowData) {
|
|
||||||
const rowDataAny = rowData as Record<string, any>;
|
|
||||||
if (rowDataAny.upc || rowDataAny.barcode) {
|
|
||||||
const upcValue = rowDataAny.upc || rowDataAny.barcode;
|
|
||||||
|
|
||||||
try {
|
// Clear any existing line/subline values immediately
|
||||||
// Mark the item_number cell as being validated
|
|
||||||
setValidatingCells(prev => {
|
|
||||||
const newSet = new Set(prev);
|
|
||||||
newSet.add(`${rowIndex}-item_number`);
|
|
||||||
return newSet;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Use supplier ID (the value being set) to validate UPC
|
|
||||||
await upcValidation.validateUpc(rowIndex, value.toString(), upcValue.toString());
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error validating UPC:', error);
|
|
||||||
} finally {
|
|
||||||
// Clear validation state for the item_number cell
|
|
||||||
setValidatingCells(prev => {
|
|
||||||
const newSet = new Set(prev);
|
|
||||||
newSet.delete(`${rowIndex}-item_number`);
|
|
||||||
return newSet;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If updating line field, fetch sublines
|
|
||||||
if (key === 'line' && value) {
|
|
||||||
// Clear any existing subline value for this row
|
|
||||||
if (originalIndex !== -1) {
|
|
||||||
setData(prevData => {
|
setData(prevData => {
|
||||||
const newData = [...prevData];
|
const newData = [...prevData];
|
||||||
newData[originalIndex] = {
|
const idx = newData.findIndex(item => item.__index === rowId);
|
||||||
...newData[originalIndex],
|
if (idx >= 0) {
|
||||||
subline: undefined
|
console.log(`Clearing line and subline values for row with ID ${rowId}`);
|
||||||
};
|
newData[idx] = {
|
||||||
|
...newData[idx],
|
||||||
|
line: undefined,
|
||||||
|
subline: undefined
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
console.warn(`Could not find row with ID ${rowId} to clear line/subline values`);
|
||||||
|
}
|
||||||
return newData;
|
return newData;
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
// Fetch product lines for the new company
|
||||||
// Use cached sublines if available, otherwise fetch
|
if (rowId && value !== undefined) {
|
||||||
if (rowData && rowData.__index) {
|
const companyId = value.toString();
|
||||||
const lineId = value.toString();
|
|
||||||
if (rowSublines[lineId]) {
|
// Force immediate fetch for better UX
|
||||||
// Use cached data
|
console.log(`Immediately fetching product lines for company ${companyId} for row ${rowId}`);
|
||||||
console.log(`Using cached sublines for line ${lineId}`);
|
|
||||||
} else {
|
// Set loading state first
|
||||||
// Fetch sublines for the new line
|
|
||||||
if (value !== undefined) {
|
|
||||||
await fetchSublines(rowData.__index as string, lineId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If updating UPC/barcode field AND there's a supplier value, validate UPC
|
|
||||||
if ((key === 'upc' || key === 'barcode') && value && rowData) {
|
|
||||||
const rowDataAny = rowData as Record<string, any>;
|
|
||||||
if (rowDataAny.supplier) {
|
|
||||||
try {
|
|
||||||
// Mark the item_number cell as being validated
|
|
||||||
setValidatingCells(prev => {
|
setValidatingCells(prev => {
|
||||||
const newSet = new Set(prev);
|
const newSet = new Set(prev);
|
||||||
newSet.add(`${rowIndex}-item_number`);
|
newSet.add(`${rowIndex}-line`);
|
||||||
return newSet;
|
return newSet;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Use supplier ID from the row data to validate UPC
|
fetchProductLines(rowId, companyId)
|
||||||
await upcValidation.validateUpc(rowIndex, rowDataAny.supplier.toString(), value.toString());
|
.then(lines => {
|
||||||
} catch (error) {
|
console.log(`Successfully loaded ${lines.length} product lines for company ${companyId}`);
|
||||||
console.error('Error validating UPC:', error);
|
})
|
||||||
} finally {
|
.catch(err => {
|
||||||
// Clear validation state for the item_number cell
|
console.error(`Error fetching product lines for company ${companyId}:`, err);
|
||||||
setValidatingCells(prev => {
|
toast.error("Failed to load product lines");
|
||||||
const newSet = new Set(prev);
|
})
|
||||||
newSet.delete(`${rowIndex}-item_number`);
|
.finally(() => {
|
||||||
return newSet;
|
// Clear loading indicator
|
||||||
});
|
setValidatingCells(prev => {
|
||||||
|
const newSet = new Set(prev);
|
||||||
|
newSet.delete(`${rowIndex}-line`);
|
||||||
|
return newSet;
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}, [data, filteredData, updateRow, fetchProductLines, fetchSublines, setData, rowProductLines, rowSublines, upcValidation]);
|
// Handle supplier + UPC validation - using the most recent values
|
||||||
|
if (key === 'supplier' && value) {
|
||||||
|
// Get the latest UPC value from the updated row
|
||||||
|
const upcValue = updatedRow.upc || updatedRow.barcode;
|
||||||
|
|
||||||
|
if (upcValue) {
|
||||||
|
console.log(`Validating UPC: rowIndex=${rowIndex}, supplier=${value}, upc=${upcValue}`);
|
||||||
|
|
||||||
|
// Mark the item_number cell as being validated
|
||||||
|
const cellKey = `${rowIndex}-item_number`;
|
||||||
|
setValidatingCells(prev => {
|
||||||
|
const newSet = new Set(prev);
|
||||||
|
newSet.add(cellKey);
|
||||||
|
return newSet;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Use a regular promise-based approach instead of await
|
||||||
|
upcValidation.validateUpc(rowIndex, value.toString(), upcValue.toString())
|
||||||
|
.then(result => {
|
||||||
|
if (result.success) {
|
||||||
|
console.log(`UPC validation successful for row ${rowIndex}`);
|
||||||
|
upcValidation.applyItemNumbersToData();
|
||||||
|
|
||||||
|
// Mark for revalidation after item numbers are updated
|
||||||
|
setTimeout(() => {
|
||||||
|
markRowForRevalidation(rowIndex, 'item_number');
|
||||||
|
}, 50);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.error("Error validating UPC:", err);
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
// Clear validation state for the item_number cell
|
||||||
|
setValidatingCells(prev => {
|
||||||
|
const newSet = new Set(prev);
|
||||||
|
newSet.delete(cellKey);
|
||||||
|
return newSet;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle line change - clear subline and fetch sublines
|
||||||
|
if (key === 'line' && value) {
|
||||||
|
console.log(`Line changed to ${value} for row ${rowIndex}, updating sublines`);
|
||||||
|
|
||||||
|
// Clear any existing subline value
|
||||||
|
setData(prevData => {
|
||||||
|
const newData = [...prevData];
|
||||||
|
const idx = newData.findIndex(item => item.__index === rowId);
|
||||||
|
if (idx >= 0) {
|
||||||
|
console.log(`Clearing subline values for row with ID ${rowId}`);
|
||||||
|
newData[idx] = {
|
||||||
|
...newData[idx],
|
||||||
|
subline: undefined
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
console.warn(`Could not find row with ID ${rowId} to clear subline values`);
|
||||||
|
}
|
||||||
|
return newData;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Fetch sublines for the new line
|
||||||
|
if (rowId && value !== undefined) {
|
||||||
|
const lineId = value.toString();
|
||||||
|
|
||||||
|
// Force immediate fetch for better UX
|
||||||
|
console.log(`Immediately fetching sublines for line ${lineId} for row ${rowId}`);
|
||||||
|
|
||||||
|
// Set loading state first
|
||||||
|
setValidatingCells(prev => {
|
||||||
|
const newSet = new Set(prev);
|
||||||
|
newSet.add(`${rowIndex}-subline`);
|
||||||
|
return newSet;
|
||||||
|
});
|
||||||
|
|
||||||
|
fetchSublines(rowId, lineId)
|
||||||
|
.then(sublines => {
|
||||||
|
console.log(`Successfully loaded ${sublines.length} sublines for line ${lineId}`);
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.error(`Error fetching sublines for line ${lineId}:`, err);
|
||||||
|
toast.error("Failed to load sublines");
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
// Clear loading indicator
|
||||||
|
setValidatingCells(prev => {
|
||||||
|
const newSet = new Set(prev);
|
||||||
|
newSet.delete(`${rowIndex}-subline`);
|
||||||
|
return newSet;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the UPC/barcode validation handler back:
|
||||||
|
// Handle UPC/barcode + supplier validation
|
||||||
|
if ((key === 'upc' || key === 'barcode') && value) {
|
||||||
|
// Get latest supplier from the updated row
|
||||||
|
const supplier = updatedRow.supplier;
|
||||||
|
|
||||||
|
if (supplier) {
|
||||||
|
console.log(`Validating UPC from UPC change: rowIndex=${rowIndex}, supplier=${supplier}, upc=${value}`);
|
||||||
|
|
||||||
|
// Mark the item_number cell as being validated
|
||||||
|
const cellKey = `${rowIndex}-item_number`;
|
||||||
|
setValidatingCells(prev => {
|
||||||
|
const newSet = new Set(prev);
|
||||||
|
newSet.add(cellKey);
|
||||||
|
return newSet;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Use a regular promise-based approach
|
||||||
|
upcValidation.validateUpc(rowIndex, supplier.toString(), value.toString())
|
||||||
|
.then(result => {
|
||||||
|
if (result.success) {
|
||||||
|
console.log(`UPC validation successful for row ${rowIndex}`);
|
||||||
|
upcValidation.applyItemNumbersToData();
|
||||||
|
|
||||||
|
// Mark for revalidation after item numbers are updated
|
||||||
|
setTimeout(() => {
|
||||||
|
markRowForRevalidation(rowIndex, 'item_number');
|
||||||
|
}, 50);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.error("Error validating UPC:", err);
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
// Clear validation state for the item_number cell
|
||||||
|
setValidatingCells(prev => {
|
||||||
|
const newSet = new Set(prev);
|
||||||
|
newSet.delete(cellKey);
|
||||||
|
return newSet;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 0); // Using 0ms timeout to defer execution until after the UI update
|
||||||
|
}, [data, filteredData, setData, fetchProductLines, fetchSublines, upcValidation, markRowForRevalidation]);
|
||||||
|
|
||||||
// Create a separate copyDown function that uses handleUpdateRow
|
// Fix the missing loading indicator clear code
|
||||||
const handleCopyDown = useCallback((rowIndex: number, fieldKey: string, endRowIndex?: number) => {
|
const handleCopyDown = useCallback((rowIndex: number, fieldKey: string, endRowIndex?: number) => {
|
||||||
// Get the value to copy from the source row
|
// Get the value to copy from the source row
|
||||||
const sourceRow = data[rowIndex];
|
const sourceRow = data[rowIndex];
|
||||||
|
if (!sourceRow) {
|
||||||
|
console.error(`Source row ${rowIndex} not found for copyDown`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const valueToCopy = sourceRow[fieldKey];
|
const valueToCopy = sourceRow[fieldKey];
|
||||||
|
|
||||||
// Create a proper copy of the value to avoid reference issues, especially for arrays (MultiSelectCell)
|
// Create a proper copy of the value to avoid reference issues, especially for arrays (MultiSelectCell)
|
||||||
@@ -506,50 +829,183 @@ const ValidationContainer = <T extends string>({
|
|||||||
|
|
||||||
// Get all rows below the source row, up to endRowIndex if specified
|
// Get all rows below the source row, up to endRowIndex if specified
|
||||||
const lastRowIndex = endRowIndex !== undefined ? Math.min(endRowIndex, data.length - 1) : data.length - 1;
|
const lastRowIndex = endRowIndex !== undefined ? Math.min(endRowIndex, data.length - 1) : data.length - 1;
|
||||||
const rowsToUpdate = data.slice(rowIndex + 1, lastRowIndex + 1);
|
const rowsToUpdate = Array.from({ length: lastRowIndex - rowIndex }, (_, i) => rowIndex + i + 1);
|
||||||
|
|
||||||
// Create a set of cells that will be in loading state
|
// Mark all cells as updating at once
|
||||||
const loadingCells = new Set<string>();
|
const updatingCells = new Set<string>();
|
||||||
|
rowsToUpdate.forEach(targetRowIndex => {
|
||||||
// Add all target cells to the loading state
|
updatingCells.add(`${targetRowIndex}-${fieldKey}`);
|
||||||
rowsToUpdate.forEach((_, index) => {
|
|
||||||
const targetRowIndex = rowIndex + 1 + index;
|
|
||||||
loadingCells.add(`${targetRowIndex}-${fieldKey}`);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Update validatingCells to show loading state
|
|
||||||
setValidatingCells(prev => {
|
setValidatingCells(prev => {
|
||||||
const newSet = new Set(prev);
|
const newSet = new Set(prev);
|
||||||
loadingCells.forEach(cell => newSet.add(cell));
|
updatingCells.forEach(cell => newSet.add(cell));
|
||||||
return newSet;
|
return newSet;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Update all rows immediately
|
// Update all rows at once efficiently with a single state update
|
||||||
rowsToUpdate.forEach((_, i) => {
|
setData(prevData => {
|
||||||
const targetRowIndex = rowIndex + 1 + i;
|
// Create a new copy of the data
|
||||||
|
const newData = [...prevData];
|
||||||
|
|
||||||
// Update the row with the copied value
|
// Update all rows at once
|
||||||
handleUpdateRow(targetRowIndex, fieldKey as T, valueCopy);
|
rowsToUpdate.forEach(targetRowIndex => {
|
||||||
|
// Find the original row using __index
|
||||||
|
const rowData = filteredData[targetRowIndex];
|
||||||
|
if (!rowData) return;
|
||||||
|
|
||||||
|
const rowId = rowData.__index;
|
||||||
|
const originalIndex = newData.findIndex(item => item.__index === rowId);
|
||||||
|
|
||||||
|
if (originalIndex !== -1) {
|
||||||
|
// Update the specific field on this row
|
||||||
|
newData[originalIndex] = {
|
||||||
|
...newData[originalIndex],
|
||||||
|
[fieldKey]: valueCopy
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
// Fall back to direct index if __index not found
|
||||||
|
if (targetRowIndex < newData.length) {
|
||||||
|
newData[targetRowIndex] = {
|
||||||
|
...newData[targetRowIndex],
|
||||||
|
[fieldKey]: valueCopy
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Remove loading state
|
return newData;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Mark rows for revalidation
|
||||||
|
rowsToUpdate.forEach(targetRowIndex => {
|
||||||
|
markRowForRevalidation(targetRowIndex, fieldKey);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Clear the loading state for all cells after a short delay
|
||||||
|
setTimeout(() => {
|
||||||
setValidatingCells(prev => {
|
setValidatingCells(prev => {
|
||||||
|
if (prev.size === 0) return prev;
|
||||||
const newSet = new Set(prev);
|
const newSet = new Set(prev);
|
||||||
newSet.delete(`${targetRowIndex}-${fieldKey}`);
|
updatingCells.forEach(cell => newSet.delete(cell));
|
||||||
return newSet;
|
return newSet;
|
||||||
});
|
});
|
||||||
});
|
}, 100);
|
||||||
}, [data, handleUpdateRow, setValidatingCells]);
|
|
||||||
|
|
||||||
// Use UPC validation when data changes
|
|
||||||
useEffect(() => {
|
|
||||||
// Skip if there's no data or already validated
|
|
||||||
if (data.length === 0 || upcValidation.initialValidationDone) return;
|
|
||||||
|
|
||||||
// Run validation immediately without timeout
|
// If copying UPC or supplier fields, validate UPC for all rows
|
||||||
upcValidation.validateAllUPCs();
|
if (fieldKey === 'upc' || fieldKey === 'barcode' || fieldKey === 'supplier') {
|
||||||
|
// Process each row in parallel
|
||||||
// No cleanup needed since we're not using a timer
|
const validationsToRun: {rowIndex: number, supplier: string, upc: string}[] = [];
|
||||||
}, [data, upcValidation]);
|
|
||||||
|
// Process each row separately to collect validation tasks
|
||||||
|
rowsToUpdate.forEach(targetRowIndex => {
|
||||||
|
const rowData = filteredData[targetRowIndex];
|
||||||
|
if (!rowData) return;
|
||||||
|
|
||||||
|
// Only validate if both UPC and supplier are present after the update
|
||||||
|
const updatedRow = {
|
||||||
|
...rowData,
|
||||||
|
[fieldKey]: valueCopy
|
||||||
|
};
|
||||||
|
|
||||||
|
const hasUpc = updatedRow.upc || updatedRow.barcode;
|
||||||
|
const hasSupplier = updatedRow.supplier;
|
||||||
|
|
||||||
|
if (hasUpc && hasSupplier) {
|
||||||
|
const upcValue = updatedRow.upc || updatedRow.barcode;
|
||||||
|
const supplierId = updatedRow.supplier;
|
||||||
|
|
||||||
|
// Queue this validation if both values are defined
|
||||||
|
if (supplierId !== undefined && upcValue !== undefined) {
|
||||||
|
validationsToRun.push({
|
||||||
|
rowIndex: targetRowIndex,
|
||||||
|
supplier: supplierId.toString(),
|
||||||
|
upc: upcValue.toString()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Run validations in parallel but limit the batch size
|
||||||
|
if (validationsToRun.length > 0) {
|
||||||
|
console.log(`Running ${validationsToRun.length} UPC validations for copyDown`);
|
||||||
|
|
||||||
|
// Mark all cells as validating
|
||||||
|
validationsToRun.forEach(({ rowIndex }) => {
|
||||||
|
const cellKey = `${rowIndex}-item_number`;
|
||||||
|
setValidatingCells(prev => {
|
||||||
|
const newSet = new Set(prev);
|
||||||
|
newSet.add(cellKey);
|
||||||
|
return newSet;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Process in smaller batches to avoid overwhelming the system
|
||||||
|
const BATCH_SIZE = 5; // Process 5 validations at a time
|
||||||
|
const processBatch = (startIdx: number) => {
|
||||||
|
const endIdx = Math.min(startIdx + BATCH_SIZE, validationsToRun.length);
|
||||||
|
const batch = validationsToRun.slice(startIdx, endIdx);
|
||||||
|
|
||||||
|
Promise.all(
|
||||||
|
batch.map(({ rowIndex, supplier, upc }) =>
|
||||||
|
upcValidation.validateUpc(rowIndex, supplier, upc)
|
||||||
|
.then(result => {
|
||||||
|
if (result.success) {
|
||||||
|
// Apply immediately for better UX
|
||||||
|
if (startIdx + BATCH_SIZE >= validationsToRun.length) {
|
||||||
|
// Apply all updates at the end with callback to mark for revalidation
|
||||||
|
upcValidation.applyItemNumbersToData(updatedRowIds => {
|
||||||
|
// Mark these rows for revalidation after a delay
|
||||||
|
setTimeout(() => {
|
||||||
|
updatedRowIds.forEach(rowIdx => {
|
||||||
|
markRowForRevalidation(rowIdx, 'item_number');
|
||||||
|
});
|
||||||
|
}, 100);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { rowIndex, success: result.success };
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.error(`Error validating UPC for row ${rowIndex}:`, err);
|
||||||
|
return { rowIndex, success: false };
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
// Clear validation state for this cell
|
||||||
|
const cellKey = `${rowIndex}-item_number`;
|
||||||
|
setValidatingCells(prev => {
|
||||||
|
if (!prev.has(cellKey)) return prev;
|
||||||
|
const newSet = new Set(prev);
|
||||||
|
newSet.delete(cellKey);
|
||||||
|
return newSet;
|
||||||
|
});
|
||||||
|
})
|
||||||
|
)
|
||||||
|
).then(() => {
|
||||||
|
// If there are more validations to run, process the next batch
|
||||||
|
if (endIdx < validationsToRun.length) {
|
||||||
|
// Add a small delay between batches to prevent UI freezing
|
||||||
|
setTimeout(() => processBatch(endIdx), 100);
|
||||||
|
} else {
|
||||||
|
console.log(`Completed all ${validationsToRun.length} UPC validations`);
|
||||||
|
// Final application of all item numbers if not done by individual batches
|
||||||
|
upcValidation.applyItemNumbersToData(updatedRowIds => {
|
||||||
|
// Mark these rows for revalidation after a delay
|
||||||
|
setTimeout(() => {
|
||||||
|
updatedRowIds.forEach(rowIdx => {
|
||||||
|
markRowForRevalidation(rowIdx, 'item_number');
|
||||||
|
});
|
||||||
|
}, 100);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Start processing the first batch
|
||||||
|
processBatch(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [data, filteredData, setData, setValidatingCells, upcValidation, markRowForRevalidation]);
|
||||||
|
|
||||||
// Memoize the rendered validation table
|
// Memoize the rendered validation table
|
||||||
const renderValidationTable = useMemo(() => {
|
const renderValidationTable = useMemo(() => {
|
||||||
@@ -611,74 +1067,6 @@ const ValidationContainer = <T extends string>({
|
|||||||
isLoadingSublines
|
isLoadingSublines
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Add scroll container ref at the container level
|
|
||||||
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
|
||||||
const lastScrollPosition = useRef({ left: 0, top: 0 });
|
|
||||||
const isScrolling = useRef(false);
|
|
||||||
|
|
||||||
// Memoize scroll handlers
|
|
||||||
const handleScroll = useCallback((event: React.UIEvent<HTMLDivElement> | Event) => {
|
|
||||||
if (!isScrolling.current) {
|
|
||||||
isScrolling.current = true;
|
|
||||||
// Use type assertion to handle both React.UIEvent and native Event
|
|
||||||
const target = event.currentTarget as HTMLDivElement;
|
|
||||||
lastScrollPosition.current = {
|
|
||||||
left: target.scrollLeft,
|
|
||||||
top: target.scrollTop
|
|
||||||
};
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
isScrolling.current = false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Add scroll event listener
|
|
||||||
useEffect(() => {
|
|
||||||
const container = scrollContainerRef.current;
|
|
||||||
if (container) {
|
|
||||||
// Convert React event handler to native event handler
|
|
||||||
const nativeHandler = ((evt: Event) => {
|
|
||||||
handleScroll(evt);
|
|
||||||
}) as EventListener;
|
|
||||||
|
|
||||||
container.addEventListener('scroll', nativeHandler, { passive: true });
|
|
||||||
return () => container.removeEventListener('scroll', nativeHandler);
|
|
||||||
}
|
|
||||||
}, [handleScroll]);
|
|
||||||
|
|
||||||
// Restore scroll position after data updates
|
|
||||||
useLayoutEffect(() => {
|
|
||||||
const container = scrollContainerRef.current;
|
|
||||||
if (container) {
|
|
||||||
const { left, top } = lastScrollPosition.current;
|
|
||||||
if (left > 0 || top > 0) {
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
if (container) {
|
|
||||||
container.scrollTo({
|
|
||||||
left,
|
|
||||||
top,
|
|
||||||
behavior: 'auto'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [filteredData]);
|
|
||||||
|
|
||||||
// Add cleanup effect to reset scroll position when component unmounts
|
|
||||||
useEffect(() => {
|
|
||||||
return () => {
|
|
||||||
// Reset the last scroll position reference
|
|
||||||
lastScrollPosition.current = { left: 0, top: 0 };
|
|
||||||
|
|
||||||
// Reset the scroll container if it exists
|
|
||||||
if (scrollContainerRef.current) {
|
|
||||||
scrollContainerRef.current.scrollTop = 0;
|
|
||||||
scrollContainerRef.current.scrollLeft = 0;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-[calc(100vh-10rem)] overflow-hidden">
|
<div className="flex flex-col h-[calc(100vh-10rem)] overflow-hidden">
|
||||||
<div className="flex-1 overflow-hidden">
|
<div className="flex-1 overflow-hidden">
|
||||||
|
|||||||
@@ -189,10 +189,6 @@ const ValidationTable = <T extends string>({
|
|||||||
}: ValidationTableProps<T>) => {
|
}: ValidationTableProps<T>) => {
|
||||||
const { translations } = useRsi<T>();
|
const { translations } = useRsi<T>();
|
||||||
|
|
||||||
// Debug logs
|
|
||||||
console.log('ValidationTable rowProductLines:', rowProductLines);
|
|
||||||
console.log('ValidationTable rowSublines:', rowSublines);
|
|
||||||
|
|
||||||
// Add state for copy down selection mode
|
// Add state for copy down selection mode
|
||||||
const [isInCopyDownMode, setIsInCopyDownMode] = useState(false);
|
const [isInCopyDownMode, setIsInCopyDownMode] = useState(false);
|
||||||
const [sourceRowIndex, setSourceRowIndex] = useState<number | null>(null);
|
const [sourceRowIndex, setSourceRowIndex] = useState<number | null>(null);
|
||||||
@@ -308,10 +304,13 @@ const ValidationTable = <T extends string>({
|
|||||||
const cache = new Map<string, readonly any[]>();
|
const cache = new Map<string, readonly any[]>();
|
||||||
|
|
||||||
fields.forEach((field) => {
|
fields.forEach((field) => {
|
||||||
// Don't skip disabled fields
|
// Get the field key
|
||||||
|
const fieldKey = String(field.key);
|
||||||
|
|
||||||
if (field.fieldType.type === 'select' || field.fieldType.type === 'multi-select') {
|
// Handle all select and multi-select fields the same way
|
||||||
const fieldKey = String(field.key);
|
if (field.fieldType &&
|
||||||
|
(typeof field.fieldType === 'object') &&
|
||||||
|
(field.fieldType.type === 'select' || field.fieldType.type === 'multi-select')) {
|
||||||
cache.set(fieldKey, (field.fieldType as any).options || []);
|
cache.set(fieldKey, (field.fieldType as any).options || []);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -357,30 +356,35 @@ const ValidationTable = <T extends string>({
|
|||||||
|
|
||||||
if (fieldKey === 'line' && rowId && rowProductLines[rowId]) {
|
if (fieldKey === 'line' && rowId && rowProductLines[rowId]) {
|
||||||
options = rowProductLines[rowId];
|
options = rowProductLines[rowId];
|
||||||
console.log(`Setting line options for row ${rowId}:`, options);
|
|
||||||
} else if (fieldKey === 'subline' && rowId && rowSublines[rowId]) {
|
} else if (fieldKey === 'subline' && rowId && rowSublines[rowId]) {
|
||||||
options = rowSublines[rowId];
|
options = rowSublines[rowId];
|
||||||
console.log(`Setting subline options for row ${rowId}:`, options);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine if this cell is in loading state
|
// Determine if this cell is in loading state - use a clear consistent approach
|
||||||
let isLoading = validatingCells.has(`${row.index}-${field.key}`);
|
let isLoading = false;
|
||||||
|
|
||||||
|
// Check the validatingCells Set first (for item_number and other fields)
|
||||||
|
const cellLoadingKey = `${row.index}-${fieldKey}`;
|
||||||
|
if (validatingCells.has(cellLoadingKey)) {
|
||||||
|
isLoading = true;
|
||||||
|
}
|
||||||
// Add loading state for line/subline fields
|
// Add loading state for line/subline fields
|
||||||
if (fieldKey === 'line' && rowId && isLoadingLines[rowId]) {
|
else if (fieldKey === 'line' && rowId && isLoadingLines[rowId]) {
|
||||||
isLoading = true;
|
isLoading = true;
|
||||||
console.log(`Line field for row ${rowId} is loading`);
|
}
|
||||||
} else if (fieldKey === 'subline' && rowId && isLoadingSublines[rowId]) {
|
else if (fieldKey === 'subline' && rowId && isLoadingSublines[rowId]) {
|
||||||
isLoading = true;
|
isLoading = true;
|
||||||
console.log(`Subline field for row ${rowId} is loading`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get validation errors for this cell
|
||||||
|
const cellErrors = validationErrors.get(row.index)?.[fieldKey] || [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MemoizedCell
|
<MemoizedCell
|
||||||
field={field as Field<string>}
|
field={field as Field<string>}
|
||||||
value={row.original[field.key as keyof typeof row.original]}
|
value={row.original[field.key as keyof typeof row.original]}
|
||||||
onChange={(value) => handleFieldUpdate(row.index, field.key as T, value)}
|
onChange={(value) => handleFieldUpdate(row.index, field.key as T, value)}
|
||||||
errors={validationErrors.get(row.index)?.[fieldKey] || []}
|
errors={cellErrors}
|
||||||
isValidating={isLoading}
|
isValidating={isLoading}
|
||||||
fieldKey={fieldKey}
|
fieldKey={fieldKey}
|
||||||
options={options}
|
options={options}
|
||||||
@@ -494,10 +498,13 @@ const ValidationTable = <T extends string>({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
{/* Custom Table Header - Always Visible */}
|
{/* Custom Table Header - Always Visible with GPU acceleration */}
|
||||||
<div
|
<div
|
||||||
className={`sticky top-0 z-20 bg-muted border-b shadow-sm`}
|
className="sticky top-0 z-20 bg-muted border-b shadow-sm will-change-transform"
|
||||||
style={{ width: `${totalWidth}px` }}
|
style={{
|
||||||
|
width: `${totalWidth}px`,
|
||||||
|
transform: 'translateZ(0)', // Force GPU acceleration
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
{table.getFlatHeaders().map((header) => {
|
{table.getFlatHeaders().map((header) => {
|
||||||
@@ -521,49 +528,57 @@ const ValidationTable = <T extends string>({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Table Body - Restore the original structure */}
|
{/* Table Body - With optimized rendering */}
|
||||||
<Table style={{ width: `${totalWidth}px`, tableLayout: 'fixed', borderCollapse: 'separate', borderSpacing: 0, marginTop: '-1px' }}>
|
<Table style={{
|
||||||
|
width: `${totalWidth}px`,
|
||||||
|
tableLayout: 'fixed',
|
||||||
|
borderCollapse: 'separate',
|
||||||
|
borderSpacing: 0,
|
||||||
|
marginTop: '-1px',
|
||||||
|
willChange: 'transform', // Help browser optimize
|
||||||
|
contain: 'content', // Contain paint operations
|
||||||
|
transform: 'translateZ(0)' // Force GPU acceleration
|
||||||
|
}}>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{table.getRowModel().rows.map((row) => (
|
{table.getRowModel().rows.map((row) => {
|
||||||
<TableRow
|
// Precompute validation error status for this row
|
||||||
key={row.id}
|
const hasErrors = validationErrors.has(parseInt(row.id)) &&
|
||||||
className={cn(
|
Object.keys(validationErrors.get(parseInt(row.id)) || {}).length > 0;
|
||||||
"hover:bg-muted/50",
|
|
||||||
row.getIsSelected() ? "bg-muted/50" : "",
|
// Precompute copy down target status
|
||||||
validationErrors.get(parseInt(row.id)) &&
|
const isCopyDownTarget = isInCopyDownMode &&
|
||||||
Object.keys(validationErrors.get(parseInt(row.id)) || {}).length > 0 ? "bg-red-50/40" : "",
|
sourceRowIndex !== null &&
|
||||||
// Add cursor-pointer class when in copy down mode for target rows
|
parseInt(row.id) > sourceRowIndex;
|
||||||
isInCopyDownMode && sourceRowIndex !== null && parseInt(row.id) > sourceRowIndex ? "cursor-pointer copy-down-target-row" : ""
|
|
||||||
)}
|
// Using CSS variables for better performance on hover/state changes
|
||||||
style={{
|
const rowStyle = {
|
||||||
// Force cursor pointer on all target rows
|
cursor: isCopyDownTarget ? 'pointer' : undefined,
|
||||||
cursor: isInCopyDownMode && sourceRowIndex !== null && parseInt(row.id) > sourceRowIndex ? 'pointer' : undefined,
|
position: 'relative' as const,
|
||||||
position: 'relative' // Ensure we can position the overlay
|
willChange: isInCopyDownMode ? 'background-color' : 'auto',
|
||||||
}}
|
contain: 'layout',
|
||||||
onMouseEnter={() => handleRowMouseEnter(parseInt(row.id))}
|
transition: 'background-color 100ms ease-in-out'
|
||||||
>
|
};
|
||||||
{row.getVisibleCells().map((cell: any) => {
|
|
||||||
const width = cell.column.getSize();
|
return (
|
||||||
return (
|
<TableRow
|
||||||
<TableCell
|
key={row.id}
|
||||||
key={cell.id}
|
className={cn(
|
||||||
style={{
|
"hover:bg-muted/50",
|
||||||
width: `${width}px`,
|
row.getIsSelected() ? "bg-muted/50" : "",
|
||||||
minWidth: `${width}px`,
|
hasErrors ? "bg-red-50/40" : "",
|
||||||
maxWidth: `${width}px`,
|
isCopyDownTarget ? "cursor-pointer copy-down-target-row" : ""
|
||||||
boxSizing: 'border-box',
|
)}
|
||||||
padding: '0',
|
style={rowStyle}
|
||||||
// Force cursor pointer on all cells in target rows
|
onMouseEnter={() => handleRowMouseEnter(parseInt(row.id))}
|
||||||
cursor: isInCopyDownMode && sourceRowIndex !== null && parseInt(row.id) > sourceRowIndex ? 'pointer' : undefined
|
>
|
||||||
}}
|
{row.getVisibleCells().map((cell: any) => (
|
||||||
className={isInCopyDownMode && sourceRowIndex !== null && parseInt(row.id) > sourceRowIndex ? "target-row-cell" : ""}
|
<React.Fragment key={cell.id}>
|
||||||
>
|
|
||||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||||
</TableCell>
|
</React.Fragment>
|
||||||
);
|
))}
|
||||||
})}
|
</TableRow>
|
||||||
</TableRow>
|
);
|
||||||
))}
|
})}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -107,23 +107,28 @@ const InputCell = <T extends string>({
|
|||||||
|
|
||||||
// Handle blur event - use transition for non-critical updates
|
// Handle blur event - use transition for non-critical updates
|
||||||
const handleBlur = useCallback(() => {
|
const handleBlur = useCallback(() => {
|
||||||
|
// First - lock in the current edit value to prevent it from being lost
|
||||||
|
const finalValue = editValue.trim();
|
||||||
|
|
||||||
|
// Then transition to non-editing state
|
||||||
startTransition(() => {
|
startTransition(() => {
|
||||||
setIsEditing(false);
|
setIsEditing(false);
|
||||||
|
|
||||||
// Format the value for storage (remove formatting like $ for price)
|
// Format the value for storage (remove formatting like $ for price)
|
||||||
let processedValue = deferredEditValue.trim();
|
let processedValue = finalValue;
|
||||||
|
|
||||||
if (isPrice && processedValue) {
|
if (isPrice && processedValue) {
|
||||||
needsProcessingRef.current = true;
|
needsProcessingRef.current = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update local display value immediately
|
// Update local display value immediately to prevent UI flicker
|
||||||
setLocalDisplayValue(processedValue);
|
setLocalDisplayValue(processedValue);
|
||||||
|
|
||||||
|
// Commit the change to parent component
|
||||||
onChange(processedValue);
|
onChange(processedValue);
|
||||||
onEndEdit?.();
|
onEndEdit?.();
|
||||||
});
|
});
|
||||||
}, [deferredEditValue, onChange, onEndEdit, isPrice]);
|
}, [editValue, onChange, onEndEdit, isPrice]);
|
||||||
|
|
||||||
// Handle direct input change - optimized to be synchronous for typing
|
// Handle direct input change - optimized to be synchronous for typing
|
||||||
const handleChange = useCallback((e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
const handleChange = useCallback((e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
||||||
|
|||||||
@@ -167,41 +167,59 @@ const MultiSelectCell = <T extends string>({
|
|||||||
const commandListRef = useRef<HTMLDivElement>(null)
|
const commandListRef = useRef<HTMLDivElement>(null)
|
||||||
// Add state for hover
|
// Add state for hover
|
||||||
const [isHovered, setIsHovered] = useState(false)
|
const [isHovered, setIsHovered] = useState(false)
|
||||||
|
// Add ref to track if we need to sync internal state with external value
|
||||||
|
const shouldSyncWithExternalValue = useRef(true)
|
||||||
|
|
||||||
// Create a memoized Set for fast lookups of selected values
|
// Create a memoized Set for fast lookups of selected values
|
||||||
const selectedValueSet = useMemo(() => new Set(internalValue), [internalValue]);
|
const selectedValueSet = useMemo(() => new Set(internalValue), [internalValue]);
|
||||||
|
|
||||||
// Sync internalValue with external value when component mounts or value changes externally
|
// Sync internalValue with external value when component mounts or value changes externally
|
||||||
|
// Modified to prevent infinite loop by checking if values are different before updating
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!open) {
|
// Only sync if we should (not during internal edits) and if not open
|
||||||
// Ensure value is always an array
|
if (shouldSyncWithExternalValue.current && !open) {
|
||||||
setInternalValue(Array.isArray(value) ? value : [])
|
const externalValue = Array.isArray(value) ? value : [];
|
||||||
|
|
||||||
|
// Only update if values are actually different to prevent infinite loops
|
||||||
|
if (internalValue.length !== externalValue.length ||
|
||||||
|
!internalValue.every(v => externalValue.includes(v)) ||
|
||||||
|
!externalValue.every(v => internalValue.includes(v))) {
|
||||||
|
setInternalValue(externalValue);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, [value, open])
|
}, [value, open, internalValue]);
|
||||||
|
|
||||||
// Handle open state changes with improved responsiveness
|
// Handle open state changes with improved responsiveness
|
||||||
const handleOpenChange = useCallback((newOpen: boolean) => {
|
const handleOpenChange = useCallback((newOpen: boolean) => {
|
||||||
if (open && !newOpen) {
|
if (open && !newOpen) {
|
||||||
|
// Prevent syncing with external value during our internal update
|
||||||
|
shouldSyncWithExternalValue.current = false;
|
||||||
|
|
||||||
// Only update parent state when dropdown closes
|
// Only update parent state when dropdown closes
|
||||||
// Avoid expensive deep comparison if lengths are different
|
// Make a defensive copy to avoid mutations
|
||||||
if (internalValue.length !== value.length ||
|
const valuesToCommit = [...internalValue];
|
||||||
internalValue.some((v, i) => v !== value[i])) {
|
|
||||||
onChange(internalValue);
|
// Immediate UI update
|
||||||
}
|
setOpen(false);
|
||||||
|
|
||||||
|
// Update parent with the value immediately
|
||||||
|
onChange(valuesToCommit);
|
||||||
if (onEndEdit) onEndEdit();
|
if (onEndEdit) onEndEdit();
|
||||||
|
|
||||||
|
// Allow syncing with external value again after a short delay
|
||||||
|
setTimeout(() => {
|
||||||
|
shouldSyncWithExternalValue.current = true;
|
||||||
|
}, 0);
|
||||||
} else if (newOpen && !open) {
|
} else if (newOpen && !open) {
|
||||||
// Sync internal state with external state when opening
|
// When opening the dropdown, sync with external value
|
||||||
setInternalValue(Array.isArray(value) ? value : []);
|
const externalValue = Array.isArray(value) ? value : [];
|
||||||
|
setInternalValue(externalValue);
|
||||||
setSearchQuery(""); // Reset search query on open
|
setSearchQuery(""); // Reset search query on open
|
||||||
|
setOpen(true);
|
||||||
if (onStartEdit) onStartEdit();
|
if (onStartEdit) onStartEdit();
|
||||||
} else if (!newOpen) {
|
} else if (!newOpen) {
|
||||||
// Handle case when dropdown is already closed but handleOpenChange is called
|
// Handle case when dropdown is already closed but handleOpenChange is called
|
||||||
// This ensures values are saved when clicking the chevron to close
|
setOpen(false);
|
||||||
if (internalValue.length !== value.length ||
|
|
||||||
internalValue.some((v, i) => v !== value[i])) {
|
|
||||||
onChange(internalValue);
|
|
||||||
}
|
|
||||||
if (onEndEdit) onEndEdit();
|
|
||||||
}
|
}
|
||||||
}, [open, internalValue, value, onChange, onStartEdit, onEndEdit]);
|
}, [open, internalValue, value, onChange, onStartEdit, onEndEdit]);
|
||||||
|
|
||||||
@@ -302,13 +320,25 @@ const MultiSelectCell = <T extends string>({
|
|||||||
|
|
||||||
// Update the handleSelect to operate on internalValue instead of directly calling onChange
|
// Update the handleSelect to operate on internalValue instead of directly calling onChange
|
||||||
const handleSelect = useCallback((selectedValue: string) => {
|
const handleSelect = useCallback((selectedValue: string) => {
|
||||||
|
// Prevent syncing with external value during our internal update
|
||||||
|
shouldSyncWithExternalValue.current = false;
|
||||||
|
|
||||||
setInternalValue(prev => {
|
setInternalValue(prev => {
|
||||||
|
let newValue;
|
||||||
if (prev.includes(selectedValue)) {
|
if (prev.includes(selectedValue)) {
|
||||||
return prev.filter(v => v !== selectedValue);
|
// Remove the value
|
||||||
|
newValue = prev.filter(v => v !== selectedValue);
|
||||||
} else {
|
} else {
|
||||||
return [...prev, selectedValue];
|
// Add the value - make a new array to avoid mutations
|
||||||
|
newValue = [...prev, selectedValue];
|
||||||
}
|
}
|
||||||
|
return newValue;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Allow syncing with external value again after a short delay
|
||||||
|
setTimeout(() => {
|
||||||
|
shouldSyncWithExternalValue.current = true;
|
||||||
|
}, 0);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Handle wheel scroll in dropdown
|
// Handle wheel scroll in dropdown
|
||||||
|
|||||||
@@ -51,8 +51,6 @@ const SelectCell = <T extends string>({
|
|||||||
// Add state for hover
|
// Add state for hover
|
||||||
const [isHovered, setIsHovered] = useState(false);
|
const [isHovered, setIsHovered] = useState(false);
|
||||||
|
|
||||||
console.log(`SelectCell: field.key=${field.key}, disabled=${disabled}, options=`, options);
|
|
||||||
|
|
||||||
// Helper function to check if a class is present in the className string
|
// Helper function to check if a class is present in the className string
|
||||||
const hasClass = (cls: string): boolean => {
|
const hasClass = (cls: string): boolean => {
|
||||||
const classNames = className.split(' ');
|
const classNames = className.split(' ');
|
||||||
@@ -68,7 +66,6 @@ const SelectCell = <T extends string>({
|
|||||||
|
|
||||||
// Memoize options processing to avoid recalculation on every render
|
// Memoize options processing to avoid recalculation on every render
|
||||||
const selectOptions = useMemo(() => {
|
const selectOptions = useMemo(() => {
|
||||||
console.log(`Processing options for ${field.key}:`, options);
|
|
||||||
// Fast path check - if we have raw options, just use those
|
// Fast path check - if we have raw options, just use those
|
||||||
if (options && options.length > 0) {
|
if (options && options.length > 0) {
|
||||||
// Check if options already have the correct structure to avoid mapping
|
// Check if options already have the correct structure to avoid mapping
|
||||||
@@ -126,8 +123,11 @@ const SelectCell = <T extends string>({
|
|||||||
|
|
||||||
// Handle selection - UPDATE INTERNAL VALUE FIRST
|
// Handle selection - UPDATE INTERNAL VALUE FIRST
|
||||||
const handleSelect = useCallback((selectedValue: string) => {
|
const handleSelect = useCallback((selectedValue: string) => {
|
||||||
|
// Store the selected value to prevent it being lost in async operations
|
||||||
|
const valueToCommit = selectedValue;
|
||||||
|
|
||||||
// 1. Update internal value immediately to prevent UI flicker
|
// 1. Update internal value immediately to prevent UI flicker
|
||||||
setInternalValue(selectedValue);
|
setInternalValue(valueToCommit);
|
||||||
|
|
||||||
// 2. Close the dropdown immediately
|
// 2. Close the dropdown immediately
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
@@ -139,105 +139,44 @@ const SelectCell = <T extends string>({
|
|||||||
// This prevents the parent component from re-rendering and causing dropdown to reopen
|
// This prevents the parent component from re-rendering and causing dropdown to reopen
|
||||||
if (onEndEdit) onEndEdit();
|
if (onEndEdit) onEndEdit();
|
||||||
|
|
||||||
// 5. Call onChange in the next tick to avoid synchronous re-renders
|
// 5. Call onChange synchronously to avoid race conditions with other cells
|
||||||
|
onChange(valueToCommit);
|
||||||
|
|
||||||
|
// 6. Clear processing state after a short delay
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
onChange(selectedValue);
|
setIsProcessing(false);
|
||||||
}, 0);
|
}, 200);
|
||||||
}, [onChange, onEndEdit]);
|
}, [onChange, onEndEdit]);
|
||||||
|
|
||||||
// If disabled, render a static view
|
// If disabled, render a static view
|
||||||
if (disabled) {
|
if (disabled) {
|
||||||
const displayText = displayValue;
|
const displayText = displayValue;
|
||||||
|
|
||||||
// For debugging, let's render the Popover component even if disabled
|
|
||||||
// This will help us determine if the issue is with the disabled state
|
|
||||||
return (
|
return (
|
||||||
<Popover
|
<div
|
||||||
open={open}
|
className={cn(
|
||||||
onOpenChange={(isOpen) => {
|
"w-full px-3 py-2 h-10 rounded-md text-sm flex items-center",
|
||||||
setOpen(isOpen);
|
"border",
|
||||||
if (isOpen && onStartEdit) onStartEdit();
|
hasErrors ? "border-destructive" : "border-input",
|
||||||
}}
|
className
|
||||||
>
|
)}
|
||||||
<PopoverTrigger asChild>
|
style={{
|
||||||
<Button
|
backgroundColor: hasClass('!bg-blue-100') ? '#dbeafe' :
|
||||||
variant="outline"
|
hasClass('!bg-blue-200') ? '#bfdbfe' :
|
||||||
role="combobox"
|
hasClass('hover:!bg-blue-100') && isHovered ? '#dbeafe' :
|
||||||
aria-expanded={open}
|
|
||||||
className={cn(
|
|
||||||
"w-full justify-between font-normal",
|
|
||||||
"border",
|
|
||||||
!internalValue && "text-muted-foreground",
|
|
||||||
isProcessing && "text-muted-foreground",
|
|
||||||
hasErrors ? "border-destructive" : "",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
style={{
|
|
||||||
backgroundColor: hasClass('!bg-blue-100') ? '#dbeafe' :
|
|
||||||
hasClass('!bg-blue-200') ? '#bfdbfe' :
|
|
||||||
hasClass('hover:!bg-blue-100') && isHovered ? '#dbeafe' :
|
|
||||||
undefined,
|
|
||||||
borderColor: hasClass('!border-blue-500') ? '#3b82f6' :
|
|
||||||
hasClass('!border-blue-200') ? '#bfdbfe' :
|
|
||||||
hasClass('!border-blue-200') && isHovered ? '#bfdbfe' :
|
|
||||||
undefined,
|
undefined,
|
||||||
borderRadius: hasClass('!rounded-md') ? '0.375rem' : undefined,
|
borderColor: hasClass('!border-blue-500') ? '#3b82f6' :
|
||||||
borderWidth: hasClass('!border-blue-500') || hasClass('!border-blue-200') ? '0px' : undefined,
|
hasClass('!border-blue-200') ? '#bfdbfe' :
|
||||||
cursor: hasClass('hover:!bg-blue-100') ? 'pointer' : undefined
|
hasClass('!border-blue-200') && isHovered ? '#bfdbfe' :
|
||||||
}}
|
undefined,
|
||||||
onClick={(e) => {
|
borderRadius: hasClass('!rounded-md') ? '0.375rem' : undefined,
|
||||||
e.preventDefault();
|
cursor: hasClass('hover:!bg-blue-100') ? 'pointer' : undefined
|
||||||
e.stopPropagation();
|
}}
|
||||||
setOpen(!open);
|
onMouseEnter={() => setIsHovered(true)}
|
||||||
if (!open && onStartEdit) onStartEdit();
|
onMouseLeave={() => setIsHovered(false)}
|
||||||
}}
|
>
|
||||||
onMouseEnter={() => setIsHovered(true)}
|
{displayText || ""}
|
||||||
onMouseLeave={() => setIsHovered(false)}
|
</div>
|
||||||
>
|
|
||||||
<span className={isProcessing ? "opacity-70" : ""}>
|
|
||||||
{displayValue}
|
|
||||||
</span>
|
|
||||||
<ChevronsUpDown className="mr-1.5 h-4 w-4 shrink-0 opacity-50" />
|
|
||||||
</Button>
|
|
||||||
</PopoverTrigger>
|
|
||||||
<PopoverContent
|
|
||||||
className="p-0 w-[var(--radix-popover-trigger-width)]"
|
|
||||||
align="start"
|
|
||||||
sideOffset={4}
|
|
||||||
>
|
|
||||||
<Command shouldFilter={true}>
|
|
||||||
<CommandInput
|
|
||||||
placeholder="Search..."
|
|
||||||
className="h-9"
|
|
||||||
/>
|
|
||||||
<CommandList
|
|
||||||
ref={commandListRef}
|
|
||||||
onWheel={handleWheel}
|
|
||||||
className="max-h-[200px]"
|
|
||||||
>
|
|
||||||
<CommandEmpty>No options found.</CommandEmpty>
|
|
||||||
<CommandGroup>
|
|
||||||
{selectOptions.map((option) => (
|
|
||||||
<CommandItem
|
|
||||||
key={option.value}
|
|
||||||
value={option.value}
|
|
||||||
onSelect={(value) => handleSelect(value)}
|
|
||||||
className="flex w-full"
|
|
||||||
>
|
|
||||||
<Check
|
|
||||||
className={cn(
|
|
||||||
"mr-2 h-4 w-4 flex-shrink-0",
|
|
||||||
internalValue === option.value ? "opacity-100" : "opacity-0"
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<span className="truncate w-full">{option.label}</span>
|
|
||||||
</CommandItem>
|
|
||||||
))}
|
|
||||||
</CommandGroup>
|
|
||||||
</CommandList>
|
|
||||||
</Command>
|
|
||||||
</PopoverContent>
|
|
||||||
</Popover>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -39,24 +39,27 @@ export const useProductLinesFetching = (data: Record<string, any>[]) => {
|
|||||||
|
|
||||||
// Fetch product lines from API
|
// Fetch product lines from API
|
||||||
const productLinesUrl = `/api/import/product-lines/${companyId}`;
|
const productLinesUrl = `/api/import/product-lines/${companyId}`;
|
||||||
console.log(`Fetching from URL: ${productLinesUrl}`);
|
|
||||||
const response = await axios.get(productLinesUrl);
|
const response = await axios.get(productLinesUrl);
|
||||||
if (response.status !== 200) {
|
|
||||||
throw new Error(`Failed to fetch product lines: ${response.status}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const productLines = response.data;
|
const lines = response.data;
|
||||||
console.log(`Received ${productLines.length} product lines for company ${companyId}`);
|
console.log(`Received ${lines.length} product lines for company ${companyId}`);
|
||||||
|
|
||||||
|
// Format the data properly for dropdown display
|
||||||
|
const formattedLines = lines.map((line: any) => ({
|
||||||
|
label: line.name || line.label || String(line.value || line.id),
|
||||||
|
value: String(line.value || line.id)
|
||||||
|
}));
|
||||||
|
|
||||||
// Store in company cache
|
// Store in company cache
|
||||||
setCompanyLinesCache(prev => ({ ...prev, [companyId]: productLines }));
|
setCompanyLinesCache(prev => ({ ...prev, [companyId]: formattedLines }));
|
||||||
|
|
||||||
// Store for this specific row
|
// Store for this specific row
|
||||||
setRowProductLines(prev => ({ ...prev, [rowIndex]: productLines }));
|
setRowProductLines(prev => ({ ...prev, [rowIndex]: formattedLines }));
|
||||||
|
|
||||||
return productLines;
|
return formattedLines;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Error fetching product lines for company ${companyId}:`, error);
|
console.error(`Error fetching product lines for company ${companyId}:`, error);
|
||||||
|
toast.error(`Failed to load product lines for company ${companyId}`);
|
||||||
|
|
||||||
// Set empty array for this company to prevent repeated failed requests
|
// Set empty array for this company to prevent repeated failed requests
|
||||||
setCompanyLinesCache(prev => ({ ...prev, [companyId]: [] }));
|
setCompanyLinesCache(prev => ({ ...prev, [companyId]: [] }));
|
||||||
@@ -92,22 +95,24 @@ export const useProductLinesFetching = (data: Record<string, any>[]) => {
|
|||||||
|
|
||||||
// Fetch sublines from API
|
// Fetch sublines from API
|
||||||
const sublinesUrl = `/api/import/sublines/${lineId}`;
|
const sublinesUrl = `/api/import/sublines/${lineId}`;
|
||||||
console.log(`Fetching from URL: ${sublinesUrl}`);
|
|
||||||
const response = await axios.get(sublinesUrl);
|
const response = await axios.get(sublinesUrl);
|
||||||
if (response.status !== 200) {
|
|
||||||
throw new Error(`Failed to fetch sublines: ${response.status}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const sublines = response.data;
|
const sublines = response.data;
|
||||||
console.log(`Received ${sublines.length} sublines for line ${lineId}`);
|
console.log(`Received ${sublines.length} sublines for line ${lineId}`);
|
||||||
|
|
||||||
|
// Format the data properly for dropdown display
|
||||||
|
const formattedSublines = sublines.map((subline: any) => ({
|
||||||
|
label: subline.name || subline.label || String(subline.value || subline.id),
|
||||||
|
value: String(subline.value || subline.id)
|
||||||
|
}));
|
||||||
|
|
||||||
// Store in line cache
|
// Store in line cache
|
||||||
setLineSublineCache(prev => ({ ...prev, [lineId]: sublines }));
|
setLineSublineCache(prev => ({ ...prev, [lineId]: formattedSublines }));
|
||||||
|
|
||||||
// Store for this specific row
|
// Store for this specific row
|
||||||
setRowSublines(prev => ({ ...prev, [rowIndex]: sublines }));
|
setRowSublines(prev => ({ ...prev, [rowIndex]: formattedSublines }));
|
||||||
|
|
||||||
return sublines;
|
return formattedSublines;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Error fetching sublines for line ${lineId}:`, error);
|
console.error(`Error fetching sublines for line ${lineId}:`, error);
|
||||||
|
|
||||||
@@ -129,6 +134,25 @@ export const useProductLinesFetching = (data: Record<string, any>[]) => {
|
|||||||
// Skip if there's no data
|
// Skip if there's no data
|
||||||
if (!data.length) return;
|
if (!data.length) return;
|
||||||
|
|
||||||
|
// First check if we need to do anything at all
|
||||||
|
let needsFetching = false;
|
||||||
|
|
||||||
|
// Quick check for any rows that would need fetching
|
||||||
|
for (const row of data) {
|
||||||
|
const rowId = row.__index;
|
||||||
|
if (!rowId) continue;
|
||||||
|
|
||||||
|
if ((row.company && !rowProductLines[rowId]) || (row.line && !rowSublines[rowId])) {
|
||||||
|
needsFetching = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If nothing needs fetching, exit early
|
||||||
|
if (!needsFetching) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
console.log("Starting to fetch product lines and sublines");
|
console.log("Starting to fetch product lines and sublines");
|
||||||
|
|
||||||
// Group rows by company and line to minimize API calls
|
// Group rows by company and line to minimize API calls
|
||||||
@@ -160,6 +184,11 @@ export const useProductLinesFetching = (data: Record<string, any>[]) => {
|
|||||||
|
|
||||||
console.log(`Need to fetch product lines for ${companiesNeeded.size} companies and sublines for ${linesNeeded.size} lines`);
|
console.log(`Need to fetch product lines for ${companiesNeeded.size} companies and sublines for ${linesNeeded.size} lines`);
|
||||||
|
|
||||||
|
// If nothing to fetch, exit early to prevent unnecessary processing
|
||||||
|
if (companiesNeeded.size === 0 && linesNeeded.size === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Create arrays to hold all fetch promises
|
// Create arrays to hold all fetch promises
|
||||||
const fetchPromises: Promise<void>[] = [];
|
const fetchPromises: Promise<void>[] = [];
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { useState, useCallback, useRef, useEffect } from 'react'
|
import { useState, useCallback, useRef, useEffect } from 'react'
|
||||||
import config from '@/config'
|
import config from '@/config'
|
||||||
|
|
||||||
|
|
||||||
interface ValidationState {
|
interface ValidationState {
|
||||||
validatingCells: Set<string>; // Using rowIndex-fieldKey as identifier
|
validatingCells: Set<string>; // Using rowIndex-fieldKey as identifier
|
||||||
itemNumbers: Map<number, string>; // Using rowIndex as key
|
itemNumbers: Map<number, string>; // Using rowIndex as key
|
||||||
validatingRows: Set<number>; // Rows currently being validated
|
validatingRows: Set<number>; // Rows currently being validated
|
||||||
|
activeValidations: Set<string>; // Active validations
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useUpcValidation = (
|
export const useUpcValidation = (
|
||||||
@@ -16,19 +16,27 @@ export const useUpcValidation = (
|
|||||||
const validationStateRef = useRef<ValidationState>({
|
const validationStateRef = useRef<ValidationState>({
|
||||||
validatingCells: new Set(),
|
validatingCells: new Set(),
|
||||||
itemNumbers: new Map(),
|
itemNumbers: new Map(),
|
||||||
validatingRows: new Set()
|
validatingRows: new Set(),
|
||||||
|
activeValidations: new Set()
|
||||||
});
|
});
|
||||||
|
|
||||||
// Use state only for forcing re-renders of specific cells
|
// Use state only for forcing re-renders of specific cells
|
||||||
const [validatingCellKeys, setValidatingCellKeys] = useState<Set<string>>(new Set());
|
const [, setValidatingCellKeys] = useState<Set<string>>(new Set());
|
||||||
const [itemNumberUpdates, setItemNumberUpdates] = useState<Map<number, string>>(new Map());
|
const [, setItemNumberUpdates] = useState<Map<number, string>>(new Map());
|
||||||
const [validatingRows, setValidatingRows] = useState<Set<number>>(new Set());
|
const [validatingRows, setValidatingRows] = useState<Set<number>>(new Set());
|
||||||
const [isValidatingUpc, setIsValidatingUpc] = useState(false);
|
const [, setIsValidatingUpc] = useState(false);
|
||||||
|
|
||||||
// Cache for UPC validation results
|
// Cache for UPC validation results
|
||||||
const processedUpcMapRef = useRef(new Map<string, string>());
|
const processedUpcMapRef = useRef(new Map<string, string>());
|
||||||
const initialUpcValidationDoneRef = useRef(false);
|
const initialUpcValidationDoneRef = useRef(false);
|
||||||
|
|
||||||
|
// For batch validation
|
||||||
|
const validationQueueRef = useRef<Array<{rowIndex: number, supplierId: string, upcValue: string}>>([]);
|
||||||
|
const isProcessingBatchRef = useRef(false);
|
||||||
|
|
||||||
|
// For validation results
|
||||||
|
const [upcValidationResults] = useState<Map<number, { itemNumber: string }>>(new Map());
|
||||||
|
|
||||||
// Helper to create cell key
|
// Helper to create cell key
|
||||||
const getCellKey = (rowIndex: number, fieldKey: string) => `${rowIndex}-${fieldKey}`;
|
const getCellKey = (rowIndex: number, fieldKey: string) => `${rowIndex}-${fieldKey}`;
|
||||||
|
|
||||||
@@ -48,6 +56,7 @@ export const useUpcValidation = (
|
|||||||
|
|
||||||
// Update item number
|
// Update item number
|
||||||
const updateItemNumber = useCallback((rowIndex: number, itemNumber: string) => {
|
const updateItemNumber = useCallback((rowIndex: number, itemNumber: string) => {
|
||||||
|
console.log(`Setting item number for row ${rowIndex} to ${itemNumber}`);
|
||||||
validationStateRef.current.itemNumbers.set(rowIndex, itemNumber);
|
validationStateRef.current.itemNumbers.set(rowIndex, itemNumber);
|
||||||
setItemNumberUpdates(new Map(validationStateRef.current.itemNumbers));
|
setItemNumberUpdates(new Map(validationStateRef.current.itemNumbers));
|
||||||
}, []);
|
}, []);
|
||||||
@@ -56,6 +65,7 @@ export const useUpcValidation = (
|
|||||||
const startValidatingRow = useCallback((rowIndex: number) => {
|
const startValidatingRow = useCallback((rowIndex: number) => {
|
||||||
validationStateRef.current.validatingRows.add(rowIndex);
|
validationStateRef.current.validatingRows.add(rowIndex);
|
||||||
setValidatingRows(new Set(validationStateRef.current.validatingRows));
|
setValidatingRows(new Set(validationStateRef.current.validatingRows));
|
||||||
|
setIsValidatingUpc(true);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Mark a row as no longer being validated
|
// Mark a row as no longer being validated
|
||||||
@@ -83,114 +93,270 @@ export const useUpcValidation = (
|
|||||||
const getItemNumber = useCallback((rowIndex: number): string | undefined => {
|
const getItemNumber = useCallback((rowIndex: number): string | undefined => {
|
||||||
return validationStateRef.current.itemNumbers.get(rowIndex);
|
return validationStateRef.current.itemNumbers.get(rowIndex);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// Fetch product by UPC from API
|
||||||
|
const fetchProductByUpc = useCallback(async (supplierId: string, upcValue: string) => {
|
||||||
|
try {
|
||||||
|
console.log(`Fetching product for UPC ${upcValue} with supplier ${supplierId}`);
|
||||||
|
const response = await fetch(`${config.apiUrl}/import/check-upc-and-generate-sku?upc=${encodeURIComponent(upcValue)}&supplierId=${encodeURIComponent(supplierId)}`);
|
||||||
|
|
||||||
|
// Handle error responses
|
||||||
|
if (response.status === 409) {
|
||||||
|
console.log(`UPC ${upcValue} already exists`);
|
||||||
|
return { error: true, message: 'UPC already exists' };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
console.error(`API error: ${response.status}`);
|
||||||
|
return { error: true, message: `API error (${response.status})` };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process successful response
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (!data.success) {
|
||||||
|
return { error: true, message: data.message || 'Unknown error' };
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
error: false,
|
||||||
|
data: {
|
||||||
|
itemNumber: data.itemNumber || '',
|
||||||
|
...data
|
||||||
|
}
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Network error:', error);
|
||||||
|
return { error: true, message: 'Network error' };
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
// Apply all pending updates to the data state
|
// Validate a UPC for a row - returns a promise that resolves when complete
|
||||||
const applyItemNumbersToData = useCallback(() => {
|
const validateUpc = useCallback(async (rowIndex: number, supplierId: string, upcValue: string) => {
|
||||||
if (validationStateRef.current.itemNumbers.size === 0) return;
|
// Clear any previous validation keys for this row to avoid cancellations
|
||||||
|
const previousKeys = Array.from(validationStateRef.current.activeValidations).filter(key =>
|
||||||
|
key.startsWith(`${rowIndex}-`)
|
||||||
|
);
|
||||||
|
previousKeys.forEach(key => validationStateRef.current.activeValidations.delete(key));
|
||||||
|
|
||||||
setData((prevData: any[]) => {
|
// Start validation - track this with the ref to avoid race conditions
|
||||||
|
startValidatingRow(rowIndex);
|
||||||
|
startValidatingCell(rowIndex, 'item_number');
|
||||||
|
|
||||||
|
console.log(`Validating UPC: rowIndex=${rowIndex}, supplierId=${supplierId}, upc=${upcValue}`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Create a unique key for this validation to track it
|
||||||
|
const validationKey = `${rowIndex}-${supplierId}-${upcValue}`;
|
||||||
|
validationStateRef.current.activeValidations.add(validationKey);
|
||||||
|
|
||||||
|
// IMPORTANT: First update the data with the new UPC value to prevent UI flicker
|
||||||
|
// This ensures the UPC field keeps showing the new value while validation runs
|
||||||
|
setData(prevData => {
|
||||||
|
const newData = [...prevData];
|
||||||
|
if (newData[rowIndex]) {
|
||||||
|
newData[rowIndex] = {
|
||||||
|
...newData[rowIndex],
|
||||||
|
upc: upcValue
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return newData;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Fetch the product by UPC
|
||||||
|
const product = await fetchProductByUpc(supplierId, upcValue);
|
||||||
|
|
||||||
|
// Check if this validation is still relevant (hasn't been superseded by another)
|
||||||
|
if (!validationStateRef.current.activeValidations.has(validationKey)) {
|
||||||
|
console.log(`Validation ${validationKey} was cancelled`);
|
||||||
|
return { success: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract the item number from the API response - check for !error since API returns { error: boolean, data: any }
|
||||||
|
if (product && !product.error && product.data?.itemNumber) {
|
||||||
|
// Store this validation result
|
||||||
|
updateItemNumber(rowIndex, product.data.itemNumber);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
itemNumber: product.data.itemNumber
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
// No item number found but validation was still attempted
|
||||||
|
console.log(`No item number found for UPC ${upcValue}`);
|
||||||
|
|
||||||
|
// Clear any existing item number to show validation was attempted and failed
|
||||||
|
if (validationStateRef.current.itemNumbers.has(rowIndex)) {
|
||||||
|
validationStateRef.current.itemNumbers.delete(rowIndex);
|
||||||
|
setItemNumberUpdates(new Map(validationStateRef.current.itemNumbers));
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: false };
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error validating UPC:', error);
|
||||||
|
return { success: false };
|
||||||
|
} finally {
|
||||||
|
// End validation
|
||||||
|
stopValidatingRow(rowIndex);
|
||||||
|
stopValidatingCell(rowIndex, 'item_number');
|
||||||
|
}
|
||||||
|
}, [fetchProductByUpc, updateItemNumber, startValidatingCell, stopValidatingCell, startValidatingRow, stopValidatingRow, setData]);
|
||||||
|
|
||||||
|
// Apply item numbers to data
|
||||||
|
const applyItemNumbersToData = useCallback((onApplied?: (updatedRowIds: number[]) => void) => {
|
||||||
|
// Create a copy of the current item numbers map to avoid race conditions
|
||||||
|
const currentItemNumbers = new Map(validationStateRef.current.itemNumbers);
|
||||||
|
|
||||||
|
// Only apply if we have any item numbers
|
||||||
|
if (currentItemNumbers.size === 0) return;
|
||||||
|
|
||||||
|
// Track updated row indices to pass to callback
|
||||||
|
const updatedRowIndices: number[] = [];
|
||||||
|
|
||||||
|
// Log for debugging
|
||||||
|
console.log(`Applying ${currentItemNumbers.size} item numbers to data`);
|
||||||
|
|
||||||
|
setData(prevData => {
|
||||||
|
// Create a new copy of the data
|
||||||
const newData = [...prevData];
|
const newData = [...prevData];
|
||||||
|
|
||||||
// Apply all item numbers without changing other data
|
// Update each row with its item number without affecting other fields
|
||||||
Array.from(validationStateRef.current.itemNumbers.entries()).forEach(([index, itemNumber]) => {
|
currentItemNumbers.forEach((itemNumber, rowIndex) => {
|
||||||
if (index >= 0 && index < newData.length) {
|
if (rowIndex < newData.length) {
|
||||||
// Only update the item_number field and leave everything else unchanged
|
console.log(`Setting item_number for row ${rowIndex} to ${itemNumber}`);
|
||||||
newData[index] = {
|
|
||||||
...newData[index],
|
// Only update the item_number field, leaving other fields unchanged
|
||||||
|
newData[rowIndex] = {
|
||||||
|
...newData[rowIndex],
|
||||||
item_number: itemNumber
|
item_number: itemNumber
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Track which rows were updated
|
||||||
|
updatedRowIndices.push(rowIndex);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return newData;
|
return newData;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Clear the item numbers state after applying
|
// Call the callback if provided, after state updates are processed
|
||||||
validationStateRef.current.itemNumbers.clear();
|
if (onApplied && updatedRowIndices.length > 0) {
|
||||||
setItemNumberUpdates(new Map());
|
// Use setTimeout to ensure this happens after the state update
|
||||||
|
setTimeout(() => {
|
||||||
|
onApplied(updatedRowIndices);
|
||||||
|
}, 100); // Use 100ms to ensure the data update is fully processed
|
||||||
|
}
|
||||||
}, [setData]);
|
}, [setData]);
|
||||||
|
|
||||||
// Validate a UPC value
|
// Process validation queue in batches - faster processing with smaller batches
|
||||||
const validateUpc = useCallback(async (
|
const processBatchValidation = useCallback(async () => {
|
||||||
rowIndex: number,
|
if (isProcessingBatchRef.current) return;
|
||||||
supplierId: string,
|
if (validationQueueRef.current.length === 0) return;
|
||||||
upcValue: string
|
|
||||||
): Promise<{success: boolean, itemNumber?: string}> => {
|
console.log(`Processing validation batch with ${validationQueueRef.current.length} items`);
|
||||||
|
isProcessingBatchRef.current = true;
|
||||||
|
|
||||||
|
// Process in smaller batches for better UI responsiveness
|
||||||
|
const BATCH_SIZE = 5;
|
||||||
|
const queue = [...validationQueueRef.current];
|
||||||
|
validationQueueRef.current = [];
|
||||||
|
|
||||||
|
// Track if any updates were made
|
||||||
|
let updatesApplied = false;
|
||||||
|
|
||||||
|
// Track updated row indices
|
||||||
|
const updatedRows: number[] = [];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Skip if either value is missing
|
// Process in small batches
|
||||||
if (!supplierId || !upcValue) {
|
for (let i = 0; i < queue.length; i += BATCH_SIZE) {
|
||||||
return { success: false };
|
const batch = queue.slice(i, i + BATCH_SIZE);
|
||||||
}
|
|
||||||
|
|
||||||
// Add logging to help debug
|
|
||||||
console.log(`Validating UPC for row ${rowIndex}. Supplier ID: ${supplierId}, UPC: ${upcValue}`);
|
|
||||||
|
|
||||||
// Start validating both UPC and item_number cells
|
|
||||||
startValidatingCell(rowIndex, 'upc');
|
|
||||||
startValidatingCell(rowIndex, 'item_number');
|
|
||||||
|
|
||||||
// Also mark the row as being validated
|
|
||||||
startValidatingRow(rowIndex);
|
|
||||||
|
|
||||||
// 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) {
|
// Process batch in parallel
|
||||||
// Use cached item number
|
const results = await Promise.all(batch.map(async ({ rowIndex, supplierId, upcValue }) => {
|
||||||
updateItemNumber(rowIndex, cachedItemNumber);
|
try {
|
||||||
return { success: true, itemNumber: cachedItemNumber };
|
// Skip if already validated
|
||||||
|
const cacheKey = `${supplierId}-${upcValue}`;
|
||||||
|
if (processedUpcMapRef.current.has(cacheKey)) {
|
||||||
|
const cachedItemNumber = processedUpcMapRef.current.get(cacheKey);
|
||||||
|
if (cachedItemNumber) {
|
||||||
|
console.log(`Using cached item number for row ${rowIndex}: ${cachedItemNumber}`);
|
||||||
|
updateItemNumber(rowIndex, cachedItemNumber);
|
||||||
|
updatesApplied = true;
|
||||||
|
updatedRows.push(rowIndex);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch from API
|
||||||
|
const result = await fetchProductByUpc(supplierId, upcValue);
|
||||||
|
|
||||||
|
if (!result.error && result.data?.itemNumber) {
|
||||||
|
const itemNumber = result.data.itemNumber;
|
||||||
|
|
||||||
|
// Store in cache
|
||||||
|
processedUpcMapRef.current.set(cacheKey, itemNumber);
|
||||||
|
|
||||||
|
// Update item number
|
||||||
|
updateItemNumber(rowIndex, itemNumber);
|
||||||
|
updatesApplied = true;
|
||||||
|
updatedRows.push(rowIndex);
|
||||||
|
|
||||||
|
console.log(`Set item number for row ${rowIndex} to ${itemNumber}`);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error processing row ${rowIndex}:`, error);
|
||||||
|
return false;
|
||||||
|
} finally {
|
||||||
|
// Clear validation state
|
||||||
|
stopValidatingRow(rowIndex);
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
// If any updates were applied in this batch, update the data
|
||||||
|
if (results.some(Boolean) && updatesApplied) {
|
||||||
|
applyItemNumbersToData(updatedRowIds => {
|
||||||
|
console.log(`Processed batch UPC validation for rows: ${updatedRowIds.join(', ')}`);
|
||||||
|
});
|
||||||
|
updatesApplied = false;
|
||||||
|
updatedRows.length = 0; // Clear the array
|
||||||
}
|
}
|
||||||
|
|
||||||
return { success: false };
|
// Small delay between batches to allow UI to update
|
||||||
}
|
if (i + BATCH_SIZE < queue.length) {
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 10));
|
||||||
// Make API call to validate UPC
|
|
||||||
const response = await fetch(`${config.apiUrl}/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
|
|
||||||
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
|
|
||||||
updateItemNumber(rowIndex, responseData.itemNumber);
|
|
||||||
|
|
||||||
return { success: true, itemNumber: responseData.itemNumber };
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return { success: false };
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Error validating UPC for row ${rowIndex}:`, error);
|
console.error('Error in batch processing:', error);
|
||||||
return { success: false };
|
|
||||||
} finally {
|
} finally {
|
||||||
// Clear validation state
|
isProcessingBatchRef.current = false;
|
||||||
stopValidatingCell(rowIndex, 'upc');
|
|
||||||
stopValidatingCell(rowIndex, 'item_number');
|
// Process any new items
|
||||||
stopValidatingRow(rowIndex);
|
if (validationQueueRef.current.length > 0) {
|
||||||
|
setTimeout(processBatchValidation, 0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, [startValidatingCell, stopValidatingCell, updateItemNumber, startValidatingRow, stopValidatingRow]);
|
}, [fetchProductByUpc, updateItemNumber, stopValidatingRow, applyItemNumbersToData]);
|
||||||
|
|
||||||
|
// For immediate processing
|
||||||
|
|
||||||
// Batch validate all UPCs in the data
|
// Batch validate all UPCs in the data
|
||||||
const validateAllUPCs = useCallback(async () => {
|
const validateAllUPCs = useCallback(async () => {
|
||||||
// Skip if we've already done the initial validation
|
// Skip if we've already done the initial validation
|
||||||
if (initialUpcValidationDoneRef.current) {
|
if (initialUpcValidationDoneRef.current) {
|
||||||
|
console.log('Initial UPC validation already done, skipping');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mark that we've done the initial validation
|
// Mark that we've started the initial validation
|
||||||
initialUpcValidationDoneRef.current = true;
|
initialUpcValidationDoneRef.current = true;
|
||||||
|
|
||||||
console.log('Starting UPC validation...');
|
console.log('Starting initial UPC validation...');
|
||||||
|
|
||||||
// Set validation state
|
// Set validation state
|
||||||
setIsValidatingUpc(true);
|
setIsValidatingUpc(true);
|
||||||
@@ -206,7 +372,7 @@ export const useUpcValidation = (
|
|||||||
});
|
});
|
||||||
|
|
||||||
const totalRows = rowsToValidate.length;
|
const totalRows = rowsToValidate.length;
|
||||||
console.log(`Found ${totalRows} rows with both supplier and UPC`);
|
console.log(`Found ${totalRows} rows with both supplier and UPC for initial validation`);
|
||||||
|
|
||||||
if (totalRows === 0) {
|
if (totalRows === 0) {
|
||||||
setIsValidatingUpc(false);
|
setIsValidatingUpc(false);
|
||||||
@@ -219,37 +385,102 @@ export const useUpcValidation = (
|
|||||||
setValidatingRows(newValidatingRows);
|
setValidatingRows(newValidatingRows);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Process all rows in parallel
|
// Process rows in batches for better UX
|
||||||
await Promise.all(
|
const BATCH_SIZE = 100;
|
||||||
rowsToValidate.map(async ({ row, index }) => {
|
const batches = [];
|
||||||
try {
|
|
||||||
const rowAny = row as Record<string, any>;
|
// Split rows into batches
|
||||||
const supplierId = rowAny.supplier.toString();
|
for (let i = 0; i < rowsToValidate.length; i += BATCH_SIZE) {
|
||||||
const upcValue = (rowAny.upc || rowAny.barcode).toString();
|
batches.push(rowsToValidate.slice(i, i + BATCH_SIZE));
|
||||||
|
}
|
||||||
// Validate the UPC
|
|
||||||
await validateUpc(index, supplierId, upcValue);
|
console.log(`Processing ${batches.length} batches for ${totalRows} rows`);
|
||||||
|
|
||||||
// Remove this row from the validating set (handled in validateUpc)
|
// Process each batch sequentially
|
||||||
} catch (error) {
|
for (let batchIndex = 0; batchIndex < batches.length; batchIndex++) {
|
||||||
console.error(`Error processing row ${index}:`, error);
|
const batch = batches[batchIndex];
|
||||||
stopValidatingRow(index);
|
console.log(`Processing batch ${batchIndex + 1}/${batches.length} with ${batch.length} rows`);
|
||||||
}
|
|
||||||
})
|
// Track updated rows in this batch
|
||||||
);
|
const batchUpdatedRows: number[] = [];
|
||||||
|
|
||||||
|
// Process all rows in current 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();
|
||||||
|
|
||||||
|
console.log(`Validating UPC in initial batch: row=${index}, supplier=${supplierId}, upc=${upcValue}`);
|
||||||
|
|
||||||
|
// Mark the item_number cell as validating
|
||||||
|
startValidatingCell(index, 'item_number');
|
||||||
|
|
||||||
|
// Validate the UPC directly (don't use validateUpc to avoid duplicate UI updates)
|
||||||
|
const cacheKey = `${supplierId}-${upcValue}`;
|
||||||
|
|
||||||
|
// Check cache first
|
||||||
|
if (processedUpcMapRef.current.has(cacheKey)) {
|
||||||
|
const cachedItemNumber = processedUpcMapRef.current.get(cacheKey);
|
||||||
|
if (cachedItemNumber) {
|
||||||
|
console.log(`Using cached item number for row ${index}: ${cachedItemNumber}`);
|
||||||
|
updateItemNumber(index, cachedItemNumber);
|
||||||
|
batchUpdatedRows.push(index);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make API call
|
||||||
|
const result = await fetchProductByUpc(supplierId, upcValue);
|
||||||
|
|
||||||
|
if (!result.error && result.data?.itemNumber) {
|
||||||
|
const itemNumber = result.data.itemNumber;
|
||||||
|
console.log(`Got item number from API for row ${index}: ${itemNumber}`);
|
||||||
|
|
||||||
|
// Cache the result
|
||||||
|
processedUpcMapRef.current.set(cacheKey, itemNumber);
|
||||||
|
|
||||||
|
// Update item number
|
||||||
|
updateItemNumber(index, itemNumber);
|
||||||
|
batchUpdatedRows.push(index);
|
||||||
|
} else {
|
||||||
|
console.warn(`No item number found for row ${index} UPC ${upcValue}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error validating row ${index}:`, error);
|
||||||
|
} finally {
|
||||||
|
// Clear validation state
|
||||||
|
stopValidatingCell(index, 'item_number');
|
||||||
|
stopValidatingRow(index);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// Apply updates for this batch
|
||||||
|
if (validationStateRef.current.itemNumbers.size > 0) {
|
||||||
|
console.log(`Applying item numbers after batch ${batchIndex + 1}`);
|
||||||
|
applyItemNumbersToData(updatedRowIds => {
|
||||||
|
console.log(`Processed initial UPC validation batch ${batchIndex + 1} for rows: ${updatedRowIds.join(', ')}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Small delay between batches to update UI
|
||||||
|
if (batchIndex < batches.length - 1) {
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 100));
|
||||||
|
}
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error in batch validation:', error);
|
console.error('Error in batch validation:', error);
|
||||||
} finally {
|
} finally {
|
||||||
// Reset validation state
|
// Make sure all validation states are cleared
|
||||||
setIsValidatingUpc(false);
|
|
||||||
validationStateRef.current.validatingRows.clear();
|
validationStateRef.current.validatingRows.clear();
|
||||||
setValidatingRows(new Set());
|
setValidatingRows(new Set());
|
||||||
console.log('Completed UPC validation');
|
setIsValidatingUpc(false);
|
||||||
|
|
||||||
// Apply item numbers to data
|
console.log('Completed initial UPC validation');
|
||||||
applyItemNumbersToData();
|
|
||||||
}
|
}
|
||||||
}, [data, validateUpc, stopValidatingRow, applyItemNumbersToData]);
|
}, [data, fetchProductByUpc, updateItemNumber, startValidatingCell, stopValidatingCell, stopValidatingRow, applyItemNumbersToData]);
|
||||||
|
|
||||||
// Run initial UPC validation when data changes
|
// Run initial UPC validation when data changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -259,43 +490,28 @@ export const useUpcValidation = (
|
|||||||
// Run validation
|
// Run validation
|
||||||
validateAllUPCs();
|
validateAllUPCs();
|
||||||
}, [data, validateAllUPCs]);
|
}, [data, validateAllUPCs]);
|
||||||
|
|
||||||
// Apply item numbers when they change
|
// Return public API
|
||||||
useEffect(() => {
|
|
||||||
// Apply item numbers if there are any
|
|
||||||
if (validationStateRef.current.itemNumbers.size > 0) {
|
|
||||||
applyItemNumbersToData();
|
|
||||||
}
|
|
||||||
}, [itemNumberUpdates, applyItemNumbersToData]);
|
|
||||||
|
|
||||||
// Reset validation state when hook is unmounted
|
|
||||||
useEffect(() => {
|
|
||||||
return () => {
|
|
||||||
initialUpcValidationDoneRef.current = false;
|
|
||||||
processedUpcMapRef.current.clear();
|
|
||||||
validationStateRef.current.validatingCells.clear();
|
|
||||||
validationStateRef.current.itemNumbers.clear();
|
|
||||||
validationStateRef.current.validatingRows.clear();
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
// Validation methods
|
||||||
validateUpc,
|
validateUpc,
|
||||||
validateAllUPCs,
|
validateAllUPCs,
|
||||||
|
|
||||||
|
// Cell state
|
||||||
isValidatingCell,
|
isValidatingCell,
|
||||||
isRowValidatingUpc,
|
isRowValidatingUpc,
|
||||||
isValidatingUpc,
|
|
||||||
|
// Row state
|
||||||
|
validatingRows: validatingRows, // Expose as a Set to components
|
||||||
|
|
||||||
|
// Item number management
|
||||||
getItemNumber,
|
getItemNumber,
|
||||||
applyItemNumbersToData,
|
applyItemNumbersToData,
|
||||||
itemNumbers: itemNumberUpdates,
|
|
||||||
validatingCells: validatingCellKeys,
|
// Results
|
||||||
validatingRows,
|
upcValidationResults,
|
||||||
resetInitialValidation: () => {
|
|
||||||
initialUpcValidationDoneRef.current = false;
|
// Initialization state
|
||||||
},
|
initialValidationDone: initialUpcValidationDoneRef.current
|
||||||
// Export the ref for direct access
|
};
|
||||||
get initialValidationDone() {
|
};
|
||||||
return initialUpcValidationDoneRef.current;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -6,7 +6,7 @@ import { RowData } from './useValidationState'
|
|||||||
|
|
||||||
// Define InfoWithSource to match the expected structure
|
// Define InfoWithSource to match the expected structure
|
||||||
// Make sure source is required (not optional)
|
// Make sure source is required (not optional)
|
||||||
interface InfoWithSource {
|
export interface InfoWithSource {
|
||||||
message: string;
|
message: string;
|
||||||
level: 'info' | 'warning' | 'error';
|
level: 'info' | 'warning' | 'error';
|
||||||
source: ErrorSources;
|
source: ErrorSources;
|
||||||
@@ -21,6 +21,40 @@ const isEmpty = (value: any): boolean =>
|
|||||||
(Array.isArray(value) && value.length === 0) ||
|
(Array.isArray(value) && value.length === 0) ||
|
||||||
(typeof value === 'object' && !Array.isArray(value) && Object.keys(value).length === 0);
|
(typeof value === 'object' && !Array.isArray(value) && Object.keys(value).length === 0);
|
||||||
|
|
||||||
|
// Cache validation results to avoid running expensive validations repeatedly
|
||||||
|
const validationResultCache = new Map<string, ValidationError[]>();
|
||||||
|
|
||||||
|
// Add debounce to prevent rapid successive validations
|
||||||
|
let validateDataTimeoutId: ReturnType<typeof setTimeout> | null = null;
|
||||||
|
|
||||||
|
// Add a function to clear cache for a specific field value
|
||||||
|
export const clearValidationCacheForField = (fieldKey: string, value: any) => {
|
||||||
|
// Create a pattern to match cache keys for this field
|
||||||
|
const pattern = new RegExp(`^${fieldKey}-`);
|
||||||
|
|
||||||
|
// Find and clear matching cache entries
|
||||||
|
validationResultCache.forEach((_, key) => {
|
||||||
|
if (pattern.test(key)) {
|
||||||
|
validationResultCache.delete(key);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add a special function to clear all uniqueness validation caches
|
||||||
|
export const clearAllUniquenessCaches = () => {
|
||||||
|
// Clear cache for common unique fields
|
||||||
|
['item_number', 'upc', 'barcode', 'supplier_no', 'notions_no'].forEach(fieldKey => {
|
||||||
|
clearValidationCacheForField(fieldKey, null);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Also clear any cache entries that might involve uniqueness validation
|
||||||
|
validationResultCache.forEach((_, key) => {
|
||||||
|
if (key.includes('unique')) {
|
||||||
|
validationResultCache.delete(key);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
export const useValidation = <T extends string>(
|
export const useValidation = <T extends string>(
|
||||||
fields: Fields<T>,
|
fields: Fields<T>,
|
||||||
rowHook?: RowHook<T>,
|
rowHook?: RowHook<T>,
|
||||||
@@ -35,6 +69,14 @@ export const useValidation = <T extends string>(
|
|||||||
|
|
||||||
if (!field.validations) return errors
|
if (!field.validations) return errors
|
||||||
|
|
||||||
|
// Create a cache key using field key, value, and validation rules
|
||||||
|
const cacheKey = `${field.key}-${String(value)}-${JSON.stringify(field.validations)}`;
|
||||||
|
|
||||||
|
// Check cache first to avoid redundant validation
|
||||||
|
if (validationResultCache.has(cacheKey)) {
|
||||||
|
return validationResultCache.get(cacheKey) || [];
|
||||||
|
}
|
||||||
|
|
||||||
field.validations.forEach(validation => {
|
field.validations.forEach(validation => {
|
||||||
switch (validation.rule) {
|
switch (validation.rule) {
|
||||||
case 'required':
|
case 'required':
|
||||||
@@ -71,6 +113,9 @@ export const useValidation = <T extends string>(
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Store results in cache to speed up future validations
|
||||||
|
validationResultCache.set(cacheKey, errors);
|
||||||
|
|
||||||
return errors
|
return errors
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
@@ -223,83 +268,256 @@ export const useValidation = <T extends string>(
|
|||||||
return uniqueErrors;
|
return uniqueErrors;
|
||||||
}, [fields]);
|
}, [fields]);
|
||||||
|
|
||||||
// Run complete validation
|
// Additional function to explicitly validate uniqueness for specified fields
|
||||||
const validateData = useCallback(async (data: RowData<T>[]) => {
|
const validateUniqueField = useCallback((data: RowData<T>[], fieldKey: string) => {
|
||||||
// Step 1: Run field and row validation for each row
|
// Field keys that need special handling for uniqueness
|
||||||
|
const uniquenessFields = ['item_number', 'upc', 'barcode', 'supplier_no', 'notions_no'];
|
||||||
|
|
||||||
// Step 2: Run unique validations
|
// If the field doesn't need uniqueness validation, return empty errors
|
||||||
const uniqueValidations = validateUnique(data);
|
if (!uniquenessFields.includes(fieldKey)) {
|
||||||
|
const field = fields.find(f => String(f.key) === fieldKey);
|
||||||
|
if (!field || !field.validations?.some(v => v.rule === 'unique')) {
|
||||||
|
return new Map<number, Record<string, InfoWithSource>>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Step 3: Run table hook
|
// Create map to track errors
|
||||||
|
const uniqueErrors = new Map<number, Record<string, InfoWithSource>>();
|
||||||
|
|
||||||
// Create a map to store all validation errors
|
// Find the field definition
|
||||||
const validationErrors = new Map<number, Record<string, InfoWithSource>>();
|
const field = fields.find(f => String(f.key) === fieldKey);
|
||||||
|
if (!field) return uniqueErrors;
|
||||||
|
|
||||||
// Merge all validation results
|
// Get validation properties
|
||||||
data.forEach((row, index) => {
|
const validation = field.validations?.find(v => v.rule === 'unique');
|
||||||
// Collect errors from all validation sources
|
const allowEmpty = validation?.allowEmpty ?? false;
|
||||||
const rowErrors: Record<string, InfoWithSource> = {};
|
const errorMessage = validation?.errorMessage || `${field.label} must be unique`;
|
||||||
|
const level = validation?.level || 'error';
|
||||||
|
|
||||||
|
// Track values for uniqueness check
|
||||||
|
const valueMap = new Map<string, number[]>();
|
||||||
|
|
||||||
|
// Build value map
|
||||||
|
data.forEach((row, rowIndex) => {
|
||||||
|
const value = String(row[fieldKey as keyof typeof row] || '');
|
||||||
|
|
||||||
// Add field-level errors (we need to extract these from the validation process)
|
// Skip empty values if allowed
|
||||||
fields.forEach(field => {
|
if (allowEmpty && isEmpty(value)) {
|
||||||
const value = row[String(field.key) as keyof typeof row];
|
return;
|
||||||
const errors = validateField(value, field as Field<T>);
|
|
||||||
|
|
||||||
if (errors.length > 0) {
|
|
||||||
rowErrors[String(field.key)] = {
|
|
||||||
message: errors[0].message,
|
|
||||||
level: errors[0].level,
|
|
||||||
source: ErrorSources.Row,
|
|
||||||
type: errors[0].type
|
|
||||||
};
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add unique validation errors
|
|
||||||
if (uniqueValidations.has(index)) {
|
|
||||||
Object.entries(uniqueValidations.get(index) || {}).forEach(([key, error]) => {
|
|
||||||
rowErrors[key] = error;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filter out "required" errors for fields that have values
|
if (!valueMap.has(value)) {
|
||||||
const filteredErrors: Record<string, InfoWithSource> = {};
|
valueMap.set(value, [rowIndex]);
|
||||||
|
} else {
|
||||||
Object.entries(rowErrors).forEach(([key, error]) => {
|
valueMap.get(value)?.push(rowIndex);
|
||||||
const fieldValue = row[key as keyof typeof row];
|
|
||||||
|
|
||||||
// If the field has a value and the error is of type Required, skip it
|
|
||||||
if (!isEmpty(fieldValue) &&
|
|
||||||
error &&
|
|
||||||
typeof error === 'object' &&
|
|
||||||
'type' in error &&
|
|
||||||
error.type === ErrorType.Required) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
filteredErrors[key] = error;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Only add to the map if there are errors
|
|
||||||
if (Object.keys(filteredErrors).length > 0) {
|
|
||||||
validationErrors.set(index, filteredErrors);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Add errors for duplicate values
|
||||||
|
valueMap.forEach((rowIndexes, value) => {
|
||||||
|
if (rowIndexes.length > 1) {
|
||||||
|
// Skip empty values
|
||||||
|
if (!value || value.trim() === '') return;
|
||||||
|
|
||||||
|
// Add error to all duplicate rows
|
||||||
|
rowIndexes.forEach(rowIndex => {
|
||||||
|
// Create errors object if needed
|
||||||
|
if (!uniqueErrors.has(rowIndex)) {
|
||||||
|
uniqueErrors.set(rowIndex, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add error for this field
|
||||||
|
uniqueErrors.get(rowIndex)![fieldKey] = {
|
||||||
|
message: errorMessage,
|
||||||
|
level: level as 'info' | 'warning' | 'error',
|
||||||
|
source: ErrorSources.Table,
|
||||||
|
type: ErrorType.Unique
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return uniqueErrors;
|
||||||
|
}, [fields]);
|
||||||
|
|
||||||
|
// Run complete validation
|
||||||
|
const validateData = useCallback(async (data: RowData<T>[], fieldToUpdate?: { rowIndex: number, fieldKey: string }) => {
|
||||||
|
const validationErrors = new Map<number, Record<string, InfoWithSource>>();
|
||||||
|
|
||||||
|
// If we're updating a specific field, only validate that field for that row
|
||||||
|
if (fieldToUpdate) {
|
||||||
|
const { rowIndex, fieldKey } = fieldToUpdate;
|
||||||
|
|
||||||
|
// Special handling for fields that often update item_number
|
||||||
|
const triggersItemNumberValidation = fieldKey === 'upc' || fieldKey === 'barcode' || fieldKey === 'supplier';
|
||||||
|
|
||||||
|
// If updating a uniqueness field or field that affects item_number, clear ALL related validation caches
|
||||||
|
const isUniqueField = fieldKey === 'upc' || fieldKey === 'item_number' ||
|
||||||
|
fieldKey === 'supplier_no' || fieldKey === 'notions_no' ||
|
||||||
|
fieldKey === 'name' || triggersItemNumberValidation;
|
||||||
|
|
||||||
|
// Force cache clearing for uniqueness-validated fields to ensure fresh validation
|
||||||
|
if (isUniqueField) {
|
||||||
|
console.log(`Clearing validation cache for uniqueness field: ${fieldKey}`);
|
||||||
|
clearValidationCacheForField(fieldKey, null);
|
||||||
|
|
||||||
|
// If a field that might affect item_number, also clear item_number cache
|
||||||
|
if (triggersItemNumberValidation) {
|
||||||
|
console.log('Also clearing item_number validation cache');
|
||||||
|
clearValidationCacheForField('item_number', null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rowIndex >= 0 && rowIndex < data.length) {
|
||||||
|
const row = data[rowIndex];
|
||||||
|
|
||||||
|
// Find the field definition
|
||||||
|
const field = fields.find(f => String(f.key) === fieldKey);
|
||||||
|
|
||||||
|
if (field) {
|
||||||
|
// Validate just this field for this row
|
||||||
|
const value = row[fieldKey as keyof typeof row];
|
||||||
|
const errors = validateField(value, field as Field<T>);
|
||||||
|
|
||||||
|
if (errors.length > 0) {
|
||||||
|
// Store the validation error
|
||||||
|
validationErrors.set(rowIndex, {
|
||||||
|
[fieldKey]: {
|
||||||
|
message: errors[0].message,
|
||||||
|
level: errors[0].level as 'info' | 'warning' | 'error',
|
||||||
|
source: ErrorSources.Row,
|
||||||
|
type: errors[0].type
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the field requires uniqueness validation or if it's item_number after UPC/Supplier change
|
||||||
|
const needsUniquenessCheck = isUniqueField ||
|
||||||
|
field.validations?.some(v => v.rule === 'unique');
|
||||||
|
|
||||||
|
if (needsUniquenessCheck) {
|
||||||
|
console.log(`Running immediate uniqueness validation for field ${fieldKey}`);
|
||||||
|
|
||||||
|
// For item_number updated via UPC validation, or direct UPC update, check both fields
|
||||||
|
if (fieldKey === 'item_number' || fieldKey === 'upc' || fieldKey === 'barcode') {
|
||||||
|
// Validate both item_number and UPC/barcode fields for uniqueness
|
||||||
|
const itemNumberUniqueErrors = validateUniqueField(data, 'item_number');
|
||||||
|
const upcUniqueErrors = validateUniqueField(data, fieldKey === 'item_number' ? 'upc' : fieldKey);
|
||||||
|
|
||||||
|
// Combine the errors
|
||||||
|
itemNumberUniqueErrors.forEach((errors, rowIdx) => {
|
||||||
|
if (!validationErrors.has(rowIdx)) {
|
||||||
|
validationErrors.set(rowIdx, {});
|
||||||
|
}
|
||||||
|
Object.assign(validationErrors.get(rowIdx)!, errors);
|
||||||
|
});
|
||||||
|
|
||||||
|
upcUniqueErrors.forEach((errors, rowIdx) => {
|
||||||
|
if (!validationErrors.has(rowIdx)) {
|
||||||
|
validationErrors.set(rowIdx, {});
|
||||||
|
}
|
||||||
|
Object.assign(validationErrors.get(rowIdx)!, errors);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Normal uniqueness validation for other fields
|
||||||
|
const uniqueErrors = validateUniqueField(data, fieldKey);
|
||||||
|
|
||||||
|
// Add unique errors to validation errors
|
||||||
|
uniqueErrors.forEach((errors, rowIdx) => {
|
||||||
|
if (!validationErrors.has(rowIdx)) {
|
||||||
|
validationErrors.set(rowIdx, {});
|
||||||
|
}
|
||||||
|
Object.assign(validationErrors.get(rowIdx)!, errors);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Full validation - all fields for all rows
|
||||||
|
console.log('Running full validation for all fields and rows');
|
||||||
|
|
||||||
|
// Clear validation cache for full validation
|
||||||
|
validationResultCache.clear();
|
||||||
|
|
||||||
|
// Process each row for field-level validations
|
||||||
|
for (let rowIndex = 0; rowIndex < data.length; rowIndex++) {
|
||||||
|
const row = data[rowIndex];
|
||||||
|
let rowErrors: Record<string, InfoWithSource> = {};
|
||||||
|
|
||||||
|
// Validate all fields for this row
|
||||||
|
fields.forEach(field => {
|
||||||
|
const fieldKey = String(field.key);
|
||||||
|
const value = row[fieldKey as keyof typeof row];
|
||||||
|
const errors = validateField(value, field as Field<T>);
|
||||||
|
|
||||||
|
if (errors.length > 0) {
|
||||||
|
rowErrors[fieldKey] = {
|
||||||
|
message: errors[0].message,
|
||||||
|
level: errors[0].level as 'info' | 'warning' | 'error',
|
||||||
|
source: ErrorSources.Row,
|
||||||
|
type: errors[0].type
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add row to validationErrors if it has any errors
|
||||||
|
if (Object.keys(rowErrors).length > 0) {
|
||||||
|
validationErrors.set(rowIndex, rowErrors);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get fields requiring uniqueness validation
|
||||||
|
const uniqueFields = fields.filter(field =>
|
||||||
|
field.validations?.some(v => v.rule === 'unique')
|
||||||
|
);
|
||||||
|
|
||||||
|
// Also add standard unique fields that might not be explicitly marked as unique
|
||||||
|
const standardUniqueFields = ['item_number', 'upc', 'barcode', 'supplier_no', 'notions_no'];
|
||||||
|
|
||||||
|
// Combine all fields that need uniqueness validation
|
||||||
|
const allUniqueFieldKeys = new Set([
|
||||||
|
...uniqueFields.map(field => String(field.key)),
|
||||||
|
...standardUniqueFields
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Log uniqueness validation fields
|
||||||
|
console.log('Validating unique fields:', Array.from(allUniqueFieldKeys));
|
||||||
|
|
||||||
|
// Run uniqueness validation for each unique field
|
||||||
|
allUniqueFieldKeys.forEach(fieldKey => {
|
||||||
|
// Check if this field exists in the data
|
||||||
|
const hasField = data.some(row => fieldKey in row);
|
||||||
|
if (!hasField) return;
|
||||||
|
|
||||||
|
const uniqueErrors = validateUniqueField(data, fieldKey);
|
||||||
|
|
||||||
|
// Add unique errors to validation errors
|
||||||
|
uniqueErrors.forEach((errors, rowIdx) => {
|
||||||
|
if (!validationErrors.has(rowIdx)) {
|
||||||
|
validationErrors.set(rowIdx, {});
|
||||||
|
}
|
||||||
|
Object.assign(validationErrors.get(rowIdx)!, errors);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('Uniqueness validation complete');
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
data: data.map((row) => {
|
data,
|
||||||
// Return the original data without __errors
|
|
||||||
return { ...row };
|
|
||||||
}),
|
|
||||||
validationErrors
|
validationErrors
|
||||||
};
|
};
|
||||||
}, [validateRow, validateUnique, validateTable, fields, validateField]);
|
}, [fields, validateField, validateUniqueField]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
validateData,
|
validateData,
|
||||||
validateField,
|
validateField,
|
||||||
validateRow,
|
validateRow,
|
||||||
validateTable,
|
validateTable,
|
||||||
validateUnique
|
validateUnique,
|
||||||
|
validateUniqueField,
|
||||||
|
clearValidationCacheForField,
|
||||||
|
clearAllUniquenessCaches
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user