Refactor validation hooks into smaller files

This commit is contained in:
2025-03-21 00:33:06 -04:00
parent 7d46ebd6ba
commit 35d2f0df7c
14 changed files with 1473 additions and 1627 deletions

View File

@@ -1,6 +1,6 @@
import { useState, useCallback } from 'react'; import { useState, useCallback } from 'react';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { getApiUrl, RowData } from './useValidationState'; import { getApiUrl, RowData } from './validationTypes';
import { Fields } from '../../../types'; import { Fields } from '../../../types';
import { Meta } from '../types'; import { Meta } from '../types';
import { addErrorsAndRunHooks } from '../utils/dataMutations'; import { addErrorsAndRunHooks } from '../utils/dataMutations';

View File

@@ -0,0 +1,162 @@
import { useCallback } from 'react';
import type { Field, Fields, RowHook, TableHook } from '../../../types';
import type { Meta } from '../types';
import { ErrorSources, ErrorType, ValidationError } from '../../../types';
import { RowData, InfoWithSource, isEmpty } from './validationTypes';
// Create a cache for validation results to avoid repeated validation of the same data
const validationResultCache = new Map();
// Add a function to clear cache for a specific field value
export const clearValidationCacheForField = (fieldKey: string) => {
// 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
export const clearAllUniquenessCaches = () => {
// Clear cache for common unique fields
['item_number', 'upc', 'barcode', 'supplier_no', 'notions_no'].forEach(fieldKey => {
clearValidationCacheForField(fieldKey);
});
// Also clear any cache entries that might involve uniqueness validation
validationResultCache.forEach((_, key) => {
if (key.includes('unique')) {
validationResultCache.delete(key);
}
});
};
export const useFieldValidation = <T extends string>(
fields: Fields<T>,
rowHook?: RowHook<T>,
tableHook?: TableHook<T>
) => {
// Validate a single field
const validateField = useCallback((
value: any,
field: Field<T>
): ValidationError[] => {
const errors: ValidationError[] = [];
if (!field.validations) return errors;
// Create a cache key using field key, value, and validation rules
const cacheKey = `${field.key}-${String(value)}-${JSON.stringify(field.validations)}`;
// Check cache first to avoid redundant validation
if (validationResultCache.has(cacheKey)) {
return validationResultCache.get(cacheKey) || [];
}
field.validations.forEach(validation => {
switch (validation.rule) {
case 'required':
// Use the shared isEmpty function
if (isEmpty(value)) {
errors.push({
message: validation.errorMessage || 'This field is required',
level: validation.level || 'error',
type: ErrorType.Required
});
}
break;
case 'unique':
// Unique validation happens at table level, not here
break;
case 'regex':
if (value !== undefined && value !== null && value !== '') {
try {
const regex = new RegExp(validation.value, validation.flags);
if (!regex.test(String(value))) {
errors.push({
message: validation.errorMessage,
level: validation.level || 'error',
type: ErrorType.Regex
});
}
} catch (error) {
console.error('Invalid regex in validation:', error);
}
}
break;
}
});
// Store results in cache to speed up future validations
validationResultCache.set(cacheKey, errors);
return errors;
}, []);
// Validate a single row
const validateRow = useCallback(async (
row: RowData<T>,
rowIndex: number,
allRows: RowData<T>[]
): Promise<Meta> => {
// Run field-level validations
const fieldErrors: Record<string, ValidationError[]> = {};
fields.forEach(field => {
const value = row[String(field.key) as keyof typeof row];
const errors = validateField(value, field as Field<T>);
if (errors.length > 0) {
fieldErrors[String(field.key)] = errors;
}
});
// Special validation for supplier and company fields - only apply if the field exists in fields
if (fields.some(field => String(field.key) === 'supplier') && isEmpty(row.supplier)) {
fieldErrors['supplier'] = [{
message: 'Supplier is required',
level: 'error',
type: ErrorType.Required
}];
}
if (fields.some(field => String(field.key) === 'company') && isEmpty(row.company)) {
fieldErrors['company'] = [{
message: 'Company is required',
level: 'error',
type: ErrorType.Required
}];
}
// Run row hook if provided
let rowHookResult: Meta = {
__index: row.__index || String(rowIndex)
};
if (rowHook) {
try {
// Call the row hook and extract only the __index property
const result = await rowHook(row, rowIndex, allRows);
rowHookResult.__index = result.__index || rowHookResult.__index;
} catch (error) {
console.error('Error in row hook:', error);
}
}
// We no longer need to merge errors since we're not storing them in the row data
// The calling code should handle storing errors in the validationErrors Map
return {
__index: row.__index || String(rowIndex)
};
}, [fields, validateField, rowHook]);
return {
validateField,
validateRow,
clearValidationCacheForField,
clearAllUniquenessCaches
};
};

View File

@@ -0,0 +1,110 @@
import { useState, useCallback, useMemo } from 'react';
import { FilterState, RowData } from './validationTypes';
import type { Fields } from '../../../types';
import { ValidationError } from '../../../types';
export const useFilterManagement = <T extends string>(
data: RowData<T>[],
fields: Fields<T>,
validationErrors: Map<number, Record<string, ValidationError[]>>
) => {
// Filter state
const [filters, setFilters] = useState<FilterState>({
searchText: "",
showErrorsOnly: false,
filterField: null,
filterValue: null,
});
// 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 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;
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) => ({
key: String(field.key),
label: field.label,
}));
}, [fields]);
// Get filter values for the selected field
const filterValues = useMemo(() => {
if (!filters.filterField) return [];
// Get unique values for the selected field
const uniqueValues = new Set<string>();
data.forEach((row) => {
const value = row[filters.filterField as keyof typeof row];
if (value !== undefined && value !== null) {
uniqueValues.add(String(value));
}
});
return Array.from(uniqueValues).map((value) => ({
value,
label: value,
}));
}, [data, filters.filterField]);
// Update filters
const updateFilters = useCallback((newFilters: Partial<FilterState>) => {
setFilters((prev) => ({
...prev,
...newFilters,
}));
}, []);
// Reset filters
const resetFilters = useCallback(() => {
setFilters({
searchText: "",
showErrorsOnly: false,
filterField: null,
filterValue: null,
});
}, []);
return {
filters,
filteredData,
filterFields,
filterValues,
updateFilters,
resetFilters
};
};

View File

@@ -0,0 +1,366 @@
import { useCallback } from 'react';
import { RowData } from './validationTypes';
import type { Field, Fields } from '../../../types';
import { ErrorType, ValidationError } from '../../../types';
export const useRowOperations = <T extends string>(
data: RowData<T>[],
fields: Fields<T>,
setData: React.Dispatch<React.SetStateAction<RowData<T>[]>>,
setValidationErrors: React.Dispatch<React.SetStateAction<Map<number, Record<string, ValidationError[]>>>>,
validateFieldFromHook: (value: any, field: Field<T>) => ValidationError[]
) => {
// 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;
}
}
// Run full validation for the field
const errors = validateFieldFromHook(value, field as unknown as Field<T>);
// 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<string, ValidationError[]> = {};
fields.forEach((field) => {
const fieldKey = String(field.key);
const value = row[fieldKey as keyof typeof row];
const errors = validateFieldFromHook(value, field as unknown as Field<T>);
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, 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);
}
}
// 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<T>
).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<T>);
// 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<string, ValidationError[]> = {};
// 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<T>);
// 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]
);
// 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]
);
return {
validateRow,
updateRow,
revalidateRows,
copyDown
};
};

View File

@@ -0,0 +1,354 @@
import { useState, useCallback } from 'react';
import { toast } from 'sonner';
import { Template, RowData, TemplateState, getApiUrl } from './validationTypes';
import { RowSelectionState } from '@tanstack/react-table';
import { ValidationError } from '../../../types';
export const useTemplateManagement = <T extends string>(
data: RowData<T>[],
setData: React.Dispatch<React.SetStateAction<RowData<T>[]>>,
rowSelection: RowSelectionState,
setValidationErrors: React.Dispatch<React.SetStateAction<Map<number, Record<string, ValidationError[]>>>>,
setRowValidationStatus: React.Dispatch<React.SetStateAction<Map<number, "pending" | "validating" | "validated" | "error">>>,
validateRow: (rowIndex: number, specificField?: string) => void,
isApplyingTemplateRef: React.MutableRefObject<boolean>
) => {
// Template state
const [templates, setTemplates] = useState<Template[]>([]);
const [isLoadingTemplates, setIsLoadingTemplates] = useState(true);
const [templateState, setTemplateState] = useState<TemplateState>({
selectedTemplateId: null,
showSaveTemplateDialog: false,
newTemplateName: "",
newTemplateType: "",
});
// Load templates
const loadTemplates = useCallback(async () => {
try {
setIsLoadingTemplates(true);
console.log("Fetching templates from:", `${getApiUrl()}/templates`);
const response = await fetch(`${getApiUrl()}/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
);
setTemplates(validTemplates);
} catch (error) {
console.error("Error fetching templates:", error);
toast.error("Failed to load templates");
} finally {
setIsLoadingTemplates(false);
}
}, []);
// Refresh templates
const refreshTemplates = useCallback(() => {
loadTemplates();
}, [loadTemplates]);
// 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<string, any> = {};
// 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<number, Record<string, ValidationError[]>>();
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<string, any>;
// 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<T>;
// 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]
);
return {
templates,
isLoadingTemplates,
templateState,
setTemplateState,
loadTemplates,
refreshTemplates,
saveTemplate,
applyTemplate,
applyTemplateToSelected
};
};

View File

@@ -0,0 +1,151 @@
import { useCallback } from 'react';
import { RowData } from './validationTypes';
import type { Fields } from '../../../types';
import { ErrorSources, ErrorType, ValidationError } from '../../../types';
export const useUniqueItemNumbersValidation = <T extends string>(
data: RowData<T>[],
fields: Fields<T>,
setValidationErrors: React.Dispatch<React.SetStateAction<Map<number, Record<string, ValidationError[]>>>>
) => {
// Update validateUniqueItemNumbers to also check for uniqueness of UPC/barcode
const validateUniqueItemNumbers = useCallback(async () => {
console.log("Validating unique fields");
// Skip if no data
if (!data.length) return;
// Track unique identifiers in maps
const uniqueFieldsMap = new Map<string, Map<string, number[]>>();
// 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
);
// Always check item_number uniqueness even if not explicitly defined
if (!uniqueFields.includes("item_number")) {
uniqueFields.push("item_number");
}
// Initialize maps for each unique field
uniqueFields.forEach((fieldKey) => {
uniqueFieldsMap.set(fieldKey, new Map<string, number[]>());
});
// Initialize batch updates
const errors = new Map<number, Record<string, ValidationError[]>>();
// Single pass through data to identify all unique values
data.forEach((row, index) => {
uniqueFields.forEach((fieldKey) => {
const value = row[fieldKey as keyof typeof row];
// Skip empty values
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) || [];
indices.push(index);
fieldMap.set(valueStr, indices);
}
});
});
// Process duplicates
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 errorObj = {
message:
validationRule?.errorMessage || `Duplicate ${fieldKey}: ${value}`,
level: validationRule?.level || ("error" as "error"),
source: ErrorSources.Table,
type: ErrorType.Unique,
};
// Add error to each row with this value
indices.forEach((rowIndex) => {
const rowErrors = errors.get(rowIndex) || {};
rowErrors[fieldKey] = [errorObj];
errors.set(rowIndex, rowErrors);
});
}
});
});
// 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) => {
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 &&
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");
}, [data, fields, setValidationErrors]);
return {
validateUniqueItemNumbers
};
};

View File

@@ -0,0 +1,132 @@
import { useCallback } from 'react';
import type { Fields } from '../../../types';
import { ErrorSources, ErrorType } from '../../../types';
import { RowData, InfoWithSource, isEmpty } from './validationTypes';
import { clearValidationCacheForField } from './useFieldValidation';
export const useUniqueValidation = <T extends string>(
fields: Fields<T>
) => {
// Additional function to explicitly validate uniqueness for specified fields
const validateUniqueField = useCallback((data: RowData<T>[], fieldKey: string) => {
// Field keys that need special handling for uniqueness
const uniquenessFields = ['item_number', 'upc', 'barcode', 'supplier_no', 'notions_no'];
// If the field doesn't need uniqueness validation, return empty errors
if (!uniquenessFields.includes(fieldKey)) {
const field = fields.find(f => String(f.key) === fieldKey);
if (!field || !field.validations?.some(v => v.rule === 'unique')) {
return new Map<number, Record<string, InfoWithSource>>();
}
}
// Create map to track errors
const uniqueErrors = new Map<number, Record<string, InfoWithSource>>();
// Find the field definition
const field = fields.find(f => String(f.key) === fieldKey);
if (!field) return uniqueErrors;
// Get validation properties
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<string, number[]>();
// Build value map
data.forEach((row, rowIndex) => {
const value = String(row[fieldKey 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, value) => {
if (rowIndexes.length > 1) {
// Skip empty values
if (!value || value.trim() === '') return;
// Add error to all duplicate rows
rowIndexes.forEach(rowIndex => {
// Create errors object if needed
if (!uniqueErrors.has(rowIndex)) {
uniqueErrors.set(rowIndex, {});
}
// Add error for this field
uniqueErrors.get(rowIndex)![fieldKey] = {
message: errorMessage,
level: level as 'info' | 'warning' | 'error',
source: ErrorSources.Table,
type: ErrorType.Unique
};
});
}
});
return uniqueErrors;
}, [fields]);
// Validate uniqueness for multiple fields
const validateUniqueFields = useCallback((data: RowData<T>[], fieldKeys: string[]) => {
// Process each field and merge results
const allErrors = new Map<number, Record<string, InfoWithSource>>();
fieldKeys.forEach(fieldKey => {
const fieldErrors = validateUniqueField(data, fieldKey);
// Merge errors
fieldErrors.forEach((errors, rowIdx) => {
if (!allErrors.has(rowIdx)) {
allErrors.set(rowIdx, {});
}
Object.assign(allErrors.get(rowIdx)!, errors);
});
});
return allErrors;
}, [validateUniqueField]);
// Run complete validation for uniqueness
const validateAllUniqueFields = useCallback((data: RowData<T>[]) => {
// Get fields requiring uniqueness validation
const uniqueFields = fields
.filter(field => field.validations?.some(v => v.rule === 'unique'))
.map(field => String(field.key));
// Also add standard unique fields that might not be explicitly marked as unique
const standardUniqueFields = ['item_number', 'upc', 'barcode', 'supplier_no', 'notions_no'];
// Combine all fields that need uniqueness validation
const allUniqueFieldKeys = [...new Set([
...uniqueFields,
...standardUniqueFields
])];
// Filter to only fields that exist in the data
const existingFields = allUniqueFieldKeys.filter(fieldKey =>
data.some(row => fieldKey in row)
);
// Validate all fields at once
return validateUniqueFields(data, existingFields);
}, [fields, validateUniqueFields]);
return {
validateUniqueField,
validateUniqueFields,
validateAllUniqueFields
};
};

View File

@@ -1,248 +1,24 @@
import { useCallback } from 'react' import { useCallback } from 'react'
import type { Field, Fields, RowHook, TableHook } from '../../../types' import type { Field, Fields, RowHook, TableHook } from '../../../types'
import type { Meta } from '../types' import { ErrorSources } from '../../../types'
import { ErrorSources, ErrorType, ValidationError } from '../../../types' import { RowData, InfoWithSource } from './validationTypes'
import { RowData } from './useValidationState' import { useFieldValidation, clearValidationCacheForField, clearAllUniquenessCaches } from './useFieldValidation'
import { useUniqueValidation } from './useUniqueValidation'
// Define InfoWithSource to match the expected structure
// Make sure source is required (not optional)
export interface InfoWithSource {
message: string;
level: 'info' | 'warning' | 'error';
source: ErrorSources;
type: ErrorType;
}
// Shared utility function for checking empty values - defined once to avoid duplication
const isEmpty = (value: any): boolean =>
value === undefined ||
value === null ||
value === '' ||
(Array.isArray(value) && value.length === 0) ||
(typeof value === 'object' && !Array.isArray(value) && Object.keys(value).length === 0);
// Create a cache for validation results to avoid repeated validation of the same data
const validationResultCache = new Map();
// Add a function to clear cache for a specific field value
export const clearValidationCacheForField = (fieldKey: string) => {
// 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
export const clearAllUniquenessCaches = () => {
// Clear cache for common unique fields
['item_number', 'upc', 'barcode', 'supplier_no', 'notions_no'].forEach(fieldKey => {
clearValidationCacheForField(fieldKey);
});
// Also clear any cache entries that might involve uniqueness validation
validationResultCache.forEach((_, key) => {
if (key.includes('unique')) {
validationResultCache.delete(key);
}
});
};
// Main validation hook that brings together field and uniqueness validation
export const useValidation = <T extends string>( export const useValidation = <T extends string>(
fields: Fields<T>, fields: Fields<T>,
rowHook?: RowHook<T>, rowHook?: RowHook<T>,
tableHook?: TableHook<T> tableHook?: TableHook<T>
) => { ) => {
// Validate a single field // Use the field validation hook
const validateField = useCallback(( const { validateField, validateRow } = useFieldValidation(fields, rowHook, tableHook);
value: any,
field: Field<T>
): ValidationError[] => {
const errors: ValidationError[] = []
if (!field.validations) return errors // Use the uniqueness validation hook
const {
// Create a cache key using field key, value, and validation rules validateUniqueField,
const cacheKey = `${field.key}-${String(value)}-${JSON.stringify(field.validations)}`; validateAllUniqueFields
} = useUniqueValidation(fields);
// Check cache first to avoid redundant validation
if (validationResultCache.has(cacheKey)) {
return validationResultCache.get(cacheKey) || [];
}
field.validations.forEach(validation => {
switch (validation.rule) {
case 'required':
// Use the shared isEmpty function
if (isEmpty(value)) {
errors.push({
message: validation.errorMessage || 'This field is required',
level: validation.level || 'error',
type: ErrorType.Required
})
}
break
case 'unique':
// Unique validation happens at table level, not here
break
case 'regex':
if (value !== undefined && value !== null && value !== '') {
try {
const regex = new RegExp(validation.value, validation.flags)
if (!regex.test(String(value))) {
errors.push({
message: validation.errorMessage,
level: validation.level || 'error',
type: ErrorType.Regex
})
}
} catch (error) {
console.error('Invalid regex in validation:', error)
}
}
break
}
})
// Store results in cache to speed up future validations
validationResultCache.set(cacheKey, errors);
return errors
}, [])
// Validate a single row
const validateRow = useCallback(async (
row: RowData<T>,
rowIndex: number,
allRows: RowData<T>[]
): Promise<Meta> => {
// Run field-level validations
const fieldErrors: Record<string, ValidationError[]> = {}
// Use the shared isEmpty function
fields.forEach(field => {
const value = row[String(field.key) as keyof typeof row]
const errors = validateField(value, field as Field<T>)
if (errors.length > 0) {
fieldErrors[String(field.key)] = errors
}
})
// Special validation for supplier and company fields - only apply if the field exists in fields
if (fields.some(field => String(field.key) === 'supplier') && isEmpty(row.supplier)) {
fieldErrors['supplier'] = [{
message: 'Supplier is required',
level: 'error',
type: ErrorType.Required
}]
}
if (fields.some(field => String(field.key) === 'company') && isEmpty(row.company)) {
fieldErrors['company'] = [{
message: 'Company is required',
level: 'error',
type: ErrorType.Required
}]
}
// Run row hook if provided
let rowHookResult: Meta = {
__index: row.__index || String(rowIndex)
}
if (rowHook) {
try {
// Call the row hook and extract only the __index property
const result = await rowHook(row, rowIndex, allRows);
rowHookResult.__index = result.__index || rowHookResult.__index;
} catch (error) {
console.error('Error in row hook:', error)
}
}
// We no longer need to merge errors since we're not storing them in the row data
// The calling code should handle storing errors in the validationErrors Map
return {
__index: row.__index || String(rowIndex)
}
}, [fields, validateField, rowHook])
// Additional function to explicitly validate uniqueness for specified fields
const validateUniqueField = useCallback((data: RowData<T>[], fieldKey: string) => {
// Field keys that need special handling for uniqueness
const uniquenessFields = ['item_number', 'upc', 'barcode', 'supplier_no', 'notions_no'];
// If the field doesn't need uniqueness validation, return empty errors
if (!uniquenessFields.includes(fieldKey)) {
const field = fields.find(f => String(f.key) === fieldKey);
if (!field || !field.validations?.some(v => v.rule === 'unique')) {
return new Map<number, Record<string, InfoWithSource>>();
}
}
// Create map to track errors
const uniqueErrors = new Map<number, Record<string, InfoWithSource>>();
// Find the field definition
const field = fields.find(f => String(f.key) === fieldKey);
if (!field) return uniqueErrors;
// Get validation properties
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<string, number[]>();
// Build value map
data.forEach((row, rowIndex) => {
const value = String(row[fieldKey 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, value) => {
if (rowIndexes.length > 1) {
// Skip empty values
if (!value || value.trim() === '') return;
// Add error to all duplicate rows
rowIndexes.forEach(rowIndex => {
// Create errors object if needed
if (!uniqueErrors.has(rowIndex)) {
uniqueErrors.set(rowIndex, {});
}
// Add error for this field
uniqueErrors.get(rowIndex)![fieldKey] = {
message: errorMessage,
level: level as 'info' | 'warning' | 'error',
source: ErrorSources.Table,
type: ErrorType.Unique
};
});
}
});
return uniqueErrors;
}, [fields]);
// Run complete validation // Run complete validation
const validateData = useCallback(async (data: RowData<T>[], fieldToUpdate?: { rowIndex: number, fieldKey: string }) => { const validateData = useCallback(async (data: RowData<T>[], fieldToUpdate?: { rowIndex: number, fieldKey: string }) => {
@@ -341,9 +117,6 @@ export const useValidation = <T extends string>(
// Full validation - all fields for all rows // Full validation - all fields for all rows
console.log('Running full validation for all fields and rows'); console.log('Running full validation for all fields and rows');
// Clear validation cache for full validation
validationResultCache.clear();
// Process each row for field-level validations // Process each row for field-level validations
for (let rowIndex = 0; rowIndex < data.length; rowIndex++) { for (let rowIndex = 0; rowIndex < data.length; rowIndex++) {
const row = data[rowIndex]; const row = data[rowIndex];
@@ -371,39 +144,16 @@ export const useValidation = <T extends string>(
} }
} }
// Get fields requiring uniqueness validation // Validate all unique fields
const uniqueFields = fields.filter(field => const uniqueErrors = validateAllUniqueFields(data);
field.validations?.some(v => v.rule === 'unique')
);
// Also add standard unique fields that might not be explicitly marked as unique // Merge in unique errors
const standardUniqueFields = ['item_number', 'upc', 'barcode', 'supplier_no', 'notions_no'];
// Combine all fields that need uniqueness validation
const allUniqueFieldKeys = new Set([
...uniqueFields.map(field => String(field.key)),
...standardUniqueFields
]);
// Log uniqueness validation fields
console.log('Validating unique fields:', Array.from(allUniqueFieldKeys));
// Run uniqueness validation for each unique field
allUniqueFieldKeys.forEach(fieldKey => {
// Check if this field exists in the data
const hasField = data.some(row => fieldKey in row);
if (!hasField) return;
const uniqueErrors = validateUniqueField(data, fieldKey);
// Add unique errors to validation errors
uniqueErrors.forEach((errors, rowIdx) => { uniqueErrors.forEach((errors, rowIdx) => {
if (!validationErrors.has(rowIdx)) { if (!validationErrors.has(rowIdx)) {
validationErrors.set(rowIdx, {}); validationErrors.set(rowIdx, {});
} }
Object.assign(validationErrors.get(rowIdx)!, errors); Object.assign(validationErrors.get(rowIdx)!, errors);
}); });
});
console.log('Uniqueness validation complete'); console.log('Uniqueness validation complete');
} }
@@ -412,7 +162,7 @@ export const useValidation = <T extends string>(
data, data,
validationErrors validationErrors
}; };
}, [fields, validateField, validateUniqueField]); }, [fields, validateField, validateUniqueField, validateAllUniqueFields]);
return { return {
validateData, validateData,
@@ -421,5 +171,5 @@ export const useValidation = <T extends string>(
validateUniqueField, validateUniqueField,
clearValidationCacheForField, clearValidationCacheForField,
clearAllUniquenessCaches clearAllUniquenessCaches
} };
} }

View File

@@ -0,0 +1,101 @@
import type { Data, Field } from "../../../types";
import { ErrorSources, ErrorType, ValidationError } from "../../../types";
import config from "@/config";
import { RowSelectionState } from "@tanstack/react-table";
// Define the Props interface for ValidationStepNew
export interface Props<T extends string> {
initialData: RowData<T>[];
file?: File;
onBack?: () => void;
onNext?: (data: RowData<T>[]) => void;
isFromScratch?: boolean;
}
// Extended Data type with meta information
export type RowData<T extends string> = Data<T> & {
__index?: string;
__template?: string;
__original?: Record<string, any>;
__corrected?: Record<string, any>;
__changes?: Record<string, boolean>;
upc?: string;
barcode?: string;
supplier?: string;
company?: string;
item_number?: string;
[key: string]: any; // Allow any string key for dynamic fields
};
// Template interface
export interface Template {
id: number;
company: string;
product_type: string;
[key: string]: string | number | boolean | undefined;
}
// Props for the useValidationState hook
export interface ValidationStateProps<T extends string> extends Props<T> {}
// Interface for validation results
export interface ValidationResult {
error?: boolean;
message?: string;
data?: Record<string, any>;
type?: ErrorType;
source?: ErrorSources;
}
// Filter state interface
export interface FilterState {
searchText: string;
showErrorsOnly: boolean;
filterField: string | null;
filterValue: string | null;
}
// UI validation state interface for useUpcValidation
export interface ValidationState {
validatingCells: Set<string>; // Using rowIndex-fieldKey as identifier
itemNumbers: Map<number, string>; // Using rowIndex as key
validatingRows: Set<number>; // Rows currently being validated
activeValidations: Set<string>; // Active validations
}
// InfoWithSource interface for validation errors
export interface InfoWithSource {
message: string;
level: 'info' | 'warning' | 'error';
source: ErrorSources;
type: ErrorType;
}
// Template state interface
export interface TemplateState {
selectedTemplateId: string | null;
showSaveTemplateDialog: boolean;
newTemplateName: string;
newTemplateType: string;
}
// Add config at the top of the file
// Import the config or access it through window
declare global {
interface Window {
config?: {
apiUrl: string;
};
}
}
// Use a helper to get API URL consistently
export const getApiUrl = () => config.apiUrl;
// Shared utility function for checking empty values
export const isEmpty = (value: any): boolean =>
value === undefined ||
value === null ||
value === '' ||
(Array.isArray(value) && value.length === 0) ||
(typeof value === 'object' && !Array.isArray(value) && Object.keys(value).length === 0);

View File

@@ -1,42 +0,0 @@
import { ErrorType } from '../types/index'
/**
* Converts an InfoWithSource or similar error object to our Error type
* @param error The error object to convert
* @returns Our standardized Error object
*/
export const convertToError = (error: any): ErrorType => {
return {
message: typeof error.message === 'string' ? error.message : String(error.message || ''),
level: error.level || 'error',
source: error.source || 'row',
type: error.type || 'custom'
}
}
/**
* Safely convert an error or array of errors to our Error[] format
* @param errors The error or array of errors to convert
* @returns Array of our Error objects
*/
export const convertToErrorArray = (errors: any): ErrorType[] => {
if (Array.isArray(errors)) {
return errors.map(convertToError)
}
return [convertToError(errors)]
}
/**
* Converts a record of errors to our standardized format
* @param errorRecord Record with string keys and error values
* @returns Standardized error record
*/
export const convertErrorRecord = (errorRecord: Record<string, any>): Record<string, ErrorType[]> => {
const result: Record<string, ErrorType[]> = {}
Object.entries(errorRecord).forEach(([key, errors]) => {
result[key] = convertToErrorArray(errors)
})
return result
}

View File

@@ -1,74 +0,0 @@
import { toast } from 'sonner'
/**
* Placeholder for validating UPC codes
* @param upcValue UPC value to validate
* @returns Validation result
*/
export const validateUpc = async (upcValue: string): Promise<any> => {
// Basic validation - UPC should be 12-14 digits
if (!/^\d{12,14}$/.test(upcValue)) {
toast.error('Invalid UPC format. UPC should be 12-14 digits.')
return {
error: true,
message: 'Invalid UPC format'
}
}
// In a real implementation, call an API to validate the UPC
// For now, just return a successful result
return {
error: false,
data: {
// Mock data that would be returned from the API
item_number: `ITEM-${upcValue.substring(0, 6)}`,
sku: `SKU-${upcValue.substring(0, 4)}`,
description: `Sample Product ${upcValue.substring(0, 4)}`
}
}
}
/**
* Generates an item number for a UPC
* @param upcValue UPC value
* @returns Generated item number
*/
export const generateItemNumber = (upcValue: string): string => {
// Simple item number generation logic
return `ITEM-${upcValue.substring(0, 6)}`
}
/**
* Placeholder for handling UPC validation process
* @param upcValue UPC value to validate
* @param rowIndex Row index being validated
* @param updateRow Function to update row data
*/
export const handleUpcValidation = async (
upcValue: string,
rowIndex: number,
updateRow: (rowIndex: number, key: string, value: any) => void
): Promise<void> => {
try {
// Validate the UPC
const result = await validateUpc(upcValue)
if (result.error) {
toast.error(result.message || 'UPC validation failed')
return
}
// Update row with the validation result data
if (result.data) {
// Update each field returned from the API
Object.entries(result.data).forEach(([key, value]) => {
updateRow(rowIndex, key, value)
})
toast.success('UPC validated successfully')
}
} catch (error) {
console.error('Error validating UPC:', error)
toast.error('Failed to validate UPC')
}
}

View File

@@ -1,44 +0,0 @@
/**
* Helper functions for validation that ensure proper error objects
*/
// Create a standard error object
export const createError = (message, level = 'error', source = 'row') => {
return { message, level, source };
};
// Convert any error to standard format
export const convertError = (error) => {
if (!error) return createError('Unknown error');
if (typeof error === 'string') {
return createError(error);
}
return {
message: error.message || 'Unknown error',
level: error.level || 'error',
source: error.source || 'row'
};
};
// Convert array of errors or single error to array
export const convertToErrorArray = (errors) => {
if (Array.isArray(errors)) {
return errors.map(convertError);
}
return [convertError(errors)];
};
// Convert a record of errors to standard format
export const convertErrorRecord = (errorRecord) => {
const result = {};
if (!errorRecord) return result;
Object.entries(errorRecord).forEach(([key, errors]) => {
result[key] = convertToErrorArray(errors);
});
return result;
};

View File

@@ -1,184 +0,0 @@
import { Field, Data, ErrorSources, ErrorType as ValidationErrorType } from '../../../types'
import { ErrorType } from '../types/index'
/**
* Formats a price value to a consistent format
* @param value The price value to format
* @returns The formatted price string
*/
export const formatPrice = (value: string | number): string => {
if (!value) return ''
// Convert to string and clean
const numericValue = String(value).replace(/[^\d.]/g, '')
// Parse the number
const number = parseFloat(numericValue)
if (isNaN(number)) return ''
// Format as currency
return number.toLocaleString('en-US', {
minimumFractionDigits: 2,
maximumFractionDigits: 2
})
}
/**
* Checks if a field is a price field
* @param field The field to check
* @returns True if the field is a price field
*/
export const isPriceField = (field: Field<any>): boolean => {
const fieldType = field.fieldType;
return (fieldType.type === 'input' || fieldType.type === 'multi-input') &&
'price' in fieldType &&
!!fieldType.price;
}
/**
* Determines if a field is a multi-input type
* @param fieldType The field type to check
* @returns True if the field is a multi-input type
*/
export const isMultiInputType = (fieldType: any): boolean => {
return fieldType.type === 'multi-input'
}
/**
* Gets the separator for multi-input fields
* @param fieldType The field type
* @returns The separator string
*/
export const getMultiInputSeparator = (fieldType: any): string => {
if (isMultiInputType(fieldType) && fieldType.separator) {
return fieldType.separator
}
return ','
}
/**
* Performs regex validation on a value
* @param value The value to validate
* @param regex The regex pattern
* @param flags Regex flags
* @returns True if validation passes
*/
export const validateRegex = (value: any, regex: string, flags?: string): boolean => {
if (value === undefined || value === null || value === '') return true
try {
const regexObj = new RegExp(regex, flags)
return regexObj.test(String(value))
} catch (error) {
console.error('Invalid regex in validation:', error)
return false
}
}
/**
* Creates a validation error object
* @param message Error message
* @param level Error level
* @param source Error source
* @param type Error type
* @returns Error object
*/
export const createError = (
message: string,
level: 'info' | 'warning' | 'error' = 'error',
source: ErrorSources = ErrorSources.Row,
type: ValidationErrorType = ValidationErrorType.Custom
): ErrorType => {
return {
message,
level,
source,
type
} as ErrorType
}
/**
* Formats a display value based on field type
* @param value The value to format
* @param field The field definition
* @returns Formatted display value
*/
export const getDisplayValue = (value: any, field: Field<any>): string => {
if (value === undefined || value === null) return ''
// Handle price fields
if (isPriceField(field)) {
return formatPrice(value)
}
// Handle multi-input fields
if (isMultiInputType(field.fieldType)) {
if (Array.isArray(value)) {
return value.join(`${getMultiInputSeparator(field.fieldType)} `)
}
}
// Handle boolean values
if (typeof value === 'boolean') {
return value ? 'Yes' : 'No'
}
return String(value)
}
/**
* Validates supplier and company fields
* @param row The data row
* @returns Object with errors for invalid fields
*/
export const validateSpecialFields = <T extends string>(row: Data<T>): Record<string, ErrorType[]> => {
const errors: Record<string, ErrorType[]> = {}
// Validate supplier field
if (!row.supplier) {
errors['supplier'] = [{
message: 'Supplier is required',
level: 'error',
source: ErrorSources.Row,
type: ValidationErrorType.Required
}]
}
// Validate company field
if (!row.company) {
errors['company'] = [{
message: 'Company is required',
level: 'error',
source: ErrorSources.Row,
type: ValidationErrorType.Required
}]
}
return errors
}
/**
* Merges multiple error objects
* @param errors Array of error objects to merge
* @returns Merged error object
*/
export const mergeErrors = (...errors: Record<string, ErrorType[]>[]): Record<string, ErrorType[]> => {
const merged: Record<string, ErrorType[]> = {}
errors.forEach(errorObj => {
if (!errorObj) return
Object.entries(errorObj).forEach(([key, errs]) => {
if (!merged[key]) {
merged[key] = []
}
merged[key] = [
...merged[key],
...(Array.isArray(errs) ? errs : [errs as ErrorType])
]
})
})
return merged
}