diff --git a/packages/desktop-client/e2e/accounts.test.ts-snapshots/Accounts-Import-Transactions-import-csv-file-twice-1-chromium-linux.png b/packages/desktop-client/e2e/accounts.test.ts-snapshots/Accounts-Import-Transactions-import-csv-file-twice-1-chromium-linux.png index e763c369d1..b87216e170 100644 Binary files a/packages/desktop-client/e2e/accounts.test.ts-snapshots/Accounts-Import-Transactions-import-csv-file-twice-1-chromium-linux.png and b/packages/desktop-client/e2e/accounts.test.ts-snapshots/Accounts-Import-Transactions-import-csv-file-twice-1-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/accounts.test.ts-snapshots/Accounts-Import-Transactions-import-csv-file-twice-2-chromium-linux.png b/packages/desktop-client/e2e/accounts.test.ts-snapshots/Accounts-Import-Transactions-import-csv-file-twice-2-chromium-linux.png index 9e1bc4bb38..0bbdd6f874 100644 Binary files a/packages/desktop-client/e2e/accounts.test.ts-snapshots/Accounts-Import-Transactions-import-csv-file-twice-2-chromium-linux.png and b/packages/desktop-client/e2e/accounts.test.ts-snapshots/Accounts-Import-Transactions-import-csv-file-twice-2-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/accounts.test.ts-snapshots/Accounts-Import-Transactions-import-csv-file-twice-3-chromium-linux.png b/packages/desktop-client/e2e/accounts.test.ts-snapshots/Accounts-Import-Transactions-import-csv-file-twice-3-chromium-linux.png index 7bce2d32cd..ef908bed7b 100644 Binary files a/packages/desktop-client/e2e/accounts.test.ts-snapshots/Accounts-Import-Transactions-import-csv-file-twice-3-chromium-linux.png and b/packages/desktop-client/e2e/accounts.test.ts-snapshots/Accounts-Import-Transactions-import-csv-file-twice-3-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/accounts.test.ts-snapshots/Accounts-Import-Transactions-imports-transactions-from-a-CSV-file-1-chromium-linux.png b/packages/desktop-client/e2e/accounts.test.ts-snapshots/Accounts-Import-Transactions-imports-transactions-from-a-CSV-file-1-chromium-linux.png index 4377d70a13..2e10d31630 100644 Binary files a/packages/desktop-client/e2e/accounts.test.ts-snapshots/Accounts-Import-Transactions-imports-transactions-from-a-CSV-file-1-chromium-linux.png and b/packages/desktop-client/e2e/accounts.test.ts-snapshots/Accounts-Import-Transactions-imports-transactions-from-a-CSV-file-1-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/accounts.test.ts-snapshots/Accounts-Import-Transactions-imports-transactions-from-a-CSV-file-2-chromium-linux.png b/packages/desktop-client/e2e/accounts.test.ts-snapshots/Accounts-Import-Transactions-imports-transactions-from-a-CSV-file-2-chromium-linux.png index a1c0728484..6029069c52 100644 Binary files a/packages/desktop-client/e2e/accounts.test.ts-snapshots/Accounts-Import-Transactions-imports-transactions-from-a-CSV-file-2-chromium-linux.png and b/packages/desktop-client/e2e/accounts.test.ts-snapshots/Accounts-Import-Transactions-imports-transactions-from-a-CSV-file-2-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/accounts.test.ts-snapshots/Accounts-Import-Transactions-imports-transactions-from-a-CSV-file-3-chromium-linux.png b/packages/desktop-client/e2e/accounts.test.ts-snapshots/Accounts-Import-Transactions-imports-transactions-from-a-CSV-file-3-chromium-linux.png index a180979d65..794295124d 100644 Binary files a/packages/desktop-client/e2e/accounts.test.ts-snapshots/Accounts-Import-Transactions-imports-transactions-from-a-CSV-file-3-chromium-linux.png and b/packages/desktop-client/e2e/accounts.test.ts-snapshots/Accounts-Import-Transactions-imports-transactions-from-a-CSV-file-3-chromium-linux.png differ diff --git a/packages/desktop-client/src/components/modals/ImportTransactionsModal/ImportTransactionsModal.tsx b/packages/desktop-client/src/components/modals/ImportTransactionsModal/ImportTransactionsModal.tsx index 2c3d16064f..c41d0419a7 100644 --- a/packages/desktop-client/src/components/modals/ImportTransactionsModal/ImportTransactionsModal.tsx +++ b/packages/desktop-client/src/components/modals/ImportTransactionsModal/ImportTransactionsModal.tsx @@ -25,6 +25,7 @@ import { Transaction } from './Transaction'; import { applyFieldMappings, dateFormats, + filterByStartDate, isDateFormat, parseAmountFields, parseDate, @@ -218,6 +219,7 @@ export function ImportTransactionsModal({ ); const [clearOnImport, setClearOnImport] = useState(true); + const [startDate, setStartDate] = useState(''); const getImportPreview = useCallback( async ( @@ -672,9 +674,19 @@ export function ImportTransactionsModal({ const importPreviewTransactions = useImportPreviewTransactionsMutation(); const onImportPreview = useEffectEvent(async () => { + // Filter by start date before preview and deduplication + const isPreParsed = isOfxFile(filetype) || isCamtFile(filetype); + const filteredTransactions = filterByStartDate( + parsedTransactions, + startDate, + isPreParsed, + fieldMappings, + parseDateFormat, + ); + // always start from the original parsed transactions, not the previewed ones to ensure rules run const previewTransactionsToImport = await getImportPreview( - parsedTransactions, + filteredTransactions, filetype, flipAmount, fieldMappings, @@ -699,7 +711,7 @@ export function ImportTransactionsModal({ return map; }, {}); - const previewTransactions = parsedTransactions + const previewTransactions = filteredTransactions .filter(trans => !trans.isMatchedTransaction) .reduce((previous, currentTrx) => { let next = previous; @@ -750,7 +762,13 @@ export function ImportTransactionsModal({ } void onImportPreview(); - }, [loadingState, parsedTransactions.length]); + }, [ + loadingState, + parsedTransactions.length, + startDate, + fieldMappings, + parseDateFormat, + ]); const headers: ComponentProps['headers'] = [ { name: t('Date'), width: 200 }, @@ -884,6 +902,39 @@ export function ImportTransactionsModal({ )} + + + {startDate && ( + + )} + + {filetype === 'csv' && ( { describe('date parsing', () => { @@ -164,4 +165,121 @@ describe('Import transactions', () => { }, ); }); + + describe('filterByStartDate', () => { + function makeTrans( + overrides: Partial, + ): ImportTransaction { + return { + trx_id: '0', + existing: false, + ignored: false, + selected: true, + selected_merge: false, + amount: 0, + inflow: 0, + outflow: 0, + inOut: '', + ...overrides, + }; + } + + it('returns all transactions when startDate is empty', () => { + const transactions = [ + makeTrans({ trx_id: '0', date: '2024-01-15' }), + makeTrans({ trx_id: '1', date: '2024-02-20' }), + ]; + const result = filterByStartDate(transactions, '', true, null, null); + expect(result).toHaveLength(2); + }); + + it('filters out transactions before startDate for pre-parsed dates', () => { + const transactions = [ + makeTrans({ trx_id: '0', date: '2024-01-15' }), + makeTrans({ trx_id: '1', date: '2024-02-20' }), + makeTrans({ trx_id: '2', date: '2024-03-01' }), + ]; + const result = filterByStartDate( + transactions, + '2024-02-01', + true, + null, + null, + ); + expect(result).toHaveLength(2); + expect(result.map(t => t.trx_id)).toEqual(['1', '2']); + }); + + it('includes transactions on the exact startDate', () => { + const transactions = [makeTrans({ trx_id: '0', date: '2024-02-01' })]; + const result = filterByStartDate( + transactions, + '2024-02-01', + true, + null, + null, + ); + expect(result).toHaveLength(1); + }); + + it('keeps transactions with unparseable dates', () => { + const transactions = [makeTrans({ trx_id: '0', date: 'invalid' })]; + const result = filterByStartDate( + transactions, + '2024-02-01', + false, + null, + 'mm dd yyyy', + ); + expect(result).toHaveLength(1); + }); + + it('works with CSV dates requiring date format parsing', () => { + const transactions = [ + makeTrans({ trx_id: '0', date: '01/15/2024' }), + makeTrans({ trx_id: '1', date: '03/01/2024' }), + ]; + const result = filterByStartDate( + transactions, + '2024-02-01', + false, + null, + 'mm dd yyyy', + ); + expect(result).toHaveLength(1); + expect(result[0].trx_id).toBe('1'); + }); + + it('works with field mappings for CSV', () => { + const transactions = [ + makeTrans({ + trx_id: '0', + Date: '01/15/2024', + } satisfies Partial), + makeTrans({ + trx_id: '1', + Date: '03/01/2024', + } satisfies Partial), + ]; + const fieldMappings = { + date: 'Date', + amount: null, + payee: null, + notes: null, + inOut: null, + category: null, + outflow: null, + inflow: null, + }; + const result = filterByStartDate( + transactions, + '2024-02-01', + false, + fieldMappings, + 'mm dd yyyy', + ); + expect(result).toHaveLength(1); + expect(result[0].trx_id).toBe('1'); + }); + }); }); diff --git a/packages/desktop-client/src/components/modals/ImportTransactionsModal/utils.ts b/packages/desktop-client/src/components/modals/ImportTransactionsModal/utils.ts index 53a58bd00c..de63939627 100644 --- a/packages/desktop-client/src/components/modals/ImportTransactionsModal/utils.ts +++ b/packages/desktop-client/src/components/modals/ImportTransactionsModal/utils.ts @@ -263,6 +263,28 @@ export function parseAmountFields( } } +export function filterByStartDate( + transactions: ImportTransaction[], + startDate: string, + isPreParsedDate: boolean, + fieldMappings: FieldMapping | null, + parseDateFormat: DateFormat | null, +): ImportTransaction[] { + if (!startDate) return transactions; + return transactions.filter(trans => { + const mapped = fieldMappings + ? applyFieldMappings(trans, fieldMappings) + : trans; + const date = isPreParsedDate + ? (mapped.date ?? null) + : parseDateFormat + ? parseDate(mapped.date ?? null, parseDateFormat) + : null; + // Keep transactions with unparseable dates (they'll error later in the normal flow) + return date == null || date >= startDate; + }); +} + export function stripCsvImportTransaction(transaction: ImportTransaction) { const { existing: _existing, diff --git a/upcoming-release-notes/7139.md b/upcoming-release-notes/7139.md new file mode 100644 index 0000000000..582a7b9d05 --- /dev/null +++ b/upcoming-release-notes/7139.md @@ -0,0 +1,6 @@ +--- +category: Enhancements +authors: [kivikakk] +--- + +Add "only import transactions since" to import dialog