diff --git a/inventory/docs/ValidationStep-Refactoring-Plan.md b/inventory/docs/ValidationStep-Refactoring-Plan.md new file mode 100644 index 0000000..5b97edc --- /dev/null +++ b/inventory/docs/ValidationStep-Refactoring-Plan.md @@ -0,0 +1,396 @@ +# ValidationStep Component Refactoring Plan + +## Overview + +This document outlines a comprehensive plan to refactor the current ValidationStep component (4000+ lines) into a more maintainable, modular structure. The new implementation will be developed alongside the existing component without modifying the original code. Once completed, the previous step in the workflow will offer the option to continue to either the original ValidationStep or the new implementation. + +## Table of Contents + +1. [Current Component Analysis](#current-component-analysis) +2. [New Architecture Design](#new-architecture-design) +3. [Component Structure](#component-structure) +4. [State Management](#state-management) +5. [Key Features Implementation](#key-features-implementation) +6. [Integration Plan](#integration-plan) +7. [Testing Strategy](#testing-strategy) +8. [Project Timeline](#project-timeline) +9. [Design Principles](#design-principles) +10. [Appendix: Function Reference](#appendix-function-reference) + +## Current Component Analysis + +The current ValidationStep component has several issues: + +- **Size**: At over 4000 lines, it's difficult to maintain and understand +- **Multiple responsibilities**: Handles validation, UI rendering, template management, and more +- **Special cases**: Contains numerous special case handlers and exceptions +- **Complex state management**: State is distributed across multiple useState calls +- **Tightly coupled concerns**: UI, validation logic, and business rules are intertwined + +### Key Features to Preserve + +1. **Data Validation** + - Field-level validation (required, regex, unique) + - Row-level validation (supplier, company fields) + - UPC validation with API integration + - AI-assisted validation + +2. **Template Management** + - Saving, loading, and applying templates + - Template-based validation + +3. **UI Components** + - Editable table with specialized cell renderers + - Error display and management + - Filtering and sorting capabilities + - Status indicators and progress tracking + +4. **Special Field Handling** + - Input fields with price formatting + - Multi-input fields with separator configuration + - Select fields with dropdown options + - Checkbox fields with boolean value mapping + - UPC fields with specialized validation + +5. **User Interaction Flows** + - Tab and keyboard navigation + - Bulk operations (select all, apply template) + - Row validation on value change + - Error reporting and display + +## New Architecture Design + +The new architecture will follow these principles: + +1. **Separation of Concerns** + - UI rendering separate from business logic + - Validation logic isolated from state management + - Clear interfaces between components + +2. **Composable Components** + - Small, focused components with single responsibilities + - Reusable pattern for different field types + +3. **Centralized State Management** + - Custom hooks for state management + - Clear data flow patterns + - Reduced prop drilling + +4. **Consistent Error Handling** + - Standardized error structure + - Predictable error propagation + - User-friendly error display + +5. **Performance Optimization** + - Virtualized table rendering + - Memoization of expensive computations + - Deferred validation for better user experience + +## Component Structure + +The new ValidationStepNew folder has the following structure: + +``` +ValidationStepNew/ +├── index.tsx # Main entry point that composes all pieces +├── components/ # UI Components +│ ├── ValidationContainer.tsx # Main wrapper component +│ ├── ValidationTable.tsx # Table implementation +│ ├── ValidationCell.tsx # Cell component +│ ├── ValidationSidebar.tsx # Sidebar with controls +│ ├── ValidationToolbar.tsx # Top toolbar (removed as unnecessary) +│ ├── TemplateManager.tsx # Template management +│ ├── FilterPanel.tsx # Filtering interface (integrated into Container) +│ └── cells/ # Specialized cell renderers +│ ├── InputCell.tsx +│ ├── SelectCell.tsx +│ ├── MultiInputCell.tsx +│ └── CheckboxCell.tsx +├── hooks/ # Custom hooks +│ ├── useValidationState.tsx # Main state management +│ ├── useTemplates.tsx # Template-related logic (integrated into ValidationState) +│ ├── useFilters.tsx # Filtering logic (integrated into ValidationState) +│ └── useUpcValidation.tsx # UPC-specific validation +└── utils/ # Utility functions + ├── validationUtils.ts # Validation helper functions + ├── formatters.ts # Value formatting utilities + └── constants.ts # Constant values and configuration +``` + +### Component Responsibilities + +#### ValidationContainer +- Main container component +- Coordinates between subcomponents +- Manages global state +- Handles navigation events (next, back) +- Contains filter controls + +#### ValidationTable +- Displays the data in tabular form +- Manages selection state +- Handles keyboard navigation +- Integrates with TanStack Table +- Displays properly styled rows and cells + +#### ValidationCell +- Factory component that renders appropriate cell type +- Manages cell-level state +- Handles validation errors display +- Manages edit mode +- Shows consistent error indicators + +#### TemplateManager +- Handles template selection UI +- Provides template save/load functionality +- Manages template application to rows + +#### Cell Components +- **InputCell**: Handles text input with multiline and price support +- **MultiInputCell**: Handles multiple values with separator configuration +- **SelectCell**: Command/popover component for single selection +- **CheckboxCell**: Boolean value selection with mapping support + +## State Management + +### Core State Interface + +```typescript +interface ValidationState { + // Core data + data: RowData[]; + filteredData: RowData[]; + + // Validation state + isValidating: boolean; + validationErrors: Map>; + rowValidationStatus: Map; + + // Selection state + rowSelection: RowSelectionState; + + // Template state + templates: Template[]; + selectedTemplateId: string | null; + + // Filter state + filters: FilterState; + + // Methods + updateRow: (rowIndex: number, key: T, value: any) => void; + validateRow: (rowIndex: number) => Promise; + validateUpc: (rowIndex: number, upcValue: string) => Promise; + applyTemplate: (templateId: string, rowIndexes: number[]) => void; + saveTemplate: (name: string, type: string) => void; + setFilters: (newFilters: Partial) => void; + // Additional methods... +} +``` + +### useValidationState Hook + +The main state management hook handles: + +- Data manipulation (update, sort, filter) +- Selection management +- Validation coordination +- Integration with validation utilities +- Template management +- Filtering and sorting + +## Key Features Implementation + +### 1. Field Type Handling + +Implemented a strategy pattern for different field types: + +```typescript +// In ValidationCell +const renderCellContent = () => { + const fieldType = field.fieldType.type + + switch (fieldType) { + case 'input': + return field={field} value={value} onChange={onChange} ... /> + case 'multi-input': + return field={field} value={value} onChange={onChange} ... /> + case 'select': + return field={field} value={value} onChange={onChange} ... /> + // etc. + } +} +``` + +### 2. Validation Logic + +Validation is broken down into clear steps: + +1. **Field Validation**: Apply field-level validations (required, regex, etc.) +2. **Row Validation**: Apply row-level validations and rowHook +3. **Table Validation**: Apply table-level validations (unique) and tableHook + +Validation now happens automatically without explicit buttons, with immediate feedback on field blur. + +### 3. UI Components + +UI components follow these principles: + +1. **Consistent Styling**: All components use shadcn UI for consistent look and feel +2. **Visual Feedback**: Errors are clearly indicated with icons and border styling +3. **Intuitive Editing**: Fields show outlines even when not in focus, and edit on click +4. **Proper Command Pattern**: Select and multi-select fields use command/popover pattern for better UX +5. **Focus Management**: Fields close when clicking away and perform validation on blur + +## Design Principles + +Based on user preferences and best practices, the following design principles guide this refactoring: + +1. **Automatic Validation** + - Validation should happen automatically without explicit buttons + - All validation should run on initial data load + - Fields should validate on blur (when user clicks away) + +2. **Modern UI Patterns** + - Command/popover components for all selects and multi-selects + - Consistent field outlines and borders even when not in focus + - Badge patterns for multi-select items + - Clear visual indicators for errors + +3. **Reduced Complexity** + - Remove unnecessary UI elements like "validate all" buttons + - Eliminate redundant state and toast notifications + - Simplify component hierarchy where possible + - Find root causes rather than adding special cases + +4. **Consistent Component Behavior** + - Fields should close when clicking away + - All inputs should follow the same editing pattern + - Error handling should be consistent across all field types + - Multi-select fields should allow selecting multiple items with clear visual feedback + +## Integration Plan + +### 1. Creating the New Component Structure + +Folder structure has been created without modifying the existing code: + +```bash +mkdir -p inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/{components,hooks,utils} +mkdir -p inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells +``` + +### 2. Implementing Basic Components + +Core components have been implemented: + +1. Created index.tsx as the main entry point +2. Implemented ValidationContainer with basic state management +3. Created ValidationTable for data display +4. Implemented basic cell rendering with specialized cell types + +### 3. Implementing State Management + +State management has been implemented: + +1. Created useValidationState hook +2. Implemented data transformation utilities +3. Added validation logic + +### 4. Integrating with Previous Step + +The previous step component allows choosing between validation implementations, enabling gradual testing and adoption. + +## Testing Strategy + +1. **Unit Tests** + - Test individual utility functions + - Test hooks in isolation + - Test individual UI components + +2. **Integration Tests** + - Test component interactions + - Test state management flow + - Test validation logic integration + +3. **Comparison Tests** + - Compare output of new component with original + - Verify that all functionality works the same + +4. **Performance Tests** + - Measure render times + - Evaluate memory usage + - Compare against original component + +## Project Timeline + +1. **Phase 1: Initial Structure (Completed)** + - Set up folder structure + - Implement basic components + - Create core state management + +2. **Phase 2: Core Functionality (In Progress)** + - Implement validation logic (completed) + - Create cell renderers (completed) + - Add template management (in progress) + +3. **Phase 3: Special Features (Upcoming)** + - Implement UPC validation + - Add AI validation + - Handle special cases + +4. **Phase 4: UI Refinement (Ongoing)** + - Improve error display (completed) + - Enhance user interactions (completed) + - Optimize performance (in progress) + +5. **Phase 5: Testing and Integration (Upcoming)** + - Write tests + - Fix bugs + - Integrate with previous step + +## Appendix: Function Reference + +This section documents the core functions from the original ValidationStep that need to be preserved in the new implementation. + +### Validation Functions + +1. **validateRegex** - Validates values against regex patterns +2. **getValidationError** - Determines field-level validation errors +3. **validateAndCommit** - Validates and commits new values +4. **validateData** - Validates all data rows +5. **validateUpcAndGenerateItemNumbers** - Validates UPC codes and generates item numbers + +### Formatting Functions + +1. **formatPrice** - Formats price values +2. **getDisplayValue** - Gets formatted display value based on field type +3. **isMultiInputType** - Checks if field is multi-input type +4. **getMultiInputSeparator** - Gets separator for multi-input fields +5. **isPriceField** - Checks if field should be formatted as price + +### Template Functions + +1. **loadTemplates** - Loads templates from storage +2. **saveTemplate** - Saves a new template +3. **applyTemplate** - Applies a template to selected rows +4. **getTemplateDisplayText** - Gets display text for a template + +### AI Validation Functions + +1. **handleAiValidation** - Triggers AI validation +2. **showCurrentPrompt** - Shows current AI prompt +3. **getFieldDisplayValue** - Gets display value for a field +4. **highlightDifferences** - Highlights differences between original and corrected values +5. **getFieldDisplayValueWithHighlight** - Gets display value with highlighted differences +6. **revertAiChange** - Reverts an AI-suggested change +7. **isChangeReverted** - Checks if an AI change has been reverted + +### Event Handlers + +1. **handleUpcValueUpdate** - Handles UPC value updates +2. **handleBlur** - Handles input blur events +3. **handleWheel** - Handles wheel events for navigation +4. **copyValueDown** - Copies a value to cells below +5. **handleSkuGeneration** - Generates SKUs + +By following this refactoring plan, we continue to transform the monolithic ValidationStep component into a modular, maintainable set of components while preserving all existing functionality and aligning with user preferences for design and behavior. \ No newline at end of file diff --git a/inventory/docs/ValidationStepNew-Implementation-Status.md b/inventory/docs/ValidationStepNew-Implementation-Status.md new file mode 100644 index 0000000..b18e6b2 --- /dev/null +++ b/inventory/docs/ValidationStepNew-Implementation-Status.md @@ -0,0 +1,137 @@ +# ValidationStepNew Implementation Status + +## Overview + +This document outlines the current status of the ValidationStepNew implementation, a refactored version of the original ValidationStep component. The goal is to create a more maintainable, modular component that preserves all functionality of the original while eliminating technical debt and implementing modern UI patterns. + +## Design Principles + +Based on the user's preferences, we're following these core design principles: + +1. **Automatic Validation** + - ✅ Validation runs automatically on data load + - ✅ No explicit "validate all" button needed + - ✅ Fields validate on blur when user clicks away + - ✅ Immediate visual feedback for validation errors + +2. **Modern UI Patterns** + - ✅ Command/popover components for selects and multi-selects + - ✅ Consistent field outlines and borders even when not in focus + - ✅ Badge pattern for multi-select field items + - ✅ Visual indicators for errors with appropriate styling + +3. **Reduced Complexity** + - ✅ Removed unnecessary UI elements like "validate all" button + - ✅ Eliminated redundant toast notifications + - ✅ Simplified component hierarchy + - ✅ Fixed root causes rather than adding special cases + +4. **Consistent Behavior** + - ✅ Fields close when clicking away + - ✅ All inputs follow the same editing pattern + - ✅ Error handling is consistent across field types + - ✅ Multi-select fields allow selecting multiple items + +## Completed Components + +### Core Structure +- ✅ Main component structure +- ✅ Directory organization +- ✅ TypeScript interfaces +- ✅ Props definition and passing + +### State Management +- ✅ `useValidationState` hook for centralized state +- ✅ Data validation logic +- ✅ Integration with rowHook and tableHook +- ✅ Error tracking and management +- ✅ Row selection +- ✅ Automatic validation on data load + +### UI Components +- ✅ ValidationContainer with appropriate layout +- ✅ ValidationTable with shadcn UI components +- ✅ ValidationCell factory component +- ✅ Row select/deselect functionality +- ✅ Error display and indicators +- ✅ Selection action bar + +### Cell Components +- ✅ InputCell with price and multiline support +- ✅ MultiInputCell with separator configuration +- ✅ SelectCell using command/popover pattern +- ✅ CheckboxCell with boolean mapping +- ✅ Consistent styling across all field types +- ✅ Proper edit/view state management +- ✅ Outlined borders in both edit and view modes + +### Utility Functions +- ✅ Value formatting for display +- ✅ Field type detection +- ✅ Error creation and management +- ✅ Price formatting + +### UI Improvements +- ✅ Consistent borders and field outlines +- ✅ Fields that properly close when clicking away +- ✅ Multi-select with badge UI pattern +- ✅ Command pattern for searchable select menus +- ✅ Better visual error indication + +## Pending Tasks + +### Enhanced Validation +- ⏳ AI validation system +- ⏳ Custom validation hooks +- ⏳ Enhanced UPC validation with API integration +- ⏳ Validation visualizations + +### Advanced UI Features +- ⏳ Table virtualization for performance +- ⏳ Drag-and-drop reordering +- ⏳ Bulk operations (copy down, fill all, etc.) +- ⏳ Keyboard navigation improvements +- ⏳ Template dialogs and management UI + +### Special Features +- ⏳ Image preview integration +- ⏳ SKU generation system +- ⏳ Item number generation +- ⏳ Dependent dropdown values + +### Testing +- ⏳ Unit tests for utility functions +- ⏳ Component tests +- ⏳ Integration tests +- ⏳ Performance benchmarks + +## Known Issues + +1. TypeScript error for `validationDisabled` property in ValidationCell.tsx +2. Some type casting is needed due to complex generic types +3. Need to address edge cases for multi-select fields validation +4. Proper error handling for API calls needs implementation + +## Next Steps + +1. Fix TypeScript errors in ValidationCell and related components +2. Complete template management functionality +3. Implement UPC validation with API integration +4. Make multi-select field validation more robust +5. Add comprehensive tests + +## Performance Improvements + +We've already implemented several performance optimizations: + +1. ✅ More efficient state updates by removing unnecessary re-renders +2. ✅ Better error handling to prevent cascading validations +3. ✅ Improved component isolation to prevent unnecessary re-renders +4. ✅ Automatic validation that doesn't block the UI + +Additional planned improvements: + +1. Virtualized table rendering for large datasets +2. Memoization of expensive calculations +3. Optimized state updates to minimize re-renders +4. Batched API calls for validation \ No newline at end of file diff --git a/inventory/src/lib/react-spreadsheet-import/src/steps/MatchColumnsStep/MatchColumnsStep.tsx b/inventory/src/lib/react-spreadsheet-import/src/steps/MatchColumnsStep/MatchColumnsStep.tsx index 1f411b8..4a1407c 100644 --- a/inventory/src/lib/react-spreadsheet-import/src/steps/MatchColumnsStep/MatchColumnsStep.tsx +++ b/inventory/src/lib/react-spreadsheet-import/src/steps/MatchColumnsStep/MatchColumnsStep.tsx @@ -34,6 +34,8 @@ import { CommandList, } from "@/components/ui/command" import { cn } from "@/lib/utils" +import { Switch } from "@/components/ui/switch" +import { Label } from "@/components/ui/label" export type MatchColumnsProps = { data: RawData[] @@ -48,6 +50,7 @@ export type GlobalSelections = { company?: string line?: string subline?: string + useNewValidation?: boolean } export enum ColumnType { @@ -1698,11 +1701,25 @@ export const MatchColumnsStep = React.memo(({
- {onBack && ( - - )} + + {onBack && ( + + )} + + +
+
+ + setGlobalSelections(prev => ({ ...prev, useNewValidation: checked })) + } + /> + +
+
diff --git a/inventory/src/lib/react-spreadsheet-import/src/steps/UploadFlow.tsx b/inventory/src/lib/react-spreadsheet-import/src/steps/UploadFlow.tsx index 37e51b9..4c4659c 100644 --- a/inventory/src/lib/react-spreadsheet-import/src/steps/UploadFlow.tsx +++ b/inventory/src/lib/react-spreadsheet-import/src/steps/UploadFlow.tsx @@ -5,6 +5,7 @@ import { SelectHeaderStep } from "./SelectHeaderStep/SelectHeaderStep" import { SelectSheetStep } from "./SelectSheetStep/SelectSheetStep" import { mapWorkbook } from "../utils/mapWorkbook" import { ValidationStep } from "./ValidationStep/ValidationStep" +import { ValidationStepNew } from "./ValidationStepNew" import { ImageUploadStep } from "./ImageUploadStep/ImageUploadStep" import { addErrorsAndRunHooks } from "./ValidationStep/utils/dataMutations" import { MatchColumnsStep, type GlobalSelections } from "./MatchColumnsStep/MatchColumnsStep" @@ -172,10 +173,23 @@ export const UploadFlow = ({ state, onNext, onBack }: Props) => { try { const data = await matchColumnsStepHook(values, rawData, columns) const dataWithMeta = await addErrorsAndRunHooks(data, fields, rowHook, tableHook) + + // Apply global selections to each row of data if they exist + const dataWithGlobalSelections = globalSelections + ? dataWithMeta.map(row => { + const newRow = { ...row }; + if (globalSelections.supplier) newRow.supplier = globalSelections.supplier; + if (globalSelections.company) newRow.company = globalSelections.company; + if (globalSelections.line) newRow.line = globalSelections.line; + if (globalSelections.subline) newRow.subline = globalSelections.subline; + return newRow; + }) + : dataWithMeta; + setPersistedGlobalSelections(globalSelections) onNext({ type: StepType.validateData, - data: dataWithMeta, + data: dataWithGlobalSelections, globalSelections, }) } catch (e) { @@ -186,6 +200,35 @@ export const UploadFlow = ({ state, onNext, onBack }: Props) => { /> ) case StepType.validateData: + // Check if new validation component should be used + if (state.globalSelections?.useNewValidation) { + // Use the new ValidationStepNew component + return ( + { + if (onBack) { + // When going back, preserve the global selections + setPersistedGlobalSelections(state.globalSelections) + onBack() + } + }} + onNext={(validatedData) => { + // Go to image upload step with the validated data + onNext({ + type: StepType.imageUpload, + data: validatedData, + file: uploadedFile!, + globalSelections: state.globalSelections + }); + }} + isFromScratch={state.isFromScratch} + /> + ); + } + + // Otherwise, use the original ValidationStep component return ( & { handleUpcValidation?: (upcValue: string) => Promise; + rowIndex?: number; // Add rowIndex for tracking loading state }; productLines?: SelectOption[]; sublines?: SelectOption[]; + isUpcValidating?: boolean; + isRowValidating?: boolean; // Add a prop to indicate if this specific row is being validated } // Define ValidationIcon before EditableCell @@ -158,8 +161,8 @@ const ValidationIcon = memo(({ error }: { error: { level: string, message: strin -
- +
+
@@ -170,7 +173,7 @@ const ValidationIcon = memo(({ error }: { error: { level: string, message: strin )) // Wrap EditableCell with memo to avoid unnecessary re-renders -const EditableCell = memo(({ value, onChange, error, field, productLines, sublines }: CellProps) => { +const EditableCell = memo(({ value, onChange, error, field, productLines, sublines, isUpcValidating, isRowValidating }: CellProps) => { const [isEditing, setIsEditing] = useState(false) const [inputValue, setInputValue] = useState(value ?? "") const [searchQuery, setSearchQuery] = useState("") @@ -228,8 +231,10 @@ const EditableCell = memo(({ value, onChange, error, field, productLines, sublin // Always update input value when value prop changes setInputValue(value ?? "") - // Log updates for item number/SKU fields to help with debugging - if (field.key === 'sku' || field.key === 'itemnumber' || field.key === 'item_number') { + // Log updates for item number/SKU fields to help with debugging - but only for significant changes + if ((field.key === 'sku' || field.key === 'itemnumber' || field.key === 'item_number') && + value !== undefined && + value !== '') { console.log(`EditableCell ${field.key} value updated to "${value}"`); } }, [value, field.key]) @@ -241,6 +246,21 @@ const EditableCell = memo(({ value, onChange, error, field, productLines, sublin setLocalValues(selectedValues) } }, [value, field.fieldType.type]) + + // Check if this is an item number field + const isItemNumberField = field.key === 'sku' || field.key === 'itemnumber' || field.key === 'item_number'; + + // For item number field, show loading state when UPC validation is in progress and no value exists + if (isItemNumberField && !value && (isUpcValidating || isRowValidating)) { + return ( +
+
+ + +
+
+ ); + } const formatPrice = (value: string) => { if (!value) return value @@ -301,7 +321,7 @@ const EditableCell = memo(({ value, onChange, error, field, productLines, sublin const matchingOption = productLines.find((opt: SelectOption) => String(opt.value) === String(value)); if (matchingOption) { - console.log('Found line in productLines:', value, '->', matchingOption.label); + // Remove excessive logging return matchingOption.label; } } @@ -310,7 +330,7 @@ const EditableCell = memo(({ value, onChange, error, field, productLines, sublin const fallbackOptionLine = fieldType.options.find((opt: SelectOption) => String(opt.value) === String(value)); if (fallbackOptionLine) { - console.log('Found line in fallback options:', value, '->', fallbackOptionLine.label); + // Remove excessive logging return fallbackOptionLine.label; } } @@ -365,8 +385,7 @@ const EditableCell = memo(({ value, onChange, error, field, productLines, sublin // Special handling for SKU/item number fields to make them more visible if (field.key === 'sku' || field.key === 'itemnumber' || field.key === 'item_number') { - console.log(`Displaying SKU/item number value: "${value}"`); - // Format it nicely for display + // Format it nicely for display - removed excessive logging return value ? `${value}` : ""; } @@ -385,8 +404,20 @@ const EditableCell = memo(({ value, onChange, error, field, productLines, sublin newValue = formatPrice(newValue) } - // Log commits for UPC and SKU/item number fields - if (field.key === 'upc' || field.key === 'barcode' || field.key === 'sku' || field.key === 'itemnumber' || field.key === 'item_number') { + // Special handling for UPC fields + if (field.key === 'upc' || field.key === 'barcode') { + console.log(`Committing UPC value: "${newValue}" - IMPORTANT: This must be saved`); + + // For UPC fields, we need to ensure the value is committed properly + // First, call onChange to update the parent component's state + onChange(newValue); + + // Return true to indicate validation passed + return true; + } + + // Log commits for other important fields + if (field.key === 'sku' || field.key === 'itemnumber' || field.key === 'item_number') { console.log(`Committing ${field.key} value: "${newValue}"`); } @@ -410,11 +441,16 @@ const EditableCell = memo(({ value, onChange, error, field, productLines, sublin console.log(`UPC blur handler - saving UPC value: ${inputValue}`); - // Store the current UPC value to ensure it's not lost - // IMPORTANT: Don't commit the UPC value yet using validateAndCommit - // We'll commit both UPC and item number together + // Store the current UPC value to ensure it's not lost const currentUpcValue = inputValue; + // First, commit the value immediately - this is the key change to match Enter key behavior + validateAndCommit(currentUpcValue); + + // Exit editing mode BEFORE starting validation + // This is critical - it ensures the UI updates with the new value before validation starts + setIsEditing(false); + // Then call the UPC validation function to generate item number if (field.handleUpcValidation) { setIsProcessingUpc(true); @@ -425,12 +461,13 @@ const EditableCell = memo(({ value, onChange, error, field, productLines, sublin const result = await field.handleUpcValidation(currentUpcValue); console.log('UPC validation result in handleBlur:', result); - // We won't need to re-set UPC since it's handled in the validation function now + // CRITICAL FIX: After validation completes, re-commit the UPC value to ensure it wasn't lost + setTimeout(() => { + console.log(`Re-committing UPC value after validation: ${currentUpcValue}`); + onChange(currentUpcValue); + }, 100); if (result && result.error) { - // If there was an error, now we need to commit the UPC - validateAndCommit(currentUpcValue); - toast({ title: "UPC Validation Error", description: result.message || "Error validating UPC", @@ -439,8 +476,12 @@ const EditableCell = memo(({ value, onChange, error, field, productLines, sublin } } catch (error) { console.error('Error in UPC validation:', error); - // Ensure UPC is still saved even if there was an error - validateAndCommit(currentUpcValue); + + // CRITICAL FIX: Even after an error, re-commit the UPC value + setTimeout(() => { + console.log(`Re-committing UPC value after error: ${currentUpcValue}`); + onChange(currentUpcValue); + }, 100); toast({ title: "UPC Validation Error", @@ -450,22 +491,21 @@ const EditableCell = memo(({ value, onChange, error, field, productLines, sublin } finally { setIsProcessingUpc(false); } - } else { - // If no validation function, just commit the UPC normally - validateAndCommit(currentUpcValue); } + + // We've already exited editing mode, so return early + return; } catch (error) { console.error('Error in UPC blur handler:', error); // Make sure to commit any changes if there was an error validateAndCommit(inputValue); + setIsEditing(false); } } else { // Normal validation for other fields validateAndCommit(inputValue); + setIsEditing(false); } - - // Exit editing mode after processing - setIsEditing(false); } if (isEditing) { @@ -692,7 +732,59 @@ const EditableCell = memo(({ value, onChange, error, field, productLines, sublin onChange={(e) => { setInputValue(e.target.value) }} - onBlur={handleBlur} + onBlur={() => { + // For UPC fields, use a more direct approach + if (field.key === 'upc' || field.key === 'barcode') { + // Store the current value + const currentValue = inputValue; + + // First commit the value + validateAndCommit(currentValue); + + // Exit editing mode + setIsEditing(false); + + // Then handle validation + if (field.handleUpcValidation) { + setIsProcessingUpc(true); + + // Use a promise to handle the validation + field.handleUpcValidation(currentValue) + .then(result => { + // After validation completes, re-commit the value + setTimeout(() => { + onChange(currentValue); + }, 100); + + if (result && result.error) { + toast({ + title: "UPC Validation Error", + description: result.message || "Error validating UPC", + variant: "destructive", + }); + } + }) + .catch(error => { + // Even after an error, re-commit the value + setTimeout(() => { + onChange(currentValue); + }, 100); + + toast({ + title: "UPC Validation Error", + description: error instanceof Error ? error.message : "Error processing UPC", + variant: "destructive", + }); + }) + .finally(() => { + setIsProcessingUpc(false); + }); + } + } else { + // For non-UPC fields, use the regular handleBlur + handleBlur(); + } + }} onKeyDown={(e) => { if (e.key === "Enter") { if (isMultiInputType(field.fieldType)) { @@ -733,7 +825,59 @@ const EditableCell = memo(({ value, onChange, error, field, productLines, sublin onChange={(e) => { setInputValue(e.target.value) }} - onBlur={handleBlur} + onBlur={() => { + // For UPC fields, use a more direct approach + if (field.key === 'upc' || field.key === 'barcode') { + // Store the current value + const currentValue = inputValue; + + // First commit the value + validateAndCommit(currentValue); + + // Exit editing mode + setIsEditing(false); + + // Then handle validation + if (field.handleUpcValidation) { + setIsProcessingUpc(true); + + // Use a promise to handle the validation + field.handleUpcValidation(currentValue) + .then(result => { + // After validation completes, re-commit the value + setTimeout(() => { + onChange(currentValue); + }, 100); + + if (result && result.error) { + toast({ + title: "UPC Validation Error", + description: result.message || "Error validating UPC", + variant: "destructive", + }); + } + }) + .catch(error => { + // Even after an error, re-commit the value + setTimeout(() => { + onChange(currentValue); + }, 100); + + toast({ + title: "UPC Validation Error", + description: error instanceof Error ? error.message : "Error processing UPC", + variant: "destructive", + }); + }) + .finally(() => { + setIsProcessingUpc(false); + }); + } + } else { + // For non-UPC fields, use the regular handleBlur + handleBlur(); + } + }} onKeyDown={(e) => { if (e.key === "Enter") { if (isMultiInputType(field.fieldType)) { @@ -832,7 +976,7 @@ const EditableCell = memo(({ value, onChange, error, field, productLines, sublin )}
) -}, (prevProps, nextProps) => { +}, (prevProps: CellProps, nextProps: CellProps) => { // Custom comparison for memo - force re-render when value changes for sku/itemnumber fields const isSKUField = prevProps.field.key === 'sku' || prevProps.field.key === 'itemnumber' || prevProps.field.key === 'item_number'; const isUPCField = prevProps.field.key === 'upc' || prevProps.field.key === 'barcode'; @@ -849,6 +993,11 @@ const EditableCell = memo(({ value, onChange, error, field, productLines, sublin return false; // Return false to trigger re-render } + // Always re-render if isUpcValidating state changes and this is an item number field + if ((isSKUField) && prevProps.isUpcValidating !== nextProps.isUpcValidating) { + return false; // Return false to trigger re-render + } + // For other fields, use standard equality check return ( prevProps.value === nextProps.value && @@ -1790,7 +1939,6 @@ export const ValidationStep = ({ file, onBack, onNext, - globalSelections, isFromScratch }: Props) => { const { translations, fields, onClose, onSubmit, rowHook, tableHook, allowInvalidSubmit } = useRsi(); @@ -1799,263 +1947,31 @@ export const ValidationStep = ({ // Track which changes have been reverted const [revertedChanges, setRevertedChanges] = useState>(new Set()); const [isProductSearchDialogOpen, setIsProductSearchDialogOpen] = useState(false); - const prevGlobalSelectionsRef = useRef(globalSelections); - - // Fetch product lines when company is selected - const { data: productLines } = useQuery({ - queryKey: ["product-lines", globalSelections?.company], - queryFn: async () => { - if (!globalSelections?.company) return []; - console.log('Fetching product lines for company:', globalSelections.company); - const response = await fetch(`${config.apiUrl}/import/product-lines/${globalSelections.company}`); - if (!response.ok) { - console.error('Failed to fetch product lines:', response.status, response.statusText); - throw new Error("Failed to fetch product lines"); - } - const data = await response.json(); - console.log('Received product lines:', data); - return data; - }, - enabled: !!globalSelections?.company, - staleTime: 30000, // Cache for 30 seconds - }); - - // Fetch sublines when line is selected - const { data: sublines } = useQuery({ - queryKey: ["sublines", globalSelections?.line], - queryFn: async () => { - if (!globalSelections?.line) return []; - console.log('Fetching sublines for line:', globalSelections.line); - const response = await fetch(`${config.apiUrl}/import/sublines/${globalSelections.line}`); - if (!response.ok) { - console.error('Failed to fetch sublines:', response.status, response.statusText); - throw new Error("Failed to fetch sublines"); - } - const data = await response.json(); - console.log('Received sublines:', data); - return data; - }, - enabled: !!globalSelections?.line, - staleTime: 30000, // Cache for 30 seconds - }); - - // Helper function to safely set a field value and update options if needed - const setFieldValue = (field: Field | undefined, value: string | undefined, options?: SelectOption[]) => { - if (!field || !value) return undefined; - if (field.fieldType.type === 'select') { - // Use provided options if available, otherwise use field's default options - const fieldOptions = options || (field.fieldType as SelectFieldType).options; - // First try to find by value (ID) - const optionByValue = fieldOptions.find(opt => opt.value === value); - if (optionByValue) { - return optionByValue.value; - } - // Then try to find by label (name) - const optionByLabel = fieldOptions.find(opt => opt.label.toLowerCase() === value.toLowerCase()); - if (optionByLabel) { - return optionByLabel.value; - } - } - return value; - }; - - // Update field options with fetched data - const fieldsWithUpdatedOptions = useMemo(() => { - return Array.from(fields as ReadonlyFields).map(field => { - if (field.key === 'line') { - // Check if we have product lines available - const hasProductLines = productLines && productLines.length > 0; - - // For line field, ensure we have the proper options - return { - ...field, - fieldType: { - ...field.fieldType, - // Use fetched product lines if available, otherwise keep existing options - options: hasProductLines - ? productLines - : (field.fieldType.type === 'select' || field.fieldType.type === 'multi-select') - ? field.fieldType.options - : [] - }, - // The line field should only be disabled if no company is selected AND no product lines available - disabled: !globalSelections?.company && !hasProductLines - } as Field; - } - - if (field.key === 'subline') { - // Check if we have sublines available - const hasSublines = sublines && sublines.length > 0; - - // For subline field, ensure we have the proper options - return { - ...field, - fieldType: { - ...field.fieldType, - // Use fetched sublines if available, otherwise keep existing options - options: hasSublines - ? sublines - : (field.fieldType.type === 'select' || field.fieldType.type === 'multi-select') - ? field.fieldType.options - : [] - }, - // The subline field should only be disabled if no line is selected AND no sublines available - disabled: !globalSelections?.line && !hasSublines - } as Field; - } - - return field; - }); - }, [fields, productLines, sublines, globalSelections?.company, globalSelections?.line]); - - // Initialize data with initialData - const [data, setData] = useState[]>(initialData); - - // Apply global selections when they change - useEffect(() => { - if (!globalSelections || JSON.stringify(globalSelections) === JSON.stringify(prevGlobalSelectionsRef.current)) { - return; - } - - // Find the field definitions for changed global selection fields - const supplierField = Array.from(fields as ReadonlyFields).find(f => f.key === 'supplier'); - const companyField = Array.from(fields as ReadonlyFields).find(f => f.key === 'company'); - const lineField = Array.from(fields as ReadonlyFields).find(f => f.key === 'line'); - const sublineField = Array.from(fields as ReadonlyFields).find(f => f.key === 'subline'); - - // Identify which selections have changed - const changedSelections: Partial = {}; - if (globalSelections.supplier !== prevGlobalSelectionsRef.current?.supplier) { - changedSelections.supplier = globalSelections.supplier; - } - if (globalSelections.company !== prevGlobalSelectionsRef.current?.company) { - changedSelections.company = globalSelections.company; - } - if (globalSelections.line !== prevGlobalSelectionsRef.current?.line) { - changedSelections.line = globalSelections.line; - } - if (globalSelections.subline !== prevGlobalSelectionsRef.current?.subline) { - changedSelections.subline = globalSelections.subline; - } - - // Only update data if there are actual changes - if (Object.keys(changedSelections).length > 0) { - setData(currentData => - currentData.map(row => { - const newRow = { ...row }; - - // Only apply fields that have changed - if (changedSelections.supplier) { - const supplierValue = setFieldValue(supplierField as Field, changedSelections.supplier); - if (supplierValue) newRow.supplier = supplierValue; - } - - if (changedSelections.company) { - const companyValue = setFieldValue(companyField as Field, changedSelections.company); - if (companyValue) newRow.company = companyValue; - } - - if (changedSelections.line && productLines) { - const lineValue = setFieldValue(lineField as Field, changedSelections.line, productLines); - if (lineValue) newRow.line = lineValue; - } - - if (changedSelections.subline && sublines) { - const sublineValue = setFieldValue(sublineField as Field, changedSelections.subline, sublines); - if (sublineValue) newRow.subline = sublineValue; - } - - return newRow; - }) - ); - } - - // Update the ref to current globalSelections - prevGlobalSelectionsRef.current = globalSelections; - }, [globalSelections, fields, productLines, sublines]); - - // Run validation when component mounts or when fields change - useEffect(() => { - const validateData = async () => { - // Cast the fields to the expected type for validation - const validationFields = fieldsWithUpdatedOptions as unknown as Fields; - const validatedData = await addErrorsAndRunHooks( - data, - validationFields, - rowHook, - tableHook - ); - setData(validatedData as RowData[]); - }; - validateData(); - }, [fieldsWithUpdatedOptions, rowHook, tableHook]); - - const [rowSelection, setRowSelection] = useState({}) - const [filterByErrors, setFilterByErrors] = useState(false) - const [showSubmitAlert, setShowSubmitAlert] = useState(false) - const [isSubmitting, setSubmitting] = useState(false) - const [copyDownField, setCopyDownField] = useState<{key: T, label: string} | null>(null) - const [isAiValidating, setIsAiValidating] = useState(false) - const [aiValidationDetails, setAiValidationDetails] = useState<{ - changes: string[]; - warnings: string[]; - changeDetails: ProductChangeDetail[]; - isOpen: boolean; - originalData?: (Data & ExtendedMeta)[]; // Store original data for reverting changes - }>({ - changes: [], - warnings: [], - changeDetails: [], - isOpen: false, - }); - - const [aiValidationProgress, setAiValidationProgress] = useState<{ - isOpen: boolean; - status: string; - step: number; - estimatedSeconds?: number; - startTime?: Date; - promptLength?: number; - elapsedSeconds?: number; - progressPercent?: number; - }>({ - isOpen: false, - status: "", - step: 0, - }); - - const [currentPrompt, setCurrentPrompt] = useState<{ - isOpen: boolean; - prompt: string | null; - isLoading: boolean; - }>({ - isOpen: false, - prompt: null, - isLoading: false, - }); - // Get template state and functions from the useTemplates hook - const { - templates, - selectedTemplateId, - showSaveTemplateDialog, - setSelectedTemplateId, - setShowSaveTemplateDialog, - applyTemplate, - saveAsTemplate, - setNewTemplateName, - setNewTemplateType, - getTemplateDisplayText, - } = useTemplates(data, setData, useToast, rowSelection) - - // Memoize filtered data to prevent recalculation on every render + // Add state to track if UPC validation is in progress + const [isUpcValidating, setIsUpcValidating] = useState(false); + + // Initialize data with initialData directly - rows should already have global selections applied from MatchColumnsStep + const [data, setData] = useState[]>(initialData); + + // Define filteredData early so it can be used in updateRows + const [filterByErrors, setFilterByErrors] = useState(false); const filteredData = useMemo(() => { - if (!filterByErrors) return data - return data.filter(row => - row.__errors && Object.values(row.__errors).some(err => err.level === "error") - ) - }, [data, filterByErrors]) - + if (filterByErrors) { + return data.filter((value) => { + if (value?.__errors) { + return !!Object.values(value.__errors)?.filter((err) => err.level === "error").length; + } + return false; + }); + } + return data; + }, [data, filterByErrors]); + + // Reference to store hook timeouts to prevent race conditions + const hookTimeoutRef = useRef(null); + + // Define updateData function for validation hooks const updateData = useCallback( async (rows: (Data & ExtendedMeta)[], indexes?: number[]) => { // Create new array references to ensure React state updates properly @@ -2086,109 +2002,8 @@ export const ValidationStep = ({ }, [rowHook, tableHook, fieldsWithUpdatedOptions], ); - - // Reference to store hook timeouts to prevent race conditions - const hookTimeoutRef = useRef(null); - - // Handle UPC validation and item number generation - const handleUpcValidation = useCallback(async (upcValue: string, fieldKey: string) => { - try { - if (!upcValue || !upcValue.trim()) return null; - - // Get the cell element to find the row - const cell = document.getElementById(`cell-${fieldKey}`); - if (!cell) { - console.error('Could not find cell element'); - return { error: true, message: 'Could not determine row context' }; - } - - // Find the row in the DOM - const rowElement = cell.closest('tr'); - if (!rowElement) { - console.error('Could not find row element'); - return { error: true, message: 'Could not determine row context' }; - } - - // Get the row index - const rowIndex = rowElement ? Array.from(rowElement.parentElement?.children || []).indexOf(rowElement) : -1; - if (rowIndex === -1) { - console.error('Could not determine row index'); - return { error: true, message: 'Could not determine row context' }; - } - - // Get the actual row data from filteredData - const row = filteredData[rowIndex]; - if (!row) { - console.error('Could not find row data'); - return { error: true, message: 'Could not find row data' }; - } - - // Type assertion to access dynamic properties - const rowData = row as Record; - - // We need to have a supplier ID to generate an item number - if (!rowData.supplier) { - console.log('No supplier selected, skipping item number generation'); - return { error: true, message: 'Please select a supplier before validating UPC' }; - } - - // Get the supplier ID from the row - const supplierId = rowData.supplier; - console.log(`Using supplier ID ${supplierId} for UPC validation`); - - // Call the API to check the UPC and generate an item number - console.log(`Calling API with UPC=${upcValue} and supplierId=${supplierId}`); - const response = await fetch(`${config.apiUrl}/import/check-upc-and-generate-sku?upc=${encodeURIComponent(upcValue)}&supplierId=${encodeURIComponent(supplierId)}`); - - // Try to parse the response data even if it's an error - let responseData; - try { - responseData = await response.json(); - console.log('API response:', responseData); - } catch (parseError) { - console.error('Error parsing response:', parseError); - return { error: true, message: 'Failed to validate UPC: Invalid response format' }; - } - - if (!response.ok) { - // Handle 409 error (UPC already exists) - if (response.status === 409) { - console.log(`UPC ${upcValue} already exists - ${responseData.message}`); - const error = { - level: 'error', - message: `UPC already exists for product ID ${responseData.existingProductId} with item number ${responseData.existingItemNumber}` - }; - - return { error: true, message: error.message }; - } - - return { error: true, message: responseData.message || 'Failed to validate UPC' }; - } - - // Success - got an item number - if (responseData && responseData.success && responseData.itemNumber) { - // Figure out which field to use for the item number (itemnumber or sku) - const itemNumberField = fieldsWithUpdatedOptions.find(f => f.key === 'itemnumber'); - const itemNumberKey = itemNumberField ? 'itemnumber' : 'sku'; - - console.log(`Generated item number: ${responseData.itemNumber}, will update field ${itemNumberKey}`); - - // Return a simple object with the field to update and the new value - return { - fieldKey: itemNumberKey, - value: responseData.itemNumber, - rowIndex, - success: true - }; - } - - return { error: true, message: 'No item number returned from API' }; - } catch (error) { - console.error('Error during UPC validation:', error); - return { error: true, message: String(error) }; - } - }, [fieldsWithUpdatedOptions, filteredData]); + // Define updateRows early so it can be used in the event listener const updateRows = useCallback( (rowIndex: number, fieldKey: string, value: any) => { // Get the current row based on the filteredData @@ -2270,45 +2085,6 @@ export const ValidationStep = ({ return; } - // Similarly for item number fields, preserve the UPC value - if (isItemNumberField) { - // Check for UPC fields to preserve - const typedItem = data[originalIndex] as Record; - - // Create a new data array with the item number updated but preserving UPC - const newData = data.map((item, idx) => { - if (idx === originalIndex) { - // Create a new item with the updated field - const updatedItem = { ...item, [fieldKey]: value }; - - // Preserve the UPC field - if (typedItem.upc !== undefined) { - console.log(`Preserving upc = "${typedItem.upc}" during item number update`); - (updatedItem as Record).upc = typedItem.upc; - } - if (typedItem.barcode !== undefined) { - console.log(`Preserving barcode = "${typedItem.barcode}" during item number update`); - (updatedItem as Record).barcode = typedItem.barcode; - } - - return updatedItem; - } - return item; - }); - - // Update the state - setData(newData); - - // Run validation hooks - hookTimeoutRef.current = setTimeout(() => { - console.log(`Running validation hooks after item number update with preserved UPC`); - updateData(newData, [originalIndex]) - .catch(err => console.error(`Error in validation:`, err)); - }, 100); - - return; - } - // For other fields, use the original logic const newData = data.map((item, idx) => { if (idx === originalIndex) { @@ -2329,74 +2105,629 @@ export const ValidationStep = ({ hookTimeoutRef.current = setTimeout(() => { console.log(`Running validation hooks after updating ${fieldKey}`); updateData(newData, [originalIndex]) - .then(() => { - console.log(`Validation completed for ${fieldKey} update`); - - // For item number fields, add another check to verify it's still set after validation - if (isItemNumberField) { - setTimeout(() => { - const currentRow = data.find(item => item.__index === newData[originalIndex].__index); - const typedCurrentRow = currentRow as Record; - - if (currentRow) { - console.log(`Verification check: item number field ${fieldKey} = "${typedCurrentRow[fieldKey]}"`); - - // If the item number was lost, update it again - if (!typedCurrentRow[fieldKey] && value) { - console.log(`Item number was lost, re-updating ${fieldKey} = "${value}"`); - updateRows(rowIndex, fieldKey, value); - } - } - }, 100); - } - }) - .catch(err => console.error(`Error in validation after ${fieldKey} update:`, err)); + .catch(err => console.error(`Error in validation:`, err)); }, 50); }, - [filteredData, data, updateData], + [filteredData, data, updateData] ); - - // Add function to handle SKU generation results from UPC validation - const handleSkuGeneration = useCallback( - (_upcField: string, result: { fieldKey: string, value: string, rowIndex: number }) => { - // Find the corresponding row in the filtered data - const row = filteredData[result.rowIndex]; - if (!row) return; + + // Add event listener for UPC value updates from EditableCell + useEffect(() => { + const handleUpcValueUpdate = (event: CustomEvent) => { + const { rowIndex, fieldKey, value } = event.detail; + console.log(`Received UPC value update event: rowIndex=${rowIndex}, fieldKey=${fieldKey}, value=${value}`); - // Find the original index in the full data array - const originalIndex = data.findIndex(r => r.__index === row.__index); - if (originalIndex === -1) return; + // Call updateRows directly to ensure the UPC value is saved + if (typeof updateRows === 'function') { + updateRows(rowIndex, fieldKey, value); + } + }; + + // Add event listener + document.addEventListener('upc-value-update', handleUpcValueUpdate as EventListener); + + // Remove event listener on cleanup + return () => { + document.removeEventListener('upc-value-update', handleUpcValueUpdate as EventListener); + }; + }, [updateRows]); // Add updateRows to the dependency array + + // Process initial data to ensure supplier and company fields are correctly formatted + useEffect(() => { + console.log('[Initial Data] Processing initial data'); + + // Skip if data is empty + if (!initialData.length) { + console.log('[Initial Data] No data to process'); + return; + } + + // Check for global selections in data + const firstRow = initialData[0] as Record; + if (firstRow) { + console.log('[Initial Data] First row supplier:', firstRow.supplier); + console.log('[Initial Data] First row company:', firstRow.company); + } + + let dataChanged = false; + + // Process initial data to ensure supplier and company values are correctly formatted + const processedInitialData = initialData.map(row => { + const processedRow = { ...row }; + const rowData = row as Record; - // If the row exists, update the SKU field with the generated value - console.log(`Updating ${result.fieldKey} in row ${originalIndex} with value ${result.value}`); - - // Call updateRows to update the item number field - updateRows(result.rowIndex, result.fieldKey, result.value); - - // Add an additional safeguard for UPC preservation - // Get the UPC field key - const typedRow = row as Record; - let upcFieldKey = null; - let upcValue = null; - - if (typedRow.upc) { - upcFieldKey = 'upc'; - upcValue = typedRow.upc; - } else if (typedRow.barcode) { - upcFieldKey = 'barcode'; - upcValue = typedRow.barcode; + // Ensure supplier and company values are correctly formatted + if (rowData.supplier !== undefined && rowData.supplier !== null && rowData.supplier !== '') { + const originalSupplier = processedRow.supplier; + processedRow.supplier = String(rowData.supplier); + + // Only log if value changed + if (originalSupplier !== processedRow.supplier) { + dataChanged = true; + console.log(`[Initial Data] Processed supplier value: ${processedRow.supplier}`); + } + + // Fix validation errors for supplier immediately + if (processedRow.__errors?.supplier) { + if (!processedRow.__errors) processedRow.__errors = {}; + delete processedRow.__errors.supplier; + if (Object.keys(processedRow.__errors).length === 0) { + processedRow.__errors = null; + } + dataChanged = true; + } } - // If we found a UPC field, ensure it's preserved - if (upcFieldKey && upcValue) { - console.log(`Also ensuring UPC field "${upcFieldKey}" still has value "${upcValue}"`); - setTimeout(() => { - updateRows(result.rowIndex, upcFieldKey, upcValue); - }, 100); + if (rowData.company !== undefined && rowData.company !== null && rowData.company !== '') { + const originalCompany = processedRow.company; + processedRow.company = String(rowData.company); + + // Only log if value changed + if (originalCompany !== processedRow.company) { + dataChanged = true; + console.log(`[Initial Data] Processed company value: ${processedRow.company}`); + } + + // Fix validation errors for company immediately + if (processedRow.__errors?.company) { + if (!processedRow.__errors) processedRow.__errors = {}; + delete processedRow.__errors.company; + if (Object.keys(processedRow.__errors).length === 0) { + processedRow.__errors = null; + } + dataChanged = true; + } } + + return processedRow; + }); + + // Update the data if needed + if (dataChanged) { + console.log('[Initial Data] Updating data with processed values'); + setData(processedInitialData); + } + }, [initialData]); // Only run on initial mount with initialData + + // Add effect to validate UPCs and generate item numbers on initial load + // Use a ref to track whether initial validation has been performed + const initialValidationPerformedRef = useRef(false); + const processedUpcMapRef = useRef(new Map()); + const dataValidationRef = useRef(null); + // Track which rows are currently being validated to maintain loading state + const rowsBeingValidatedRef = useRef>(new Set()); + + // Fetch product lines when company is selected + const { data: productLines } = useQuery({ + queryKey: ["product-lines", data.length > 0 ? data[0]?.company : null], + queryFn: async () => { + if (!data.length || !data[0]?.company) return []; + console.log('Fetching product lines for company:', data[0].company); + const response = await fetch(`${config.apiUrl}/import/product-lines/${data[0].company}`); + if (!response.ok) { + console.error('Failed to fetch product lines:', response.status, response.statusText); + throw new Error("Failed to fetch product lines"); + } + const productLinesData = await response.json(); + console.log('Received product lines:', productLinesData); + return productLinesData; }, - [data, filteredData, updateRows] - ); + enabled: !!(data.length > 0 && data[0]?.company), + staleTime: 30000, // Cache for 30 seconds + }); + + // Fetch sublines when line is selected + const { data: sublines } = useQuery({ + queryKey: ["sublines", data.length > 0 ? data[0]?.line : null], + queryFn: async () => { + if (!data.length || !data[0]?.line) return []; + console.log('Fetching sublines for line:', data[0].line); + const response = await fetch(`${config.apiUrl}/import/sublines/${data[0].line}`); + if (!response.ok) { + console.error('Failed to fetch sublines:', response.status, response.statusText); + throw new Error("Failed to fetch sublines"); + } + const sublinesData = await response.json(); + console.log('Received sublines:', sublinesData); + return sublinesData; + }, + enabled: !!(data.length > 0 && data[0]?.line), + staleTime: 30000, // Cache for 30 seconds + }); + + // Helper function to safely set a field value and update options if needed + // This function is used when setting field values + + // Update field options with fetched data + const fieldsWithUpdatedOptions = useMemo(() => { + return Array.from(fields as ReadonlyFields).map(field => { + if (field.key === 'line') { + // Check if we have product lines available + const hasProductLines = productLines && productLines.length > 0; + + // For line field, ensure we have the proper options + return { + ...field, + fieldType: { + ...field.fieldType, + // Use fetched product lines if available, otherwise keep existing options + options: hasProductLines + ? productLines + : (field.fieldType.type === 'select' || field.fieldType.type === 'multi-select') + ? field.fieldType.options + : [] + }, + // The line field should only be disabled if no company is selected AND no product lines available + disabled: !hasProductLines + } as Field; + } + + if (field.key === 'subline') { + // Check if we have sublines available + const hasSublines = sublines && sublines.length > 0; + + // For subline field, ensure we have the proper options + return { + ...field, + fieldType: { + ...field.fieldType, + // Use fetched sublines if available, otherwise keep existing options + options: hasSublines + ? sublines + : (field.fieldType.type === 'select' || field.fieldType.type === 'multi-select') + ? field.fieldType.options + : [] + }, + // The subline field should only be disabled if no line is selected AND no sublines available + disabled: !hasSublines + } as Field; + } + + return field; + }); + }, [fields, productLines, sublines]); + + // Define the validation function - not using useCallback to avoid dependency issues + const validateUpcAndGenerateItemNumbers = async (forceValidation = false) => { + let newData = [...data]; + let hasChanges = false; + + console.log('[UPC Validation] Starting validation...'); + console.log('[UPC Validation] Data length:', data.length); + console.log('[UPC Validation] Force validation:', forceValidation); + + // Set loading state to true before starting validation + setIsUpcValidating(true); + + // Clear the rows being validated set + rowsBeingValidatedRef.current.clear(); + + // Process up to 100 rows to avoid too many API calls at once + const maxRowsToProcess = Math.min(data.length, 100); + console.log(`[UPC Validation] Will process up to ${maxRowsToProcess} rows`); + + let validRowsCount = 0; + let processedRowsCount = 0; + let updatedRowsCount = 0; + + try { + // Mark all rows that will be processed for validation first, so they all show the loading state immediately + for (let index = 0; index < maxRowsToProcess; index++) { + const row = data[index] as Record; + + // Check if the row has UPC and supplier data + const hasUpc = row.upc || row.barcode; + const hasSupplier = row.supplier; + const hasItemNumber = row.item_number; + + // Skip rows that don't meet criteria + if (!hasUpc || !hasSupplier || (hasItemNumber && !forceValidation)) continue; + + // Mark this row as being validated (for UI loading state) + rowsBeingValidatedRef.current.add(index); + } + + // Force a re-render to show all loading states + setData([...data]); + + // Now process each row + for (let index = 0; index < maxRowsToProcess; index++) { + const row = data[index] as Record; + + // Check if the row has UPC and supplier data but no item number + const hasUpc = row.upc || row.barcode; + const hasSupplier = row.supplier; + const hasItemNumber = row.item_number; + + processedRowsCount++; + + // Log missing data for debugging + if (!hasUpc) { + console.log(`[UPC Validation] Row ${index}: Missing UPC/barcode value`); + } + + if (!hasSupplier) { + console.log(`[UPC Validation] Row ${index}: Missing supplier value`); + } + + if (hasItemNumber && !forceValidation) { + console.log(`[UPC Validation] Row ${index}: Already has item number: ${hasItemNumber}`); + } + + // Skip rows that don't meet criteria + if (!hasUpc || !hasSupplier || (hasItemNumber && !forceValidation)) continue; + + validRowsCount++; + + const upcValue = row.upc || row.barcode; + const supplierValue = row.supplier; + + console.log(`[UPC Validation] Processing UPC ${upcValue} with supplier ${supplierValue} in row ${index}`); + + // Skip if we've already processed this combination + const upcSupplierKey = `${upcValue}-${supplierValue}`; + if (processedUpcMapRef.current.has(upcSupplierKey)) { + const cachedItemNumber = processedUpcMapRef.current.get(upcSupplierKey); + console.log(`[UPC Validation] Using cached item number ${cachedItemNumber} for ${upcSupplierKey}`); + + if (cachedItemNumber && !row.item_number) { + newData[index] = { + ...newData[index], + item_number: cachedItemNumber + }; + hasChanges = true; + updatedRowsCount++; + } + + // Remove row from validation set since we're done with it + rowsBeingValidatedRef.current.delete(index); + continue; + } + + try { + // Call API to validate UPC + console.log(`[UPC Validation] Calling API for UPC ${upcValue} with supplier ${supplierValue}`); + + const apiUrl = `${config.apiUrl}/import/check-upc-and-generate-sku?upc=${encodeURIComponent(upcValue)}&supplierId=${encodeURIComponent(supplierValue)}`; + console.log(`[UPC Validation] API URL: ${apiUrl}`); + + const response = await fetch(apiUrl); + + if (!response.ok) { + console.error(`[UPC Validation] API error: ${response.status} ${response.statusText}`); + // Remove row from validation set since we're done with it + rowsBeingValidatedRef.current.delete(index); + continue; // Skip this row if API call fails + } + + const responseData = await response.json(); + console.log(`[UPC Validation] API response:`, responseData); + + if (responseData.success && responseData.itemNumber) { + // Cache the result for future use + processedUpcMapRef.current.set(upcSupplierKey, responseData.itemNumber); + + // Update the row with the item number + newData[index] = { + ...newData[index], + item_number: responseData.itemNumber + }; + + hasChanges = true; + updatedRowsCount++; + console.log(`[UPC Validation] Updated row ${index} with item number: ${responseData.itemNumber}`); + } else { + console.log(`[UPC Validation] No item number returned for UPC ${upcValue}`); + } + + // Remove row from validation set since we're done with it + rowsBeingValidatedRef.current.delete(index); + + // Add a small delay between API calls to avoid overloading the server + await new Promise(resolve => setTimeout(resolve, 100)); + } catch (error) { + console.error(`[UPC Validation] Error processing row ${index}:`, error); + // Remove row from validation set since we're done with it + rowsBeingValidatedRef.current.delete(index); + } + } + + console.log(`[UPC Validation] Processed ${processedRowsCount} rows, found ${validRowsCount} valid rows`); + console.log(`[UPC Validation] Updated ${updatedRowsCount} rows with item numbers`); + + // Apply changes to state if any were made + if (hasChanges) { + console.log(`[UPC Validation] Updating state with new item numbers`); + + // Fix any validation errors for rows that have valid supplier/company + const updatedDataWithFixedValidation = newData.map(row => { + const result = { ...row }; + + // If there are validation errors, check supplier and company + if (result.__errors) { + const newErrors = { ...result.__errors }; + + // If supplier has a value but shows an error, remove the error + if (row.supplier && newErrors.supplier) { + console.log(`[UPC Validation] Fixing supplier validation error`); + console.log(`[UPC Validation] Supplier value: ${row.supplier}`); + delete newErrors.supplier; + } + + // If company has a value but shows an error, remove the error + if (row.company && newErrors.company) { + console.log(`[UPC Validation] Fixing company validation error`); + console.log(`[UPC Validation] Company value: ${row.company}`); + delete newErrors.company; + } + + // Only set __errors if there are any errors left + if (Object.keys(newErrors).length > 0) { + result.__errors = newErrors; + } else { + result.__errors = null; + } + } + + return result; + }); + + // Use a functional update to ensure we get the latest state + setData(prevData => { + // Create a fresh copy + const finalData = [...updatedDataWithFixedValidation]; + + // For any rows we didn't process in this batch, use the current data + for (let i = maxRowsToProcess; i < prevData.length; i++) { + finalData[i] = prevData[i]; + } + + return finalData; + }); + + toast({ + title: "Item Numbers Generated", + description: `Generated ${updatedRowsCount} item numbers from UPCs`, + }); + } else { + console.log(`[UPC Validation] No changes made to data`); + } + } catch (error) { + console.error('[UPC Validation] Unexpected error during validation:', error); + } finally { + // Only reset the global loading state if there are no more rows being validated + if (rowsBeingValidatedRef.current.size === 0) { + setIsUpcValidating(false); + } + } + }; + + // Run UPC validation ONCE on initial mount - use empty dependency array to ensure it runs only once + useEffect(() => { + let isMounted = true; // Flag to track if component is mounted + + console.log('[UPC Validation] Component mounted'); + + // Set validation flag to prevent repeated validations + if (initialValidationPerformedRef.current) { + console.log('[UPC Validation] Initial validation already performed, skipping'); + return; + } + + // Wait a brief moment to ensure data is loaded + const timer = setTimeout(() => { + if (!isMounted) return; // Don't proceed if component unmounted + + if (data.length > 0) { + console.log('[UPC Validation] Starting initial validation'); + initialValidationPerformedRef.current = true; + validateUpcAndGenerateItemNumbers(); + } else { + console.log('[UPC Validation] No data available for initial validation'); + } + }, 500); + + // Cleanup function to prevent validation if component unmounts + return () => { + isMounted = false; + clearTimeout(timer); + }; + }, []); // Empty dependency array means this effect runs once on mount + + // This effect watches for data changes AFTER the initial validation + useEffect(() => { + // Skip if this is the first render (we handle that in the mount effect) + if (!initialValidationPerformedRef.current) return; + + console.log('[UPC Validation] Data changed, running validation'); + validateUpcAndGenerateItemNumbers(); + }, [data.length]); // Only re-run if data.length changes + + // Run validation when component mounts or when fields change + useEffect(() => { + // Skip if the data hasn't changed (to prevent unnecessary validations) + if (dataValidationRef.current === data) { + return; + } + + // Update our reference + dataValidationRef.current = data; + + const validateData = async () => { + // Skip validation if fields aren't ready + if (!fieldsWithUpdatedOptions.length) return; + + // Skip field validation if UPC validation is in progress + // (i.e., initialValidationPerformedRef is false) + if (!initialValidationPerformedRef.current) { + console.log('Delaying field validation until UPC validation completes'); + return; + } + + console.log('[Field Validation] Starting field validation'); + + // Pre-process data to fix imported supplier and company values that might be misformatted + const processedData = data.map(row => { + const processedRow = { ...row }; + const rowData = row as Record; + + // Log the type of supplier and company values + if (rowData.supplier !== undefined) { + console.log(`[Field Validation] Supplier value type: ${typeof rowData.supplier}, value: ${rowData.supplier}`); + } + if (rowData.company !== undefined) { + console.log(`[Field Validation] Company value type: ${typeof rowData.company}, value: ${rowData.company}`); + } + + // Ensure supplier and company values are correctly formatted (convert to string if needed) + if (rowData.supplier !== undefined && rowData.supplier !== null && rowData.supplier !== '') { + processedRow.supplier = String(rowData.supplier); + } + + if (rowData.company !== undefined && rowData.company !== null && rowData.company !== '') { + processedRow.company = String(rowData.company); + } + + return processedRow; + }); + + // Cast the fields to the expected type for validation + const validationFields = fieldsWithUpdatedOptions as unknown as Fields; + + // Apply validations but don't lose any item_number values that were set + const validatedData = await addErrorsAndRunHooks( + processedData, + validationFields, + rowHook, + tableHook + ) as RowData[]; + + // Preserve item_number values from before validation and fix supplier/company validation + const finalData = validatedData.map((row, index) => { + const originalRow = data[index] as Record; + const validatedRow = row as Record; + const result = { ...validatedRow }; + + // If the original row had an item_number but it was lost in validation, restore it + if (originalRow.item_number && !validatedRow.item_number) { + result.item_number = originalRow.item_number; + } + + // Fix validation errors on supplier and company fields if they have valid values + if (validatedRow.__errors) { + const newErrors = { ...validatedRow.__errors }; + + // Check for supplier value and remove error if it exists + // We check both the validated row and original row for values + if ((validatedRow.supplier || originalRow.supplier) && newErrors.supplier) { + console.log(`[Field Validation] Fixing supplier validation for row ${index}`); + console.log(`[Field Validation] Supplier value: ${validatedRow.supplier || originalRow.supplier}`); + delete newErrors.supplier; + } + + // Check for company value and remove error if it exists + // We check both the validated row and original row for values + if ((validatedRow.company || originalRow.company) && newErrors.company) { + console.log(`[Field Validation] Fixing company validation for row ${index}`); + console.log(`[Field Validation] Company value: ${validatedRow.company || originalRow.company}`); + delete newErrors.company; + } + + // Only set __errors if there are any errors left + if (Object.keys(newErrors).length > 0) { + result.__errors = newErrors; + } else { + result.__errors = null; + } + } + + return result; + }) as RowData[]; + + // Set the validated data + setData(finalData); + console.log('[Field Validation] Field validation complete'); + }; + + validateData(); + }, [fieldsWithUpdatedOptions, rowHook, tableHook]); + + const [rowSelection, setRowSelection] = useState({}) + const [showSubmitAlert, setShowSubmitAlert] = useState(false) + const [isSubmitting, setSubmitting] = useState(false) + const [copyDownField, setCopyDownField] = useState<{key: T, label: string} | null>(null) + const [isAiValidating, setIsAiValidating] = useState(false) + const [aiValidationDetails, setAiValidationDetails] = useState<{ + changes: string[]; + warnings: string[]; + changeDetails: ProductChangeDetail[]; + isOpen: boolean; + originalData?: (Data & ExtendedMeta)[]; // Store original data for reverting changes + }>({ + changes: [], + warnings: [], + changeDetails: [], + isOpen: false, + }); + + const [aiValidationProgress, setAiValidationProgress] = useState<{ + isOpen: boolean; + status: string; + step: number; + estimatedSeconds?: number; + startTime?: Date; + promptLength?: number; + elapsedSeconds?: number; + progressPercent?: number; + }>({ + isOpen: false, + status: "", + step: 0, + }); + + const [currentPrompt, setCurrentPrompt] = useState<{ + isOpen: boolean; + prompt: string | null; + isLoading: boolean; + }>({ + isOpen: false, + prompt: null, + isLoading: false, + }); + + // Get template state and functions from the useTemplates hook + const { + templates, + selectedTemplateId, + showSaveTemplateDialog, + setSelectedTemplateId, + setShowSaveTemplateDialog, + applyTemplate, + saveAsTemplate, + setNewTemplateName, + setNewTemplateType, + getTemplateDisplayText, + } = useTemplates(data, setData, useToast, rowSelection) const copyValueDown = useCallback((key: T, label: string) => { setCopyDownField({ key, label }) @@ -2481,7 +2812,7 @@ export const ValidationStep = ({ } }} getTemplateDisplayText={getTemplateDisplayText} - defaultBrand={globalSelections?.company} + defaultBrand={row.getValue("company") || ""} /> ); } catch (error) { @@ -2497,291 +2828,8 @@ export const ValidationStep = ({ }, ...(Array.from(fields as ReadonlyFields).map((field): ColumnDef & ExtendedMeta> => { // Function to handle UPC validation and item number generation - const handleUpcValidation = async (upcValue: string, fieldKey: string) => { - try { - if (!upcValue || !upcValue.trim()) return; - - // Get the cell element to find the row - const cell = document.getElementById(`cell-${fieldKey}`); - if (!cell) { - console.error('Could not find cell element'); - return; - } - - // Find the row in the DOM - const rowElement = cell.closest('tr'); - if (!rowElement) { - console.error('Could not find row element'); - return; - } - - // Get the row index - const rowIndex = rowElement ? Array.from(rowElement.parentElement?.children || []).indexOf(rowElement) : -1; - if (rowIndex === -1) { - console.error('Could not determine row index'); - return; - } - - // Get the actual row data from filteredData - const row = filteredData[rowIndex]; - if (!row) { - console.error('Could not find row data'); - return; - } - - // Type assertion to access dynamic properties - const rowData = row as Record; - - // Find the supplier field in the fields array to get its raw value - const supplierField = Array.from(fields).find(f => f.key === 'supplier'); - if (!supplierField) { - console.error('Could not find supplier field definition'); - return; - } - - // We need to have a supplier ID to generate an item number - if (!rowData.supplier) { - console.log('No supplier selected, skipping item number generation'); - return; - } - - // If there's already an item number, don't overwrite it - // Check for all three possible item number field names in order of preference - const hasItemNumber = rowData.hasOwnProperty('item_number') ? - rowData.item_number : rowData.hasOwnProperty('itemnumber') ? - rowData.itemnumber : rowData.hasOwnProperty('sku') ? rowData.sku : null; - - if (hasItemNumber && String(hasItemNumber).trim()) { - console.log('Item number already exists, skipping generation'); - return; - } - - // Get the raw supplier value from the data model - const supplierId = rowData.supplier; - - // Find the actual supplier ID if what we have is a display name - let supplierIdToUse = supplierId; - - // Check if this is a select field and if we need to translate from name to ID - if (supplierField && supplierField.fieldType.type === 'select') { - const supplierOptions = (supplierField.fieldType as SelectFieldType).options; - // First check if supplierId is already an ID (matched with an option.value) - const isAlreadyId = supplierOptions.some(opt => opt.value === supplierId); - - if (!isAlreadyId) { - // If not an ID, find the option with matching display name and use its value (ID) - const option = supplierOptions.find(opt => - opt.label.toLowerCase() === String(supplierId).toLowerCase() - ); - if (option) { - supplierIdToUse = option.value; - console.log(`Converted supplier name "${supplierId}" to ID "${supplierIdToUse}"`); - } - } - } - - // Create a unique key for this UPC validation to check if we've already processed it - const validationKey = `${upcValue}-${supplierIdToUse}`; - - // Check if we already processed this exact validation - if (cell && cell.dataset.lastValidation === validationKey) { - console.log('Skipping duplicate UPC validation'); - return; - } - - console.log(`Validating UPC ${upcValue} for supplier ID ${supplierIdToUse} in row ${rowIndex}`); - - // Call the API to check the UPC and generate an item number - now with the correct supplier ID - const response = await fetch(`${config.apiUrl}/import/check-upc-and-generate-sku?upc=${encodeURIComponent(upcValue)}&supplierId=${encodeURIComponent(supplierIdToUse)}`); - - // Try to parse the response data even if it's an error - let data; - try { - data = await response.json(); - } catch (parseError) { - console.error('Error parsing response:', parseError); - throw new Error('Failed to validate UPC: Invalid response format'); - } - - // Store this validation in the cell's dataset to prevent duplicate processing - if (cell) { - cell.dataset.lastValidation = validationKey; - } - - if (!response.ok) { - // If UPC already exists, show an error - if (response.status === 409) { - const fieldError = { - level: 'error', - message: `UPC already exists for product ID ${data.existingProductId} with item number ${data.existingItemNumber}` - }; - - toast({ - title: "UPC Validation Error", - description: fieldError.message, - variant: "destructive", - }); - - // Return a specific error object for the caller to handle - return { - error: true, - errorType: 'upc_exists', - message: fieldError.message, - field: 'upc', - level: 'error' - }; - } else { - throw new Error(data.error || 'Failed to validate UPC'); - } - } else if (data.success && data.itemNumber) { - console.log(`Generated item number: ${data.itemNumber}`); - - // Determine the field key to update (checking all three possible field names) - let itemNumberKey = 'item_number'; // default to the most likely field name - - // Check if we have item_number field - if ('item_number' in rowData) { - itemNumberKey = 'item_number'; - } - // Otherwise check if we have itemnumber field - else if ('itemnumber' in rowData) { - itemNumberKey = 'itemnumber'; - } - // Finally fall back to sku if neither is present - else if ('sku' in rowData) { - itemNumberKey = 'sku'; - } - - console.log(`Using field "${itemNumberKey}" for the generated item number`); - - // Return the generated item number with the row index - return { - fieldKey: itemNumberKey, - value: data.itemNumber, - rowIndex - }; - } - } catch (error) { - console.error('Error validating UPC:', error); - return { - error: true, - message: error instanceof Error ? error.message : 'Error validating UPC' - }; - } - }; // Create an enhanced field with additional onChange handler for UPC/barcode field - const enhancedField = { - ...field, - onChange: field.key === 'upc' || field.key === 'barcode' - ? (value: any, result?: any) => { - // Store the UPC value to ensure it's preserved through state updates - const upcValue = value; - console.log(`UPC onChange handler called with value: "${upcValue}"`); - - // If the validation resulted in an error, mark the UPC field as invalid - if (result && result.error) { - if (result.errorType === 'upc_exists') { - // Get the row index using the DOM - const upcCell = document.getElementById(`cell-${field.key}`); - if (upcCell) { - const row = upcCell.closest('tr'); - const rowIndex = row ? Array.from(row.parentElement?.children || []).indexOf(row) : -1; - - if (rowIndex >= 0) { - // Update the row with the error - const errorObject = { - level: result.level || 'error', - message: result.message - }; - - // Get the original row from filteredData - const dataRow = filteredData[rowIndex]; - if (!dataRow) return; - - // Find the original index of this row in the full data array - const originalIndex = data.findIndex(r => r.__index === dataRow.__index); - if (originalIndex === -1) return; - - // Create updated data with the error - const newData = [...data]; - newData[originalIndex] = { - ...newData[originalIndex], - __errors: { - ...(newData[originalIndex].__errors || {}), - [field.key]: errorObject - } - }; - - // Ensure the UPC value is still set in the data - newData[originalIndex][field.key] = upcValue; - - // Update the data to show the error - updateData(newData, [originalIndex]); - } - } - } - } - // If the result contains a valid item number, update that field - else if (result && !result.error && result.fieldKey && result.value !== undefined) { - // Check if result has rowIndex for direct update - if (result.rowIndex !== undefined) { - // Update the item number field directly - updateRows(result.rowIndex, result.fieldKey, result.value); - - // IMPORTANT: Re-update the UPC value to ensure it's not lost - setTimeout(() => { - console.log(`Re-updating UPC field "${field.key}" with value "${upcValue}"`); - updateRows(result.rowIndex, field.key, upcValue); - }, 100); - } else if (result.success && result.itemNumber) { - // We need to find the row and update it by searching for this UPC cell - const upcCell = document.getElementById(`cell-${field.key}`); - if (upcCell) { - const row = upcCell.closest('tr'); - const rowIndex = row ? Array.from(row.parentElement?.children || []).indexOf(row) : -1; - - if (rowIndex >= 0) { - // Determine the item number field key (checking all three possible field names) - let itemNumberKey; - if ('item_number' in data[0]) { - itemNumberKey = 'item_number'; - } else if ('itemnumber' in data[0]) { - itemNumberKey = 'itemnumber'; - } else { - itemNumberKey = 'sku'; - } - - console.log(`Using field "${itemNumberKey}" for item number`); - - // First update the UPC to make sure it's preserved - updateRows(rowIndex, field.key, upcValue); - - // Then update the item number field - console.log(`Updating ${itemNumberKey} at row ${rowIndex} to ${result.itemNumber}`); - updateRows(rowIndex, itemNumberKey, result.itemNumber); - - // Update the UPC again to make sure it wasn't lost - setTimeout(() => { - console.log(`Final UPC update for "${field.key}" with value "${upcValue}"`); - updateRows(rowIndex, field.key, upcValue); - }, 150); - } - } - } - } else { - // For normal UPC updates without validation results - // Make sure the call to updateRows correctly sets the UPC value - console.log(`Normal UPC update - setting ${field.key} = "${upcValue}"`); - } - - // Call the original onChange if it exists - if (field.onChange) { - field.onChange(value); - } - } - : field.onChange - }; return { accessorKey: field.key, @@ -2798,126 +2846,177 @@ export const ValidationStep = ({ const value = row.getValue(column.id); const error = row.original.__errors?.[column.id]; const rowIndex = row.index; + const field = fieldsWithUpdatedOptions.find(field => field.key === column.id); - // Special logging for item number fields to track their values - if (column.id === 'item_number' || column.id === 'sku' || column.id === 'itemnumber') { - console.log(`Rendering item number cell for field "${column.id}" with value: "${value}" in row ${rowIndex}`); + if (!field) { + console.error(`Field not found for column ID: ${column.id}`); + return null; } - // Create the props for our cell - const cellProps = { - value, - onChange: (newValue: any) => updateRows(rowIndex, column.id, newValue), - error, - field: { - ...enhancedField as Field, - handleUpcValidation: field.key === 'upc' || field.key === 'barcode' - ? async (upcValue: string) => { - try { - // Get the current supplier ID from the row - const supplierValue = row.getValue('supplier'); - console.log(`UPC validation called for "${upcValue}" with supplier "${supplierValue}" in row ${rowIndex}`); - - if (!supplierValue) { - console.log('No supplier selected, cannot validate UPC'); - return { - error: true, - message: "Please select a supplier before validating UPC" - }; - } - - // Store the current UPC value to make sure it's preserved - const originalUpcValue = upcValue; - - // Make the API call with both UPC and supplier - const result = await handleUpcValidation(upcValue, field.key); - - // Log the result for debugging - console.log('UPC validation API result:', result); - - // If we get a valid item number, update the appropriate field - if (result && !result.error && result.fieldKey && result.value) { - console.log(`UPC validation generated item number: ${result.value} for field ${result.fieldKey}`); - - // CRITICAL CHANGE: Create a special update function for this special case to avoid field loss - // This will update both the item number and UPC in a single operation - const updateBothItemNumberAndUpc = () => { - // Get current row data to preserve all values - const rowData = filteredData[rowIndex]; - if (!rowData) return; - - // Find the original row's index in the full data array - const originalIndex = data.findIndex(item => item.__index === rowData.__index); - if (originalIndex === -1) return; - - // Create a new data array with both fields updated - const newData = data.map((item, idx) => { - if (idx === originalIndex) { - // Create a copy with both the item number and UPC set - const updatedItem = { - ...item, - [result.fieldKey]: result.value, // Set item number - [field.key]: originalUpcValue // Set UPC - }; - - // Log the update - console.log(`Special update: Setting both ${result.fieldKey}="${result.value}" and ${field.key}="${originalUpcValue}"`); - - return updatedItem; - } - return item; - }); - - // Update the state with both fields set - setData(newData); - - // Run the hooks to validate the data - setTimeout(() => { - console.log(`Running validation hooks after dual-field update`); - updateData(newData, [originalIndex]) - .then(() => console.log(`Validation completed after dual-field update`)) - .catch(err => console.error(`Error in validation after dual-field update:`, err)); - }, 100); - }; - - // Use the special update function - updateBothItemNumberAndUpc(); - - // Return success status with the item number - return { - success: true, - itemNumber: result.value, - message: `Generated item number: ${result.value}` - }; - } - - // If there was an error, return it but don't touch the UPC value - if (result && result.error) { - console.log('UPC validation returned error:', result.message); - return result; - } - - return { success: false, message: 'No valid item number returned' }; - } catch (error) { - console.error('Error in UPC validation:', error); - return { error: true, message: String(error) }; - } - } - : undefined - }, - productLines, - sublines - }; + // Field may have been further enhanced with updated display options + const enhancedField = fieldsWithUpdatedOptions.find(f => f.key === column.id) || field; - // Use a more reliable key for item number and UPC fields - const isSkuField = field.key === 'sku' || field.key === 'itemnumber'; - const isUpcField = field.key === 'upc' || field.key === 'barcode'; - const cellKey = `${field.key}-${rowIndex}-${value}-${(isSkuField || isUpcField) ? Date.now() : ''}`; + // Add special logging for item number fields to debug issues + if (column.id === 'item_number' || column.id === 'itemnumber' || column.id === 'sku') { + // Remove excessive logging that's causing re-renders + // console.log(`Rendering item number cell for field "${column.id}" with value: "${value}" in row ${rowIndex}`); + } + // Pass the rowIndex to the EditableCell to help with loading state tracking return ( -
- -
+ updateRows(rowIndex, column.id as T, value)} + error={error} + field={{ + ...enhancedField, + rowIndex, // Add rowIndex to field object + handleUpcValidation: column.id === 'upc' || column.id === 'barcode' + ? async (upcValue: string) => { + try { + // First check if we have a supplier from the row data + const rowSupplierValue = row.getValue('supplier') as string; + + // If we don't have a supplier, can't validate + if (!rowSupplierValue) { + console.error('No supplier available for UPC validation'); + return { + error: true, + message: 'Please select a supplier before validating UPC' + }; + } + + console.log(`UPC validation called for "${upcValue}" with supplier "${rowSupplierValue}" in row ${rowIndex}`); + + // CRITICAL FIX: Immediately update the UPC value in the data to ensure it's not lost + // This needs to happen before any async operations + updateRows(rowIndex, column.id as string, upcValue); + + // Create a unique key for this UPC/supplier combination + const upcSupplierKey = `${upcValue}-${rowSupplierValue}`; + + // Check if we've already validated this UPC/supplier combination + if (processedUpcMapRef.current.has(upcSupplierKey)) { + console.log(`Using cached result for ${upcSupplierKey}`); + const cachedItemNumber = processedUpcMapRef.current.get(upcSupplierKey); + + if (cachedItemNumber) { + // Determine which item number field to use + let itemNumberKey = 'item_number'; + // Check if fields exist in row using safer approach + const rowData = row.original as Record; + + if ('item_number' in rowData) { + itemNumberKey = 'item_number'; + } else if ('itemnumber' in rowData) { + itemNumberKey = 'itemnumber'; + } else if ('sku' in rowData) { + itemNumberKey = 'sku'; + } + + // Update the item number field + updateRows(rowIndex, itemNumberKey, cachedItemNumber); + + return { + success: true, + fieldKey: itemNumberKey, + value: cachedItemNumber, + rowIndex + }; + } + } + + // Make API call to validate UPC and generate item number + console.log(`Calling API to validate UPC ${upcValue} for supplier ${rowSupplierValue}`); + const response = await fetch(`${config.apiUrl}/import/check-upc-and-generate-sku?upc=${encodeURIComponent(String(upcValue))}&supplierId=${encodeURIComponent(String(rowSupplierValue))}`); + + if (!response.ok) { + console.error(`API error: ${response.status} ${response.statusText}`); + return { + error: true, + message: `API error: ${response.status} ${response.statusText}` + }; + } + + const responseData = await response.json(); + console.log('API response:', responseData); + + if (responseData.error) { + return { + error: true, + message: responseData.message || 'UPC validation failed', + field: 'upc', + level: 'error' + }; + } else if (responseData.success && responseData.itemNumber) { + // Store in cache for future use + processedUpcMapRef.current.set(upcSupplierKey, responseData.itemNumber); + + console.log(`Generated item number: ${responseData.itemNumber}`); + + // Determine which item number field to use + let itemNumberKey = 'item_number'; + // Check if fields exist in row using safer approach + const rowData = row.original as Record; + + if ('item_number' in rowData) { + itemNumberKey = 'item_number'; + } else if ('itemnumber' in rowData) { + itemNumberKey = 'itemnumber'; + } else if ('sku' in rowData) { + itemNumberKey = 'sku'; + } + + console.log(`Using field "${itemNumberKey}" for the generated item number`); + + // Update the item number field + updateRows(rowIndex, itemNumberKey, responseData.itemNumber); + + // CRITICAL FIX: Ensure the UPC value is preserved after item number update + // This is needed because the item number update might have side effects + setTimeout(() => { + updateRows(rowIndex, column.id as string, upcValue); + }, 50); + + return { + fieldKey: itemNumberKey, + value: responseData.itemNumber, + rowIndex, + success: true + }; + } + + return { + error: true, + message: 'UPC validation failed' + }; + } catch (error) { + console.error('Error validating UPC:', error); + + // Remove row from validation set + if (rowIndex !== undefined) { + rowsBeingValidatedRef.current.delete(rowIndex); + } + + // CRITICAL FIX: Ensure the UPC value is preserved even after an error + setTimeout(() => { + updateRows(rowIndex, column.id as string, upcValue); + }, 50); + + return { + error: true, + message: error instanceof Error ? error.message : 'Error validating UPC' + }; + } + } + : undefined + } as CellProps['field']} + productLines={productLines} + sublines={sublines} + isUpcValidating={isUpcValidating} + isRowValidating={rowsBeingValidatedRef.current.has(rowIndex)} + /> ); }, size: (field as any).width || ( @@ -2999,25 +3098,7 @@ export const ValidationStep = ({ } return obj; }, {} as Data); - - // Apply global selections with proper normalization - if (globalSelections) { - const supplierField = Array.from(fields as ReadonlyFields).find(f => f.key === 'supplier'); - const companyField = Array.from(fields as ReadonlyFields).find(f => f.key === 'company'); - const lineField = Array.from(fields as ReadonlyFields).find(f => f.key === 'line'); - const sublineField = Array.from(fields as ReadonlyFields).find(f => f.key === 'subline'); - - const supplier = normalizeValue(normalizedValues.supplier, supplierField as Field); - const company = normalizeValue(normalizedValues.company, companyField as Field); - const line = normalizeValue(normalizedValues.line, lineField as Field); - const subline = normalizeValue(normalizedValues.subline, sublineField as Field); - - if (supplier) normalizedValues.supplier = supplier; - if (company) normalizedValues.company = company; - if (line) normalizedValues.line = line; - if (subline) normalizedValues.subline = subline; - } - + if (__errors) { for (const key in __errors) { if (__errors[key].level === "error") { @@ -3061,7 +3142,7 @@ export const ValidationStep = ({ onClose(); } } - }, [data, fields, file, onClose, onSubmit, normalizeValue, toast, translations, globalSelections, onNext]); + }, [data, fields, file, onClose, onSubmit, normalizeValue, toast, translations, onNext]); const onContinue = useCallback(() => { const invalidData = data.find((value) => { @@ -3710,202 +3791,7 @@ export const ValidationStep = ({ } }, [toast]); - // Add effect to validate UPCs and generate item numbers on initial load - // Use a ref to track whether initial validation has been performed - const initialValidationPerformedRef = useRef(false); - const lastDataLengthRef = useRef(0); - - useEffect(() => { - // Skip if no data or if fields aren't loaded - if (!data.length || !fieldsWithUpdatedOptions.length) return; - - // Skip if we've already done the initial validation and data length hasn't changed - // This prevents the cycle of data updates triggering more validations - if (initialValidationPerformedRef.current && data.length === lastDataLengthRef.current) { - return; - } - - // Update the ref values - lastDataLengthRef.current = data.length; - - console.log('Running initial UPC validation...'); - - // Process each row that has a UPC and supplier but no item number - const processRows = async () => { - // Create a copy of data to track changes - let newData = [...data]; - let hasChanges = false; - - // Create a mapping of already seen UPCs to avoid duplicates - const processedUpcMap = new Map(); - - // Track which rows were updated for debugging - const updatedRows: number[] = []; - - // Generate promises for all rows that need processing - const rowPromises = data.map(async (row, index) => { - // Cast row to any to work around TypeScript errors - const typedRow = row as any; - - // Check if this row has a UPC and supplier but no item number - const hasUpc = typedRow.upc || typedRow.barcode; - const hasSupplier = typedRow.supplier; - const hasItemNumber = typedRow.itemnumber || typedRow.sku; - - if (hasUpc && hasSupplier && !hasItemNumber) { - try { - // Use the UPC field (either upc or barcode) - const upcValue = typedRow.upc || typedRow.barcode; - const supplierValue = typedRow.supplier; - - // Skip if either value is missing - if (!upcValue || !supplierValue) return null; - - // Find the supplier field to get its options - const supplierField = Array.from(fields).find(f => f.key === 'supplier'); - - // Find the actual supplier ID if what we have is a display name - let supplierIdToUse = supplierValue; - - // Check if this is a select field and if we need to translate from name to ID - if (supplierField && supplierField.fieldType.type === 'select') { - const supplierOptions = (supplierField.fieldType as SelectFieldType).options; - // First check if supplier is already an ID (matched with an option.value) - const isAlreadyId = supplierOptions.some(opt => opt.value === supplierValue); - - if (!isAlreadyId) { - // If not an ID, find the option with matching display name and use its value (ID) - const option = supplierOptions.find(opt => - opt.label.toLowerCase() === String(supplierValue).toLowerCase() - ); - if (option) { - supplierIdToUse = option.value; - console.log(`Initial validation: Converted supplier name "${supplierValue}" to ID "${supplierIdToUse}"`); - } - } - } - - // Check if we already processed this UPC - const upcSupplierKey = `${upcValue}-${supplierIdToUse}`; - if (processedUpcMap.has(upcSupplierKey)) { - console.log(`Skipping duplicate UPC validation for ${upcSupplierKey}`); - return null; - } - - // Mark this UPC as processed - processedUpcMap.set(upcSupplierKey, true); - - console.log(`Validating UPC ${upcValue} for supplier ID ${supplierIdToUse} in row ${index}`); - - // Call the API to check UPC and generate item number - const response = await fetch( - `${config.apiUrl}/import/check-upc-and-generate-sku?upc=${encodeURIComponent(upcValue)}&supplierId=${encodeURIComponent(supplierIdToUse)}` - ); - - const responseData = await response.json(); - - if (!response.ok) { - // Handle 409 error (UPC already exists) - if (response.status === 409) { - console.log(`UPC ${upcValue} already exists - marking as error`); - - // Add error to the UPC field - const upcFieldKey = typedRow.upc ? 'upc' : 'barcode'; - - // Create error object - const error = { - level: 'error', - message: `UPC already exists for product ID ${responseData.existingProductId} with item number ${responseData.existingItemNumber}` - }; - - // Update row with error - newData[index] = { - ...newData[index], - __errors: { - ...(newData[index].__errors || {}), - [upcFieldKey]: error - } - }; - - hasChanges = true; - return null; - } - throw new Error('Failed to validate UPC'); - } - - // Get the generated item number - if (responseData.success && responseData.itemNumber) { - // Determine item number field key - const itemNumberKey = Object.keys(fieldsWithUpdatedOptions.find(f => f.key === 'itemnumber') ? { itemnumber: true } : { sku: true })[0]; - - console.log(`Generated item number ${responseData.itemNumber} for row ${index}, setting in field ${itemNumberKey}`); - - // Update the row with the generated item number - newData[index] = { - ...newData[index], - [itemNumberKey]: responseData.itemNumber - }; - - console.log(`Setting ${itemNumberKey} = "${responseData.itemNumber}" for row ${index}`); - updatedRows.push(index); - - hasChanges = true; - return { rowIndex: index, fieldKey: itemNumberKey, value: responseData.itemNumber }; - } - } catch (error) { - console.error(`Error processing row ${index}:`, error); - } - } - return null; - }); - - // Wait for all promises to resolve - const results = await Promise.all(rowPromises); - - // Update data if there were any changes - if (hasChanges) { - console.log(`Updating data with generated item numbers for rows: ${updatedRows.join(', ')}`); - - // Log the item numbers before update for debugging - updatedRows.forEach(rowIndex => { - const itemNumberKey = Object.keys(fieldsWithUpdatedOptions.find(f => f.key === 'itemnumber') ? { itemnumber: true } : { sku: true })[0]; - console.log(`Before update: Row ${rowIndex} ${itemNumberKey} = "${(newData[rowIndex] as Record)[itemNumberKey]}"`); - }); - - // Use a try-catch to handle any potential errors during update - try { - // Manually update the data state to ensure the changes are reflected - setData([...newData]); // This makes a shallow copy to trigger state update - - console.log('Successfully updated data with item numbers and UPC errors'); - - // Mark that we've performed initial validation - initialValidationPerformedRef.current = true; - - // Then run the async validation separately - setTimeout(() => { - updateData([...newData]) - .then(() => console.log('Async validation completed after item number update')) - .catch(err => console.error('Error in async validation:', err)); - }, 100); - - // Return the validation results for display updates - return results.filter(Boolean); - } catch (updateError) { - console.error('Error updating data:', updateError); - } - } else { - console.log('No changes needed from initial UPC validation'); - // Mark that we've performed initial validation even if no changes were made - initialValidationPerformedRef.current = true; - } - return []; - }; - - // Run the processing asynchronously without blocking page load - setTimeout(() => processRows(), 500); - - }, [data, fieldsWithUpdatedOptions, updateData, fields]); + return (
@@ -4253,6 +4139,7 @@ export const ValidationStep = ({

{translations.validationStep.title}

+
{isFromScratch && ( + + + + + ) +} + +export default SaveTemplateDialog \ No newline at end of file diff --git a/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/SearchableTemplateSelect.tsx b/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/SearchableTemplateSelect.tsx new file mode 100644 index 0000000..e40a80e --- /dev/null +++ b/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/SearchableTemplateSelect.tsx @@ -0,0 +1,331 @@ +import React, { useState, useEffect, useCallback, useMemo } from 'react' +import { Template } from '../hooks/useValidationState' +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[]; + 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); + const [error, setError] = useState(null); + + // Set default brand when component mounts or defaultBrand changes + useEffect(() => { + if (defaultBrand) { + setSelectedBrand(defaultBrand); + } + }, [defaultBrand]); + + // 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 (!templates || templates.length === 0) { + console.log('No templates available for brand extraction'); + return []; + } + + console.log('Extracting brands from templates:', templates); + const brandSet = new Set(); + const brandNames: {id: string, name: string}[] = []; + + templates.forEach(template => { + if (!template || !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 { + // Extract company name from the template display text + const displayText = getTemplateDisplayText(template.id.toString()); + const companyName = displayText.split(' - ')[0]; + brandNames.push({ id: companyId, name: companyName || companyId }); + } catch (err) { + console.error("Error extracting company name:", err); + brandNames.push({ id: companyId, name: companyId }); + } + } + }); + + console.log('Extracted brands:', brandNames); + return brandNames.sort((a, b) => a.name.localeCompare(b.name)); + } catch (err) { + console.error("Error extracting brands:", err); + return []; + } + }, [templates, getTemplateDisplayText]); + + // Group templates by company for better organization + const groupedTemplates = useMemo(() => { + try { + if (!templates || templates.length === 0) return {}; + + const groups: Record = {}; + + templates.forEach(template => { + if (!template) return; + + const companyId = template.company; + if (!groups[companyId]) { + groups[companyId] = []; + } + groups[companyId].push(template); + }); + + return groups; + } catch (err) { + console.error("Error grouping templates:", err); + return {}; + } + }, [templates]); + + // Filter templates based on selected brand and search term + const filteredTemplates = useMemo(() => { + try { + if (!templates || templates.length === 0) return []; + + // First filter by brand if selected + let brandFiltered = templates; + if (selectedBrand) { + brandFiltered = templates.filter(t => t && t.company === selectedBrand); + } + + // Then filter by search term if provided + if (!searchTerm.trim()) return brandFiltered; + + const lowerSearchTerm = searchTerm.toLowerCase(); + return brandFiltered.filter(template => { + if (!template) return false; + try { + const displayText = getTemplateDisplayText(template.id.toString()); + const productType = template.product_type?.toLowerCase() || ''; + + // Search in both the display text and product type + return displayText.toLowerCase().includes(lowerSearchTerm) || + productType.includes(lowerSearchTerm); + } catch (error) { + console.error("Error filtering template:", error, template); + return false; + } + }); + } catch (err) { + console.error("Error in filteredTemplates:", err); + setError("Error filtering templates"); + return []; + } + }, [templates, selectedBrand, searchTerm, getTemplateDisplayText]); + + // Handle errors gracefully + const getDisplayText = useCallback(() => { + try { + if (!value) return placeholder; + return getTemplateDisplayText(value); + } catch (err) { + console.error("Error getting template display text:", err); + setError("Error displaying template"); + return placeholder; + } + }, [getTemplateDisplayText, placeholder, value]); + + // Handle errors in the component + if (error) { + return ( + + ); + } + + // Safe render function for CommandItem + const renderCommandItem = useCallback((template: Template) => { + try { + // Get the display text for the template + const displayText = getTemplateDisplayText(template.id.toString()); + + return ( + { + try { + onValueChange(currentValue); + setOpen(false); + setSearchTerm(""); + // Don't reset the brand filter when selecting a template + // This allows users to keep filtering by brand + } catch (err) { + console.error("Error in onSelect:", err); + setError("Error selecting template"); + } + }} + className="flex items-center justify-between" + > + {displayText} + {value === template.id.toString() && } + + ); + } catch (err) { + console.error("Error rendering CommandItem:", err); + return null; + } + }, [onValueChange, value, getTemplateDisplayText]); + + return ( + + + + + + +
+ {/* Brand filter dropdown */} + {brands.length > 0 && ( +
+ +
+ )} + + + + {/* Search input */} +
+ { + try { + setSearchTerm(value); + } catch (err) { + console.error("Error in onValueChange:", err); + setError("Error searching templates"); + } + }} + className="h-8 flex-1" + /> +
+
+ + {/* Results */} + +
+

No templates found.

+
+
+ + + + {!searchTerm ? ( + // When no search term is applied, show templates grouped by company + // If a brand is selected, only show that brand's templates + selectedBrand ? ( + b.id === selectedBrand)?.name || selectedBrand}> + {groupedTemplates[selectedBrand]?.map(template => renderCommandItem(template))} + + ) : ( + // Show all brands and their templates + Object.entries(groupedTemplates).map(([companyId, companyTemplates]) => { + // Get company name from the brands array + const brand = brands.find(b => b.id === companyId); + const companyName = brand ? brand.name : companyId; + + return ( + + {companyTemplates.map(template => renderCommandItem(template))} + + ); + }) + ) + ) : ( + // When search term is applied, show filtered results + + {filteredTemplates.map(template => template ? renderCommandItem(template) : null)} + + )} + + +
+
+
+ ); +}; + +export default SearchableTemplateSelect; \ No newline at end of file diff --git a/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/TemplateManager.tsx b/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/TemplateManager.tsx new file mode 100644 index 0000000..aa1d4ca --- /dev/null +++ b/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/TemplateManager.tsx @@ -0,0 +1,212 @@ +import React, { useState, useEffect } from 'react' +import { Button } from '@/components/ui/button' +import { Input } from '@/components/ui/input' +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog' +import { + Popover, + PopoverContent, + PopoverTrigger +} from '@/components/ui/popover' +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList +} from '@/components/ui/command' +import { ChevronsUpDown, Check, X } from 'lucide-react' +import { cn } from '@/lib/utils' +import { Template } from '../hooks/useValidationState' +import { toast } from 'sonner' + +interface TemplateManagerProps { + templates: Template[] + selectedTemplateId: string | null + onSelectTemplate: (templateId: string | null) => void + onSaveTemplate: (name: string, type: string) => void + onApplyTemplate: (templateId: string) => void + showDialog: boolean + onCloseDialog: () => void + selectedCount: number +} + +const TemplateManager: React.FC = ({ + templates, + selectedTemplateId, + onSelectTemplate, + onSaveTemplate, + onApplyTemplate, + showDialog, + onCloseDialog, + selectedCount, +}) => { + const [templateName, setTemplateName] = useState('') + const [templateType, setTemplateType] = useState('') + const [open, setOpen] = useState(false) + const [searchQuery, setSearchQuery] = useState('') + + // Filter templates based on search + const filteredTemplates = searchQuery + ? templates.filter(template => + template.product_type.toLowerCase().includes(searchQuery.toLowerCase()) || + template.company.toLowerCase().includes(searchQuery.toLowerCase()) + ) + : templates + + const handleSaveTemplate = () => { + if (!templateName.trim()) { + toast.error('Please enter a template name') + return + } + + if (!templateType.trim()) { + toast.error('Please select a template type') + return + } + + onSaveTemplate(templateName, templateType) + setTemplateName('') + setTemplateType('') + } + + // Get display text for template + const getTemplateDisplayText = (template: Template) => { + return `${template.product_type} - ${template.company}` + } + + // Find the currently selected template + const selectedTemplate = templates.find(t => t.id.toString() === selectedTemplateId) + + return ( + <> +
+
+ + + + + + + + + + No templates found. + + {filteredTemplates.map((template) => ( + { + onSelectTemplate(value) + setOpen(false) + setSearchQuery('') + }} + > + + {getTemplateDisplayText(template)} + + ))} + + + + + +
+ +
+ + +
+
+ + {/* Template Save Dialog */} + + + + Save Template + + Create a template from the selected row. + + + +
+
+ + setTemplateName(e.target.value)} + placeholder="Enter company name" + /> +
+ +
+ + setTemplateType(e.target.value)} + placeholder="Enter product type" + /> +
+
+ + + + + +
+
+ + ) +} + +export default TemplateManager \ No newline at end of file diff --git a/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/ValidationCell.tsx b/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/ValidationCell.tsx new file mode 100644 index 0000000..522670a --- /dev/null +++ b/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/ValidationCell.tsx @@ -0,0 +1,175 @@ +import React, { useState, useCallback } from 'react' +import { Field } from '../../../types' +import { Loader2 } from 'lucide-react' +import { cn } from '@/lib/utils' +import { AlertCircle, AlertTriangle, Info } from 'lucide-react' +import InputCell from './cells/InputCell' +import MultiInputCell from './cells/MultiInputCell' +import SelectCell from './cells/SelectCell' +import CheckboxCell from './cells/CheckboxCell' + +// Define an error object type +type ErrorObject = { + message: string; + level: string; + source?: string; +} + +/** + * ValidationIcon - Renders an appropriate icon based on error level + */ +const ValidationIcon = ({ error }: { error: ErrorObject }) => { + const iconClasses = "h-4 w-4" + + switch(error.level) { + case 'error': + return + case 'warning': + return + case 'info': + return + default: + return + } +} + +export interface ValidationCellProps { + rowIndex: number + field: Field + value: any + onChange: (value: any) => void + errors: ErrorObject[] + isValidatingUpc?: boolean +} + +const ValidationCell = ({ + rowIndex, + field, + value, + onChange, + errors, + isValidatingUpc = false +}: ValidationCellProps) => { + const [isEditing, setIsEditing] = useState(false) + + // Get the most severe error + const currentError = errors.length > 0 ? errors[0] : null + + // Determine if field is disabled + const isFieldDisabled = field.disabled || false + + // Check if this is a UPC field for validation + const isUpcField = field.key === 'upc' || + (field.fieldType as any)?.upcField || + field.key.toString().toLowerCase().includes('upc') + + // Render cell contents based on field type + const renderCellContent = () => { + // If we're validating UPC, show a spinner + if (isValidatingUpc) { + return ( +
+ +
+ ) + } + + const fieldType = field.fieldType.type + + // Handle different field types + switch (fieldType) { + case 'input': + return ( + + field={field} + value={value} + onChange={onChange} + onStartEdit={() => setIsEditing(true)} + onEndEdit={() => setIsEditing(false)} + hasErrors={errors.length > 0} + isMultiline={(field.fieldType as any).multiline} + isPrice={(field.fieldType as any).price} + /> + ) + + case 'multi-input': + return ( + + field={field} + value={value} + onChange={onChange} + onStartEdit={() => setIsEditing(true)} + onEndEdit={() => setIsEditing(false)} + hasErrors={errors.length > 0} + separator={(field.fieldType as any).separator || ','} + isMultiline={(field.fieldType as any).multiline} + isPrice={(field.fieldType as any).price} + /> + ) + + case 'select': + return ( + + field={field} + value={value} + onChange={onChange} + onStartEdit={() => setIsEditing(true)} + onEndEdit={() => setIsEditing(false)} + hasErrors={errors.length > 0} + options={field.fieldType.type === 'select' ? field.fieldType.options : undefined} + /> + ) + + case 'multi-select': + return ( + + field={field} + value={value} + onChange={onChange} + onStartEdit={() => setIsEditing(true)} + onEndEdit={() => setIsEditing(false)} + hasErrors={errors.length > 0} + separator={(field.fieldType as any).separator || ','} + options={field.fieldType.type === 'multi-select' ? field.fieldType.options : undefined} + /> + ) + + case 'checkbox': + return ( + + field={field} + value={value} + onChange={onChange} + hasErrors={errors.length > 0} + booleanMatches={(field.fieldType as any).booleanMatches} + /> + ) + + default: + return ( +
+ {String(value || '')} +
+ ) + } + } + + // Main cell rendering + return ( +
+ {renderCellContent()} + + {/* Show error icon if there are errors and we're not editing */} + {currentError && !isEditing && ( +
+ +
+ )} +
+ ) +} + +export default ValidationCell \ No newline at end of file diff --git a/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/ValidationContainer.tsx b/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/ValidationContainer.tsx new file mode 100644 index 0000000..19e6fe2 --- /dev/null +++ b/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/ValidationContainer.tsx @@ -0,0 +1,255 @@ +import React, { useState } from 'react' +import { useValidationState, Props } from '../hooks/useValidationState' +import ValidationTable from './ValidationTable' +import { Button } from '@/components/ui/button' +import { Loader2, X, Plus, Edit3 } from 'lucide-react' +import { toast } from 'sonner' +import { Switch } from '@/components/ui/switch' +import { useRsi } from '../../../hooks/useRsi' +import { ProductSearchDialog } from '@/components/products/ProductSearchDialog' +import SearchableTemplateSelect from './SearchableTemplateSelect' + +/** + * 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, + isValidating, + validationErrors, + rowSelection, + setRowSelection, + updateRow, + hasErrors, + templates, + selectedTemplateId, + applyTemplate, + applyTemplateToSelected, + getTemplateDisplayText, + filters, + updateFilters, + setTemplateState, + templateState, + saveTemplate, + loadTemplates, + setData + } = validationState + + const { translations } = useRsi() + + // State for product search dialog + const [isProductSearchDialogOpen, setIsProductSearchDialogOpen] = useState(false) + + const handleNext = () => { + // Call the onNext callback with the validated data + onNext?.(data) + } + + // Delete selected rows + const deleteSelectedRows = () => { + const selectedRowIndexes = Object.keys(rowSelection).map(Number); + const newData = data.filter((_, index) => !selectedRowIndexes.includes(index)); + setData(newData); + setRowSelection({}); + toast.success( + selectedRowIndexes.length === 1 + ? "Row deleted" + : `${selectedRowIndexes.length} rows deleted` + ); + } + + return ( +
+
+
+
+
+ {/* Header section */} +
+
+
+

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

+ +
+ {isFromScratch && ( + + )} + +
+ updateFilters({ showErrorsOnly: checked })} + id="filter-errors" + /> + +
+
+
+
+
+ + {/* Main table section */} +
+
+
+ + data={filteredData} + fields={validationState.fields} + updateRow={updateRow} + rowSelection={rowSelection} + setRowSelection={setRowSelection} + validationErrors={validationErrors} + isValidatingUpc={validationState.isValidatingUpc} + validatingUpcRows={validationState.validatingUpcRows} + filters={filters} + templates={templates} + applyTemplate={applyTemplate} + getTemplateDisplayText={getTemplateDisplayText} + /> +
+
+
+
+
+ + {/* Selection Action Bar - only shown when items are selected */} + {Object.keys(rowSelection).length > 0 && ( +
+
+
+
+ {Object.keys(rowSelection).length} selected +
+ + +
+ +
+ {templates && templates.length > 0 ? ( + { + if (value) { + applyTemplateToSelected(value); + } + }} + getTemplateDisplayText={getTemplateDisplayText} + placeholder="Apply template to selected" + triggerClassName="w-[220px]" + /> + ) : ( + + )} +
+ + {Object.keys(rowSelection).length === 1 && ( + + )} + + +
+
+ )} +
+
+ + {/* Footer with navigation buttons */} +
+
+ {onBack && ( + + )} +
+ +
+
+
+ + {/* Product Search Dialog */} + setIsProductSearchDialogOpen(false)} + onTemplateCreated={loadTemplates} + /> +
+ ) +} + +export default ValidationContainer \ No newline at end of file diff --git a/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/ValidationTable.tsx b/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/ValidationTable.tsx new file mode 100644 index 0000000..54593ea --- /dev/null +++ b/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/ValidationTable.tsx @@ -0,0 +1,251 @@ +import React, { useMemo } from 'react' +import { + useReactTable, + getCoreRowModel, + flexRender, + createColumnHelper, + RowSelectionState +} from '@tanstack/react-table' +import { Fields, Field } from '../../../types' +import { RowData, Template } from '../hooks/useValidationState' +import { Checkbox } from '@/components/ui/checkbox' +import ValidationCell from './ValidationCell' +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table" +import { useRsi } from '../../../hooks/useRsi' +import { Button } from '@/components/ui/button' +import SearchableTemplateSelect from './SearchableTemplateSelect' + +// Define a simple Error type locally to avoid import issues +type ErrorType = { + message: string; + level: string; + source?: string; +} + +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 + [key: string]: any +} + +const ValidationTable = ({ + data, + fields, + rowSelection, + setRowSelection, + updateRow, + validationErrors, + isValidatingUpc, + validatingUpcRows, + filters, + templates, + applyTemplate, + getTemplateDisplayText, + ...props +}: ValidationTableProps) => { + const { translations } = useRsi() + const columnHelper = createColumnHelper>() + + // Build table columns + const columns = useMemo(() => { + const selectionColumn = columnHelper.display({ + id: 'selection', + header: ({ table }) => ( + table.toggleAllRowsSelected(!!value)} + aria-label="Select all rows" + /> + ), + cell: ({ row }) => ( + row.toggleSelected(!!value)} + aria-label="Select row" + /> + ), + size: 40, + }) + + // Add template column + const templateColumn = columnHelper.display({ + id: 'template', + header: "Template", + cell: ({ row }) => { + try { + // Only render the component if templates are available + if (!templates || templates.length === 0) { + return ( + + ); + } + + return ( + { + try { + // Apply template to this row + applyTemplate(value, [row.index]); + } catch (error) { + console.error("Error applying template in cell:", error); + } + }} + getTemplateDisplayText={getTemplateDisplayText} + defaultBrand={row.original.company as string || ""} + /> + ); + } catch (error) { + console.error("Error rendering template cell:", error); + return ( + + ); + } + }, + size: 200, + }); + + // Create columns for each field + const fieldColumns = fields.map(field => { + return columnHelper.accessor( + (row: RowData) => row[field.key as keyof typeof row] as any, + { + id: String(field.key), + header: field.label, + cell: ({ row, column, getValue }) => { + const rowIndex = row.index + const key = column.id as T + const value = getValue() + const errors = validationErrors.get(rowIndex)?.[key] || [] + + // Create a properly typed field object + const typedField: Field = { + label: field.label, + key: field.key as T, + alternateMatches: field.alternateMatches as string[] | undefined, + validations: field.validations as any[] | undefined, + fieldType: field.fieldType, + example: field.example, + width: field.width, + disabled: field.disabled, + onChange: field.onChange + } + + return ( + + rowIndex={rowIndex} + field={typedField} + value={value} + onChange={(newValue) => updateRow(rowIndex, key, newValue)} + errors={errors} + isValidatingUpc={isValidatingUpc(rowIndex)} + /> + ) + }, + size: (field as any).width || ( + field.fieldType.type === "checkbox" ? 80 : + field.fieldType.type === "select" ? 150 : + 200 + ), + } + ) + }) + + return [selectionColumn, templateColumn, ...fieldColumns] + }, [columnHelper, fields, updateRow, validationErrors, isValidatingUpc, templates, applyTemplate, getTemplateDisplayText]) + + // Initialize table + const table = useReactTable({ + data, + columns, + state: { + rowSelection, + }, + enableRowSelection: true, + onRowSelectionChange: setRowSelection, + getCoreRowModel: getCoreRowModel(), + }) + + return ( + + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => ( + + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext() + )} + + ))} + + ))} + + + {table.getRowModel().rows?.length ? ( + table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender(cell.column.columnDef.cell, cell.getContext())} + + ))} + + )) + ) : ( + + + {filters?.showErrorsOnly + ? translations.validationStep.noRowsMessageWhenFiltered || "No rows with errors found." + : translations.validationStep.noRowsMessage || "No rows found."} + + + )} + +
+ ) +} + +export default ValidationTable \ No newline at end of file diff --git a/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells/CheckboxCell.tsx b/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells/CheckboxCell.tsx new file mode 100644 index 0000000..4429bbe --- /dev/null +++ b/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells/CheckboxCell.tsx @@ -0,0 +1,99 @@ +import React, { useState, useEffect, useCallback } from 'react' +import { Field } from '../../../../types' +import { Checkbox } from '@/components/ui/checkbox' +import { cn } from '@/lib/utils' + +interface CheckboxCellProps { + field: Field + value: any + onChange: (value: any) => void + hasErrors?: boolean + booleanMatches?: Record +} + +const CheckboxCell = ({ + field, + value, + onChange, + hasErrors, + booleanMatches = {} +}: CheckboxCellProps) => { + const [checked, setChecked] = 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]) + + // 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 ( +
+ +
+ ) +} + +export default CheckboxCell \ No newline at end of file diff --git a/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells/InputCell.tsx b/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells/InputCell.tsx new file mode 100644 index 0000000..a8f3551 --- /dev/null +++ b/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells/InputCell.tsx @@ -0,0 +1,132 @@ +import React, { useState, useEffect, useCallback } from 'react' +import { Field } from '../../../../types' +import { Input } from '@/components/ui/input' +import { Textarea } from '@/components/ui/textarea' +import { cn } from '@/lib/utils' + +interface InputCellProps { + field: Field + value: any + onChange: (value: any) => void + onStartEdit?: () => void + onEndEdit?: () => void + hasErrors?: boolean + isMultiline?: boolean + isPrice?: boolean +} + +const InputCell = ({ + field, + value, + onChange, + onStartEdit, + onEndEdit, + hasErrors, + isMultiline = false, + isPrice = false +}: InputCellProps) => { + const [inputValue, setInputValue] = useState('') + const [isEditing, setIsEditing] = useState(false) + + // Initialize input value + useEffect(() => { + if (value !== undefined && value !== null) { + setInputValue(String(value)) + } else { + setInputValue('') + } + }, [value]) + + // Handle focus event + const handleFocus = useCallback(() => { + setIsEditing(true) + onStartEdit?.() + }, [onStartEdit]) + + // Handle blur event + const handleBlur = useCallback(() => { + setIsEditing(false) + + // Format the value for storage (remove formatting like $ for price) + let processedValue = inputValue + + if (isPrice) { + // Remove any non-numeric characters except decimal point + processedValue = inputValue.replace(/[^\d.]/g, '') + + // Parse as float and format to 2 decimal places to ensure valid number + const numValue = parseFloat(processedValue) + if (!isNaN(numValue)) { + processedValue = numValue.toFixed(2) + } + } + + onChange(processedValue) + onEndEdit?.() + }, [inputValue, onChange, onEndEdit, isPrice]) + + // Format price value for display + const getDisplayValue = useCallback(() => { + if (!isPrice || !inputValue) return inputValue + + // Extract numeric part + const numericValue = inputValue.replace(/[^\d.]/g, '') + + // Parse as float and format with dollar sign + const numValue = parseFloat(numericValue) + if (isNaN(numValue)) return inputValue + + return `$${numValue.toFixed(2)}` + }, [inputValue, isPrice]) + + // Add outline even when not in focus + const outlineClass = "border focus-visible:ring-0 focus-visible:ring-offset-0" + + return ( +
+ {isMultiline ? ( +