From 68ca7e93a1161cd41c5772686dd82ea668a148c9 Mon Sep 17 00:00:00 2001 From: Matt Date: Thu, 6 Mar 2025 01:45:05 -0500 Subject: [PATCH] Fix dropdown values saving, add back checkbox column, mostly fix validation, fix some field types --- inventory-server/src/routes/ai-validation.js | 2 +- inventory-server/src/routes/import.js | 14 +- .../steps/ValidationStep/ValidationStep.tsx | 4373 ----------------- .../src/steps/ValidationStep/types.ts | 5 - .../ValidationStep/utils/dataMutations.ts | 151 - .../components/AiValidationDialogs.tsx | 4 +- .../components/SearchableTemplateSelect.tsx | 18 +- .../components/ValidationCell.tsx | 44 +- .../components/ValidationContainer.tsx | 83 +- .../components/ValidationTable.tsx | 172 +- .../components/cells/InputCell.tsx | 39 +- .../hooks/useValidationState.tsx | 802 ++- 12 files changed, 860 insertions(+), 4847 deletions(-) delete mode 100644 inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStep/ValidationStep.tsx delete mode 100644 inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStep/types.ts delete mode 100644 inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStep/utils/dataMutations.ts diff --git a/inventory-server/src/routes/ai-validation.js b/inventory-server/src/routes/ai-validation.js index 08f7174..7d0e2db 100644 --- a/inventory-server/src/routes/ai-validation.js +++ b/inventory-server/src/routes/ai-validation.js @@ -742,7 +742,7 @@ router.post("/validate", async (req, res) => { console.log("🤖 Sending request to OpenAI..."); const completion = await openai.chat.completions.create({ - model: "gpt-4o", + model: "o3-mini", messages: [ { role: "user", diff --git a/inventory-server/src/routes/import.js b/inventory-server/src/routes/import.js index f4b295d..fff8134 100644 --- a/inventory-server/src/routes/import.js +++ b/inventory-server/src/routes/import.js @@ -924,16 +924,16 @@ router.get('/check-upc-and-generate-sku', async (req, res) => { }); } - // Step 2: Generate item number - supplierId-last6DigitsOfUPC minus last digit + // Step 2: Generate item number - supplierId-last5DigitsOfUPC minus last digit let itemNumber = ''; const upcStr = String(upc); - // Extract the last 6 digits of the UPC, removing the last digit (checksum) - // So we get 5 digits from positions: length-7 to length-2 - if (upcStr.length >= 7) { - const lastSixMinusOne = upcStr.substring(upcStr.length - 7, upcStr.length - 1); - itemNumber = `${supplierId}-${lastSixMinusOne}`; - } else if (upcStr.length >= 6) { + // Extract the last 5 digits of the UPC, removing the last digit (checksum) + // So we get 5 digits from positions: length-6 to length-2 + if (upcStr.length >= 6) { + const lastFiveMinusOne = upcStr.substring(upcStr.length - 6, upcStr.length - 1); + itemNumber = `${supplierId}-${lastFiveMinusOne}`; + } else if (upcStr.length >= 5) { // If UPC is shorter, use as many digits as possible const digitsToUse = upcStr.substring(0, upcStr.length - 1); itemNumber = `${supplierId}-${digitsToUse}`; diff --git a/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStep/ValidationStep.tsx b/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStep/ValidationStep.tsx deleted file mode 100644 index 3e8aca5..0000000 --- a/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStep/ValidationStep.tsx +++ /dev/null @@ -1,4373 +0,0 @@ -import { useCallback, useMemo, useState, useEffect, memo, useRef } from "react" -import { useRsi } from "../../hooks/useRsi" -import type { Meta, Error } from "./types" -import { addErrorsAndRunHooks } from "./utils/dataMutations" -import type { Data, SelectOption, Result, Fields, Field } from "../../types" -import { Check, ChevronsUpDown, ArrowDown, AlertCircle, Loader2, X, Plus, Edit3 } from "lucide-react" -import { cn } from "@/lib/utils" -import { - Command, - CommandEmpty, - CommandGroup, - CommandInput, - CommandItem, - CommandList, -} from "@/components/ui/command" -import { - Popover, - PopoverContent, - PopoverTrigger, -} from "@/components/ui/popover" -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from "@/components/ui/table" -import { - useReactTable, - getCoreRowModel, - type ColumnDef, - flexRender, - type RowSelectionState, -} from "@tanstack/react-table" -import { Checkbox } from "@/components/ui/checkbox" -import { Input } from "@/components/ui/input" -import { Button } from "@/components/ui/button" -import { Switch } from "@/components/ui/switch" -import { useToast } from "@/hooks/use-toast" -import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, - AlertDialogPortal, - AlertDialogOverlay, -} from "@/components/ui/alert-dialog" -import { - Tooltip, - TooltipContent, - TooltipProvider, - TooltipTrigger, -} from "@/components/ui/tooltip" -import config from "@/config" -import { - Dialog, - DialogContent, - DialogDescription, - DialogHeader, - DialogTitle, - DialogFooter, -} from "@/components/ui/dialog" -import { ScrollArea } from "@/components/ui/scroll-area" -import { Code } from "@/components/ui/code" -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select" -import type { GlobalSelections } from "../MatchColumnsStep/MatchColumnsStep" -import { useQuery } from "@tanstack/react-query" -import { Badge } from "@/components/ui/badge" -import { CheckIcon } from "lucide-react" -import * as Diff from 'diff' -import { ProductSearchDialog } from '../../../../../components/products/ProductSearchDialog'; - -// Template interface -interface Template { - id: number; - company: string; - product_type: string; - [key: string]: string | number | boolean | undefined; -} - -// Extend Meta type with template -type ExtendedMeta = Meta & { - __template?: string; -} - -// Define a type for the row data that includes both Data and Meta -type RowData = Data & ExtendedMeta; - -// Define template-related types -type TemplateState = { - selectedTemplateId: string | null; - showSaveTemplateDialog: boolean; - newTemplateName: string; - newTemplateType: string; -} - -type Props = { - initialData: RowData[] - file: File - onBack?: () => void - onNext?: (data: RowData[]) => void - globalSelections?: GlobalSelections - isFromScratch?: boolean -} - -// Remove the local Field type declaration since we're importing it -type BaseFieldType = { - multiline?: boolean; - price?: boolean; -} - -type InputFieldType = BaseFieldType & { - type: "input"; -} - -type MultiInputFieldType = BaseFieldType & { - type: "multi-input"; - separator?: string; -} - -type SelectFieldType = { - type: "select" | "multi-select"; - options: readonly SelectOption[]; -} - -type CheckboxFieldType = { - type: "checkbox"; - booleanMatches?: { [key: string]: boolean }; -} - -type FieldType = InputFieldType | MultiInputFieldType | SelectFieldType | CheckboxFieldType; - - -type CellProps = { - value: any; - onChange: (value: any) => void; - error?: { level: string; message: string }; - field: Field & { - 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 -const ValidationIcon = memo(({ error }: { error: { level: string, message: string } }) => ( - - - -
- -
-
- -

{error.message}

-
-
-
-)) - -// Wrap EditableCell with memo to avoid unnecessary re-renders -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("") - const [localValues, setLocalValues] = useState([]) - const [isProcessingUpc, setIsProcessingUpc] = useState(false) - const { toast } = useToast() - - // Determine if the field should be disabled based on its key and context - const isFieldDisabled = useMemo(() => { - // If the field is already disabled by the parent component, respect that - if (field.disabled) return true; - - // Never disable item number fields with values - if ((field.key === 'sku' || field.key === 'itemnumber' || field.key === 'item_number') && value) { - return false; - } - - // Special handling for line and subline fields - if (field.key === 'line') { - // Never disable line field if it already has a value - if (value) return false; - - // The line field should be enabled if we have a company selected or product lines available - const hasCompanySelected = (field.fieldType.type === 'select' || field.fieldType.type === 'multi-select') && - field.fieldType.options && field.fieldType.options.length > 0; - const hasFetchedProductLines = productLines && productLines.length > 0; - - return !(hasCompanySelected || hasFetchedProductLines); - } - if (field.key === 'subline') { - // Never disable subline field if it already has a value - if (value) return false; - - // The subline field should be enabled if we have a line selected or sublines available - const hasLineSelected = (field.fieldType.type === 'select' || field.fieldType.type === 'multi-select') && - field.fieldType.options && field.fieldType.options.length > 0; - const hasFetchedSublines = sublines && sublines.length > 0; - - return !(hasLineSelected || hasFetchedSublines); - } - - // For other fields, use the disabled property - return field.disabled; - }, [field.key, field.disabled, field.fieldType, productLines, sublines, value]); - - - const handleWheel = useCallback((e: React.WheelEvent) => { - const commandList = e.currentTarget; - commandList.scrollTop += e.deltaY; - e.stopPropagation(); - }, []); - - // Update input value when value changes - use useEffect to ensure synchronization - useEffect(() => { - // Always update input value when value prop changes - setInputValue(value ?? "") - - // 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]) - - // Keep localValues in sync with value for multi-select - useEffect(() => { - if (field.fieldType.type === "multi-select") { - const selectedValues = Array.isArray(value) ? value : value ? [value] : [] - 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 - // Remove dollar signs and trim - return value.replace(/^\$/, '').trim() - } - - const validateRegex = (val: any) => { - // Handle non-string values - if (val === undefined || val === null) return undefined - if (Array.isArray(val)) { - // For arrays (multi-select), join values with comma - val = val.join(", ") - } - if (typeof val === "boolean") { - // For booleans, convert to "Yes"/"No" - val = val ? "Yes" : "No" - } - - // Convert to string and check if empty/whitespace - const strVal = String(val) - if (!strVal || !strVal.trim()) return undefined - - const regexValidation = field.validations?.find(v => v.rule === "regex") - if (regexValidation) { - let testValue = strVal - // For price fields, remove dollar sign before testing - if (isPriceField(field.fieldType)) { - testValue = formatPrice(strVal) - } - - const regex = new RegExp(regexValidation.value, regexValidation.flags) - if (!regex.test(testValue)) { - return { level: regexValidation.level || "error", message: regexValidation.errorMessage } - } - } - return undefined - } - - // Only show validation errors when not editing and value is invalid - const getValidationError = () => { - if (isEditing) return undefined - // Only validate if we have a non-empty value - if (!value || !value.toString().trim()) return undefined - return error || validateRegex(value) - } - - const currentError = getValidationError() - - const getDisplayValue = (value: any, fieldType: Field["fieldType"]) => { - if (fieldType.type === "select" || fieldType.type === "multi-select") { - if (fieldType.type === "select") { - // For line and subline fields, ensure we're using the latest options - if (field.key === 'line') { - - // First try to find in productLines if available - if (productLines?.length) { - const matchingOption = productLines.find((opt: SelectOption) => - String(opt.value) === String(value)); - if (matchingOption) { - // Remove excessive logging - return matchingOption.label; - } - } - // Fall back to fieldType options if productLines not available yet - if (fieldType.options?.length) { - const fallbackOptionLine = fieldType.options.find((opt: SelectOption) => - String(opt.value) === String(value)); - if (fallbackOptionLine) { - // Remove excessive logging - return fallbackOptionLine.label; - } - } - return value; - } - if (field.key === 'subline') { - // Log current state for debugging - console.log('Getting display value for subline:', { - value, - sublines: sublines?.length ?? 0, - options: fieldType.options?.length ?? 0 - }); - - // First try to find in sublines if available - if (sublines?.length) { - const matchingOption = sublines.find((opt: SelectOption) => - String(opt.value) === String(value)); - if (matchingOption) { - console.log('Found subline in sublines:', value, '->', matchingOption.label); - return matchingOption.label; - } - } - // Fall back to fieldType options if sublines not available yet - if (fieldType.options?.length) { - const fallbackOptionSubline = fieldType.options.find((opt: SelectOption) => - String(opt.value) === String(value)); - if (fallbackOptionSubline) { - console.log('Found subline in fallback options:', value, '->', fallbackOptionSubline.label); - return fallbackOptionSubline.label; - } - } - console.log('Unable to find display value for subline:', value); - return value; - } - return fieldType.options?.find((opt: SelectOption) => String(opt.value) === String(value))?.label || value; - } - if (Array.isArray(value)) { - const options = field.key === 'line' && productLines?.length ? productLines : - field.key === 'subline' && sublines?.length ? sublines : - fieldType.options; - return value.map(v => options.find((opt: SelectOption) => String(opt.value) === String(v))?.label || v).join(", "); - } - return value; - } - if (fieldType.type === "checkbox") { - if (typeof value === "boolean") return value ? "Yes" : "No"; - return value; - } - if (fieldType.type === "multi-input" && Array.isArray(value)) { - return value.join(", "); - } - - // Special handling for SKU/item number fields to make them more visible - if (field.key === 'sku' || field.key === 'itemnumber' || field.key === 'item_number') { - // Format it nicely for display - removed excessive logging - return value ? `${value}` : ""; - } - - return value; - } - - const validateAndCommit = (newValue: string | boolean) => { - // For non-string values (like checkboxes), just commit - if (typeof newValue !== 'string') { - onChange(newValue) - return true - } - - // Handle price fields - if (isPriceField(field.fieldType)) { - newValue = formatPrice(newValue) - } - - // 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}"`); - } - - // Always commit the value - onChange(newValue) - - // Return whether validation passed (only validate non-empty values) - return !validateRegex(newValue) - } - - const handleBlur = async () => { - // Special handling for UPC field - if (field.key === 'upc' || field.key === 'barcode') { - try { - // Skip if input value is empty - if (!inputValue.trim()) { - validateAndCommit(inputValue); - setIsEditing(false); - return; - } - - console.log(`UPC blur handler - saving UPC value: ${inputValue}`); - - // 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); - - try { - console.log(`Cell blur - calling handleUpcValidation for ${currentUpcValue}`); - // Call the UPC validation function - const result = await field.handleUpcValidation(currentUpcValue); - console.log('UPC validation result in handleBlur:', result); - - // 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) { - toast({ - title: "UPC Validation Error", - description: result.message || "Error validating UPC", - variant: "destructive", - }); - } - } catch (error) { - console.error('Error in UPC validation:', error); - - // 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", - description: error instanceof Error ? error.message : "Error processing UPC", - variant: "destructive", - }); - } finally { - setIsProcessingUpc(false); - } - } - - // 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); - } - } - - if (isEditing) { - switch (field.fieldType.type) { - case "select": - return ( -
- { - if (!open) { - validateAndCommit(value) - setIsEditing(false) - } else { - setIsEditing(true) - } - }} - > - - - - - - - - No options found. - - {(field.key === 'line' && productLines ? productLines : - field.key === 'subline' && sublines ? sublines : - field.fieldType.options) - .filter(option => - option.label.toLowerCase().includes(searchQuery.toLowerCase()) - ) - .map((option) => ( - { - onChange(currentValue); - if (field.onChange) { - field.onChange(currentValue); - } - setSearchQuery("") - setIsEditing(false) - }} - > - {option.label} - - - ))} - - - - - - {currentError && } -
- ) - case "multi-select": - return ( -
- - - - - { - setIsEditing(false) - onChange(localValues) - }} - onInteractOutside={(e) => { - const target = e.target as HTMLElement - if (!target.closest('[role="listbox"]')) { - setIsEditing(false) - onChange(localValues) - } - }} - > - - - - No options found. - - {(field.key === 'line' && productLines ? productLines : - field.key === 'subline' && sublines ? sublines : - field.fieldType.options) - .filter(option => - option.label.toLowerCase().includes(searchQuery.toLowerCase()) - ) - .map((option) => ( - { - const valueIndex = localValues.indexOf(option.value) - const newValues = valueIndex === -1 - ? [...localValues, option.value] - : localValues.filter((_, i) => i !== valueIndex) - setLocalValues(newValues) - }} - onMouseDown={(e) => e.preventDefault()} - > - {option.label} - - - ))} - - - - - - {currentError && } -
- ) - case "input": - case "multi-input": - if (field.fieldType.multiline) { - return ( -
- { - if (!open) { - validateAndCommit(inputValue) - setIsEditing(false) - } - }} - > - - - - -
-

{field.label}

-