Mobile running balance (#5219)

* start

* small fix

* clean

* working for regular transactions

* working for schedules

* cleanup

* typing

* cleanup

* cleanup

* vrt

* bunny

* use pref

* use pref right, lint

* more lint

* vrt

* pass hasInitialBalances to isLoading

* remove comment

* Add option to calculate running balances in useTransactions hook

* Fix typecheck error

* Fix lint error

* use the updated hook

* typecheck

* simplify

* don't show balances when searching

* Add runningBalances to usePreviewTransactions and an option to set the starting balance to start running balance calculation from

* Add filter to usePreviewTransactions and set startingBalance to account and category preview transaction hooks

* use runningbalance from preview transactions hook

* lint

* lint;typecheck

* remove initial from preview balances

* remove unneeded type

* Apply suggestions from code review

Co-authored-by: Joel Jeremy Marquez <joeljeremy.marquez@gmail.com>

* typecheck; align right; change color

* types

* add a menu item

* cleanup

* fix for loot-core migrated files

* lint;type

* fix import

* only schedules need fixed

* lint

* it works

* cleanup

* make lint happy

* [autofix.ci] apply automated fixes

* simplify a bit

* fix import

* feedback

* fixed regular transaction balance calculation

* fix numbers not showing

* fix schedule running balance

* note

* attempt to update properly

* type

* remove the useEffect that I don't think should be requred

* remove old note

* cleanup

* typeing

* I FINALLY FOUND THE PROBLEM

* cleaner balance calculation

* fixes

* fix zeros

---------

Co-authored-by: Joel Jeremy Marquez <joeljeremy.marquez@gmail.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
youngcw
2025-07-03 09:26:28 -07:00
committed by GitHub
parent 485830c859
commit a20805bfae
12 changed files with 226 additions and 144 deletions

View File

@@ -16,6 +16,7 @@ import { View } from '@actual-app/components/view';
import { listen, send } from 'loot-core/platform/client/fetch';
import { type Query } from 'loot-core/shared/query';
import { isPreviewId } from 'loot-core/shared/transactions';
import { type IntegerAmount } from 'loot-core/shared/util';
import {
type AccountEntity,
type TransactionEntity,
@@ -31,8 +32,9 @@ import { SchedulesProvider } from '@desktop-client/hooks/useCachedSchedules';
import { useDateFormat } from '@desktop-client/hooks/useDateFormat';
import { useFailedAccounts } from '@desktop-client/hooks/useFailedAccounts';
import { useNavigate } from '@desktop-client/hooks/useNavigate';
import { useTransactions } from '@desktop-client/hooks/usePreviewTransactions';
import { accountSchedulesQuery } from '@desktop-client/hooks/useSchedules';
import { useTransactions } from '@desktop-client/hooks/useTransactions';
import { useSyncedPref } from '@desktop-client/hooks/useSyncedPref';
import { useTransactionsSearch } from '@desktop-client/hooks/useTransactionsSearch';
import {
collapseModals,
@@ -148,6 +150,19 @@ function AccountHeader({ account }: { readonly account: AccountEntity }) {
dispatch(reopenAccount({ id: account.id }));
}, [account.id, dispatch]);
const [showBalances, setBalances] = useSyncedPref(
`show-balances-${account.id}`,
);
const onToggleRunningBalance = useCallback(() => {
const newVal = showBalances === 'true' ? 'false' : 'true';
setBalances(newVal);
dispatch(
collapseModals({
rootModalName: 'account-menu',
}),
);
}, [showBalances, setBalances, dispatch]);
const onClick = useCallback(() => {
dispatch(
pushModal({
@@ -159,6 +174,7 @@ function AccountHeader({ account }: { readonly account: AccountEntity }) {
onEditNotes,
onCloseAccount,
onReopenAccount,
onToggleRunningBalance,
},
},
}),
@@ -170,6 +186,7 @@ function AccountHeader({ account }: { readonly account: AccountEntity }) {
onEditNotes,
onReopenAccount,
onSave,
onToggleRunningBalance,
]);
return (
@@ -241,30 +258,6 @@ function TransactionListWithPreviews({
readonly accountName: AccountEntity['name'] | string;
}) {
const { t } = useTranslation();
const baseTransactionsQuery = useCallback(
() =>
queries.transactions(accountId).options({ splits: 'all' }).select('*'),
[accountId],
);
const [transactionsQuery, setTransactionsQuery] = useState<Query>(
baseTransactionsQuery(),
);
const {
transactions,
isLoading: isTransactionsLoading,
reload: reloadTransactions,
isLoadingMore,
loadMore: loadMoreTransactions,
} = useTransactions({
query: transactionsQuery,
});
const { previewTransactions, isLoading: isPreviewTransactionsLoading } =
useAccountPreviewTransactions({
accountId: account?.id,
});
const dateFormat = useDateFormat() || 'MM/dd/yyyy';
const dispatch = useDispatch();
const navigate = useNavigate();
@@ -275,6 +268,64 @@ function TransactionListWithPreviews({
}
}, [accountId, dispatch]);
const baseTransactionsQuery = useCallback(
() =>
queries.transactions(accountId).options({ splits: 'all' }).select('*'),
[accountId],
);
const runningBalancesQuery = useCallback(
() =>
queries
.transactions(accountId)
.options({ splits: 'none' })
.select({ balance: { $sumOver: '$amount' } }),
[accountId],
);
const [showBalances] = useSyncedPref(`show-balances-${accountId}`);
const [transactionsQuery, setTransactionsQuery] = useState<Query>(
baseTransactionsQuery(),
);
const [balancesQuery] = useState<Query>(runningBalancesQuery);
const {
transactions,
runningBalances,
isLoading: isTransactionsLoading,
reload: reloadTransactions,
isLoadingMore,
loadMore: loadMoreTransactions,
} = useTransactions({
query: transactionsQuery,
runningBalanceQuery: balancesQuery,
options: {
calculateRunningBalances: true,
},
});
const { isSearching, search: onSearch } = useTransactionsSearch({
updateQuery: setTransactionsQuery,
resetQuery: () => setTransactionsQuery(baseTransactionsQuery()),
dateFormat,
});
const {
previewTransactions,
runningBalances: previewRunningBalances,
isLoading: isPreviewTransactionsLoading,
} = useAccountPreviewTransactions({
accountId: account?.id,
});
const allBalances = useMemo(
() =>
new Map<TransactionEntity['id'], IntegerAmount>([
...previewRunningBalances,
...runningBalances,
]),
[runningBalances, previewRunningBalances],
);
useEffect(() => {
if (accountId) {
dispatch(markAccountRead({ id: accountId }));
@@ -296,12 +347,6 @@ function TransactionListWithPreviews({
});
}, [dispatch, reloadTransactions]);
const { isSearching, search: onSearch } = useTransactionsSearch({
updateQuery: setTransactionsQuery,
resetQuery: () => setTransactionsQuery(baseTransactionsQuery()),
dateFormat,
});
const onOpenTransaction = useCallback(
(transaction: TransactionEntity) => {
if (!isPreviewId(transaction.id)) {
@@ -370,6 +415,8 @@ function TransactionListWithPreviews({
balance={balanceQueries.balance}
balanceCleared={balanceQueries.cleared}
balanceUncleared={balanceQueries.uncleared}
runningBalances={allBalances}
showBalances={isSearching ? false : showBalances === 'true'}
isLoadingMore={isLoadingMore}
onLoadMore={loadMoreTransactions}
searchPlaceholder={t('Search {{accountName}}', { accountName })}

View File

@@ -154,6 +154,7 @@ function TransactionListWithPreviews({
balance={balance}
balanceCleared={balanceCleared}
balanceUncleared={balanceUncleared}
runningBalances={undefined}
searchPlaceholder={`Search ${category.name}`}
onSearch={onSearch}
isLoadingMore={isLoadingMore}

View File

@@ -32,7 +32,11 @@ import { View } from '@actual-app/components/view';
import * as monthUtils from 'loot-core/shared/months';
import { isPreviewId } from 'loot-core/shared/transactions';
import { validForTransfer } from 'loot-core/shared/transfer';
import { groupById, integerToCurrency } from 'loot-core/shared/util';
import {
groupById,
type IntegerAmount,
integerToCurrency,
} from 'loot-core/shared/util';
import {
type AccountEntity,
type TransactionEntity,
@@ -84,6 +88,8 @@ function Loading({ style, 'aria-label': ariaLabel }: LoadingProps) {
type TransactionListProps = {
isLoading: boolean;
transactions: readonly TransactionEntity[];
showBalances?: boolean;
runningBalances?: Map<TransactionEntity['id'], IntegerAmount>;
onOpenTransaction?: (transaction: TransactionEntity) => void;
isLoadingMore: boolean;
onLoadMore: () => void;
@@ -93,6 +99,8 @@ type TransactionListProps = {
export function TransactionList({
isLoading,
transactions,
showBalances,
runningBalances,
onOpenTransaction,
isLoadingMore,
onLoadMore,
@@ -198,10 +206,13 @@ export function TransactionList({
t => !isPreviewId(t.id) || !t.is_child,
)}
addIdAndValue
dependencies={[transactions, showBalances, runningBalances]}
>
{transaction => (
<TransactionListItem
key={transaction.id}
showBalance={showBalances}
balance={runningBalances?.get(transaction.id)}
value={transaction}
onPress={trans => onTransactionPress(trans)}
onLongPress={trans => onTransactionPress(trans, true)}

View File

@@ -30,7 +30,7 @@ import {
} from '@react-aria/interactions';
import { isPreviewId } from 'loot-core/shared/transactions';
import { integerToCurrency } from 'loot-core/shared/util';
import { type IntegerAmount, integerToCurrency } from 'loot-core/shared/util';
import {
type AccountEntity,
type TransactionEntity,
@@ -38,7 +38,10 @@ import {
import { lookupName, Status } from './TransactionEdit';
import { makeAmountFullStyle } from '@desktop-client/components/budget/util';
import {
makeAmountFullStyle,
makeBalanceAmountStyle,
} from '@desktop-client/components/budget/util';
import { useAccount } from '@desktop-client/hooks/useAccount';
import { useCachedSchedules } from '@desktop-client/hooks/useCachedSchedules';
import { useCategories } from '@desktop-client/hooks/useCategories';
@@ -73,11 +76,15 @@ const getScheduleIconStyle = ({ isPreview }: { isPreview: boolean }) => ({
type TransactionListItemProps = ComponentPropsWithoutRef<
typeof ListBoxItem<TransactionEntity>
> & {
showBalance?: boolean;
balance?: IntegerAmount;
onPress: (transaction: TransactionEntity) => void;
onLongPress: (transaction: TransactionEntity) => void;
};
export function TransactionListItem({
showBalance,
balance,
onPress,
onLongPress,
...props
@@ -282,7 +289,7 @@ export function TransactionListItem({
</TextOneLine>
)}
</View>
<View style={{ justifyContent: 'center' }}>
<View style={{ textAlign: 'right' }}>
<Text
style={{
...textStyle,
@@ -291,6 +298,17 @@ export function TransactionListItem({
>
{integerToCurrency(amount)}
</Text>
{showBalance && (
<Text
style={{
fontSize: 11,
fontWeight: '400',
...makeBalanceAmountStyle(balance || 0),
}}
>
{integerToCurrency(balance || 0)}
</Text>
)}
</View>
</View>
</Button>

View File

@@ -6,6 +6,7 @@ import { styles } from '@actual-app/components/styles';
import { theme } from '@actual-app/components/theme';
import { View } from '@actual-app/components/view';
import { type IntegerAmount } from 'loot-core/shared/util';
import {
type AccountEntity,
type TransactionEntity,
@@ -86,6 +87,8 @@ type TransactionListWithBalancesProps = {
balanceUncleared?:
| Binding<'category', 'balanceUncleared'>
| Binding<'account', 'balanceUncleared'>;
showBalances?: boolean;
runningBalances?: Map<TransactionEntity['id'], IntegerAmount>;
searchPlaceholder: string;
onSearch: (searchText: string) => void;
isLoadingMore: boolean;
@@ -101,6 +104,8 @@ export function TransactionListWithBalances({
balance,
balanceCleared,
balanceUncleared,
showBalances,
runningBalances,
searchPlaceholder = 'Search...',
onSearch,
isLoadingMore,
@@ -148,6 +153,8 @@ export function TransactionListWithBalances({
<TransactionList
isLoading={isLoading}
transactions={transactions}
showBalances={showBalances}
runningBalances={runningBalances}
isLoadingMore={isLoadingMore}
onLoadMore={onLoadMore}
onOpenTransaction={onOpenTransaction}

View File

@@ -33,6 +33,7 @@ import { validateAccountName } from '@desktop-client/components/util/accountVali
import { useAccount } from '@desktop-client/hooks/useAccount';
import { useAccounts } from '@desktop-client/hooks/useAccounts';
import { useNotes } from '@desktop-client/hooks/useNotes';
import { useSyncedPref } from '@desktop-client/hooks/useSyncedPref';
import { type Modal as ModalType } from '@desktop-client/modals/modalsSlice';
type AccountMenuModalProps = Extract<
@@ -47,6 +48,7 @@ export function AccountMenuModal({
onReopenAccount,
onEditNotes,
onClose,
onToggleRunningBalance,
}: AccountMenuModalProps) {
const { t } = useTranslation();
const account = useAccount(accountId);
@@ -124,6 +126,7 @@ export function AccountMenuModal({
account={account}
onClose={onCloseAccount}
onReopen={onReopenAccount}
onToggleRunningBalance={onToggleRunningBalance}
/>
}
title={
@@ -158,7 +161,7 @@ export function AccountMenuModal({
notes={
originalNotes && originalNotes.length > 0
? originalNotes
: 'No notes'
: t('No notes')
}
editable={false}
focused={false}
@@ -201,12 +204,14 @@ type AdditionalAccountMenuProps = {
account: AccountEntity;
onClose?: (accountId: string) => void;
onReopen?: (accountId: string) => void;
onToggleRunningBalance?: () => void;
};
function AdditionalAccountMenu({
account,
onClose,
onReopen,
onToggleRunningBalance,
}: AdditionalAccountMenuProps) {
const { t } = useTranslation();
const triggerRef = useRef(null);
@@ -220,6 +225,7 @@ function AdditionalAccountMenu({
...itemStyle,
...(item.name === 'close' && { color: theme.errorTextMenu }),
});
const [showBalances] = useSyncedPref(`show-balances-${account.id}`);
return (
<View>
@@ -245,6 +251,13 @@ function AdditionalAccountMenu({
<Menu
getItemStyle={getItemStyle}
items={[
{
name: 'balance',
text:
showBalances === 'true'
? t('Hide running balance')
: t('Show running balance'),
},
account.closed
? {
name: 'reopen',
@@ -268,6 +281,9 @@ function AdditionalAccountMenu({
case 'reopen':
onReopen?.(account.id);
break;
case 'balance':
onToggleRunningBalance?.();
break;
default:
throw new Error(`Unrecognized menu option: ${name}`);
}

View File

@@ -709,6 +709,7 @@ function CalendarInner({ widget, parameters }: CalendarInnerProps) {
onOpenTransaction={onOpenTransaction}
isLoadingMore={false}
account={undefined}
runningBalances={undefined}
/>
</View>
</animated.div>

View File

@@ -1,6 +1,6 @@
import { useCallback, useMemo } from 'react';
import { groupById } from 'loot-core/shared/util';
import { groupById, type IntegerAmount } from 'loot-core/shared/util';
import {
type ScheduleEntity,
type AccountEntity,
@@ -19,9 +19,15 @@ type UseAccountPreviewTransactionsProps = {
accountId?: AccountEntity['id'] | undefined;
};
type UseAccountPreviewTransactionsResult = ReturnType<
typeof usePreviewTransactions
>;
// Mirrors the `splits` AQL option from the server
type TransactionSplitsOption = 'all' | 'inline' | 'grouped' | 'none';
type UseAccountPreviewTransactionsResult = {
previewTransactions: ReadonlyArray<TransactionEntity>;
runningBalances: Map<TransactionEntity['id'], IntegerAmount>;
isLoading: boolean;
error?: Error;
};
/**
* Preview transactions for a given account. This will invert the payees, accounts,
@@ -70,21 +76,17 @@ export function useAccountPreviewTransactions({
const {
previewTransactions: allPreviewTransactions,
runningBalances: allRunningBalances,
isLoading,
error,
} = usePreviewTransactions({
filter: accountSchedulesFilter,
options: {
startingBalance: accountBalanceValue ?? 0,
},
});
return useMemo(() => {
if (!accountId) {
return {
previewTransactions: allPreviewTransactions,
runningBalances: allRunningBalances,
runningBalances: new Map(),
isLoading,
error,
};
@@ -97,6 +99,11 @@ export function useAccountPreviewTransactions({
getTransferAccountByPayee,
});
const allRunningBalances = calculateRunningBalancesBottomUp(
previewTransactions,
'all',
accountBalanceValue ?? 0,
);
const transactionIds = new Set(previewTransactions.map(t => t.id));
const runningBalances = allRunningBalances;
for (const transactionId of runningBalances.keys()) {
@@ -113,8 +120,8 @@ export function useAccountPreviewTransactions({
};
}, [
accountId,
accountBalanceValue,
allPreviewTransactions,
allRunningBalances,
error,
getPayeeByTransferAccount,
getTransferAccountByPayee,
@@ -164,3 +171,41 @@ function accountPreview({
};
});
}
export function calculateRunningBalancesBottomUp(
transactions: TransactionEntity[],
splits: TransactionSplitsOption,
startingBalance: IntegerAmount = 0,
) {
return (
transactions
.filter(t => {
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 starting balance + the amount of the transaction
acc.set(transaction.id, startingBalance + 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>())
);
}

View File

@@ -6,6 +6,7 @@ import {
type CategoryEntity,
} from 'loot-core/types/models';
import { calculateRunningBalancesBottomUp } from './useAccountPreviewTransactions';
import { useCategory } from './useCategory';
import { useCategoryScheduleGoalTemplates } from './useCategoryScheduleGoalTemplates';
import { usePreviewTransactions } from './usePreviewTransactions';
@@ -51,14 +52,10 @@ export function useCategoryPreviewTransactions({
const {
previewTransactions: allPreviewTransactions,
runningBalances: allRunningBalances,
isLoading,
error,
} = usePreviewTransactions({
filter: categorySchedulesFilter,
options: {
startingBalance: categoryBalanceValue ?? 0,
},
});
return useMemo(() => {
@@ -77,7 +74,11 @@ export function useCategoryPreviewTransactions({
);
const transactionIds = new Set(previewTransactions.map(t => t.id));
const runningBalances = allRunningBalances;
const runningBalances = calculateRunningBalancesBottomUp(
previewTransactions,
'all',
categoryBalanceValue ?? 0,
);
for (const transactionId of runningBalances.keys()) {
if (!transactionIds.has(transactionId)) {
runningBalances.delete(transactionId);
@@ -92,8 +93,8 @@ export function useCategoryPreviewTransactions({
};
}, [
allPreviewTransactions,
allRunningBalances,
category,
categoryBalanceValue,
error,
isLoading,
schedulesToPreview,

View File

@@ -23,6 +23,7 @@ import { useCachedSchedules } from './useCachedSchedules';
import { type ScheduleStatuses } from './useSchedules';
import { useSyncedPref } from './useSyncedPref';
import { aqlQuery } from '@desktop-client/queries/aqlQuery';
import {
pagedQuery,
type PagedQuery,
@@ -44,6 +45,10 @@ type UseTransactionsProps = {
* to prevent unnecessary re-renders i.e. `useMemo`, `useState`, etc.
*/
query?: Query;
/**
* Query to use to calculate the running balance
*/
runningBalanceQuery?: Query;
/**
* The options to configure the hook behavior.
*/
@@ -109,6 +114,7 @@ type UseTransactionsResult = {
export function useTransactions({
query,
runningBalanceQuery,
options = { pageCount: 50, calculateRunningBalances: false },
}: UseTransactionsProps): UseTransactionsResult {
const [isLoading, setIsLoading] = useState(true);
@@ -156,19 +162,6 @@ export function useTransactions({
onData: data => {
if (!isUnmounted) {
setTransactions(data);
const calculateFn = getCalculateRunningBalancesFn(
optionsRef.current?.calculateRunningBalances,
);
if (calculateFn) {
setRunningBalances(
calculateFn(
data,
query.state.tableOptions?.splits as TransactionSplitsOption,
),
);
}
setIsLoading(false);
}
},
@@ -184,6 +177,20 @@ export function useTransactions({
};
}, [query]);
useEffect(() => {
if (options.calculateRunningBalances && runningBalanceQuery) {
aqlQuery(runningBalanceQuery).then(data => {
const map = new Map<TransactionEntity['id'], IntegerAmount>();
data.data.forEach((val: { id: string; balance: IntegerAmount }) => {
map.set(val.id, val.balance);
});
setRunningBalances(map);
});
} else {
setRunningBalances(new Map());
}
}, [runningBalanceQuery, options.calculateRunningBalances]);
const loadMore = useCallback(async () => {
if (!pagedQueryRef.current) {
return;
@@ -216,24 +223,16 @@ export function useTransactions({
type UsePreviewTransactionsProps = {
filter?: (schedule: ScheduleEntity) => boolean;
options?: {
/**
* The starting balance to start the running balance calculation from.
*/
startingBalance?: IntegerAmount;
};
};
type UsePreviewTransactionsResult = {
previewTransactions: ReadonlyArray<TransactionEntity>;
runningBalances: Map<TransactionEntity['id'], IntegerAmount>;
isLoading: boolean;
error?: Error;
};
export function usePreviewTransactions({
filter,
options,
}: UsePreviewTransactionsProps = {}): UsePreviewTransactionsResult {
const [previewTransactions, setPreviewTransactions] = useState<
TransactionEntity[]
@@ -246,18 +245,9 @@ export function usePreviewTransactions({
} = useCachedSchedules();
const [isLoading, setIsLoading] = useState(isSchedulesLoading);
const [error, setError] = useState<Error | undefined>(undefined);
const [runningBalances, setRunningBalances] = useState<
Map<TransactionEntity['id'], IntegerAmount>
>(new Map());
const [upcomingLength] = useSyncedPref('upcomingScheduledTransactionLength');
// We don't want to re-render if options changes.
// Putting options in a ref will prevent that and
// allow us to use the latest options on next render.
const optionsRef = useRef(options);
optionsRef.current = options;
const scheduleTransactions = useMemo(() => {
if (isSchedulesLoading) {
return [];
@@ -372,19 +362,6 @@ export function usePreviewTransactions({
const ungroupedTransactions = ungroupTransactions(withDefaults);
setPreviewTransactions(ungroupedTransactions);
setRunningBalances(
// We always use the bottom up calculation for preview transactions
// because the hook controls the order of the transactions. We don't
// need to provide a custom way for consumers to calculate the running
// balances, at least as of writing.
calculateRunningBalancesBottomUp(
ungroupedTransactions,
// Preview transactions are behaves like 'all' splits
'all',
optionsRef.current?.startingBalance,
),
);
setIsLoading(false);
}
})
@@ -403,7 +380,6 @@ export function usePreviewTransactions({
const returnError = error || scheduleQueryError;
return {
previewTransactions,
runningBalances,
isLoading: isLoading || isSchedulesLoading,
...(returnError && { error: returnError }),
};
@@ -419,51 +395,3 @@ export function isForPreview(
['due', 'upcoming', 'missed', 'paid'].includes(status!)
);
}
function getCalculateRunningBalancesFn(
calculateRunningBalances: CalculateRunningBalancesOption = false,
) {
return calculateRunningBalances === true
? calculateRunningBalancesBottomUp
: typeof calculateRunningBalances === 'function'
? calculateRunningBalances
: undefined;
}
export function calculateRunningBalancesBottomUp(
transactions: TransactionEntity[],
splits: TransactionSplitsOption,
startingBalance: IntegerAmount = 0,
) {
return (
transactions
.filter(t => {
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 starting balance + the amount of the transaction
acc.set(transaction.id, startingBalance + 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>())
);
}

View File

@@ -282,6 +282,7 @@ export type Modal =
onReopenAccount: (accountId: AccountEntity['id']) => void;
onEditNotes: (id: NoteEntity['id']) => void;
onClose?: () => void;
onToggleRunningBalance?: () => void;
};
}
| {