diff --git a/inventory/src/components/product-import/steps/MatchColumnsStep/MatchColumnsStep.tsx b/inventory/src/components/product-import/steps/MatchColumnsStep/MatchColumnsStep.tsx index 9d251d4..f79a5fe 100644 --- a/inventory/src/components/product-import/steps/MatchColumnsStep/MatchColumnsStep.tsx +++ b/inventory/src/components/product-import/steps/MatchColumnsStep/MatchColumnsStep.tsx @@ -1021,7 +1021,7 @@ const MatchColumnsStepComponent = ({ setColumns( columns.map>((column, index) => { - if (columnIndex === index) { + if (column.index === columnIndex) { // Set the new column value const updatedColumn = setColumn(column, field as Field, data, autoMapSelectValues); @@ -1143,15 +1143,15 @@ const MatchColumnsStepComponent = ({ const onIgnore = useCallback( (columnIndex: number) => { - setColumns(columns.map((column, index) => (columnIndex === index ? setIgnoreColumn(column) : column))) + setColumns(columns.map((column) => (column.index === columnIndex ? setIgnoreColumn(column) : column))) }, [columns, setColumns], ) const onToggleAiSupplemental = useCallback( (columnIndex: number) => { - setColumns(columns.map((column, index) => { - if (columnIndex !== index) return column; + setColumns(columns.map((column) => { + if (column.index !== columnIndex) return column; if (column.type === ColumnType.aiSupplemental) { return { type: ColumnType.empty, index: column.index, header: column.header } as Column; @@ -1168,7 +1168,7 @@ const MatchColumnsStepComponent = ({ const onRevertIgnore = useCallback( (columnIndex: number) => { - setColumns(columns.map((column, index) => (columnIndex === index ? setColumn(column) : column))) + setColumns(columns.map((column) => (column.index === columnIndex ? setColumn(column) : column))) }, [columns, setColumns], ) @@ -1177,7 +1177,7 @@ const MatchColumnsStepComponent = ({ (value: string, columnIndex: number, entry: string) => { setColumns( columns.map((column, index) => - columnIndex === index && "matchedOptions" in column + column.index === columnIndex && "matchedOptions" in column ? setSubColumn(column as MatchedSelectColumn | MatchedSelectOptionsColumn | MatchedMultiSelectColumn, entry, value) : column, ), diff --git a/inventory/src/components/product-import/steps/MatchColumnsStep/utils/normalizeTableData.ts b/inventory/src/components/product-import/steps/MatchColumnsStep/utils/normalizeTableData.ts index 356c831..18af991 100644 --- a/inventory/src/components/product-import/steps/MatchColumnsStep/utils/normalizeTableData.ts +++ b/inventory/src/components/product-import/steps/MatchColumnsStep/utils/normalizeTableData.ts @@ -4,8 +4,8 @@ import { normalizeCheckboxValue } from "./normalizeCheckboxValue" export const normalizeTableData = (columns: Columns, data: RawData[], fields: Fields) => data.map((row) => - columns.reduce((acc, column, index) => { - const curr = row[index] + columns.reduce((acc, column) => { + const curr = row[column.index] switch (column.type) { case ColumnType.matchedCheckbox: { const field = fields.find((field) => field.key === column.value)! diff --git a/inventory/src/components/product-import/utils/mapWorkbook.ts b/inventory/src/components/product-import/utils/mapWorkbook.ts index 6182090..c0a90ed 100644 --- a/inventory/src/components/product-import/utils/mapWorkbook.ts +++ b/inventory/src/components/product-import/utils/mapWorkbook.ts @@ -1,16 +1,153 @@ import * as XLSX from "xlsx" +import type { CellObject } from "xlsx" import type { RawData } from "../types" +const SCIENTIFIC_NOTATION_REGEX = /^[+-]?(?:\d+\.?\d*|\d*\.?\d+)e[+-]?\d+$/i + +const convertScientificToDecimalString = (input: string): string => { + const match = input.toLowerCase().match(/^([+-]?)(\d+)(?:\.(\d+))?e([+-]?\d+)$/) + if (!match) return input + + const [, sign, integerPart, fractionalPart = "", exponentPart] = match + const exponent = parseInt(exponentPart, 10) + + if (Number.isNaN(exponent)) return input + + const digits = `${integerPart}${fractionalPart}` + + if (exponent >= 0) { + const decimalIndex = integerPart.length + exponent + if (decimalIndex >= digits.length) { + const zerosToAppend = decimalIndex - digits.length + return `${sign}${digits}${"0".repeat(zerosToAppend)}` + } + + const whole = digits.slice(0, decimalIndex) || "0" + const fraction = digits.slice(decimalIndex).replace(/0+$/, "") + return fraction ? `${sign}${whole}.${fraction}` : `${sign}${whole}` + } + + const decimalIndex = integerPart.length + exponent + if (decimalIndex <= 0) { + const zerosToPrepend = Math.abs(decimalIndex) + const fractionDigits = `${"0".repeat(zerosToPrepend)}${digits}`.replace(/0+$/, "") + return fractionDigits ? `${sign}0.${fractionDigits}` : "0" + } + + const whole = digits.slice(0, decimalIndex) || "0" + const fractionDigits = digits.slice(decimalIndex).replace(/0+$/, "") + return fractionDigits ? `${sign}${whole}.${fractionDigits}` : `${sign}${whole}` +} + +const numberToPlainString = (value: number): string => { + if (!Number.isFinite(value)) return "" + const stringified = value.toString() + return SCIENTIFIC_NOTATION_REGEX.test(stringified) ? convertScientificToDecimalString(stringified) : stringified +} + +const normalizeFromCell = (cell: CellObject | undefined): string | undefined => { + if (!cell) return undefined + + const { v, w } = cell + const cellType = (cell.t as string) || "" + + if (typeof w === "string" && w.trim() !== "") { + const trimmed = w.trim() + return SCIENTIFIC_NOTATION_REGEX.test(trimmed) ? convertScientificToDecimalString(trimmed) : trimmed + } + + switch (cellType) { + case "n": + if (typeof v === "number") return numberToPlainString(v) + if (typeof v === "string") { + const trimmed = v.trim() + return SCIENTIFIC_NOTATION_REGEX.test(trimmed) ? convertScientificToDecimalString(trimmed) : trimmed + } + return v === undefined || v === null ? "" : String(v) + case "s": + case "str": + return typeof v === "string" ? v : v === undefined || v === null ? "" : String(v) + case "b": + return v ? "TRUE" : "FALSE" + case "d": + if (v instanceof Date) return v.toISOString() + if (typeof v === "number") { + const date = XLSX.SSF.parse_date_code(v) + if (date) { + const year = date.y.toString().padStart(4, "0") + const month = date.m.toString().padStart(2, "0") + const day = date.d.toString().padStart(2, "0") + return `${year}-${month}-${day}` + } + } + return v === undefined || v === null ? "" : String(v) + default: + return v === undefined || v === null ? "" : String(v) + } +} + +const normalizeCellValue = (value: unknown, cell: CellObject | undefined): string => { + if (value === undefined || value === null || value === "") { + const fallback = normalizeFromCell(cell) + return fallback !== undefined ? fallback : "" + } + + if (typeof value === "number") { + return numberToPlainString(value) + } + + if (typeof value === "string") { + const trimmed = value.trim() + if (trimmed === "") { + const fallback = normalizeFromCell(cell) + return fallback !== undefined ? fallback : "" + } + return SCIENTIFIC_NOTATION_REGEX.test(trimmed) ? convertScientificToDecimalString(trimmed) : value + } + + if (typeof value === "boolean") { + return value ? "TRUE" : "FALSE" + } + + if (value instanceof Date) { + return value.toISOString() + } + + const fallback = normalizeFromCell(cell) + return fallback !== undefined ? fallback : String(value) +} + 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, { + const rangeRef = worksheet["!ref"] || "A1" + const sheetRange = XLSX.utils.decode_range(rangeRef) + + const sheetData = XLSX.utils.sheet_to_json(worksheet, { header: 1, - raw: false, + raw: true, defval: "", + blankrows: true, }) - return data + const columnCount = Math.max( + sheetRange.e.c - sheetRange.s.c + 1, + ...sheetData.map((row) => row.length), + ) + + return sheetData.map((row, rowIndex) => { + const sheetRow = sheetRange.s.r + rowIndex + const normalizedRow: string[] = [] + + for (let columnOffset = 0; columnOffset < columnCount; columnOffset++) { + const sheetColumn = sheetRange.s.c + columnOffset + const cellAddress = XLSX.utils.encode_cell({ r: sheetRow, c: sheetColumn }) + const cell = worksheet[cellAddress] as CellObject | undefined + const value = row[columnOffset] + normalizedRow.push(normalizeCellValue(value, cell)) + } + + return normalizedRow as RawData + }) }