From 7340b48b706d8edf2aac79fffd17c2aa937e96c0 Mon Sep 17 00:00:00 2001 From: Joel Jeremy Marquez <joeljeremy.marquez@gmail.com> Date: Thu, 20 Jul 2023 05:21:12 -0700 Subject: [PATCH] Include scheduled transactions for the month in Account's running balance (#1354) --- .../src/components/accounts/Account.js | 386 ++++++++++-------- .../transactions/TransactionList.js | 2 + .../transactions/TransactionsTable.js | 25 +- upcoming-release-notes/1354.md | 6 + 4 files changed, 236 insertions(+), 183 deletions(-) create mode 100644 upcoming-release-notes/1354.md diff --git a/packages/desktop-client/src/components/accounts/Account.js b/packages/desktop-client/src/components/accounts/Account.js index 4b19fa501..a38f79ae4 100644 --- a/packages/desktop-client/src/components/accounts/Account.js +++ b/packages/desktop-client/src/components/accounts/Account.js @@ -74,10 +74,19 @@ function EmptyMessage({ onAdd }) { ); } -function AllTransactions({ account = {}, transactions, filtered, children }) { +function AllTransactions({ + account = {}, + transactions, + balances, + showBalances, + filtered, + children, +}) { const { id: accountId } = account; let scheduleData = useCachedSchedules(); + transactions ??= []; + let schedules = useMemo( () => scheduleData @@ -94,7 +103,7 @@ function AllTransactions({ account = {}, transactions, filtered, children }) { let prependTransactions = useMemo(() => { return schedules.map(schedule => ({ - id: 'preview/' + schedule.id, + id: `preview/${schedule.id}`, payee: schedule._payee, account: schedule._account, amount: schedule._amount, @@ -105,6 +114,36 @@ function AllTransactions({ account = {}, transactions, filtered, children }) { })); }, [schedules, accountId]); + let runningBalance = useMemo(() => { + if (!showBalances) { + return 0; + } + + return balances && transactions?.length > 0 + ? balances[transactions[0].id]?.balance ?? 0 + : 0; + }, [showBalances, balances, transactions]); + + let prependBalances = useMemo(() => { + if (!showBalances) { + return null; + } + + // Reverse so we can calculate from earliest upcoming schedule. + let scheduledBalances = [...prependTransactions] + .reverse() + .map(scheduledTransaction => { + let amount = + (scheduledTransaction._inverse ? -1 : 1) * + scheduledTransaction.amount; + return { + balance: (runningBalance += amount), + id: scheduledTransaction.id, + }; + }); + return groupById(scheduledBalances); + }, [showBalances, prependTransactions, runningBalance]); + let allTransactions = useMemo(() => { // Don't prepend scheduled transactions if we are filtering if (!filtered && prependTransactions.length > 0) { @@ -113,10 +152,18 @@ function AllTransactions({ account = {}, transactions, filtered, children }) { return transactions; }, [filtered, prependTransactions, transactions]); + let allBalances = useMemo(() => { + // Don't prepend scheduled transactions if we are filtering + if (!filtered && prependBalances && balances) { + return { ...prependBalances, ...balances }; + } + return balances; + }, [filtered, prependBalances, balances]); + if (scheduleData == null) { - return children(null); + return children(transactions, balances); } - return children(allTransactions); + return children(allTransactions, allBalances); } function getField(field) { @@ -152,7 +199,7 @@ class AccountInternal extends PureComponent { transactions: [], transactionsCount: 0, showBalances: props.showBalances, - balances: [], + balances: null, showCleared: props.showCleared, editingName: false, isAdding: false, @@ -340,12 +387,11 @@ class AccountInternal extends PureComponent { transactionsFiltered: isFiltered, loading: false, workingHard: false, + balances: this.state.showBalances + ? await this.calculateBalances() + : null, }, () => { - if (this.state.showBalances) { - this.calculateBalances(); - } - if (firstLoad) { this.table.current?.scrollToTop(); } @@ -372,7 +418,7 @@ class AccountInternal extends PureComponent { loading: true, search: '', showBalances: nextProps.showBalances, - balances: [], + balances: null, showCleared: nextProps.showCleared, }, () => { @@ -487,7 +533,7 @@ class AccountInternal extends PureComponent { async calculateBalances() { if (!this.canCalculateBalance()) { - return; + return null; } let { data } = await runQuery( @@ -497,7 +543,7 @@ class AccountInternal extends PureComponent { .select([{ balance: { $sumOver: '$amount' } }]), ); - this.setState({ balances: groupById(data) }); + return groupById(data); } onAddTransaction = () => { @@ -552,32 +598,36 @@ class AccountInternal extends PureComponent { case 'toggle-balance': if (this.state.showBalances) { this.props.savePrefs({ ['show-balances-' + accountId]: false }); - this.setState({ showBalances: false, balances: [] }); + this.setState({ showBalances: false, balances: null }); } else { - this.setState({ - transactions: [], - transactionCount: 0, - filters: [], - search: '', - sort: [], - showBalances: true, - }); - this.fetchTransactions(); this.props.savePrefs({ ['show-balances-' + accountId]: true }); - this.calculateBalances(); + this.setState( + { + transactions: [], + transactionCount: 0, + filters: [], + search: '', + sort: [], + showBalances: true, + }, + () => { + this.fetchTransactions(); + }, + ); } break; case 'remove-sorting': { - let filters = this.state.filters; - this.setState({ sort: [] }); - if (filters.length > 0) { - this.applyFilters([...filters]); - } else { - this.fetchTransactions(); - } - if (this.state.search !== '') { - this.onSearch(this.state.search); - } + this.setState({ sort: [] }, () => { + let filters = this.state.filters; + if (filters.length > 0) { + this.applyFilters([...filters]); + } else { + this.fetchTransactions(); + } + if (this.state.search !== '') { + this.onSearch(this.state.search); + } + }); break; } case 'toggle-cleared': @@ -984,16 +1034,25 @@ class AccountInternal extends PureComponent { this.currentQuery = this.rootQuery.filter({ [conditionsOpKey]: [...filters, ...customFilters], }); - this.updateQuery(this.currentQuery, true); - this.setState({ filters: conditions }); + + this.setState({ filters: conditions }, () => { + this.updateQuery(this.currentQuery, true); + }); } else { - this.setState({ transactions: [], transactionCount: 0 }); - this.fetchTransactions(); - this.setState({ filters: conditions }); - } + this.setState( + { + transactions: [], + transactionCount: 0, + filters: conditions, + }, + () => { + this.fetchTransactions(); - if (this.state.sort.length !== 0) { - this.applySort(); + if (this.state.sort.length !== 0) { + this.applySort(); + } + }, + ); } }; @@ -1127,143 +1186,134 @@ class AccountInternal extends PureComponent { <AllTransactions account={account} transactions={transactions} + balances={balances} + showBalances={showBalances} filtered={transactionsFiltered} > - {allTransactions => - allTransactions == null ? null : ( - <SelectedProviderWithItems - name="transactions" - items={allTransactions} - fetchAllIds={this.fetchAllIds} - registerDispatch={dispatch => (this.dispatchSelected = dispatch)} - > - <View style={[styles.page]}> - <AccountHeader + {(allTransactions, allBalances) => ( + <SelectedProviderWithItems + name="transactions" + items={allTransactions} + fetchAllIds={this.fetchAllIds} + registerDispatch={dispatch => (this.dispatchSelected = dispatch)} + > + <View style={[styles.page]}> + <AccountHeader + tableRef={this.table} + editingName={editingName} + isNameEditable={isNameEditable} + workingHard={workingHard} + account={account} + filterId={filterId} + filtersList={this.props.filtersList} + location={this.props.location} + accountName={accountName} + accountsSyncing={accountsSyncing} + accounts={accounts} + transactions={transactions} + showBalances={showBalances} + showExtraBalances={showExtraBalances} + showCleared={showCleared} + showEmptyMessage={showEmptyMessage} + balanceQuery={balanceQuery} + canCalculateBalance={this.canCalculateBalance} + reconcileAmount={reconcileAmount} + search={this.state.search} + filters={this.state.filters} + conditionsOp={this.state.conditionsOp} + savePrefs={this.props.savePrefs} + onSearch={this.onSearch} + onShowTransactions={this.onShowTransactions} + onMenuSelect={this.onMenuSelect} + onAddTransaction={this.onAddTransaction} + onToggleExtraBalances={this.onToggleExtraBalances} + onSaveName={this.onSaveName} + onExposeName={this.onExposeName} + onReconcile={this.onReconcile} + onDoneReconciling={this.onDoneReconciling} + onCreateReconciliationTransaction={ + this.onCreateReconciliationTransaction + } + onSync={this.onSync} + onImport={this.onImport} + onBatchDelete={this.onBatchDelete} + onBatchDuplicate={this.onBatchDuplicate} + onBatchEdit={this.onBatchEdit} + onBatchUnlink={this.onBatchUnlink} + onCreateRule={this.onCreateRule} + onUpdateFilter={this.onUpdateFilter} + onClearFilters={this.onClearFilters} + onReloadSavedFilter={this.onReloadSavedFilter} + onCondOpChange={this.onCondOpChange} + onDeleteFilter={this.onDeleteFilter} + onApplyFilter={this.onApplyFilter} + onScheduleAction={this.onScheduleAction} + /> + + <View style={{ flex: 1 }}> + <TransactionList tableRef={this.table} - editingName={editingName} - isNameEditable={isNameEditable} - workingHard={workingHard} account={account} - filterId={filterId} - filtersList={this.props.filtersList} - location={this.props.location} - accountName={accountName} - accountsSyncing={accountsSyncing} - accounts={accounts} transactions={transactions} + allTransactions={allTransactions} + animated={this.animated} + loadMoreTransactions={() => + this.paged && this.paged.fetchNext() + } + accounts={accounts} + category={category} + categoryGroups={categoryGroups} + payees={payees} + balances={allBalances} showBalances={showBalances} - showExtraBalances={showExtraBalances} showCleared={showCleared} - showEmptyMessage={showEmptyMessage} - balanceQuery={balanceQuery} - canCalculateBalance={this.canCalculateBalance} - isSorted={this.state.sort.length !== 0} - reconcileAmount={reconcileAmount} - search={this.state.search} - filters={this.state.filters} - conditionsOp={this.state.conditionsOp} - savePrefs={this.props.savePrefs} - onSearch={this.onSearch} - onShowTransactions={this.onShowTransactions} - onMenuSelect={this.onMenuSelect} - onAddTransaction={this.onAddTransaction} - onToggleExtraBalances={this.onToggleExtraBalances} - onSaveName={this.onSaveName} - onExposeName={this.onExposeName} - onReconcile={this.onReconcile} - onDoneReconciling={this.onDoneReconciling} - onCreateReconciliationTransaction={ - this.onCreateReconciliationTransaction + showAccount={ + !accountId || + accountId === 'offbudget' || + accountId === 'budgeted' || + accountId === 'uncategorized' + } + isAdding={this.state.isAdding} + isNew={this.isNew} + isMatched={this.isMatched} + isFiltered={ + this.state.search !== '' || this.state.filters.length > 0 } - onSync={this.onSync} - onImport={this.onImport} - onBatchDelete={this.onBatchDelete} - onBatchDuplicate={this.onBatchDuplicate} - onBatchEdit={this.onBatchEdit} - onBatchUnlink={this.onBatchUnlink} - onCreateRule={this.onCreateRule} - onUpdateFilter={this.onUpdateFilter} - onClearFilters={this.onClearFilters} - onReloadSavedFilter={this.onReloadSavedFilter} - onCondOpChange={this.onCondOpChange} - onDeleteFilter={this.onDeleteFilter} - onApplyFilter={this.onApplyFilter} - onScheduleAction={this.onScheduleAction} + dateFormat={dateFormat} + hideFraction={hideFraction} + addNotification={addNotification} + renderEmpty={() => + showEmptyMessage ? ( + <EmptyMessage onAdd={() => replaceModal('add-account')} /> + ) : !loading ? ( + <View + style={{ + marginTop: 20, + textAlign: 'center', + fontStyle: 'italic', + }} + > + No transactions + </View> + ) : null + } + onChange={this.onTransactionsChange} + onRefetch={this.refetchTransactions} + onRefetchUpToRow={row => + this.paged.refetchUpToRow(row, { + field: 'date', + order: 'desc', + }) + } + onCloseAddTransaction={() => + this.setState({ isAdding: false }) + } + onCreatePayee={this.onCreatePayee} /> - - <View style={{ flex: 1 }}> - <TransactionList - tableRef={this.table} - account={account} - transactions={transactions} - allTransactions={allTransactions} - animated={this.animated} - loadMoreTransactions={() => - this.paged && this.paged.fetchNext() - } - accounts={accounts} - category={category} - categoryGroups={categoryGroups} - payees={payees} - balances={ - showBalances && this.canCalculateBalance() - ? balances - : null - } - showCleared={showCleared} - showAccount={ - !accountId || - accountId === 'offbudget' || - accountId === 'budgeted' || - accountId === 'uncategorized' - } - isAdding={this.state.isAdding} - isNew={this.isNew} - isMatched={this.isMatched} - isFiltered={ - this.state.search !== '' || this.state.filters.length > 0 - } - dateFormat={dateFormat} - hideFraction={hideFraction} - addNotification={addNotification} - renderEmpty={() => - showEmptyMessage ? ( - <EmptyMessage - onAdd={() => replaceModal('add-account')} - /> - ) : !loading ? ( - <View - style={{ - marginTop: 20, - textAlign: 'center', - fontStyle: 'italic', - }} - > - No transactions - </View> - ) : null - } - onSort={this.onSort} - sortField={this.state.sort.field} - ascDesc={this.state.sort.ascDesc} - onChange={this.onTransactionsChange} - onRefetch={this.refetchTransactions} - onRefetchUpToRow={row => - this.paged.refetchUpToRow(row, { - field: 'date', - order: 'desc', - }) - } - onCloseAddTransaction={() => - this.setState({ isAdding: false }) - } - onCreatePayee={this.onCreatePayee} - /> - </View> </View> - </SelectedProviderWithItems> - ) - } + </View> + </SelectedProviderWithItems> + )} </AllTransactions> ); } diff --git a/packages/desktop-client/src/components/transactions/TransactionList.js b/packages/desktop-client/src/components/transactions/TransactionList.js index 2e9b20b49..96c4445b0 100644 --- a/packages/desktop-client/src/components/transactions/TransactionList.js +++ b/packages/desktop-client/src/components/transactions/TransactionList.js @@ -65,6 +65,7 @@ export default function TransactionList({ categoryGroups, payees, balances, + showBalances, showCleared, showAccount, headerContent, @@ -172,6 +173,7 @@ export default function TransactionList({ accounts={accounts} categoryGroups={categoryGroups} payees={payees} + showBalances={showBalances} balances={balances} showCleared={showCleared} showAccount={showAccount} diff --git a/packages/desktop-client/src/components/transactions/TransactionsTable.js b/packages/desktop-client/src/components/transactions/TransactionsTable.js index 6bd35ac6a..c2121ebfb 100644 --- a/packages/desktop-client/src/components/transactions/TransactionsTable.js +++ b/packages/desktop-client/src/components/transactions/TransactionsTable.js @@ -1223,11 +1223,7 @@ const Transaction = memo(function Transaction(props) { {showBalance && ( <Cell name="balance" - value={ - balance == null || isChild || isPreview - ? '' - : integerToCurrency(balance) - } + value={balance == null || isChild ? '' : integerToCurrency(balance)} valueStyle={{ color: balance < 0 ? colors.r4 : colors.g4 }} style={[styles.tnum, amountStyle]} width={88} @@ -1486,6 +1482,7 @@ function TransactionTableInner({ showCleared, showAccount, showCategory, + showBalances, balances, hideFraction, isNew, @@ -1532,24 +1529,22 @@ function TransactionTableInner({ transaction={trans} showAccount={showAccount} showCategory={showCategory} - showBalance={!!balances} + showBalance={showBalances} showCleared={showCleared} hovered={hovered} selected={selected} highlighted={false} - added={isNew && isNew(trans.id)} - expanded={isExpanded && isExpanded(trans.id)} - matched={isMatched && isMatched(trans.id)} + added={isNew?.(trans.id)} + expanded={isExpanded?.(trans.id)} + matched={isMatched?.(trans.id)} showZeroInDeposit={isChildDeposit} - balance={balances && balances[trans.id] && balances[trans.id].balance} + balance={balances?.[trans.id]?.balance} focusedField={editing && tableNavigator.focusedField} accounts={accounts} categoryGroups={categoryGroups} payees={payees} inheritedFields={ - parent && parent.payee === trans.payee - ? new Set(['payee']) - : new Set() + parent?.payee === trans.payee ? new Set(['payee']) : new Set() } dateFormat={dateFormat} hideFraction={hideFraction} @@ -1578,7 +1573,7 @@ function TransactionTableInner({ hasSelected={props.selectedItems.size > 0} showAccount={props.showAccount} showCategory={props.showCategory} - showBalance={!!props.balances} + showBalance={props.showBalances} showCleared={props.showCleared} scrollWidth={scrollWidth} onSort={props.onSort} @@ -1602,7 +1597,7 @@ function TransactionTableInner({ payees={props.payees || []} showAccount={props.showAccount} showCategory={props.showCategory} - showBalance={!!props.balances} + showBalance={props.showBalances} showCleared={props.showCleared} dateFormat={dateFormat} hideFraction={props.hideFraction} diff --git a/upcoming-release-notes/1354.md b/upcoming-release-notes/1354.md new file mode 100644 index 000000000..555096a69 --- /dev/null +++ b/upcoming-release-notes/1354.md @@ -0,0 +1,6 @@ +--- +category: Enhancements +authors: [joel-jeremy] +--- + +Scheduled transactions for the month to show up in Account's running balance -- GitLab