mirror of
https://github.com/actualbudget/actual.git
synced 2026-03-11 12:43:09 -05:00
Add option to calculate running balances in useTransactions hook (#4987)
* Add option to calculate running balances in useTransactions hook * Fix typecheck error * Fix lint error * Cleanup * retrigger checks
This commit is contained in:
committed by
GitHub
parent
45ff94590b
commit
d9dd96d0de
@@ -7,7 +7,7 @@ import debounce from 'lodash/debounce';
|
||||
|
||||
import { send } from '../../platform/client/fetch';
|
||||
import { currentDay, addDays, parseDate } from '../../shared/months';
|
||||
import { type Query } from '../../shared/query';
|
||||
import { type QueryState, type Query } from '../../shared/query';
|
||||
import {
|
||||
getScheduledAmount,
|
||||
extractScheduleConds,
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
scheduleIsRecurring,
|
||||
} from '../../shared/schedules';
|
||||
import { ungroupTransactions } from '../../shared/transactions';
|
||||
import { type IntegerAmount } from '../../shared/util';
|
||||
import {
|
||||
type ScheduleEntity,
|
||||
type TransactionEntity,
|
||||
@@ -31,23 +32,77 @@ type UseTransactionsProps = {
|
||||
* to prevent unnecessary re-renders i.e. `useMemo`, `useState`, etc.
|
||||
*/
|
||||
query?: Query;
|
||||
/**
|
||||
* The options to configure the hook behavior.
|
||||
*/
|
||||
options?: {
|
||||
/**
|
||||
* The number of transactions to load at a time.
|
||||
* This is used for pagination and should be set to a reasonable number
|
||||
* to avoid loading too many transactions at once.
|
||||
* The default is 50.
|
||||
* @default 50
|
||||
*/
|
||||
pageCount?: number;
|
||||
/**
|
||||
* Whether to calculate running balances for the transactions returned by the query.
|
||||
* This can be set to `true` to calculate running balances for all transactions
|
||||
* (using the default running balance calculation), or a function that takes the
|
||||
* transactions and the query state and returns a map of transaction IDs to running balances.
|
||||
* The function will be called with the transactions and the query state
|
||||
* whenever the transactions are loaded or reloaded.
|
||||
*
|
||||
* The default running balance calculation is a simple sum of the transaction amounts
|
||||
* in reverse order (bottom up). This works well if the transactions are ordered by
|
||||
* date in descending order. If the query orders the transactions differently,
|
||||
* a custom `calculateRunningBalances` function should be used instead.
|
||||
* @default false
|
||||
*/
|
||||
calculateRunningBalances?:
|
||||
| ((
|
||||
transactions: TransactionEntity[],
|
||||
queryState: QueryState,
|
||||
) => Map<TransactionEntity['id'], IntegerAmount>)
|
||||
| boolean;
|
||||
};
|
||||
};
|
||||
|
||||
type UseTransactionsResult = {
|
||||
/**
|
||||
* The transactions returned by the query.
|
||||
*/
|
||||
transactions: ReadonlyArray<TransactionEntity>;
|
||||
/**
|
||||
* The running balances for the transactions returned by the query.
|
||||
* This is only populated if `calculateRunningBalances` is either set to `true`
|
||||
* or a function that implements the calculation in the options.
|
||||
*/
|
||||
runningBalances: Map<TransactionEntity['id'], IntegerAmount>;
|
||||
/**
|
||||
* Whether the transactions are currently being loaded.
|
||||
*/
|
||||
isLoading: boolean;
|
||||
/**
|
||||
* An error that occurred while loading the transactions.
|
||||
*/
|
||||
error?: Error;
|
||||
/**
|
||||
* Reload the transactions.
|
||||
*/
|
||||
reload: () => void;
|
||||
/**
|
||||
* Load more transactions.
|
||||
*/
|
||||
loadMore: () => void;
|
||||
/**
|
||||
* Whether more transactions are currently being loaded.
|
||||
*/
|
||||
isLoadingMore: boolean;
|
||||
};
|
||||
|
||||
export function useTransactions({
|
||||
query,
|
||||
options = { pageCount: 50 },
|
||||
options = { pageCount: 50, calculateRunningBalances: false },
|
||||
}: UseTransactionsProps): UseTransactionsResult {
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [isLoadingMore, setIsLoadingMore] = useState(false);
|
||||
@@ -55,6 +110,9 @@ export function useTransactions({
|
||||
const [transactions, setTransactions] = useState<
|
||||
ReadonlyArray<TransactionEntity>
|
||||
>([]);
|
||||
const [runningBalances, setRunningBalances] = useState<
|
||||
Map<TransactionEntity['id'], IntegerAmount>
|
||||
>(new Map());
|
||||
|
||||
const pagedQueryRef = useRef<PagedQuery<TransactionEntity> | null>(null);
|
||||
|
||||
@@ -91,6 +149,12 @@ export function useTransactions({
|
||||
onData: data => {
|
||||
if (!isUnmounted) {
|
||||
setTransactions(data);
|
||||
|
||||
const calculateFn = getCalculateRunningBalancesFn(optionsRef.current);
|
||||
if (calculateFn) {
|
||||
setRunningBalances(calculateFn(data, query.state));
|
||||
}
|
||||
|
||||
setIsLoading(false);
|
||||
}
|
||||
},
|
||||
@@ -127,6 +191,7 @@ export function useTransactions({
|
||||
|
||||
return {
|
||||
transactions,
|
||||
runningBalances,
|
||||
isLoading,
|
||||
...(error && { error }),
|
||||
reload,
|
||||
@@ -344,3 +409,52 @@ function isForPreview(schedule: ScheduleEntity, statuses: ScheduleStatuses) {
|
||||
['due', 'upcoming', 'missed', 'paid'].includes(status!)
|
||||
);
|
||||
}
|
||||
|
||||
function getCalculateRunningBalancesFn(
|
||||
options: UseTransactionsProps['options'],
|
||||
) {
|
||||
const calculateRunningBalances = options?.calculateRunningBalances ?? false;
|
||||
return calculateRunningBalances === true
|
||||
? calculateRunningBalancesBottomUp
|
||||
: typeof calculateRunningBalances === 'function'
|
||||
? calculateRunningBalances
|
||||
: undefined;
|
||||
}
|
||||
|
||||
function calculateRunningBalancesBottomUp(
|
||||
transactions: TransactionEntity[],
|
||||
queryState: QueryState,
|
||||
) {
|
||||
return (
|
||||
transactions
|
||||
.filter(t => {
|
||||
const splits = queryState.tableOptions?.splits;
|
||||
switch (splits) {
|
||||
case 'all':
|
||||
// Only calculate parent/non-split amounts
|
||||
return !t.parent_id;
|
||||
default:
|
||||
// inline
|
||||
// grouped
|
||||
// none
|
||||
return true;
|
||||
}
|
||||
})
|
||||
// We're using `reduceRight` here to calculate the running balance in reverse order (bottom up).
|
||||
.reduceRight((acc, transaction, index, arr) => {
|
||||
const previousTransactionIndex = index + 1;
|
||||
if (previousTransactionIndex >= arr.length) {
|
||||
// This is the last transaction in the list,
|
||||
// so we set the running balance to the amount of the transaction
|
||||
acc.set(transaction.id, transaction.amount);
|
||||
return acc;
|
||||
}
|
||||
const previousTransaction = arr[previousTransactionIndex];
|
||||
const previousRunningBalance = acc.get(previousTransaction.id) ?? 0;
|
||||
const currentRunningBalance =
|
||||
previousRunningBalance + transaction.amount;
|
||||
acc.set(transaction.id, currentRunningBalance);
|
||||
return acc;
|
||||
}, new Map<TransactionEntity['id'], IntegerAmount>())
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { IntegerAmount } from '../../shared/util';
|
||||
|
||||
import { AccountEntity } from './account';
|
||||
import { CategoryEntity } from './category';
|
||||
import { PayeeEntity } from './payee';
|
||||
@@ -10,7 +12,7 @@ export interface TransactionEntity {
|
||||
parent_id?: TransactionEntity['id'];
|
||||
account: AccountEntity['id'];
|
||||
category?: CategoryEntity['id'];
|
||||
amount: number;
|
||||
amount: IntegerAmount;
|
||||
payee?: PayeeEntity['id'];
|
||||
notes?: string;
|
||||
date: string;
|
||||
|
||||
6
upcoming-release-notes/4987.md
Normal file
6
upcoming-release-notes/4987.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
category: Enhancements
|
||||
authors: [joel-jeremy]
|
||||
---
|
||||
|
||||
Add option to calculate running balances in useTransactions hook
|
||||
Reference in New Issue
Block a user