Skip to content

Commit

Permalink
Filtering transaction list based on selected accounts
Browse files Browse the repository at this point in the history
  • Loading branch information
tgrosinger committed Dec 18, 2021
1 parent 28abe97 commit afddd19
Show file tree
Hide file tree
Showing 6 changed files with 211 additions and 14 deletions.
17 changes: 15 additions & 2 deletions src/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ export interface Expenseline {
amount?: number;
currency?: string;
account: string;
dealiasedAccount?: string;
reconcile?: '' | '*' | '!';
comment?: string;
id?: number;
Expand Down Expand Up @@ -160,6 +161,20 @@ export const parse = (
}
});

const aliasMap = parseAliases(aliases);

txs.forEach((tx) => {
tx.value.expenselines.forEach((line) => {
if (!line.account || line.account === '') {
return;
}
const dealiasedAccount = dealiasAccount(line.account, aliasMap);
if (dealiasedAccount !== line.account) {
line.dealiasedAccount = dealiasedAccount;
}
});
});

const payees = sortedUniq(
txs
.map(({ value }) => value.payee)
Expand All @@ -171,8 +186,6 @@ export const parse = (
).sort((a, b) => (a.toLowerCase() > b.toLowerCase() ? 1 : -1)),
);

const aliasMap = parseAliases(aliases);

const assetAccounts: string[] = [];
const expenseAccounts: string[] = [];
const incomeAccounts: string[] = [];
Expand Down
32 changes: 32 additions & 0 deletions src/transaction-utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Transaction } from './parser';
import { some } from 'lodash';

/**
* getTotal returns the total value of the transaction. It assumes that all
Expand Down Expand Up @@ -45,6 +46,37 @@ export const getCurrency = (
return defaultCurrency;
};

export type Filter = (tx: Transaction) => boolean;

/**
* filterByAccount accepts an account name and attempts to match to
* transactions. Checks both account name an dealiased acocunt name.
*/
export const filterByAccount =
(account: string): Filter =>
(tx: Transaction): boolean =>
some(
tx.value.expenselines,
(line) =>
(line.account && line.account.startsWith(account)) ||
(line.dealiasedAccount && line.dealiasedAccount.startsWith(account)),
);

export const filterByPayeeExact =
(account: string): Filter =>
(tx: Transaction): boolean =>
tx.value.payee === account;

/**
* filterTransactions filters the provided transactions if _any_ of the provided
* filters match. To _and_ filters, apply this function sequentially.
*/
export const filterTransactions = (
txs: Transaction[],
...filters: Filter[]
): Transaction[] =>
filters.length > 0 ? txs.filter((tx) => some(filters, (fn) => fn(tx))) : txs;

export const dealiasAccount = (
account: string,
aliases: Map<string, string>,
Expand Down
3 changes: 2 additions & 1 deletion src/ui/LedgerDashboard.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { TransactionCache } from '../parser';
import { AccountsList } from './AccountsList';
import { AccountVisualization } from './AccountVisualization';
import { MobileTransactionList, TransactionList } from './TransactionPage';
import { MobileTransactionList, TransactionList } from './TransactionList';
import { Platform } from 'obsidian';
import React from 'react';
import styled from 'styled-components';
Expand Down Expand Up @@ -97,6 +97,7 @@ const DesktopDashboard: React.FC<{
<TransactionList
currencySymbol={props.currencySymbol}
txCache={props.txCache}
selectedAccounts={selectedAccounts}
setSelectedAccount={(account: string) =>
setSelectedAccounts([account])
}
Expand Down
30 changes: 19 additions & 11 deletions src/ui/TransactionPage.tsx → src/ui/TransactionList.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import { Transaction, TransactionCache } from '../parser';
import { getTotal } from '../transaction-utils';
import {
filterByAccount,
filterTransactions,
getTotal,
} from '../transaction-utils';
import React from 'react';
import { Column, useSortBy, useTable } from 'react-table';
import { Column, useFilters, useSortBy, useTable } from 'react-table';
import styled from 'styled-components';

export const MobileTransactionList: React.FC<{
Expand Down Expand Up @@ -70,7 +74,7 @@ const TableStyles = styled.div`
`;

const buildTableRows = (
txCache: TransactionCache,
transactions: Transaction[],
currencySymbol: string,
): {
date: string;
Expand All @@ -79,7 +83,7 @@ const buildTableRows = (
from: string;
to: string;
}[] =>
txCache.transactions.map((tx: Transaction) => {
transactions.map((tx: Transaction) => {
if (tx.value.expenselines.length === 2) {
// If there are only two lines, then this is a simple 'from->to' transaction
return {
Expand All @@ -91,11 +95,10 @@ const buildTableRows = (
};
}
// Otherwise, there are multiple 'to' lines to consider

return {
date: tx.value.date,
payee: tx.value.payee,
total: '---',
total: getTotal(tx, currencySymbol),
from: '---',
to: '---',
};
Expand All @@ -104,12 +107,17 @@ const buildTableRows = (
export const TransactionList: React.FC<{
currencySymbol: string;
txCache: TransactionCache;
selectedAccounts: string[];
setSelectedAccount: (accountName: string) => void;
}> = (props): JSX.Element => {
const data = React.useMemo(
() => buildTableRows(props.txCache, props.currencySymbol),
[props.txCache],
);
const data = React.useMemo(() => {
const filters = props.selectedAccounts.map((a) => filterByAccount(a));
const filteredTransactions = filterTransactions(
props.txCache.transactions,
...filters,
);
return buildTableRows(filteredTransactions, props.currencySymbol);
}, [props.txCache, props.selectedAccounts]);
const columns = React.useMemo<Column[]>(
() => [
{
Expand All @@ -135,7 +143,7 @@ export const TransactionList: React.FC<{
],
[],
);
const tableInstance = useTable({ columns, data }, useSortBy);
const tableInstance = useTable({ columns, data }, useFilters, useSortBy);

const { getTableProps, getTableBodyProps, headerGroups, rows, prepareRow } =
tableInstance;
Expand Down
3 changes: 3 additions & 0 deletions tests/parser.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -519,12 +519,14 @@ alias b=Banking
expenselines: [
{
account: 'e:Spending Money',
dealiasedAccount: 'Expenses:Spending Money',
amount: 20,
currency: '$',
reconcile: '',
},
{
account: 'b:CreditUnion',
dealiasedAccount: 'Banking:CreditUnion',
reconcile: '',
},
],
Expand Down Expand Up @@ -574,6 +576,7 @@ alias b=Banking
expenselines: [
{
account: 'e:Spending Money',
dealiasedAccount: 'Expenses:Spending Money',
amount: 20,
currency: '$',
reconcile: '',
Expand Down
140 changes: 140 additions & 0 deletions tests/transaction-utils.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { Transaction } from '../src/parser';
import {
filterByAccount,
filterByPayeeExact,
filterTransactions,
getCurrency,
getTotal,
makeAccountTree,
Expand Down Expand Up @@ -311,3 +314,140 @@ describe('sortAccountTree()', () => {
expect(input).toEqual(expected);
});
});

describe('filterTransactions', () => {
const tx1: Transaction = {
type: 'tx',
value: {
date: '2021-12-31',
payee: 'Costco',
expenselines: [
{
account: 'e:Spending Money',
dealiasedAccount: 'Expenses:Spending Money',
amount: 100,
currency: '$',
},
{
account: 'c:Citi',
dealiasedAccount: 'Credit:City',
},
],
},
};
const tx2: Transaction = {
type: 'tx',
value: {
date: '2021-12-30',
payee: "Trader Joe's",
expenselines: [
{
account: 'e:Food:Grocery',
dealiasedAccount: 'Expenses:Food:Grocery',
amount: 120,
currency: '$',
},
{
account: 'c:Citi',
dealiasedAccount: 'Credit:City',
},
],
},
};
const tx3: Transaction = {
type: 'tx',
value: {
date: '2021-12-29',
payee: 'PCC',
expenselines: [
{
account: 'e:Food:Grocery',
dealiasedAccount: 'Expenses:Food:Grocery',
amount: 20,
currency: '$',
},
{
account: 'c:Citi',
dealiasedAccount: 'Credit:City',
},
],
},
};
test('When there are no filters', () => {
const input = [tx1, tx2, tx3];
const result = filterTransactions(input);
expect(result).toEqual(input);
});

describe('filterByAccount', () => {
test('When the account matches', () => {
const input = [tx1, tx2, tx3];
const result = filterTransactions(
input,
filterByAccount('e:Spending Money'),
);
expect(result).toEqual([tx1]);
});
test('When the no accounts match', () => {
const input = [tx1, tx2, tx3];
const result = filterTransactions(
input,
filterByAccount('e:House:Maintenance'),
);
expect(result).toEqual([]);
});
test('When there are multiple matches', () => {
const input = [tx1, tx2, tx3];
const result = filterTransactions(
input,
filterByAccount('e:Food:Grocery'),
);
expect(result).toEqual([tx2, tx3]);
});
test('When filtering by dealiased account name', () => {
const input = [tx1, tx2, tx3];
const result = filterTransactions(
input,
filterByAccount('Expenses:Food:Grocery'),
);
expect(result).toEqual([tx2, tx3]);
});
});

describe('filterByPayee', () => {
test('When the payee matches', () => {
const input = [tx1, tx2, tx3];
const result = filterTransactions(input, filterByPayeeExact('Costco'));
expect(result).toEqual([tx1]);
});
test('When there are no matches', () => {
const input = [tx1, tx2, tx3];
const result = filterTransactions(
input,
filterByPayeeExact('Home Depot'),
);
expect(result).toEqual([]);
});
});

describe('mutliple filters', () => {
test('When the payee and account match different transactions', () => {
const input = [tx1, tx2, tx3];
const result = filterTransactions(
input,
filterByPayeeExact('PCC'),
filterByAccount('e:Spending Money'),
);
expect(result).toEqual([tx1, tx3]);
});
test('When matching multiple of the same filter', () => {
const input = [tx1, tx2, tx3];
const result = filterTransactions(
input,
filterByPayeeExact('PCC'),
filterByPayeeExact("Trader Joe's"),
);
expect(result).toEqual([tx2, tx3]);
});
});
});

0 comments on commit afddd19

Please sign in to comment.