Merge pull request #564 from bigcapitalhq/fix-banking-bugs

fix: Banking service bugs
This commit is contained in:
Ahmed Bouhuolia
2024-08-05 23:06:03 +02:00
committed by GitHub
20 changed files with 276 additions and 128 deletions

View File

@@ -20,6 +20,7 @@ export default class UncategorizedCashflowTransaction extends mixin(
description!: string; description!: string;
plaidTransactionId!: string; plaidTransactionId!: string;
recognizedTransactionId!: number; recognizedTransactionId!: number;
excludedAt: Date;
/** /**
* Table name. * Table name.
@@ -45,6 +46,7 @@ export default class UncategorizedCashflowTransaction extends mixin(
'isDepositTransaction', 'isDepositTransaction',
'isWithdrawalTransaction', 'isWithdrawalTransaction',
'isRecognized', 'isRecognized',
'isExcluded'
]; ];
} }
@@ -89,6 +91,14 @@ export default class UncategorizedCashflowTransaction extends mixin(
return !!this.recognizedTransactionId; return !!this.recognizedTransactionId;
} }
/**
* Detarmines whether the transaction is excluded.
* @returns {boolean}
*/
public get isExcluded(): boolean {
return !!this.excludedAt;
}
/** /**
* Model modifiers. * Model modifiers.
*/ */

View File

@@ -1,6 +1,7 @@
import { Inject, Service } from 'typedi'; import { Inject, Service } from 'typedi';
import { initialize } from 'objection'; import { initialize } from 'objection';
import HasTenancyService from '@/services/Tenancy/TenancyService'; import HasTenancyService from '@/services/Tenancy/TenancyService';
import { UncategorizedTransactionTransformer } from '@/services/Cashflow/UncategorizedTransactionTransformer';
@Service() @Service()
export class GetBankAccountSummary { export class GetBankAccountSummary {
@@ -31,17 +32,21 @@ export class GetBankAccountSummary {
.findById(bankAccountId) .findById(bankAccountId)
.throwIfNotFound(); .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. // Retrieves the uncategorized transactions count of the given bank account.
const uncategorizedTranasctionsCount = const uncategorizedTranasctionsCount =
await UncategorizedCashflowTransaction.query().onBuild((q) => { await UncategorizedCashflowTransaction.query().onBuild((q) => {
// Include just the given account. commonQuery(q);
q.where('accountId', bankAccountId);
// Only the not excluded.
q.modify('notExcluded');
// Only the not categorized.
q.modify('notCategorized');
// Only the not matched bank transactions. // Only the not matched bank transactions.
q.withGraphJoined('matchedBankTransactions'); q.withGraphJoined('matchedBankTransactions');
@@ -52,25 +57,40 @@ export class GetBankAccountSummary {
q.first(); q.first();
}); });
// Retrieves the recognized transactions count of the given bank account. // Retrives the recognized transactions count.
const recognizedTransactionsCount = await RecognizedBankTransaction.query() const recognizedTransactionsCount =
.whereExists( await UncategorizedCashflowTransaction.query().onBuild((q) => {
UncategorizedCashflowTransaction.query().where( commonQuery(q);
'accountId',
bankAccountId q.withGraphJoined('recognizedTransaction');
) q.whereNotNull('recognizedTransaction.id');
)
.count('id as total') // Count the results.
.first(); q.count('uncategorized_cashflow_transactions.id as total');
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 = const totalUncategorizedTransactions =
uncategorizedTranasctionsCount?.total || 0; uncategorizedTranasctionsCount?.total || 0;
const totalRecognizedTransactions = recognizedTransactionsCount?.total || 0; const totalRecognizedTransactions = recognizedTransactionsCount?.total || 0;
const totalExcludedTransactions = excludedTransactionsCount?.total || 0;
return { return {
name: bankAccount.name, name: bankAccount.name,
totalUncategorizedTransactions, totalUncategorizedTransactions,
totalRecognizedTransactions, totalRecognizedTransactions,
totalExcludedTransactions,
}; };
} }
} }

View File

@@ -1,7 +1,11 @@
import { Knex } from 'knex';
import { Inject, Service } from 'typedi';
import HasTenancyService from '@/services/Tenancy/TenancyService'; import HasTenancyService from '@/services/Tenancy/TenancyService';
import UnitOfWork from '@/services/UnitOfWork'; import UnitOfWork from '@/services/UnitOfWork';
import { Inject, Service } from 'typedi'; import {
import { validateTransactionNotCategorized } from './utils'; validateTransactionNotCategorized,
validateTransactionNotExcluded,
} from './utils';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher'; import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import events from '@/subscribers/events'; import events from '@/subscribers/events';
import { import {
@@ -37,9 +41,13 @@ export class ExcludeBankTransaction {
.findById(uncategorizedTransactionId) .findById(uncategorizedTransactionId)
.throwIfNotFound(); .throwIfNotFound();
// Validate the transaction shouldn't be excluded.
validateTransactionNotExcluded(oldUncategorizedTransaction);
// Validate the transaction shouldn't be categorized.
validateTransactionNotCategorized(oldUncategorizedTransaction); 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, { await this.eventPublisher.emitAsync(events.bankTransactions.onExcluding, {
tenantId, tenantId,
uncategorizedTransactionId, uncategorizedTransactionId,

View File

@@ -1,6 +1,6 @@
import { Inject, Service } from 'typedi'; import { Inject, Service } from 'typedi';
import PromisePool from '@supercharge/promise-pool'; import PromisePool from '@supercharge/promise-pool';
import { castArray } from 'lodash'; import { castArray, uniq } from 'lodash';
import { ExcludeBankTransaction } from './ExcludeBankTransaction'; import { ExcludeBankTransaction } from './ExcludeBankTransaction';
@Service() @Service()
@@ -18,7 +18,7 @@ export class ExcludeBankTransactions {
tenantId: number, tenantId: number,
bankTransactionIds: Array<number> | number bankTransactionIds: Array<number> | number
) { ) {
const _bankTransactionIds = castArray(bankTransactionIds); const _bankTransactionIds = uniq(castArray(bankTransactionIds));
await PromisePool.withConcurrency(1) await PromisePool.withConcurrency(1)
.for(_bankTransactionIds) .for(_bankTransactionIds)

View File

@@ -1,7 +1,11 @@
import { Knex } from 'knex';
import { Inject, Service } from 'typedi';
import HasTenancyService from '@/services/Tenancy/TenancyService'; import HasTenancyService from '@/services/Tenancy/TenancyService';
import UnitOfWork from '@/services/UnitOfWork'; import UnitOfWork from '@/services/UnitOfWork';
import { Inject, Service } from 'typedi'; import {
import { validateTransactionNotCategorized } from './utils'; validateTransactionNotCategorized,
validateTransactionShouldBeExcluded,
} from './utils';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher'; import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import events from '@/subscribers/events'; import events from '@/subscribers/events';
import { import {
@@ -37,9 +41,13 @@ export class UnexcludeBankTransaction {
.findById(uncategorizedTransactionId) .findById(uncategorizedTransactionId)
.throwIfNotFound(); .throwIfNotFound();
// Validate the transaction should be excludded.
validateTransactionShouldBeExcluded(oldUncategorizedTransaction);
// Validate the transaction shouldn't be categorized.
validateTransactionNotCategorized(oldUncategorizedTransaction); validateTransactionNotCategorized(oldUncategorizedTransaction);
return this.uow.withTransaction(tenantId, async (trx) => { return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
await this.eventPublisher.emitAsync( await this.eventPublisher.emitAsync(
events.bankTransactions.onUnexcluding, events.bankTransactions.onUnexcluding,
{ {

View File

@@ -1,7 +1,7 @@
import { Inject, Service } from 'typedi'; import { Inject, Service } from 'typedi';
import PromisePool from '@supercharge/promise-pool'; import PromisePool from '@supercharge/promise-pool';
import { UnexcludeBankTransaction } from './UnexcludeBankTransaction'; import { UnexcludeBankTransaction } from './UnexcludeBankTransaction';
import { castArray } from 'lodash'; import { castArray, uniq } from 'lodash';
@Service() @Service()
export class UnexcludeBankTransactions { export class UnexcludeBankTransactions {
@@ -17,7 +17,7 @@ export class UnexcludeBankTransactions {
tenantId: number, tenantId: number,
bankTransactionIds: Array<number> | number bankTransactionIds: Array<number> | number
) { ) {
const _bankTransactionIds = castArray(bankTransactionIds); const _bankTransactionIds = uniq(castArray(bankTransactionIds));
await PromisePool.withConcurrency(1) await PromisePool.withConcurrency(1)
.for(_bankTransactionIds) .for(_bankTransactionIds)

View File

@@ -3,6 +3,8 @@ import UncategorizedCashflowTransaction from '@/models/UncategorizedCashflowTran
const ERRORS = { const ERRORS = {
TRANSACTION_ALREADY_CATEGORIZED: 'TRANSACTION_ALREADY_CATEGORIZED', TRANSACTION_ALREADY_CATEGORIZED: 'TRANSACTION_ALREADY_CATEGORIZED',
TRANSACTION_ALREADY_EXCLUDED: 'TRANSACTION_ALREADY_EXCLUDED',
TRANSACTION_NOT_EXCLUDED: 'TRANSACTION_NOT_EXCLUDED',
}; };
export const validateTransactionNotCategorized = ( export const validateTransactionNotCategorized = (
@@ -12,3 +14,19 @@ export const validateTransactionNotCategorized = (
throw new ServiceError(ERRORS.TRANSACTION_ALREADY_CATEGORIZED); 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);
}
};

View File

@@ -34,6 +34,7 @@ export class GetRecognizedTransactionsService {
q.withGraphFetched('recognizedTransaction.assignAccount'); q.withGraphFetched('recognizedTransaction.assignAccount');
q.withGraphFetched('recognizedTransaction.bankRule'); q.withGraphFetched('recognizedTransaction.bankRule');
q.whereNotNull('recognizedTransactionId'); q.whereNotNull('recognizedTransactionId');
q.modify('notExcluded');
if (_filter.accountId) { if (_filter.accountId) {
q.where('accountId', _filter.accountId); q.where('accountId', _filter.accountId);

View File

@@ -1,5 +1,9 @@
import React from 'react'; // @ts-nocheck
import { AppShellProvider, useAppShellContext } from './AppContentShellProvider'; import React, { forwardRef, Ref } from 'react';
import {
AppShellProvider,
useAppShellContext,
} from './AppContentShellProvider';
import { Box, BoxProps } from '../../Layout'; import { Box, BoxProps } from '../../Layout';
import styles from './AppContentShell.module.scss'; import styles from './AppContentShell.module.scss';
@@ -12,50 +16,73 @@ interface AppContentShellProps {
hideMain?: boolean; hideMain?: boolean;
} }
export function AppContentShell({ export const AppContentShell = forwardRef(
asideProps, (
mainProps, {
topbarOffset = 0, asideProps,
hideAside = false, mainProps,
hideMain = false, topbarOffset = 0,
...restProps hideAside = false,
}: AppContentShellProps) { hideMain = false,
return ( ...restProps
<AppShellProvider }: AppContentShellProps,
mainProps={mainProps} ref: Ref<HTMLDivElement>,
asideProps={asideProps} ) => {
topbarOffset={topbarOffset} return (
hideAside={hideAside} <AppShellProvider
hideMain={hideMain} mainProps={mainProps}
> asideProps={asideProps}
<Box {...restProps} className={styles.root} /> topbarOffset={topbarOffset}
</AppShellProvider> hideAside={hideAside}
); hideMain={hideMain}
} >
<Box {...restProps} className={styles.root} ref={ref} />
</AppShellProvider>
);
},
);
AppContentShell.displayName = 'AppContentShell';
interface AppContentShellMainProps extends BoxProps {} 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<HTMLDivElement>) => {
const { hideMain } = useAppShellContext();
if (hideMain === true) { if (hideMain === true) {
return null; return null;
} }
return <Box {...props} className={styles.main} />; return <Box {...props} className={styles.main} ref={ref} />;
} },
);
AppContentShellMain.displayName = 'AppContentShellMain';
interface AppContentShellAsideProps extends BoxProps { interface AppContentShellAsideProps extends BoxProps {
children: React.ReactNode; 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<HTMLDivElement>) => {
const { hideAside } = useAppShellContext();
if (hideAside === true) { if (hideAside === true) {
return null; return null;
} }
return <Box {...props} className={styles.aside} />; return <Box {...props} className={styles.aside} ref={ref} />;
} },
);
AppContentShellAside.displayName = 'AppContentShellAside';
AppContentShell.Main = AppContentShellMain; AppContentShell.Main = AppContentShellMain;
AppContentShell.Aside = AppContentShellAside; AppContentShell.Aside = AppContentShellAside;

View File

@@ -25,14 +25,13 @@ function TableVirtualizedListRow({ index, isScrolling, isVisible, style }) {
export function TableVirtualizedListRows() { export function TableVirtualizedListRows() {
const { const {
table: { page }, table: { page },
props: { vListrowHeight, vListOverscanRowCount }, props: { vListrowHeight, vListOverscanRowCount, windowScrollerProps },
} = useContext(TableContext); } = useContext(TableContext);
// Dashboard content pane. // Dashboard content pane.
const dashboardContentPane = React.useMemo( const scrollElement =
() => document.querySelector(`.${CLASSES.DASHBOARD_CONTENT_PANE}`), windowScrollerProps?.scrollElement ||
[], document.querySelector(`.${CLASSES.DASHBOARD_CONTENT_PANE}`);
);
const rowRenderer = React.useCallback( const rowRenderer = React.useCallback(
({ key, ...args }) => <TableVirtualizedListRow {...args} key={key} />, ({ key, ...args }) => <TableVirtualizedListRow {...args} key={key} />,
@@ -40,7 +39,7 @@ export function TableVirtualizedListRows() {
); );
return ( return (
<WindowScroller scrollElement={dashboardContentPane}> <WindowScroller scrollElement={scrollElement}>
{({ height, isScrolling, onChildScroll, scrollTop }) => ( {({ height, isScrolling, onChildScroll, scrollTop }) => (
<AutoSizer disableHeight> <AutoSizer disableHeight>
{({ width }) => ( {({ width }) => (

View File

@@ -1,12 +1,15 @@
import React from 'react'; import React, { forwardRef, Ref } from 'react';
import { HTMLDivProps, Props } from '@blueprintjs/core'; import { HTMLDivProps, Props } from '@blueprintjs/core';
export interface BoxProps extends Props, HTMLDivProps { export interface BoxProps extends Props, HTMLDivProps {
className?: string; className?: string;
} }
export function Box({ className, ...rest }: BoxProps) { export const Box = forwardRef(
const Element = 'div'; ({ className, ...rest }: BoxProps, ref: Ref<HTMLDivElement>) => {
const Element = 'div';
return <Element className={className} {...rest} />; return <Element className={className} ref={ref} {...rest} />;
} },
);
Box.displayName = '@bigcapital/Box';

View File

@@ -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',
};

View File

@@ -1,4 +1,5 @@
// @ts-nocheck // @ts-nocheck
import { useMemo } from 'react';
import styled from 'styled-components'; import styled from 'styled-components';
import { ContentTabs } from '@/components/ContentTabs/ContentTabs'; import { ContentTabs } from '@/components/ContentTabs/ContentTabs';
import { useAccountTransactionsContext } from './AccountTransactionsProvider'; import { useAccountTransactionsContext } from './AccountTransactionsProvider';
@@ -8,15 +9,19 @@ const AccountContentTabs = styled(ContentTabs)`
`; `;
export function AccountTransactionsFilterTabs() { export function AccountTransactionsFilterTabs() {
const { filterTab, setFilterTab, currentAccount } = const { filterTab, setFilterTab, bankAccountMetaSummary, currentAccount } =
useAccountTransactionsContext(); useAccountTransactionsContext();
const handleChange = (value) => { const handleChange = (value) => {
setFilterTab(value); setFilterTab(value);
}; };
const hasUncategorizedTransx = Boolean( // Detarmines whether show the uncategorized transactions tab.
currentAccount.uncategorized_transactions, const hasUncategorizedTransx = useMemo(
() =>
bankAccountMetaSummary?.totalUncategorizedTransactions > 0 ||
bankAccountMetaSummary?.totalExcludedTransactions > 0,
[bankAccountMetaSummary],
); );
return ( return (

View File

@@ -29,28 +29,41 @@ function AccountTransactionsListRoot({
return ( return (
<AccountTransactionsProvider> <AccountTransactionsProvider>
<AppContentShell hideAside={!openMatchingTransactionAside}> <AppContentShell hideAside={!openMatchingTransactionAside}>
<AppContentShell.Main> <AccountTransactionsMain />
<AccountTransactionsActionsBar /> <AccountTransactionsAside />
<AccountTransactionsDetailsBar />
<AccountTransactionsProgressBar />
<DashboardPageContent>
<AccountTransactionsFilterTabs />
<Suspense fallback={<Spinner size={30} />}>
<AccountTransactionsContent />
</Suspense>
</DashboardPageContent>
</AppContentShell.Main>
<AppContentShell.Aside>
<CategorizeTransactionAside />
</AppContentShell.Aside>
</AppContentShell> </AppContentShell>
</AccountTransactionsProvider> </AccountTransactionsProvider>
); );
} }
function AccountTransactionsMain() {
const { setScrollableRef } = useAccountTransactionsContext();
return (
<AppContentShell.Main ref={(e) => setScrollableRef(e)}>
<AccountTransactionsActionsBar />
<AccountTransactionsDetailsBar />
<AccountTransactionsProgressBar />
<DashboardPageContent>
<AccountTransactionsFilterTabs />
<Suspense fallback={<Spinner size={30} />}>
<AccountTransactionsContent />
</Suspense>
</DashboardPageContent>
</AppContentShell.Main>
);
}
function AccountTransactionsAside() {
return (
<AppContentShell.Aside>
<CategorizeTransactionAside />
</AppContentShell.Aside>
);
}
export default R.compose( export default R.compose(
withBanking( withBanking(
({ selectedUncategorizedTransactionId, openMatchingTransactionAside }) => ({ ({ selectedUncategorizedTransactionId, openMatchingTransactionAside }) => ({

View File

@@ -1,5 +1,5 @@
// @ts-nocheck // @ts-nocheck
import React from 'react'; import React, { useRef, useState } from 'react';
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
import { DashboardInsider } from '@/components'; import { DashboardInsider } from '@/components';
import { useCashflowAccounts, useAccount } from '@/hooks/query'; import { useCashflowAccounts, useAccount } from '@/hooks/query';
@@ -41,6 +41,8 @@ function AccountTransactionsProvider({ query, ...props }) {
isLoading: isBankAccountMetaSummaryLoading, isLoading: isBankAccountMetaSummaryLoading,
} = useGetBankAccountSummaryMeta(accountId); } = useGetBankAccountSummaryMeta(accountId);
const [scrollableRef, setScrollableRef] = useState();
// Provider payload. // Provider payload.
const provider = { const provider = {
accountId, accountId,
@@ -56,6 +58,9 @@ function AccountTransactionsProvider({ query, ...props }) {
filterTab, filterTab,
setFilterTab, setFilterTab,
scrollableRef,
setScrollableRef
}; };
return ( return (

View File

@@ -16,6 +16,7 @@ import { TABLES } from '@/constants/tables';
import { useMemorizedColumnsWidths } from '@/hooks'; import { useMemorizedColumnsWidths } from '@/hooks';
import { useExcludedTransactionsColumns } from './_utils'; import { useExcludedTransactionsColumns } from './_utils';
import { useExcludedTransactionsBoot } from './ExcludedTransactionsTableBoot'; import { useExcludedTransactionsBoot } from './ExcludedTransactionsTableBoot';
import { useAccountTransactionsContext } from '../AccountTransactionsProvider';
import { ActionsMenu } from './_components'; import { ActionsMenu } from './_components';
import { useUnexcludeUncategorizedTransaction } from '@/hooks/query/bank-rules'; import { useUnexcludeUncategorizedTransaction } from '@/hooks/query/bank-rules';
@@ -37,6 +38,8 @@ function ExcludedTransactionsTableRoot({
const { mutateAsync: unexcludeBankTransaction } = const { mutateAsync: unexcludeBankTransaction } =
useUnexcludeUncategorizedTransaction(); useUnexcludeUncategorizedTransaction();
const { scrollableRef } = useAccountTransactionsContext();
// Retrieve table columns. // Retrieve table columns.
const columns = useExcludedTransactionsColumns(); const columns = useExcludedTransactionsColumns();
@@ -97,6 +100,7 @@ function ExcludedTransactionsTableRoot({
className="table-constrant" className="table-constrant"
selectionColumn={true} selectionColumn={true}
onSelectedRowsChange={handleSelectedRowsChange} onSelectedRowsChange={handleSelectedRowsChange}
windowScrollerProps={{ scrollElement: scrollableRef }}
payload={{ payload={{
onRestore: handleRestoreClick, onRestore: handleRestoreClick,
}} }}

View File

@@ -20,6 +20,7 @@ import { useRecognizedTransactionsBoot } from './RecognizedTransactionsTableBoot
import { ActionsMenu } from './_components'; import { ActionsMenu } from './_components';
import { compose } from '@/utils'; import { compose } from '@/utils';
import { useAccountTransactionsContext } from '../AccountTransactionsProvider';
import { useExcludeUncategorizedTransaction } from '@/hooks/query/bank-rules'; import { useExcludeUncategorizedTransaction } from '@/hooks/query/bank-rules';
import { import {
WithBankingActionsProps, WithBankingActionsProps,
@@ -33,8 +34,8 @@ interface RecognizedTransactionsTableProps extends WithBankingActionsProps {}
* Renders the recognized account transactions datatable. * Renders the recognized account transactions datatable.
*/ */
function RecognizedTransactionsTableRoot({ function RecognizedTransactionsTableRoot({
// #withBanking // #withBankingActions
setUncategorizedTransactionIdForMatching, setTransactionsToCategorizeSelected,
}: RecognizedTransactionsTableProps) { }: RecognizedTransactionsTableProps) {
const { mutateAsync: excludeBankTransaction } = const { mutateAsync: excludeBankTransaction } =
useExcludeUncategorizedTransaction(); useExcludeUncategorizedTransaction();
@@ -49,9 +50,11 @@ function RecognizedTransactionsTableRoot({
const [initialColumnsWidths, , handleColumnResizing] = const [initialColumnsWidths, , handleColumnResizing] =
useMemorizedColumnsWidths(TABLES.UNCATEGORIZED_ACCOUNT_TRANSACTIONS); useMemorizedColumnsWidths(TABLES.UNCATEGORIZED_ACCOUNT_TRANSACTIONS);
const { scrollableRef } = useAccountTransactionsContext();
// Handle cell click. // Handle cell click.
const handleCellClick = (cell, event) => { const handleCellClick = (cell, event) => {
setUncategorizedTransactionIdForMatching( setTransactionsToCategorizeSelected(
cell.row.original.uncategorized_transaction_id, cell.row.original.uncategorized_transaction_id,
); );
}; };
@@ -74,7 +77,7 @@ function RecognizedTransactionsTableRoot({
// Handles categorize button click. // Handles categorize button click.
const handleCategorizeClick = (transaction) => { const handleCategorizeClick = (transaction) => {
setUncategorizedTransactionIdForMatching( setTransactionsToCategorizeSelected(
transaction.uncategorized_transaction_id, transaction.uncategorized_transaction_id,
); );
}; };
@@ -102,6 +105,7 @@ function RecognizedTransactionsTableRoot({
vListOverscanRowCount={0} vListOverscanRowCount={0}
initialColumnsWidths={initialColumnsWidths} initialColumnsWidths={initialColumnsWidths}
onColumnResizing={handleColumnResizing} onColumnResizing={handleColumnResizing}
windowScrollerProps={{ scrollElement: scrollableRef }}
noResults={<RecognizedTransactionsTableNoResults />} noResults={<RecognizedTransactionsTableNoResults />}
className="table-constrant" className="table-constrant"
payload={{ payload={{

View File

@@ -22,6 +22,7 @@ import { useMemorizedColumnsWidths } from '@/hooks';
import { useAccountUncategorizedTransactionsContext } from '../AllTransactionsUncategorizedBoot'; import { useAccountUncategorizedTransactionsContext } from '../AllTransactionsUncategorizedBoot';
import { useExcludeUncategorizedTransaction } from '@/hooks/query/bank-rules'; import { useExcludeUncategorizedTransaction } from '@/hooks/query/bank-rules';
import { useAccountUncategorizedTransactionsColumns } from './hooks'; import { useAccountUncategorizedTransactionsColumns } from './hooks';
import { useAccountTransactionsContext } from '../AccountTransactionsProvider';
import { compose } from '@/utils'; import { compose } from '@/utils';
import { withBanking } from '../../withBanking'; import { withBanking } from '../../withBanking';
@@ -48,6 +49,8 @@ function AccountTransactionsDataTable({
// Retrieve table columns. // Retrieve table columns.
const columns = useAccountUncategorizedTransactionsColumns(); const columns = useAccountUncategorizedTransactionsColumns();
const { scrollableRef } = useAccountTransactionsContext();
// Retrieve list context. // Retrieve list context.
const { uncategorizedTransactions, isUncategorizedTransactionsLoading } = const { uncategorizedTransactions, isUncategorizedTransactionsLoading } =
useAccountUncategorizedTransactionsContext(); useAccountUncategorizedTransactionsContext();
@@ -71,6 +74,11 @@ function AccountTransactionsDataTable({
const handleCategorizeBtnClick = (transaction) => { const handleCategorizeBtnClick = (transaction) => {
setUncategorizedTransactionIdForMatching(transaction.id); setUncategorizedTransactionIdForMatching(transaction.id);
}; };
// handles table selected rows change.
const handleSelectedRowsChange = (selected) => {
const transactionIds = selected.map((r) => r.original.id);
setUncategorizedTransactionsSelected(transactionIds);
};
// Handle exclude transaction. // Handle exclude transaction.
const handleExcludeTransaction = (transaction) => { const handleExcludeTransaction = (transaction) => {
excludeTransaction(transaction.id) excludeTransaction(transaction.id)
@@ -118,6 +126,8 @@ function AccountTransactionsDataTable({
onExclude: handleExcludeTransaction, onExclude: handleExcludeTransaction,
onCategorize: handleCategorizeBtnClick, onCategorize: handleCategorizeBtnClick,
}} }}
onSelectedRowsChange={handleSelectedRowsChange}
windowScrollerProps={{ scrollElement: scrollableRef }}
className={clsx('table-constrant', styles.table, { className={clsx('table-constrant', styles.table, {
[styles.showCategorizeColumn]: enableMultipleCategorization, [styles.showCategorizeColumn]: enableMultipleCategorization,
})} })}

View File

@@ -13,21 +13,12 @@ import {
import useApiRequest from '../useRequest'; import useApiRequest from '../useRequest';
import { transformToCamelCase } from '@/utils'; import { transformToCamelCase } from '@/utils';
import t from './types'; import t from './types';
import { BANK_QUERY_KEY } from '@/constants/query-keys/banking';
const QUERY_KEY = { // Common cache invalidator.
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',
};
const commonInvalidateQueries = (query: QueryClient) => { const commonInvalidateQueries = (query: QueryClient) => {
query.invalidateQueries(QUERY_KEY.BANK_RULES); query.invalidateQueries(BANK_QUERY_KEY.BANK_RULES);
query.invalidateQueries(QUERY_KEY.RECOGNIZED_BANK_TRANSACTIONS_INFINITY); query.invalidateQueries(BANK_QUERY_KEY.RECOGNIZED_BANK_TRANSACTIONS_INFINITY);
}; };
interface CreateBankRuleValues { interface CreateBankRuleValues {
@@ -185,7 +176,7 @@ export function useDeleteBankRule(
commonInvalidateQueries(queryClient); commonInvalidateQueries(queryClient);
queryClient.invalidateQueries( queryClient.invalidateQueries(
QUERY_KEY.RECOGNIZED_BANK_TRANSACTIONS_INFINITY, BANK_QUERY_KEY.RECOGNIZED_BANK_TRANSACTIONS_INFINITY,
); );
queryClient.invalidateQueries([ queryClient.invalidateQueries([
t.CASHFLOW_ACCOUNT_UNCATEGORIZED_TRANSACTIONS_INFINITY, t.CASHFLOW_ACCOUNT_UNCATEGORIZED_TRANSACTIONS_INFINITY,
@@ -209,7 +200,7 @@ export function useBankRules(
const apiRequest = useApiRequest(); const apiRequest = useApiRequest();
return useQuery<BankRulesResponse, Error>( return useQuery<BankRulesResponse, Error>(
[QUERY_KEY.BANK_RULES], [BANK_QUERY_KEY.BANK_RULES],
() => apiRequest.get('/banking/rules').then((res) => res.data.bank_rules), () => apiRequest.get('/banking/rules').then((res) => res.data.bank_rules),
{ ...options }, { ...options },
); );
@@ -230,7 +221,7 @@ export function useBankRule(
const apiRequest = useApiRequest(); const apiRequest = useApiRequest();
return useQuery<GetBankRuleRes, Error>( return useQuery<GetBankRuleRes, Error>(
[QUERY_KEY.BANK_RULES, bankRuleId], [BANK_QUERY_KEY.BANK_RULES, bankRuleId],
() => () =>
apiRequest apiRequest
.get(`/banking/rules/${bankRuleId}`) .get(`/banking/rules/${bankRuleId}`)
@@ -260,7 +251,7 @@ export function useGetBankTransactionsMatches(
const apiRequest = useApiRequest(); const apiRequest = useApiRequest();
return useQuery<GetBankTransactionsMatchesResponse, Error>( return useQuery<GetBankTransactionsMatchesResponse, Error>(
[QUERY_KEY.BANK_TRANSACTION_MATCHES, uncategorizeTransactionsIds], [BANK_QUERY_KEY.BANK_TRANSACTION_MATCHES, uncategorizeTransactionsIds],
() => () =>
apiRequest apiRequest
.get(`/cashflow/transactions/matches`, { .get(`/cashflow/transactions/matches`, {
@@ -273,7 +264,9 @@ export function useGetBankTransactionsMatches(
const onValidateExcludeUncategorizedTransaction = (queryClient) => { const onValidateExcludeUncategorizedTransaction = (queryClient) => {
// Invalidate queries. // Invalidate queries.
queryClient.invalidateQueries(QUERY_KEY.EXCLUDED_BANK_TRANSACTIONS_INFINITY); queryClient.invalidateQueries(
BANK_QUERY_KEY.EXCLUDED_BANK_TRANSACTIONS_INFINITY,
);
queryClient.invalidateQueries( queryClient.invalidateQueries(
t.CASHFLOW_ACCOUNT_UNCATEGORIZED_TRANSACTIONS_INFINITY, t.CASHFLOW_ACCOUNT_UNCATEGORIZED_TRANSACTIONS_INFINITY,
); );
@@ -282,7 +275,7 @@ const onValidateExcludeUncategorizedTransaction = (queryClient) => {
queryClient.invalidateQueries(t.ACCOUNT); queryClient.invalidateQueries(t.ACCOUNT);
// invalidate bank account summary. // invalidate bank account summary.
queryClient.invalidateQueries(QUERY_KEY.BANK_ACCOUNT_SUMMARY_META); queryClient.invalidateQueries(BANK_QUERY_KEY.BANK_ACCOUNT_SUMMARY_META);
}; };
type ExcludeUncategorizedTransactionValue = number; type ExcludeUncategorizedTransactionValue = number;
@@ -319,6 +312,10 @@ export function useExcludeUncategorizedTransaction(
{ {
onSuccess: (res, id) => { onSuccess: (res, id) => {
onValidateExcludeUncategorizedTransaction(queryClient); onValidateExcludeUncategorizedTransaction(queryClient);
queryClient.invalidateQueries([
BANK_QUERY_KEY.BANK_ACCOUNT_SUMMARY_META,
id,
]);
}, },
...options, ...options,
}, },
@@ -360,6 +357,10 @@ export function useUnexcludeUncategorizedTransaction(
{ {
onSuccess: (res, id) => { onSuccess: (res, id) => {
onValidateExcludeUncategorizedTransaction(queryClient); onValidateExcludeUncategorizedTransaction(queryClient);
queryClient.invalidateQueries([
BANK_QUERY_KEY.BANK_ACCOUNT_SUMMARY_META,
id,
]);
}, },
...options, ...options,
}, },
@@ -483,7 +484,7 @@ export function useMatchUncategorizedTransaction(
queryClient.invalidateQueries(t.ACCOUNT); queryClient.invalidateQueries(t.ACCOUNT);
// Invalidate bank account summary. // Invalidate bank account summary.
queryClient.invalidateQueries(QUERY_KEY.BANK_ACCOUNT_SUMMARY_META); queryClient.invalidateQueries(BANK_QUERY_KEY.BANK_ACCOUNT_SUMMARY_META);
}, },
...props, ...props,
}); });
@@ -529,7 +530,7 @@ export function useUnmatchMatchedUncategorizedTransaction(
queryClient.invalidateQueries(t.ACCOUNT); queryClient.invalidateQueries(t.ACCOUNT);
// Invalidate bank account summary. // Invalidate bank account summary.
queryClient.invalidateQueries(QUERY_KEY.BANK_ACCOUNT_SUMMARY_META); queryClient.invalidateQueries(BANK_QUERY_KEY.BANK_ACCOUNT_SUMMARY_META);
}, },
...props, ...props,
}); });
@@ -550,7 +551,7 @@ export function useGetRecognizedBankTransaction(
const apiRequest = useApiRequest(); const apiRequest = useApiRequest();
return useQuery<GetRecognizedBankTransactionRes, Error>( return useQuery<GetRecognizedBankTransactionRes, Error>(
[QUERY_KEY.RECOGNIZED_BANK_TRANSACTION, uncategorizedTransactionId], [BANK_QUERY_KEY.RECOGNIZED_BANK_TRANSACTION, uncategorizedTransactionId],
() => () =>
apiRequest apiRequest
.get(`/banking/recognized/transactions/${uncategorizedTransactionId}`) .get(`/banking/recognized/transactions/${uncategorizedTransactionId}`)
@@ -578,7 +579,7 @@ export function useGetBankAccountSummaryMeta(
const apiRequest = useApiRequest(); const apiRequest = useApiRequest();
return useQuery<GetBankAccountSummaryMetaRes, Error>( return useQuery<GetBankAccountSummaryMetaRes, Error>(
[QUERY_KEY.BANK_ACCOUNT_SUMMARY_META, bankAccountId], [BANK_QUERY_KEY.BANK_ACCOUNT_SUMMARY_META, bankAccountId],
() => () =>
apiRequest apiRequest
.get(`/banking/bank_accounts/${bankAccountId}/meta`) .get(`/banking/bank_accounts/${bankAccountId}/meta`)
@@ -610,7 +611,7 @@ export function useGetAutofillCategorizeTransaction(
return useQuery<GetAutofillCategorizeTransaction, Error>( return useQuery<GetAutofillCategorizeTransaction, Error>(
[ [
QUERY_KEY.AUTOFILL_CATEGORIZE_BANK_TRANSACTION, BANK_QUERY_KEY.AUTOFILL_CATEGORIZE_BANK_TRANSACTION,
uncategorizedTransactionIds, uncategorizedTransactionIds,
], ],
() => () =>
@@ -634,7 +635,7 @@ export function useRecognizedBankTransactionsInfinity(
const apiRequest = useApiRequest(); const apiRequest = useApiRequest();
return useInfiniteQuery( return useInfiniteQuery(
[QUERY_KEY.RECOGNIZED_BANK_TRANSACTIONS_INFINITY, query], [BANK_QUERY_KEY.RECOGNIZED_BANK_TRANSACTIONS_INFINITY, query],
async ({ pageParam = 1 }) => { async ({ pageParam = 1 }) => {
const response = await apiRequest.http({ const response = await apiRequest.http({
...axios, ...axios,
@@ -666,7 +667,7 @@ export function useExcludedBankTransactionsInfinity(
const apiRequest = useApiRequest(); const apiRequest = useApiRequest();
return useInfiniteQuery( return useInfiniteQuery(
[QUERY_KEY.EXCLUDED_BANK_TRANSACTIONS_INFINITY, query], [BANK_QUERY_KEY.EXCLUDED_BANK_TRANSACTIONS_INFINITY, query],
async ({ pageParam = 1 }) => { async ({ pageParam = 1 }) => {
const response = await apiRequest.http({ const response = await apiRequest.http({
...axios, ...axios,

View File

@@ -9,6 +9,7 @@ import useApiRequest from '../useRequest';
import { transformToCamelCase } from '@/utils'; import { transformToCamelCase } from '@/utils';
import { downloadFile, useDownloadFile } from '../useDownloadFile'; import { downloadFile, useDownloadFile } from '../useDownloadFile';
import T from './types'; import T from './types';
import { BANK_QUERY_KEY } from '@/constants/query-keys/banking';
const QueryKeys = { const QueryKeys = {
ImportPreview: 'ImportPreview', ImportPreview: 'ImportPreview',
@@ -127,8 +128,8 @@ export const useSampleSheetImport = () => {
/** /**
* Invalidates resources cached queries based on the given resource name, * Invalidates resources cached queries based on the given resource name,
* @param queryClient * @param queryClient
* @param resource * @param resource
*/ */
const invalidateResourcesOnImport = ( const invalidateResourcesOnImport = (
queryClient: QueryClient, queryClient: QueryClient,
@@ -215,6 +216,7 @@ const invalidateResourcesOnImport = (
T.CASHFLOW_ACCOUNT_UNCATEGORIZED_TRANSACTIONS_INFINITY, T.CASHFLOW_ACCOUNT_UNCATEGORIZED_TRANSACTIONS_INFINITY,
); );
queryClient.invalidateQueries(T.CASHFLOW_UNCAATEGORIZED_TRANSACTION); queryClient.invalidateQueries(T.CASHFLOW_UNCAATEGORIZED_TRANSACTION);
queryClient.invalidateQueries(BANK_QUERY_KEY.BANK_ACCOUNT_SUMMARY_META);
break; break;
} }
}; };