From 91730d204ee81de7d593a43d4aab503cc05e8983 Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Tue, 2 Jul 2024 19:21:26 +0200 Subject: [PATCH] feat: get bank account meta summary --- .../Banking/BankAccountsController.ts | 49 +++++++++++ .../controllers/Banking/BankingController.ts | 6 +- .../BankAccounts/GetBankAccountSummary.ts | 53 ++++++++++++ .../Cashflow/GetUncategorizedTransactions.ts | 15 ++-- .../UncategorizedTransactionTransformer.ts | 83 ++++++++++++++++++- .../components/EmptyStatus/EmptyStatus.tsx | 22 +++-- .../BankRulesLandingEmptyState.module.scss | 3 + .../RulesList/BankRulesLandingEmptyState.tsx | 22 +++-- .../Banking/Rules/RulesList/RulesListBoot.tsx | 6 +- .../Banking/Rules/RulesList/RulesTable.tsx | 4 +- .../AccountTransactionsProvider.tsx | 14 +++- .../AccountTransactionsUncategorizeFilter.tsx | 27 +++++- .../AccountTransactionsUncategorizedTable.tsx | 2 +- .../ExcludedTransactionsTable.tsx | 2 +- .../RecognizedTransactionsTable.module.scss | 20 +++++ .../RecognizedTransactionsTable.tsx | 29 ++++++- .../AccountTransactions/components.tsx | 59 ++++++++++--- .../CategorizeTransactionAside.module.scss | 2 + .../CategorizeTransactionTabs.tsx | 16 +++- .../MatchingTransaction.tsx | 28 +++++-- .../CategorizeTransactionAside/utils.ts | 46 +++++++--- packages/webapp/src/hooks/query/bank-rules.ts | 37 ++++++++- 22 files changed, 476 insertions(+), 69 deletions(-) create mode 100644 packages/server/src/api/controllers/Banking/BankAccountsController.ts create mode 100644 packages/server/src/services/Banking/BankAccounts/GetBankAccountSummary.ts create mode 100644 packages/webapp/src/containers/Banking/Rules/RulesList/BankRulesLandingEmptyState.module.scss create mode 100644 packages/webapp/src/containers/CashFlow/AccountTransactions/RecognizedTransactions/RecognizedTransactionsTable.module.scss diff --git a/packages/server/src/api/controllers/Banking/BankAccountsController.ts b/packages/server/src/api/controllers/Banking/BankAccountsController.ts new file mode 100644 index 000000000..4b062768f --- /dev/null +++ b/packages/server/src/api/controllers/Banking/BankAccountsController.ts @@ -0,0 +1,49 @@ +import { Inject, Service } from 'typedi'; +import { NextFunction, Request, Response, Router } from 'express'; +import BaseController from '@/api/controllers/BaseController'; +import { CashflowApplication } from '@/services/Cashflow/CashflowApplication'; +import { GetBankAccountSummary } from '@/services/Banking/BankAccounts/GetBankAccountSummary'; + +@Service() +export class BankAccountsController extends BaseController { + @Inject() + private getBankAccountSummaryService: GetBankAccountSummary; + + /** + * Router constructor. + */ + router() { + const router = Router(); + + router.get('/:bankAccountId/meta', this.getBankAccountSummary.bind(this)); + + return router; + } + + /** + * Retrieves the bank account meta summary. + * @param {Request} req + * @param {Response} res + * @param {NextFunction} next + * @returns {Promise} + */ + async getBankAccountSummary( + req: Request<{ bankAccountId: number }>, + res: Response, + next: NextFunction + ) { + const { bankAccountId } = req.params; + const { tenantId } = req; + + try { + const data = + await this.getBankAccountSummaryService.getBankAccountSummary( + tenantId, + bankAccountId + ); + return res.status(200).send({ data }); + } catch (error) { + next(error); + } + } +} diff --git a/packages/server/src/api/controllers/Banking/BankingController.ts b/packages/server/src/api/controllers/Banking/BankingController.ts index ffb42b89a..30695b19d 100644 --- a/packages/server/src/api/controllers/Banking/BankingController.ts +++ b/packages/server/src/api/controllers/Banking/BankingController.ts @@ -5,6 +5,7 @@ import { PlaidBankingController } from './PlaidBankingController'; import { BankingRulesController } from './BankingRulesController'; import { BankTransactionsMatchingController } from './BankTransactionsMatchingController'; import { RecognizedTransactionsController } from './RecognizedTransactionsController'; +import { BankAccountsController } from './BankAccountsController'; @Service() export class BankingController extends BaseController { @@ -24,7 +25,10 @@ export class BankingController extends BaseController { '/recognized', Container.get(RecognizedTransactionsController).router() ); - + router.use( + '/bank_accounts', + Container.get(BankAccountsController).router() + ); return router; } } diff --git a/packages/server/src/services/Banking/BankAccounts/GetBankAccountSummary.ts b/packages/server/src/services/Banking/BankAccounts/GetBankAccountSummary.ts new file mode 100644 index 000000000..a3cb35b98 --- /dev/null +++ b/packages/server/src/services/Banking/BankAccounts/GetBankAccountSummary.ts @@ -0,0 +1,53 @@ +import HasTenancyService from '@/services/Tenancy/TenancyService'; +import { Server } from 'socket.io'; +import { Inject, Service } from 'typedi'; + +@Service() +export class GetBankAccountSummary { + @Inject() + private tenancy: HasTenancyService; + + /** + * Retrieves the bank account meta summary + * @param {number} tenantId + * @param {number} bankAccountId + * @returns + */ + public async getBankAccountSummary(tenantId: number, bankAccountId: number) { + const { + Account, + UncategorizedCashflowTransaction, + RecognizedBankTransaction, + } = this.tenancy.models(tenantId); + + const bankAccount = await Account.query() + .findById(bankAccountId) + .throwIfNotFound(); + + const uncategorizedTranasctionsCount = + await UncategorizedCashflowTransaction.query() + .where('accountId', bankAccountId) + .count('id as total') + .first(); + + const recognizedTransactionsCount = await RecognizedBankTransaction.query() + .whereExists( + UncategorizedCashflowTransaction.query().where( + 'accountId', + bankAccountId + ) + ) + .count('id as total') + .first(); + + const totalUncategorizedTransactions = + uncategorizedTranasctionsCount?.total; + const totalRecognizedTransactions = recognizedTransactionsCount?.total; + + return { + name: bankAccount.name, + totalUncategorizedTransactions, + totalRecognizedTransactions, + }; + } +} diff --git a/packages/server/src/services/Cashflow/GetUncategorizedTransactions.ts b/packages/server/src/services/Cashflow/GetUncategorizedTransactions.ts index 5501baf75..69199cc99 100644 --- a/packages/server/src/services/Cashflow/GetUncategorizedTransactions.ts +++ b/packages/server/src/services/Cashflow/GetUncategorizedTransactions.ts @@ -32,11 +32,16 @@ export class GetUncategorizedTransactions { }; const { results, pagination } = await UncategorizedCashflowTransaction.query() - .where('accountId', accountId) - .where('categorized', false) - .modify('notExcluded') - .withGraphFetched('account') - .orderBy('date', 'DESC') + .onBuild((q) => { + q.where('accountId', accountId); + q.where('categorized', false); + q.modify('notExcluded'); + + q.withGraphFetched('account'); + q.withGraphFetched('recognizedTransaction.assignAccount'); + + q.orderBy('date', 'DESC'); + }) .pagination(_query.page - 1, _query.pageSize); const data = await this.transformer.transform( diff --git a/packages/server/src/services/Cashflow/UncategorizedTransactionTransformer.ts b/packages/server/src/services/Cashflow/UncategorizedTransactionTransformer.ts index 5328b8e70..83fe13e5e 100644 --- a/packages/server/src/services/Cashflow/UncategorizedTransactionTransformer.ts +++ b/packages/server/src/services/Cashflow/UncategorizedTransactionTransformer.ts @@ -12,9 +12,25 @@ export class UncategorizedTransactionTransformer extends Transformer { 'formattedDate', 'formattedDepositAmount', 'formattedWithdrawalAmount', + + 'assignedAccountId', + 'assignedAccountName', + 'assignedAccountCode', + 'assignedPayee', + 'assignedMemo', + 'assignedCategory', + 'assignedCategoryFormatted', ]; }; + /** + * Exclude all attributes. + * @returns {Array} + */ + public excludeAttributes = (): string[] => { + return ['recognizedTransaction']; + }; + /** * Formattes the transaction date. * @param transaction @@ -26,7 +42,7 @@ export class UncategorizedTransactionTransformer extends Transformer { /** * Formatted amount. - * @param transaction + * @param transaction * @returns {string} */ public formattedAmount(transaction) { @@ -62,4 +78,69 @@ export class UncategorizedTransactionTransformer extends Transformer { } return ''; } + + // -------------------------------------------------------- + // # Recgonized transaction + // -------------------------------------------------------- + /** + * Get the assigned account ID of the transaction. + * @param {object} transaction + * @returns {number} + */ + public assignedAccountId(transaction: any): number { + return transaction.recognizedTransaction?.assignedAccountId; + } + + /** + * Get the assigned account name of the transaction. + * @param {object} transaction + * @returns {string} + */ + public assignedAccountName(transaction: any): string { + return transaction.recognizedTransaction?.assignAccount?.name; + } + + /** + * Get the assigned account code of the transaction. + * @param {object} transaction + * @returns {string} + */ + public assignedAccountCode(transaction: any): string { + return transaction.recognizedTransaction?.assignAccount?.code; + } + + /** + * Get the assigned payee of the transaction. + * @param {object} transaction + * @returns {string} + */ + public getAssignedPayee(transaction: any): string { + return transaction.recognizedTransaction?.assignedPayee; + } + + /** + * Get the assigned memo of the transaction. + * @param {object} transaction + * @returns {string} + */ + public assignedMemo(transaction: any): string { + return transaction.recognizedTransaction?.assignedMemo; + } + + /** + * Get the assigned category of the transaction. + * @param {object} transaction + * @returns {string} + */ + public assignedCategory(transaction: any): string { + return transaction.recognizedTransaction?.assignedCategory; + } + + /** + * Get the assigned formatted category. + * @returns {string} + */ + public assignedCategoryFormatted() { + return 'Other Income'; + } } diff --git a/packages/webapp/src/components/EmptyStatus/EmptyStatus.tsx b/packages/webapp/src/components/EmptyStatus/EmptyStatus.tsx index 1b05c76d6..3465cde3c 100644 --- a/packages/webapp/src/components/EmptyStatus/EmptyStatus.tsx +++ b/packages/webapp/src/components/EmptyStatus/EmptyStatus.tsx @@ -1,18 +1,28 @@ // @ts-nocheck import React from 'react'; -import classNames from 'classnames'; +import clsx from 'classnames'; import Style from '@/style/components/DataTable/DataTableEmptyStatus.module.scss'; /** * Datatable empty status. */ -export function EmptyStatus({ title, description, action, children }) { +export function EmptyStatus({ + title, + description, + action, + children, + classNames, +}) { return ( -
-

{title}

-
{description}
-
{action}
+
+

{title}

+
+ {description} +
+
+ {action} +
{children}
); diff --git a/packages/webapp/src/containers/Banking/Rules/RulesList/BankRulesLandingEmptyState.module.scss b/packages/webapp/src/containers/Banking/Rules/RulesList/BankRulesLandingEmptyState.module.scss new file mode 100644 index 000000000..9bea9513f --- /dev/null +++ b/packages/webapp/src/containers/Banking/Rules/RulesList/BankRulesLandingEmptyState.module.scss @@ -0,0 +1,3 @@ +.root{ + max-width: 600px; +} \ No newline at end of file diff --git a/packages/webapp/src/containers/Banking/Rules/RulesList/BankRulesLandingEmptyState.tsx b/packages/webapp/src/containers/Banking/Rules/RulesList/BankRulesLandingEmptyState.tsx index 4ad67f0b6..737fdca6d 100644 --- a/packages/webapp/src/containers/Banking/Rules/RulesList/BankRulesLandingEmptyState.tsx +++ b/packages/webapp/src/containers/Banking/Rules/RulesList/BankRulesLandingEmptyState.tsx @@ -4,32 +4,44 @@ import { Button, Intent } from '@blueprintjs/core'; import { EmptyStatus, Can, FormattedMessage as T } from '@/components'; import { AbilitySubject, BankRuleAction } from '@/constants/abilityOption'; import withDialogActions from '@/containers/Dialog/withDialogActions'; +import { DialogsName } from '@/constants/dialogs'; +import styles from './BankRulesLandingEmptyState.module.scss'; function BankRulesLandingEmptyStateRoot({ // #withDialogAction openDialog, }) { + const handleNewBtnClick = () => { + openDialog(DialogsName.BankRuleForm); + }; + return ( - Setup the organization taxes to start tracking taxes on sales - transactions. + Bank rules will run automatically to categorize the incoming bank + transactions under the conditions you set up.

} action={ <> - + } + classNames={{ root: styles.root }} /> ); } diff --git a/packages/webapp/src/containers/Banking/Rules/RulesList/RulesListBoot.tsx b/packages/webapp/src/containers/Banking/Rules/RulesList/RulesListBoot.tsx index a022d150e..b9391f924 100644 --- a/packages/webapp/src/containers/Banking/Rules/RulesList/RulesListBoot.tsx +++ b/packages/webapp/src/containers/Banking/Rules/RulesList/RulesListBoot.tsx @@ -1,10 +1,12 @@ import React, { createContext } from 'react'; import { DialogContent } from '@/components'; import { useBankRules } from '@/hooks/query/bank-rules'; +import { isEmpty } from 'lodash'; interface RulesListBootValues { bankRules: any; isBankRulesLoading: boolean; + isEmptyState: boolean; } const RulesListBootContext = createContext( @@ -18,9 +20,11 @@ interface RulesListBootProps { function RulesListBoot({ ...props }: RulesListBootProps) { const { data: bankRules, isLoading: isBankRulesLoading } = useBankRules(); - const provider = { bankRules, isBankRulesLoading } as RulesListBootValues; + const isEmptyState = !isBankRulesLoading && isEmpty(bankRules); const isLoading = isBankRulesLoading; + const provider = { bankRules, isBankRulesLoading, isEmptyState } as RulesListBootValues; + return ( diff --git a/packages/webapp/src/containers/Banking/Rules/RulesList/RulesTable.tsx b/packages/webapp/src/containers/Banking/Rules/RulesList/RulesTable.tsx index d541e88cf..9ad4fcd6b 100644 --- a/packages/webapp/src/containers/Banking/Rules/RulesList/RulesTable.tsx +++ b/packages/webapp/src/containers/Banking/Rules/RulesList/RulesTable.tsx @@ -33,7 +33,7 @@ function RulesTable({ }) { // Invoices table columns. const columns = useBankRulesTableColumns(); - const { bankRules } = useRulesListBoot(); + const { bankRules, isEmptyState } = useRulesListBoot(); // Handle edit bank rule. const handleDeleteBankRule = ({ id }) => { @@ -45,8 +45,6 @@ function RulesTable({ openDialog(DialogsName.BankRuleForm, { bankRuleId: id }); }; - const isEmptyState = false; - // Display invoice empty status instead of the table. if (isEmptyState) { return ; diff --git a/packages/webapp/src/containers/CashFlow/AccountTransactions/AccountTransactionsProvider.tsx b/packages/webapp/src/containers/CashFlow/AccountTransactions/AccountTransactionsProvider.tsx index 1b3c98a29..ba4447c69 100644 --- a/packages/webapp/src/containers/CashFlow/AccountTransactions/AccountTransactionsProvider.tsx +++ b/packages/webapp/src/containers/CashFlow/AccountTransactions/AccountTransactionsProvider.tsx @@ -4,6 +4,7 @@ import { useParams } from 'react-router-dom'; import { DashboardInsider } from '@/components'; import { useCashflowAccounts, useAccount } from '@/hooks/query'; import { useAppQueryString } from '@/hooks'; +import { useGetBankAccountSummaryMeta } from '@/hooks/query/bank-rules'; const AccountTransactionsContext = React.createContext(); @@ -20,31 +21,38 @@ function AccountTransactionsProvider({ query, ...props }) { const setFilterTab = (value: string) => { setLocationQuery({ filter: value }); }; - // Fetch cashflow accounts. + // Retrieves cashflow accounts. const { data: cashflowAccounts, isFetching: isCashFlowAccountsFetching, isLoading: isCashFlowAccountsLoading, } = useCashflowAccounts(query, { keepPreviousData: true }); - // Retrieve specific account details. - + // Retrieves specific account details. const { data: currentAccount, isFetching: isCurrentAccountFetching, isLoading: isCurrentAccountLoading, } = useAccount(accountId, { keepPreviousData: true }); + // Retrieves the bank account meta summary. + const { + data: bankAccountMetaSummary, + isLoading: isBankAccountMetaSummaryLoading, + } = useGetBankAccountSummaryMeta(accountId); + // Provider payload. const provider = { accountId, cashflowAccounts, currentAccount, + bankAccountMetaSummary, isCashFlowAccountsFetching, isCashFlowAccountsLoading, isCurrentAccountFetching, isCurrentAccountLoading, + isBankAccountMetaSummaryLoading, filterTab, setFilterTab, diff --git a/packages/webapp/src/containers/CashFlow/AccountTransactions/AccountTransactionsUncategorizeFilter.tsx b/packages/webapp/src/containers/CashFlow/AccountTransactions/AccountTransactionsUncategorizeFilter.tsx index a17309b36..ca42a0b4d 100644 --- a/packages/webapp/src/containers/CashFlow/AccountTransactions/AccountTransactionsUncategorizeFilter.tsx +++ b/packages/webapp/src/containers/CashFlow/AccountTransactions/AccountTransactionsUncategorizeFilter.tsx @@ -4,6 +4,7 @@ import { Tag } from '@blueprintjs/core'; import { useAppQueryString } from '@/hooks'; import { useUncontrolled } from '@/hooks/useUncontrolled'; import { Group } from '@/components'; +import { useAccountTransactionsContext } from './AccountTransactionsProvider'; const Root = styled.div` display: flex; @@ -26,8 +27,13 @@ const FilterTag = styled(Tag)` `; export function AccountTransactionsUncategorizeFilter() { + const { bankAccountMetaSummary } = useAccountTransactionsContext(); const [locationQuery, setLocationQuery] = useAppQueryString(); + const totalUncategorized = + bankAccountMetaSummary?.totalUncategorizedTransactions; + const totalRecognized = bankAccountMetaSummary?.totalRecognizedTransactions; + const handleTabsChange = (value) => { setLocationQuery({ uncategorizedFilter: value }); }; @@ -36,8 +42,22 @@ export function AccountTransactionsUncategorizeFilter() { + All ({totalUncategorized}) + + ), + }, + { + value: 'recognized', + label: ( + <> + Recognized ({totalRecognized}) + + ), + }, ]} value={locationQuery?.uncategorizedFilter || 'all'} onValueChange={handleTabsChange} @@ -66,8 +86,9 @@ function SegmentedTabs({ options, initialValue, value, onValueChange }) { }); return ( - {options.map((option) => ( + {options.map((option, index) => ( handleChange(option.value)} diff --git a/packages/webapp/src/containers/CashFlow/AccountTransactions/AccountTransactionsUncategorizedTable.tsx b/packages/webapp/src/containers/CashFlow/AccountTransactions/AccountTransactionsUncategorizedTable.tsx index 4bc51c292..326a71358 100644 --- a/packages/webapp/src/containers/CashFlow/AccountTransactions/AccountTransactionsUncategorizedTable.tsx +++ b/packages/webapp/src/containers/CashFlow/AccountTransactions/AccountTransactionsUncategorizedTable.tsx @@ -102,7 +102,7 @@ function AccountTransactionsDataTable({ vListOverscanRowCount={0} initialColumnsWidths={initialColumnsWidths} onColumnResizing={handleColumnResizing} - noResults={} + noResults={'There is no uncategorized transactions in the current account.'} className="table-constrant" payload={{ onExclude: handleExcludeTransaction, diff --git a/packages/webapp/src/containers/CashFlow/AccountTransactions/ExcludedTransactions/ExcludedTransactionsTable.tsx b/packages/webapp/src/containers/CashFlow/AccountTransactions/ExcludedTransactions/ExcludedTransactionsTable.tsx index 63ca62fa2..14e959dfa 100644 --- a/packages/webapp/src/containers/CashFlow/AccountTransactions/ExcludedTransactions/ExcludedTransactionsTable.tsx +++ b/packages/webapp/src/containers/CashFlow/AccountTransactions/ExcludedTransactions/ExcludedTransactionsTable.tsx @@ -81,7 +81,7 @@ function ExcludedTransactionsTableRoot() { vListOverscanRowCount={0} initialColumnsWidths={initialColumnsWidths} onColumnResizing={handleColumnResizing} - // noResults={} + noResults={'There is no excluded bank transactions.'} className="table-constrant" payload={{ onRestore: handleRestoreClick, diff --git a/packages/webapp/src/containers/CashFlow/AccountTransactions/RecognizedTransactions/RecognizedTransactionsTable.module.scss b/packages/webapp/src/containers/CashFlow/AccountTransactions/RecognizedTransactions/RecognizedTransactionsTable.module.scss new file mode 100644 index 000000000..62b5a09f4 --- /dev/null +++ b/packages/webapp/src/containers/CashFlow/AccountTransactions/RecognizedTransactions/RecognizedTransactionsTable.module.scss @@ -0,0 +1,20 @@ + + +.emptyState{ + text-align: center; + font-size: 15px; + color: #738091; + + :global ul{ + list-style: inside; + + li{ + margin-bottom: 12px; + + &::marker{ + color: #C5CBD3; + font-size: 12px; + } + } + } +} \ No newline at end of file diff --git a/packages/webapp/src/containers/CashFlow/AccountTransactions/RecognizedTransactions/RecognizedTransactionsTable.tsx b/packages/webapp/src/containers/CashFlow/AccountTransactions/RecognizedTransactions/RecognizedTransactionsTable.tsx index 1987d894e..3739aad31 100644 --- a/packages/webapp/src/containers/CashFlow/AccountTransactions/RecognizedTransactions/RecognizedTransactionsTable.tsx +++ b/packages/webapp/src/containers/CashFlow/AccountTransactions/RecognizedTransactions/RecognizedTransactionsTable.tsx @@ -10,6 +10,7 @@ import { TableVirtualizedListRows, FormattedMessage as T, AppToaster, + Stack, } from '@/components'; import { TABLES } from '@/constants/tables'; @@ -24,11 +25,12 @@ import { useRecognizedTransactionsBoot } from './RecognizedTransactionsTableBoot import { ActionsMenu } from './_components'; import { compose } from '@/utils'; import { useExcludeUncategorizedTransaction } from '@/hooks/query/bank-rules'; -import { Intent } from '@blueprintjs/core'; +import { Intent, Text } from '@blueprintjs/core'; import { WithBankingActionsProps, withBankingActions, } from '../../withBankingActions'; +import styles from './RecognizedTransactionsTable.module.scss'; interface RecognizedTransactionsTableProps extends WithBankingActionsProps {} @@ -114,7 +116,7 @@ function RecognizedTransactionsTableRoot({ vListOverscanRowCount={0} initialColumnsWidths={initialColumnsWidths} onColumnResizing={handleColumnResizing} - noResults={} + noResults={} className="table-constrant" payload={{ onExclude: handleExcludeClick, @@ -168,3 +170,26 @@ const CashflowTransactionsTable = styled(DashboardConstrantTable)` } } `; + +function RecognizedTransactionsTableNoResults() { + return ( + + + There are no Recognized transactions due to one of the following + reasons: + + +
    +
  • + Transaction Rules have not yet been created. Transactions are + recognized based on the rule criteria. +
  • + +
  • + The transactions in your bank do not satisfy the criteria in any of + your transaction rule(s). +
  • +
+
+ ); +} diff --git a/packages/webapp/src/containers/CashFlow/AccountTransactions/components.tsx b/packages/webapp/src/containers/CashFlow/AccountTransactions/components.tsx index 99331156f..2829e25dd 100644 --- a/packages/webapp/src/containers/CashFlow/AccountTransactions/components.tsx +++ b/packages/webapp/src/containers/CashFlow/AccountTransactions/components.tsx @@ -1,8 +1,24 @@ // @ts-nocheck import React from 'react'; import intl from 'react-intl-universal'; -import { Intent, Menu, MenuItem, MenuDivider, Tag } from '@blueprintjs/core'; -import { Can, FormatDateCell, Icon, MaterialProgressBar } from '@/components'; +import { + Intent, + Menu, + MenuItem, + MenuDivider, + Tag, + Popover, + PopoverInteractionKind, + Position, + Tooltip, +} from '@blueprintjs/core'; +import { + Box, + Can, + FormatDateCell, + Icon, + MaterialProgressBar, +} from '@/components'; import { useAccountTransactionsContext } from './AccountTransactionsProvider'; import { safeCallback } from '@/utils'; @@ -128,6 +144,34 @@ export function AccountTransactionsProgressBar() { ) : null; } +function statusAccessor(transaction) { + return transaction.is_recognized ? ( + + {transaction.assigned_category_formatted} + + {transaction.assigned_account_name} + + } + > + + + Recognized + + + + ) : null; +} + /** * Retrieve account uncategorized transctions table columns. */ @@ -170,16 +214,7 @@ export function useAccountUncategorizedTransactionsColumns() { { id: 'status', Header: 'Status', - accessor: () => - false ? ( - - 1 Matches - - ) : ( - - Recognized - - ), + accessor: statusAccessor, }, { id: 'deposit', diff --git a/packages/webapp/src/containers/CashFlow/CategorizeTransactionAside/CategorizeTransactionAside.module.scss b/packages/webapp/src/containers/CashFlow/CategorizeTransactionAside/CategorizeTransactionAside.module.scss index 56e081f4e..bc6646aa5 100644 --- a/packages/webapp/src/containers/CashFlow/CategorizeTransactionAside/CategorizeTransactionAside.module.scss +++ b/packages/webapp/src/containers/CashFlow/CategorizeTransactionAside/CategorizeTransactionAside.module.scss @@ -2,6 +2,7 @@ .root { flex: 1 1 auto; overflow: auto; + padding-bottom: 60px; } .transaction { @@ -35,4 +36,5 @@ .footerTotal { padding: 8px 16px; border-top: 1px solid #d8d9d9; + line-height: 24px; } diff --git a/packages/webapp/src/containers/CashFlow/CategorizeTransactionAside/CategorizeTransactionTabs.tsx b/packages/webapp/src/containers/CashFlow/CategorizeTransactionAside/CategorizeTransactionTabs.tsx index b5bda42c9..3e81d8ae5 100644 --- a/packages/webapp/src/containers/CashFlow/CategorizeTransactionAside/CategorizeTransactionTabs.tsx +++ b/packages/webapp/src/containers/CashFlow/CategorizeTransactionAside/CategorizeTransactionTabs.tsx @@ -1,11 +1,23 @@ +// @ts-nocheck import { Tab, Tabs } from '@blueprintjs/core'; import { MatchingBankTransaction } from './MatchingTransaction'; -import styles from './CategorizeTransactionTabs.module.scss'; import { CategorizeTransactionContent } from '../CategorizeTransaction/drawers/CategorizeTransactionDrawer/CategorizeTransactionContent'; +import { useCategorizeTransactionTabsBoot } from './CategorizeTransactionTabsBoot'; +import styles from './CategorizeTransactionTabs.module.scss'; export function CategorizeTransactionTabs() { + const { uncategorizedTransaction } = useCategorizeTransactionTabsBoot(); + const defaultSelectedTabId = uncategorizedTransaction?.is_recognized + ? 'categorize' + : 'matching'; + return ( - + { + const totalPending = useGetPendingAmountMatched(); + const showReconcileLink = useIsShowReconcileTransactionLink(); + const handleCancelBtnClick = () => { closeMatchingTransactionAside(); }; - const totalPending = useGetPendingAmountMatched(); return ( - - Add Reconcile Transaction + - - - + {showReconcileLink && ( + + Add Reconcile Transaction + + + )} + Pending diff --git a/packages/webapp/src/containers/CashFlow/CategorizeTransactionAside/utils.ts b/packages/webapp/src/containers/CashFlow/CategorizeTransactionAside/utils.ts index 0f388da34..6cb13f0b0 100644 --- a/packages/webapp/src/containers/CashFlow/CategorizeTransactionAside/utils.ts +++ b/packages/webapp/src/containers/CashFlow/CategorizeTransactionAside/utils.ts @@ -1,6 +1,8 @@ import { useFormikContext } from 'formik'; import { MatchingTransactionFormValues } from './types'; import { useMatchingTransactionBoot } from './MatchingTransactionBoot'; +import { useCategorizeTransactionTabsBoot } from './CategorizeTransactionTabsBoot'; +import { useMemo } from 'react'; export const transformToReq = (values: MatchingTransactionFormValues) => { const matchedTransactions = Object.entries(values.matched) @@ -17,18 +19,38 @@ export const transformToReq = (values: MatchingTransactionFormValues) => { export const useGetPendingAmountMatched = () => { const { values } = useFormikContext(); const { perfectMatches, possibleMatches } = useMatchingTransactionBoot(); + const { uncategorizedTransaction } = useCategorizeTransactionTabsBoot(); - const matchedItems = [...perfectMatches, ...possibleMatches].filter( - (match) => { - const key = `${match.transactionType}-${match.transactionId}`; - return values.matched[key]; - }, - ); - const totalMatchedAmount = matchedItems.reduce( - (total, item) => total + parseFloat(item.amount), - 0, - ); - const pendingAmount = 0 - totalMatchedAmount; + return useMemo(() => { + const matchedItems = [...perfectMatches, ...possibleMatches].filter( + (match) => { + const key = `${match.transactionType}-${match.transactionId}`; + return values.matched[key]; + }, + ); + const totalMatchedAmount = matchedItems.reduce( + (total, item) => total + parseFloat(item.amount), + 0, + ); + const amount = uncategorizedTransaction.amount; + const pendingAmount = amount - totalMatchedAmount; - return pendingAmount; + return pendingAmount; + }, [uncategorizedTransaction, perfectMatches, possibleMatches, values]); +}; + +export const useAtleastOneMatchedSelected = () => { + const { values } = useFormikContext(); + + return useMemo(() => { + const matchedCount = Object.values(values.matched).filter(Boolean).length; + return matchedCount > 0; + }, [values]); +}; + +export const useIsShowReconcileTransactionLink = () => { + const pendingAmount = useGetPendingAmountMatched(); + const atleastOneSelected = useAtleastOneMatchedSelected(); + + return atleastOneSelected && pendingAmount !== 0; }; diff --git a/packages/webapp/src/hooks/query/bank-rules.ts b/packages/webapp/src/hooks/query/bank-rules.ts index 665d8a9a6..62a457bf5 100644 --- a/packages/webapp/src/hooks/query/bank-rules.ts +++ b/packages/webapp/src/hooks/query/bank-rules.ts @@ -21,6 +21,7 @@ const QUERY_KEY = { EXCLUDED_BANK_TRANSACTIONS_INFINITY: 'EXCLUDED_BANK_TRANSACTIONS_INFINITY', RECOGNIZED_BANK_TRANSACTIONS_INFINITY: 'RECOGNIZED_BANK_TRANSACTIONS_INFINITY', + BANK_ACCOUNT_SUMMARY_META: 'BANK_ACCOUNT_SUMMARY_META', }; const commonInvalidateQueries = (query: QueryClient) => { @@ -111,6 +112,10 @@ export function useDeleteBankRule( { onSuccess: (res, id) => { commonInvalidateQueries(queryClient); + + queryClient.invalidateQueries( + QUERY_KEY.RECOGNIZED_BANK_TRANSACTIONS_INFINITY, + ); }, ...options, }, @@ -323,8 +328,8 @@ interface GetRecognizedBankTransactionRes {} /** * REtrieves the given recognized bank transaction. - * @param {number} uncategorizedTransactionId - * @param {UseQueryOptions} options + * @param {number} uncategorizedTransactionId + * @param {UseQueryOptions} options * @returns {UseQueryResult} */ export function useGetRecognizedBankTransaction( @@ -343,6 +348,34 @@ export function useGetRecognizedBankTransaction( ); } +interface GetBankAccountSummaryMetaRes { + name: string; + totalUncategorizedTransactions: number; + totalRecognizedTransactions: number; +} + +/** + * Get the given bank account meta summary. + * @param {number} bankAccountId + * @param {UseQueryOptions} options + * @returns {UseQueryResult} + */ +export function useGetBankAccountSummaryMeta( + bankAccountId: number, + options?: UseQueryOptions, +): UseQueryResult { + const apiRequest = useApiRequest(); + + return useQuery( + [QUERY_KEY.BANK_ACCOUNT_SUMMARY_META, bankAccountId], + () => + apiRequest + .get(`/banking/bank_accounts/${bankAccountId}/meta`) + .then((res) => transformToCamelCase(res.data?.data)), + { ...options }, + ); +} + /** * @returns */