From 8e99a3145576093e3be1c43ec6d3445d51cfd338 Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Mon, 5 Aug 2024 15:56:11 +0200 Subject: [PATCH 1/6] fix: validate exclude and unexclude uncategorized transaction --- .../models/UncategorizedCashflowTransaction.ts | 10 ++++++++++ .../Banking/Exclude/ExcludeBankTransaction.ts | 14 +++++++++++--- .../Banking/Exclude/ExcludeBankTransactions.ts | 4 ++-- .../Exclude/UnexcludeBankTransaction.ts | 14 +++++++++++--- .../Exclude/UnexcludeBankTransactions.ts | 4 ++-- .../src/services/Banking/Exclude/utils.ts | 18 ++++++++++++++++++ .../AccountTransactionsUncategorizedTable.tsx | 6 ++++++ packages/webapp/src/hooks/query/bank-rules.ts | 8 ++++++++ 8 files changed, 68 insertions(+), 10 deletions(-) diff --git a/packages/server/src/models/UncategorizedCashflowTransaction.ts b/packages/server/src/models/UncategorizedCashflowTransaction.ts index 2f3f0a50e..218c35982 100644 --- a/packages/server/src/models/UncategorizedCashflowTransaction.ts +++ b/packages/server/src/models/UncategorizedCashflowTransaction.ts @@ -20,6 +20,7 @@ export default class UncategorizedCashflowTransaction extends mixin( description!: string; plaidTransactionId!: string; recognizedTransactionId!: number; + excludedAt: Date; /** * Table name. @@ -45,6 +46,7 @@ export default class UncategorizedCashflowTransaction extends mixin( 'isDepositTransaction', 'isWithdrawalTransaction', 'isRecognized', + 'isExcluded' ]; } @@ -89,6 +91,14 @@ export default class UncategorizedCashflowTransaction extends mixin( return !!this.recognizedTransactionId; } + /** + * Detarmines whether the transaction is excluded. + * @returns {boolean} + */ + public get isExcluded(): boolean { + return !!this.excludedAt; + } + /** * Model modifiers. */ diff --git a/packages/server/src/services/Banking/Exclude/ExcludeBankTransaction.ts b/packages/server/src/services/Banking/Exclude/ExcludeBankTransaction.ts index 16a25433c..7b8012ccd 100644 --- a/packages/server/src/services/Banking/Exclude/ExcludeBankTransaction.ts +++ b/packages/server/src/services/Banking/Exclude/ExcludeBankTransaction.ts @@ -1,7 +1,11 @@ +import { Knex } from 'knex'; +import { Inject, Service } from 'typedi'; import HasTenancyService from '@/services/Tenancy/TenancyService'; import UnitOfWork from '@/services/UnitOfWork'; -import { Inject, Service } from 'typedi'; -import { validateTransactionNotCategorized } from './utils'; +import { + validateTransactionNotCategorized, + validateTransactionNotExcluded, +} from './utils'; import { EventPublisher } from '@/lib/EventPublisher/EventPublisher'; import events from '@/subscribers/events'; import { @@ -37,9 +41,13 @@ export class ExcludeBankTransaction { .findById(uncategorizedTransactionId) .throwIfNotFound(); + // Validate the transaction shouldn't be excluded. + validateTransactionNotExcluded(oldUncategorizedTransaction); + + // Validate the transaction shouldn't be categorized. validateTransactionNotCategorized(oldUncategorizedTransaction); - return this.uow.withTransaction(tenantId, async (trx) => { + return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => { await this.eventPublisher.emitAsync(events.bankTransactions.onExcluding, { tenantId, uncategorizedTransactionId, diff --git a/packages/server/src/services/Banking/Exclude/ExcludeBankTransactions.ts b/packages/server/src/services/Banking/Exclude/ExcludeBankTransactions.ts index 65d65a7c1..91bdf5320 100644 --- a/packages/server/src/services/Banking/Exclude/ExcludeBankTransactions.ts +++ b/packages/server/src/services/Banking/Exclude/ExcludeBankTransactions.ts @@ -1,6 +1,6 @@ import { Inject, Service } from 'typedi'; import PromisePool from '@supercharge/promise-pool'; -import { castArray } from 'lodash'; +import { castArray, uniq } from 'lodash'; import { ExcludeBankTransaction } from './ExcludeBankTransaction'; @Service() @@ -18,7 +18,7 @@ export class ExcludeBankTransactions { tenantId: number, bankTransactionIds: Array | number ) { - const _bankTransactionIds = castArray(bankTransactionIds); + const _bankTransactionIds = uniq(castArray(bankTransactionIds)); await PromisePool.withConcurrency(1) .for(_bankTransactionIds) diff --git a/packages/server/src/services/Banking/Exclude/UnexcludeBankTransaction.ts b/packages/server/src/services/Banking/Exclude/UnexcludeBankTransaction.ts index 46bd81862..f6bb1bfad 100644 --- a/packages/server/src/services/Banking/Exclude/UnexcludeBankTransaction.ts +++ b/packages/server/src/services/Banking/Exclude/UnexcludeBankTransaction.ts @@ -1,7 +1,11 @@ +import { Knex } from 'knex'; +import { Inject, Service } from 'typedi'; import HasTenancyService from '@/services/Tenancy/TenancyService'; import UnitOfWork from '@/services/UnitOfWork'; -import { Inject, Service } from 'typedi'; -import { validateTransactionNotCategorized } from './utils'; +import { + validateTransactionNotCategorized, + validateTransactionShouldBeExcluded, +} from './utils'; import { EventPublisher } from '@/lib/EventPublisher/EventPublisher'; import events from '@/subscribers/events'; import { @@ -37,9 +41,13 @@ export class UnexcludeBankTransaction { .findById(uncategorizedTransactionId) .throwIfNotFound(); + // Validate the transaction should be excludded. + validateTransactionShouldBeExcluded(oldUncategorizedTransaction); + + // Validate the transaction shouldn't be categorized. validateTransactionNotCategorized(oldUncategorizedTransaction); - return this.uow.withTransaction(tenantId, async (trx) => { + return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => { await this.eventPublisher.emitAsync( events.bankTransactions.onUnexcluding, { diff --git a/packages/server/src/services/Banking/Exclude/UnexcludeBankTransactions.ts b/packages/server/src/services/Banking/Exclude/UnexcludeBankTransactions.ts index 846ea1fd8..d3743aeeb 100644 --- a/packages/server/src/services/Banking/Exclude/UnexcludeBankTransactions.ts +++ b/packages/server/src/services/Banking/Exclude/UnexcludeBankTransactions.ts @@ -1,7 +1,7 @@ import { Inject, Service } from 'typedi'; import PromisePool from '@supercharge/promise-pool'; import { UnexcludeBankTransaction } from './UnexcludeBankTransaction'; -import { castArray } from 'lodash'; +import { castArray, uniq } from 'lodash'; @Service() export class UnexcludeBankTransactions { @@ -17,7 +17,7 @@ export class UnexcludeBankTransactions { tenantId: number, bankTransactionIds: Array | number ) { - const _bankTransactionIds = castArray(bankTransactionIds); + const _bankTransactionIds = uniq(castArray(bankTransactionIds)); await PromisePool.withConcurrency(1) .for(_bankTransactionIds) diff --git a/packages/server/src/services/Banking/Exclude/utils.ts b/packages/server/src/services/Banking/Exclude/utils.ts index 6d4f02a9a..04b06e8ca 100644 --- a/packages/server/src/services/Banking/Exclude/utils.ts +++ b/packages/server/src/services/Banking/Exclude/utils.ts @@ -3,6 +3,8 @@ import UncategorizedCashflowTransaction from '@/models/UncategorizedCashflowTran const ERRORS = { TRANSACTION_ALREADY_CATEGORIZED: 'TRANSACTION_ALREADY_CATEGORIZED', + TRANSACTION_ALREADY_EXCLUDED: 'TRANSACTION_ALREADY_EXCLUDED', + TRANSACTION_NOT_EXCLUDED: 'TRANSACTION_NOT_EXCLUDED', }; export const validateTransactionNotCategorized = ( @@ -12,3 +14,19 @@ export const validateTransactionNotCategorized = ( throw new ServiceError(ERRORS.TRANSACTION_ALREADY_CATEGORIZED); } }; + +export const validateTransactionNotExcluded = ( + transaction: UncategorizedCashflowTransaction +) => { + if (transaction.isExcluded) { + throw new ServiceError(ERRORS.TRANSACTION_ALREADY_EXCLUDED); + } +}; + +export const validateTransactionShouldBeExcluded = ( + transaction: UncategorizedCashflowTransaction +) => { + if (!transaction.isExcluded) { + throw new ServiceError(ERRORS.TRANSACTION_NOT_EXCLUDED); + } +}; diff --git a/packages/webapp/src/containers/CashFlow/AccountTransactions/UncategorizedTransactions/AccountTransactionsUncategorizedTable.tsx b/packages/webapp/src/containers/CashFlow/AccountTransactions/UncategorizedTransactions/AccountTransactionsUncategorizedTable.tsx index d3c575898..f2bd264bd 100644 --- a/packages/webapp/src/containers/CashFlow/AccountTransactions/UncategorizedTransactions/AccountTransactionsUncategorizedTable.tsx +++ b/packages/webapp/src/containers/CashFlow/AccountTransactions/UncategorizedTransactions/AccountTransactionsUncategorizedTable.tsx @@ -71,6 +71,11 @@ function AccountTransactionsDataTable({ const handleCategorizeBtnClick = (transaction) => { setUncategorizedTransactionIdForMatching(transaction.id); }; + // handles table selected rows change. + const handleSelectedRowsChange = (selected) => { + const transactionIds = selected.map((r) => r.original.id); + setUncategorizedTransactionsSelected(transactionIds); + }; // Handle exclude transaction. const handleExcludeTransaction = (transaction) => { excludeTransaction(transaction.id) @@ -118,6 +123,7 @@ function AccountTransactionsDataTable({ onExclude: handleExcludeTransaction, onCategorize: handleCategorizeBtnClick, }} + onSelectedRowsChange={handleSelectedRowsChange} className={clsx('table-constrant', styles.table, { [styles.showCategorizeColumn]: enableMultipleCategorization, })} diff --git a/packages/webapp/src/hooks/query/bank-rules.ts b/packages/webapp/src/hooks/query/bank-rules.ts index 39b0c9154..92103bd2f 100644 --- a/packages/webapp/src/hooks/query/bank-rules.ts +++ b/packages/webapp/src/hooks/query/bank-rules.ts @@ -319,6 +319,10 @@ export function useExcludeUncategorizedTransaction( { onSuccess: (res, id) => { onValidateExcludeUncategorizedTransaction(queryClient); + queryClient.invalidateQueries([ + QUERY_KEY.BANK_ACCOUNT_SUMMARY_META, + id, + ]); }, ...options, }, @@ -360,6 +364,10 @@ export function useUnexcludeUncategorizedTransaction( { onSuccess: (res, id) => { onValidateExcludeUncategorizedTransaction(queryClient); + queryClient.invalidateQueries([ + QUERY_KEY.BANK_ACCOUNT_SUMMARY_META, + id, + ]); }, ...options, }, From 64c0732e5fe575af51a5757ff767071d3804d962 Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Mon, 5 Aug 2024 20:57:13 +0200 Subject: [PATCH 2/6] fix: infinity scrolling of bank account transactions --- .../AppContentShell/AppContentShell.tsx | 99 ++++++++++++------- .../Datatable/TableVirtualizedRows.tsx | 11 +-- .../webapp/src/components/Layout/Box/Box.tsx | 13 ++- .../AccountTransactionsList.tsx | 47 +++++---- .../AccountTransactionsProvider.tsx | 7 +- .../ExcludedTransactionsTable.tsx | 4 + .../RecognizedTransactionsTable.tsx | 4 + .../AccountTransactionsUncategorizedTable.tsx | 4 + 8 files changed, 124 insertions(+), 65 deletions(-) diff --git a/packages/webapp/src/components/AppShell/AppContentShell/AppContentShell.tsx b/packages/webapp/src/components/AppShell/AppContentShell/AppContentShell.tsx index da442eb6e..83299110a 100644 --- a/packages/webapp/src/components/AppShell/AppContentShell/AppContentShell.tsx +++ b/packages/webapp/src/components/AppShell/AppContentShell/AppContentShell.tsx @@ -1,5 +1,9 @@ -import React from 'react'; -import { AppShellProvider, useAppShellContext } from './AppContentShellProvider'; +// @ts-nocheck +import React, { forwardRef, Ref } from 'react'; +import { + AppShellProvider, + useAppShellContext, +} from './AppContentShellProvider'; import { Box, BoxProps } from '../../Layout'; import styles from './AppContentShell.module.scss'; @@ -12,50 +16,73 @@ interface AppContentShellProps { hideMain?: boolean; } -export function AppContentShell({ - asideProps, - mainProps, - topbarOffset = 0, - hideAside = false, - hideMain = false, - ...restProps -}: AppContentShellProps) { - return ( - - - - ); -} +export const AppContentShell = forwardRef( + ( + { + asideProps, + mainProps, + topbarOffset = 0, + hideAside = false, + hideMain = false, + ...restProps + }: AppContentShellProps, + ref: Ref, + ) => { + return ( + + + + ); + }, +); +AppContentShell.displayName = 'AppContentShell'; interface AppContentShellMainProps extends BoxProps {} -function AppContentShellMain({ ...props }: AppContentShellMainProps) { - const { hideMain } = useAppShellContext(); +/** + * Main content of the app shell. + * @param {AppContentShellMainProps} props - + * @returns {React.ReactNode} + */ +const AppContentShellMain = forwardRef( + ({ ...props }: AppContentShellMainProps, ref: Ref) => { + const { hideMain } = useAppShellContext(); - if (hideMain === true) { - return null; - } - return ; -} + if (hideMain === true) { + return null; + } + return ; + }, +); + +AppContentShellMain.displayName = 'AppContentShellMain'; interface AppContentShellAsideProps extends BoxProps { children: React.ReactNode; } -function AppContentShellAside({ ...props }: AppContentShellAsideProps) { - const { hideAside } = useAppShellContext(); +/** + * Aside content of the app shell. + * @param {AppContentShellAsideProps} props + * @returns {React.ReactNode} + */ +const AppContentShellAside = forwardRef( + ({ ...props }: AppContentShellAsideProps, ref: Ref) => { + const { hideAside } = useAppShellContext(); - if (hideAside === true) { - return null; - } - return ; -} + if (hideAside === true) { + return null; + } + return ; + }, +); +AppContentShellAside.displayName = 'AppContentShellAside'; AppContentShell.Main = AppContentShellMain; AppContentShell.Aside = AppContentShellAside; diff --git a/packages/webapp/src/components/Datatable/TableVirtualizedRows.tsx b/packages/webapp/src/components/Datatable/TableVirtualizedRows.tsx index 563575b12..ab2bc35b3 100644 --- a/packages/webapp/src/components/Datatable/TableVirtualizedRows.tsx +++ b/packages/webapp/src/components/Datatable/TableVirtualizedRows.tsx @@ -25,14 +25,13 @@ function TableVirtualizedListRow({ index, isScrolling, isVisible, style }) { export function TableVirtualizedListRows() { const { table: { page }, - props: { vListrowHeight, vListOverscanRowCount }, + props: { vListrowHeight, vListOverscanRowCount, windowScrollerProps }, } = useContext(TableContext); // Dashboard content pane. - const dashboardContentPane = React.useMemo( - () => document.querySelector(`.${CLASSES.DASHBOARD_CONTENT_PANE}`), - [], - ); + const scrollElement = + windowScrollerProps?.scrollElement || + document.querySelector(`.${CLASSES.DASHBOARD_CONTENT_PANE}`); const rowRenderer = React.useCallback( ({ key, ...args }) => , @@ -40,7 +39,7 @@ export function TableVirtualizedListRows() { ); return ( - + {({ height, isScrolling, onChildScroll, scrollTop }) => ( {({ width }) => ( diff --git a/packages/webapp/src/components/Layout/Box/Box.tsx b/packages/webapp/src/components/Layout/Box/Box.tsx index 15acc305b..9d104cf58 100644 --- a/packages/webapp/src/components/Layout/Box/Box.tsx +++ b/packages/webapp/src/components/Layout/Box/Box.tsx @@ -1,12 +1,15 @@ -import React from 'react'; +import React, { forwardRef, Ref } from 'react'; import { HTMLDivProps, Props } from '@blueprintjs/core'; export interface BoxProps extends Props, HTMLDivProps { className?: string; } -export function Box({ className, ...rest }: BoxProps) { - const Element = 'div'; +export const Box = forwardRef( + ({ className, ...rest }: BoxProps, ref: Ref) => { + const Element = 'div'; - return ; -} + return ; + }, +); +Box.displayName = '@bigcapital/Box'; diff --git a/packages/webapp/src/containers/CashFlow/AccountTransactions/AccountTransactionsList.tsx b/packages/webapp/src/containers/CashFlow/AccountTransactions/AccountTransactionsList.tsx index 671747706..c5a8fed62 100644 --- a/packages/webapp/src/containers/CashFlow/AccountTransactions/AccountTransactionsList.tsx +++ b/packages/webapp/src/containers/CashFlow/AccountTransactions/AccountTransactionsList.tsx @@ -29,28 +29,41 @@ function AccountTransactionsListRoot({ return ( - - - - - - - - - }> - - - - - - - - + + ); } +function AccountTransactionsMain() { + const { setScrollableRef } = useAccountTransactionsContext(); + + return ( + setScrollableRef(e)}> + + + + + + + + }> + + + + + ); +} + +function AccountTransactionsAside() { + return ( + + + + ); +} + export default R.compose( withBanking( ({ selectedUncategorizedTransactionId, openMatchingTransactionAside }) => ({ diff --git a/packages/webapp/src/containers/CashFlow/AccountTransactions/AccountTransactionsProvider.tsx b/packages/webapp/src/containers/CashFlow/AccountTransactions/AccountTransactionsProvider.tsx index ba4447c69..4266fae1c 100644 --- a/packages/webapp/src/containers/CashFlow/AccountTransactions/AccountTransactionsProvider.tsx +++ b/packages/webapp/src/containers/CashFlow/AccountTransactions/AccountTransactionsProvider.tsx @@ -1,5 +1,5 @@ // @ts-nocheck -import React from 'react'; +import React, { useRef, useState } from 'react'; import { useParams } from 'react-router-dom'; import { DashboardInsider } from '@/components'; import { useCashflowAccounts, useAccount } from '@/hooks/query'; @@ -41,6 +41,8 @@ function AccountTransactionsProvider({ query, ...props }) { isLoading: isBankAccountMetaSummaryLoading, } = useGetBankAccountSummaryMeta(accountId); + const [scrollableRef, setScrollableRef] = useState(); + // Provider payload. const provider = { accountId, @@ -56,6 +58,9 @@ function AccountTransactionsProvider({ query, ...props }) { filterTab, setFilterTab, + + scrollableRef, + setScrollableRef }; return ( diff --git a/packages/webapp/src/containers/CashFlow/AccountTransactions/ExcludedTransactions/ExcludedTransactionsTable.tsx b/packages/webapp/src/containers/CashFlow/AccountTransactions/ExcludedTransactions/ExcludedTransactionsTable.tsx index 90fa018f1..c84ec27aa 100644 --- a/packages/webapp/src/containers/CashFlow/AccountTransactions/ExcludedTransactions/ExcludedTransactionsTable.tsx +++ b/packages/webapp/src/containers/CashFlow/AccountTransactions/ExcludedTransactions/ExcludedTransactionsTable.tsx @@ -16,6 +16,7 @@ import { TABLES } from '@/constants/tables'; import { useMemorizedColumnsWidths } from '@/hooks'; import { useExcludedTransactionsColumns } from './_utils'; import { useExcludedTransactionsBoot } from './ExcludedTransactionsTableBoot'; +import { useAccountTransactionsContext } from '../AccountTransactionsProvider'; import { ActionsMenu } from './_components'; import { useUnexcludeUncategorizedTransaction } from '@/hooks/query/bank-rules'; @@ -37,6 +38,8 @@ function ExcludedTransactionsTableRoot({ const { mutateAsync: unexcludeBankTransaction } = useUnexcludeUncategorizedTransaction(); + const { scrollableRef } = useAccountTransactionsContext(); + // Retrieve table columns. const columns = useExcludedTransactionsColumns(); @@ -97,6 +100,7 @@ function ExcludedTransactionsTableRoot({ className="table-constrant" selectionColumn={true} onSelectedRowsChange={handleSelectedRowsChange} + windowScrollerProps={{ scrollElement: scrollableRef }} payload={{ onRestore: handleRestoreClick, }} diff --git a/packages/webapp/src/containers/CashFlow/AccountTransactions/RecognizedTransactions/RecognizedTransactionsTable.tsx b/packages/webapp/src/containers/CashFlow/AccountTransactions/RecognizedTransactions/RecognizedTransactionsTable.tsx index a3732eb6e..bfdf90d2e 100644 --- a/packages/webapp/src/containers/CashFlow/AccountTransactions/RecognizedTransactions/RecognizedTransactionsTable.tsx +++ b/packages/webapp/src/containers/CashFlow/AccountTransactions/RecognizedTransactions/RecognizedTransactionsTable.tsx @@ -20,6 +20,7 @@ import { useRecognizedTransactionsBoot } from './RecognizedTransactionsTableBoot import { ActionsMenu } from './_components'; import { compose } from '@/utils'; +import { useAccountTransactionsContext } from '../AccountTransactionsProvider'; import { useExcludeUncategorizedTransaction } from '@/hooks/query/bank-rules'; import { WithBankingActionsProps, @@ -49,6 +50,8 @@ function RecognizedTransactionsTableRoot({ const [initialColumnsWidths, , handleColumnResizing] = useMemorizedColumnsWidths(TABLES.UNCATEGORIZED_ACCOUNT_TRANSACTIONS); + const { scrollableRef } = useAccountTransactionsContext(); + // Handle cell click. const handleCellClick = (cell, event) => { setUncategorizedTransactionIdForMatching( @@ -102,6 +105,7 @@ function RecognizedTransactionsTableRoot({ vListOverscanRowCount={0} initialColumnsWidths={initialColumnsWidths} onColumnResizing={handleColumnResizing} + windowScrollerProps={{ scrollElement: scrollableRef }} noResults={} className="table-constrant" payload={{ diff --git a/packages/webapp/src/containers/CashFlow/AccountTransactions/UncategorizedTransactions/AccountTransactionsUncategorizedTable.tsx b/packages/webapp/src/containers/CashFlow/AccountTransactions/UncategorizedTransactions/AccountTransactionsUncategorizedTable.tsx index f2bd264bd..bd586adb4 100644 --- a/packages/webapp/src/containers/CashFlow/AccountTransactions/UncategorizedTransactions/AccountTransactionsUncategorizedTable.tsx +++ b/packages/webapp/src/containers/CashFlow/AccountTransactions/UncategorizedTransactions/AccountTransactionsUncategorizedTable.tsx @@ -22,6 +22,7 @@ import { useMemorizedColumnsWidths } from '@/hooks'; import { useAccountUncategorizedTransactionsContext } from '../AllTransactionsUncategorizedBoot'; import { useExcludeUncategorizedTransaction } from '@/hooks/query/bank-rules'; import { useAccountUncategorizedTransactionsColumns } from './hooks'; +import { useAccountTransactionsContext } from '../AccountTransactionsProvider'; import { compose } from '@/utils'; import { withBanking } from '../../withBanking'; @@ -48,6 +49,8 @@ function AccountTransactionsDataTable({ // Retrieve table columns. const columns = useAccountUncategorizedTransactionsColumns(); + const { scrollableRef } = useAccountTransactionsContext(); + // Retrieve list context. const { uncategorizedTransactions, isUncategorizedTransactionsLoading } = useAccountUncategorizedTransactionsContext(); @@ -124,6 +127,7 @@ function AccountTransactionsDataTable({ onCategorize: handleCategorizeBtnClick, }} onSelectedRowsChange={handleSelectedRowsChange} + windowScrollerProps={{ scrollElement: scrollableRef }} className={clsx('table-constrant', styles.table, { [styles.showCategorizeColumn]: enableMultipleCategorization, })} From f6350d3d61342ed9beb0a77556ae6d7d56047806 Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Mon, 5 Aug 2024 21:11:15 +0200 Subject: [PATCH 3/6] fix: Should not show the excluded transactions in recognized transactions --- .../BankAccounts/GetBankAccountSummary.ts | 42 +++++++++++-------- .../Cashflow/GetRecongizedTransactions.ts | 1 + 2 files changed, 25 insertions(+), 18 deletions(-) diff --git a/packages/server/src/services/Banking/BankAccounts/GetBankAccountSummary.ts b/packages/server/src/services/Banking/BankAccounts/GetBankAccountSummary.ts index cfa3c8d4c..b8c173dab 100644 --- a/packages/server/src/services/Banking/BankAccounts/GetBankAccountSummary.ts +++ b/packages/server/src/services/Banking/BankAccounts/GetBankAccountSummary.ts @@ -31,17 +31,21 @@ export class GetBankAccountSummary { .findById(bankAccountId) .throwIfNotFound(); + const commonQuery = (q) => { + // Include just the given account. + q.where('accountId', bankAccountId); + + // Only the not excluded. + q.modify('notExcluded'); + + // Only the not categorized. + q.modify('notCategorized'); + }; + // Retrieves the uncategorized transactions count of the given bank account. const uncategorizedTranasctionsCount = await UncategorizedCashflowTransaction.query().onBuild((q) => { - // Include just the given account. - q.where('accountId', bankAccountId); - - // Only the not excluded. - q.modify('notExcluded'); - - // Only the not categorized. - q.modify('notCategorized'); + commonQuery(q); // Only the not matched bank transactions. q.withGraphJoined('matchedBankTransactions'); @@ -52,16 +56,18 @@ export class GetBankAccountSummary { q.first(); }); - // Retrieves the recognized transactions count of the given bank account. - const recognizedTransactionsCount = await RecognizedBankTransaction.query() - .whereExists( - UncategorizedCashflowTransaction.query().where( - 'accountId', - bankAccountId - ) - ) - .count('id as total') - .first(); + // Retrives the recognized transactions count. + const recognizedTransactionsCount = + await UncategorizedCashflowTransaction.query().onBuild((q) => { + commonQuery(q); + + q.withGraphJoined('recognizedTransaction'); + q.whereNotNull('recognizedTransaction.id'); + + // Count the results. + q.count('uncategorized_cashflow_transactions.id as total'); + q.first(); + }); const totalUncategorizedTransactions = uncategorizedTranasctionsCount?.total || 0; diff --git a/packages/server/src/services/Cashflow/GetRecongizedTransactions.ts b/packages/server/src/services/Cashflow/GetRecongizedTransactions.ts index 3bc6ac4fc..9ba2bd102 100644 --- a/packages/server/src/services/Cashflow/GetRecongizedTransactions.ts +++ b/packages/server/src/services/Cashflow/GetRecongizedTransactions.ts @@ -34,6 +34,7 @@ export class GetRecognizedTransactionsService { q.withGraphFetched('recognizedTransaction.assignAccount'); q.withGraphFetched('recognizedTransaction.bankRule'); q.whereNotNull('recognizedTransactionId'); + q.modify('notExcluded'); if (_filter.accountId) { q.where('accountId', _filter.accountId); From c7a85c4cf879a5099d86bf3ad0b41e1b923ef883 Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Mon, 5 Aug 2024 21:20:11 +0200 Subject: [PATCH 4/6] fix: categorize transactions on recognized transactions table --- .../RecognizedTransactionsTable.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/webapp/src/containers/CashFlow/AccountTransactions/RecognizedTransactions/RecognizedTransactionsTable.tsx b/packages/webapp/src/containers/CashFlow/AccountTransactions/RecognizedTransactions/RecognizedTransactionsTable.tsx index bfdf90d2e..979e65092 100644 --- a/packages/webapp/src/containers/CashFlow/AccountTransactions/RecognizedTransactions/RecognizedTransactionsTable.tsx +++ b/packages/webapp/src/containers/CashFlow/AccountTransactions/RecognizedTransactions/RecognizedTransactionsTable.tsx @@ -34,8 +34,8 @@ interface RecognizedTransactionsTableProps extends WithBankingActionsProps {} * Renders the recognized account transactions datatable. */ function RecognizedTransactionsTableRoot({ - // #withBanking - setUncategorizedTransactionIdForMatching, + // #withBankingActions + setTransactionsToCategorizeSelected, }: RecognizedTransactionsTableProps) { const { mutateAsync: excludeBankTransaction } = useExcludeUncategorizedTransaction(); @@ -54,7 +54,7 @@ function RecognizedTransactionsTableRoot({ // Handle cell click. const handleCellClick = (cell, event) => { - setUncategorizedTransactionIdForMatching( + setTransactionsToCategorizeSelected( cell.row.original.uncategorized_transaction_id, ); }; @@ -77,7 +77,7 @@ function RecognizedTransactionsTableRoot({ // Handles categorize button click. const handleCategorizeClick = (transaction) => { - setUncategorizedTransactionIdForMatching( + setTransactionsToCategorizeSelected( transaction.uncategorized_transaction_id, ); }; From a09fe26df7c6ca2b3d2a02caa71d95dfca6d8914 Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Mon, 5 Aug 2024 21:36:34 +0200 Subject: [PATCH 5/6] fix: group query key constants in seperate file --- .../src/constants/query-keys/banking.ts | 10 ++++ packages/webapp/src/hooks/query/bank-rules.ts | 49 ++++++++----------- packages/webapp/src/hooks/query/import.ts | 6 ++- 3 files changed, 35 insertions(+), 30 deletions(-) create mode 100644 packages/webapp/src/constants/query-keys/banking.ts diff --git a/packages/webapp/src/constants/query-keys/banking.ts b/packages/webapp/src/constants/query-keys/banking.ts new file mode 100644 index 000000000..7164470e2 --- /dev/null +++ b/packages/webapp/src/constants/query-keys/banking.ts @@ -0,0 +1,10 @@ +export const BANK_QUERY_KEY = { + BANK_RULES: 'BANK_RULE', + BANK_TRANSACTION_MATCHES: 'BANK_TRANSACTION_MATCHES', + RECOGNIZED_BANK_TRANSACTION: 'RECOGNIZED_BANK_TRANSACTION', + EXCLUDED_BANK_TRANSACTIONS_INFINITY: 'EXCLUDED_BANK_TRANSACTIONS_INFINITY', + RECOGNIZED_BANK_TRANSACTIONS_INFINITY: + 'RECOGNIZED_BANK_TRANSACTIONS_INFINITY', + BANK_ACCOUNT_SUMMARY_META: 'BANK_ACCOUNT_SUMMARY_META', + AUTOFILL_CATEGORIZE_BANK_TRANSACTION: 'AUTOFILL_CATEGORIZE_BANK_TRANSACTION', +}; diff --git a/packages/webapp/src/hooks/query/bank-rules.ts b/packages/webapp/src/hooks/query/bank-rules.ts index 92103bd2f..512f6e423 100644 --- a/packages/webapp/src/hooks/query/bank-rules.ts +++ b/packages/webapp/src/hooks/query/bank-rules.ts @@ -13,21 +13,12 @@ import { import useApiRequest from '../useRequest'; import { transformToCamelCase } from '@/utils'; import t from './types'; +import { BANK_QUERY_KEY } from '@/constants/query-keys/banking'; -const QUERY_KEY = { - BANK_RULES: 'BANK_RULE', - BANK_TRANSACTION_MATCHES: 'BANK_TRANSACTION_MATCHES', - RECOGNIZED_BANK_TRANSACTION: 'RECOGNIZED_BANK_TRANSACTION', - EXCLUDED_BANK_TRANSACTIONS_INFINITY: 'EXCLUDED_BANK_TRANSACTIONS_INFINITY', - RECOGNIZED_BANK_TRANSACTIONS_INFINITY: - 'RECOGNIZED_BANK_TRANSACTIONS_INFINITY', - BANK_ACCOUNT_SUMMARY_META: 'BANK_ACCOUNT_SUMMARY_META', - AUTOFILL_CATEGORIZE_BANK_TRANSACTION: 'AUTOFILL_CATEGORIZE_BANK_TRANSACTION', -}; - +// Common cache invalidator. const commonInvalidateQueries = (query: QueryClient) => { - query.invalidateQueries(QUERY_KEY.BANK_RULES); - query.invalidateQueries(QUERY_KEY.RECOGNIZED_BANK_TRANSACTIONS_INFINITY); + query.invalidateQueries(BANK_QUERY_KEY.BANK_RULES); + query.invalidateQueries(BANK_QUERY_KEY.RECOGNIZED_BANK_TRANSACTIONS_INFINITY); }; interface CreateBankRuleValues { @@ -185,7 +176,7 @@ export function useDeleteBankRule( commonInvalidateQueries(queryClient); queryClient.invalidateQueries( - QUERY_KEY.RECOGNIZED_BANK_TRANSACTIONS_INFINITY, + BANK_QUERY_KEY.RECOGNIZED_BANK_TRANSACTIONS_INFINITY, ); queryClient.invalidateQueries([ t.CASHFLOW_ACCOUNT_UNCATEGORIZED_TRANSACTIONS_INFINITY, @@ -209,7 +200,7 @@ export function useBankRules( const apiRequest = useApiRequest(); return useQuery( - [QUERY_KEY.BANK_RULES], + [BANK_QUERY_KEY.BANK_RULES], () => apiRequest.get('/banking/rules').then((res) => res.data.bank_rules), { ...options }, ); @@ -230,7 +221,7 @@ export function useBankRule( const apiRequest = useApiRequest(); return useQuery( - [QUERY_KEY.BANK_RULES, bankRuleId], + [BANK_QUERY_KEY.BANK_RULES, bankRuleId], () => apiRequest .get(`/banking/rules/${bankRuleId}`) @@ -260,7 +251,7 @@ export function useGetBankTransactionsMatches( const apiRequest = useApiRequest(); return useQuery( - [QUERY_KEY.BANK_TRANSACTION_MATCHES, uncategorizeTransactionsIds], + [BANK_QUERY_KEY.BANK_TRANSACTION_MATCHES, uncategorizeTransactionsIds], () => apiRequest .get(`/cashflow/transactions/matches`, { @@ -273,7 +264,9 @@ export function useGetBankTransactionsMatches( const onValidateExcludeUncategorizedTransaction = (queryClient) => { // Invalidate queries. - queryClient.invalidateQueries(QUERY_KEY.EXCLUDED_BANK_TRANSACTIONS_INFINITY); + queryClient.invalidateQueries( + BANK_QUERY_KEY.EXCLUDED_BANK_TRANSACTIONS_INFINITY, + ); queryClient.invalidateQueries( t.CASHFLOW_ACCOUNT_UNCATEGORIZED_TRANSACTIONS_INFINITY, ); @@ -282,7 +275,7 @@ const onValidateExcludeUncategorizedTransaction = (queryClient) => { queryClient.invalidateQueries(t.ACCOUNT); // invalidate bank account summary. - queryClient.invalidateQueries(QUERY_KEY.BANK_ACCOUNT_SUMMARY_META); + queryClient.invalidateQueries(BANK_QUERY_KEY.BANK_ACCOUNT_SUMMARY_META); }; type ExcludeUncategorizedTransactionValue = number; @@ -320,7 +313,7 @@ export function useExcludeUncategorizedTransaction( onSuccess: (res, id) => { onValidateExcludeUncategorizedTransaction(queryClient); queryClient.invalidateQueries([ - QUERY_KEY.BANK_ACCOUNT_SUMMARY_META, + BANK_QUERY_KEY.BANK_ACCOUNT_SUMMARY_META, id, ]); }, @@ -365,7 +358,7 @@ export function useUnexcludeUncategorizedTransaction( onSuccess: (res, id) => { onValidateExcludeUncategorizedTransaction(queryClient); queryClient.invalidateQueries([ - QUERY_KEY.BANK_ACCOUNT_SUMMARY_META, + BANK_QUERY_KEY.BANK_ACCOUNT_SUMMARY_META, id, ]); }, @@ -491,7 +484,7 @@ export function useMatchUncategorizedTransaction( queryClient.invalidateQueries(t.ACCOUNT); // Invalidate bank account summary. - queryClient.invalidateQueries(QUERY_KEY.BANK_ACCOUNT_SUMMARY_META); + queryClient.invalidateQueries(BANK_QUERY_KEY.BANK_ACCOUNT_SUMMARY_META); }, ...props, }); @@ -537,7 +530,7 @@ export function useUnmatchMatchedUncategorizedTransaction( queryClient.invalidateQueries(t.ACCOUNT); // Invalidate bank account summary. - queryClient.invalidateQueries(QUERY_KEY.BANK_ACCOUNT_SUMMARY_META); + queryClient.invalidateQueries(BANK_QUERY_KEY.BANK_ACCOUNT_SUMMARY_META); }, ...props, }); @@ -558,7 +551,7 @@ export function useGetRecognizedBankTransaction( const apiRequest = useApiRequest(); return useQuery( - [QUERY_KEY.RECOGNIZED_BANK_TRANSACTION, uncategorizedTransactionId], + [BANK_QUERY_KEY.RECOGNIZED_BANK_TRANSACTION, uncategorizedTransactionId], () => apiRequest .get(`/banking/recognized/transactions/${uncategorizedTransactionId}`) @@ -586,7 +579,7 @@ export function useGetBankAccountSummaryMeta( const apiRequest = useApiRequest(); return useQuery( - [QUERY_KEY.BANK_ACCOUNT_SUMMARY_META, bankAccountId], + [BANK_QUERY_KEY.BANK_ACCOUNT_SUMMARY_META, bankAccountId], () => apiRequest .get(`/banking/bank_accounts/${bankAccountId}/meta`) @@ -618,7 +611,7 @@ export function useGetAutofillCategorizeTransaction( return useQuery( [ - QUERY_KEY.AUTOFILL_CATEGORIZE_BANK_TRANSACTION, + BANK_QUERY_KEY.AUTOFILL_CATEGORIZE_BANK_TRANSACTION, uncategorizedTransactionIds, ], () => @@ -642,7 +635,7 @@ export function useRecognizedBankTransactionsInfinity( const apiRequest = useApiRequest(); return useInfiniteQuery( - [QUERY_KEY.RECOGNIZED_BANK_TRANSACTIONS_INFINITY, query], + [BANK_QUERY_KEY.RECOGNIZED_BANK_TRANSACTIONS_INFINITY, query], async ({ pageParam = 1 }) => { const response = await apiRequest.http({ ...axios, @@ -674,7 +667,7 @@ export function useExcludedBankTransactionsInfinity( const apiRequest = useApiRequest(); return useInfiniteQuery( - [QUERY_KEY.EXCLUDED_BANK_TRANSACTIONS_INFINITY, query], + [BANK_QUERY_KEY.EXCLUDED_BANK_TRANSACTIONS_INFINITY, query], async ({ pageParam = 1 }) => { const response = await apiRequest.http({ ...axios, diff --git a/packages/webapp/src/hooks/query/import.ts b/packages/webapp/src/hooks/query/import.ts index 2c50d6851..62135b86c 100644 --- a/packages/webapp/src/hooks/query/import.ts +++ b/packages/webapp/src/hooks/query/import.ts @@ -9,6 +9,7 @@ import useApiRequest from '../useRequest'; import { transformToCamelCase } from '@/utils'; import { downloadFile, useDownloadFile } from '../useDownloadFile'; import T from './types'; +import { BANK_QUERY_KEY } from '@/constants/query-keys/banking'; const QueryKeys = { ImportPreview: 'ImportPreview', @@ -127,8 +128,8 @@ export const useSampleSheetImport = () => { /** * Invalidates resources cached queries based on the given resource name, - * @param queryClient - * @param resource + * @param queryClient + * @param resource */ const invalidateResourcesOnImport = ( queryClient: QueryClient, @@ -215,6 +216,7 @@ const invalidateResourcesOnImport = ( T.CASHFLOW_ACCOUNT_UNCATEGORIZED_TRANSACTIONS_INFINITY, ); queryClient.invalidateQueries(T.CASHFLOW_UNCAATEGORIZED_TRANSACTION); + queryClient.invalidateQueries(BANK_QUERY_KEY.BANK_ACCOUNT_SUMMARY_META); break; } }; From 521b083ed7da1901c798e115c3a87a72dc8957eb Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Mon, 5 Aug 2024 22:50:58 +0200 Subject: [PATCH 6/6] fix: retrieve the excluded transactions count --- .../Banking/BankAccounts/GetBankAccountSummary.ts | 14 ++++++++++++++ .../AccountTransactionsFilterTabs.tsx | 11 ++++++++--- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/packages/server/src/services/Banking/BankAccounts/GetBankAccountSummary.ts b/packages/server/src/services/Banking/BankAccounts/GetBankAccountSummary.ts index b8c173dab..29c68abb6 100644 --- a/packages/server/src/services/Banking/BankAccounts/GetBankAccountSummary.ts +++ b/packages/server/src/services/Banking/BankAccounts/GetBankAccountSummary.ts @@ -1,6 +1,7 @@ import { Inject, Service } from 'typedi'; import { initialize } from 'objection'; import HasTenancyService from '@/services/Tenancy/TenancyService'; +import { UncategorizedTransactionTransformer } from '@/services/Cashflow/UncategorizedTransactionTransformer'; @Service() export class GetBankAccountSummary { @@ -69,14 +70,27 @@ export class GetBankAccountSummary { q.first(); }); + const excludedTransactionsCount = + await UncategorizedCashflowTransaction.query().onBuild((q) => { + q.where('accountId', bankAccountId); + q.modify('excluded'); + + // Count the results. + q.count('uncategorized_cashflow_transactions.id as total'); + q.first(); + }); + const totalUncategorizedTransactions = uncategorizedTranasctionsCount?.total || 0; const totalRecognizedTransactions = recognizedTransactionsCount?.total || 0; + const totalExcludedTransactions = excludedTransactionsCount?.total || 0; + return { name: bankAccount.name, totalUncategorizedTransactions, totalRecognizedTransactions, + totalExcludedTransactions, }; } } diff --git a/packages/webapp/src/containers/CashFlow/AccountTransactions/AccountTransactionsFilterTabs.tsx b/packages/webapp/src/containers/CashFlow/AccountTransactions/AccountTransactionsFilterTabs.tsx index c2d40059d..3cd798feb 100644 --- a/packages/webapp/src/containers/CashFlow/AccountTransactions/AccountTransactionsFilterTabs.tsx +++ b/packages/webapp/src/containers/CashFlow/AccountTransactions/AccountTransactionsFilterTabs.tsx @@ -1,4 +1,5 @@ // @ts-nocheck +import { useMemo } from 'react'; import styled from 'styled-components'; import { ContentTabs } from '@/components/ContentTabs/ContentTabs'; import { useAccountTransactionsContext } from './AccountTransactionsProvider'; @@ -8,15 +9,19 @@ const AccountContentTabs = styled(ContentTabs)` `; export function AccountTransactionsFilterTabs() { - const { filterTab, setFilterTab, currentAccount } = + const { filterTab, setFilterTab, bankAccountMetaSummary, currentAccount } = useAccountTransactionsContext(); const handleChange = (value) => { setFilterTab(value); }; - const hasUncategorizedTransx = Boolean( - currentAccount.uncategorized_transactions, + // Detarmines whether show the uncategorized transactions tab. + const hasUncategorizedTransx = useMemo( + () => + bankAccountMetaSummary?.totalUncategorizedTransactions > 0 || + bankAccountMetaSummary?.totalExcludedTransactions > 0, + [bankAccountMetaSummary], ); return (