diff --git a/src/libs/actions/ImportSpreadsheet.ts b/src/libs/actions/ImportSpreadsheet.ts index 0c1d31b888cf..5dc2fd8881ba 100644 --- a/src/libs/actions/ImportSpreadsheet.ts +++ b/src/libs/actions/ImportSpreadsheet.ts @@ -201,6 +201,38 @@ function applySavedColumnMappings(spreadsheetData: string[][], savedLayout: Save } } +/** + * Applies saved company card column mappings to the spreadsheet data. + * + * @param spreadsheetData - The spreadsheet data in column-major format + * @param savedColumnMappings - Saved mappings from uploadLayoutSettings, keyed by field role with a column index value + * @param availableColumnRoles - The field roles currently selectable in the mapping UI + */ +function applyCompanyCardSavedColumnMappings(spreadsheetData: string[][], savedColumnMappings: Record, availableColumnRoles: string[]): void { + if (!savedColumnMappings) { + return; + } + + const validRoles = new Set(availableColumnRoles); + const numColumns = spreadsheetData.length; + const columnUpdates: Record = {}; + + for (const [role, indexValue] of Object.entries(savedColumnMappings)) { + if (role === CONST.CSV_IMPORT_COLUMNS.IGNORE || !validRoles.has(role)) { + continue; + } + const index = Number(indexValue); + if (!Number.isInteger(index) || index < 0 || index >= numColumns) { + continue; + } + columnUpdates[index] = role; + } + + if (Object.keys(columnUpdates).length > 0) { + Onyx.merge(ONYXKEYS.IMPORTED_SPREADSHEET, {columns: columnUpdates}); + } +} + export { setSpreadsheetData, setColumnName, @@ -210,6 +242,7 @@ export { setImportTransactionCurrency, setImportTransactionSettings, applySavedColumnMappings, + applyCompanyCardSavedColumnMappings, getImportFailedFinalModal, getImportFinalModalID, getImportFinalModalOnyxData, diff --git a/src/pages/workspace/companyCards/addNew/CompanyCardsImportedPage.tsx b/src/pages/workspace/companyCards/addNew/CompanyCardsImportedPage.tsx index 4c43f593956f..1a6366485fd2 100644 --- a/src/pages/workspace/companyCards/addNew/CompanyCardsImportedPage.tsx +++ b/src/pages/workspace/companyCards/addNew/CompanyCardsImportedPage.tsx @@ -9,6 +9,7 @@ import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; import usePolicy from '@hooks/usePolicy'; +import {applyCompanyCardSavedColumnMappings} from '@libs/actions/ImportSpreadsheet'; import {getCSVFeedType} from '@libs/CardUtils'; import {findDuplicate, generateColumnNames} from '@libs/importSpreadsheetUtils'; import Navigation from '@libs/Navigation/Navigation'; @@ -30,7 +31,7 @@ import type {Errors} from '@src/types/onyx/OnyxCommon'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import isLoadingOnyxValue from '@src/types/utils/isLoadingOnyxValue'; -import React, {useMemo, useState} from 'react'; +import React, {useEffect, useMemo, useRef, useState} from 'react'; type CompanyCardsImportedPageProps = PlatformStackScreenProps; @@ -83,6 +84,35 @@ function CompanyCardsImportedPage({route}: CompanyCardsImportedPageProps) { return [...baseRoles, ...advancedRoles]; })(); + const savedColumnMappings = Object.entries(workspaceCardFeeds?.settings?.companyCards ?? {}).find(([feedKey]) => feedKey === layoutType)?.[1]?.uploadLayoutSettings?.columnMappings; + const hasAppliedSavedMappings = useRef(false); + const lastProcessedDataRef = useRef(spreadsheet?.data); + const lastAdvancedFieldsRef = useRef(shouldUseAdvancedFields); + + useEffect(() => { + // Reset the flag when new spreadsheet data is loaded, or when the set of selectable roles changes. + if (spreadsheet?.data !== lastProcessedDataRef.current || shouldUseAdvancedFields !== lastAdvancedFieldsRef.current) { + hasAppliedSavedMappings.current = false; + lastProcessedDataRef.current = spreadsheet?.data; + lastAdvancedFieldsRef.current = shouldUseAdvancedFields; + } + + if (hasAppliedSavedMappings.current) { + return; + } + + if (!spreadsheet?.data || isEmptyObject(savedColumnMappings)) { + return; + } + + hasAppliedSavedMappings.current = true; + applyCompanyCardSavedColumnMappings( + spreadsheet.data, + savedColumnMappings, + columnRoles.map((role) => role.value), + ); + }, [spreadsheet?.data, savedColumnMappings, columnRoles, shouldUseAdvancedFields]); + const requiredColumns = columnRoles.filter((role) => role.isRequired); const validate = () => { diff --git a/tests/unit/ImportTransactions.test.ts b/tests/unit/ImportTransactions.test.ts index c91d4548ca4f..17d85e737155 100644 --- a/tests/unit/ImportTransactions.test.ts +++ b/tests/unit/ImportTransactions.test.ts @@ -1,4 +1,4 @@ -import {applySavedColumnMappings, getImportFinalModalOnyxData} from '@libs/actions/ImportSpreadsheet'; +import {applyCompanyCardSavedColumnMappings, applySavedColumnMappings, getImportFinalModalOnyxData} from '@libs/actions/ImportSpreadsheet'; import importTransactionsFromCSV, {buildColumnLayout, buildTransactionListFromSpreadsheet, getColumnIndexes} from '@libs/actions/ImportTransactions'; import * as API from '@libs/API'; @@ -776,6 +776,98 @@ describe('ImportTransactions', () => { }); }); + describe('applyCompanyCardSavedColumnMappings', () => { + const availableRoles = [ + CONST.CSV_IMPORT_COLUMNS.IGNORE, + CONST.CSV_IMPORT_COLUMNS.CARD_NUMBER, + CONST.CSV_IMPORT_COLUMNS.POSTED_DATE, + CONST.CSV_IMPORT_COLUMNS.MERCHANT, + CONST.CSV_IMPORT_COLUMNS.AMOUNT, + CONST.CSV_IMPORT_COLUMNS.CURRENCY, + ]; + + // Column-major spreadsheet data (each entry is a column) + const spreadsheetData = [ + ['Card', '4111', '4111'], + ['Posted', '2024-01-01', '2024-01-02'], + ['Merchant', 'Store A', 'Store B'], + ['Amount', '10.00', '20.00'], + ['Currency', 'USD', 'USD'], + ]; + + it('should apply saved company card column mappings by index', () => { + const savedColumnMappings = { + cardNumber: '0', + postedDate: '1', + merchant: '2', + amount: '3', + currency: '4', + }; + + applyCompanyCardSavedColumnMappings(spreadsheetData, savedColumnMappings, availableRoles); + + expect(Onyx.merge).toHaveBeenCalledWith(ONYXKEYS.IMPORTED_SPREADSHEET, { + columns: { + 0: CONST.CSV_IMPORT_COLUMNS.CARD_NUMBER, + 1: CONST.CSV_IMPORT_COLUMNS.POSTED_DATE, + 2: CONST.CSV_IMPORT_COLUMNS.MERCHANT, + 3: CONST.CSV_IMPORT_COLUMNS.AMOUNT, + 4: CONST.CSV_IMPORT_COLUMNS.CURRENCY, + }, + }); + }); + + it('should skip roles that are not selectable in the current context', () => { + const savedColumnMappings = { + cardNumber: '0', + merchant: '2', + amount: '3', + // Advanced field not available when advanced fields are disabled + originalAmount: '1', + // Auto-generated column that is never a selectable role + externalID: '4', + }; + + applyCompanyCardSavedColumnMappings(spreadsheetData, savedColumnMappings, availableRoles); + + expect(Onyx.merge).toHaveBeenCalledWith(ONYXKEYS.IMPORTED_SPREADSHEET, { + columns: { + 0: CONST.CSV_IMPORT_COLUMNS.CARD_NUMBER, + 2: CONST.CSV_IMPORT_COLUMNS.MERCHANT, + 3: CONST.CSV_IMPORT_COLUMNS.AMOUNT, + }, + }); + }); + + it('should skip the ignore role and out-of-range indexes', () => { + const savedColumnMappings = { + ignore: '0', + merchant: '2', + amount: '99', + currency: '-1', + }; + + applyCompanyCardSavedColumnMappings(spreadsheetData, savedColumnMappings, availableRoles); + + expect(Onyx.merge).toHaveBeenCalledWith(ONYXKEYS.IMPORTED_SPREADSHEET, { + columns: { + 2: CONST.CSV_IMPORT_COLUMNS.MERCHANT, + }, + }); + }); + + it('should not call Onyx.merge when no columns can be mapped', () => { + const savedColumnMappings = { + originalAmount: '1', + originalCurrency: '4', + }; + + applyCompanyCardSavedColumnMappings(spreadsheetData, savedColumnMappings, availableRoles); + + expect(Onyx.merge).not.toHaveBeenCalled(); + }); + }); + describe('importTransactionsFromCSV', () => { const CURRENT_USER_ACCOUNT_ID = 12345; const validSpreadsheet = {