diff --git a/inventory/src/components/product-import/steps/ValidationStepOLD/components/AiValidationDialogs.tsx b/inventory/src/components/product-import/steps/ValidationStepOLD/components/AiValidationDialogs.tsx deleted file mode 100644 index 2af46e9..0000000 --- a/inventory/src/components/product-import/steps/ValidationStepOLD/components/AiValidationDialogs.tsx +++ /dev/null @@ -1,981 +0,0 @@ -import React, { useState } from "react"; -import { - Dialog, - DialogContent, - DialogHeader, - DialogTitle, - DialogDescription, -} from "@/components/ui/dialog"; -import { ScrollArea } from "@/components/ui/scroll-area"; -import { Button } from "@/components/ui/button"; -import { Loader2, CheckIcon, XIcon } from "lucide-react"; -import { Code } from "@/components/ui/code"; -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from "@/components/ui/table"; -import { - AiValidationDetails, - AiValidationProgress, - CurrentPrompt, -} from "../hooks/useAiValidation"; -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; -import { Badge } from "@/components/ui/badge"; -import { Protected } from "@/components/auth/Protected"; - -interface TaxonomyStats { - categories: number; - themes: number; - colors: number; - taxCodes: number; - sizeCategories: number; - suppliers: number; - companies: number; - artists: number; -} - -interface DebugData { - taxonomyStats: TaxonomyStats | null; - basePrompt: string; - sampleFullPrompt: string; - promptLength: number; - apiFormat?: Array<{ - role: string; - content: string; - }>; - promptSources?: { - systemPrompt?: { id: number; prompt_text: string }; - generalPrompt?: { id: number; prompt_text: string }; - companyPrompts?: Array<{ - id: number; - company: string; - companyName?: string; - prompt_text: string; - }>; - }; - estimatedProcessingTime?: { - seconds: number | null; - sampleCount: number; - }; -} - -interface AiValidationDialogsProps { - aiValidationProgress: AiValidationProgress; - aiValidationDetails: AiValidationDetails; - currentPrompt: CurrentPrompt; - setAiValidationProgress: React.Dispatch< - React.SetStateAction - >; - setAiValidationDetails: React.Dispatch< - React.SetStateAction - >; - setCurrentPrompt: React.Dispatch>; - revertAiChange: (productIndex: number, fieldKey: string) => void; - isChangeReverted: (productIndex: number, fieldKey: string) => boolean; - getFieldDisplayValueWithHighlight: ( - fieldKey: string, - originalValue: any, - correctedValue: any - ) => { originalHtml: string; correctedHtml: string }; - fields: readonly any[]; - debugData?: DebugData; -} - -export const AiValidationDialogs: React.FC = ({ - aiValidationProgress, - aiValidationDetails, - currentPrompt, - setAiValidationProgress, - setAiValidationDetails, - setCurrentPrompt, - revertAiChange, - isChangeReverted, - getFieldDisplayValueWithHighlight, - fields, -}) => { - const [costPerMillionTokens, setCostPerMillionTokens] = useState(1.25); // Default cost - - // Create our own state to track changes - const [localReversionState, setLocalReversionState] = useState< - Record - >({}); - - // Initialize local state from the isChangeReverted function when component mounts - // or when aiValidationDetails changes - React.useEffect(() => { - if ( - aiValidationDetails.changeDetails && - aiValidationDetails.changeDetails.length > 0 - ) { - const initialState: Record = {}; - - aiValidationDetails.changeDetails.forEach((product) => { - product.changes.forEach((change) => { - const key = `${product.productIndex}-${change.field}`; - initialState[key] = isChangeReverted( - product.productIndex, - change.field - ); - }); - }); - - setLocalReversionState(initialState); - } - }, [aiValidationDetails.changeDetails, isChangeReverted]); - - // This function will toggle the local state for a given change - const toggleChangeAcceptance = (productIndex: number, fieldKey: string) => { - const key = `${productIndex}-${fieldKey}`; - const currentlyRejected = !!localReversionState[key]; - - // Toggle the local state - setLocalReversionState((prev) => ({ - ...prev, - [key]: !prev[key], - })); - - // Only call revertAiChange when toggling to rejected state - // Since revertAiChange is specifically for rejecting changes - if (!currentlyRejected) { - revertAiChange(productIndex, fieldKey); - } - }; - - // Function to check local reversion state - const isChangeLocallyReverted = ( - productIndex: number, - fieldKey: string - ): boolean => { - const key = `${productIndex}-${fieldKey}`; - return !!localReversionState[key]; - }; - - // Format time helper - const formatTime = (seconds: number): string => { - if (seconds < 60) { - return `${Math.round(seconds)} seconds`; - } else { - const minutes = Math.floor(seconds / 60); - const remainingSeconds = Math.round(seconds % 60); - return `${minutes}m ${remainingSeconds}s`; - } - }; - - // Calculate token costs - const calculateTokenCost = (promptLength: number): number => { - const estimatedTokens = Math.round(promptLength / 4); - return (estimatedTokens / 1_000_000) * costPerMillionTokens * 100; // In cents - }; - - const formatNumber = (value: number | null | undefined): string => { - if (value === null || value === undefined) { - return "—"; - } - return value.toLocaleString(); - }; - - // Use the prompt length from the current prompt - const promptLength = currentPrompt.prompt ? currentPrompt.prompt.length : 0; - const tokenUsage = aiValidationDetails.tokenUsage; - const formattedReasoningEffort = aiValidationDetails.reasoningEffort - ? aiValidationDetails.reasoningEffort.charAt(0).toUpperCase() + - aiValidationDetails.reasoningEffort.slice(1) - : null; - - return ( - <> - {/* Current Prompt Dialog with Debug Info */} - - setCurrentPrompt((prev) => ({ ...prev, isOpen: open })) - } - > - - - Current AI Prompt - - This is the current prompt that would be sent to the AI for - validation - - - -
- {/* Debug Information Section - Fixed at the top */} -
- {currentPrompt.isLoading ? ( -
- ) : ( - <> -
- - - - Prompt Length - - - -
-
- - Characters: - {" "} - - {promptLength} - -
-
- - Tokens: - {" "} - - ~{Math.round(promptLength / 4)} - -
-
-
-
- - - - - Cost Estimate - - - -
-
- - { - const value = parseFloat(e.target.value); - if (!isNaN(value)) { - setCostPerMillionTokens(value); - } - }} - /> - -
-
- Cost:{" "} - - {calculateTokenCost(promptLength).toFixed(1)}¢ - -
-
-
-
- - - - - Processing Time - - - -
- {currentPrompt.debugData?.estimatedProcessingTime ? ( - currentPrompt.debugData.estimatedProcessingTime - .seconds ? ( - <> -
- - Estimated time: - {" "} - - {formatTime( - currentPrompt.debugData - .estimatedProcessingTime.seconds - )} - -
-
- Based on{" "} - { - currentPrompt.debugData - .estimatedProcessingTime.sampleCount - }{" "} - similar validation - {currentPrompt.debugData - .estimatedProcessingTime.sampleCount !== 1 - ? "s" - : ""} -
- - ) : ( -
- No historical data available for this prompt - size -
- ) - ) : ( -
- No processing time data available -
- )} -
-
-
-
- - )} -
- - {/* Prompt Section - Scrollable content */} -
- {currentPrompt.isLoading ? ( -
- -
- ) : ( - <> - {currentPrompt.debugData?.apiFormat ? ( -
- {/* Prompt Sources Card - Fixed at the top of the content area */} - - - - Prompt Sources - - - -
- - document - .getElementById("system-message") - ?.scrollIntoView({ behavior: "smooth" }) - } - > - System - - - document - .getElementById("general-section") - ?.scrollIntoView({ behavior: "smooth" }) - } - > - General - - - {currentPrompt.debugData.promptSources?.companyPrompts?.map( - (company, idx) => ( - - document - .getElementById("company-section") - ?.scrollIntoView({ behavior: "smooth" }) - } - > - {company.companyName || - `Company ${company.company}`} - - ) - )} - - - document - .getElementById("taxonomy-section") - ?.scrollIntoView({ behavior: "smooth" }) - } - > - Taxonomy - - - document - .getElementById("product-section") - ?.scrollIntoView({ behavior: "smooth" }) - } - > - Products - -
-
-
- - - {currentPrompt.debugData.apiFormat.map( - (message, idx: number) => ( -
-
- Role: {message.role} -
- - - {message.role === "user" ? ( -
- {(() => { - const content = message.content; - - // Find section boundaries by looking for specific markers - const companySpecificStartIndex = - content.indexOf( - "--- COMPANY-SPECIFIC INSTRUCTIONS ---" - ); - const companySpecificEndIndex = - content.indexOf( - "--- END COMPANY-SPECIFIC INSTRUCTIONS ---" - ); - - const taxonomyStartIndex = - content.indexOf( - "All Available Categories:" - ); - const taxonomyFallbackStartIndex = - content.indexOf( - "Available Categories:" - ); - const actualTaxonomyStartIndex = - taxonomyStartIndex >= 0 - ? taxonomyStartIndex - : taxonomyFallbackStartIndex; - - const productDataStartIndex = - content.indexOf( - "----------Here is the product data to validate----------" - ); - - // If we can't find any markers, just return the content as-is - if ( - actualTaxonomyStartIndex < 0 && - productDataStartIndex < 0 && - companySpecificStartIndex < 0 - ) { - return content; - } - - // Determine section indices - let generalEndIndex = content.length; - - if (companySpecificStartIndex >= 0) { - generalEndIndex = - companySpecificStartIndex; - } else if ( - actualTaxonomyStartIndex >= 0 - ) { - generalEndIndex = - actualTaxonomyStartIndex; - } else if (productDataStartIndex >= 0) { - generalEndIndex = productDataStartIndex; - } - - // Determine where taxonomy starts - let taxonomyEndIndex = content.length; - if (productDataStartIndex >= 0) { - taxonomyEndIndex = - productDataStartIndex; - } - - // Segments to render with appropriate styling - const segments = []; - - // General section (beginning to company/taxonomy/product) - if (generalEndIndex > 0) { - segments.push( -
-
- General Prompt -
-
-                                              {content.substring(
-                                                0,
-                                                generalEndIndex
-                                              )}
-                                            
-
- ); - } - - // Company-specific section if present - if ( - companySpecificStartIndex >= 0 && - companySpecificEndIndex >= 0 - ) { - segments.push( -
-
- Company-Specific Instructions -
-
-                                              {content.substring(
-                                                companySpecificStartIndex,
-                                                companySpecificEndIndex +
-                                                  "--- END COMPANY-SPECIFIC INSTRUCTIONS ---"
-                                                    .length
-                                              )}
-                                            
-
- ); - } - - // Taxonomy section - if (actualTaxonomyStartIndex >= 0) { - const taxEnd = taxonomyEndIndex; - segments.push( -
-
- Taxonomy Data -
-
-                                              {content.substring(
-                                                actualTaxonomyStartIndex,
-                                                taxEnd
-                                              )}
-                                            
-
- ); - } - - // Product data section - if (productDataStartIndex >= 0) { - segments.push( -
-
- Product Data -
-
-                                              {content.substring(
-                                                productDataStartIndex
-                                              )}
-                                            
-
- ); - } - - return <>{segments}; - })()} -
- ) : ( -
-                                    {message.content}
-                                  
- )} -
-
- ) - )} -
-
- ) : ( - - - {currentPrompt.prompt} - - - )} - - )} -
-
-
-
- - {/* AI Validation Progress Dialog */} - { - // Only allow closing if validation failed - if (!open && aiValidationProgress.step === -1) { - setAiValidationProgress((prev) => ({ ...prev, isOpen: false })); - } - }} - > - - - AI Validation Progress - -
-
-
-
-
-
-
-
- {aiValidationProgress.step === -1 - ? "❌" - : `${ - aiValidationProgress.progressPercent !== undefined - ? Math.round(aiValidationProgress.progressPercent) - : Math.round((aiValidationProgress.step / 5) * 100) - }%`} -
-
-

- {aiValidationProgress.status} -

- {(() => { - // Only show time remaining if we have an estimate and are in progress - return ( - aiValidationProgress.estimatedSeconds && - aiValidationProgress.elapsedSeconds !== undefined && - aiValidationProgress.step > 0 && - aiValidationProgress.step < 5 && (() => { - const elapsedSeconds = aiValidationProgress.elapsedSeconds; - const totalEstimatedSeconds = aiValidationProgress.estimatedSeconds; - const remainingSeconds = Math.max( - 0, - totalEstimatedSeconds - elapsedSeconds - ); - - if (remainingSeconds <= 5) { - return null; - } - - const message = remainingSeconds < 60 - ? `Approximately ${Math.round(remainingSeconds)} seconds remaining` - : (() => { - const minutes = Math.floor(remainingSeconds / 60); - const seconds = Math.round(remainingSeconds % 60); - return `Approximately ${minutes}m ${seconds}s remaining`; - })(); - - return ( -
- {message} - {aiValidationProgress.promptLength && ( -

- Prompt length:{" "} - {aiValidationProgress.promptLength.toLocaleString()}{" "} - characters -

- )} -
- ); - })() - ); - })()} -
- -
- - {/* AI Validation Results Dialog */} - - setAiValidationDetails((prev) => ({ ...prev, isOpen: open })) - } - > - - - AI Validation Results - - Review the changes and warnings suggested by the AI - - - - {(aiValidationDetails.model || tokenUsage || formattedReasoningEffort) && ( -
-
- {aiValidationDetails.model && ( - - Model · {aiValidationDetails.model} - - )} - {formattedReasoningEffort && ( - - Reasoning {formattedReasoningEffort} - - )} -
- {tokenUsage && ( -
-
- - Prompt tokens - - - {formatNumber(tokenUsage.prompt)} - -
-
- - Completion tokens - - - {formatNumber(tokenUsage.completion)} - -
-
- - Total tokens - - - {formatNumber(tokenUsage.total)} - -
-
- - Reasoning tokens - - - {formatNumber(tokenUsage.reasoning)} - -
-
- - Cached prompt tokens - - - {formatNumber(tokenUsage.cachedPrompt)} - -
-
- )} -
- )} -
- {(aiValidationDetails.summary || - (aiValidationDetails.changes && aiValidationDetails.changes.length > 0) || - (aiValidationDetails.warnings && aiValidationDetails.warnings.length > 0)) && ( - - - Overall Assessment - - - {aiValidationDetails.changes && - aiValidationDetails.changes.length > 0 && ( -
-

- Key Changes -

-
    - {aiValidationDetails.changes.map((change, idx) => ( -
  • {change}
  • - ))} -
-
- )} - {aiValidationDetails.warnings && - aiValidationDetails.warnings.length > 0 && ( -
-

- Warnings -

-
    - {aiValidationDetails.warnings.map((warning, idx) => ( -
  • {warning}
  • - ))} -
-
- )} - {aiValidationDetails.summary && ( -

{aiValidationDetails.summary}

- )} -
-
- )} - - {aiValidationDetails.changeDetails && - aiValidationDetails.changeDetails.length > 0 ? ( -
-

Detailed Changes:

- {aiValidationDetails.changeDetails.map((product, i) => { - // Find the title change if it exists - const titleChange = product.changes.find( - (c) => c.field === "title" - ); - const titleValue = titleChange - ? titleChange.corrected - : product.title; - - return ( -
-

- {titleValue || `Product ${product.productIndex + 1}`} -

- - - - Field - - Original Value - - - Corrected Value - - - Accept Changes? - - - - - {product.changes.map((change, j) => { - const field = fields.find( - (f) => f.key === change.field - ); - const fieldLabel = field - ? field.label - : change.field; - const isReverted = isChangeLocallyReverted( - product.productIndex, - change.field - ); - - // Get highlighted differences - const { originalHtml, correctedHtml } = - getFieldDisplayValueWithHighlight( - change.field, - change.original, - change.corrected - ); - - return ( - - - {fieldLabel} - - -
- - -
- - -
- - -
-
- - ); - })} - -
-
- ); - })} -
- ) : ( -
-

No field-level changes were suggested by the AI.

-
- )} -
-
-
- - ); -}; diff --git a/inventory/src/components/product-import/steps/ValidationStepOLD/components/BaseCellContent.tsx b/inventory/src/components/product-import/steps/ValidationStepOLD/components/BaseCellContent.tsx deleted file mode 100644 index 27593af..0000000 --- a/inventory/src/components/product-import/steps/ValidationStepOLD/components/BaseCellContent.tsx +++ /dev/null @@ -1,42 +0,0 @@ -// Define MultiSelectCell component to fix the import issue -type MultiSelectCellProps = { - field: string; - value: any; - onChange: (value: any) => void; - options: any[]; - hasErrors: boolean; - className?: string; -}; - -// Using _ to indicate intentionally unused parameters -const MultiSelectCell = (_: MultiSelectCellProps) => { - // This is a placeholder implementation - return null; -}; - -const BaseCellContent = ({ fieldType, field, value, onChange, options, hasErrors, className }: { - fieldType: string; - field: string; - value: any; - onChange: (value: any) => void; - options: any[]; - hasErrors: boolean; - className?: string; -}) => { - if (fieldType === 'multi-select' || fieldType === 'multi-input') { - return ( - - ); - } - - return null; -}; - -export default BaseCellContent; \ No newline at end of file diff --git a/inventory/src/components/product-import/steps/ValidationStepOLD/components/InitializingValidation.tsx b/inventory/src/components/product-import/steps/ValidationStepOLD/components/InitializingValidation.tsx deleted file mode 100644 index b225eb2..0000000 --- a/inventory/src/components/product-import/steps/ValidationStepOLD/components/InitializingValidation.tsx +++ /dev/null @@ -1,73 +0,0 @@ -import React from 'react' -import { Loader2, Check, X } from 'lucide-react' -import { cn } from '@/lib/utils' - -interface InitializationTask { - label: string - status: 'pending' | 'in_progress' | 'completed' | 'failed' -} - -interface InitializingValidationProps { - totalRows: number - tasks: InitializationTask[] -} - -export const InitializingValidation: React.FC = ({ - totalRows, - tasks -}) => { - return ( -
- -

Initializing Validation

-

Processing {totalRows} rows...

- - {/* Task checklist */} -
- {tasks.map((task, index) => ( -
- {/* Status icon */} -
- {task.status === 'completed' && ( - - )} - {task.status === 'failed' && ( - - )} - {task.status === 'in_progress' && ( - - )} - {task.status === 'pending' && ( - - )} -
- - {/* Task label */} - - {task.label} - -
- ))} -
-
- ) -} - -export default InitializingValidation diff --git a/inventory/src/components/product-import/steps/ValidationStepOLD/components/SearchableTemplateSelect.tsx b/inventory/src/components/product-import/steps/ValidationStepOLD/components/SearchableTemplateSelect.tsx deleted file mode 100644 index 6507b3e..0000000 --- a/inventory/src/components/product-import/steps/ValidationStepOLD/components/SearchableTemplateSelect.tsx +++ /dev/null @@ -1,328 +0,0 @@ -import React, { useState, useEffect, useCallback, useMemo } from 'react' -import { Template } from '../hooks/validationTypes' -import { Button } from '@/components/ui/button' -import { - Command, - CommandEmpty, - CommandGroup, - CommandInput, - CommandItem, - CommandList, - CommandSeparator, -} from '@/components/ui/command' -import { - Popover, - PopoverContent, - PopoverTrigger, -} from '@/components/ui/popover' -import { ScrollArea } from '@/components/ui/scroll-area' -import { cn } from '@/lib/utils' -import { Check, ChevronsUpDown } from 'lucide-react' -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select" - -interface SearchableTemplateSelectProps { - templates: Template[] | undefined; - value: string; - onValueChange: (value: string) => void; - getTemplateDisplayText: (templateId: string | null) => string; - placeholder?: string; - className?: string; - triggerClassName?: string; - defaultBrand?: string; -} - -const SearchableTemplateSelect: React.FC = ({ - templates = [], - value, - onValueChange, - getTemplateDisplayText, - placeholder = "Select template", - className, - triggerClassName, - defaultBrand, -}) => { - const [searchTerm, setSearchTerm] = useState(""); - const [selectedBrand, setSelectedBrand] = useState(null); - const [open, setOpen] = useState(false); - - // Set default brand when component mounts or defaultBrand changes - useEffect(() => { - if (defaultBrand) { - setSelectedBrand(defaultBrand); - } - }, [defaultBrand]); - - // Force a re-render when templates change from empty to non-empty - useEffect(() => { - if (templates && templates.length > 0) { - // Force a re-render by updating state - setSearchTerm(""); - } - }, [templates]); - - // Handle wheel events for scrolling - const handleWheel = (e: React.WheelEvent) => { - const scrollArea = e.currentTarget; - scrollArea.scrollTop += e.deltaY; - }; - - // Extract unique brands from templates - const brands = useMemo(() => { - try { - if (!Array.isArray(templates) || templates.length === 0) { - return []; - } - - const brandSet = new Set(); - const brandNames: {id: string, name: string}[] = []; - - templates.forEach(template => { - if (!template?.company) return; - - const companyId = template.company; - if (!brandSet.has(companyId)) { - brandSet.add(companyId); - - // Try to get the company name from the template display text - try { - const displayText = getTemplateDisplayText(template.id.toString()); - const companyName = displayText.split(' - ')[0]; - brandNames.push({ id: companyId, name: companyName || companyId }); - } catch (err) { - brandNames.push({ id: companyId, name: companyId }); - } - } - }); - - return brandNames.sort((a, b) => a.name.localeCompare(b.name)); - } catch (err) { - return []; - } - }, [templates, getTemplateDisplayText]); - - // Group templates by company for better organization - const groupedTemplates = useMemo(() => { - try { - if (!Array.isArray(templates) || templates.length === 0) return {}; - - const groups: Record = {}; - - templates.forEach(template => { - if (!template?.company) return; - - const companyId = template.company; - if (!groups[companyId]) { - groups[companyId] = []; - } - groups[companyId].push(template); - }); - - return groups; - } catch (err) { - return {}; - } - }, [templates]); - - // Filter templates based on selected brand and search term - const filteredTemplates = useMemo(() => { - try { - if (!Array.isArray(templates) || templates.length === 0) return []; - - // First filter by brand if selected - let brandFiltered = templates; - if (selectedBrand) { - // Check if the selected brand has any templates - const brandTemplates = templates.filter(t => t?.company === selectedBrand); - - // If the selected brand has templates, use them; otherwise, show all templates - brandFiltered = brandTemplates.length > 0 ? brandTemplates : templates; - } - - // Then filter by search term if provided - if (!searchTerm.trim()) return brandFiltered; - - const lowerSearchTerm = searchTerm.toLowerCase(); - return brandFiltered.filter(template => { - if (!template?.id) return false; - try { - const displayText = getTemplateDisplayText(template.id.toString()); - const productType = template.product_type?.toLowerCase() || ''; - - return displayText.toLowerCase().includes(lowerSearchTerm) || - productType.includes(lowerSearchTerm); - } catch (error) { - return false; - } - }); - } catch (err) { - return []; - } - }, [templates, selectedBrand, searchTerm, getTemplateDisplayText]); - - // Handle errors gracefully - const getDisplayText = useCallback(() => { - try { - if (!value) return placeholder; - const template = templates.find(t => t.id.toString() === value); - if (!template) return placeholder; - - // Get the original display text - const originalText = getTemplateDisplayText(value); - - // Check if it has the expected format "Brand - Product Type" - if (originalText.includes(' - ')) { - const [brand, productType] = originalText.split(' - ', 2); - // Reverse the order to "Product Type - Brand" - return `${productType} - ${brand}`; - } - - // If it doesn't match the expected format, return the original text - return originalText; - } catch (err) { - console.error('Error getting display text:', err); - return placeholder; - } - }, [getTemplateDisplayText, placeholder, value, templates]); - - // Safe render function for CommandItem - const renderCommandItem = useCallback((template: Template) => { - if (!template?.id) return null; - - try { - const displayText = getTemplateDisplayText(template.id.toString()); - - return ( - { - try { - onValueChange(currentValue); - setOpen(false); - setSearchTerm(""); - } catch (err) { - console.error('Error selecting template:', err); - } - }} - className="flex items-center justify-between" - > - {displayText} - {value === template.id.toString() && } - - ); - } catch (err) { - console.error('Error rendering template item:', err); - return null; - } - }, [onValueChange, value, getTemplateDisplayText]); - - return ( - - - - - - -
- {brands.length > 0 && ( -
- -
- )} - - - -
- -
-
- - -
-

No templates found.

-
-
- - - - {!searchTerm ? ( - selectedBrand ? ( - groupedTemplates[selectedBrand]?.length > 0 ? ( - b.id === selectedBrand)?.name || selectedBrand}> - {groupedTemplates[selectedBrand]?.map(template => renderCommandItem(template))} - - ) : ( - // If selected brand has no templates, show all brands - Object.entries(groupedTemplates).map(([companyId, companyTemplates]) => { - const brand = brands.find(b => b.id === companyId); - const companyName = brand ? brand.name : companyId; - - return ( - - {companyTemplates.map(template => renderCommandItem(template))} - - ); - }) - ) - ) : ( - Object.entries(groupedTemplates).map(([companyId, companyTemplates]) => { - const brand = brands.find(b => b.id === companyId); - const companyName = brand ? brand.name : companyId; - - return ( - - {companyTemplates.map(template => renderCommandItem(template))} - - ); - }) - ) - ) : ( - - {filteredTemplates.map(template => renderCommandItem(template))} - - )} - - -
-
-
- ); -}; - -export default SearchableTemplateSelect; \ No newline at end of file diff --git a/inventory/src/components/product-import/steps/ValidationStepOLD/components/UpcValidationTableAdapter.tsx b/inventory/src/components/product-import/steps/ValidationStepOLD/components/UpcValidationTableAdapter.tsx deleted file mode 100644 index e805244..0000000 --- a/inventory/src/components/product-import/steps/ValidationStepOLD/components/UpcValidationTableAdapter.tsx +++ /dev/null @@ -1,158 +0,0 @@ -import React, { useMemo } from 'react' -import ValidationTable from './ValidationTable' -import { RowSelectionState } from '@tanstack/react-table' -import { Fields } from '../../../types' -import { Template } from '../hooks/validationTypes' - -interface UpcValidationTableAdapterProps { - data: any[] - fields: Fields - validationErrors: Map> - rowSelection: RowSelectionState - setRowSelection: React.Dispatch> - updateRow: (rowIndex: number, key: T, value: any) => void - filters: any - templates: Template[] - applyTemplate: (templateId: string, rowIndexes: number[]) => void - getTemplateDisplayText: (templateId: string | null) => string - isValidatingUpc: (rowIndex: number) => boolean - validatingUpcRows: number[] - copyDown: (rowIndex: number, fieldKey: string, endRowIndex?: number) => void - validatingCells: Set - isLoadingTemplates: boolean - editingCells: Set - setEditingCells: React.Dispatch>> - rowProductLines: Record - rowSublines: Record - isLoadingLines: Record - isLoadingSublines: Record - upcValidation: { - validatingRows: Set - getItemNumber: (rowIndex: number) => string | undefined - } - itemNumbers?: Map -} - -/** - * UpcValidationTableAdapter component - connects UPC validation data to ValidationTable - * - * This component adapts UPC validation data and functionality to work with the core ValidationTable, - * transforming item numbers and validation states into a format the table component can render. - */ -function UpcValidationTableAdapter({ - data, - fields, - validationErrors, - rowSelection, - setRowSelection, - updateRow, - filters, - templates, - applyTemplate, - getTemplateDisplayText, - isValidatingUpc, - validatingUpcRows, - copyDown, - validatingCells: externalValidatingCells, - isLoadingTemplates, - editingCells, - setEditingCells, - rowProductLines, - rowSublines, - isLoadingLines, - isLoadingSublines, - upcValidation, - itemNumbers -}: UpcValidationTableAdapterProps) { - // Prepare the validation table with UPC data - - // Create combined validatingCells set from validating rows and external cells - const combinedValidatingCells = useMemo(() => { - const combined = new Set(); - - // Add UPC validation cells - upcValidation.validatingRows.forEach(rowIndex => { - // Only mark the item_number cells as validating, NOT the UPC or supplier - combined.add(`${rowIndex}-item_number`); - }); - - // Add any other validating cells from state - externalValidatingCells.forEach(cellKey => { - combined.add(cellKey); - }); - - return combined; - }, [upcValidation.validatingRows, externalValidatingCells]); - - // Create a consolidated item numbers map from all sources - const consolidatedItemNumbers = useMemo(() => { - const result = new Map(); - - // First add from itemNumbers directly - this is the source of truth for template applications - if (itemNumbers) { - itemNumbers.forEach((itemNumber, rowIndex) => { - result.set(rowIndex, itemNumber); - }); - } - - // For each row, ensure we have the most up-to-date item number - data.forEach((_, index) => { - // Check if upcValidation has an item number for this row - const itemNumber = upcValidation.getItemNumber(index); - if (itemNumber) { - result.set(index, itemNumber); - } - - // Also check if it's directly in the data - const dataItemNumber = data[index].item_number; - if (dataItemNumber && !result.has(index)) { - result.set(index, dataItemNumber); - } - }); - - return result; - }, [data, itemNumbers, upcValidation]); - - // Create upcValidationResults map using the consolidated item numbers - const upcValidationResults = useMemo(() => { - const results = new Map(); - - // Populate with our consolidated item numbers - consolidatedItemNumbers.forEach((itemNumber, rowIndex) => { - results.set(rowIndex, { itemNumber }); - }); - - return results; - }, [consolidatedItemNumbers]); - - // Render the validation table with the provided props and UPC data - return ( - void} - validationErrors={validationErrors} - isValidatingUpc={isValidatingUpc} - validatingUpcRows={validatingUpcRows} - filters={filters} - templates={templates} - applyTemplate={applyTemplate} - getTemplateDisplayText={getTemplateDisplayText} - validatingCells={combinedValidatingCells} - itemNumbers={consolidatedItemNumbers} - isLoadingTemplates={isLoadingTemplates} - copyDown={copyDown} - upcValidationResults={upcValidationResults} - rowProductLines={rowProductLines} - rowSublines={rowSublines} - isLoadingLines={isLoadingLines} - isLoadingSublines={isLoadingSublines} - editingCells={editingCells} - setEditingCells={setEditingCells} - /> - ) -} - -export default UpcValidationTableAdapter \ No newline at end of file diff --git a/inventory/src/components/product-import/steps/ValidationStepOLD/components/ValidationCell.tsx b/inventory/src/components/product-import/steps/ValidationStepOLD/components/ValidationCell.tsx deleted file mode 100644 index 1dc375a..0000000 --- a/inventory/src/components/product-import/steps/ValidationStepOLD/components/ValidationCell.tsx +++ /dev/null @@ -1,661 +0,0 @@ -import React from 'react' -import { Field, ErrorType } from '../../../types' -import { AlertCircle, ArrowDown, Wand2, X } from 'lucide-react' -import { - Tooltip, - TooltipContent, - TooltipProvider, - TooltipTrigger, -} from '@/components/ui/tooltip' -import InputCell from './cells/InputCell' -import SelectCell from './cells/SelectCell' -import MultiSelectCell from './cells/MultiSelectCell' -import { TableCell } from '@/components/ui/table' -import { Skeleton } from '@/components/ui/skeleton' -import { useToast } from '@/hooks/use-toast' -import config from '@/config' - -// Context for copy down selection mode -export const CopyDownContext = React.createContext<{ - isInCopyDownMode: boolean; - sourceRowIndex: number | null; - sourceFieldKey: string | null; - targetRowIndex: number | null; - setIsInCopyDownMode: (value: boolean) => void; - setSourceRowIndex: (value: number | null) => void; - setSourceFieldKey: (value: string | null) => void; - setTargetRowIndex: (value: number | null) => void; - handleCopyDownComplete: (sourceRowIndex: number, fieldKey: string, targetRowIndex: number) => void; -}>({ - isInCopyDownMode: false, - sourceRowIndex: null, - sourceFieldKey: null, - targetRowIndex: null, - setIsInCopyDownMode: () => {}, - setSourceRowIndex: () => {}, - setSourceFieldKey: () => {}, - setTargetRowIndex: () => {}, - handleCopyDownComplete: () => {}, -}); - -// Define error object type -type ErrorObject = { - message: string; - level: string; - source?: string; - type?: ErrorType; -} - -// Helper function to check if a value is empty - utility function shared by all components -const isEmpty = (val: any): boolean => - val === undefined || - val === null || - val === '' || - (Array.isArray(val) && val.length === 0) || - (typeof val === 'object' && !Array.isArray(val) && Object.keys(val).length === 0); - -// Memoized validation icon component -const ValidationIcon = React.memo(({ error }: { error: ErrorObject }) => ( - - - -
- -
-
- -

{error.message}

-
-
-
-)); - -ValidationIcon.displayName = 'ValidationIcon'; - -// Memoized base cell content component -const BaseCellContent = React.memo(({ - field, - value, - onChange, - hasErrors, - options = [], - className = '', - fieldKey = '', - onStartEdit, - onEndEdit -}: { - field: Field; - value: any; - onChange: (value: any) => void; - hasErrors: boolean; - options?: readonly any[]; - className?: string; - fieldKey?: string; - onStartEdit?: () => void; - onEndEdit?: () => void; -}) => { - // Get field type information - const fieldType = fieldKey === 'line' || fieldKey === 'subline' - ? 'select' - : typeof field.fieldType === 'string' - ? field.fieldType - : field.fieldType?.type || 'input'; - - // Check for multiline input - const isMultiline = typeof field.fieldType === 'object' && - (field.fieldType.type === 'input' || field.fieldType.type === 'multi-input') && - field.fieldType.multiline === true; - - // Check for price field - const isPrice = typeof field.fieldType === 'object' && - (field.fieldType.type === 'input' || field.fieldType.type === 'multi-input') && - field.fieldType.price === true; - - // Special case for line and subline - check this first, before any other field type checks - if (fieldKey === 'line' || fieldKey === 'subline') { - // Force these fields to always use SelectCell regardless of fieldType - return ( - - ); - } - - if (fieldType === 'select') { - return ( - - ); - } - - if (fieldType === 'multi-select' || fieldType === 'multi-input') { - return ( - - ); - } - - return ( - - ); -}, (prev, next) => { - // Shallow array comparison for options if arrays - const optionsEqual = prev.options === next.options || - (Array.isArray(prev.options) && Array.isArray(next.options) && - prev.options.length === next.options.length && - prev.options.every((opt, idx) => opt === (next.options as any[])[idx])); - - return ( - prev.value === next.value && - prev.hasErrors === next.hasErrors && - prev.field === next.field && - prev.className === next.className && - optionsEqual - ); -}); - -BaseCellContent.displayName = 'BaseCellContent'; - -export interface ValidationCellProps { - field: Field - value: any - onChange: (value: any) => void - errors: ErrorObject[] - isValidating?: boolean - fieldKey: string - options?: readonly any[] - itemNumber?: string - width: number - rowIndex: number - copyDown?: (endRowIndex?: number) => void - totalRows?: number - rowData: Record - editingCells: Set - setEditingCells: React.Dispatch>> -} - -// Add efficient error message extraction function - -// Highly optimized error processing function with fast paths for common cases -function processErrors(value: any, errors: ErrorObject[]): { - hasError: boolean; - isRequiredButEmpty: boolean; - shouldShowErrorIcon: boolean; - errorMessages: string; -} { - // Fast path - if no errors or empty error array, return immediately - if (!errors || errors.length === 0) { - return { - hasError: false, - isRequiredButEmpty: false, - shouldShowErrorIcon: false, - errorMessages: '' - }; - } - - // Use the shared isEmpty function for value checking - const valueIsEmpty = isEmpty(value); - - // Fast path for the most common case - required field with empty value - if (valueIsEmpty && errors.length === 1 && errors[0].type === ErrorType.Required) { - return { - hasError: true, - isRequiredButEmpty: true, - shouldShowErrorIcon: false, - errorMessages: '' - }; - } - - // For non-empty values with errors, we need to show error icons - const hasError = errors.some(error => error.level === 'error' || error.level === 'warning'); - - // For empty values with required errors, show only a border - const isRequiredButEmpty = valueIsEmpty && errors.some(error => error.type === ErrorType.Required); - - // Show error icons for non-empty fields with errors, or for empty fields with non-required errors - const shouldShowErrorIcon = hasError && (!valueIsEmpty || !errors.every(error => error.type === ErrorType.Required)); - - // Only compute error messages if we're going to show an icon - const errorMessages = shouldShowErrorIcon - ? errors - .filter(e => e.level === 'error' || e.level === 'warning') - .map(e => e.message) - .join('\n') - : ''; - - return { - hasError, - isRequiredButEmpty, - shouldShowErrorIcon, - errorMessages - }; -} - -// Helper function to compare error arrays efficiently with a hash-based approach -function compareErrorArrays(prevErrors: ErrorObject[], nextErrors: ErrorObject[]): boolean { - // Fast path for referential equality - if (prevErrors === nextErrors) return true; - - // Fast path for length check - if (!prevErrors || !nextErrors) return prevErrors === nextErrors; - if (prevErrors.length !== nextErrors.length) return false; - - // Generate simple hash from error properties - const getErrorHash = (error: ErrorObject): string => { - return `${error.message}|${error.level}|${error.type || ''}`; - }; - - // Compare using hashes - const prevHashes = prevErrors.map(getErrorHash); - const nextHashes = nextErrors.map(getErrorHash); - - // Sort hashes to ensure consistent order - prevHashes.sort(); - nextHashes.sort(); - - // Compare sorted hash arrays - return prevHashes.join(',') === nextHashes.join(','); -} - -const ValidationCell = React.memo(({ - field, - value, - onChange, - errors, - isValidating, - fieldKey, - options = [], - itemNumber, - width, - copyDown, - rowIndex, - totalRows = 0, - rowData, - editingCells, - setEditingCells -}: ValidationCellProps) => { - // Use the CopyDown context - const copyDownContext = React.useContext(CopyDownContext); - const { toast } = useToast(); - const [isGeneratingUpc, setIsGeneratingUpc] = React.useState(false); - - // CRITICAL FIX: For item_number fields, always prioritize the itemNumber prop over the value - // This ensures that when the itemNumber changes, the display value changes - let displayValue; - if (fieldKey === 'item_number' && itemNumber) { - // Prioritize itemNumber prop for item_number fields - displayValue = itemNumber; - } else { - displayValue = value; - } - - // Use the optimized processErrors function to avoid redundant filtering - const { - hasError, - isRequiredButEmpty, - shouldShowErrorIcon, - errorMessages - } = React.useMemo(() => processErrors(displayValue, errors), [displayValue, errors]); - - // Track whether this cell is the source of a copy-down operation - const isSourceCell = copyDownContext.isInCopyDownMode && - rowIndex === copyDownContext.sourceRowIndex && - fieldKey === copyDownContext.sourceFieldKey; - - // Add state for hover on copy down button - const [isCopyDownHovered, setIsCopyDownHovered] = React.useState(false); - // Add state for hover on target row - const [isTargetRowHovered, setIsTargetRowHovered] = React.useState(false); - - // PERFORMANCE FIX: Create cell key for editing state management - const cellKey = `${rowIndex}-${fieldKey}`; - const isEditingCell = editingCells.has(cellKey); - - // SINGLE-CLICK EDITING FIX: Create editing state management functions - const handleStartEdit = React.useCallback(() => { - setEditingCells(prev => new Set([...prev, cellKey])); - }, [setEditingCells, cellKey]); - - const handleEndEdit = React.useCallback(() => { - setEditingCells(prev => { - const newSet = new Set(prev); - newSet.delete(cellKey); - return newSet; - }); - }, [setEditingCells, cellKey]); - - // Handle copy down button click - const handleCopyDownClick = React.useCallback(() => { - if (copyDown && totalRows > rowIndex + 1) { - // Enter copy down mode - copyDownContext.setIsInCopyDownMode(true); - copyDownContext.setSourceRowIndex(rowIndex); - copyDownContext.setSourceFieldKey(fieldKey); - } - }, [copyDown, copyDownContext, fieldKey, rowIndex, totalRows]); - - // Check if this cell is in a row that can be a target for copy down - const isInTargetRow = copyDownContext.isInCopyDownMode && - copyDownContext.sourceFieldKey === fieldKey && - rowIndex > (copyDownContext.sourceRowIndex || 0); - - // Check if this row is the currently selected target row - const isSelectedTarget = isInTargetRow && rowIndex <= (copyDownContext.targetRowIndex || 0); - - // Handle click on a potential target cell - const handleTargetCellClick = React.useCallback(() => { - if (isInTargetRow && copyDownContext.sourceRowIndex !== null && copyDownContext.sourceFieldKey !== null) { - copyDownContext.handleCopyDownComplete( - copyDownContext.sourceRowIndex, - copyDownContext.sourceFieldKey, - rowIndex - ); - } - }, [copyDownContext, isInTargetRow, rowIndex]); - - // Memoize the cell style objects to avoid recreating them on every render - const cellStyle = React.useMemo(() => ({ - width: `${width}px`, - minWidth: `${width}px`, - maxWidth: `${width}px`, - boxSizing: 'border-box' as const, - cursor: isInTargetRow ? 'pointer' : undefined - }), [width, isInTargetRow]); - - // Memoize the cell class name to prevent re-calculating on every render - const cellClassName = React.useMemo(() => { - if (isSourceCell || isSelectedTarget || isInTargetRow) { - return isSourceCell ? '!bg-blue-100 !border-blue-500 !rounded-md' : - isSelectedTarget ? '!bg-blue-200 !border-blue-200 !rounded-md' : - isInTargetRow ? 'hover:!bg-blue-100 !border-blue-200 !rounded-md' : ''; - } - return ''; - }, [isSourceCell, isSelectedTarget, isInTargetRow]); - - const isUpcField = fieldKey === 'upc'; - const baseIsLoading = isValidating === true; - const showGeneratingSkeleton = isUpcField && isGeneratingUpc; - const isLoading = baseIsLoading || showGeneratingSkeleton; - - const supplierRaw = rowData?.supplier ?? rowData?.supplier_id ?? rowData?.supplierId; - const supplierIdString = supplierRaw !== undefined && supplierRaw !== null - ? String(supplierRaw).trim() - : ''; - const normalizedSupplierId = /^\d+$/.test(supplierIdString) ? supplierIdString : ''; - const canGenerateUpc = normalizedSupplierId !== ''; - const upcValueEmpty = isUpcField && isEmpty(displayValue); - const showGenerateButton = upcValueEmpty && !isEditingCell && !copyDownContext.isInCopyDownMode && !isInTargetRow && !isLoading; - const cellClassNameWithPadding = showGenerateButton ? `${cellClassName} pr-10`.trim() : cellClassName; - const buttonDisabled = !canGenerateUpc || isGeneratingUpc; - const tooltipMessage = canGenerateUpc ? 'Generate UPC' : 'Select a supplier before generating a UPC'; - - const handleGenerateUpc = React.useCallback(async () => { - if (!normalizedSupplierId) { - toast({ - variant: 'destructive', - description: 'Select a supplier before generating a UPC.', - }); - return; - } - - if (isGeneratingUpc) { - return; - } - - setIsGeneratingUpc(true); - - try { - const response = await fetch(`${config.apiUrl}/import/generate-upc`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ supplierId: normalizedSupplierId }) - }); - - let payload = null; - try { - payload = await response.json(); - } catch (parseError) { - // Ignore JSON parse errors and handle via status code - } - - if (!response.ok) { - const message = payload?.error || `Request failed (${response.status})`; - throw new Error(message); - } - - if (!payload || !payload.success || !payload.upc) { - throw new Error(payload?.error || 'Unexpected response while generating UPC'); - } - - onChange(payload.upc); - } catch (error) { - console.error('Error generating UPC:', error); - const errorMessage = - typeof error === 'object' && - error !== null && - 'message' in error && - typeof (error as { message?: unknown }).message === 'string' - ? (error as { message: string }).message - : 'Failed to generate UPC'; - toast({ - variant: 'destructive', - description: errorMessage, - }); - } finally { - setIsGeneratingUpc(false); - } - }, [normalizedSupplierId, isGeneratingUpc, onChange, toast]); - - const handleGenerateButtonClick = React.useCallback((event: React.MouseEvent) => { - event.preventDefault(); - event.stopPropagation(); - if (!buttonDisabled) { - handleGenerateUpc(); - } - }, [buttonDisabled, handleGenerateUpc]); - const containerClassName = `truncate overflow-hidden${isCopyDownHovered && !copyDownContext.isInCopyDownMode ? ' bg-blue-50/50' : ''}${showGenerateButton ? ' relative group/upc' : ''}`.trim(); - - return ( - setIsTargetRowHovered(true) : undefined} - onMouseLeave={isInTargetRow ? () => setIsTargetRowHovered(false) : undefined} - > -
- {shouldShowErrorIcon && !isInTargetRow && ( -
- -
- )} - {!shouldShowErrorIcon && copyDown && !isEmpty(displayValue) && !copyDownContext.isInCopyDownMode && ( -
- - - - - - -
-

Copy value to rows below

-
-
-
-
-
- )} - {isSourceCell && ( -
- - - - - - -

Cancel copy down

-
-
-
-
- )} - {isLoading ? ( -
- -
- ) : ( -
- - {showGenerateButton && ( - - - - - - -

{tooltipMessage}

-
-
-
- )} -
- )} -
-
- ); -}, (prevProps, nextProps) => { - // Fast path: if all props are the same object - if (prevProps === nextProps) return true; - - // Optimize the memo comparison function, checking most impactful props first - // Check isValidating first as it's most likely to change frequently - if (prevProps.isValidating !== nextProps.isValidating) return false; - - // Then check value changes - if (prevProps.value !== nextProps.value) return false; - - // Item number is related to validation state - if (prevProps.itemNumber !== nextProps.itemNumber) return false; - - // Check errors with our optimized comparison function - if (!compareErrorArrays(prevProps.errors, nextProps.errors)) return false; - - // Check field identity - if (prevProps.field !== nextProps.field) return false; - - if (prevProps.rowData !== nextProps.rowData) return false; - if (prevProps.editingCells !== nextProps.editingCells) return false; - - // Shallow options comparison - only if field type is select or multi-select - if (prevProps.field.fieldType?.type === 'select' || prevProps.field.fieldType?.type === 'multi-select') { - const optionsEqual = prevProps.options === nextProps.options || - (Array.isArray(prevProps.options) && - Array.isArray(nextProps.options) && - prevProps.options.length === nextProps.options.length && - prevProps.options.every((opt, idx) => { - const nextOptions = nextProps.options || []; - return opt === nextOptions[idx]; - })); - - if (!optionsEqual) return false; - } - - // Check copy down context changes - const copyDownContextChanged = - prevProps.rowIndex !== nextProps.rowIndex || - prevProps.fieldKey !== nextProps.fieldKey; - - if (copyDownContextChanged) return false; - - // All essential props are the same - we can skip re-rendering - return true; -}); - -ValidationCell.displayName = 'ValidationCell'; - -export default ValidationCell; diff --git a/inventory/src/components/product-import/steps/ValidationStepOLD/components/ValidationContainer.tsx b/inventory/src/components/product-import/steps/ValidationStepOLD/components/ValidationContainer.tsx deleted file mode 100644 index 3c1165b..0000000 --- a/inventory/src/components/product-import/steps/ValidationStepOLD/components/ValidationContainer.tsx +++ /dev/null @@ -1,1078 +0,0 @@ -import React, { useState, useEffect, useCallback, useMemo } from 'react' -import { useValidationState } from '../hooks/useValidationState' -import { Props, RowData } from '../hooks/validationTypes' -import { Button } from '@/components/ui/button' -import { Loader2, X, Plus, Edit3, Sparkles, FileText } from 'lucide-react' -import { toast } from 'sonner' -import { Switch } from '@/components/ui/switch' -import { useRsi } from '../../../hooks/useRsi' -import SearchableTemplateSelect from './SearchableTemplateSelect' -import { useAiValidation } from '../hooks/useAiValidation' -import { AiValidationDialogs } from './AiValidationDialogs' -import { Fields } from '../../../types' -import { SearchProductTemplateDialog } from '@/components/templates/SearchProductTemplateDialog' -import { TemplateForm } from '@/components/templates/TemplateForm' -import { CreateProductCategoryDialog, type CreatedCategoryInfo } from '@/components/product-import/CreateProductCategoryDialog' -import axios from 'axios' -import { RowSelectionState } from '@tanstack/react-table' -import { useProductLinesFetching } from '../hooks/useProductLinesFetching' -import UpcValidationTableAdapter from './UpcValidationTableAdapter' -import { Skeleton } from '@/components/ui/skeleton' -import { Protected } from '@/components/auth/Protected' -import { normalizeCountryCode } from '../utils/countryUtils' -import { cleanPriceField } from '../utils/priceUtils' -import { correctUpcValue } from '../utils/upcUtils' -import InitializingValidation from './InitializingValidation' -/** - * ValidationContainer component - the main wrapper for the validation step - * - * This component is responsible for: - * - Managing global state using hooks - * - Coordinating between subcomponents - * - Handling navigation events (next, back) - */ -const ValidationContainer = ({ - initialData, - file, - onBack, - onNext, - isFromScratch -}: Props) => { - // Use our main validation state hook - const validationState = useValidationState({ - initialData, - file, - onBack, - onNext, - isFromScratch - }) - - const { - data, - filteredData, - validationErrors, - rowSelection, - setRowSelection, - templates, - selectedTemplateId, - applyTemplate, - applyTemplateToSelected, - getTemplateDisplayText, - filters, - updateFilters, - loadTemplates, - setData, - fields, - upcValidation, - isLoadingTemplates, - isInitializing, - initializationTasks, - validatingCells, - setValidatingCells, - editingCells, - setEditingCells, - updateRow, - revalidateRows - } = validationState - - const dataIndexByRowId = useMemo(() => { - const map = new Map() - data.forEach((row, index) => { - const rowId = (row as Record).__index - if (rowId !== undefined && rowId !== null) { - map.set(rowId, index) - } - }) - return map - }, [data]) - - // Use product lines fetching hook - const { - rowProductLines, - rowSublines, - isLoadingLines, - isLoadingSublines, - fetchProductLines, - fetchSublines - } = useProductLinesFetching(data); - const handleValidationCategoryCreated = useCallback( - async ({ type, parentId }: CreatedCategoryInfo) => { - try { - if (type === "line") { - await fetchProductLines(null, parentId); - } else { - await fetchSublines(null, parentId); - } - } catch (error) { - console.error("Failed to refresh product categories:", error); - } - }, - [fetchProductLines, fetchSublines], - ); - - // Function to check if a specific row is being validated - memoized - const isRowValidatingUpc = upcValidation.isRowValidatingUpc; - - // Apply all pending updates to the data state - - // Use AI validation hook - const aiValidation = useAiValidation( - data, - setData, - fields as Fields, - // Create a wrapper function that adapts the rowHook to the expected signature - validationState.rowHook ? - async (row) => { - // Call the original rowHook and return the row itself instead of just Meta - await validationState.rowHook(row, 0, data); - return row; - } : - undefined, - // Create a wrapper function that adapts the tableHook to the expected signature - validationState.tableHook ? - async (rows) => { - // Call the original tableHook and return the rows themselves - await validationState.tableHook(rows); - return rows; - } : - undefined, - revalidateRows // Pass revalidateRows for post-AI validation - ); - - const { translations } = useRsi() - - // State for product search dialog - const [isProductSearchDialogOpen, setIsProductSearchDialogOpen] = useState(false) - - // Add new state for template form dialog - const [isTemplateFormOpen, setIsTemplateFormOpen] = useState(false) - const [templateFormInitialData, setTemplateFormInitialData] = useState(null) - - const [fieldOptions, setFieldOptions] = useState(null) - - const selectedRowCategoryDefaults = useMemo(() => { - const selectedEntries = Object.entries(rowSelection).filter(([, selected]) => selected); - - const resolveRowByKey = (key: string): Record | undefined => { - const numericIndex = Number(key); - if (!Number.isNaN(numericIndex) && numericIndex >= 0 && numericIndex < filteredData.length) { - return filteredData[numericIndex] as Record; - } - - return data.find((row) => { - if (row.__index === undefined || row.__index === null) { - return false; - } - return String(row.__index) === key; - }) as Record | undefined; - }; - - const targetRows: Record[] = selectedEntries.length - ? selectedEntries - .map(([key]) => resolveRowByKey(key)) - .filter((row): row is Record => Boolean(row)) - : (filteredData.length ? filteredData : data) as Record[]; - - if (!targetRows.length) { - return { company: undefined as string | undefined, line: undefined as string | undefined }; - } - - const uniqueCompanyValues = new Set(); - const uniqueLineValues = new Set(); - - targetRows.forEach((row) => { - const companyValue = row.company; - if (companyValue !== undefined && companyValue !== null && String(companyValue).trim() !== "") { - uniqueCompanyValues.add(String(companyValue)); - } - - const lineValue = row.line; - if (lineValue !== undefined && lineValue !== null && String(lineValue).trim() !== "") { - uniqueLineValues.add(String(lineValue)); - } - }); - - const resolvedCompany = uniqueCompanyValues.size === 1 ? Array.from(uniqueCompanyValues)[0] : undefined; - const resolvedLine = uniqueLineValues.size === 1 ? Array.from(uniqueLineValues)[0] : undefined; - - return { - company: resolvedCompany, - line: resolvedLine, - }; - }, [rowSelection, filteredData, data]); - - // Track fields that need revalidation due to value changes - // Combined state: Map - if empty array, revalidate all fields - const [fieldsToRevalidate, setFieldsToRevalidate] = useState>(new Map()); - - // Function to mark a row for revalidation - const markRowForRevalidation = useCallback((rowIndex: number, fieldKey?: string) => { - // Map filtered rowIndex to original data index via __index - const originalIndex = (() => { - try { - const row = filteredData[rowIndex]; - if (!row) return rowIndex; - const id = row.__index; - if (!id) return rowIndex; - const idx = data.findIndex(r => r.__index === id); - return idx >= 0 ? idx : rowIndex; - } catch { - return rowIndex; - } - })(); - - setFieldsToRevalidate(prev => { - const newMap = new Map(prev); - const existingFields = newMap.get(originalIndex) || []; - - if (fieldKey && !existingFields.includes(fieldKey)) { - newMap.set(originalIndex, [...existingFields, fieldKey]); - } else if (!fieldKey) { - newMap.set(originalIndex, existingFields); - } - - return newMap; - }); - }, [data, filteredData]); - - // Add a ref to track the last validation time - - // Trigger revalidation only for specifically marked fields - useEffect(() => { - if (fieldsToRevalidate.size === 0) return; - - // Extract rows and fields map - const rowsToRevalidate = Array.from(fieldsToRevalidate.keys()); - const fieldsMap: {[rowIndex: number]: string[]} = {}; - fieldsToRevalidate.forEach((fields, rowIndex) => { - fieldsMap[rowIndex] = fields; - }); - - // Clear the revalidation map - setFieldsToRevalidate(new Map()); - - // Revalidate each row with specific fields information - revalidateRows(rowsToRevalidate, fieldsMap); - }, [fieldsToRevalidate, revalidateRows]); - - // Function to fetch field options for template form - const fetchFieldOptions = useCallback(async () => { - try { - const response = await axios.get('/api/import/field-options'); - - // Check if suppliers are included in the response - if (response.data && response.data.suppliers) { - } else { - console.warn('No suppliers found in field options response'); - } - - setFieldOptions(response.data); - return response.data; - } catch (error) { - console.error('Error fetching field options:', error); - toast.error('Failed to load field options'); - return null; - } - }, []); - - useEffect(() => { - if (!fieldOptions) { - fetchFieldOptions(); - } - }, [fieldOptions, fetchFieldOptions]); - - // Function to prepare row data for the template form - const prepareRowDataForTemplateForm = useCallback(() => { - // Get the selected row key (should be only one) - const selectedKey = Object.entries(rowSelection) - .filter(([_, selected]) => selected === true) - .map(([key, _]) => key)[0]; - - if (!selectedKey) return null; - - // Try to find the row in the data array - let selectedRow; - - // First check if the key is an index in filteredData - const numericIndex = parseInt(selectedKey); - if (!isNaN(numericIndex) && numericIndex >= 0 && numericIndex < filteredData.length) { - selectedRow = filteredData[numericIndex]; - } - - // If not found by index, try to find it by __index property - if (!selectedRow) { - selectedRow = data.find(row => row.__index === selectedKey); - } - - // If still not found, return null - if (!selectedRow) { - console.error('Selected row not found:', selectedKey); - return null; - } - - // TemplateForm expects supplier as a NUMBER - the field options have numeric values - // Convert the supplier to a number if possible - let supplierValue; - if (selectedRow.supplier) { - const numSupplier = Number(selectedRow.supplier); - supplierValue = !isNaN(numSupplier) ? numSupplier : selectedRow.supplier; - } else { - supplierValue = undefined; - } - - // Create template form data with the correctly typed supplier value - return { - company: selectedRow.company || '', - product_type: selectedRow.product_type || '', - supplier: supplierValue, - msrp: selectedRow.msrp ? Number(Number(selectedRow.msrp).toFixed(2)) : undefined, - cost_each: selectedRow.cost_each ? Number(Number(selectedRow.cost_each).toFixed(2)) : undefined, - qty_per_unit: selectedRow.qty_per_unit ? Number(selectedRow.qty_per_unit) : undefined, - case_qty: selectedRow.case_qty ? Number(selectedRow.case_qty) : undefined, - hts_code: selectedRow.hts_code || undefined, - description: selectedRow.description || undefined, - weight: selectedRow.weight ? Number(Number(selectedRow.weight).toFixed(2)) : undefined, - length: selectedRow.length ? Number(Number(selectedRow.length).toFixed(2)) : undefined, - width: selectedRow.width ? Number(Number(selectedRow.width).toFixed(2)) : undefined, - height: selectedRow.height ? Number(Number(selectedRow.height).toFixed(2)) : undefined, - tax_cat: selectedRow.tax_cat ? String(selectedRow.tax_cat) : undefined, - size_cat: selectedRow.size_cat ? String(selectedRow.size_cat) : undefined, - categories: Array.isArray(selectedRow.categories) ? selectedRow.categories : - (selectedRow.categories ? [selectedRow.categories] : []), - ship_restrictions: selectedRow.ship_restrictions ? String(selectedRow.ship_restrictions) : undefined - }; - }, [data, filteredData, rowSelection]); - - // Add useEffect to fetch field options when template form opens - useEffect(() => { - if (isTemplateFormOpen && !fieldOptions) { - fetchFieldOptions(); - } - }, [isTemplateFormOpen, fieldOptions, fetchFieldOptions]); - - // Function to handle opening the template form - const openTemplateForm = useCallback(async () => { - const templateData = prepareRowDataForTemplateForm(); - if (!templateData) return; - - setTemplateFormInitialData(templateData); - - // Always fetch fresh field options to ensure supplier list is up to date - try { - const options = await fetchFieldOptions(); - if (options && options.suppliers) { - - setIsTemplateFormOpen(true); - } else { - console.error('Failed to load suppliers for template form'); - toast.error('Could not load supplier options'); - } - } catch (error) { - console.error('Error loading field options:', error); - toast.error('Failed to prepare template form'); - } - }, [prepareRowDataForTemplateForm, fetchFieldOptions]); - - // Create a function to validate uniqueness if validateUniqueItemNumbers is not available - - // Apply item numbers to data and trigger revalidation for uniqueness - - // Handle next button click - memoized - const handleNext = useCallback(() => { - // Make sure any pending item numbers are applied - upcValidation.applyItemNumbersToData(updatedRowIds => { - // Mark updated rows for revalidation - updatedRowIds.forEach(rowIndex => { - markRowForRevalidation(rowIndex, 'item_number'); - }); - - // Small delay to ensure all validations complete before proceeding - setTimeout(() => { - // Call the onNext callback with the validated data - onNext?.(data); - }, 100); - }); - - // If no item numbers to apply, just proceed - if (upcValidation.validatingRows.size === 0) { - onNext?.(data); - } - }, [onNext, data, upcValidation, markRowForRevalidation]); - - const deleteSelectedRows = useCallback(() => { - // Get selected row keys (which may be UUIDs) - const selectedKeys = Object.entries(rowSelection) - .filter(([_, selected]) => selected === true) - .map(([key, _]) => key); - - - if (selectedKeys.length === 0) { - toast.error("No rows selected"); - return; - } - - // Map UUID keys to array indices - const selectedIndices = 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 - - - if (selectedIndices.length === 0) { - toast.error('Could not find selected rows'); - return; - } - - // Sort indices in descending order to avoid index shifting during removal - const sortedIndices = [...selectedIndices].sort((a, b) => b - a); - - // Create a new array without the selected rows - const newData = [...data]; - - // Remove rows from bottom up to avoid index issues - sortedIndices.forEach(index => { - if (index >= 0 && index < newData.length) { - newData.splice(index, 1); - } - }); - - // Update the data with rows removed - setData(newData); - - // Clear row selection - setRowSelection({}); - - // Show success message - toast.success( - selectedIndices.length === 1 - ? "Row deleted" - : `${selectedIndices.length} rows deleted` - ); - - // Reindex the data in the next render cycle - requestAnimationFrame(() => { - // Update indices to maintain consistency - setData(current => - current.map((row, newIndex) => ({ - ...row, - __index: String(newIndex) - })) - ); - }); - }, [data, rowSelection, setData, setRowSelection]); - - // Memoize handlers - // This function is defined for potential future use but not currently used - // eslint-disable-next-line @typescript-eslint/no-unused-vars - - const handleRowSelectionChange = useCallback( - (value: React.SetStateAction) => { - setRowSelection(value); - }, - [setRowSelection] - ); - - // Track if we're currently validating a UPC - - // Track last UPC update to prevent conflicting changes - - // Add these ref declarations here, at component level - - // Helper: Process field value transformations - const processFieldValue = useCallback((key: T, value: any): any => { - let processedValue = value; - - // Clean price fields - if ((key === 'msrp' || key === 'cost_each') && value !== undefined && value !== null) { - processedValue = cleanPriceField(value); - } - - // Normalize country code - if (key === 'coo' && typeof value === 'string' && value.trim()) { - const normalized = normalizeCountryCode(value); - if (normalized) { - processedValue = normalized; - } else { - const trimmed = value.trim(); - if (trimmed.length === 2) { - processedValue = trimmed.toUpperCase(); - } - } - } - - if ((key === 'upc' || key === 'barcode') && value !== undefined && value !== null) { - const { corrected } = correctUpcValue(value); - processedValue = corrected; - } - - return processedValue; - }, []); - - // Helper: Handle company change side effects - const handleCompanyChange = useCallback((rowIndex: number, rowId: any, companyId: string) => { - // Clear 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 - }; - } else if (rowIndex >= 0 && rowIndex < newData.length) { - // Fallback if __index is not yet present on the row - newData[rowIndex] = { - ...newData[rowIndex], - line: undefined, - subline: undefined - }; - } - return newData; - }); - - // Fetch product lines - setValidatingCells(prev => new Set(prev).add(`${rowIndex}-line`)); - - setTimeout(() => { - // Use __index when available, otherwise fall back to the UI row index - const rowKey = (rowId !== undefined && rowId !== null) ? rowId : rowIndex; - fetchProductLines(rowKey, companyId) - .catch(err => { - console.error(`Error fetching product lines for company ${companyId}:`, err); - toast.error("Failed to load product lines"); - }) - .finally(() => { - setValidatingCells(prev => { - const newSet = new Set(prev); - newSet.delete(`${rowIndex}-line`); - return newSet; - }); - }); - }, 100); - }, [setData, fetchProductLines]); - - // Helper: Handle line change side effects - const handleLineChange = useCallback((rowIndex: number, rowId: any, lineId: string) => { - // Clear subline value - setData(prevData => { - const newData = [...prevData]; - const idx = newData.findIndex(item => item.__index === rowId); - if (idx >= 0) { - newData[idx] = { - ...newData[idx], - subline: undefined - }; - } else if (rowIndex >= 0 && rowIndex < newData.length) { - // Fallback if __index is not yet present on the row - newData[rowIndex] = { - ...newData[rowIndex], - subline: undefined - }; - } else { - console.warn(`Could not find row with ID ${rowId} to clear subline values`); - } - return newData; - }); - - // Fetch sublines - setValidatingCells(prev => new Set(prev).add(`${rowIndex}-subline`)); - - // Use __index when available, otherwise fall back to the UI row index - const rowKey = (rowId !== undefined && rowId !== null) ? rowId : rowIndex; - fetchSublines(rowKey, lineId) - .catch(err => { - console.error(`Error fetching sublines for line ${lineId}:`, err); - toast.error("Failed to load sublines"); - }) - .finally(() => { - setValidatingCells(prev => { - const newSet = new Set(prev); - newSet.delete(`${rowIndex}-subline`); - return newSet; - }); - }); - }, [setData, fetchSublines]); - - // Helper: Handle UPC validation - const handleUpcValidation = useCallback((rowIndex: number, supplier: string, upc: string) => { - const cellKey = `${rowIndex}-item_number`; - setValidatingCells(prev => new Set(prev).add(cellKey)); - - upcValidation.validateUpc(rowIndex, supplier, upc) - .then(result => { - if (result.success) { - upcValidation.applyItemNumbersToData(); - setTimeout(() => markRowForRevalidation(rowIndex, 'item_number'), 50); - } - }) - .catch(err => console.error("Error validating UPC:", err)) - .finally(() => { - setValidatingCells(prev => { - const newSet = new Set(prev); - newSet.delete(cellKey); - return newSet; - }); - }); - }, [upcValidation, markRowForRevalidation]); - - // Main update handler - simplified to focus on core logic - const handleUpdateRow = useCallback(async (rowIndex: number, key: T, value: any) => { - // Process value transformations - const processedValue = processFieldValue(key, value); - - // Find the row in the data - const rowData = filteredData[rowIndex]; - if (!rowData) { - console.error(`No row data found for index ${rowIndex}`); - return; - } - - // Use __index to find the actual row in the full data array - const rowId = rowData.__index; - const originalIndex = data.findIndex(item => item.__index === rowId); - const idx = originalIndex >= 0 ? originalIndex : rowIndex; - - // Update the row with validation - updateRow(idx, key as unknown as any, processedValue); - - // Handle secondary effects asynchronously - requestAnimationFrame(() => { - if (key === 'company' && value) { - handleCompanyChange(rowIndex, rowId, value.toString()); - } - - if (key === 'line' && value) { - handleLineChange(rowIndex, rowId, value.toString()); - } - - if (key === 'supplier' && value) { - const upcValue = (data[rowIndex] as any)?.upc || (data[rowIndex] as any)?.barcode; - if (upcValue) { - const normalized = correctUpcValue(upcValue).corrected; - if (normalized) { - handleUpcValidation(rowIndex, value.toString(), normalized); - } - } - } - - if ((key === 'upc' || key === 'barcode') && processedValue) { - const supplier = (data[rowIndex] as any)?.supplier; - if (supplier) { - const normalized = correctUpcValue(processedValue).corrected; - if (normalized) { - handleUpcValidation(rowIndex, supplier.toString(), normalized); - } - } - } - }); - }, [data, filteredData, updateRow, processFieldValue, handleCompanyChange, handleLineChange, handleUpcValidation]); - - // Copy-down that keeps validations in sync for all affected rows - const handleCopyDown = useCallback((rowIndex: number, fieldKey: string, endRowIndex?: number) => { - const getActualIndex = (filteredIndex: number) => { - const filteredRow = filteredData[filteredIndex] as Record | undefined - if (!filteredRow) return -1 - const rowId = filteredRow.__index - if (rowId === undefined || rowId === null) return -1 - const actualIndex = dataIndexByRowId.get(rowId) - return actualIndex === undefined ? -1 : actualIndex - } - - const sourceActualIndex = getActualIndex(rowIndex) - if (sourceActualIndex < 0) { - console.error(`Unable to resolve source index ${rowIndex} for copyDown`) - return - } - - const sourceRow = data[sourceActualIndex] as Record | undefined - if (!sourceRow) { - console.error(`Source row ${sourceActualIndex} not found for copyDown`) - return - } - - const baseValue = sourceRow[fieldKey] - const cloneValue = (value: any) => { - if (Array.isArray(value)) return [...value] - if (value && typeof value === 'object') return { ...value } - return value - } - - const maxFilteredIndex = filteredData.length - 1 - const lastFilteredIndex = endRowIndex !== undefined ? Math.min(endRowIndex, maxFilteredIndex) : maxFilteredIndex - if (lastFilteredIndex <= rowIndex) return - - const updatedRows = new Set() - updatedRows.add(sourceActualIndex) - - const snapshot = data.map((row) => ({ ...row })) as RowData[] - - for (let idx = rowIndex + 1; idx <= lastFilteredIndex; idx++) { - const targetActualIndex = getActualIndex(idx) - if (targetActualIndex < 0) continue - - updatedRows.add(targetActualIndex) - snapshot[targetActualIndex] = { - ...snapshot[targetActualIndex], - [fieldKey]: cloneValue(baseValue) - } - - updateRow(targetActualIndex, fieldKey as T, cloneValue(baseValue)) - } - - const affectedIndexes = Array.from(updatedRows) - const fieldsMap: { [rowIndex: number]: string[] } = {} - affectedIndexes.forEach((idx) => { - fieldsMap[idx] = [fieldKey] - }) - - revalidateRows(affectedIndexes, fieldsMap, snapshot) - }, [data, dataIndexByRowId, filteredData, updateRow, revalidateRows]) - - // Memoize the rendered validation table - const renderValidationTable = useMemo(() => { - // Create wrapper for applyTemplate that matches the expected interface - const applyTemplateWrapper = (templateId: string, rowIndexes: number[]) => { - if (rowIndexes.length === 1) { - // Single row apply - pass the array with a single index - applyTemplate(templateId, rowIndexes); - } else if (rowIndexes.length > 1) { - // Multiple rows - use applyTemplateToSelected - applyTemplateToSelected(templateId); - } - }; - - return ( - } - validationErrors={validationErrors} - rowSelection={rowSelection} - setRowSelection={handleRowSelectionChange} - updateRow={handleUpdateRow} - filters={filters} - templates={templates} - applyTemplate={applyTemplateWrapper} - editingCells={editingCells} - setEditingCells={setEditingCells} - getTemplateDisplayText={getTemplateDisplayText} - isValidatingUpc={isRowValidatingUpc} - validatingUpcRows={Array.from(upcValidation.validatingRows)} - copyDown={handleCopyDown} - validatingCells={validatingCells} - isLoadingTemplates={isLoadingTemplates} - rowProductLines={rowProductLines} - rowSublines={rowSublines} - isLoadingLines={isLoadingLines} - isLoadingSublines={isLoadingSublines} - upcValidation={upcValidation} - itemNumbers={upcValidation.itemNumbers} - /> - ); - }, [ - filteredData, - fields, - validationErrors, - rowSelection, - handleRowSelectionChange, - handleUpdateRow, - filters, - templates, - applyTemplate, - applyTemplateToSelected, - getTemplateDisplayText, - isRowValidatingUpc, - upcValidation, - handleCopyDown, - validatingCells, - isLoadingTemplates, - rowProductLines, - rowSublines, - isLoadingLines, - isLoadingSublines - ]); - - // Show loading state during initialization - if (isInitializing) { - // Convert initializationTasks object to array format - const tasksArray = Object.values(initializationTasks).map(task => ({ - label: task.label, - status: task.status as 'pending' | 'in_progress' | 'completed' | 'failed' - })); - - return ( - - ); - } - - return ( -
{ - // Prevent stray text selection when clicking away from cells - try { - const sel = window.getSelection?.(); - if (sel && sel.type === 'Range') { - sel.removeAllRanges(); - } - } catch {} - }} - > -
-
-
-
- {/* Header section */} -
-
-
-

- {translations.validationStep.title || "Validate Data"} -

- -
- {isFromScratch && ( - - )} - - - - New Line/Subline - - } - companies={fieldOptions?.companies || []} - defaultCompanyId={selectedRowCategoryDefaults.company} - defaultLineId={selectedRowCategoryDefaults.line} - onCreated={handleValidationCategoryCreated} - /> -
- updateFilters({ showErrorsOnly: checked })} - id="filter-errors" - /> - -
-
-
-
-
- - {/* Main table section */} -
-
-
-
- {renderValidationTable} -
-
-
-
-
-
- - {/* Selection Action Bar - only shown when items are selected */} - {Object.keys(rowSelection).length > 0 && ( -
-
-
-
- {Object.keys(rowSelection).length} selected -
- - -
- -
- {isLoadingTemplates ? ( - - ) : templates && templates.length > 0 ? ( - { - if (value) { - applyTemplateToSelected(value); - } - }} - getTemplateDisplayText={getTemplateDisplayText} - placeholder="Apply template to selected rows" - triggerClassName="w-[250px] text-xs h-8" - /> - ) : ( - - )} -
- - {Object.keys(rowSelection).length === 1 && ( - - )} - - -
-
- )} -
-
- - {/* Footer with navigation buttons */} -
-
- {onBack && ( - - )} -
- {/* Show Prompt Button */} - - - - - {/* AI Validate Button */} - - - -
-
-
- - {/* AI Validation Dialogs */} - - - {/* Product Search Dialog */} - setIsProductSearchDialogOpen(false)} - onTemplateCreated={loadTemplates} - /> - - {/* Template Form Dialog */} - setIsTemplateFormOpen(false)} - onSuccess={() => { - loadTemplates(); - setIsTemplateFormOpen(false); - }} - initialData={templateFormInitialData} - mode="create" - fieldOptions={fieldOptions} - /> -
- ) -} - -export default ValidationContainer diff --git a/inventory/src/components/product-import/steps/ValidationStepOLD/components/ValidationTable.tsx b/inventory/src/components/product-import/steps/ValidationStepOLD/components/ValidationTable.tsx deleted file mode 100644 index 9690096..0000000 --- a/inventory/src/components/product-import/steps/ValidationStepOLD/components/ValidationTable.tsx +++ /dev/null @@ -1,657 +0,0 @@ -import React, { useMemo, useCallback, useState } from 'react' -import { - useReactTable, - getCoreRowModel, - flexRender, - RowSelectionState, - ColumnDef -} from '@tanstack/react-table' -import { Fields, Field } from '../../../types' -import { RowData, Template } from '../hooks/validationTypes' -import ValidationCell, { CopyDownContext } from './ValidationCell' -import { useRsi } from '../../../hooks/useRsi' -import SearchableTemplateSelect from './SearchableTemplateSelect' -import { Table, TableBody, TableRow, TableCell } from '@/components/ui/table' -import { Checkbox } from '@/components/ui/checkbox' -import { cn } from '@/lib/utils' -import { Button } from '@/components/ui/button' -import { Skeleton } from '@/components/ui/skeleton' - -// Define a simple Error type locally to avoid import issues -type ErrorType = { - message: string; - level: string; - source?: string; -} - -// Stable empty errors array to prevent unnecessary re-renders -// Use a mutable empty array to satisfy the ErrorType[] type -const EMPTY_ERRORS: ErrorType[] = []; - -interface ValidationTableProps { - data: RowData[] - fields: Fields - rowSelection: RowSelectionState - setRowSelection: React.Dispatch> - updateRow: (rowIndex: number, key: T, value: any) => void - validationErrors: Map> - isValidatingUpc: (rowIndex: number) => boolean - validatingUpcRows: number[] - filters?: { showErrorsOnly?: boolean } - templates: Template[] - applyTemplate: (templateId: string, rowIndexes: number[]) => void - getTemplateDisplayText: (templateId: string | null) => string - rowProductLines?: Record - rowSublines?: Record - isLoadingLines?: Record - isLoadingSublines?: Record - upcValidationResults: Map - validatingCells: Set - itemNumbers: Map - isLoadingTemplates?: boolean - copyDown: (rowIndex: number, key: string, endRowIndex?: number) => void - editingCells: Set - setEditingCells: React.Dispatch>> - [key: string]: any -} - -// Simple template select component - let React handle optimization -const TemplateSelectWrapper = ({ - templates, - value, - onValueChange, - getTemplateDisplayText, - defaultBrand, - isLoading -}: { - templates: Template[], - value: string, - onValueChange: (value: string) => void, - getTemplateDisplayText: (value: string | null) => string, - defaultBrand?: string, - isLoading?: boolean -}) => { - if (isLoading) { - return ( -
- -
- ); - } - - return ( - - ); -}; - -const ValidationTable = ({ - data, - fields, - rowSelection, - setRowSelection, - updateRow, - validationErrors, - filters, - templates, - applyTemplate, - getTemplateDisplayText, - validatingCells, - itemNumbers, - isLoadingTemplates = false, - copyDown, - editingCells, - setEditingCells, - rowProductLines = {}, - rowSublines = {}, - isLoadingLines = {}, - isLoadingSublines = {}, - isValidatingUpc, - validatingUpcRows = [], - upcValidationResults -}: ValidationTableProps) => { - const { translations } = useRsi(); - - // Copy-down state combined into single object - type CopyDownState = { - sourceRowIndex: number; - sourceFieldKey: string; - targetRowIndex: number | null; - }; - const [copyDownState, setCopyDownState] = useState(null); - - // Handle copy down completion - const handleCopyDownComplete = useCallback((sourceRowIndex: number, fieldKey: string, targetRowIndex: number) => { - copyDown(sourceRowIndex, fieldKey, targetRowIndex); - setCopyDownState(null); - }, [copyDown]); - - // Create copy down context value - // Use a ref to track partial state during initialization - const partialCopyDownRef = React.useRef<{ rowIndex?: number; fieldKey?: string }>({}); - - const copyDownContextValue = useMemo(() => ({ - isInCopyDownMode: copyDownState !== null, - sourceRowIndex: copyDownState?.sourceRowIndex ?? null, - sourceFieldKey: copyDownState?.sourceFieldKey ?? null, - targetRowIndex: copyDownState?.targetRowIndex ?? null, - setIsInCopyDownMode: (value: boolean) => { - if (!value) { - setCopyDownState(null); - partialCopyDownRef.current = {}; - } - }, - setSourceRowIndex: (rowIndex: number | null) => { - if (rowIndex !== null) { - partialCopyDownRef.current.rowIndex = rowIndex; - // If we have both values, set the full state - if (partialCopyDownRef.current.fieldKey !== undefined) { - setCopyDownState({ - sourceRowIndex: rowIndex, - sourceFieldKey: partialCopyDownRef.current.fieldKey, - targetRowIndex: null - }); - partialCopyDownRef.current = {}; - } - } - }, - setSourceFieldKey: (fieldKey: string | null) => { - if (fieldKey !== null) { - partialCopyDownRef.current.fieldKey = fieldKey; - // If we have both values, set the full state - if (partialCopyDownRef.current.rowIndex !== undefined) { - setCopyDownState({ - sourceRowIndex: partialCopyDownRef.current.rowIndex, - sourceFieldKey: fieldKey, - targetRowIndex: null - }); - partialCopyDownRef.current = {}; - } - } - }, - setTargetRowIndex: (rowIndex: number | null) => { - if (copyDownState) { - setCopyDownState({ - ...copyDownState, - targetRowIndex: rowIndex - }); - } - }, - handleCopyDownComplete - }), [copyDownState, handleCopyDownComplete]); - - // Update targetRowIndex when hovering over rows in copy down mode - const handleRowMouseEnter = useCallback((rowIndex: number) => { - if (copyDownState && copyDownState.sourceRowIndex < rowIndex) { - setCopyDownState({ - ...copyDownState, - targetRowIndex: rowIndex - }); - } - }, [copyDownState]); - - // Memoize the selection column with stable callback - const handleSelectAll = useCallback((value: boolean, table: any) => { - table.toggleAllPageRowsSelected(!!value); - }, []); - - const handleRowSelect = useCallback((value: boolean, row: any) => { - row.toggleSelected(!!value); - }, []); - - const selectionColumn = useMemo((): ColumnDef, any> => ({ - id: 'select', - header: ({ table }) => ( -
- handleSelectAll(!!value, table)} - aria-label="Select all" - /> -
- ), - cell: ({ row }) => ( -
- handleRowSelect(!!value, row)} - aria-label="Select row" - /> -
- ), - enableSorting: false, - enableHiding: false, - size: 50, - }), [handleSelectAll, handleRowSelect]); - - // Memoize template selection handler - const handleTemplateChange = useCallback((value: string, rowIndex: number) => { - applyTemplate(value, [rowIndex]); - }, [applyTemplate]); - - // Memoize the template column with stable callback - const templateColumn = useMemo((): ColumnDef, any> => ({ - accessorKey: '__template', - header: 'Template', - size: 200, - cell: ({ row }) => { - const templateValue = row.original.__template || null; - const defaultBrand = row.original.company || undefined; - const rowIndex = data.findIndex(r => r === row.original); - - return ( - -
- handleTemplateChange(value, rowIndex)} - getTemplateDisplayText={getTemplateDisplayText} - defaultBrand={defaultBrand} - isLoading={isLoadingTemplates} - /> -
-
- ); - } - }), [templates, handleTemplateChange, getTemplateDisplayText, isLoadingTemplates, data]); - - // Cache options by field key to avoid recreating arrays - const optionsCache = useMemo(() => { - const cache = new Map(); - - fields.forEach((field) => { - // Get the field key - const fieldKey = String(field.key); - - // Handle all select and multi-select fields the same way - if (field.fieldType && - (typeof field.fieldType === 'object') && - (field.fieldType.type === 'select' || field.fieldType.type === 'multi-select')) { - cache.set(fieldKey, (field.fieldType as any).options || []); - } - }); - - return cache; - }, [fields]); - - // Memoize the field update handler - const handleFieldUpdate = useCallback((rowIndex: number, fieldKey: T, value: any) => { - updateRow(rowIndex, fieldKey, value); - }, [updateRow]); - - // Memoize the copyDown handler - const handleCopyDown = useCallback((rowIndex: number, fieldKey: string, endRowIndex?: number) => { - copyDown(rowIndex, fieldKey, endRowIndex); - }, [copyDown]); - - // Use validatingUpcRows for calculation - const isRowValidatingUpc = useCallback((rowIndex: number) => { - return isValidatingUpc?.(rowIndex) || validatingUpcRows.includes(rowIndex); - }, [isValidatingUpc, validatingUpcRows]); - - // Use upcValidationResults for display, prioritizing the most recent values - const getRowUpcResult = useCallback((rowIndex: number) => { - // ALWAYS get from the data array directly - most authoritative source - const rowData = data[rowIndex]; - if (rowData && rowData.item_number) { - return rowData.item_number; - } - - // Maps are only backup sources when data doesn't have a value - const itemNumberFromMap = itemNumbers.get(rowIndex); - if (itemNumberFromMap) { - return itemNumberFromMap; - } - - // Last resort - upcValidationResults - const upcResult = upcValidationResults?.get(rowIndex)?.itemNumber; - if (upcResult) { - return upcResult; - } - - return undefined; - }, [data, itemNumbers, upcValidationResults]); - - // Memoize field columns with stable handlers - const fieldColumns = useMemo(() => fields.map((field): ColumnDef, any> | null => { - // Don't filter out disabled fields, just pass the disabled state to the cell component - - const fieldWidth = field.width || ( - field.fieldType.type === "checkbox" ? 80 : - field.fieldType.type === "select" ? 150 : - field.fieldType.type === "multi-select" ? 200 : - (field.fieldType.type === "input" || field.fieldType.type === "multi-input") && - (field.fieldType as any).multiline ? 300 : - 150 - ); - - const fieldKey = String(field.key); - // Get cached options for this field - const fieldOptions = optionsCache.get(fieldKey) || []; - - return { - accessorKey: fieldKey, - header: field.label || fieldKey, - size: fieldWidth, - cell: ({ row }) => { - // Get row-specific options for line and subline fields - let options = fieldOptions; - const rowId = (row.original as any).__index; - const lookupKey = (rowId !== undefined && rowId !== null) ? rowId : row.index; - - if (fieldKey === 'line' && lookupKey !== undefined && rowProductLines[lookupKey]) { - options = rowProductLines[lookupKey]; - } else if (fieldKey === 'subline' && lookupKey !== undefined && rowSublines[lookupKey]) { - options = rowSublines[lookupKey]; - } - - // Get the current cell value first - const currentValue = fieldKey === 'item_number' && row.original[field.key] - ? row.original[field.key] - : row.original[field.key as keyof typeof row.original]; - - // Determine if this cell is in loading state - let isLoading = false; - - const cellLoadingKey = `${row.index}-${fieldKey}`; - const isEmpty = currentValue === undefined || currentValue === null || currentValue === '' || - (Array.isArray(currentValue) && currentValue.length === 0); - - // CRITICAL: Check validatingCells FIRST - this shows loading for item_number during UPC validation - // even if the field already has a value (because we're fetching a new one) - if (validatingCells.has(cellLoadingKey)) { - isLoading = true; - } - // Only show loading for empty fields for these other cases - else if (isEmpty) { - // Check if UPC is validating for this row and field is item_number - if (fieldKey === 'item_number' && isRowValidatingUpc(row.index)) { - isLoading = true; - } - // Add loading state for line/subline fields - else if (fieldKey === 'line' && lookupKey !== undefined && isLoadingLines[lookupKey]) { - isLoading = true; - } - else if (fieldKey === 'subline' && lookupKey !== undefined && isLoadingSublines[lookupKey]) { - isLoading = true; - } - } - - // Get validation errors for this cell - // Use stable EMPTY_ERRORS to avoid new array creation on every render - const cellErrors = validationErrors.get(row.index)?.[fieldKey] || EMPTY_ERRORS; - - // Create a copy of the field with guaranteed field type for line and subline fields - let fieldWithType = field; - - // Ensure line and subline fields always have the correct fieldType - if (fieldKey === 'line' || fieldKey === 'subline') { - // Create a deep clone of the field to prevent any reference issues - fieldWithType = { - ...JSON.parse(JSON.stringify(field)), // Ensure deep clone - fieldType: { - type: 'select', - options: options - }, - // Explicitly mark as not disabled to ensure dropdown works - disabled: false - }; - - } - - // CRITICAL CHANGE: Get item number directly from row data first for item_number fields - let itemNumber; - if (fieldKey === 'item_number') { - // Check directly in row data first - this is the most accurate source - const directValue = row.original[fieldKey]; - if (directValue) { - itemNumber = directValue; - } else { - // Fall back to centralized getter that checks all sources - itemNumber = getRowUpcResult(row.index); - } - } - - // Create stable keys that only change when actual content changes - const cellKey = fieldKey === 'item_number' - ? `cell-${row.index}-${fieldKey}-${itemNumber || 'empty'}` // Only change when itemNumber actually changes - : `cell-${row.index}-${fieldKey}`; - - return ( - } - value={currentValue} - onChange={(value) => handleFieldUpdate(row.index, field.key as T, value)} - errors={cellErrors} - isValidating={isLoading} - fieldKey={fieldKey} - options={options} - itemNumber={itemNumber} - width={fieldWidth} - rowIndex={row.index} - copyDown={(endRowIndex?: number) => handleCopyDown(row.index, field.key as string, endRowIndex)} - totalRows={data.length} - rowData={row.original as Record} - editingCells={editingCells} - setEditingCells={setEditingCells} - /> - ); - } - }; - }).filter((col): col is ColumnDef, any> => col !== null), - [fields, validationErrors, validatingCells, itemNumbers, handleFieldUpdate, handleCopyDown, optionsCache, - data.length, rowProductLines, rowSublines, isLoadingLines, isLoadingSublines, - isRowValidatingUpc, getRowUpcResult]); - - // Combine columns - const columns = useMemo(() => [selectionColumn, templateColumn, ...fieldColumns], [selectionColumn, templateColumn, fieldColumns]); - - const table = useReactTable({ - data, - columns, - state: { - rowSelection, - }, - enableRowSelection: true, - onRowSelectionChange: setRowSelection, - getCoreRowModel: getCoreRowModel(), - getRowId: useCallback((_row: RowData, index: number) => String(index), []), - }); - - // Calculate total table width for stable horizontal scrolling - const totalWidth = useMemo(() => { - return columns.reduce((total, col) => total + (col.size || 0), 0); - }, [columns]); - - // Don't render if no data - if (data.length === 0) { - return ( -
-

- {filters?.showErrorsOnly - ? translations.validationStep.noRowsMessageWhenFiltered || "No rows with errors" - : translations.validationStep.noRowsMessage || "No data to display"} -

-
- ); - } - - return ( - -
- {/* Add global styles for copy down mode */} - {copyDownState && ( - - )} - {copyDownState && ( -
-
{ - // Find the column index - const colIndex = columns.findIndex(col => - 'accessorKey' in col && col.accessorKey === copyDownState.sourceFieldKey - ); - - // If column not found, position at a default location - if (colIndex === -1) return '50px'; - - // Calculate position based on column widths - let position = 0; - for (let i = 0; i < colIndex; i++) { - position += columns[i].size || 0; - } - - // Add half of the current column width to center it - position += (columns[colIndex].size || 0) / 2; - - // Adjust to center the notification - position -= 120; // Half of the notification width - - return `${Math.max(50, position)}px`; - })() - }} - > -
- Click on the last row you want to copy to -
- -
-
- )} -
- {/* Custom Table Header - Always Visible with GPU acceleration */} -
-
- {table.getFlatHeaders().map((header) => { - const width = header.getSize(); - return ( -
- {flexRender(header.column.columnDef.header, header.getContext())} -
- ); - })} -
-
- - {/* Table Body - With optimized rendering */} - - - {table.getRowModel().rows.map((row) => { - // Precompute validation error status for this row - const hasErrors = validationErrors.has(parseInt(row.id)) && - Object.keys(validationErrors.get(parseInt(row.id)) || {}).length > 0; - - // Precompute copy down target status - const isCopyDownTarget = copyDownState !== null && - parseInt(row.id) > copyDownState.sourceRowIndex; - - // Using CSS variables for better performance on hover/state changes - const rowStyle = { - cursor: isCopyDownTarget ? 'pointer' : undefined, - position: 'relative' as const, - willChange: copyDownState ? 'background-color' : 'auto', - contain: 'layout', - transition: 'background-color 100ms ease-in-out' - }; - - return ( - handleRowMouseEnter(parseInt(row.id))} - > - {row.getVisibleCells().map((cell: any) => ( - - {flexRender(cell.column.columnDef.cell, cell.getContext())} - - ))} - - ); - })} - -
-
-
-
- ); -}; - -// Memo comparator: re-render when any prop affecting visible state changes. -// Keep this conservative to avoid skipping updates for loading/options states. -const areEqual = (prev: ValidationTableProps, next: ValidationTableProps) => { - return ( - // Core props - prev.data === next.data && - prev.validationErrors === next.validationErrors && - prev.rowSelection === next.rowSelection && - // Loading + validation state that affects cell skeletons - prev.validatingCells === next.validatingCells && - prev.isLoadingLines === next.isLoadingLines && - prev.isLoadingSublines === next.isLoadingSublines && - // Options sources used for line/subline selects - prev.rowProductLines === next.rowProductLines && - prev.rowSublines === next.rowSublines - ); -}; - -export default React.memo(ValidationTable, areEqual); diff --git a/inventory/src/components/product-import/steps/ValidationStepOLD/components/cells/CheckboxCell.tsx b/inventory/src/components/product-import/steps/ValidationStepOLD/components/cells/CheckboxCell.tsx deleted file mode 100644 index db18398..0000000 --- a/inventory/src/components/product-import/steps/ValidationStepOLD/components/cells/CheckboxCell.tsx +++ /dev/null @@ -1,145 +0,0 @@ -import { useState, useEffect, useCallback } from 'react' -import { Field } from '../../../../types' -import { Checkbox } from '@/components/ui/checkbox' -import { cn } from '@/lib/utils' -import React from 'react' - -interface CheckboxCellProps { - field: Field - value: any - onChange: (value: any) => void - hasErrors?: boolean - booleanMatches?: Record - className?: string -} - -const CheckboxCell = ({ - field, - value, - onChange, - hasErrors, - booleanMatches = {}, - className = '' -}: CheckboxCellProps) => { - const [checked, setChecked] = useState(false) - // Add state for hover - const [isHovered, setIsHovered] = useState(false) - - // Initialize checkbox state - useEffect(() => { - if (value === undefined || value === null) { - setChecked(false) - return - } - - if (typeof value === 'boolean') { - setChecked(value) - return - } - - // Handle string values using booleanMatches - if (typeof value === 'string') { - // First try the field's booleanMatches - const fieldBooleanMatches = field.fieldType.type === 'checkbox' - ? field.fieldType.booleanMatches || {} - : {} - - // Merge with the provided booleanMatches, with the provided ones taking precedence - const allMatches = { ...fieldBooleanMatches, ...booleanMatches } - - // Try to find the value in the matches - const matchEntry = Object.entries(allMatches).find(([k]) => - k.toLowerCase() === value.toLowerCase()) - - if (matchEntry) { - setChecked(matchEntry[1]) - return - } - - // If no match found, use common true/false strings - const trueStrings = ['yes', 'true', '1', 'y'] - const falseStrings = ['no', 'false', '0', 'n'] - - if (trueStrings.includes(value.toLowerCase())) { - setChecked(true) - return - } - - if (falseStrings.includes(value.toLowerCase())) { - setChecked(false) - return - } - } - - // For any other values, try to convert to boolean - setChecked(!!value) - }, [value, field.fieldType, booleanMatches]) - - // Helper function to check if a class is present in the className string - const hasClass = (cls: string): boolean => { - const classNames = (className || '').split(' '); - return classNames.includes(cls); - }; - - // Handle checkbox change - const handleChange = useCallback((checked: boolean) => { - setChecked(checked) - onChange(checked) - }, [onChange]) - - // Add outline even when not in focus - const outlineClass = "border focus-visible:ring-0 focus-visible:ring-offset-0" - - return ( -
setIsHovered(true)} - onMouseLeave={() => setIsHovered(false)} - > - -
- ) -} - -export default React.memo(CheckboxCell, (prev, next) => { - if (prev.hasErrors !== next.hasErrors) return false; - if (prev.field !== next.field) return false; - if (prev.value !== next.value) return false; - if (prev.className !== next.className) return false; - - // Compare booleanMatches objects - const prevMatches = prev.booleanMatches || {}; - const nextMatches = next.booleanMatches || {}; - const prevKeys = Object.keys(prevMatches); - const nextKeys = Object.keys(nextMatches); - - if (prevKeys.length !== nextKeys.length) return false; - - for (const key of prevKeys) { - if (prevMatches[key] !== nextMatches[key]) return false; - } - - return true; -}); \ No newline at end of file diff --git a/inventory/src/components/product-import/steps/ValidationStepOLD/components/cells/InputCell.tsx b/inventory/src/components/product-import/steps/ValidationStepOLD/components/cells/InputCell.tsx deleted file mode 100644 index 353f9fa..0000000 --- a/inventory/src/components/product-import/steps/ValidationStepOLD/components/cells/InputCell.tsx +++ /dev/null @@ -1,215 +0,0 @@ -import React, { useState, useCallback, useMemo } from 'react' -import { Field } from '../../../../types' -import { Input } from '@/components/ui/input' -import { cn } from '@/lib/utils' -import MultilineInput from './MultilineInput' - -interface InputCellProps { - field: Field - value: any - onChange: (value: any) => void - onStartEdit?: () => void - onEndEdit?: () => void - hasErrors?: boolean - isMultiline?: boolean - isPrice?: boolean - disabled?: boolean - className?: string -} - -// (removed unused formatPrice helper) - -const InputCell = ({ - field, - value, - onChange, - onStartEdit, - onEndEdit, - hasErrors, - isMultiline = false, - isPrice = false, - disabled = false, - className = '' -}: InputCellProps) => { - const [isEditing, setIsEditing] = useState(false); - const [editValue, setEditValue] = useState(''); - const [isHovered, setIsHovered] = useState(false); - - // Remove optimistic updates and rely on parent state - - // Helper function to check if a class is present in the className string - const hasClass = (cls: string): boolean => { - const classNames = className.split(' '); - return classNames.includes(cls); - }; - - // No complex initialization needed - - // Handle focus event - const handleFocus = useCallback(() => { - setIsEditing(true); - - if (value !== undefined && value !== null) { - if (isPrice) { - // Remove any non-numeric characters except decimal point for editing - const numericValue = String(value).replace(/[^\d.]/g, ''); - setEditValue(numericValue); - } else { - setEditValue(String(value)); - } - } else { - setEditValue(''); - } - - onStartEdit?.(); - }, [value, onStartEdit, isPrice]); - - // Handle blur event - save to parent only - const handleBlur = useCallback(() => { - const finalValue = editValue.trim(); - - // Save to parent - parent must update immediately for this to work - onChange(finalValue); - - // Exit editing mode - setIsEditing(false); - onEndEdit?.(); - }, [editValue, onChange, onEndEdit]); - - // Handle direct input change - optimized to be synchronous for typing - const handleChange = useCallback((e: React.ChangeEvent) => { - const newValue = isPrice ? e.target.value.replace(/[$,]/g, '') : e.target.value; - setEditValue(newValue); - }, [isPrice]); - - // Get the display value - use parent value directly - const displayValue = useMemo(() => { - const currentValue = value ?? ''; - - // Handle price formatting for display - if (isPrice && currentValue !== '' && currentValue !== undefined && currentValue !== null) { - if (typeof currentValue === 'number') { - return currentValue.toFixed(2); - } else if (typeof currentValue === 'string' && /^-?\d+(\.\d+)?$/.test(currentValue)) { - return parseFloat(currentValue).toFixed(2); - } - } - - // For non-price or invalid price values, return as-is - return String(currentValue); - }, [isPrice, value]); - - // Add outline even when not in focus - const outlineClass = "border focus-visible:ring-0 focus-visible:ring-offset-0"; - - // If disabled, just render the value without any interactivity - if (disabled) { - return ( -
setIsHovered(true)} - onMouseLeave={() => setIsHovered(false)} - > - {displayValue} -
- ); - } - - // Render multiline fields using the dedicated MultilineInput component - if (isMultiline) { - return ( - - ); - } - - // Original component for non-multiline fields - return ( -
- {isEditing ? ( - - ) : ( -
setIsHovered(true)} - onMouseLeave={() => setIsHovered(false)} - > - {displayValue} -
- )} -
- ) -} - -// Simplified memo comparison -export default React.memo(InputCell, (prev, next) => { - // Only re-render if essential props change - return prev.value === next.value && - prev.hasErrors === next.hasErrors && - prev.disabled === next.disabled && - prev.field === next.field; -}); diff --git a/inventory/src/components/product-import/steps/ValidationStepOLD/components/cells/MultiSelectCell.tsx b/inventory/src/components/product-import/steps/ValidationStepOLD/components/cells/MultiSelectCell.tsx deleted file mode 100644 index c83b57b..0000000 --- a/inventory/src/components/product-import/steps/ValidationStepOLD/components/cells/MultiSelectCell.tsx +++ /dev/null @@ -1,576 +0,0 @@ -import React, { useState, useCallback, useMemo, useEffect, useRef } from 'react' -import { Field } from '../../../../types' -import { cn } from '@/lib/utils' -import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from '@/components/ui/command' -import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover' -import { Button } from '@/components/ui/button' -import { Check, ChevronsUpDown } from 'lucide-react' -import { Badge } from '@/components/ui/badge' - -// Define a type for field options -interface FieldOption { - label: string; - value: string; - hex?: string; // optional hex color for colors field -} - -interface MultiSelectCellProps { - field: Field - value: string[] - onChange: (value: string[]) => void - onStartEdit?: () => void - onEndEdit?: () => void - hasErrors?: boolean - options?: readonly FieldOption[] - disabled?: boolean - className?: string -} - -// Memoized option item to prevent unnecessary renders for large option lists -const OptionItem = React.memo(({ - option, - isSelected, - onSelect -}: { - option: FieldOption, - isSelected: boolean, - onSelect: (value: string) => void -}) => ( - onSelect(option.value)} - className="flex w-full" - > -
- - {option.label} -
-
-), (prev, next) => { - return prev.option.value === next.option.value && - prev.isSelected === next.isSelected; -}); - -OptionItem.displayName = 'OptionItem'; - -// Create a virtualized list component for large option lists -const VirtualizedOptions = React.memo(({ - options, - selectedValues, - onSelect, - maxHeight = 200 -}: { - options: FieldOption[], - selectedValues: Set, - onSelect: (value: string) => void, - maxHeight?: number -}) => { - const listRef = useRef(null); - - // Only render visible options for better performance with large lists - const [visibleOptions, setVisibleOptions] = useState([]); - const [scrollPosition, setScrollPosition] = useState(0); - - // Constants for virtualization - const itemHeight = 32; // Height of each option item in pixels - const visibleCount = Math.ceil(maxHeight / itemHeight) + 2; // Number of visible items + buffer - - // Handle scroll events - const handleScroll = useCallback(() => { - if (listRef.current) { - setScrollPosition(listRef.current.scrollTop); - } - }, []); - - // Update visible options based on scroll position - useEffect(() => { - if (options.length <= visibleCount) { - // If fewer options than visible count, just show all - setVisibleOptions(options); - return; - } - - // Calculate start and end indices - const startIndex = Math.floor(scrollPosition / itemHeight); - const endIndex = Math.min(startIndex + visibleCount, options.length); - - // Update visible options - setVisibleOptions(options.slice(Math.max(0, startIndex), endIndex)); - }, [options, scrollPosition, visibleCount, itemHeight]); - - // If fewer than the threshold, render all directly - if (options.length <= 100) { - return ( -
- {options.map(option => ( - - ))} -
- ); - } - - return ( -
-
-
- {visibleOptions.map(option => ( - - ))} -
-
-
- ); -}); - -VirtualizedOptions.displayName = 'VirtualizedOptions'; - -const MultiSelectCell = ({ - field, - value = [], - onChange, - onStartEdit, - onEndEdit, - hasErrors, - options: providedOptions, - disabled = false, - className = '' -}: MultiSelectCellProps) => { - const [open, setOpen] = useState(false) - const [searchQuery, setSearchQuery] = useState("") - // Add internal state for tracking selections - ensure value is always an array - const [internalValue, setInternalValue] = useState(Array.isArray(value) ? value : []) - // Ref for the command list to enable scrolling - const commandListRef = useRef(null) - // Add state for hover - const [isHovered, setIsHovered] = useState(false) - // Add ref to track if we need to sync internal state with external value - const shouldSyncWithExternalValue = useRef(true) - - // Create a memoized Set for fast lookups of selected values - const selectedValueSet = useMemo(() => new Set(internalValue), [internalValue]); - - // Sync internalValue with external value when component mounts or value changes externally - // Modified to prevent infinite loop by checking if values are different before updating - useEffect(() => { - // Only sync if we should (not during internal edits) and if not open - if (shouldSyncWithExternalValue.current && !open) { - const externalValue = Array.isArray(value) ? value : []; - - // Only update if values are actually different to prevent infinite loops - if (internalValue.length !== externalValue.length || - !internalValue.every(v => externalValue.includes(v)) || - !externalValue.every(v => internalValue.includes(v))) { - setInternalValue(externalValue); - } - } - }, [value, open, internalValue]); - - // Handle open state changes with improved responsiveness - const handleOpenChange = useCallback((newOpen: boolean) => { - if (open && !newOpen) { - // Prevent syncing with external value during our internal update - shouldSyncWithExternalValue.current = false; - - // Only update parent state when dropdown closes - // Make a defensive copy to avoid mutations - const valuesToCommit = [...internalValue]; - - // Immediate UI update - setOpen(false); - - // Update parent with the value immediately - onChange(valuesToCommit); - if (onEndEdit) onEndEdit(); - - // Allow syncing with external value again after a short delay - setTimeout(() => { - shouldSyncWithExternalValue.current = true; - }, 0); - } else if (newOpen && !open) { - // When opening the dropdown, sync with external value - const externalValue = Array.isArray(value) ? value : []; - setInternalValue(externalValue); - setSearchQuery(""); // Reset search query on open - setOpen(true); - if (onStartEdit) onStartEdit(); - } else if (!newOpen) { - // Handle case when dropdown is already closed but handleOpenChange is called - setOpen(false); - } - }, [open, internalValue, value, onChange, onStartEdit, onEndEdit]); - - // Memoize field options to prevent unnecessary recalculations - const selectOptions = useMemo(() => { - const fieldType = field.fieldType; - const fieldOptions = fieldType && - (fieldType.type === 'select' || fieldType.type === 'multi-select') && - fieldType.options ? - fieldType.options : - []; - - // Use provided options or field options, ensuring they have the correct shape - // Skip this work if we have a large number of options and they didn't change - if (providedOptions && providedOptions.length > 0) { - // Check if options are already in the right format - if (typeof providedOptions[0] === 'object' && 'label' in providedOptions[0] && 'value' in providedOptions[0]) { - // Preserve optional hex if present (hex or hex_color without #) - return (providedOptions as any[]).map(opt => ({ - label: opt.label, - value: String(opt.value), - hex: opt.hex - || (opt.hexColor ? `#${String(opt.hexColor).replace(/^#/, '')}` : undefined) - || (opt.hex_color ? `#${String(opt.hex_color).replace(/^#/, '')}` : undefined) - })) as FieldOption[]; - } - - return (providedOptions as any[]).map(option => ({ - label: option.label || String(option.value), - value: String(option.value), - hex: option.hex - || (option.hexColor ? `#${String(option.hexColor).replace(/^#/, '')}` : undefined) - || (option.hex_color ? `#${String(option.hex_color).replace(/^#/, '')}` : undefined) - })); - } - - // Check field options format - if (fieldOptions.length > 0) { - if (typeof fieldOptions[0] === 'object' && 'label' in fieldOptions[0] && 'value' in fieldOptions[0]) { - return (fieldOptions as any[]).map(opt => ({ - label: opt.label, - value: String(opt.value), - hex: opt.hex - || (opt.hexColor ? `#${String(opt.hexColor).replace(/^#/, '')}` : undefined) - || (opt.hex_color ? `#${String(opt.hex_color).replace(/^#/, '')}` : undefined) - })) as FieldOption[]; - } - - return (fieldOptions as any[]).map(option => ({ - label: option.label || String(option.value), - value: String(option.value), - hex: option.hex - || (option.hexColor ? `#${String(option.hexColor).replace(/^#/, '')}` : undefined) - || (option.hex_color ? `#${String(option.hex_color).replace(/^#/, '')}` : undefined) - })); - } - - // Add default option if no options available - return [{ label: 'No options available', value: '' }]; - }, [field.fieldType, providedOptions]); - - // Use deferredValue for search to prevent UI blocking with large lists - const deferredSearchQuery = React.useDeferredValue(searchQuery); - - // Memoize filtered options based on search query - efficient filtering algorithm - const filteredOptions = useMemo(() => { - // If no search query, return all options - if (!deferredSearchQuery.trim()) return selectOptions; - - const query = deferredSearchQuery.toLowerCase(); - - // Use faster algorithm for large option lists - if (selectOptions.length > 100) { - return selectOptions.filter(option => { - // First check starting with the query (most relevant) - if (option.label.toLowerCase().startsWith(query)) return true; - - // Then check includes for more general matches - return option.label.toLowerCase().includes(query); - }); - } - - // For smaller lists, do full text search - return selectOptions.filter(option => - option.label.toLowerCase().includes(query) - ); - }, [selectOptions, deferredSearchQuery]); - - // Sort options with selected items at the top for the dropdown - only for smaller lists - const sortedOptions = useMemo(() => { - // Skip expensive sorting for large lists - if (selectOptions.length > 100) return filteredOptions; - - return [...filteredOptions].sort((a, b) => { - const aSelected = selectedValueSet.has(a.value); - const bSelected = selectedValueSet.has(b.value); - - if (aSelected && !bSelected) return -1; - if (!aSelected && bSelected) return 1; - return a.label.localeCompare(b.label); - }); - }, [filteredOptions, selectedValueSet, selectOptions.length]); - - // Memoize selected values display - const selectedValues = useMemo(() => { - // Use a map for looking up options by value for better performance - const optionsMap = new Map(selectOptions.map(opt => [opt.value, opt])); - - return internalValue.map(v => { - const option = optionsMap.get(v); - return { - value: v, - label: option ? option.label : String(v) - }; - }); - }, [internalValue, selectOptions]); - - // Update the handleSelect to operate on internalValue instead of directly calling onChange - const handleSelect = useCallback((selectedValue: string) => { - // Prevent syncing with external value during our internal update - shouldSyncWithExternalValue.current = false; - - setInternalValue(prev => { - let newValue; - if (prev.includes(selectedValue)) { - // Remove the value - newValue = prev.filter(v => v !== selectedValue); - } else { - // Add the value - make a new array to avoid mutations - newValue = [...prev, selectedValue]; - } - return newValue; - }); - - // Allow syncing with external value again after a short delay - setTimeout(() => { - shouldSyncWithExternalValue.current = true; - }, 0); - }, []); - - // Handle wheel scroll in dropdown - const handleWheel = useCallback((e: React.WheelEvent) => { - if (commandListRef.current) { - e.stopPropagation(); - commandListRef.current.scrollTop += e.deltaY; - } - }, []); - - // Helper function to check if a class is present in the className string - const hasClass = (cls: string): boolean => { - const classNames = className.split(' '); - return classNames.includes(cls); - }; - - // If disabled, just render the value without any interactivity - if (disabled) { - const displayValue = internalValue.length > 0 - ? internalValue.map(val => { - const option = selectOptions.find(opt => opt.value === val); - return option ? option.label : val; - }).join(', ') - : ''; - - return ( -
setIsHovered(true)} - onMouseLeave={() => setIsHovered(false)} - > - {displayValue || ""} -
- ); - } - - return ( - { - // Only open the popover if we're not in copy down mode - if (!hasClass('!bg-blue-100') && !hasClass('!bg-blue-200') && !hasClass('hover:!bg-blue-100')) { - setOpen(o); - handleOpenChange(o); - } - }}> - - - - - - - - No results found. - - {sortedOptions.map((option) => ( - handleSelect(option.value)} - className="cursor-pointer" - > -
- {field.key === 'colors' && option.hex && ( - - )} - {option.label} -
- {selectedValueSet.has(option.value) && ( - - )} -
- ))} -
-
-
-
-
- ); -}; - -MultiSelectCell.displayName = 'MultiSelectCell'; - -export default React.memo(MultiSelectCell, (prev, next) => { - // Check primitive props first (cheap comparisons) - if (prev.hasErrors !== next.hasErrors) return false; - if (prev.disabled !== next.disabled) return false; - if (prev.className !== next.className) return false; - - // Check field reference - if (prev.field !== next.field) return false; - - // Check value arrays (potentially expensive for large arrays) - // Handle undefined or null values safely - const prevValue = prev.value || []; - const nextValue = next.value || []; - - if (prevValue.length !== nextValue.length) return false; - for (let i = 0; i < prevValue.length; i++) { - if (prevValue[i] !== nextValue[i]) return false; - } - - // Check options (potentially expensive for large option lists) - const prevOptions = prev.options || []; - const nextOptions = next.options || []; - if (prevOptions.length !== nextOptions.length) return false; - - // For large option lists, just compare references - if (prevOptions.length > 100) { - return prevOptions === nextOptions; - } - - // For smaller lists, do a shallow comparison - for (let i = 0; i < prevOptions.length; i++) { - if (prevOptions[i] !== nextOptions[i]) return false; - } - - return true; -}); diff --git a/inventory/src/components/product-import/steps/ValidationStepOLD/components/cells/MultilineInput.tsx b/inventory/src/components/product-import/steps/ValidationStepOLD/components/cells/MultilineInput.tsx deleted file mode 100644 index 65b3d65..0000000 --- a/inventory/src/components/product-import/steps/ValidationStepOLD/components/cells/MultilineInput.tsx +++ /dev/null @@ -1,238 +0,0 @@ -import React, { useState, useCallback, useRef, useEffect } from 'react' -import { Field } from '../../../../types' -import { Textarea } from '@/components/ui/textarea' -import { cn } from '@/lib/utils' -import { Popover, PopoverTrigger, PopoverContent } from '@/components/ui/popover' -import { X } from 'lucide-react' -import { Button } from '@/components/ui/button' - -interface MultilineInputProps { - field: Field - value: any - onChange: (value: any) => void - hasErrors?: boolean - disabled?: boolean - className?: string -} - -const MultilineInput = ({ - field, - value, - onChange, - hasErrors = false, - disabled = false, - className = '' -}: MultilineInputProps) => { - const [popoverOpen, setPopoverOpen] = useState(false); - const [editValue, setEditValue] = useState(''); - const [localDisplayValue, setLocalDisplayValue] = useState(null); - const cellRef = useRef(null); - const preventReopenRef = useRef(false); - const pendingChangeRef = useRef(null); - - // Add state for hover - const [isHovered, setIsHovered] = useState(false); - - // Helper function to check if a class is present in the className string - const hasClass = (cls: string): boolean => { - const classNames = (className || '').split(' '); - return classNames.includes(cls); - }; - - // Initialize localDisplayValue on mount and when value changes externally - useEffect(() => { - if (localDisplayValue === null || - (typeof value === 'string' && typeof localDisplayValue === 'string' && - value.trim() !== localDisplayValue.trim())) { - setLocalDisplayValue(value); - } - }, [value, localDisplayValue]); - - // Process any pending changes in the background - useEffect(() => { - if (pendingChangeRef.current !== null && !popoverOpen) { - const newValue = pendingChangeRef.current; - pendingChangeRef.current = null; - // Apply changes after the popover is closed - if (newValue !== value) { - onChange(newValue); - } - } - }, [popoverOpen, onChange, value]); - - // Handle trigger click to toggle the popover - const handleTriggerClick = useCallback((e: React.MouseEvent) => { - if (preventReopenRef.current) { - e.preventDefault(); - e.stopPropagation(); - preventReopenRef.current = false; - return; - } - - // Only process if not already open - if (!popoverOpen) { - setPopoverOpen(true); - // Initialize edit value from the current display - setEditValue(localDisplayValue || value || ''); - } - }, [popoverOpen, value, localDisplayValue]); - - // Handle immediate close of popover - const handleClosePopover = useCallback(() => { - // Only process if we have changes - if (editValue !== value || editValue !== localDisplayValue) { - // Store pending changes for async processing - pendingChangeRef.current = editValue; - - // Update local display immediately - setLocalDisplayValue(editValue); - - // Queue up the change to be processed in the background - setTimeout(() => { - onChange(editValue); - }, 0); - } - - // Immediately close popover - setPopoverOpen(false); - - // Prevent reopening - preventReopenRef.current = true; - setTimeout(() => { - preventReopenRef.current = false; - }, 100); - }, [editValue, value, localDisplayValue, onChange]); - - // Handle clicking outside the popover - const handleInteractOutside = useCallback(() => { - handleClosePopover(); - }, [handleClosePopover]); - - // Handle popover open/close - const handlePopoverOpenChange = useCallback((open: boolean) => { - if (!open && popoverOpen) { - // Just call the close handler - handleClosePopover(); - } else if (open && !popoverOpen) { - // When opening, set edit value from current display - setEditValue(localDisplayValue || value || ''); - setPopoverOpen(true); - } - }, [value, popoverOpen, handleClosePopover, localDisplayValue]); - - // Handle direct input change - const handleChange = useCallback((e: React.ChangeEvent) => { - setEditValue(e.target.value); - }, []); - - // Calculate display value - const displayValue = localDisplayValue !== null ? localDisplayValue : (value ?? ''); - - // Add outline even when not in focus - const outlineClass = "border focus-visible:ring-0 focus-visible:ring-offset-0"; - - // If disabled, just render the value without any interactivity - if (disabled) { - return ( -
setIsHovered(true)} - onMouseLeave={() => setIsHovered(false)} - > - {displayValue} -
- ); - } - - return ( -
- - -
setIsHovered(true)} - onMouseLeave={() => setIsHovered(false)} - > - {displayValue} -
-
- -
- - -