diff --git a/packages/desktop-client/src/components/mobile/accounts/AccountTransactions.tsx b/packages/desktop-client/src/components/mobile/accounts/AccountTransactions.tsx index 1e7ef9d3c8..9bf7e47781 100644 --- a/packages/desktop-client/src/components/mobile/accounts/AccountTransactions.tsx +++ b/packages/desktop-client/src/components/mobile/accounts/AccountTransactions.tsx @@ -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( - 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,59 @@ function TransactionListWithPreviews({ } }, [accountId, dispatch]); + const baseTransactionsQuery = useCallback( + () => + queries.transactions(accountId).options({ splits: 'all' }).select('*'), + [accountId], + ); + + const [showBalances] = useSyncedPref(`show-balances-${accountId}`); + const [transactionsQuery, setTransactionsQuery] = useState( + baseTransactionsQuery(), + ); + const { + transactions, + runningBalances, + isLoading: isTransactionsLoading, + reload: reloadTransactions, + isLoadingMore, + loadMore: loadMoreTransactions, + } = useTransactions({ + query: transactionsQuery, + options: { + calculateRunningBalances: true, + }, + }); + + const { isSearching, search: onSearch } = useTransactionsSearch({ + updateQuery: setTransactionsQuery, + resetQuery: () => setTransactionsQuery(baseTransactionsQuery()), + dateFormat, + }); + + const { + previewTransactions, + runningBalances: previewRunningBalances, + isLoading: isPreviewTransactionsLoading, + } = useAccountPreviewTransactions({ + accountId: account?.id, + getRunningBalances: showBalances === 'true', + }); + + useEffect(() => { + reloadTransactions(); + }, [showBalances, reloadTransactions]); + + const allBalances = + showBalances === 'true' + ? isSearching + ? undefined + : new Map([ + ...runningBalances, + ...previewRunningBalances, + ]) + : undefined; + useEffect(() => { if (accountId) { dispatch(markAccountRead({ id: accountId })); @@ -296,12 +342,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 +410,7 @@ function TransactionListWithPreviews({ balance={balanceQueries.balance} balanceCleared={balanceQueries.cleared} balanceUncleared={balanceQueries.uncleared} + runningBalances={allBalances} isLoadingMore={isLoadingMore} onLoadMore={loadMoreTransactions} searchPlaceholder={t('Search {{accountName}}', { accountName })} diff --git a/packages/desktop-client/src/components/mobile/budget/CategoryTransactions.tsx b/packages/desktop-client/src/components/mobile/budget/CategoryTransactions.tsx index 40abfc7e5f..0aa86c24ba 100644 --- a/packages/desktop-client/src/components/mobile/budget/CategoryTransactions.tsx +++ b/packages/desktop-client/src/components/mobile/budget/CategoryTransactions.tsx @@ -154,6 +154,7 @@ function TransactionListWithPreviews({ balance={balance} balanceCleared={balanceCleared} balanceUncleared={balanceUncleared} + runningBalances={undefined} searchPlaceholder={`Search ${category.name}`} onSearch={onSearch} isLoadingMore={isLoadingMore} diff --git a/packages/desktop-client/src/components/mobile/transactions/TransactionList.tsx b/packages/desktop-client/src/components/mobile/transactions/TransactionList.tsx index 61ae659cd6..d1aded00e8 100644 --- a/packages/desktop-client/src/components/mobile/transactions/TransactionList.tsx +++ b/packages/desktop-client/src/components/mobile/transactions/TransactionList.tsx @@ -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, @@ -83,6 +87,7 @@ function Loading({ style, 'aria-label': ariaLabel }: LoadingProps) { type TransactionListProps = { isLoading: boolean; transactions: readonly TransactionEntity[]; + runningBalances: Map | undefined; onOpenTransaction?: (transaction: TransactionEntity) => void; isLoadingMore: boolean; onLoadMore: () => void; @@ -92,6 +97,7 @@ type TransactionListProps = { export function TransactionList({ isLoading, transactions, + runningBalances, onOpenTransaction, isLoadingMore, onLoadMore, @@ -201,6 +207,7 @@ export function TransactionList({ {transaction => ( onTransactionPress(trans)} onLongPress={trans => onTransactionPress(trans, true)} diff --git a/packages/desktop-client/src/components/mobile/transactions/TransactionListItem.tsx b/packages/desktop-client/src/components/mobile/transactions/TransactionListItem.tsx index f41501907a..0bb8ca12eb 100644 --- a/packages/desktop-client/src/components/mobile/transactions/TransactionListItem.tsx +++ b/packages/desktop-client/src/components/mobile/transactions/TransactionListItem.tsx @@ -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,13 @@ const getScheduleIconStyle = ({ isPreview }: { isPreview: boolean }) => ({ type TransactionListItemProps = ComponentPropsWithoutRef< typeof ListBoxItem > & { + balance: IntegerAmount | null; onPress: (transaction: TransactionEntity) => void; onLongPress: (transaction: TransactionEntity) => void; }; export function TransactionListItem({ + balance, onPress, onLongPress, ...props @@ -282,7 +287,7 @@ export function TransactionListItem({ )} - + {integerToCurrency(amount)} + {balance != null && ( + + {integerToCurrency(balance)} + + )} diff --git a/packages/desktop-client/src/components/mobile/transactions/TransactionListWithBalances.tsx b/packages/desktop-client/src/components/mobile/transactions/TransactionListWithBalances.tsx index 3297de169a..8d77c559b8 100644 --- a/packages/desktop-client/src/components/mobile/transactions/TransactionListWithBalances.tsx +++ b/packages/desktop-client/src/components/mobile/transactions/TransactionListWithBalances.tsx @@ -86,6 +86,7 @@ type TransactionListWithBalancesProps = { balanceUncleared?: | Binding<'category', 'balanceUncleared'> | Binding<'account', 'balanceUncleared'>; + runningBalances: Map | undefined; searchPlaceholder: string; onSearch: (searchText: string) => void; isLoadingMore: boolean; @@ -101,6 +102,7 @@ export function TransactionListWithBalances({ balance, balanceCleared, balanceUncleared, + runningBalances, searchPlaceholder = 'Search...', onSearch, isLoadingMore, @@ -148,6 +150,7 @@ export function TransactionListWithBalances({ } title={ @@ -158,7 +161,7 @@ export function AccountMenuModal({ notes={ originalNotes && originalNotes.length > 0 ? originalNotes - : 'No notes' + : t('No notes') } editable={false} focused={false} @@ -201,13 +204,16 @@ 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); const [menuOpen, setMenuOpen] = useState(false); const itemStyle: CSSProperties = { @@ -219,6 +225,7 @@ function AdditionalAccountMenu({ ...itemStyle, ...(item.name === 'close' && { color: theme.errorTextMenu }), }); + const [showBalances] = useSyncedPref(`show-balances-${account.id}`); return ( @@ -244,16 +251,23 @@ function AdditionalAccountMenu({ diff --git a/packages/desktop-client/src/hooks/useAccountPreviewTransactions.ts b/packages/desktop-client/src/hooks/useAccountPreviewTransactions.ts index 08e9171ade..fc038821f3 100644 --- a/packages/desktop-client/src/hooks/useAccountPreviewTransactions.ts +++ b/packages/desktop-client/src/hooks/useAccountPreviewTransactions.ts @@ -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, @@ -17,6 +17,7 @@ import { accountBalance } from '@desktop-client/spreadsheet/bindings'; type UseAccountPreviewTransactionsProps = { accountId?: AccountEntity['id'] | undefined; + getRunningBalances?: boolean; }; type UseAccountPreviewTransactionsResult = ReturnType< @@ -29,6 +30,7 @@ type UseAccountPreviewTransactionsResult = ReturnType< */ export function useAccountPreviewTransactions({ accountId, + getRunningBalances, }: UseAccountPreviewTransactionsProps): UseAccountPreviewTransactionsResult { const accounts = useAccounts(); const accountsById = useMemo(() => groupById(accounts), [accounts]); @@ -108,11 +110,14 @@ export function useAccountPreviewTransactions({ return { isLoading, previewTransactions, - runningBalances, + runningBalances: getRunningBalances + ? runningBalances + : new Map(), error, }; }, [ accountId, + getRunningBalances, allPreviewTransactions, allRunningBalances, error, diff --git a/packages/desktop-client/src/modals/modalsSlice.ts b/packages/desktop-client/src/modals/modalsSlice.ts index d5c2bdb6dd..8fd71b9ac2 100644 --- a/packages/desktop-client/src/modals/modalsSlice.ts +++ b/packages/desktop-client/src/modals/modalsSlice.ts @@ -282,6 +282,7 @@ export type Modal = onReopenAccount: (accountId: AccountEntity['id']) => void; onEditNotes: (id: NoteEntity['id']) => void; onClose?: () => void; + onToggleRunningBalance?: () => void; }; } | { diff --git a/upcoming-release-notes/4809.md b/upcoming-release-notes/4809.md new file mode 100644 index 0000000000..eba8508c3f --- /dev/null +++ b/upcoming-release-notes/4809.md @@ -0,0 +1,6 @@ +--- +category: Enhancements +authors: [youngcw] +--- + +Add running balance to mobile