diff --git a/inventory/src/components/products/ProductSearchDialog.tsx b/inventory/src/components/products/ProductSearchDialog.tsx index 13b20f7..10c2ede 100644 --- a/inventory/src/components/products/ProductSearchDialog.tsx +++ b/inventory/src/components/products/ProductSearchDialog.tsx @@ -1427,10 +1427,10 @@ export function ProductSearchDialog({ isOpen, onClose, onTemplateCreated }: Prod -
+
- + 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 index 54593ea..83b09de 100644 --- 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 @@ -4,22 +4,12 @@ import { getCoreRowModel, flexRender, createColumnHelper, - RowSelectionState -} from '@tanstack/react-table' + 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 @@ -42,9 +32,105 @@ interface ValidationTableProps { templates: Template[] applyTemplate: (templateId: string, rowIndexes: number[]) => void getTemplateDisplayText: (templateId: string | null) => string + rowProductLines?: Record + rowSublines?: Record + isLoadingLines?: Record + isLoadingSublines?: Record [key: string]: any } +// Memoized cell component to prevent unnecessary re-renders +const MemoizedCell = React.memo( + ({ + rowIndex, + field, + value, + errors, + isValidatingUpc, + fieldOptions, + isOptionsLoading, + updateRow, + columnId + }: { + rowIndex: number + field: Field + value: any + errors: ErrorType[] + isValidatingUpc: (rowIndex: number) => boolean + fieldOptions?: any[] + isOptionsLoading?: boolean + updateRow: (rowIndex: number, key: any, value: any) => void + columnId: string + }) => { + const handleChange = (newValue: any) => { + updateRow(rowIndex, columnId, newValue); + }; + + return ( + + ); + }, + // Custom comparison function for the memo + (prevProps, nextProps) => { + // Re-render only if any of these props changed + return ( + prevProps.value === nextProps.value && + prevProps.errors === nextProps.errors && + prevProps.fieldOptions === nextProps.fieldOptions && + prevProps.isOptionsLoading === nextProps.isOptionsLoading && + prevProps.isValidatingUpc(prevProps.rowIndex) === nextProps.isValidatingUpc(nextProps.rowIndex) + ); + } +); + +// Memoized template cell component +const MemoizedTemplateCell = React.memo( + ({ + rowIndex, + templateValue, + templates, + applyTemplate, + getTemplateDisplayText + }: { + rowIndex: number + templateValue: string | null + templates: Template[] + applyTemplate: (templateId: string, rowIndexes: number[]) => void + getTemplateDisplayText: (templateId: string | null) => string + }) => { + const handleTemplateChange = (value: string) => { + applyTemplate(value, [rowIndex]); + }; + + return ( + + template ? getTemplateDisplayText(template) : 'Select template' + } + /> + ); + }, + // Custom comparison function for the memo + (prevProps, nextProps) => { + return ( + prevProps.templateValue === nextProps.templateValue && + prevProps.templates === nextProps.templates + ); + } +); + const ValidationTable = ({ data, fields, @@ -53,128 +139,147 @@ const ValidationTable = ({ updateRow, validationErrors, isValidatingUpc, - validatingUpcRows, filters, templates, applyTemplate, getTemplateDisplayText, - ...props -}: ValidationTableProps) => { + rowProductLines = {}, + rowSublines = {}, + isLoadingLines = {}, + isLoadingSublines = {}}: ValidationTableProps) => { const { translations } = useRsi() const columnHelper = createColumnHelper>() - // Build table columns + // Define columns for the table const columns = useMemo(() => { + // Selection column const selectionColumn = columnHelper.display({ - id: 'selection', + id: 'select', header: ({ table }) => ( - table.toggleAllRowsSelected(!!value)} - aria-label="Select all rows" - /> +
+ table.toggleAllPageRowsSelected(!!value)} + aria-label="Select all" + /> +
), cell: ({ row }) => ( - row.toggleSelected(!!value)} - aria-label="Select row" - /> +
+ row.toggleSelected(!!value)} + aria-label="Select row" + /> +
), - size: 40, - }) + size: 50, + }); - // Add template column + // Template column const templateColumn = columnHelper.display({ id: 'template', - header: "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 ( - - ); - } + const rowIndex = row.index; + return ( + + ); }, size: 200, }); // Create columns for each field const fieldColumns = fields.map(field => { + // Get the field width directly from the field definition + // These are exactly the values defined in Import.tsx + const fieldWidth = field.width || ( + field.fieldType.type === "checkbox" ? 80 : + field.fieldType.type === "select" ? 150 : + field.fieldType.type === "multi-select" ? 200 : + (field.fieldType.type === "input" || field.fieldType.type === "multi-input") && + (field.fieldType as any).multiline ? 300 : + 150 + ); + return columnHelper.accessor( - (row: RowData) => row[field.key as keyof typeof row] as any, + (row: RowData) => row[field.key as keyof typeof row], { - id: String(field.key), + id: 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 + cell: ({ row, column }) => { + try { + const rowIndex = row.index; + const value = row.getValue(column.id); + const errors = validationErrors.get(rowIndex)?.[column.id] || []; + const rowId = row.original?.__index; + + // Determine if we have custom options for this field + let fieldOptions; + let isOptionsLoading = false; + + // Handle line field - use company-specific product lines + if (field.key === 'line' && rowId && rowProductLines[rowId]) { + fieldOptions = rowProductLines[rowId]; + isOptionsLoading = isLoadingLines[rowId] || false; + } + // Handle subline field - use line-specific sublines + else if (field.key === 'subline' && rowId && rowSublines[rowId]) { + fieldOptions = rowSublines[rowId]; + isOptionsLoading = isLoadingSublines[rowId] || false; + } + + // Cast the field type for ValidationCell + const typedField = field as Field; + + return ( + + ); + } catch (error) { + console.error(`Error rendering cell for column ${column.id}:`, error); + return ( +
+ Error rendering cell +
+ ); } - - 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 - ), + size: fieldWidth, } - ) - }) + ); + }); - return [selectionColumn, templateColumn, ...fieldColumns] - }, [columnHelper, fields, updateRow, validationErrors, isValidatingUpc, templates, applyTemplate, getTemplateDisplayText]) + return [selectionColumn, templateColumn, ...fieldColumns]; + }, [ + columnHelper, + fields, + templates, + applyTemplate, + getTemplateDisplayText, + rowProductLines, + rowSublines, + isLoadingLines, + isLoadingSublines, + validationErrors, + isValidatingUpc, + updateRow + ]); // Initialize table const table = useReactTable({ @@ -186,66 +291,84 @@ const ValidationTable = ({ 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."} - - - )} - -
- ) -} + // Apply filters to rows if needed + const filteredRows = useMemo(() => { + let rows = table.getRowModel().rows; + + if (filters?.showErrorsOnly) { + rows = rows.filter(row => { + const rowIndex = row.index; + return validationErrors.has(rowIndex) && + Object.values(validationErrors.get(rowIndex) || {}).some(errors => errors.length > 0); + }); + } + + return rows; + }, [table, filters, validationErrors]); -export default ValidationTable \ No newline at end of file + return ( +
+
+ + + + {table.getHeaderGroups()[0].headers.map((header) => ( + + ))} + + + + {filteredRows.length ? ( + filteredRows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + ))} + + )) + ) : ( + + + + )} + +
+ {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext() + )} +
+ {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/InputCell.tsx b/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells/InputCell.tsx index a8f3551..8d51604 100644 --- 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 @@ -90,7 +90,6 @@ const InputCell = ({ onChange={(e) => setInputValue(e.target.value)} onFocus={handleFocus} onBlur={handleBlur} - placeholder={field.description} className={cn( "min-h-[80px] resize-none", outlineClass, @@ -105,7 +104,6 @@ const InputCell = ({ onChange={(e) => setInputValue(e.target.value)} onFocus={handleFocus} onBlur={handleBlur} - placeholder={field.description} autoFocus className={cn( outlineClass, @@ -121,7 +119,7 @@ const InputCell = ({ hasErrors ? "border-destructive" : "border-input" )} > - {isPrice ? getDisplayValue() : (inputValue || {field.description})} + {isPrice ? getDisplayValue() : (inputValue)}
) )} diff --git a/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells/MultiInputCell.tsx b/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells/MultiInputCell.tsx index f1ff24d..a273d1b 100644 --- a/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells/MultiInputCell.tsx +++ b/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells/MultiInputCell.tsx @@ -177,7 +177,7 @@ const MultiInputCell = ({ ) }) ) : ( - {field.description || "Select options..."} + { "Select options..."} )}
diff --git a/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells/SelectCell.tsx b/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells/SelectCell.tsx index ef913f8..6d7ff03 100644 --- a/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells/SelectCell.tsx +++ b/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells/SelectCell.tsx @@ -65,7 +65,7 @@ const SelectCell = ({ // Get current display value const displayValue = value ? selectOptions.find(option => String(option.value) === String(value))?.label || String(value) : - field.description || 'Select...'; + 'Select...'; const handleSelect = (selectedValue: string) => { onChange(selectedValue); diff --git a/inventory/src/lib/react-spreadsheet-import/src/utils/mapWorkbook.ts b/inventory/src/lib/react-spreadsheet-import/src/utils/mapWorkbook.ts index a00a833..6182090 100644 --- a/inventory/src/lib/react-spreadsheet-import/src/utils/mapWorkbook.ts +++ b/inventory/src/lib/react-spreadsheet-import/src/utils/mapWorkbook.ts @@ -1,9 +1,10 @@ import * as XLSX from "xlsx" import type { RawData } from "../types" -export const mapWorkbook = (workbook: XLSX.WorkBook): RawData[] => { - const firstSheetName = workbook.SheetNames[0] - const worksheet = workbook.Sheets[firstSheetName] +export const mapWorkbook = (workbook: XLSX.WorkBook, sheetName?: string): RawData[] => { + // Use the provided sheetName or default to the first sheet + const sheetToUse = sheetName || workbook.SheetNames[0] + const worksheet = workbook.Sheets[sheetToUse] const data = XLSX.utils.sheet_to_json(worksheet, { header: 1, diff --git a/inventory/src/pages/Import.tsx b/inventory/src/pages/Import.tsx index d96c517..b7cbace 100644 --- a/inventory/src/pages/Import.tsx +++ b/inventory/src/pages/Import.tsx @@ -31,7 +31,7 @@ const BASE_IMPORT_FIELDS = [ type: "select", options: [], // Will be populated from API }, - width: 200, + width: 220, validations: [{ rule: "required", errorMessage: "Required", level: "error" }], }, { @@ -43,7 +43,7 @@ const BASE_IMPORT_FIELDS = [ type: "select", options: [], // Will be populated dynamically based on company selection }, - width: 180, + width: 220, validations: [{ rule: "required", errorMessage: "Required", level: "error" }], }, { @@ -54,7 +54,7 @@ const BASE_IMPORT_FIELDS = [ type: "select", options: [], // Will be populated dynamically based on line selection }, - width: 180, + width: 220, }, { label: "UPC", @@ -86,7 +86,7 @@ const BASE_IMPORT_FIELDS = [ description: "Supplier's product identifier", alternateMatches: ["sku", "item#", "mfg item #", "item", "supplier #"], fieldType: { type: "input" }, - width: 180, + width: 130, validations: [ { rule: "required", errorMessage: "Required", level: "error" }, { rule: "unique", errorMessage: "Must be unique", level: "error" }, @@ -98,7 +98,7 @@ const BASE_IMPORT_FIELDS = [ description: "Internal notions number", alternateMatches: ["notions #","nmc"], fieldType: { type: "input" }, - width: 110, + width: 100, validations: [ { rule: "required", errorMessage: "Required", level: "error" }, { rule: "unique", errorMessage: "Must be unique", level: "error" }, @@ -133,12 +133,12 @@ const BASE_IMPORT_FIELDS = [ ], }, { - label: "Qty Per Unit", + label: "Min Qty", key: "qty_per_unit", description: "Quantity of items per individual unit", alternateMatches: ["inner pack", "inner", "min qty", "unit qty", "min. order qty", "supplier qty/unit"], fieldType: { type: "input" }, - width: 90, + width: 80, validations: [ { rule: "required", errorMessage: "Required", level: "error" }, { rule: "regex", value: "^[0-9]+$", errorMessage: "Must be a number", level: "error" }, @@ -165,7 +165,7 @@ const BASE_IMPORT_FIELDS = [ description: "Number of units per case", alternateMatches: ["mc qty","case qty","case pack","box ct"], fieldType: { type: "input" }, - width: 50, + width: 100, validations: [ { rule: "regex", value: "^[0-9]+$", errorMessage: "Must be a number", level: "error" }, ], @@ -178,7 +178,7 @@ const BASE_IMPORT_FIELDS = [ type: "select", options: [], // Will be populated from API }, - width: 180, + width: 200, validations: [{ rule: "required", errorMessage: "Required", level: "error" }], }, { @@ -189,7 +189,7 @@ const BASE_IMPORT_FIELDS = [ type: "select", options: [], // Will be populated from API }, - width: 180, + width: 200, }, { label: "ETA Date", @@ -256,12 +256,12 @@ const BASE_IMPORT_FIELDS = [ validations: [{ rule: "required", errorMessage: "Required", level: "error" }], }, { - label: "Country Of Origin", + label: "COO", key: "coo", description: "2-letter country code (ISO)", alternateMatches: ["coo", "country of origin"], fieldType: { type: "input" }, - width: 100, + width: 70, validations: [ { rule: "regex", value: "^[A-Z]{2}$", errorMessage: "Must be 2 letters", level: "error" }, ], @@ -296,7 +296,7 @@ const BASE_IMPORT_FIELDS = [ type: "input", multiline: true }, - width: 400, + width: 500, validations: [{ rule: "required", errorMessage: "Required", level: "error" }], }, {