Move product line fetching out of ValidationContainer, clean up some unused files

This commit is contained in:
2025-03-17 16:24:47 -04:00
parent aa9664c459
commit 136f767309
12 changed files with 787 additions and 986 deletions

View File

@@ -1,87 +0,0 @@
import React, { useState } from 'react'
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog"
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
interface SaveTemplateDialogProps {
isOpen: boolean
onClose: () => void
onSave: (company: string, productType: string) => void
}
const SaveTemplateDialog: React.FC<SaveTemplateDialogProps> = ({
isOpen,
onClose,
onSave,
}) => {
const [company, setCompany] = useState("")
const [productType, setProductType] = useState("")
return (
<Dialog open={isOpen} onOpenChange={(open) => {
if (!open) {
onClose()
setCompany("")
setProductType("")
}
}}>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle>Save as Template</DialogTitle>
<DialogDescription>
Enter the company and product type for this template.
</DialogDescription>
</DialogHeader>
<div className="space-y-4 py-4">
<div className="space-y-2">
<label htmlFor="company" className="text-sm font-medium">
Company
</label>
<Input
id="company"
value={company}
onChange={(e) => setCompany(e.target.value)}
placeholder="Enter company name"
/>
</div>
<div className="space-y-2">
<label htmlFor="productType" className="text-sm font-medium">
Product Type
</label>
<Input
id="productType"
value={productType}
onChange={(e) => setProductType(e.target.value)}
placeholder="Enter product type"
/>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={onClose}>
Cancel
</Button>
<Button
onClick={() => {
onSave(company, productType)
onClose()
setCompany("")
setProductType("")
}}
disabled={!company || !productType}
>
Save Template
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)
}
export default SaveTemplateDialog

View File

@@ -1,212 +0,0 @@
import React, { useState } from 'react'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import {
Popover,
PopoverContent,
PopoverTrigger
} from '@/components/ui/popover'
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList
} from '@/components/ui/command'
import { ChevronsUpDown, Check } from 'lucide-react'
import { cn } from '@/lib/utils'
import { Template } from '../hooks/useValidationState'
import { toast } from 'sonner'
interface TemplateManagerProps {
templates: Template[]
selectedTemplateId: string | null
onSelectTemplate: (templateId: string | null) => void
onSaveTemplate: (name: string, type: string) => void
onApplyTemplate: (templateId: string) => void
showDialog: boolean
onCloseDialog: () => void
selectedCount: number
}
const TemplateManager: React.FC<TemplateManagerProps> = ({
templates,
selectedTemplateId,
onSelectTemplate,
onSaveTemplate,
onApplyTemplate,
showDialog,
onCloseDialog,
selectedCount,
}) => {
const [templateName, setTemplateName] = useState('')
const [templateType, setTemplateType] = useState('')
const [open, setOpen] = useState(false)
const [searchQuery, setSearchQuery] = useState('')
// Filter templates based on search
const filteredTemplates = searchQuery
? templates.filter(template =>
template.product_type.toLowerCase().includes(searchQuery.toLowerCase()) ||
template.company.toLowerCase().includes(searchQuery.toLowerCase())
)
: templates
const handleSaveTemplate = () => {
if (!templateName.trim()) {
toast.error('Please enter a template name')
return
}
if (!templateType.trim()) {
toast.error('Please select a template type')
return
}
onSaveTemplate(templateName, templateType)
setTemplateName('')
setTemplateType('')
}
// Get display text for template
const getTemplateDisplayText = (template: Template) => {
return `${template.product_type} - ${template.company}`
}
// Find the currently selected template
const selectedTemplate = templates.find(t => t.id.toString() === selectedTemplateId)
return (
<>
<div className="space-y-4">
<div className="flex flex-col space-y-2">
<label className="text-sm font-medium">Template</label>
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={open}
className="justify-between w-full"
>
{selectedTemplate
? getTemplateDisplayText(selectedTemplate)
: "Select a template"}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="p-0 w-[350px]">
<Command>
<CommandInput
placeholder="Search templates..."
value={searchQuery}
onValueChange={setSearchQuery}
/>
<CommandList>
<CommandEmpty>No templates found.</CommandEmpty>
<CommandGroup>
{filteredTemplates.map((template) => (
<CommandItem
key={template.id}
value={template.id.toString()}
onSelect={(value) => {
onSelectTemplate(value)
setOpen(false)
setSearchQuery('')
}}
>
<Check
className={cn(
"mr-2 h-4 w-4",
selectedTemplateId === template.id.toString()
? "opacity-100"
: "opacity-0"
)}
/>
{getTemplateDisplayText(template)}
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
</div>
<div className="flex space-x-2">
<Button
variant="outline"
onClick={() => selectedTemplateId && onApplyTemplate(selectedTemplateId)}
disabled={!selectedTemplateId || selectedCount === 0}
className="flex-1"
>
Apply Template
</Button>
<Button
variant="outline"
onClick={onCloseDialog}
disabled={selectedCount === 0}
className="flex-1"
>
Save as Template
</Button>
</div>
</div>
{/* Template Save Dialog */}
<Dialog open={showDialog} onOpenChange={onCloseDialog}>
<DialogContent>
<DialogHeader>
<DialogTitle>Save Template</DialogTitle>
<DialogDescription>
Create a template from the selected row.
</DialogDescription>
</DialogHeader>
<div className="space-y-4 py-4">
<div className="flex flex-col space-y-2">
<label className="text-sm font-medium">Company</label>
<Input
value={templateName}
onChange={(e) => setTemplateName(e.target.value)}
placeholder="Enter company name"
/>
</div>
<div className="flex flex-col space-y-2">
<label className="text-sm font-medium">Product Type</label>
<Input
value={templateType}
onChange={(e) => setTemplateType(e.target.value)}
placeholder="Enter product type"
/>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={onCloseDialog}>
Cancel
</Button>
<Button
onClick={handleSaveTemplate}
disabled={!templateName.trim() || !templateType.trim()}
>
Save Template
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</>
)
}
export default TemplateManager

View File

@@ -16,6 +16,7 @@ import { TemplateForm } from '@/components/templates/TemplateForm'
import axios from 'axios'
import { RowSelectionState } from '@tanstack/react-table'
import { useUpcValidation } from '../hooks/useUpcValidation'
import { useProductLinesFetching } from '../hooks/useProductLinesFetching'
/**
* ValidationContainer component - the main wrapper for the validation step
@@ -60,18 +61,15 @@ const ValidationContainer = <T extends string>({
fields,
isLoadingTemplates } = validationState
// Add state for tracking product lines and sublines per row
const [rowProductLines, setRowProductLines] = useState<Record<string, any[]>>({});
const [rowSublines, setRowSublines] = useState<Record<string, any[]>>({});
// These variables are used in the fetchProductLines and fetchSublines functions
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [isLoadingLines, setIsLoadingLines] = useState<Record<string, boolean>>({});
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [isLoadingSublines, setIsLoadingSublines] = useState<Record<string, boolean>>({});
// Add caches for product lines and sublines by company/line ID
const [companyLinesCache, setCompanyLinesCache] = useState<Record<string, any[]>>({});
const [lineSublineCache, setLineSublineCache] = useState<Record<string, any[]>>({});
// Use product lines fetching hook
const {
rowProductLines,
rowSublines,
isLoadingLines,
isLoadingSublines,
fetchProductLines,
fetchSublines
} = useProductLinesFetching(data);
// Add state for tracking cells in loading state
const [validatingCells, setValidatingCells] = useState<Set<string>>(new Set());
@@ -85,556 +83,6 @@ const ValidationContainer = <T extends string>({
// Apply all pending updates to the data state
const applyItemNumbersToData = upcValidation.applyItemNumbersToData;
// Function to fetch product lines for a specific company - memoized
const fetchProductLines = useCallback(async (rowIndex: string | number, companyId: string) => {
try {
// Only fetch if we have a valid company ID
if (!companyId) return;
console.log(`Fetching product lines for row ${rowIndex}, company ${companyId}`);
// Check if we already have this company's lines in the cache
if (companyLinesCache[companyId]) {
console.log(`Using cached product lines for company ${companyId}`);
// Use cached data
setRowProductLines(prev => ({ ...prev, [rowIndex]: companyLinesCache[companyId] }));
return companyLinesCache[companyId];
}
// Set loading state for this row
setIsLoadingLines(prev => ({ ...prev, [rowIndex]: true }));
// Fetch product lines from API
const productLinesUrl = `/api/import/product-lines/${companyId}`;
console.log(`Fetching from URL: ${productLinesUrl}`);
const response = await axios.get(productLinesUrl);
if (response.status !== 200) {
throw new Error(`Failed to fetch product lines: ${response.status}`);
}
const productLines = response.data;
console.log(`Received ${productLines.length} product lines for company ${companyId}`);
// Store in company cache
setCompanyLinesCache(prev => ({ ...prev, [companyId]: productLines }));
// Store for this specific row
setRowProductLines(prev => ({ ...prev, [rowIndex]: productLines }));
return productLines;
} catch (error) {
console.error(`Error fetching product lines for company ${companyId}:`, error);
// Set empty array for this company to prevent repeated failed requests
setCompanyLinesCache(prev => ({ ...prev, [companyId]: [] }));
// Store empty array for this specific row
setRowProductLines(prev => ({ ...prev, [rowIndex]: [] }));
return [];
} finally {
// Clear loading state
setIsLoadingLines(prev => ({ ...prev, [rowIndex]: false }));
}
}, [companyLinesCache]);
// Function to fetch sublines for a specific line - memoized
const fetchSublines = useCallback(async (rowIndex: string | number, lineId: string) => {
try {
// Only fetch if we have a valid line ID
if (!lineId) return;
console.log(`Fetching sublines for row ${rowIndex}, line ${lineId}`);
// Check if we already have this line's sublines in the cache
if (lineSublineCache[lineId]) {
console.log(`Using cached sublines for line ${lineId}`);
// Use cached data
setRowSublines(prev => ({ ...prev, [rowIndex]: lineSublineCache[lineId] }));
return lineSublineCache[lineId];
}
// Set loading state for this row
setIsLoadingSublines(prev => ({ ...prev, [rowIndex]: true }));
// Fetch sublines from API
const sublinesUrl = `/api/import/sublines/${lineId}`;
console.log(`Fetching from URL: ${sublinesUrl}`);
const response = await axios.get(sublinesUrl);
if (response.status !== 200) {
throw new Error(`Failed to fetch sublines: ${response.status}`);
}
const sublines = response.data;
console.log(`Received ${sublines.length} sublines for line ${lineId}`);
// Store in line cache
setLineSublineCache(prev => ({ ...prev, [lineId]: sublines }));
// Store for this specific row
setRowSublines(prev => ({ ...prev, [rowIndex]: sublines }));
return sublines;
} catch (error) {
console.error(`Error fetching sublines for line ${lineId}:`, error);
// Set empty array for this line to prevent repeated failed requests
setLineSublineCache(prev => ({ ...prev, [lineId]: [] }));
// Store empty array for this specific row
setRowSublines(prev => ({ ...prev, [rowIndex]: [] }));
return [];
} finally {
// Clear loading state
setIsLoadingSublines(prev => ({ ...prev, [rowIndex]: false }));
}
}, [lineSublineCache]);
// Enhanced updateRow function - memoized
const enhancedUpdateRow = useCallback(async (rowIndex: number, fieldKey: T, value: any) => {
// Process value before updating data
console.log(`enhancedUpdateRow called: rowIndex=${rowIndex}, fieldKey=${fieldKey}, value=`, value);
let processedValue = value;
// Strip dollar signs from price fields
if ((fieldKey === 'msrp' || fieldKey === 'cost_each') && typeof value === 'string') {
processedValue = value.replace(/[$,]/g, '');
// Also ensure it's a valid number
const numValue = parseFloat(processedValue);
if (!isNaN(numValue)) {
processedValue = numValue.toFixed(2);
}
}
// Save current scroll position
const scrollPosition = {
left: window.scrollX,
top: window.scrollY
};
// Find the original index in the data array
const rowData = filteredData[rowIndex];
const originalIndex = data.findIndex(item => item.__index === rowData?.__index);
if (originalIndex === -1) {
// If we can't find the original row, just do a simple update
updateRow(rowIndex, fieldKey, processedValue);
} else {
// Update the data directly
setData(prevData => {
const newData = [...prevData];
const updatedRow = {
...newData[originalIndex],
[fieldKey]: processedValue
};
newData[originalIndex] = updatedRow;
return newData;
});
}
// Restore scroll position after update
setTimeout(() => {
window.scrollTo(scrollPosition.left, scrollPosition.top);
}, 0);
// Now handle any additional logic for specific fields
if (fieldKey === 'company' && value) {
// Clear any existing line/subline values for this row if company changes
if (originalIndex !== -1) {
setData(prevData => {
const newData = [...prevData];
newData[originalIndex] = {
...newData[originalIndex],
line: undefined,
subline: undefined
};
return newData;
});
}
// Use cached product lines if available, otherwise fetch
if (rowData && rowData.__index) {
const companyId = value.toString();
if (companyLinesCache[companyId]) {
// Use cached data
console.log(`Using cached product lines for company ${companyId}`);
setRowProductLines(prev => ({
...prev,
[rowData.__index as string]: companyLinesCache[companyId]
}));
} else {
// Fetch product lines for the new company
setTimeout(async () => {
if (value !== undefined) {
await fetchProductLines(rowData.__index as string, companyId);
}
}, 0);
}
}
}
// If updating supplier field AND there's a UPC value, validate UPC
if (fieldKey === 'supplier' && value && rowData) {
const rowDataAny = rowData as Record<string, any>;
if (rowDataAny.upc || rowDataAny.barcode) {
const upcValue = rowDataAny.upc || rowDataAny.barcode;
try {
// Mark the item_number cell as being validated
setValidatingCells(prev => {
const newSet = new Set(prev);
newSet.add(`${rowIndex}-item_number`);
return newSet;
});
// Use supplier ID (the value being set) to validate UPC
await upcValidation.validateUpc(rowIndex, value.toString(), upcValue.toString());
} catch (error) {
console.error('Error validating UPC:', error);
} finally {
// Clear validation state for the item_number cell
setValidatingCells(prev => {
const newSet = new Set(prev);
newSet.delete(`${rowIndex}-item_number`);
return newSet;
});
}
}
}
// If updating line field, fetch sublines
if (fieldKey === 'line' && value) {
// Clear any existing subline value for this row
if (originalIndex !== -1) {
setData(prevData => {
const newData = [...prevData];
newData[originalIndex] = {
...newData[originalIndex],
subline: undefined
};
return newData;
});
}
// Use cached sublines if available, otherwise fetch
if (rowData && rowData.__index) {
const lineId = value.toString();
if (lineSublineCache[lineId]) {
// Use cached data
console.log(`Using cached sublines for line ${lineId}`);
setRowSublines(prev => ({
...prev,
[rowData.__index as string]: lineSublineCache[lineId]
}));
} else {
// Fetch sublines for the new line
setTimeout(async () => {
if (value !== undefined) {
await fetchSublines(rowData.__index as string, lineId);
}
}, 0);
}
}
}
// If updating UPC/barcode field AND there's a supplier value, validate UPC
if ((fieldKey === 'upc' || fieldKey === 'barcode') && value && rowData) {
const rowDataAny = rowData as Record<string, any>;
if (rowDataAny.supplier) {
try {
// Mark the item_number cell as being validated
setValidatingCells(prev => {
const newSet = new Set(prev);
newSet.add(`${rowIndex}-item_number`);
return newSet;
});
// Use supplier ID from the row data to validate UPC
await upcValidation.validateUpc(rowIndex, rowDataAny.supplier.toString(), value.toString());
} catch (error) {
console.error('Error validating UPC:', error);
} finally {
// Clear validation state for the item_number cell
setValidatingCells(prev => {
const newSet = new Set(prev);
newSet.delete(`${rowIndex}-item_number`);
return newSet;
});
}
}
}
}, [data, filteredData, updateRow, fetchProductLines, fetchSublines, setData, companyLinesCache, lineSublineCache, upcValidation]);
// When data changes, fetch product lines and sublines for rows that have company/line values
useEffect(() => {
// Skip if there's no data
if (!data.length) return;
console.log("Starting to fetch product lines and sublines");
// Group rows by company and line to minimize API calls
const companiesNeeded = new Map<string, string[]>(); // company ID -> row IDs
const linesNeeded = new Map<string, string[]>(); // line ID -> row IDs
data.forEach(row => {
const rowId = row.__index;
if (!rowId) return; // Skip rows without an index
// If row has company but no product lines fetched yet
if (row.company && !rowProductLines[rowId]) {
const companyId = row.company.toString();
if (!companiesNeeded.has(companyId)) {
companiesNeeded.set(companyId, []);
}
companiesNeeded.get(companyId)?.push(rowId);
}
// If row has line but no sublines fetched yet
if (row.line && !rowSublines[rowId]) {
const lineId = row.line.toString();
if (!linesNeeded.has(lineId)) {
linesNeeded.set(lineId, []);
}
linesNeeded.get(lineId)?.push(rowId);
}
});
console.log(`Need to fetch product lines for ${companiesNeeded.size} companies and sublines for ${linesNeeded.size} lines`);
// Create arrays to hold all fetch promises
const fetchPromises: Promise<void>[] = [];
// Set initial loading states for all affected rows
const lineLoadingUpdates: Record<string, boolean> = {};
const sublineLoadingUpdates: Record<string, boolean> = {};
// Process companies that need product lines
companiesNeeded.forEach((rowIds, companyId) => {
// Skip if already in cache
if (companyLinesCache[companyId]) {
console.log(`Using cached product lines for company ${companyId}`);
// Use cached data for all rows with this company
const lines = companyLinesCache[companyId];
const updates: Record<string, any[]> = {};
rowIds.forEach(rowId => {
updates[rowId] = lines;
});
setRowProductLines(prev => ({ ...prev, ...updates }));
return;
}
// Set loading state for all affected rows
rowIds.forEach(rowId => {
lineLoadingUpdates[rowId] = true;
});
// Create fetch promise
const fetchPromise = (async () => {
// Safety timeout to ensure loading state is cleared after 10 seconds
const timeoutId = setTimeout(() => {
console.log(`Safety timeout triggered for company ${companyId}`);
const clearLoadingUpdates: Record<string, boolean> = {};
rowIds.forEach(rowId => {
clearLoadingUpdates[rowId] = false;
});
setIsLoadingLines(prev => ({ ...prev, ...clearLoadingUpdates }));
// Set empty cache to prevent repeated requests
setCompanyLinesCache(prev => ({ ...prev, [companyId]: [] }));
// Update rows with empty array
const updates: Record<string, any[]> = {};
rowIds.forEach(rowId => {
updates[rowId] = [];
});
setRowProductLines(prev => ({ ...prev, ...updates }));
toast.error(`Timeout loading product lines for company ${companyId}`);
}, 10000);
try {
console.log(`Fetching product lines for company ${companyId} (affecting ${rowIds.length} rows)`);
// Fetch product lines from API
const productLinesUrl = `/api/import/product-lines/${companyId}`;
console.log(`Fetching from URL: ${productLinesUrl}`);
const response = await axios.get(productLinesUrl);
console.log(`Product lines API response status for company ${companyId}:`, response.status);
const productLines = response.data;
console.log(`Received ${productLines.length} product lines for company ${companyId}`);
// Store in company cache
setCompanyLinesCache(prev => ({ ...prev, [companyId]: productLines }));
// Update all rows with this company
const updates: Record<string, any[]> = {};
rowIds.forEach(rowId => {
updates[rowId] = productLines;
});
setRowProductLines(prev => ({ ...prev, ...updates }));
} catch (error) {
console.error(`Error fetching product lines for company ${companyId}:`, error);
// Set empty array for this company to prevent repeated failed requests
setCompanyLinesCache(prev => ({ ...prev, [companyId]: [] }));
// Update rows with empty array
const updates: Record<string, any[]> = {};
rowIds.forEach(rowId => {
updates[rowId] = [];
});
setRowProductLines(prev => ({ ...prev, ...updates }));
// Show error toast
toast.error(`Failed to load product lines for company ${companyId}`);
} finally {
// Clear the safety timeout
clearTimeout(timeoutId);
// Clear loading state for all affected rows
const clearLoadingUpdates: Record<string, boolean> = {};
rowIds.forEach(rowId => {
clearLoadingUpdates[rowId] = false;
});
setIsLoadingLines(prev => ({ ...prev, ...clearLoadingUpdates }));
}
})();
fetchPromises.push(fetchPromise);
});
// Process lines that need sublines
linesNeeded.forEach((rowIds, lineId) => {
// Skip if already in cache
if (lineSublineCache[lineId]) {
console.log(`Using cached sublines for line ${lineId}`);
// Use cached data for all rows with this line
const sublines = lineSublineCache[lineId];
const updates: Record<string, any[]> = {};
rowIds.forEach(rowId => {
updates[rowId] = sublines;
});
setRowSublines(prev => ({ ...prev, ...updates }));
return;
}
// Set loading state for all affected rows
rowIds.forEach(rowId => {
sublineLoadingUpdates[rowId] = true;
});
// Create fetch promise
const fetchPromise = (async () => {
// Safety timeout to ensure loading state is cleared after 10 seconds
const timeoutId = setTimeout(() => {
console.log(`Safety timeout triggered for line ${lineId}`);
const clearLoadingUpdates: Record<string, boolean> = {};
rowIds.forEach(rowId => {
clearLoadingUpdates[rowId] = false;
});
setIsLoadingSublines(prev => ({ ...prev, ...clearLoadingUpdates }));
// Set empty cache to prevent repeated requests
setLineSublineCache(prev => ({ ...prev, [lineId]: [] }));
// Update rows with empty array
const updates: Record<string, any[]> = {};
rowIds.forEach(rowId => {
updates[rowId] = [];
});
setRowSublines(prev => ({ ...prev, ...updates }));
toast.error(`Timeout loading sublines for line ${lineId}`);
}, 10000);
try {
console.log(`Fetching sublines for line ${lineId} (affecting ${rowIds.length} rows)`);
// Fetch sublines from API
const sublinesUrl = `/api/import/sublines/${lineId}`;
console.log(`Fetching from URL: ${sublinesUrl}`);
const response = await axios.get(sublinesUrl);
console.log(`Sublines API response status for line ${lineId}:`, response.status);
const sublines = response.data;
console.log(`Received ${sublines.length} sublines for line ${lineId}`);
// Store in line cache
setLineSublineCache(prev => ({ ...prev, [lineId]: sublines }));
// Update all rows with this line
const updates: Record<string, any[]> = {};
rowIds.forEach(rowId => {
updates[rowId] = sublines;
});
setRowSublines(prev => ({ ...prev, ...updates }));
} catch (error) {
console.error(`Error fetching sublines for line ${lineId}:`, error);
// Set empty array for this line to prevent repeated failed requests
setLineSublineCache(prev => ({ ...prev, [lineId]: [] }));
// Update rows with empty array
const updates: Record<string, any[]> = {};
rowIds.forEach(rowId => {
updates[rowId] = [];
});
setRowSublines(prev => ({ ...prev, ...updates }));
// Show error toast
toast.error(`Failed to load sublines for line ${lineId}`);
} finally {
// Clear the safety timeout
clearTimeout(timeoutId);
// Clear loading state for all affected rows
const clearLoadingUpdates: Record<string, boolean> = {};
rowIds.forEach(rowId => {
clearLoadingUpdates[rowId] = false;
});
setIsLoadingSublines(prev => ({ ...prev, ...clearLoadingUpdates }));
}
})();
fetchPromises.push(fetchPromise);
});
// Set initial loading states
if (Object.keys(lineLoadingUpdates).length > 0) {
console.log(`Setting loading state for ${Object.keys(lineLoadingUpdates).length} rows (product lines)`);
setIsLoadingLines(prev => ({ ...prev, ...lineLoadingUpdates }));
}
if (Object.keys(sublineLoadingUpdates).length > 0) {
console.log(`Setting loading state for ${Object.keys(sublineLoadingUpdates).length} rows (sublines)`);
setIsLoadingSublines(prev => ({ ...prev, ...sublineLoadingUpdates }));
}
// Run all fetch operations in parallel
Promise.all(fetchPromises).then(() => {
console.log("All product lines and sublines fetch operations completed");
}).catch(error => {
console.error('Error in fetch operations:', error);
});
}, [data, rowProductLines, rowSublines, companyLinesCache, lineSublineCache]);
// Use UPC validation when data changes
useEffect(() => {
// Skip if there's no data or already validated
if (data.length === 0 || upcValidation.initialValidationDone) return;
// Run validation immediately without timeout
upcValidation.validateAllUPCs();
// No cleanup needed since we're not using a timer
}, [data, upcValidation]);
// Use AI validation hook
const aiValidation = useAiValidation<T>(
data,
@@ -885,11 +333,171 @@ const ValidationContainer = <T extends string>({
setRowSelection(newSelection);
}, [setRowSelection]);
const handleUpdateRow = useCallback((rowIndex: number, key: T, value: any) => {
enhancedUpdateRow(rowIndex, key, value);
}, [enhancedUpdateRow]);
// Enhanced copy down that uses enhancedUpdateRow instead of regular updateRow
const handleUpdateRow = useCallback(async (rowIndex: number, key: T, value: any) => {
// Process value before updating data
console.log(`enhancedUpdateRow called: rowIndex=${rowIndex}, fieldKey=${key}, value=`, value);
let processedValue = value;
// Strip dollar signs from price fields
if ((key === 'msrp' || key === 'cost_each') && typeof value === 'string') {
processedValue = value.replace(/[$,]/g, '');
// Also ensure it's a valid number
const numValue = parseFloat(processedValue);
if (!isNaN(numValue)) {
processedValue = numValue.toFixed(2);
}
}
// Save current scroll position
const scrollPosition = {
left: window.scrollX,
top: window.scrollY
};
// Find the original index in the data array
const rowData = filteredData[rowIndex];
const originalIndex = data.findIndex(item => item.__index === rowData?.__index);
if (originalIndex === -1) {
// If we can't find the original row, just do a simple update
updateRow(rowIndex, key, processedValue);
} else {
// Update the data directly
setData(prevData => {
const newData = [...prevData];
const updatedRow = {
...newData[originalIndex],
[key]: processedValue
};
newData[originalIndex] = updatedRow;
return newData;
});
}
// Restore scroll position after update
setTimeout(() => {
window.scrollTo(scrollPosition.left, scrollPosition.top);
}, 0);
// Now handle any additional logic for specific fields
if (key === 'company' && value) {
// Clear any existing line/subline values for this row if company changes
if (originalIndex !== -1) {
setData(prevData => {
const newData = [...prevData];
newData[originalIndex] = {
...newData[originalIndex],
line: undefined,
subline: undefined
};
return newData;
});
}
// Use cached product lines if available, otherwise fetch
if (rowData && rowData.__index) {
const companyId = value.toString();
if (rowProductLines[companyId]) {
// Use cached data
console.log(`Using cached product lines for company ${companyId}`);
} else {
// Fetch product lines for the new company
if (value !== undefined) {
await fetchProductLines(rowData.__index as string, companyId);
}
}
}
}
// If updating supplier field AND there's a UPC value, validate UPC
if (key === 'supplier' && value && rowData) {
const rowDataAny = rowData as Record<string, any>;
if (rowDataAny.upc || rowDataAny.barcode) {
const upcValue = rowDataAny.upc || rowDataAny.barcode;
try {
// Mark the item_number cell as being validated
setValidatingCells(prev => {
const newSet = new Set(prev);
newSet.add(`${rowIndex}-item_number`);
return newSet;
});
// Use supplier ID (the value being set) to validate UPC
await upcValidation.validateUpc(rowIndex, value.toString(), upcValue.toString());
} catch (error) {
console.error('Error validating UPC:', error);
} finally {
// Clear validation state for the item_number cell
setValidatingCells(prev => {
const newSet = new Set(prev);
newSet.delete(`${rowIndex}-item_number`);
return newSet;
});
}
}
}
// If updating line field, fetch sublines
if (key === 'line' && value) {
// Clear any existing subline value for this row
if (originalIndex !== -1) {
setData(prevData => {
const newData = [...prevData];
newData[originalIndex] = {
...newData[originalIndex],
subline: undefined
};
return newData;
});
}
// Use cached sublines if available, otherwise fetch
if (rowData && rowData.__index) {
const lineId = value.toString();
if (rowSublines[lineId]) {
// Use cached data
console.log(`Using cached sublines for line ${lineId}`);
} else {
// Fetch sublines for the new line
if (value !== undefined) {
await fetchSublines(rowData.__index as string, lineId);
}
}
}
}
// If updating UPC/barcode field AND there's a supplier value, validate UPC
if ((key === 'upc' || key === 'barcode') && value && rowData) {
const rowDataAny = rowData as Record<string, any>;
if (rowDataAny.supplier) {
try {
// Mark the item_number cell as being validated
setValidatingCells(prev => {
const newSet = new Set(prev);
newSet.add(`${rowIndex}-item_number`);
return newSet;
});
// Use supplier ID from the row data to validate UPC
await upcValidation.validateUpc(rowIndex, rowDataAny.supplier.toString(), value.toString());
} catch (error) {
console.error('Error validating UPC:', error);
} finally {
// Clear validation state for the item_number cell
setValidatingCells(prev => {
const newSet = new Set(prev);
newSet.delete(`${rowIndex}-item_number`);
return newSet;
});
}
}
}
}, [data, filteredData, updateRow, fetchProductLines, fetchSublines, setData, rowProductLines, rowSublines, upcValidation]);
// Create a separate copyDown function that uses handleUpdateRow
const handleCopyDown = useCallback((rowIndex: number, fieldKey: string, endRowIndex?: number) => {
// Get the value to copy from the source row
const sourceRow = data[rowIndex];
@@ -923,7 +531,7 @@ const ValidationContainer = <T extends string>({
const targetRowIndex = rowIndex + 1 + i;
// Update the row with the copied value
enhancedUpdateRow(targetRowIndex, fieldKey as T, valueCopy);
handleUpdateRow(targetRowIndex, fieldKey as T, valueCopy);
// Remove loading state
setValidatingCells(prev => {
@@ -932,8 +540,19 @@ const ValidationContainer = <T extends string>({
return newSet;
});
});
}, [data, enhancedUpdateRow, setValidatingCells]);
}, [data, handleUpdateRow, setValidatingCells]);
// Use UPC validation when data changes
useEffect(() => {
// Skip if there's no data or already validated
if (data.length === 0 || upcValidation.initialValidationDone) return;
// Run validation immediately without timeout
upcValidation.validateAllUPCs();
// No cleanup needed since we're not using a timer
}, [data, upcValidation]);
// Memoize the enhanced validation table component
const EnhancedValidationTable = useMemo(() => React.memo((props: React.ComponentProps<typeof ValidationTable>) => {
// Create validatingCells set from validating rows, but only for item_number fields
@@ -993,7 +612,7 @@ const ValidationContainer = <T extends string>({
fields={fields as unknown as Fields<string>}
rowSelection={rowSelection}
setRowSelection={handleRowSelectionChange as React.Dispatch<React.SetStateAction<RowSelectionState>>}
updateRow={handleUpdateRow as (rowIndex: number, key: string, value: any) => void}
updateRow={handleUpdateRow as unknown as (rowIndex: number, key: string, value: any) => void}
validationErrors={validationErrors}
isValidatingUpc={isRowValidatingUpc}
validatingUpcRows={Array.from(upcValidation.validatingRows)}

View File

@@ -1,117 +0,0 @@
import { useState, useCallback, useMemo } from 'react'
import type { Fields } from '../../../types'
import { RowData } from './useValidationState'
export interface FilterState {
searchText: string
showErrorsOnly: boolean
filterField: string | null
filterValue: string | null
}
export const useFilters = <T extends string>(
data: RowData<T>[],
fields: Fields<T>,
validationErrors: Map<number, Record<string, any>>
) => {
// Filter state
const [filters, setFilters] = useState<FilterState>({
searchText: '',
showErrorsOnly: false,
filterField: null,
filterValue: null
})
// Get available filter fields
const filterFields = useMemo(() => {
return fields.map(field => ({
key: field.key,
label: field.label
}))
}, [fields])
// Get available 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
}))
}, [])
// Apply filters to data
const applyFilters = useCallback((dataToFilter: RowData<T>[]) => {
return dataToFilter.filter((row, index) => {
// Filter by search text
if (filters.searchText) {
const lowerSearchText = filters.searchText.toLowerCase()
const matchesSearch = Object.entries(row).some(([key, value]) => {
// Skip metadata fields
if (key.startsWith('__')) return false
// Check if the value contains the search text
return value !== undefined &&
value !== null &&
String(value).toLowerCase().includes(lowerSearchText)
})
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]
return fieldValue !== undefined &&
fieldValue !== null &&
String(fieldValue) === filters.filterValue
}
return true
})
}, [filters, validationErrors])
// Reset all filters
const resetFilters = useCallback(() => {
setFilters({
searchText: '',
showErrorsOnly: false,
filterField: null,
filterValue: null
})
}, [])
return {
filters,
filterFields,
filterValues,
updateFilters,
applyFilters,
resetFilters
}
}

View File

@@ -0,0 +1,391 @@
import { useState, useCallback, useEffect } from 'react'
import axios from 'axios'
import { toast } from 'sonner'
/**
* Custom hook for managing product lines and sublines fetching with caching
*/
export const useProductLinesFetching = (data: Record<string, any>[]) => {
// State for tracking product lines and sublines per row
const [rowProductLines, setRowProductLines] = useState<Record<string, any[]>>({});
const [rowSublines, setRowSublines] = useState<Record<string, any[]>>({});
// State for tracking loading states
const [isLoadingLines, setIsLoadingLines] = useState<Record<string, boolean>>({});
const [isLoadingSublines, setIsLoadingSublines] = useState<Record<string, boolean>>({});
// Add caches for product lines and sublines by company/line ID
const [companyLinesCache, setCompanyLinesCache] = useState<Record<string, any[]>>({});
const [lineSublineCache, setLineSublineCache] = useState<Record<string, any[]>>({});
// Function to fetch product lines for a specific company - memoized
const fetchProductLines = useCallback(async (rowIndex: string | number, companyId: string) => {
try {
// Only fetch if we have a valid company ID
if (!companyId) return;
console.log(`Fetching product lines for row ${rowIndex}, company ${companyId}`);
// Check if we already have this company's lines in the cache
if (companyLinesCache[companyId]) {
console.log(`Using cached product lines for company ${companyId}`);
// Use cached data
setRowProductLines(prev => ({ ...prev, [rowIndex]: companyLinesCache[companyId] }));
return companyLinesCache[companyId];
}
// Set loading state for this row
setIsLoadingLines(prev => ({ ...prev, [rowIndex]: true }));
// Fetch product lines from API
const productLinesUrl = `/api/import/product-lines/${companyId}`;
console.log(`Fetching from URL: ${productLinesUrl}`);
const response = await axios.get(productLinesUrl);
if (response.status !== 200) {
throw new Error(`Failed to fetch product lines: ${response.status}`);
}
const productLines = response.data;
console.log(`Received ${productLines.length} product lines for company ${companyId}`);
// Store in company cache
setCompanyLinesCache(prev => ({ ...prev, [companyId]: productLines }));
// Store for this specific row
setRowProductLines(prev => ({ ...prev, [rowIndex]: productLines }));
return productLines;
} catch (error) {
console.error(`Error fetching product lines for company ${companyId}:`, error);
// Set empty array for this company to prevent repeated failed requests
setCompanyLinesCache(prev => ({ ...prev, [companyId]: [] }));
// Store empty array for this specific row
setRowProductLines(prev => ({ ...prev, [rowIndex]: [] }));
return [];
} finally {
// Clear loading state
setIsLoadingLines(prev => ({ ...prev, [rowIndex]: false }));
}
}, [companyLinesCache]);
// Function to fetch sublines for a specific line - memoized
const fetchSublines = useCallback(async (rowIndex: string | number, lineId: string) => {
try {
// Only fetch if we have a valid line ID
if (!lineId) return;
console.log(`Fetching sublines for row ${rowIndex}, line ${lineId}`);
// Check if we already have this line's sublines in the cache
if (lineSublineCache[lineId]) {
console.log(`Using cached sublines for line ${lineId}`);
// Use cached data
setRowSublines(prev => ({ ...prev, [rowIndex]: lineSublineCache[lineId] }));
return lineSublineCache[lineId];
}
// Set loading state for this row
setIsLoadingSublines(prev => ({ ...prev, [rowIndex]: true }));
// Fetch sublines from API
const sublinesUrl = `/api/import/sublines/${lineId}`;
console.log(`Fetching from URL: ${sublinesUrl}`);
const response = await axios.get(sublinesUrl);
if (response.status !== 200) {
throw new Error(`Failed to fetch sublines: ${response.status}`);
}
const sublines = response.data;
console.log(`Received ${sublines.length} sublines for line ${lineId}`);
// Store in line cache
setLineSublineCache(prev => ({ ...prev, [lineId]: sublines }));
// Store for this specific row
setRowSublines(prev => ({ ...prev, [rowIndex]: sublines }));
return sublines;
} catch (error) {
console.error(`Error fetching sublines for line ${lineId}:`, error);
// Set empty array for this line to prevent repeated failed requests
setLineSublineCache(prev => ({ ...prev, [lineId]: [] }));
// Store empty array for this specific row
setRowSublines(prev => ({ ...prev, [rowIndex]: [] }));
return [];
} finally {
// Clear loading state
setIsLoadingSublines(prev => ({ ...prev, [rowIndex]: false }));
}
}, [lineSublineCache]);
// When data changes, fetch product lines and sublines for rows that have company/line values
useEffect(() => {
// Skip if there's no data
if (!data.length) return;
console.log("Starting to fetch product lines and sublines");
// Group rows by company and line to minimize API calls
const companiesNeeded = new Map<string, string[]>(); // company ID -> row IDs
const linesNeeded = new Map<string, string[]>(); // line ID -> row IDs
data.forEach(row => {
const rowId = row.__index;
if (!rowId) return; // Skip rows without an index
// If row has company but no product lines fetched yet
if (row.company && !rowProductLines[rowId]) {
const companyId = row.company.toString();
if (!companiesNeeded.has(companyId)) {
companiesNeeded.set(companyId, []);
}
companiesNeeded.get(companyId)?.push(rowId);
}
// If row has line but no sublines fetched yet
if (row.line && !rowSublines[rowId]) {
const lineId = row.line.toString();
if (!linesNeeded.has(lineId)) {
linesNeeded.set(lineId, []);
}
linesNeeded.get(lineId)?.push(rowId);
}
});
console.log(`Need to fetch product lines for ${companiesNeeded.size} companies and sublines for ${linesNeeded.size} lines`);
// Create arrays to hold all fetch promises
const fetchPromises: Promise<void>[] = [];
// Set initial loading states for all affected rows
const lineLoadingUpdates: Record<string, boolean> = {};
const sublineLoadingUpdates: Record<string, boolean> = {};
// Process companies that need product lines
companiesNeeded.forEach((rowIds, companyId) => {
// Skip if already in cache
if (companyLinesCache[companyId]) {
console.log(`Using cached product lines for company ${companyId}`);
// Use cached data for all rows with this company
const lines = companyLinesCache[companyId];
const updates: Record<string, any[]> = {};
rowIds.forEach(rowId => {
updates[rowId] = lines;
});
setRowProductLines(prev => ({ ...prev, ...updates }));
return;
}
// Set loading state for all affected rows
rowIds.forEach(rowId => {
lineLoadingUpdates[rowId] = true;
});
// Create fetch promise
const fetchPromise = (async () => {
// Safety timeout to ensure loading state is cleared after 10 seconds
const timeoutId = setTimeout(() => {
console.log(`Safety timeout triggered for company ${companyId}`);
const clearLoadingUpdates: Record<string, boolean> = {};
rowIds.forEach(rowId => {
clearLoadingUpdates[rowId] = false;
});
setIsLoadingLines(prev => ({ ...prev, ...clearLoadingUpdates }));
// Set empty cache to prevent repeated requests
setCompanyLinesCache(prev => ({ ...prev, [companyId]: [] }));
// Update rows with empty array
const updates: Record<string, any[]> = {};
rowIds.forEach(rowId => {
updates[rowId] = [];
});
setRowProductLines(prev => ({ ...prev, ...updates }));
toast.error(`Timeout loading product lines for company ${companyId}`);
}, 10000);
try {
console.log(`Fetching product lines for company ${companyId} (affecting ${rowIds.length} rows)`);
// Fetch product lines from API
const productLinesUrl = `/api/import/product-lines/${companyId}`;
console.log(`Fetching from URL: ${productLinesUrl}`);
const response = await axios.get(productLinesUrl);
console.log(`Product lines API response status for company ${companyId}:`, response.status);
const productLines = response.data;
console.log(`Received ${productLines.length} product lines for company ${companyId}`);
// Store in company cache
setCompanyLinesCache(prev => ({ ...prev, [companyId]: productLines }));
// Update all rows with this company
const updates: Record<string, any[]> = {};
rowIds.forEach(rowId => {
updates[rowId] = productLines;
});
setRowProductLines(prev => ({ ...prev, ...updates }));
} catch (error) {
console.error(`Error fetching product lines for company ${companyId}:`, error);
// Set empty array for this company to prevent repeated failed requests
setCompanyLinesCache(prev => ({ ...prev, [companyId]: [] }));
// Update rows with empty array
const updates: Record<string, any[]> = {};
rowIds.forEach(rowId => {
updates[rowId] = [];
});
setRowProductLines(prev => ({ ...prev, ...updates }));
// Show error toast
toast.error(`Failed to load product lines for company ${companyId}`);
} finally {
// Clear the safety timeout
clearTimeout(timeoutId);
// Clear loading state for all affected rows
const clearLoadingUpdates: Record<string, boolean> = {};
rowIds.forEach(rowId => {
clearLoadingUpdates[rowId] = false;
});
setIsLoadingLines(prev => ({ ...prev, ...clearLoadingUpdates }));
}
})();
fetchPromises.push(fetchPromise);
});
// Process lines that need sublines
linesNeeded.forEach((rowIds, lineId) => {
// Skip if already in cache
if (lineSublineCache[lineId]) {
console.log(`Using cached sublines for line ${lineId}`);
// Use cached data for all rows with this line
const sublines = lineSublineCache[lineId];
const updates: Record<string, any[]> = {};
rowIds.forEach(rowId => {
updates[rowId] = sublines;
});
setRowSublines(prev => ({ ...prev, ...updates }));
return;
}
// Set loading state for all affected rows
rowIds.forEach(rowId => {
sublineLoadingUpdates[rowId] = true;
});
// Create fetch promise
const fetchPromise = (async () => {
// Safety timeout to ensure loading state is cleared after 10 seconds
const timeoutId = setTimeout(() => {
console.log(`Safety timeout triggered for line ${lineId}`);
const clearLoadingUpdates: Record<string, boolean> = {};
rowIds.forEach(rowId => {
clearLoadingUpdates[rowId] = false;
});
setIsLoadingSublines(prev => ({ ...prev, ...clearLoadingUpdates }));
// Set empty cache to prevent repeated requests
setLineSublineCache(prev => ({ ...prev, [lineId]: [] }));
// Update rows with empty array
const updates: Record<string, any[]> = {};
rowIds.forEach(rowId => {
updates[rowId] = [];
});
setRowSublines(prev => ({ ...prev, ...updates }));
toast.error(`Timeout loading sublines for line ${lineId}`);
}, 10000);
try {
console.log(`Fetching sublines for line ${lineId} (affecting ${rowIds.length} rows)`);
// Fetch sublines from API
const sublinesUrl = `/api/import/sublines/${lineId}`;
console.log(`Fetching from URL: ${sublinesUrl}`);
const response = await axios.get(sublinesUrl);
console.log(`Sublines API response status for line ${lineId}:`, response.status);
const sublines = response.data;
console.log(`Received ${sublines.length} sublines for line ${lineId}`);
// Store in line cache
setLineSublineCache(prev => ({ ...prev, [lineId]: sublines }));
// Update all rows with this line
const updates: Record<string, any[]> = {};
rowIds.forEach(rowId => {
updates[rowId] = sublines;
});
setRowSublines(prev => ({ ...prev, ...updates }));
} catch (error) {
console.error(`Error fetching sublines for line ${lineId}:`, error);
// Set empty array for this line to prevent repeated failed requests
setLineSublineCache(prev => ({ ...prev, [lineId]: [] }));
// Update rows with empty array
const updates: Record<string, any[]> = {};
rowIds.forEach(rowId => {
updates[rowId] = [];
});
setRowSublines(prev => ({ ...prev, ...updates }));
// Show error toast
toast.error(`Failed to load sublines for line ${lineId}`);
} finally {
// Clear the safety timeout
clearTimeout(timeoutId);
// Clear loading state for all affected rows
const clearLoadingUpdates: Record<string, boolean> = {};
rowIds.forEach(rowId => {
clearLoadingUpdates[rowId] = false;
});
setIsLoadingSublines(prev => ({ ...prev, ...clearLoadingUpdates }));
}
})();
fetchPromises.push(fetchPromise);
});
// Set initial loading states
if (Object.keys(lineLoadingUpdates).length > 0) {
console.log(`Setting loading state for ${Object.keys(lineLoadingUpdates).length} rows (product lines)`);
setIsLoadingLines(prev => ({ ...prev, ...lineLoadingUpdates }));
}
if (Object.keys(sublineLoadingUpdates).length > 0) {
console.log(`Setting loading state for ${Object.keys(sublineLoadingUpdates).length} rows (sublines)`);
setIsLoadingSublines(prev => ({ ...prev, ...sublineLoadingUpdates }));
}
// Run all fetch operations in parallel
Promise.all(fetchPromises).then(() => {
console.log("All product lines and sublines fetch operations completed");
}).catch(error => {
console.error('Error in fetch operations:', error);
});
}, [data, rowProductLines, rowSublines, companyLinesCache, lineSublineCache]);
return {
rowProductLines,
rowSublines,
isLoadingLines,
isLoadingSublines,
fetchProductLines,
fetchSublines
};
};