Files
actual/packages/desktop-client/src/components/mobile/accounts/AccountTransactions.tsx
Adam Stück ca944baee5 feat: show/hide reconciled transactions on mobile (#6896)
* feat: show/hide reconciled transactions on mobile

Resolves #2969

* [autofix.ci] apply automated fixes

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-02-13 17:25:55 +00:00

226 lines
7.3 KiB
TypeScript

import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { send } from 'loot-core/platform/client/connection';
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, TransactionEntity } from 'loot-core/types/models';
import { markAccountRead } from '@desktop-client/accounts/accountsSlice';
import { syncAndDownload } from '@desktop-client/app/appSlice';
import { TransactionListWithBalances } from '@desktop-client/components/mobile/transactions/TransactionListWithBalances';
import { useAccountPreviewTransactions } from '@desktop-client/hooks/useAccountPreviewTransactions';
import { SchedulesProvider } from '@desktop-client/hooks/useCachedSchedules';
import { useDateFormat } from '@desktop-client/hooks/useDateFormat';
import { useNavigate } from '@desktop-client/hooks/useNavigate';
import { getSchedulesQuery } from '@desktop-client/hooks/useSchedules';
import { useSheetValue } from '@desktop-client/hooks/useSheetValue';
import { useSyncedPref } from '@desktop-client/hooks/useSyncedPref';
import {
calculateRunningBalancesTopDown,
useTransactions,
} from '@desktop-client/hooks/useTransactions';
import { useTransactionsSearch } from '@desktop-client/hooks/useTransactionsSearch';
import { collapseModals, pushModal } from '@desktop-client/modals/modalsSlice';
import * as queries from '@desktop-client/queries';
import { useDispatch } from '@desktop-client/redux';
import * as bindings from '@desktop-client/spreadsheet/bindings';
export function AccountTransactions({
account,
}: {
readonly account: AccountEntity;
}) {
const schedulesQuery = useMemo(
() => getSchedulesQuery(account.id),
[account.id],
);
return (
<SchedulesProvider query={schedulesQuery}>
<TransactionListWithPreviews account={account} />
</SchedulesProvider>
);
}
function TransactionListWithPreviews({
account,
}: {
readonly account: AccountEntity;
}) {
const { t } = useTranslation();
const dateFormat = useDateFormat() || 'MM/dd/yyyy';
const dispatch = useDispatch();
const navigate = useNavigate();
const baseTransactionsQuery = useCallback(
() =>
queries.transactions(account.id).options({ splits: 'all' }).select('*'),
[account.id],
);
const [showRunningBalances] = useSyncedPref(`show-balances-${account.id}`);
const [hideReconciled] = useSyncedPref(`hide-reconciled-${account.id}`);
const [transactionsQuery, setTransactionsQuery] = useState<Query>(
baseTransactionsQuery(),
);
const { isSearching, search: onSearch } = useTransactionsSearch({
updateQuery: setTransactionsQuery,
resetQuery: () => setTransactionsQuery(baseTransactionsQuery()),
dateFormat,
});
const shouldCalculateRunningBalances =
showRunningBalances === 'true' && !!account?.id && !isSearching;
const accountBalanceValue = useSheetValue<
'account',
'balance' | 'accounts-balance'
>(
account?.id
? bindings.accountBalance(account?.id)
: bindings.allAccountBalance(),
);
const {
transactions,
runningBalances,
isPending: isTransactionsLoading,
isFetchingNextPage: isLoadingMoreTransactions,
fetchNextPage: fetchMoreTransactions,
} = useTransactions({
query: transactionsQuery,
options: {
calculateRunningBalances: shouldCalculateRunningBalances
? calculateRunningBalancesTopDown
: shouldCalculateRunningBalances,
startingBalance: accountBalanceValue || 0,
},
});
const {
previewTransactions,
runningBalances: previewRunningBalances,
isLoading: isPreviewTransactionsLoading,
} = useAccountPreviewTransactions({
accountId: account?.id,
});
const onRefresh = useCallback(() => {
if (account.id) {
dispatch(syncAndDownload({ accountId: account.id }));
}
}, [account.id, dispatch]);
const allBalances = useMemo(
() =>
new Map<TransactionEntity['id'], IntegerAmount>([
...previewRunningBalances,
...runningBalances,
]),
[runningBalances, previewRunningBalances],
);
useEffect(() => {
if (account.id) {
dispatch(markAccountRead({ id: account.id }));
}
}, [account.id, dispatch]);
const onOpenTransaction = useCallback(
(transaction: TransactionEntity) => {
if (!isPreviewId(transaction.id)) {
navigate(`/transactions/${transaction.id}`);
} else {
dispatch(
pushModal({
modal: {
name: 'scheduled-transaction-menu',
options: {
transactionId: transaction.id,
onPost: async (transactionId, today = false) => {
const parts = transactionId.split('/');
await send('schedule/post-transaction', {
id: parts[1],
today,
});
dispatch(
collapseModals({
rootModalName: 'scheduled-transaction-menu',
}),
);
},
onSkip: async transactionId => {
const parts = transactionId.split('/');
await send('schedule/skip-next-date', { id: parts[1] });
dispatch(
collapseModals({
rootModalName: 'scheduled-transaction-menu',
}),
);
},
onComplete: async transactionId => {
const parts = transactionId.split('/');
await send('schedule/update', {
schedule: { id: parts[1], completed: true },
});
dispatch(
collapseModals({
rootModalName: 'scheduled-transaction-menu',
}),
);
},
},
},
}),
);
}
},
[dispatch, navigate],
);
const balanceBindings = useMemo(
() => ({
balance: bindings.accountBalance(account.id),
cleared: bindings.accountBalanceCleared(account.id),
uncleared: bindings.accountBalanceUncleared(account.id),
}),
[account],
);
const baseTransactions = !isSearching
? // Do not render child transactions in the list, unless searching
previewTransactions.concat(transactions.filter(t => !t.is_child))
: transactions;
const transactionsToDisplay =
hideReconciled === 'true'
? baseTransactions.filter(t => !t.reconciled)
: baseTransactions;
return (
<TransactionListWithBalances
isLoading={
isSearching
? isTransactionsLoading
: isTransactionsLoading || isPreviewTransactionsLoading
}
transactions={transactionsToDisplay}
balance={balanceBindings.balance}
balanceCleared={balanceBindings.cleared}
balanceUncleared={balanceBindings.uncleared}
runningBalances={allBalances}
showRunningBalances={shouldCalculateRunningBalances}
isLoadingMore={isLoadingMoreTransactions}
onLoadMore={fetchMoreTransactions}
searchPlaceholder={t('Search {{accountName}}', {
accountName: account.name,
})}
onSearch={onSearch}
onOpenTransaction={onOpenTransaction}
onRefresh={onRefresh}
/>
);
}