diff --git a/docs/validation-hook-refactor.md b/docs/validation-hook-refactor.md new file mode 100644 index 0000000..6895166 --- /dev/null +++ b/docs/validation-hook-refactor.md @@ -0,0 +1,131 @@ + + +# Refactoring Plan for Validation Code + +## Current Structure Analysis +- **useValidationState.tsx**: ~1650 lines - Core validation state management +- **useValidation.tsx**: ~425 lines - Field/data validation utility +- **useUpcValidation.tsx**: ~410 lines - UPC-specific validation + +## Proposed New Structure + +### 1. Core Types & Utilities (150-200 lines) +**File: `validation/types.ts`** +- All interfaces and types (RowData, ValidationError, FilterState, Template, etc.) +- Shared utility functions (isEmpty, getCellKey, etc.) + +**File: `validation/utils.ts`** +- Generic validation utility functions +- Caching mechanism and cache clearing helpers +- API URL helpers + +### 2. Field Validation (300-350 lines) +**File: `validation/hooks/useFieldValidation.ts`** +- `validateField` function +- Field-level validation logic +- Required, regex, and other field validations + +### 3. Uniqueness Validation (250-300 lines) +**File: `validation/hooks/useUniquenessValidation.ts`** +- `validateUniqueField` function +- `validateUniqueItemNumbers` function +- All uniqueness checking logic + +### 4. UPC Validation (300-350 lines) +**File: `validation/hooks/useUpcValidation.ts`** +- `fetchProductByUpc` function +- `validateUpc` function +- `applyItemNumbersToData` function +- UPC validation state management + +### 5. Validation Status Management (300-350 lines) +**File: `validation/hooks/useValidationStatus.ts`** +- Error state management +- Row validation status tracking +- Validation indicators and refs +- Batch validation processing + +### 6. Data Management (300-350 lines) +**File: `validation/hooks/useValidationData.ts`** +- Data state management +- Row updates +- Data filtering +- Initial data processing + +### 7. Template Management (250-300 lines) +**File: `validation/hooks/useTemplateManagement.ts`** +- Template saving +- Template application +- Template loading +- Template display helpers + +### 8. Main Validation Hook (300-350 lines) +**File: `validation/hooks/useValidation.ts`** +- Main hook that composes all other hooks +- Public API export +- Initialization logic +- Core validation flow + +## Function Distribution + +### Core Types & Utilities +- All interfaces (InfoWithSource, ValidationState, etc.) +- `isEmpty` utility +- `getApiUrl` helper + +### Field Validation +- `validateField` +- `validateRow` +- `validateData` (partial) +- All validation result caching + +### Uniqueness Validation +- `validateUniqueField` +- `validateUniqueItemNumbers` +- Uniqueness caching mechanisms + +### UPC Validation +- `fetchProductByUpc` +- `validateUpc` +- `validateAllUPCs` +- `applyItemNumbersToData` +- UPC validation state tracking (cells, rows) + +### Validation Status Management +- `startValidatingCell`/`stopValidatingCell` +- `startValidatingRow`/`stopValidatingRow` +- `isValidatingCell`/`isRowValidatingUpc` +- Error state management +- `revalidateRows` + +### Data Management +- Initial data cleaning/processing +- `updateRow` +- `copyDown` +- Search/filter functionality +- `filteredData` calculation + +### Template Management +- `saveTemplate` +- `applyTemplate` +- `applyTemplateToSelected` +- `getTemplateDisplayText` +- `loadTemplates`/`refreshTemplates` + +### Main Validation Hook +- Composition of all other hooks +- Initialization logic +- Button/navigation handling +- Field options management + +## Implementation Approach + +1. **Start with Types**: Create the types file first, as all other files will depend on it +2. **Create Utility Functions**: Move shared utilities next +3. **Build Core Validation**: Extract the field validation and uniqueness validation +4. **Separate UPC Logic**: Move all UPC-specific code to its own module +5. **Extract State Management**: Move data and status management to separate files +6. **Move Template Logic**: Extract template functionality +7. **Create Composition Hook**: Build the main hook that uses all other hooks + +This approach will give you more maintainable code with clearer separation of concerns, making it easier to understand, test, and modify each component independently. diff --git a/inventory/src/components/product-import/steps/ValidationStepNew/components/ValidationTable.tsx b/inventory/src/components/product-import/steps/ValidationStepNew/components/ValidationTable.tsx index 9f0be32..e1a1bff 100644 --- a/inventory/src/components/product-import/steps/ValidationStepNew/components/ValidationTable.tsx +++ b/inventory/src/components/product-import/steps/ValidationStepNew/components/ValidationTable.tsx @@ -15,7 +15,7 @@ import { Table, TableBody, TableRow, TableCell } from '@/components/ui/table' import { Checkbox } from '@/components/ui/checkbox' import { cn } from '@/lib/utils' import { Button } from '@/components/ui/button' -import { Loader2 } from 'lucide-react' +import { Skeleton } from '@/components/ui/skeleton' // Define a simple Error type locally to avoid import issues type ErrorType = { @@ -67,10 +67,9 @@ const MemoizedTemplateSelect = React.memo(({ }) => { if (isLoading) { return ( - +
+ +
); } diff --git a/inventory/src/components/product-import/steps/ValidationStepNew/hooks/useTemplates.tsx b/inventory/src/components/product-import/steps/ValidationStepNew/hooks/useTemplates.tsx deleted file mode 100644 index 5197d51..0000000 --- a/inventory/src/components/product-import/steps/ValidationStepNew/hooks/useTemplates.tsx +++ /dev/null @@ -1,263 +0,0 @@ -import { useState, useEffect, useCallback } from 'react' -import { RowSelectionState } from '@tanstack/react-table' -import { Template, RowData, getApiUrl } from './useValidationState' - -interface TemplateState { - selectedTemplateId: string | null - showSaveTemplateDialog: boolean - newTemplateName: string - newTemplateType: string -} - -export const useTemplates = ( - data: RowData[], - setData: React.Dispatch[]>>, - toast: any, - rowSelection: RowSelectionState -) => { - const [templates, setTemplates] = useState([]) - const [templateState, setTemplateState] = useState({ - selectedTemplateId: null, - showSaveTemplateDialog: false, - newTemplateName: '', - newTemplateType: '', - }) - - // Load templates from API - const loadTemplates = useCallback(async () => { - try { - console.log('Fetching templates...'); - const response = await fetch(`${getApiUrl()}/templates`) - console.log('Templates response status:', response.status); - - if (!response.ok) throw new Error('Failed to fetch templates') - - const templateData = await response.json() - console.log('Templates fetched successfully:', templateData); - - // Validate template data - const validTemplates = templateData.filter((t: any) => - t && typeof t === 'object' && t.id && t.company && t.product_type - ); - - if (validTemplates.length !== templateData.length) { - console.warn('Some templates were filtered out due to invalid data', { - original: templateData.length, - valid: validTemplates.length - }); - } - - setTemplates(validTemplates) - } catch (error) { - console.error('Error loading templates:', error) - toast({ - title: 'Error', - description: 'Failed to load templates', - }) - } - }, [toast]) - - // Save a new template based on selected rows - const saveTemplate = useCallback(async (name: string, type: string) => { - try { - // Get selected rows - const selectedRows = Object.keys(rowSelection) - .map(index => data[parseInt(index)]) - .filter(Boolean) - - if (selectedRows.length === 0) { - toast({ - title: 'Error', - description: 'Please select at least one row to create a template', - }) - return - } - - // Create template based on selected rows - const template: Template = { - id: Date.now(), // Temporary ID, will be replaced by server - company: selectedRows[0].company as string || '', - product_type: type, - ...selectedRows[0], // Copy all fields from the first selected row - } - - // Remove metadata fields - delete (template as any).__meta - delete (template as any).__template - delete (template as any).__original - delete (template as any).__corrected - delete (template as any).__changes - - // Send to API - const response = await fetch(`${getApiUrl()}/templates`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - company: template.company, - product_type: type, - ...Object.fromEntries( - Object.entries(template).filter(([key]) => - !['company', 'product_type'].includes(key) - ) - ) - }), - }) - - if (!response.ok) { - throw new Error('Failed to save template') - } - - // Reload templates to get the server-generated ID - await loadTemplates() - - toast({ - title: 'Success', - description: `Template "${name}" saved successfully`, - }) - - // Reset dialog state - setTemplateState(prev => ({ - ...prev, - showSaveTemplateDialog: false, - newTemplateName: '', - newTemplateType: '', - })) - } catch (error) { - console.error('Error saving template:', error) - toast({ - title: 'Error', - description: 'Failed to save template', - }) - } - }, [data, rowSelection, toast, loadTemplates]) - - // Apply a template to selected rows - const applyTemplate = useCallback((templateId: string, rowIndexes: number[]) => { - const template = templates.find(t => t.id.toString() === templateId) - - if (!template) { - toast({ - title: 'Error', - description: 'Template not found', - }) - return - } - - setData(prevData => { - const newData = [...prevData] - - rowIndexes.forEach(index => { - if (index >= 0 && index < newData.length) { - // Create a new row with template values - const updatedRow = { ...newData[index] } - - // Apply template fields (excluding metadata and ID fields) - Object.entries(template).forEach(([key, value]) => { - if (!['id', 'company', 'product_type', 'created_at', 'updated_at'].includes(key)) { - // Handle numeric values that might be stored as strings - if (typeof value === 'string' && /^\d+(\.\d+)?$/.test(value)) { - // If it's a price field, add the dollar sign - if (['msrp', 'cost_each'].includes(key)) { - updatedRow[key as keyof typeof updatedRow] = `$${value}` as any; - } else { - updatedRow[key as keyof typeof updatedRow] = value as any; - } - } - // Special handling for array fields like categories and ship_restrictions - else if (key === 'categories' || key === 'ship_restrictions') { - if (Array.isArray(value)) { - updatedRow[key as keyof typeof updatedRow] = value as any; - } else if (typeof value === 'string') { - try { - // Try to parse as JSON if it's a JSON string - if (value.startsWith('[') && value.endsWith(']')) { - const parsed = JSON.parse(value); - updatedRow[key as keyof typeof updatedRow] = parsed as any; - } - // Otherwise, it might be a PostgreSQL array format like {val1,val2} - else if (value.startsWith('{') && value.endsWith('}')) { - const parsed = value.slice(1, -1).split(','); - updatedRow[key as keyof typeof updatedRow] = parsed as any; - } - // If it's a single value, wrap it in an array - else { - updatedRow[key as keyof typeof updatedRow] = [value] as any; - } - } catch (error) { - console.error(`Error parsing ${key}:`, error); - // If parsing fails, use as-is - updatedRow[key as keyof typeof updatedRow] = value as any; - } - } else { - updatedRow[key as keyof typeof updatedRow] = value as any; - } - } else { - updatedRow[key as keyof typeof updatedRow] = value as any; - } - } - }) - - // Mark the row as using this template - updatedRow.__template = templateId - - // Update the row in the data array - newData[index] = updatedRow - } - }) - - return newData - }) - - toast({ - title: 'Success', - description: `Template applied to ${rowIndexes.length} row(s)`, - }) - }, [templates, setData, toast]) - - // Get display text for a template - const getTemplateDisplayText = useCallback((templateId: string | null) => { - if (!templateId) return 'Select a template' - - const template = templates.find(t => t.id.toString() === templateId) - return template - ? `${template.company} - ${template.product_type}` - : 'Unknown template' - }, [templates]) - - // Load templates on component mount and set up refresh event listener - useEffect(() => { - loadTemplates() - - // Add event listener for template refresh - const handleRefreshTemplates = () => { - loadTemplates() - } - - window.addEventListener('refresh-templates', handleRefreshTemplates) - - // Clean up event listener - return () => { - window.removeEventListener('refresh-templates', handleRefreshTemplates) - } - }, [loadTemplates]) - - return { - templates, - selectedTemplateId: templateState.selectedTemplateId, - showSaveTemplateDialog: templateState.showSaveTemplateDialog, - newTemplateName: templateState.newTemplateName, - newTemplateType: templateState.newTemplateType, - setTemplateState, - loadTemplates, - saveTemplate, - applyTemplate, - getTemplateDisplayText, - // Helper method to apply to selected rows - applyTemplateToSelected: (templateId: string) => { - const selectedIndexes = Object.keys(rowSelection).map(i => parseInt(i)) - applyTemplate(templateId, selectedIndexes) - } - } -} \ No newline at end of file diff --git a/inventory/src/components/product-import/steps/ValidationStepNew/hooks/useUpcValidation.tsx b/inventory/src/components/product-import/steps/ValidationStepNew/hooks/useUpcValidation.tsx index a6866f9..f72cd7a 100644 --- a/inventory/src/components/product-import/steps/ValidationStepNew/hooks/useUpcValidation.tsx +++ b/inventory/src/components/product-import/steps/ValidationStepNew/hooks/useUpcValidation.tsx @@ -30,13 +30,6 @@ export const useUpcValidation = ( const processedUpcMapRef = useRef(new Map()); const initialUpcValidationDoneRef = useRef(false); - // For batch validation - const validationQueueRef = useRef>([]); - const isProcessingBatchRef = useRef(false); - - // For validation results - const [upcValidationResults] = useState>(new Map()); - // Helper to create cell key const getCellKey = (rowIndex: number, fieldKey: string) => `${rowIndex}-${fieldKey}`; @@ -249,102 +242,6 @@ export const useUpcValidation = ( } }, [setData]); - // Process validation queue in batches - faster processing with smaller batches - const processBatchValidation = useCallback(async () => { - if (isProcessingBatchRef.current) return; - if (validationQueueRef.current.length === 0) return; - - 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 { - // Process in small batches - for (let i = 0; i < queue.length; i += BATCH_SIZE) { - const batch = queue.slice(i, i + BATCH_SIZE); - - // Process batch in parallel - const results = await Promise.all(batch.map(async ({ rowIndex, supplierId, upcValue }) => { - try { - // 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 - } - - // Small delay between batches to allow UI to update - if (i + BATCH_SIZE < queue.length) { - await new Promise(resolve => setTimeout(resolve, 10)); - } - } - } catch (error) { - console.error('Error in batch processing:', error); - } finally { - isProcessingBatchRef.current = false; - - // Process any new items - if (validationQueueRef.current.length > 0) { - setTimeout(processBatchValidation, 0); - } - } - }, [fetchProductByUpc, updateItemNumber, stopValidatingRow, applyItemNumbersToData]); - - // For immediate processing - // Batch validate all UPCs in the data const validateAllUPCs = useCallback(async () => { // Skip if we've already done the initial validation @@ -508,9 +405,6 @@ export const useUpcValidation = ( getItemNumber, applyItemNumbersToData, - // Results - upcValidationResults, - // Initialization state initialValidationDone: initialUpcValidationDoneRef.current }; diff --git a/inventory/src/components/product-import/steps/ValidationStepNew/hooks/useValidation.tsx b/inventory/src/components/product-import/steps/ValidationStepNew/hooks/useValidation.tsx index 34fcde9..6cc336f 100644 --- a/inventory/src/components/product-import/steps/ValidationStepNew/hooks/useValidation.tsx +++ b/inventory/src/components/product-import/steps/ValidationStepNew/hooks/useValidation.tsx @@ -23,13 +23,15 @@ const isEmpty = (value: any): boolean => // Create a cache for validation results to avoid repeated validation of the same data const validationResultCache = new Map(); -const validationCache: Record = {}; // Add a function to clear cache for a specific field value export const clearValidationCacheForField = (fieldKey: string) => { - // Clear cache - const cacheKey = `field_${fieldKey}`; - delete validationCache[cacheKey]; + // Look for entries that match this field key + validationResultCache.forEach((_, key) => { + if (key.startsWith(`${fieldKey}-`)) { + validationResultCache.delete(key); + } + }); }; // Add a special function to clear all uniqueness validation caches @@ -170,96 +172,6 @@ export const useValidation = ( } }, [fields, validateField, rowHook]) - // Validate all data at the table level - const validateTable = useCallback(async (data: RowData[]): Promise => { - if (!tableHook) { - return data.map((row, index) => ({ - __index: row.__index || String(index) - })) - } - - try { - const tableResults = await tableHook(data) - - // Process table validation results - return tableResults.map((result, index) => { - return { - __index: result.__index || data[index].__index || String(index) - } - }) - } catch (error) { - console.error('Error in table hook:', error) - return data.map((row, index) => ({ - __index: row.__index || String(index) - })) - } - }, [tableHook]) - - // Validate unique fields across the table - const validateUnique = useCallback((data: RowData[]) => { - // Create a map to store errors for each row - const uniqueErrors = new Map>(); - - // Find fields with unique validation - const uniqueFields = fields.filter(field => - field.validations?.some(v => v.rule === 'unique') - ); - - if (uniqueFields.length === 0) { - return uniqueErrors; - } - - // Check each unique field - uniqueFields.forEach(field => { - const { key } = field; - const validation = field.validations?.find(v => v.rule === 'unique'); - const allowEmpty = validation?.allowEmpty ?? false; - const errorMessage = validation?.errorMessage || `${field.label} must be unique`; - const level = validation?.level || 'error'; - - // Track values for uniqueness check - const valueMap = new Map(); - - // Build value map - data.forEach((row, rowIndex) => { - const value = String(row[String(key) as keyof typeof row] || ''); - - // Skip empty values if allowed - if (allowEmpty && isEmpty(value)) { - return; - } - - if (!valueMap.has(value)) { - valueMap.set(value, [rowIndex]); - } else { - valueMap.get(value)?.push(rowIndex); - } - }); - - // Add errors for duplicate values - valueMap.forEach((rowIndexes) => { - if (rowIndexes.length > 1) { - // Add error to all duplicate rows - rowIndexes.forEach(rowIndex => { - // Get existing errors for this row or create a new object - const rowErrors = uniqueErrors.get(rowIndex) || {}; - - rowErrors[String(key)] = { - message: errorMessage, - level, - source: ErrorSources.Table, - type: ErrorType.Unique - }; - - uniqueErrors.set(rowIndex, rowErrors); - }); - } - }); - }); - - return uniqueErrors; - }, [fields]); - // Additional function to explicitly validate uniqueness for specified fields const validateUniqueField = useCallback((data: RowData[], fieldKey: string) => { // Field keys that need special handling for uniqueness @@ -506,8 +418,6 @@ export const useValidation = ( validateData, validateField, validateRow, - validateTable, - validateUnique, validateUniqueField, clearValidationCacheForField, clearAllUniquenessCaches diff --git a/inventory/src/components/product-import/steps/ValidationStepNew/hooks/useValidationState.tsx b/inventory/src/components/product-import/steps/ValidationStepNew/hooks/useValidationState.tsx index 9d2dc88..5e78396 100644 --- a/inventory/src/components/product-import/steps/ValidationStepNew/hooks/useValidationState.tsx +++ b/inventory/src/components/product-import/steps/ValidationStepNew/hooks/useValidationState.tsx @@ -1,29 +1,20 @@ -import { useState, useEffect, useCallback, useMemo, useRef } from 'react' -import { useRsi } from '../../../hooks/useRsi' -import type { Data, Field } from '../../../types' -import { ErrorSources, ErrorType, ValidationError } from '../../../types' -import { RowSelectionState } from '@tanstack/react-table' -import { toast } from 'sonner' +import { useState, useEffect, useCallback, useMemo, useRef } from "react"; +import { useRsi } from "../../../hooks/useRsi"; +import type { Data, Field } from "../../../types"; +import { ErrorSources, ErrorType, ValidationError } from "../../../types"; +import { RowSelectionState } from "@tanstack/react-table"; +import { toast } from "sonner"; import { useQuery } from "@tanstack/react-query"; import config from "@/config"; -import { useValidation } from './useValidation' - -// Helper function to check if a value is empty - -// Use the ValidationError type from types.ts instead of defining ErrorType here -// type ErrorType = { -// message: string; -// level: string; -// source?: string; -// } +import { useValidation } from "./useValidation"; // Define the Props interface for ValidationStepNew export interface Props { - initialData: RowData[] - file?: File - onBack?: () => void - onNext?: (data: RowData[]) => void - isFromScratch?: boolean + initialData: RowData[]; + file?: File; + onBack?: () => void; + onNext?: (data: RowData[]) => void; + isFromScratch?: boolean; } // Extended Data type with meta information @@ -39,7 +30,7 @@ export type RowData = Data & { company?: string; item_number?: string; [key: string]: any; // Allow any string key for dynamic fields -} +}; // Template interface export interface Template { @@ -82,467 +73,479 @@ declare global { // Use a helper to get API URL consistently export const getApiUrl = () => config.apiUrl; -// Add debounce utility -const DEBOUNCE_DELAY = 0; // No delay - -function debounce any>( - func: T, - wait: number -): (...args: Parameters) => void { - let timeout: NodeJS.Timeout; - return (...args: Parameters) => { - clearTimeout(timeout); - // Execute immediately if no delay - if (wait <= 0) { - func(...args); - } else { - timeout = setTimeout(() => func(...args), wait); - } - }; -} - // Main validation state hook export const useValidationState = ({ initialData, onBack, - onNext}: Props) => { + onNext, +}: Props) => { const { fields, rowHook, tableHook } = useRsi(); - + // Import validateData from useValidation at the beginning - const { validateField: validateFieldFromHook } = useValidation(fields, rowHook, tableHook); - + const { validateField: validateFieldFromHook } = useValidation( + fields, + rowHook, + tableHook + ); + // Add ref to track template application state const isApplyingTemplateRef = useRef(false); - + // Core data state const [data, setData] = useState[]>(() => { // Clean price fields in initial data before setting state - return initialData.map(row => { + return initialData.map((row) => { const updatedRow = { ...row } as Record; - + // Clean MSRP - if (typeof updatedRow.msrp === 'string') { - updatedRow.msrp = updatedRow.msrp.replace(/[$,]/g, ''); + if (typeof updatedRow.msrp === "string") { + updatedRow.msrp = updatedRow.msrp.replace(/[$,]/g, ""); const numValue = parseFloat(updatedRow.msrp); if (!isNaN(numValue)) { updatedRow.msrp = numValue.toFixed(2); } } - + // Clean cost_each - if (typeof updatedRow.cost_each === 'string') { - updatedRow.cost_each = updatedRow.cost_each.replace(/[$,]/g, ''); + if (typeof updatedRow.cost_each === "string") { + updatedRow.cost_each = updatedRow.cost_each.replace(/[$,]/g, ""); const numValue = parseFloat(updatedRow.cost_each); if (!isNaN(numValue)) { updatedRow.cost_each = numValue.toFixed(2); } } - + // Set default tax category if not already set - if (updatedRow.tax_cat === undefined || updatedRow.tax_cat === null || updatedRow.tax_cat === '') { - updatedRow.tax_cat = '0'; + if ( + updatedRow.tax_cat === undefined || + updatedRow.tax_cat === null || + updatedRow.tax_cat === "" + ) { + updatedRow.tax_cat = "0"; } - + // Set default shipping restrictions if not already set - if (updatedRow.ship_restrictions === undefined || updatedRow.ship_restrictions === null || updatedRow.ship_restrictions === '') { - updatedRow.ship_restrictions = '0'; + if ( + updatedRow.ship_restrictions === undefined || + updatedRow.ship_restrictions === null || + updatedRow.ship_restrictions === "" + ) { + updatedRow.ship_restrictions = "0"; } - + return updatedRow as RowData; }); - }) - - // Function to clean price fields in data - + }); + // Row selection state - const [rowSelection, setRowSelection] = useState({}) - + const [rowSelection, setRowSelection] = useState({}); + // Validation state - const [isValidating] = useState(false) - const [validationErrors, setValidationErrors] = useState>>(new Map()) - const [rowValidationStatus, setRowValidationStatus] = useState>(new Map()) - const [] = useState>(new Set()) - + const [isValidating] = useState(false); + const [validationErrors, setValidationErrors] = useState< + Map> + >(new Map()); + const [rowValidationStatus, setRowValidationStatus] = useState< + Map + >(new Map()); + // Template state - const [templates, setTemplates] = useState([]) - const [isLoadingTemplates, setIsLoadingTemplates] = useState(true) + const [templates, setTemplates] = useState([]); + const [isLoadingTemplates, setIsLoadingTemplates] = useState(true); const [templateState, setTemplateState] = useState({ selectedTemplateId: null as string | null, showSaveTemplateDialog: false, - newTemplateName: '', - newTemplateType: '', - }) - + newTemplateName: "", + newTemplateType: "", + }); + // Filter state const [filters, setFilters] = useState({ - searchText: '', + searchText: "", showErrorsOnly: false, filterField: null, - filterValue: null - }) - - // UPC validation state - const [validatingUpcRows, setValidatingUpcRows] = useState([]) - const [upcValidationResults, setUpcValidationResults] = useState>(new Map()) - - // Create a UPC cache to prevent duplicate API calls - const processedUpcMapRef = useRef(new Map()); + filterValue: null, + }); + const initialValidationDoneRef = useRef(false); - - // Add debounce timer ref for item number validation - - // Add batch update state - - // Optimized batch update function with single state update - - // Queue a row update to be processed in batch - - // Tracking rows that need validation - - // Helper function to validate a field value - now just an alias for validateFieldFromHook - const validateField = validateFieldFromHook; // Helper function to validate a field value - const fieldValidationHelper = useCallback((rowIndex: number, specificField?: string) => { - // Skip validation if row doesn't exist - if (rowIndex < 0 || rowIndex >= data.length) return; - - // Get the row data - const row = data[rowIndex]; - - // If validating a specific field, only check that field - if (specificField) { - const field = fields.find(f => String(f.key) === specificField); - if (field) { - const value = row[specificField as keyof typeof row]; - - // Use state setter instead of direct mutation - setValidationErrors(prev => { - const newErrors = new Map(prev); - const existingErrors = {...(newErrors.get(rowIndex) || {})}; - - // Quick check for required fields - this prevents flashing errors - const isRequired = field.validations?.some(v => v.rule === 'required'); - const isEmpty = value === undefined || value === null || value === '' || - (Array.isArray(value) && value.length === 0) || - (typeof value === 'object' && value !== null && Object.keys(value).length === 0); - - // For non-empty values, remove required errors immediately - if (isRequired && !isEmpty && existingErrors[specificField]) { - const nonRequiredErrors = existingErrors[specificField].filter(e => e.type !== ErrorType.Required); - if (nonRequiredErrors.length === 0) { - // If no other errors, remove the field entirely from errors - delete existingErrors[specificField]; - } else { - existingErrors[specificField] = nonRequiredErrors; + const fieldValidationHelper = useCallback( + (rowIndex: number, specificField?: string) => { + // Skip validation if row doesn't exist + if (rowIndex < 0 || rowIndex >= data.length) return; + + // Get the row data + const row = data[rowIndex]; + + // If validating a specific field, only check that field + if (specificField) { + const field = fields.find((f) => String(f.key) === specificField); + if (field) { + const value = row[specificField as keyof typeof row]; + + // Use state setter instead of direct mutation + setValidationErrors((prev) => { + const newErrors = new Map(prev); + const existingErrors = { ...(newErrors.get(rowIndex) || {}) }; + + // Quick check for required fields - this prevents flashing errors + const isRequired = field.validations?.some( + (v) => v.rule === "required" + ); + const isEmpty = + value === undefined || + value === null || + value === "" || + (Array.isArray(value) && value.length === 0) || + (typeof value === "object" && + value !== null && + Object.keys(value).length === 0); + + // For non-empty values, remove required errors immediately + if (isRequired && !isEmpty && existingErrors[specificField]) { + const nonRequiredErrors = existingErrors[specificField].filter( + (e) => e.type !== ErrorType.Required + ); + if (nonRequiredErrors.length === 0) { + // If no other errors, remove the field entirely from errors + delete existingErrors[specificField]; + } else { + existingErrors[specificField] = nonRequiredErrors; + } } - } - - // Run full validation for the field - const errors = validateField(value, field as unknown as Field); - - // Update validation errors for this field - if (errors.length > 0) { - existingErrors[specificField] = errors; - } else { - delete existingErrors[specificField]; - } - - // Update validation errors map - if (Object.keys(existingErrors).length > 0) { - newErrors.set(rowIndex, existingErrors); - } else { - newErrors.delete(rowIndex); - } - - return newErrors; - }); - } - } else { - // Validate all fields in the row - setValidationErrors(prev => { - const newErrors = new Map(prev); - const rowErrors: Record = {}; - - fields.forEach(field => { - const fieldKey = String(field.key); - const value = row[fieldKey as keyof typeof row]; - const errors = validateField(value, field as unknown as Field); - - if (errors.length > 0) { - rowErrors[fieldKey] = errors; - } - }); - - // Update validation errors map - if (Object.keys(rowErrors).length > 0) { - newErrors.set(rowIndex, rowErrors); - } else { - newErrors.delete(rowIndex); - } - - return newErrors; - }); - } - }, [data, fields, validateField, setValidationErrors]); - // Use validateRow as an alias for fieldValidationHelper for compatibility - const validateRow = fieldValidationHelper; + // Run full validation for the field + const errors = validateFieldFromHook(value, field as unknown as Field); - // Smart markRowForRevalidation function that validates instead of just clearing errors - - // Modified updateRow function that properly handles field-specific validation - const updateRow = useCallback((rowIndex: number, key: T, value: any) => { - // Process value before updating data - let processedValue = value; - - // Strip dollar signs from price fields - if ((key === 'msrp' || key === 'cost_each') && typeof value === 'string') { - processedValue = value.replace(/[$,]/g, ''); - - // Also ensure it's a valid number - const numValue = parseFloat(processedValue); - if (!isNaN(numValue)) { - processedValue = numValue.toFixed(2); - } - } - - // Find the row data first - const rowData = data[rowIndex]; - if (!rowData) { - console.error(`No row data found for index ${rowIndex}`); - return; - } - - // Create a copy of the row to avoid mutation - const updatedRow = { ...rowData, [key]: processedValue }; - - // Update the data immediately - this sets the value - setData(prevData => { - const newData = [...prevData]; - if (rowIndex >= 0 && rowIndex < newData.length) { - newData[rowIndex] = updatedRow; - } - return newData; - }); - - // Find the field definition - const field = fields.find(f => String(f.key) === key); - if (!field) return; - - // CRITICAL FIX: Combine both validation operations into a single state update - // to prevent intermediate rendering that causes error icon flashing - setValidationErrors(prev => { - const newMap = new Map(prev); - const existingErrors = newMap.get(rowIndex) || {}; - const newRowErrors = { ...existingErrors }; - - // Check for required field first - const isRequired = field.validations?.some(v => v.rule === 'required'); - const isEmpty = processedValue === undefined || processedValue === null || processedValue === '' || - (Array.isArray(processedValue) && processedValue.length === 0) || - (typeof processedValue === 'object' && processedValue !== null && Object.keys(processedValue).length === 0); - - // For required fields with values, remove required errors - if (isRequired && !isEmpty && newRowErrors[key as string]) { - const hasRequiredError = newRowErrors[key as string].some(e => e.type === ErrorType.Required); - - if (hasRequiredError) { - // Remove required errors but keep other types of errors - const nonRequiredErrors = newRowErrors[key as string].filter(e => e.type !== ErrorType.Required); - - if (nonRequiredErrors.length === 0) { - // If no other errors, delete the field's errors entirely - delete newRowErrors[key as string]; - } else { - // Otherwise keep non-required errors - newRowErrors[key as string] = nonRequiredErrors; - } - } - } - - // Now run full validation for the field (except for required which we already handled) - const errors = validateField(processedValue, field as unknown as Field) - .filter(e => e.type !== ErrorType.Required || isEmpty); - - // Update with new validation results - if (errors.length > 0) { - newRowErrors[key as string] = errors; - } else if (!newRowErrors[key as string]) { - // If no errors found and no existing errors, ensure field is removed from errors - delete newRowErrors[key as string]; - } - - // Update the map - if (Object.keys(newRowErrors).length > 0) { - newMap.set(rowIndex, newRowErrors); - } else { - newMap.delete(rowIndex); - } - - return newMap; - }); - - // Handle simple secondary effects here - setTimeout(() => { - // Use __index to find the actual row in the full data array - const rowId = rowData.__index; - - // Handle company change - clear line/subline - if (key === 'company' && processedValue) { - // Clear any existing line/subline values - setData(prevData => { - const newData = [...prevData]; - const idx = newData.findIndex(item => item.__index === rowId); - if (idx >= 0) { - newData[idx] = { - ...newData[idx], - line: undefined, - subline: undefined - }; - } - return newData; - }); - } - - // Handle line change - clear subline - if (key === 'line' && processedValue) { - // Clear any existing subline value - setData(prevData => { - const newData = [...prevData]; - const idx = newData.findIndex(item => item.__index === rowId); - if (idx >= 0) { - newData[idx] = { - ...newData[idx], - subline: undefined - }; - } - return newData; - }); - } - }, 50); - }, [data, fields, validateField, setData, setValidationErrors]); - - // Improved revalidateRows function - const revalidateRows = useCallback(async (rowIndexes: number[], updatedFields?: { [rowIndex: number]: string[] }) => { - // Process all specified rows using a single state update to avoid race conditions - setValidationErrors(prev => { - const newErrors = new Map(prev); - - // Process each row - for (const rowIndex of rowIndexes) { - if (rowIndex < 0 || rowIndex >= data.length) continue; - - const row = data[rowIndex]; - if (!row) continue; - - // If we have specific fields to update for this row - const fieldsToValidate = updatedFields?.[rowIndex] || []; - - if (fieldsToValidate.length > 0) { - // Get existing errors for this row - const existingRowErrors = { ...(newErrors.get(rowIndex) || {}) }; - - // Validate each specified field - for (const fieldKey of fieldsToValidate) { - const field = fields.find(f => String(f.key) === fieldKey); - if (!field) continue; - - const value = row[fieldKey as keyof typeof row]; - - // Check if this is a required field with a value - - // Run validation for this field - const errors = validateField(value, field as unknown as Field); - - // Update errors for this field + // Update validation errors for this field if (errors.length > 0) { - existingRowErrors[fieldKey] = errors; + existingErrors[specificField] = errors; } else { - delete existingRowErrors[fieldKey]; + delete existingErrors[specificField]; } - } - - // Update the row's errors - if (Object.keys(existingRowErrors).length > 0) { - newErrors.set(rowIndex, existingRowErrors); - } else { - newErrors.delete(rowIndex); - } - } else { - // No specific fields provided - validate the entire row + + // Update validation errors map + if (Object.keys(existingErrors).length > 0) { + newErrors.set(rowIndex, existingErrors); + } else { + newErrors.delete(rowIndex); + } + + return newErrors; + }); + } + } else { + // Validate all fields in the row + setValidationErrors((prev) => { + const newErrors = new Map(prev); const rowErrors: Record = {}; - - // Validate all fields in the row - for (const field of fields) { + + fields.forEach((field) => { const fieldKey = String(field.key); const value = row[fieldKey as keyof typeof row]; - - // Run validation for this field - const errors = validateField(value, field as unknown as Field); - - // Update errors for this field + const errors = validateFieldFromHook(value, field as unknown as Field); + if (errors.length > 0) { rowErrors[fieldKey] = errors; } - } - - // Update the row's errors + }); + + // Update validation errors map if (Object.keys(rowErrors).length > 0) { newErrors.set(rowIndex, rowErrors); } else { newErrors.delete(rowIndex); } + + return newErrors; + }); + } + }, + [data, fields, validateFieldFromHook, setValidationErrors] + ); + + // Use validateRow as an alias for fieldValidationHelper for compatibility + const validateRow = fieldValidationHelper; + + // Modified updateRow function that properly handles field-specific validation + const updateRow = useCallback( + (rowIndex: number, key: T, value: any) => { + // Process value before updating data + let processedValue = value; + + // Strip dollar signs from price fields + if ( + (key === "msrp" || key === "cost_each") && + typeof value === "string" + ) { + processedValue = value.replace(/[$,]/g, ""); + + // Also ensure it's a valid number + const numValue = parseFloat(processedValue); + if (!isNaN(numValue)) { + processedValue = numValue.toFixed(2); } } - - return newErrors; - }); - }, [data, fields, validateField]); + + // Find the row data first + const rowData = data[rowIndex]; + if (!rowData) { + console.error(`No row data found for index ${rowIndex}`); + return; + } + + // Create a copy of the row to avoid mutation + const updatedRow = { ...rowData, [key]: processedValue }; + + // Update the data immediately - this sets the value + setData((prevData) => { + const newData = [...prevData]; + if (rowIndex >= 0 && rowIndex < newData.length) { + newData[rowIndex] = updatedRow; + } + return newData; + }); + + // Find the field definition + const field = fields.find((f) => String(f.key) === key); + if (!field) return; + + // CRITICAL FIX: Combine both validation operations into a single state update + // to prevent intermediate rendering that causes error icon flashing + setValidationErrors((prev) => { + const newMap = new Map(prev); + const existingErrors = newMap.get(rowIndex) || {}; + const newRowErrors = { ...existingErrors }; + + // Check for required field first + const isRequired = field.validations?.some( + (v) => v.rule === "required" + ); + const isEmpty = + processedValue === undefined || + processedValue === null || + processedValue === "" || + (Array.isArray(processedValue) && processedValue.length === 0) || + (typeof processedValue === "object" && + processedValue !== null && + Object.keys(processedValue).length === 0); + + // For required fields with values, remove required errors + if (isRequired && !isEmpty && newRowErrors[key as string]) { + const hasRequiredError = newRowErrors[key as string].some( + (e) => e.type === ErrorType.Required + ); + + if (hasRequiredError) { + // Remove required errors but keep other types of errors + const nonRequiredErrors = newRowErrors[key as string].filter( + (e) => e.type !== ErrorType.Required + ); + + if (nonRequiredErrors.length === 0) { + // If no other errors, delete the field's errors entirely + delete newRowErrors[key as string]; + } else { + // Otherwise keep non-required errors + newRowErrors[key as string] = nonRequiredErrors; + } + } + } + + // Now run full validation for the field (except for required which we already handled) + const errors = validateFieldFromHook( + processedValue, + field as unknown as Field + ).filter((e) => e.type !== ErrorType.Required || isEmpty); + + // Update with new validation results + if (errors.length > 0) { + newRowErrors[key as string] = errors; + } else if (!newRowErrors[key as string]) { + // If no errors found and no existing errors, ensure field is removed from errors + delete newRowErrors[key as string]; + } + + // Update the map + if (Object.keys(newRowErrors).length > 0) { + newMap.set(rowIndex, newRowErrors); + } else { + newMap.delete(rowIndex); + } + + return newMap; + }); + + // Handle simple secondary effects here + setTimeout(() => { + // Use __index to find the actual row in the full data array + const rowId = rowData.__index; + + // Handle company change - clear line/subline + if (key === "company" && processedValue) { + // Clear any existing line/subline values + setData((prevData) => { + const newData = [...prevData]; + const idx = newData.findIndex((item) => item.__index === rowId); + if (idx >= 0) { + newData[idx] = { + ...newData[idx], + line: undefined, + subline: undefined, + }; + } + return newData; + }); + } + + // Handle line change - clear subline + if (key === "line" && processedValue) { + // Clear any existing subline value + setData((prevData) => { + const newData = [...prevData]; + const idx = newData.findIndex((item) => item.__index === rowId); + if (idx >= 0) { + newData[idx] = { + ...newData[idx], + subline: undefined, + }; + } + return newData; + }); + } + }, 50); + }, + [data, fields, validateFieldFromHook, setData, setValidationErrors] + ); + + // Improved revalidateRows function + const revalidateRows = useCallback( + async ( + rowIndexes: number[], + updatedFields?: { [rowIndex: number]: string[] } + ) => { + // Process all specified rows using a single state update to avoid race conditions + setValidationErrors((prev) => { + const newErrors = new Map(prev); + + // Process each row + for (const rowIndex of rowIndexes) { + if (rowIndex < 0 || rowIndex >= data.length) continue; + + const row = data[rowIndex]; + if (!row) continue; + + // If we have specific fields to update for this row + const fieldsToValidate = updatedFields?.[rowIndex] || []; + + if (fieldsToValidate.length > 0) { + // Get existing errors for this row + const existingRowErrors = { ...(newErrors.get(rowIndex) || {}) }; + + // Validate each specified field + for (const fieldKey of fieldsToValidate) { + const field = fields.find((f) => String(f.key) === fieldKey); + if (!field) continue; + + const value = row[fieldKey as keyof typeof row]; + + // Run validation for this field + const errors = validateFieldFromHook(value, field as unknown as Field); + + // Update errors for this field + if (errors.length > 0) { + existingRowErrors[fieldKey] = errors; + } else { + delete existingRowErrors[fieldKey]; + } + } + + // Update the row's errors + if (Object.keys(existingRowErrors).length > 0) { + newErrors.set(rowIndex, existingRowErrors); + } else { + newErrors.delete(rowIndex); + } + } else { + // No specific fields provided - validate the entire row + const rowErrors: Record = {}; + + // Validate all fields in the row + for (const field of fields) { + const fieldKey = String(field.key); + const value = row[fieldKey as keyof typeof row]; + + // Run validation for this field + const errors = validateFieldFromHook(value, field as unknown as Field); + + // Update errors for this field + if (errors.length > 0) { + rowErrors[fieldKey] = errors; + } + } + + // Update the row's errors + if (Object.keys(rowErrors).length > 0) { + newErrors.set(rowIndex, rowErrors); + } else { + newErrors.delete(rowIndex); + } + } + } + + return newErrors; + }); + }, + [data, fields, validateFieldFromHook] + ); // Update validateUniqueItemNumbers to also check for uniqueness of UPC/barcode const validateUniqueItemNumbers = useCallback(async () => { - console.log('Validating unique fields'); - + console.log("Validating unique fields"); + // Skip if no data if (!data.length) return; - + // Track unique identifiers in maps const uniqueFieldsMap = new Map>(); - + // Find fields that need uniqueness validation - const uniqueFields = fields.filter(field => - field.validations?.some(v => v.rule === 'unique') - ).map(field => String(field.key)); - - console.log(`Found ${uniqueFields.length} fields requiring uniqueness validation:`, uniqueFields); - + const uniqueFields = fields + .filter((field) => field.validations?.some((v) => v.rule === "unique")) + .map((field) => String(field.key)); + + console.log( + `Found ${uniqueFields.length} fields requiring uniqueness validation:`, + uniqueFields + ); + // Always check item_number uniqueness even if not explicitly defined - if (!uniqueFields.includes('item_number')) { - uniqueFields.push('item_number'); + if (!uniqueFields.includes("item_number")) { + uniqueFields.push("item_number"); } - + // Initialize maps for each unique field - uniqueFields.forEach(fieldKey => { + uniqueFields.forEach((fieldKey) => { uniqueFieldsMap.set(fieldKey, new Map()); }); - + // Initialize batch updates const errors = new Map>(); - + // Single pass through data to identify all unique values data.forEach((row, index) => { - uniqueFields.forEach(fieldKey => { + uniqueFields.forEach((fieldKey) => { const value = row[fieldKey as keyof typeof row]; - + // Skip empty values - if (value === undefined || value === null || value === '') { + if (value === undefined || value === null || value === "") { return; } - + const valueStr = String(value); const fieldMap = uniqueFieldsMap.get(fieldKey); - + if (fieldMap) { // Get or initialize the array of indices for this value const indices = fieldMap.get(valueStr) || []; @@ -551,28 +554,31 @@ export const useValidationState = ({ } }); }); - + // Process duplicates - uniqueFields.forEach(fieldKey => { + uniqueFields.forEach((fieldKey) => { const fieldMap = uniqueFieldsMap.get(fieldKey); if (!fieldMap) return; - + fieldMap.forEach((indices, value) => { // Only process if there are duplicates if (indices.length > 1) { // Get the validation rule for this field - const field = fields.find(f => String(f.key) === fieldKey); - const validationRule = field?.validations?.find(v => v.rule === 'unique'); - + const field = fields.find((f) => String(f.key) === fieldKey); + const validationRule = field?.validations?.find( + (v) => v.rule === "unique" + ); + const errorObj = { - message: validationRule?.errorMessage || `Duplicate ${fieldKey}: ${value}`, - level: validationRule?.level || 'error' as 'error', + message: + validationRule?.errorMessage || `Duplicate ${fieldKey}: ${value}`, + level: validationRule?.level || ("error" as "error"), source: ErrorSources.Table, - type: ErrorType.Unique + type: ErrorType.Unique, }; - + // Add error to each row with this value - indices.forEach(rowIndex => { + indices.forEach((rowIndex) => { const rowErrors = errors.get(rowIndex) || {}; rowErrors[fieldKey] = [errorObj]; errors.set(rowIndex, rowErrors); @@ -580,52 +586,55 @@ export const useValidationState = ({ } }); }); - + // Apply batch updates only if we have errors to report if (errors.size > 0) { // OPTIMIZATION: Check if we actually have new errors before updating state let hasChanges = false; - + // We'll update errors with a single batch operation - setValidationErrors(prev => { + setValidationErrors((prev) => { const newMap = new Map(prev); - + // Check each row for changes errors.forEach((rowErrors, rowIndex) => { const existingErrors = newMap.get(rowIndex) || {}; const updatedErrors = { ...existingErrors }; let rowHasChanges = false; - + // Check each field for changes Object.entries(rowErrors).forEach(([fieldKey, fieldErrors]) => { // Compare with existing errors const existingFieldErrors = existingErrors[fieldKey]; - - if (!existingFieldErrors || - existingFieldErrors.length !== fieldErrors.length || - !existingFieldErrors.every((err, idx) => - err.message === fieldErrors[idx].message && + + if ( + !existingFieldErrors || + existingFieldErrors.length !== fieldErrors.length || + !existingFieldErrors.every( + (err, idx) => + err.message === fieldErrors[idx].message && err.type === fieldErrors[idx].type - )) { + ) + ) { // We have a change updatedErrors[fieldKey] = fieldErrors; rowHasChanges = true; hasChanges = true; } }); - + // Only update if we have changes if (rowHasChanges) { newMap.set(rowIndex, updatedErrors); } }); - + // Only return a new map if we have changes return hasChanges ? newMap : prev; }); } - - console.log('Uniqueness validation complete'); + + console.log("Uniqueness validation complete"); }, [data, fields]); // Add ref to prevent recursive validation @@ -635,71 +644,83 @@ export const useValidationState = ({ useEffect(() => { // Skip initial load - we have a separate initialization process if (!initialValidationDoneRef.current) return; - + // Don't run validation during template application if (isApplyingTemplateRef.current) return; - + // CRITICAL FIX: Skip validation if we're already validating to prevent infinite loops if (isValidatingRef.current) return; - - console.log('Running validation on data change'); + + console.log("Running validation on data change"); isValidatingRef.current = true; - + // For faster validation, run synchronously instead of in an async function const validateFields = () => { try { // Run regex validations on all rows - const regexFields = fields.filter(field => field.validations?.some(v => v.rule === 'regex')); + const regexFields = fields.filter((field) => + field.validations?.some((v) => v.rule === "regex") + ); if (regexFields.length > 0) { // Create a map to collect validation errors - const regexErrors = new Map>(); - + const regexErrors = new Map< + number, + Record + >(); + // Check each row for regex errors data.forEach((row, rowIndex) => { const rowErrors: Record = {}; let hasErrors = false; - + // Check each regex field - regexFields.forEach(field => { + regexFields.forEach((field) => { const key = String(field.key); const value = row[key as keyof typeof row]; - + // Skip empty values - if (value === undefined || value === null || value === '') { + if (value === undefined || value === null || value === "") { return; } - + // Find regex validation - const regexValidation = field.validations?.find(v => v.rule === 'regex'); + const regexValidation = field.validations?.find( + (v) => v.rule === "regex" + ); if (regexValidation) { try { // Check if value matches regex - const regex = new RegExp(regexValidation.value, regexValidation.flags); + const regex = new RegExp( + regexValidation.value, + regexValidation.flags + ); if (!regex.test(String(value))) { // Add regex validation error - rowErrors[key] = [{ - message: regexValidation.errorMessage, - level: regexValidation.level || 'error', - source: ErrorSources.Row, - type: ErrorType.Regex - }]; + rowErrors[key] = [ + { + message: regexValidation.errorMessage, + level: regexValidation.level || "error", + source: ErrorSources.Row, + type: ErrorType.Regex, + }, + ]; hasErrors = true; } } catch (error) { - console.error('Invalid regex in validation:', error); + console.error("Invalid regex in validation:", error); } } }); - + // Add errors if any found if (hasErrors) { regexErrors.set(rowIndex, rowErrors); } }); - + // Update validation errors if (regexErrors.size > 0) { - setValidationErrors(prev => { + setValidationErrors((prev) => { const newErrors = new Map(prev); // Merge in regex errors for (const [rowIndex, errors] of regexErrors.entries()) { @@ -720,547 +741,400 @@ export const useValidationState = ({ }, 100); } }; - + // Run validation immediately validateFields(); - }, [data, fields, validateUniqueItemNumbers]); - // Fetch product by UPC from API - optimized with proper error handling and types - const fetchProductByUpc = useCallback(async (supplier: string, upc: string): Promise => { - try { - // Check cache first - const cacheKey = `${supplier}-${upc}`; - if (processedUpcMapRef.current.has(cacheKey)) { - const cachedItemNumber = processedUpcMapRef.current.get(cacheKey); - if (cachedItemNumber) { - return { - error: false, - data: { - itemNumber: cachedItemNumber - } - }; - } - } - - // Use the correct endpoint and parameter names - const response = await fetch(`${getApiUrl()}/import/check-upc-and-generate-sku?supplierId=${encodeURIComponent(supplier)}&upc=${encodeURIComponent(upc)}`, { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - }, - }); - - // Handle non-200 responses - if (!response.ok) { - // Properly handle different error status codes - if (response.status === 404) { - return { - error: true, - message: 'UPC not found', - }; - } else if (response.status === 409) { - // Handle the case where UPC already exists - const errorData = await response.json(); - return { - error: true, - message: `UPC already exists (${errorData.existingItemNumber})`, - data: { - itemNumber: errorData.existingItemNumber || '', - }, - type: ErrorType.Unique, - source: ErrorSources.Api - }; - } else if (response.status === 429) { - return { - error: true, - message: 'Too many requests, please try again later', - }; - } else { - return { - error: true, - message: `API error (${response.status})`, - }; - } - } - - // Parse successful response - const data = await response.json(); - - // If data has an error property, it's an API-level error - if (data.error) { - return { - error: true, - message: data.message || 'Error validating UPC', - }; - } - - // Cache the result - if (data.itemNumber) { - processedUpcMapRef.current.set(cacheKey, data.itemNumber); - } - - // Return successful validation with product data - return { - error: false, - data: { - itemNumber: data.itemNumber || '', - ...data - }, - }; - } catch (error) { - console.error('Error in fetchProductByUpc:', error); - return { - error: true, - message: 'Network error validating UPC', - }; - } - }, []); - - // Add batch validation queue - const validationQueueRef = useRef<{rowIndex: number, supplierId: string, upcValue: string}[]>([]); - const isProcessingBatchRef = useRef(false); - - // Process validation queue in batches - const processBatchValidation = useCallback(async () => { - if (isProcessingBatchRef.current) return; - if (validationQueueRef.current.length === 0) return; - - isProcessingBatchRef.current = true; - - // Process all items in the queue at once - const allItems = [...validationQueueRef.current]; - validationQueueRef.current = []; // Clear the queue - - try { - await Promise.all(allItems.map(async ({ rowIndex, supplierId, upcValue }) => { - // Skip if already validated - const cacheKey = `${supplierId}-${upcValue}`; - if (processedUpcMapRef.current.has(cacheKey)) return; - - const result = await fetchProductByUpc(supplierId, upcValue); - - if (!result.error && result.data?.itemNumber) { - processedUpcMapRef.current.set(cacheKey, result.data.itemNumber); - setUpcValidationResults(prev => { - const newResults = new Map(prev); - newResults.set(rowIndex, { itemNumber: result.data?.itemNumber || '' }); - return newResults; - }); - } - })); - } finally { - isProcessingBatchRef.current = false; - - // Process any new items that might have been added during processing - if (validationQueueRef.current.length > 0) { - processBatchValidation(); - } - } - }, [fetchProductByUpc]); - - // Debounced version of processBatchValidation - const debouncedProcessBatch = useMemo( - () => debounce(processBatchValidation, DEBOUNCE_DELAY), - [processBatchValidation] - ); - - // Modified validateUpc to use queue - const validateUpc = useCallback(async (rowIndex: number, supplierId: string, upcValue: string): Promise<{ success: boolean, itemNumber?: string }> => { - try { - if (!supplierId || !upcValue) { - return { success: false }; - } - - // Check cache first - const cacheKey = `${supplierId}-${upcValue}`; - if (processedUpcMapRef.current.has(cacheKey)) { - const cachedItemNumber = processedUpcMapRef.current.get(cacheKey); - if (cachedItemNumber) { - setUpcValidationResults(prev => { - const newResults = new Map(prev); - newResults.set(rowIndex, { itemNumber: cachedItemNumber }); - return newResults; - }); - return { success: true, itemNumber: cachedItemNumber }; - } - return { success: false }; - } - - // Add to validation queue - validationQueueRef.current.push({ rowIndex, supplierId, upcValue }); - setValidatingUpcRows(prev => [...prev, rowIndex]); - - // Trigger batch processing - debouncedProcessBatch(); - - return { success: true }; - } catch (error) { - console.error('Error in validateUpc:', error); - return { success: false }; - } - }, [debouncedProcessBatch]); - - // Track which cells are currently being validated - allows targeted re-rendering - const isValidatingUpc = useCallback((rowIndex: number) => { - return validatingUpcRows.includes(rowIndex); - }, [validatingUpcRows]); - // Filter data based on current filter state const filteredData = useMemo(() => { return data.filter((row, index) => { // Filter by search text if (filters.searchText) { const searchLower = filters.searchText.toLowerCase(); - const matchesSearch = fields.some(field => { + const matchesSearch = fields.some((field) => { const value = row[field.key as keyof typeof row]; if (value === undefined || value === null) return false; return String(value).toLowerCase().includes(searchLower); }); if (!matchesSearch) return false; } - + // Filter by errors if (filters.showErrorsOnly) { - const hasErrors = validationErrors.has(index) && - Object.keys(validationErrors.get(index) || {}).length > 0; + const hasErrors = + validationErrors.has(index) && + Object.keys(validationErrors.get(index) || {}).length > 0; if (!hasErrors) return false; } - + // Filter by field value if (filters.filterField && filters.filterValue) { const fieldValue = row[filters.filterField as keyof typeof row]; if (fieldValue === undefined) return false; - + const valueStr = String(fieldValue).toLowerCase(); const filterStr = filters.filterValue.toLowerCase(); - + if (!valueStr.includes(filterStr)) return false; } - + return true; }); }, [data, fields, filters, validationErrors]); - + // Get filter fields const filterFields = useMemo(() => { - return fields.map(field => ({ + return fields.map((field) => ({ key: String(field.key), - label: field.label - })) - }, [fields]) - + label: field.label, + })); + }, [fields]); + // Get filter values for the selected field const filterValues = useMemo(() => { - if (!filters.filterField) return [] - + if (!filters.filterField) return []; + // Get unique values for the selected field - const uniqueValues = new Set() - - data.forEach(row => { - const value = row[filters.filterField as keyof typeof row] + const uniqueValues = new Set(); + + data.forEach((row) => { + const value = row[filters.filterField as keyof typeof row]; if (value !== undefined && value !== null) { - uniqueValues.add(String(value)) + uniqueValues.add(String(value)); } - }) - - return Array.from(uniqueValues).map(value => ({ + }); + + return Array.from(uniqueValues).map((value) => ({ value, - label: value - })) - }, [data, filters.filterField]) - + label: value, + })); + }, [data, filters.filterField]); + // Update filters const updateFilters = useCallback((newFilters: Partial) => { - setFilters(prev => ({ + setFilters((prev) => ({ ...prev, - ...newFilters - })) - }, []) - + ...newFilters, + })); + }, []); + // Reset filters const resetFilters = useCallback(() => { setFilters({ - searchText: '', + searchText: "", showErrorsOnly: false, filterField: null, - filterValue: null - }) - }, []) - - // Copy a cell value to all cells below it in the same column - const copyDown = useCallback((rowIndex: number, key: T) => { - // Get the source value to copy - const sourceValue = data[rowIndex][key]; - - // Update all rows below with the same value using the existing updateRow function - // This ensures all validation logic runs consistently - for (let i = rowIndex + 1; i < data.length; i++) { - // Just use updateRow which will handle validation with proper timing - updateRow(i, key, sourceValue); - } - }, [data, updateRow]); - - // Add this at the top of the component, after other useRef declarations - const validationTimeoutsRef = useRef>({}); - - // Clean up timeouts on unmount - useEffect(() => { - return () => { - // Clear all validation timeouts - Object.values(validationTimeoutsRef.current).forEach(timeout => { - clearTimeout(timeout); - }); - }; + filterValue: null, + }); }, []); - - // Save a new template - const saveTemplate = useCallback(async (name: string, type: string) => { - try { - // Get selected rows - const selectedRowIndex = Number(Object.keys(rowSelection)[0]); - const selectedRow = data[selectedRowIndex]; - - if (!selectedRow) { - toast.error('Please select a row to create a template') - return - } - - // Extract data for template, removing metadata fields - const { __index, __template, __original, __corrected, __changes, ...templateData } = selectedRow as any; - - // Clean numeric values (remove $ from price fields) - const cleanedData: Record = {}; - - // Process each key-value pair - Object.entries(templateData).forEach(([key, value]) => { - // Handle numeric values with dollar signs - if (typeof value === 'string' && value.includes('$')) { - cleanedData[key] = value.replace(/[$,\s]/g, '').trim(); - } - // Handle array values (like categories or ship_restrictions) - else if (Array.isArray(value)) { - cleanedData[key] = value; - } - // Handle other values - else { - cleanedData[key] = value; - } - }); - - // Send the template to the API - const response = await fetch(`${getApiUrl()}/templates`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - ...cleanedData, - company: name, - product_type: type, - }), - }); - - if (!response.ok) { - const errorData = await response.json(); - throw new Error(errorData.error || errorData.details || "Failed to save template"); - } - - // Get the new template from the response - const newTemplate = await response.json(); - - // Update the templates list with the new template - setTemplates(prev => [...prev, newTemplate]); - - // Update the row to show it's using this template - setData(prev => { - const newData = [...prev]; - if (newData[selectedRowIndex]) { - newData[selectedRowIndex] = { - ...newData[selectedRowIndex], - __template: newTemplate.id.toString() - }; - } - return newData; - }); - - toast.success(`Template "${name}" saved successfully`) - - // Reset dialog state - setTemplateState(prev => ({ - ...prev, - showSaveTemplateDialog: false, - newTemplateName: '', - newTemplateType: '', - })) - } catch (error) { - console.error('Error saving template:', error) - toast.error(error instanceof Error ? error.message : 'Failed to save template') - } - }, [data, rowSelection, setData]); - // Apply template to rows - optimized version - const applyTemplate = useCallback((templateId: string, rowIndexes: number[]) => { - const template = templates.find(t => t.id.toString() === templateId); - - if (!template) { - toast.error('Template not found'); - return; - } - - console.log(`Applying template ${templateId} to rows:`, rowIndexes); - - // Validate row indexes - const validRowIndexes = rowIndexes.filter(index => - index >= 0 && index < data.length && Number.isInteger(index) - ); - - if (validRowIndexes.length === 0) { - toast.error('No valid rows to update'); - console.error('Invalid row indexes:', rowIndexes); - return; - } - - // Set the template application flag - isApplyingTemplateRef.current = true; - - // Save scroll position - const scrollPosition = { - left: window.scrollX, - top: window.scrollY - }; - - // Create a copy of data and process all rows at once to minimize state updates - const newData = [...data]; - const batchErrors = new Map>(); - const batchStatuses = new Map(); - - // Extract template fields once outside the loop - const templateFields = Object.entries(template).filter(([key]) => - !['id', '__meta', '__template', '__original', '__corrected', '__changes'].includes(key) - ); - - // Apply template to each valid row - validRowIndexes.forEach(index => { - // Create a new row with template values - const originalRow = newData[index]; - const updatedRow = { ...originalRow } as Record; - - // Apply template fields (excluding metadata fields) - for (const [key, value] of templateFields) { - updatedRow[key] = value; + // Copy a cell value to all cells below it in the same column + const copyDown = useCallback( + (rowIndex: number, key: T) => { + // Get the source value to copy + const sourceValue = data[rowIndex][key]; + + // Update all rows below with the same value using the existing updateRow function + // This ensures all validation logic runs consistently + for (let i = rowIndex + 1; i < data.length; i++) { + // Just use updateRow which will handle validation with proper timing + updateRow(i, key, sourceValue); } - - // Mark the row as using this template - updatedRow.__template = templateId; - - // Update the row in the data array - newData[index] = updatedRow as RowData; - - // Clear validation errors and mark as validated - batchErrors.set(index, {}); - batchStatuses.set(index, 'validated'); - }); - - // Perform a single update for all rows - setData(newData); - - // Update all validation errors and statuses at once - setValidationErrors(prev => { - const newErrors = new Map(prev); - for (const [rowIndex, errors] of batchErrors.entries()) { - newErrors.set(rowIndex, errors); - } - return newErrors; - }); - - setRowValidationStatus(prev => { - const newStatus = new Map(prev); - for (const [rowIndex, status] of batchStatuses.entries()) { - newStatus.set(rowIndex, status); - } - return newStatus; - }); - - // Restore scroll position - requestAnimationFrame(() => { - window.scrollTo(scrollPosition.left, scrollPosition.top); - }); - - // Show success toast - if (validRowIndexes.length === 1) { - toast.success('Template applied'); - } else if (validRowIndexes.length > 1) { - toast.success(`Template applied to ${validRowIndexes.length} rows`); - } - - // Check which rows need UPC validation - const upcValidationRows = validRowIndexes.filter(rowIndex => { - const row = newData[rowIndex]; - return row && row.upc && row.supplier; - }); - - // Validate UPCs for rows that have both UPC and supplier - if (upcValidationRows.length > 0) { - console.log(`Validating UPCs for ${upcValidationRows.length} rows after template application`); - - // Schedule UPC validation for the next tick to allow UI to update first - setTimeout(() => { - upcValidationRows.forEach(rowIndex => { - const row = newData[rowIndex]; - if (row && row.upc && row.supplier) { - validateRow(rowIndex); + }, + [data, updateRow] + ); + + // Save a new template + const saveTemplate = useCallback( + async (name: string, type: string) => { + try { + // Get selected rows + const selectedRowIndex = Number(Object.keys(rowSelection)[0]); + const selectedRow = data[selectedRowIndex]; + + if (!selectedRow) { + toast.error("Please select a row to create a template"); + return; + } + + // Extract data for template, removing metadata fields + const { + __index, + __template, + __original, + __corrected, + __changes, + ...templateData + } = selectedRow as any; + + // Clean numeric values (remove $ from price fields) + const cleanedData: Record = {}; + + // Process each key-value pair + Object.entries(templateData).forEach(([key, value]) => { + // Handle numeric values with dollar signs + if (typeof value === "string" && value.includes("$")) { + cleanedData[key] = value.replace(/[$,\s]/g, "").trim(); + } + // Handle array values (like categories or ship_restrictions) + else if (Array.isArray(value)) { + cleanedData[key] = value; + } + // Handle other values + else { + cleanedData[key] = value; } }); - }, 100); - } - - // Reset the template application flag - isApplyingTemplateRef.current = false; - }, [data, templates, setData, setValidationErrors, setRowValidationStatus, validateRow]); + + // Send the template to the API + const response = await fetch(`${getApiUrl()}/templates`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + ...cleanedData, + company: name, + product_type: type, + }), + }); + + if (!response.ok) { + const errorData = await response.json(); + throw new Error( + errorData.error || errorData.details || "Failed to save template" + ); + } + + // Get the new template from the response + const newTemplate = await response.json(); + + // Update the templates list with the new template + setTemplates((prev) => [...prev, newTemplate]); + + // Update the row to show it's using this template + setData((prev) => { + const newData = [...prev]; + if (newData[selectedRowIndex]) { + newData[selectedRowIndex] = { + ...newData[selectedRowIndex], + __template: newTemplate.id.toString(), + }; + } + return newData; + }); + + toast.success(`Template "${name}" saved successfully`); + + // Reset dialog state + setTemplateState((prev) => ({ + ...prev, + showSaveTemplateDialog: false, + newTemplateName: "", + newTemplateType: "", + })); + } catch (error) { + console.error("Error saving template:", error); + toast.error( + error instanceof Error ? error.message : "Failed to save template" + ); + } + }, + [data, rowSelection, setData] + ); + + // Apply template to rows - optimized version + const applyTemplate = useCallback( + (templateId: string, rowIndexes: number[]) => { + const template = templates.find((t) => t.id.toString() === templateId); + + if (!template) { + toast.error("Template not found"); + return; + } + + console.log(`Applying template ${templateId} to rows:`, rowIndexes); + + // Validate row indexes + const validRowIndexes = rowIndexes.filter( + (index) => index >= 0 && index < data.length && Number.isInteger(index) + ); + + if (validRowIndexes.length === 0) { + toast.error("No valid rows to update"); + console.error("Invalid row indexes:", rowIndexes); + return; + } + + // Set the template application flag + isApplyingTemplateRef.current = true; + + // Save scroll position + const scrollPosition = { + left: window.scrollX, + top: window.scrollY, + }; + + // Create a copy of data and process all rows at once to minimize state updates + const newData = [...data]; + const batchErrors = new Map>(); + const batchStatuses = new Map< + number, + "pending" | "validating" | "validated" | "error" + >(); + + // Extract template fields once outside the loop + const templateFields = Object.entries(template).filter( + ([key]) => + ![ + "id", + "__meta", + "__template", + "__original", + "__corrected", + "__changes", + ].includes(key) + ); + + // Apply template to each valid row + validRowIndexes.forEach((index) => { + // Create a new row with template values + const originalRow = newData[index]; + const updatedRow = { ...originalRow } as Record; + + // Apply template fields (excluding metadata fields) + for (const [key, value] of templateFields) { + updatedRow[key] = value; + } + + // Mark the row as using this template + updatedRow.__template = templateId; + + // Update the row in the data array + newData[index] = updatedRow as RowData; + + // Clear validation errors and mark as validated + batchErrors.set(index, {}); + batchStatuses.set(index, "validated"); + }); + + // Perform a single update for all rows + setData(newData); + + // Update all validation errors and statuses at once + setValidationErrors((prev) => { + const newErrors = new Map(prev); + for (const [rowIndex, errors] of batchErrors.entries()) { + newErrors.set(rowIndex, errors); + } + return newErrors; + }); + + setRowValidationStatus((prev) => { + const newStatus = new Map(prev); + for (const [rowIndex, status] of batchStatuses.entries()) { + newStatus.set(rowIndex, status); + } + return newStatus; + }); + + // Restore scroll position + requestAnimationFrame(() => { + window.scrollTo(scrollPosition.left, scrollPosition.top); + }); + + // Show success toast + if (validRowIndexes.length === 1) { + toast.success("Template applied"); + } else if (validRowIndexes.length > 1) { + toast.success(`Template applied to ${validRowIndexes.length} rows`); + } + + // Check which rows need UPC validation + const upcValidationRows = validRowIndexes.filter((rowIndex) => { + const row = newData[rowIndex]; + return row && row.upc && row.supplier; + }); + + // Validate UPCs for rows that have both UPC and supplier + if (upcValidationRows.length > 0) { + console.log( + `Validating UPCs for ${upcValidationRows.length} rows after template application` + ); + + // Schedule UPC validation for the next tick to allow UI to update first + setTimeout(() => { + upcValidationRows.forEach((rowIndex) => { + const row = newData[rowIndex]; + if (row && row.upc && row.supplier) { + validateRow(rowIndex); + } + }); + }, 100); + } + + // Reset the template application flag + isApplyingTemplateRef.current = false; + }, + [ + data, + templates, + setData, + setValidationErrors, + setRowValidationStatus, + validateRow, + ] + ); // Apply template to selected rows - const applyTemplateToSelected = useCallback((templateId: string) => { - if (!templateId) return; - - // Update the selected template ID - setTemplateState(prev => ({ - ...prev, - selectedTemplateId: templateId - })); - - // Get selected row keys (which may be UUIDs) - const selectedKeys = Object.entries(rowSelection) - .filter(([_, selected]) => selected === true) - .map(([key, _]) => key); - - console.log('Selected row keys:', selectedKeys); - - if (selectedKeys.length === 0) { - toast.error('No rows selected'); - return; - } - - // Map UUID keys to array indices - const selectedIndexes = selectedKeys.map(key => { - // Find the matching row index in the data array - const index = data.findIndex(row => - (row.__index && row.__index === key) || // Match by __index - (String(data.indexOf(row)) === key) // Or by numeric index - ); - return index; - }).filter(index => index !== -1); // Filter out any not found - - console.log('Mapped row indices:', selectedIndexes); - - if (selectedIndexes.length === 0) { - toast.error('Could not find selected rows'); - return; - } - - // Apply template to selected rows - applyTemplate(templateId, selectedIndexes); - }, [rowSelection, applyTemplate, setTemplateState, data]); + const applyTemplateToSelected = useCallback( + (templateId: string) => { + if (!templateId) return; + + // Update the selected template ID + setTemplateState((prev) => ({ + ...prev, + selectedTemplateId: templateId, + })); + + // Get selected row keys (which may be UUIDs) + const selectedKeys = Object.entries(rowSelection) + .filter(([_, selected]) => selected === true) + .map(([key, _]) => key); + + console.log("Selected row keys:", selectedKeys); + + if (selectedKeys.length === 0) { + toast.error("No rows selected"); + return; + } + + // Map UUID keys to array indices + const selectedIndexes = selectedKeys + .map((key) => { + // Find the matching row index in the data array + const index = data.findIndex( + (row) => + (row.__index && row.__index === key) || // Match by __index + String(data.indexOf(row)) === key // Or by numeric index + ); + return index; + }) + .filter((index) => index !== -1); // Filter out any not found + + console.log("Mapped row indices:", selectedIndexes); + + if (selectedIndexes.length === 0) { + toast.error("Could not find selected rows"); + return; + } + + // Apply template to selected rows + applyTemplate(templateId, selectedIndexes); + }, + [rowSelection, applyTemplate, setTemplateState, data] + ); // Add field options query const { data: fieldOptionsData } = useQuery({ @@ -1277,51 +1151,60 @@ export const useValidationState = ({ }); // Get display text for a template - const getTemplateDisplayText = useCallback((templateId: string | null) => { - if (!templateId) return 'Select a template' + const getTemplateDisplayText = useCallback( + (templateId: string | null) => { + if (!templateId) return "Select a template"; - const template = templates.find(t => t.id.toString() === templateId) - if (!template) return 'Unknown template' + const template = templates.find((t) => t.id.toString() === templateId); + if (!template) return "Unknown template"; - try { - const companyId = template.company || ""; - const productType = template.product_type || "Unknown Type"; - - // Find company name from field options - const companyName = fieldOptionsData?.companies?.find((c: { value: string; label: string }) => - c.value === companyId - )?.label || companyId; - - return `${companyName} - ${productType}`; - } catch (error) { - console.error("Error formatting template display text:", error, template); - return "Error displaying template"; - } - }, [templates, fieldOptionsData]); + try { + const companyId = template.company || ""; + const productType = template.product_type || "Unknown Type"; + + // Find company name from field options + const companyName = + fieldOptionsData?.companies?.find( + (c: { value: string; label: string }) => c.value === companyId + )?.label || companyId; + + return `${companyName} - ${productType}`; + } catch (error) { + console.error( + "Error formatting template display text:", + error, + template + ); + return "Error displaying template"; + } + }, + [templates, fieldOptionsData] + ); // Check if there are any errors const hasErrors = useMemo(() => { for (const [_, status] of rowValidationStatus.entries()) { - if (status === 'error') return true + if (status === "error") return true; } - return false + return false; }, [rowValidationStatus]); // Load templates const loadTemplates = useCallback(async () => { try { setIsLoadingTemplates(true); - console.log('Fetching templates from:', `${getApiUrl()}/templates`); + console.log("Fetching templates from:", `${getApiUrl()}/templates`); const response = await fetch(`${getApiUrl()}/templates`); - if (!response.ok) throw new Error('Failed to fetch templates'); + if (!response.ok) throw new Error("Failed to fetch templates"); const templateData = await response.json(); - const validTemplates = templateData.filter((t: any) => - t && typeof t === 'object' && t.id && t.company && t.product_type + const validTemplates = templateData.filter( + (t: any) => + t && typeof t === "object" && t.id && t.company && t.product_type ); setTemplates(validTemplates); } catch (error) { - console.error('Error fetching templates:', error); - toast.error('Failed to load templates'); + console.error("Error fetching templates:", error); + toast.error("Failed to load templates"); } finally { setIsLoadingTemplates(false); } @@ -1333,147 +1216,186 @@ export const useValidationState = ({ }, [loadTemplates]); // Create a function to handle button clicks (continue or back) - const handleButtonClick = useCallback(async (direction: 'next' | 'back') => { - if (direction === 'back' && onBack) { - // If a specific action is defined for back, use it - onBack(); - return; - } - - if (direction === 'next') { - // When proceeding to the next screen, check for unvalidated rows first - const hasErrors = [...validationErrors.entries()].some(([_, errors]) => { - return Object.values(errors).some(errorSet => - errorSet.some(error => error.type !== ErrorType.Required) + const handleButtonClick = useCallback( + async (direction: "next" | "back") => { + if (direction === "back" && onBack) { + // If a specific action is defined for back, use it + onBack(); + return; + } + + if (direction === "next") { + // When proceeding to the next screen, check for unvalidated rows first + const hasErrors = [...validationErrors.entries()].some( + ([_, errors]) => { + return Object.values(errors).some((errorSet) => + errorSet.some((error) => error.type !== ErrorType.Required) + ); + } ); - }); - - if (hasErrors) { - // We have validation errors - ask the user to fix them first or continue anyway - const shouldContinue = window.confirm( - 'There are validation errors in your data. Do you want to continue anyway?' - ); - - if (!shouldContinue) { - // User chose to fix errors - return; + + if (hasErrors) { + // We have validation errors - ask the user to fix them first or continue anyway + const shouldContinue = window.confirm( + "There are validation errors in your data. Do you want to continue anyway?" + ); + + if (!shouldContinue) { + // User chose to fix errors + return; + } + } + + // Prepare the data for the next step + try { + // No toast here - unnecessary and distracting + + // Call onNext with the cleaned data + if (onNext) { + // Remove metadata fields before passing to onNext + const cleanedData = data.map((row) => { + const { + __index, + __template, + __original, + __corrected, + __changes, + ...cleanRow + } = row; + return cleanRow as Data; + }); + + onNext(cleanedData); + } + } catch (error) { + console.error("Error proceeding to next step:", error); + toast.error("Error saving data"); } } - - // Prepare the data for the next step - try { - // No toast here - unnecessary and distracting - - // Call onNext with the cleaned data - if (onNext) { - // Remove metadata fields before passing to onNext - const cleanedData = data.map(row => { - const { __index, __template, __original, __corrected, __changes, ...cleanRow } = row; - return cleanRow as Data; - }); - - onNext(cleanedData); - } - } catch (error) { - console.error('Error proceeding to next step:', error); - toast.error('Error saving data'); - } - } - }, [data, onBack, onNext, validationErrors]); + }, + [data, onBack, onNext, validationErrors] + ); // Initialize validation on mount useEffect(() => { if (initialValidationDoneRef.current) return; - - console.log('Running initial validation'); - + + console.log("Running initial validation"); + const runCompleteValidation = async () => { if (!data || data.length === 0) return; - - console.log('Running complete validation...'); - + + console.log("Running complete validation..."); + // Get required fields - const requiredFields = fields.filter(field => field.validations?.some(v => v.rule === 'required')); + const requiredFields = fields.filter((field) => + field.validations?.some((v) => v.rule === "required") + ); console.log(`Found ${requiredFields.length} required fields`); - + // Get fields that have regex validation - const regexFields = fields.filter(field => field.validations?.some(v => v.rule === 'regex')); + const regexFields = fields.filter((field) => + field.validations?.some((v) => v.rule === "regex") + ); console.log(`Found ${regexFields.length} fields with regex validation`); - + // Get fields that need uniqueness validation - const uniqueFields = fields.filter(field => field.validations?.some(v => v.rule === 'unique')); - console.log(`Found ${uniqueFields.length} fields requiring uniqueness validation`); - + const uniqueFields = fields.filter((field) => + field.validations?.some((v) => v.rule === "unique") + ); + console.log( + `Found ${uniqueFields.length} fields requiring uniqueness validation` + ); + // Limit batch size to avoid UI freezing const BATCH_SIZE = 100; const totalRows = data.length; - + // Initialize new data for any modifications const newData = [...data]; - + // Create a temporary Map to collect all validation errors - const validationErrorsTemp = new Map>(); - + const validationErrorsTemp = new Map< + number, + Record + >(); + // Variables for batching let currentBatch = 0; const totalBatches = Math.ceil(totalRows / BATCH_SIZE); - + const processBatch = async () => { // Calculate batch range const startIdx = currentBatch * BATCH_SIZE; const endIdx = Math.min(startIdx + BATCH_SIZE, totalRows); - console.log(`Processing batch ${currentBatch + 1}/${totalBatches} (rows ${startIdx} to ${endIdx - 1})`); - + console.log( + `Processing batch ${ + currentBatch + 1 + }/${totalBatches} (rows ${startIdx} to ${endIdx - 1})` + ); + // Process rows in this batch const batchPromises: Promise[] = []; - + for (let rowIndex = startIdx; rowIndex < endIdx; rowIndex++) { batchPromises.push( - new Promise(resolve => { + new Promise((resolve) => { const row = data[rowIndex]; - + // Skip if row is empty or undefined if (!row) { resolve(); return; } - + // Store field errors for this row const fieldErrors: Record = {}; let hasErrors = false; - + // Check if price fields need formatting const rowAsRecord = row as Record; let mSrpNeedsProcessing = false; let costEachNeedsProcessing = false; - - if (rowAsRecord.msrp && typeof rowAsRecord.msrp === 'string' && - (rowAsRecord.msrp.includes('$') || rowAsRecord.msrp.includes(','))) { + + if ( + rowAsRecord.msrp && + typeof rowAsRecord.msrp === "string" && + (rowAsRecord.msrp.includes("$") || + rowAsRecord.msrp.includes(",")) + ) { mSrpNeedsProcessing = true; } - - if (rowAsRecord.cost_each && typeof rowAsRecord.cost_each === 'string' && - (rowAsRecord.cost_each.includes('$') || rowAsRecord.cost_each.includes(','))) { + + if ( + rowAsRecord.cost_each && + typeof rowAsRecord.cost_each === "string" && + (rowAsRecord.cost_each.includes("$") || + rowAsRecord.cost_each.includes(",")) + ) { costEachNeedsProcessing = true; } - + // Process price fields if needed if (mSrpNeedsProcessing || costEachNeedsProcessing) { // Create a clean copy only if needed - const cleanedRow = {...row} as Record; - + const cleanedRow = { ...row } as Record; + if (mSrpNeedsProcessing) { - const msrpValue = rowAsRecord.msrp.replace(/[$,]/g, ''); + const msrpValue = rowAsRecord.msrp.replace(/[$,]/g, ""); const numValue = parseFloat(msrpValue); - cleanedRow.msrp = !isNaN(numValue) ? numValue.toFixed(2) : msrpValue; + cleanedRow.msrp = !isNaN(numValue) + ? numValue.toFixed(2) + : msrpValue; } - + if (costEachNeedsProcessing) { - const costValue = rowAsRecord.cost_each.replace(/[$,]/g, ''); + const costValue = rowAsRecord.cost_each.replace(/[$,]/g, ""); const numValue = parseFloat(costValue); - cleanedRow.cost_each = !isNaN(numValue) ? numValue.toFixed(2) : costValue; + cleanedRow.cost_each = !isNaN(numValue) + ? numValue.toFixed(2) + : costValue; } - + newData[rowIndex] = cleanedRow as RowData; } @@ -1481,104 +1403,120 @@ export const useValidationState = ({ for (const field of requiredFields) { const key = String(field.key); const value = row[key as keyof typeof row]; - + // Skip non-required empty fields - if (value === undefined || value === null || value === '' || - (Array.isArray(value) && value.length === 0) || - (typeof value === 'object' && value !== null && Object.keys(value).length === 0)) { - + if ( + value === undefined || + value === null || + value === "" || + (Array.isArray(value) && value.length === 0) || + (typeof value === "object" && + value !== null && + Object.keys(value).length === 0) + ) { // Add error for empty required fields - fieldErrors[key] = [{ - message: field.validations?.find(v => v.rule === 'required')?.errorMessage || 'This field is required', - level: 'error', - source: ErrorSources.Row, - type: ErrorType.Required - }]; + fieldErrors[key] = [ + { + message: + field.validations?.find((v) => v.rule === "required") + ?.errorMessage || "This field is required", + level: "error", + source: ErrorSources.Row, + type: ErrorType.Required, + }, + ]; hasErrors = true; } } - + // Validate regex fields - even if they have data for (const field of regexFields) { const key = String(field.key); const value = row[key as keyof typeof row]; - + // Skip empty values as they're handled by required validation - if (value === undefined || value === null || value === '') { + if (value === undefined || value === null || value === "") { continue; } - + // Find regex validation - const regexValidation = field.validations?.find(v => v.rule === 'regex'); + const regexValidation = field.validations?.find( + (v) => v.rule === "regex" + ); if (regexValidation) { try { // Check if value matches regex - const regex = new RegExp(regexValidation.value, regexValidation.flags); + const regex = new RegExp( + regexValidation.value, + regexValidation.flags + ); if (!regex.test(String(value))) { // Add regex validation error - fieldErrors[key] = [{ - message: regexValidation.errorMessage, - level: regexValidation.level || 'error', - source: ErrorSources.Row, - type: ErrorType.Regex - }]; + fieldErrors[key] = [ + { + message: regexValidation.errorMessage, + level: regexValidation.level || "error", + source: ErrorSources.Row, + type: ErrorType.Regex, + }, + ]; hasErrors = true; } } catch (error) { - console.error('Invalid regex in validation:', error); + console.error("Invalid regex in validation:", error); } } } - + // Update validation errors for this row if (hasErrors) { validationErrorsTemp.set(rowIndex, fieldErrors); } - + resolve(); }) ); } - + // Wait for all row validations to complete await Promise.all(batchPromises); }; - + const processAllBatches = async () => { for (let batch = 0; batch < totalBatches; batch++) { currentBatch = batch; await processBatch(); - + // Yield to UI thread periodically if (batch % 2 === 1) { - await new Promise(resolve => setTimeout(resolve, 0)); + await new Promise((resolve) => setTimeout(resolve, 0)); } } - + // All batches complete - console.log('All initial validation batches complete'); - + console.log("All initial validation batches complete"); + // Apply collected validation errors all at once setValidationErrors(validationErrorsTemp); - + // Apply any data changes (like price formatting) if (JSON.stringify(data) !== JSON.stringify(newData)) { setData(newData); } - + // Run uniqueness validation after the basic validation validateUniqueItemNumbers(); - + // Mark that initial validation is done initialValidationDoneRef.current = true; - - console.log('Initial validation complete'); + + console.log("Initial validation complete"); }; - + // Start the validation process processAllBatches(); }; - + // Run the complete validation runCompleteValidation(); }, [data, fields, setData, setValidationErrors]); @@ -1587,48 +1525,51 @@ export const useValidationState = ({ const fieldsWithOptions = useMemo(() => { if (!fieldOptionsData) return fields; - return fields.map(field => { + return fields.map((field) => { // Skip fields that aren't select or multi-select - if (typeof field.fieldType !== 'object' || - (field.fieldType.type !== 'select' && field.fieldType.type !== 'multi-select')) { + if ( + typeof field.fieldType !== "object" || + (field.fieldType.type !== "select" && + field.fieldType.type !== "multi-select") + ) { return field; } // Get the correct options based on field key let options = []; switch (field.key) { - case 'company': + case "company": options = [...(fieldOptionsData.companies || [])]; break; - case 'supplier': + case "supplier": options = [...(fieldOptionsData.suppliers || [])]; break; - case 'categories': + case "categories": options = [...(fieldOptionsData.categories || [])]; break; - case 'themes': + case "themes": options = [...(fieldOptionsData.themes || [])]; break; - case 'colors': + case "colors": options = [...(fieldOptionsData.colors || [])]; break; - case 'tax_cat': + case "tax_cat": options = [...(fieldOptionsData.taxCategories || [])]; // Ensure tax_cat is always a select, not multi-select return { ...field, fieldType: { - type: 'select', - options - } + type: "select", + options, + }, }; - case 'ship_restrictions': + case "ship_restrictions": options = [...(fieldOptionsData.shippingRestrictions || [])]; break; - case 'artist': + case "artist": options = [...(fieldOptionsData.artists || [])]; break; - case 'size_cat': + case "size_cat": options = [...(fieldOptionsData.sizes || [])]; break; default: @@ -1639,8 +1580,8 @@ export const useValidationState = ({ ...field, fieldType: { ...field.fieldType, - options - } + options, + }, }; }); }, [fields, fieldOptionsData]); @@ -1650,81 +1591,27 @@ export const useValidationState = ({ loadTemplates(); }, [loadTemplates]); - // Watch for UPC/barcode and supplier changes to trigger validation - useEffect(() => { - // Skip during initial load - if (!initialValidationDoneRef.current) return; - - // Skip template application - if (isApplyingTemplateRef.current) return; - - // Skip if we're already processing - if (isProcessingBatchRef.current) return; - - // Look for rows with both UPC/barcode and supplier that need validation - const rowsToValidate = data.map((row, index) => { - const upc = row.upc || row.barcode; - const supplier = row.supplier; - - if (upc && supplier) { - // Check if this combination is already in the cache - const cacheKey = `${supplier}-${upc}`; - if (!processedUpcMapRef.current.has(cacheKey)) { - return { index, upc, supplier }; - } - } - return null; - }).filter(Boolean); - - // If we have rows to validate, queue them up - if (rowsToValidate.length > 0) { - console.log(`Found ${rowsToValidate.length} rows needing UPC validation`); - - // Add each row to the validation queue - for (const row of rowsToValidate) { - if (row) { - const { index, upc, supplier } = row; - validationQueueRef.current.push({ - rowIndex: index, - supplierId: String(supplier), - upcValue: String(upc) - }); - - // Mark as validating - setValidatingUpcRows(prev => { - if (prev.includes(index)) return prev; - return [...prev, index]; - }); - } - } - - // Process the queue - debouncedProcessBatch(); - } - }, [data, debouncedProcessBatch]); - return { // Data data, setData, filteredData, - + // Validation isValidating, validationErrors, rowValidationStatus, validateRow, - validateUpc, hasErrors, - + // Row selection rowSelection, setRowSelection, - + // Row manipulation updateRow, copyDown, - + // Templates templates, isLoadingTemplates, @@ -1740,28 +1627,23 @@ export const useValidationState = ({ applyTemplateToSelected, getTemplateDisplayText, refreshTemplates, - + // Filters filters, filterFields, filterValues, updateFilters, resetFilters, - - // UPC validation - validatingUpcRows, - isValidatingUpc, - + // Fields reference fields: fieldsWithOptions, // Return updated fields with options - + // Hooks rowHook, tableHook, - + // Button handling handleButtonClick, - upcValidationResults, revalidateRows, - } -} \ No newline at end of file + }; +}; \ No newline at end of file