mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-21 15:20:34 +00:00
feat: get bank account meta summary
This commit is contained in:
@@ -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<Response|null>}
|
||||||
|
*/
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ import { PlaidBankingController } from './PlaidBankingController';
|
|||||||
import { BankingRulesController } from './BankingRulesController';
|
import { BankingRulesController } from './BankingRulesController';
|
||||||
import { BankTransactionsMatchingController } from './BankTransactionsMatchingController';
|
import { BankTransactionsMatchingController } from './BankTransactionsMatchingController';
|
||||||
import { RecognizedTransactionsController } from './RecognizedTransactionsController';
|
import { RecognizedTransactionsController } from './RecognizedTransactionsController';
|
||||||
|
import { BankAccountsController } from './BankAccountsController';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class BankingController extends BaseController {
|
export class BankingController extends BaseController {
|
||||||
@@ -24,7 +25,10 @@ export class BankingController extends BaseController {
|
|||||||
'/recognized',
|
'/recognized',
|
||||||
Container.get(RecognizedTransactionsController).router()
|
Container.get(RecognizedTransactionsController).router()
|
||||||
);
|
);
|
||||||
|
router.use(
|
||||||
|
'/bank_accounts',
|
||||||
|
Container.get(BankAccountsController).router()
|
||||||
|
);
|
||||||
return router;
|
return router;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -32,11 +32,16 @@ export class GetUncategorizedTransactions {
|
|||||||
};
|
};
|
||||||
const { results, pagination } =
|
const { results, pagination } =
|
||||||
await UncategorizedCashflowTransaction.query()
|
await UncategorizedCashflowTransaction.query()
|
||||||
.where('accountId', accountId)
|
.onBuild((q) => {
|
||||||
.where('categorized', false)
|
q.where('accountId', accountId);
|
||||||
.modify('notExcluded')
|
q.where('categorized', false);
|
||||||
.withGraphFetched('account')
|
q.modify('notExcluded');
|
||||||
.orderBy('date', 'DESC')
|
|
||||||
|
q.withGraphFetched('account');
|
||||||
|
q.withGraphFetched('recognizedTransaction.assignAccount');
|
||||||
|
|
||||||
|
q.orderBy('date', 'DESC');
|
||||||
|
})
|
||||||
.pagination(_query.page - 1, _query.pageSize);
|
.pagination(_query.page - 1, _query.pageSize);
|
||||||
|
|
||||||
const data = await this.transformer.transform(
|
const data = await this.transformer.transform(
|
||||||
|
|||||||
@@ -12,9 +12,25 @@ export class UncategorizedTransactionTransformer extends Transformer {
|
|||||||
'formattedDate',
|
'formattedDate',
|
||||||
'formattedDepositAmount',
|
'formattedDepositAmount',
|
||||||
'formattedWithdrawalAmount',
|
'formattedWithdrawalAmount',
|
||||||
|
|
||||||
|
'assignedAccountId',
|
||||||
|
'assignedAccountName',
|
||||||
|
'assignedAccountCode',
|
||||||
|
'assignedPayee',
|
||||||
|
'assignedMemo',
|
||||||
|
'assignedCategory',
|
||||||
|
'assignedCategoryFormatted',
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exclude all attributes.
|
||||||
|
* @returns {Array<string>}
|
||||||
|
*/
|
||||||
|
public excludeAttributes = (): string[] => {
|
||||||
|
return ['recognizedTransaction'];
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Formattes the transaction date.
|
* Formattes the transaction date.
|
||||||
* @param transaction
|
* @param transaction
|
||||||
@@ -62,4 +78,69 @@ export class UncategorizedTransactionTransformer extends Transformer {
|
|||||||
}
|
}
|
||||||
return '';
|
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';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,28 @@
|
|||||||
// @ts-nocheck
|
// @ts-nocheck
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import classNames from 'classnames';
|
import clsx from 'classnames';
|
||||||
|
|
||||||
import Style from '@/style/components/DataTable/DataTableEmptyStatus.module.scss';
|
import Style from '@/style/components/DataTable/DataTableEmptyStatus.module.scss';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Datatable empty status.
|
* Datatable empty status.
|
||||||
*/
|
*/
|
||||||
export function EmptyStatus({ title, description, action, children }) {
|
export function EmptyStatus({
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
action,
|
||||||
|
children,
|
||||||
|
classNames,
|
||||||
|
}) {
|
||||||
return (
|
return (
|
||||||
<div className={classNames(Style.root)}>
|
<div className={clsx(Style.root, classNames?.root)}>
|
||||||
<h1 className={classNames(Style.root_title)}>{title}</h1>
|
<h1 className={clsx(Style.root_title, classNames?.title)}>{title}</h1>
|
||||||
<div className={classNames(Style.root_desc)}>{description}</div>
|
<div className={clsx(Style.root_desc, classNames?.description)}>
|
||||||
<div className={classNames(Style.root_actions)}>{action}</div>
|
{description}
|
||||||
|
</div>
|
||||||
|
<div className={clsx(Style.root_actions, classNames?.actions)}>
|
||||||
|
{action}
|
||||||
|
</div>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
.root{
|
||||||
|
max-width: 600px;
|
||||||
|
}
|
||||||
@@ -4,32 +4,44 @@ import { Button, Intent } from '@blueprintjs/core';
|
|||||||
import { EmptyStatus, Can, FormattedMessage as T } from '@/components';
|
import { EmptyStatus, Can, FormattedMessage as T } from '@/components';
|
||||||
import { AbilitySubject, BankRuleAction } from '@/constants/abilityOption';
|
import { AbilitySubject, BankRuleAction } from '@/constants/abilityOption';
|
||||||
import withDialogActions from '@/containers/Dialog/withDialogActions';
|
import withDialogActions from '@/containers/Dialog/withDialogActions';
|
||||||
|
import { DialogsName } from '@/constants/dialogs';
|
||||||
|
import styles from './BankRulesLandingEmptyState.module.scss';
|
||||||
|
|
||||||
function BankRulesLandingEmptyStateRoot({
|
function BankRulesLandingEmptyStateRoot({
|
||||||
// #withDialogAction
|
// #withDialogAction
|
||||||
openDialog,
|
openDialog,
|
||||||
}) {
|
}) {
|
||||||
|
const handleNewBtnClick = () => {
|
||||||
|
openDialog(DialogsName.BankRuleForm);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EmptyStatus
|
<EmptyStatus
|
||||||
title={"The organization doesn't have taxes, yet!"}
|
title={'Create rules to categorize bank transactions automatically'}
|
||||||
description={
|
description={
|
||||||
<p>
|
<p>
|
||||||
Setup the organization taxes to start tracking taxes on sales
|
Bank rules will run automatically to categorize the incoming bank
|
||||||
transactions.
|
transactions under the conditions you set up.
|
||||||
</p>
|
</p>
|
||||||
}
|
}
|
||||||
action={
|
action={
|
||||||
<>
|
<>
|
||||||
<Can I={BankRuleAction.Create} a={AbilitySubject.BankRule}>
|
<Can I={BankRuleAction.Create} a={AbilitySubject.BankRule}>
|
||||||
<Button intent={Intent.PRIMARY} large={true} onClick={() => {}}>
|
<Button
|
||||||
New tax rate
|
intent={Intent.PRIMARY}
|
||||||
|
large={true}
|
||||||
|
onClick={handleNewBtnClick}
|
||||||
|
>
|
||||||
|
New Bank Rule
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button intent={Intent.NONE} large={true}>
|
<Button intent={Intent.NONE} large={true}>
|
||||||
<T id={'learn_more'} />
|
<T id={'learn_more'} />
|
||||||
</Button>
|
</Button>
|
||||||
</Can>
|
</Can>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
classNames={{ root: styles.root }}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import React, { createContext } from 'react';
|
import React, { createContext } from 'react';
|
||||||
import { DialogContent } from '@/components';
|
import { DialogContent } from '@/components';
|
||||||
import { useBankRules } from '@/hooks/query/bank-rules';
|
import { useBankRules } from '@/hooks/query/bank-rules';
|
||||||
|
import { isEmpty } from 'lodash';
|
||||||
|
|
||||||
interface RulesListBootValues {
|
interface RulesListBootValues {
|
||||||
bankRules: any;
|
bankRules: any;
|
||||||
isBankRulesLoading: boolean;
|
isBankRulesLoading: boolean;
|
||||||
|
isEmptyState: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const RulesListBootContext = createContext<RulesListBootValues>(
|
const RulesListBootContext = createContext<RulesListBootValues>(
|
||||||
@@ -18,9 +20,11 @@ interface RulesListBootProps {
|
|||||||
function RulesListBoot({ ...props }: RulesListBootProps) {
|
function RulesListBoot({ ...props }: RulesListBootProps) {
|
||||||
const { data: bankRules, isLoading: isBankRulesLoading } = useBankRules();
|
const { data: bankRules, isLoading: isBankRulesLoading } = useBankRules();
|
||||||
|
|
||||||
const provider = { bankRules, isBankRulesLoading } as RulesListBootValues;
|
const isEmptyState = !isBankRulesLoading && isEmpty(bankRules);
|
||||||
const isLoading = isBankRulesLoading;
|
const isLoading = isBankRulesLoading;
|
||||||
|
|
||||||
|
const provider = { bankRules, isBankRulesLoading, isEmptyState } as RulesListBootValues;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DialogContent isLoading={isLoading}>
|
<DialogContent isLoading={isLoading}>
|
||||||
<RulesListBootContext.Provider {...props} value={provider} />
|
<RulesListBootContext.Provider {...props} value={provider} />
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ function RulesTable({
|
|||||||
}) {
|
}) {
|
||||||
// Invoices table columns.
|
// Invoices table columns.
|
||||||
const columns = useBankRulesTableColumns();
|
const columns = useBankRulesTableColumns();
|
||||||
const { bankRules } = useRulesListBoot();
|
const { bankRules, isEmptyState } = useRulesListBoot();
|
||||||
|
|
||||||
// Handle edit bank rule.
|
// Handle edit bank rule.
|
||||||
const handleDeleteBankRule = ({ id }) => {
|
const handleDeleteBankRule = ({ id }) => {
|
||||||
@@ -45,8 +45,6 @@ function RulesTable({
|
|||||||
openDialog(DialogsName.BankRuleForm, { bankRuleId: id });
|
openDialog(DialogsName.BankRuleForm, { bankRuleId: id });
|
||||||
};
|
};
|
||||||
|
|
||||||
const isEmptyState = false;
|
|
||||||
|
|
||||||
// Display invoice empty status instead of the table.
|
// Display invoice empty status instead of the table.
|
||||||
if (isEmptyState) {
|
if (isEmptyState) {
|
||||||
return <BankRulesLandingEmptyState />;
|
return <BankRulesLandingEmptyState />;
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ 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';
|
||||||
import { useAppQueryString } from '@/hooks';
|
import { useAppQueryString } from '@/hooks';
|
||||||
|
import { useGetBankAccountSummaryMeta } from '@/hooks/query/bank-rules';
|
||||||
|
|
||||||
const AccountTransactionsContext = React.createContext();
|
const AccountTransactionsContext = React.createContext();
|
||||||
|
|
||||||
@@ -20,31 +21,38 @@ function AccountTransactionsProvider({ query, ...props }) {
|
|||||||
const setFilterTab = (value: string) => {
|
const setFilterTab = (value: string) => {
|
||||||
setLocationQuery({ filter: value });
|
setLocationQuery({ filter: value });
|
||||||
};
|
};
|
||||||
// Fetch cashflow accounts.
|
// Retrieves cashflow accounts.
|
||||||
const {
|
const {
|
||||||
data: cashflowAccounts,
|
data: cashflowAccounts,
|
||||||
isFetching: isCashFlowAccountsFetching,
|
isFetching: isCashFlowAccountsFetching,
|
||||||
isLoading: isCashFlowAccountsLoading,
|
isLoading: isCashFlowAccountsLoading,
|
||||||
} = useCashflowAccounts(query, { keepPreviousData: true });
|
} = useCashflowAccounts(query, { keepPreviousData: true });
|
||||||
|
|
||||||
// Retrieve specific account details.
|
// Retrieves specific account details.
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: currentAccount,
|
data: currentAccount,
|
||||||
isFetching: isCurrentAccountFetching,
|
isFetching: isCurrentAccountFetching,
|
||||||
isLoading: isCurrentAccountLoading,
|
isLoading: isCurrentAccountLoading,
|
||||||
} = useAccount(accountId, { keepPreviousData: true });
|
} = useAccount(accountId, { keepPreviousData: true });
|
||||||
|
|
||||||
|
// Retrieves the bank account meta summary.
|
||||||
|
const {
|
||||||
|
data: bankAccountMetaSummary,
|
||||||
|
isLoading: isBankAccountMetaSummaryLoading,
|
||||||
|
} = useGetBankAccountSummaryMeta(accountId);
|
||||||
|
|
||||||
// Provider payload.
|
// Provider payload.
|
||||||
const provider = {
|
const provider = {
|
||||||
accountId,
|
accountId,
|
||||||
cashflowAccounts,
|
cashflowAccounts,
|
||||||
currentAccount,
|
currentAccount,
|
||||||
|
bankAccountMetaSummary,
|
||||||
|
|
||||||
isCashFlowAccountsFetching,
|
isCashFlowAccountsFetching,
|
||||||
isCashFlowAccountsLoading,
|
isCashFlowAccountsLoading,
|
||||||
isCurrentAccountFetching,
|
isCurrentAccountFetching,
|
||||||
isCurrentAccountLoading,
|
isCurrentAccountLoading,
|
||||||
|
isBankAccountMetaSummaryLoading,
|
||||||
|
|
||||||
filterTab,
|
filterTab,
|
||||||
setFilterTab,
|
setFilterTab,
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { Tag } from '@blueprintjs/core';
|
|||||||
import { useAppQueryString } from '@/hooks';
|
import { useAppQueryString } from '@/hooks';
|
||||||
import { useUncontrolled } from '@/hooks/useUncontrolled';
|
import { useUncontrolled } from '@/hooks/useUncontrolled';
|
||||||
import { Group } from '@/components';
|
import { Group } from '@/components';
|
||||||
|
import { useAccountTransactionsContext } from './AccountTransactionsProvider';
|
||||||
|
|
||||||
const Root = styled.div`
|
const Root = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -26,8 +27,13 @@ const FilterTag = styled(Tag)`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
export function AccountTransactionsUncategorizeFilter() {
|
export function AccountTransactionsUncategorizeFilter() {
|
||||||
|
const { bankAccountMetaSummary } = useAccountTransactionsContext();
|
||||||
const [locationQuery, setLocationQuery] = useAppQueryString();
|
const [locationQuery, setLocationQuery] = useAppQueryString();
|
||||||
|
|
||||||
|
const totalUncategorized =
|
||||||
|
bankAccountMetaSummary?.totalUncategorizedTransactions;
|
||||||
|
const totalRecognized = bankAccountMetaSummary?.totalRecognizedTransactions;
|
||||||
|
|
||||||
const handleTabsChange = (value) => {
|
const handleTabsChange = (value) => {
|
||||||
setLocationQuery({ uncategorizedFilter: value });
|
setLocationQuery({ uncategorizedFilter: value });
|
||||||
};
|
};
|
||||||
@@ -36,8 +42,22 @@ export function AccountTransactionsUncategorizeFilter() {
|
|||||||
<Group position={'apart'}>
|
<Group position={'apart'}>
|
||||||
<SegmentedTabs
|
<SegmentedTabs
|
||||||
options={[
|
options={[
|
||||||
{ value: 'all', label: 'All' },
|
{
|
||||||
{ value: 'recognized', label: 'Recognized' },
|
value: 'all',
|
||||||
|
label: (
|
||||||
|
<>
|
||||||
|
All <strong>({totalUncategorized})</strong>
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'recognized',
|
||||||
|
label: (
|
||||||
|
<>
|
||||||
|
Recognized <strong>({totalRecognized})</strong>
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
},
|
||||||
]}
|
]}
|
||||||
value={locationQuery?.uncategorizedFilter || 'all'}
|
value={locationQuery?.uncategorizedFilter || 'all'}
|
||||||
onValueChange={handleTabsChange}
|
onValueChange={handleTabsChange}
|
||||||
@@ -66,8 +86,9 @@ function SegmentedTabs({ options, initialValue, value, onValueChange }) {
|
|||||||
});
|
});
|
||||||
return (
|
return (
|
||||||
<Root>
|
<Root>
|
||||||
{options.map((option) => (
|
{options.map((option, index) => (
|
||||||
<FilterTag
|
<FilterTag
|
||||||
|
key={index}
|
||||||
round
|
round
|
||||||
interactive
|
interactive
|
||||||
onClick={() => handleChange(option.value)}
|
onClick={() => handleChange(option.value)}
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ function AccountTransactionsDataTable({
|
|||||||
vListOverscanRowCount={0}
|
vListOverscanRowCount={0}
|
||||||
initialColumnsWidths={initialColumnsWidths}
|
initialColumnsWidths={initialColumnsWidths}
|
||||||
onColumnResizing={handleColumnResizing}
|
onColumnResizing={handleColumnResizing}
|
||||||
noResults={<T id={'cash_flow.account_transactions.no_results'} />}
|
noResults={'There is no uncategorized transactions in the current account.'}
|
||||||
className="table-constrant"
|
className="table-constrant"
|
||||||
payload={{
|
payload={{
|
||||||
onExclude: handleExcludeTransaction,
|
onExclude: handleExcludeTransaction,
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ function ExcludedTransactionsTableRoot() {
|
|||||||
vListOverscanRowCount={0}
|
vListOverscanRowCount={0}
|
||||||
initialColumnsWidths={initialColumnsWidths}
|
initialColumnsWidths={initialColumnsWidths}
|
||||||
onColumnResizing={handleColumnResizing}
|
onColumnResizing={handleColumnResizing}
|
||||||
// noResults={<T id={'cash_flow.account_transactions.no_results'} />}
|
noResults={'There is no excluded bank transactions.'}
|
||||||
className="table-constrant"
|
className="table-constrant"
|
||||||
payload={{
|
payload={{
|
||||||
onRestore: handleRestoreClick,
|
onRestore: handleRestoreClick,
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
TableVirtualizedListRows,
|
TableVirtualizedListRows,
|
||||||
FormattedMessage as T,
|
FormattedMessage as T,
|
||||||
AppToaster,
|
AppToaster,
|
||||||
|
Stack,
|
||||||
} from '@/components';
|
} from '@/components';
|
||||||
import { TABLES } from '@/constants/tables';
|
import { TABLES } from '@/constants/tables';
|
||||||
|
|
||||||
@@ -24,11 +25,12 @@ import { useRecognizedTransactionsBoot } from './RecognizedTransactionsTableBoot
|
|||||||
import { ActionsMenu } from './_components';
|
import { ActionsMenu } from './_components';
|
||||||
import { compose } from '@/utils';
|
import { compose } from '@/utils';
|
||||||
import { useExcludeUncategorizedTransaction } from '@/hooks/query/bank-rules';
|
import { useExcludeUncategorizedTransaction } from '@/hooks/query/bank-rules';
|
||||||
import { Intent } from '@blueprintjs/core';
|
import { Intent, Text } from '@blueprintjs/core';
|
||||||
import {
|
import {
|
||||||
WithBankingActionsProps,
|
WithBankingActionsProps,
|
||||||
withBankingActions,
|
withBankingActions,
|
||||||
} from '../../withBankingActions';
|
} from '../../withBankingActions';
|
||||||
|
import styles from './RecognizedTransactionsTable.module.scss';
|
||||||
|
|
||||||
interface RecognizedTransactionsTableProps extends WithBankingActionsProps {}
|
interface RecognizedTransactionsTableProps extends WithBankingActionsProps {}
|
||||||
|
|
||||||
@@ -114,7 +116,7 @@ function RecognizedTransactionsTableRoot({
|
|||||||
vListOverscanRowCount={0}
|
vListOverscanRowCount={0}
|
||||||
initialColumnsWidths={initialColumnsWidths}
|
initialColumnsWidths={initialColumnsWidths}
|
||||||
onColumnResizing={handleColumnResizing}
|
onColumnResizing={handleColumnResizing}
|
||||||
noResults={<T id={'cash_flow.account_transactions.no_results'} />}
|
noResults={<RecognizedTransactionsTableNoResults />}
|
||||||
className="table-constrant"
|
className="table-constrant"
|
||||||
payload={{
|
payload={{
|
||||||
onExclude: handleExcludeClick,
|
onExclude: handleExcludeClick,
|
||||||
@@ -168,3 +170,26 @@ const CashflowTransactionsTable = styled(DashboardConstrantTable)`
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
function RecognizedTransactionsTableNoResults() {
|
||||||
|
return (
|
||||||
|
<Stack spacing={12} className={styles.emptyState}>
|
||||||
|
<Text>
|
||||||
|
There are no Recognized transactions due to one of the following
|
||||||
|
reasons:
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
Transaction Rules have not yet been created. Transactions are
|
||||||
|
recognized based on the rule criteria.
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li>
|
||||||
|
The transactions in your bank do not satisfy the criteria in any of
|
||||||
|
your transaction rule(s).
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,8 +1,24 @@
|
|||||||
// @ts-nocheck
|
// @ts-nocheck
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import intl from 'react-intl-universal';
|
import intl from 'react-intl-universal';
|
||||||
import { Intent, Menu, MenuItem, MenuDivider, Tag } from '@blueprintjs/core';
|
import {
|
||||||
import { Can, FormatDateCell, Icon, MaterialProgressBar } from '@/components';
|
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 { useAccountTransactionsContext } from './AccountTransactionsProvider';
|
||||||
import { safeCallback } from '@/utils';
|
import { safeCallback } from '@/utils';
|
||||||
|
|
||||||
@@ -128,6 +144,34 @@ export function AccountTransactionsProgressBar() {
|
|||||||
) : null;
|
) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function statusAccessor(transaction) {
|
||||||
|
return transaction.is_recognized ? (
|
||||||
|
<Tooltip
|
||||||
|
compact
|
||||||
|
interactionKind={PopoverInteractionKind.HOVER}
|
||||||
|
position={Position.RIGHT}
|
||||||
|
content={
|
||||||
|
<Box>
|
||||||
|
<span>{transaction.assigned_category_formatted}</span>
|
||||||
|
<Icon
|
||||||
|
icon={'arrowRight'}
|
||||||
|
color={'#8F99A8'}
|
||||||
|
iconSize={12}
|
||||||
|
style={{ marginLeft: 8, marginRight: 8 }}
|
||||||
|
/>
|
||||||
|
<span>{transaction.assigned_account_name}</span>
|
||||||
|
</Box>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Box>
|
||||||
|
<Tag intent={Intent.SUCCESS} interactive>
|
||||||
|
Recognized
|
||||||
|
</Tag>
|
||||||
|
</Box>
|
||||||
|
</Tooltip>
|
||||||
|
) : null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve account uncategorized transctions table columns.
|
* Retrieve account uncategorized transctions table columns.
|
||||||
*/
|
*/
|
||||||
@@ -170,16 +214,7 @@ export function useAccountUncategorizedTransactionsColumns() {
|
|||||||
{
|
{
|
||||||
id: 'status',
|
id: 'status',
|
||||||
Header: 'Status',
|
Header: 'Status',
|
||||||
accessor: () =>
|
accessor: statusAccessor,
|
||||||
false ? (
|
|
||||||
<Tag intent={Intent.SUCCESS} interactive>
|
|
||||||
1 Matches
|
|
||||||
</Tag>
|
|
||||||
) : (
|
|
||||||
<Tag intent={Intent.SUCCESS} interactive>
|
|
||||||
Recognized
|
|
||||||
</Tag>
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'deposit',
|
id: 'deposit',
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
.root {
|
.root {
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
|
padding-bottom: 60px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.transaction {
|
.transaction {
|
||||||
@@ -35,4 +36,5 @@
|
|||||||
.footerTotal {
|
.footerTotal {
|
||||||
padding: 8px 16px;
|
padding: 8px 16px;
|
||||||
border-top: 1px solid #d8d9d9;
|
border-top: 1px solid #d8d9d9;
|
||||||
|
line-height: 24px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,23 @@
|
|||||||
|
// @ts-nocheck
|
||||||
import { Tab, Tabs } from '@blueprintjs/core';
|
import { Tab, Tabs } from '@blueprintjs/core';
|
||||||
import { MatchingBankTransaction } from './MatchingTransaction';
|
import { MatchingBankTransaction } from './MatchingTransaction';
|
||||||
import styles from './CategorizeTransactionTabs.module.scss';
|
|
||||||
import { CategorizeTransactionContent } from '../CategorizeTransaction/drawers/CategorizeTransactionDrawer/CategorizeTransactionContent';
|
import { CategorizeTransactionContent } from '../CategorizeTransaction/drawers/CategorizeTransactionDrawer/CategorizeTransactionContent';
|
||||||
|
import { useCategorizeTransactionTabsBoot } from './CategorizeTransactionTabsBoot';
|
||||||
|
import styles from './CategorizeTransactionTabs.module.scss';
|
||||||
|
|
||||||
export function CategorizeTransactionTabs() {
|
export function CategorizeTransactionTabs() {
|
||||||
|
const { uncategorizedTransaction } = useCategorizeTransactionTabsBoot();
|
||||||
|
const defaultSelectedTabId = uncategorizedTransaction?.is_recognized
|
||||||
|
? 'categorize'
|
||||||
|
: 'matching';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tabs large renderActiveTabPanelOnly className={styles.tabs}>
|
<Tabs
|
||||||
|
large
|
||||||
|
renderActiveTabPanelOnly
|
||||||
|
defaultSelectedTabId={defaultSelectedTabId}
|
||||||
|
className={styles.tabs}
|
||||||
|
>
|
||||||
<Tab
|
<Tab
|
||||||
id="categorize"
|
id="categorize"
|
||||||
title="Categorize Transaction"
|
title="Categorize Transaction"
|
||||||
|
|||||||
@@ -2,22 +2,26 @@
|
|||||||
import { isEmpty } from 'lodash';
|
import { isEmpty } from 'lodash';
|
||||||
import * as R from 'ramda';
|
import * as R from 'ramda';
|
||||||
import { AnchorButton, Button, Intent, Tag, Text } from '@blueprintjs/core';
|
import { AnchorButton, Button, Intent, Tag, Text } from '@blueprintjs/core';
|
||||||
|
import { FastField, FastFieldProps, Formik } from 'formik';
|
||||||
import { AppToaster, Box, FormatNumber, Group, Stack } from '@/components';
|
import { AppToaster, Box, FormatNumber, Group, Stack } from '@/components';
|
||||||
import {
|
import {
|
||||||
MatchingTransactionBoot,
|
MatchingTransactionBoot,
|
||||||
useMatchingTransactionBoot,
|
useMatchingTransactionBoot,
|
||||||
} from './MatchingTransactionBoot';
|
} from './MatchingTransactionBoot';
|
||||||
import { MatchTransaction, MatchTransactionProps } from './MatchTransaction';
|
import { MatchTransaction, MatchTransactionProps } from './MatchTransaction';
|
||||||
import styles from './CategorizeTransactionAside.module.scss';
|
|
||||||
import { FastField, FastFieldProps, Form, Formik } from 'formik';
|
|
||||||
import { useMatchUncategorizedTransaction } from '@/hooks/query/bank-rules';
|
import { useMatchUncategorizedTransaction } from '@/hooks/query/bank-rules';
|
||||||
import { MatchingTransactionFormValues } from './types';
|
import { MatchingTransactionFormValues } from './types';
|
||||||
import { transformToReq, useGetPendingAmountMatched } from './utils';
|
import {
|
||||||
|
transformToReq,
|
||||||
|
useGetPendingAmountMatched,
|
||||||
|
useIsShowReconcileTransactionLink,
|
||||||
|
} from './utils';
|
||||||
import { useCategorizeTransactionTabsBoot } from './CategorizeTransactionTabsBoot';
|
import { useCategorizeTransactionTabsBoot } from './CategorizeTransactionTabsBoot';
|
||||||
import {
|
import {
|
||||||
WithBankingActionsProps,
|
WithBankingActionsProps,
|
||||||
withBankingActions,
|
withBankingActions,
|
||||||
} from '../withBankingActions';
|
} from '../withBankingActions';
|
||||||
|
import styles from './CategorizeTransactionAside.module.scss';
|
||||||
|
|
||||||
const initialValues = {
|
const initialValues = {
|
||||||
matched: {},
|
matched: {},
|
||||||
@@ -189,20 +193,26 @@ interface MatchTransctionFooterProps extends WithBankingActionsProps {}
|
|||||||
*/
|
*/
|
||||||
const MatchTransactionFooter = R.compose(withBankingActions)(
|
const MatchTransactionFooter = R.compose(withBankingActions)(
|
||||||
({ closeMatchingTransactionAside }: MatchTransctionFooterProps) => {
|
({ closeMatchingTransactionAside }: MatchTransctionFooterProps) => {
|
||||||
|
const totalPending = useGetPendingAmountMatched();
|
||||||
|
const showReconcileLink = useIsShowReconcileTransactionLink();
|
||||||
|
|
||||||
const handleCancelBtnClick = () => {
|
const handleCancelBtnClick = () => {
|
||||||
closeMatchingTransactionAside();
|
closeMatchingTransactionAside();
|
||||||
};
|
};
|
||||||
const totalPending = useGetPendingAmountMatched();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box className={styles.footer}>
|
<Box className={styles.footer}>
|
||||||
<Box className={styles.footerTotal}>
|
<Box className={styles.footerTotal}>
|
||||||
<Group position={'apart'}>
|
<Group position={'apart'}>
|
||||||
|
{showReconcileLink && (
|
||||||
<AnchorButton small minimal intent={Intent.PRIMARY}>
|
<AnchorButton small minimal intent={Intent.PRIMARY}>
|
||||||
Add Reconcile Transaction +
|
Add Reconcile Transaction +
|
||||||
</AnchorButton>
|
</AnchorButton>
|
||||||
|
)}
|
||||||
<Text style={{ fontSize: 13 }} tagName="span">
|
<Text
|
||||||
|
style={{ fontSize: 14, marginLeft: 'auto', color: '#5F6B7C' }}
|
||||||
|
tagName="span"
|
||||||
|
>
|
||||||
Pending <FormatNumber value={totalPending} currency={'USD'} />
|
Pending <FormatNumber value={totalPending} currency={'USD'} />
|
||||||
</Text>
|
</Text>
|
||||||
</Group>
|
</Group>
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { useFormikContext } from 'formik';
|
import { useFormikContext } from 'formik';
|
||||||
import { MatchingTransactionFormValues } from './types';
|
import { MatchingTransactionFormValues } from './types';
|
||||||
import { useMatchingTransactionBoot } from './MatchingTransactionBoot';
|
import { useMatchingTransactionBoot } from './MatchingTransactionBoot';
|
||||||
|
import { useCategorizeTransactionTabsBoot } from './CategorizeTransactionTabsBoot';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
export const transformToReq = (values: MatchingTransactionFormValues) => {
|
export const transformToReq = (values: MatchingTransactionFormValues) => {
|
||||||
const matchedTransactions = Object.entries(values.matched)
|
const matchedTransactions = Object.entries(values.matched)
|
||||||
@@ -17,7 +19,9 @@ export const transformToReq = (values: MatchingTransactionFormValues) => {
|
|||||||
export const useGetPendingAmountMatched = () => {
|
export const useGetPendingAmountMatched = () => {
|
||||||
const { values } = useFormikContext<MatchingTransactionFormValues>();
|
const { values } = useFormikContext<MatchingTransactionFormValues>();
|
||||||
const { perfectMatches, possibleMatches } = useMatchingTransactionBoot();
|
const { perfectMatches, possibleMatches } = useMatchingTransactionBoot();
|
||||||
|
const { uncategorizedTransaction } = useCategorizeTransactionTabsBoot();
|
||||||
|
|
||||||
|
return useMemo(() => {
|
||||||
const matchedItems = [...perfectMatches, ...possibleMatches].filter(
|
const matchedItems = [...perfectMatches, ...possibleMatches].filter(
|
||||||
(match) => {
|
(match) => {
|
||||||
const key = `${match.transactionType}-${match.transactionId}`;
|
const key = `${match.transactionType}-${match.transactionId}`;
|
||||||
@@ -28,7 +32,25 @@ export const useGetPendingAmountMatched = () => {
|
|||||||
(total, item) => total + parseFloat(item.amount),
|
(total, item) => total + parseFloat(item.amount),
|
||||||
0,
|
0,
|
||||||
);
|
);
|
||||||
const pendingAmount = 0 - totalMatchedAmount;
|
const amount = uncategorizedTransaction.amount;
|
||||||
|
const pendingAmount = amount - totalMatchedAmount;
|
||||||
|
|
||||||
return pendingAmount;
|
return pendingAmount;
|
||||||
|
}, [uncategorizedTransaction, perfectMatches, possibleMatches, values]);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useAtleastOneMatchedSelected = () => {
|
||||||
|
const { values } = useFormikContext<MatchingTransactionFormValues>();
|
||||||
|
|
||||||
|
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;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ const QUERY_KEY = {
|
|||||||
EXCLUDED_BANK_TRANSACTIONS_INFINITY: 'EXCLUDED_BANK_TRANSACTIONS_INFINITY',
|
EXCLUDED_BANK_TRANSACTIONS_INFINITY: 'EXCLUDED_BANK_TRANSACTIONS_INFINITY',
|
||||||
RECOGNIZED_BANK_TRANSACTIONS_INFINITY:
|
RECOGNIZED_BANK_TRANSACTIONS_INFINITY:
|
||||||
'RECOGNIZED_BANK_TRANSACTIONS_INFINITY',
|
'RECOGNIZED_BANK_TRANSACTIONS_INFINITY',
|
||||||
|
BANK_ACCOUNT_SUMMARY_META: 'BANK_ACCOUNT_SUMMARY_META',
|
||||||
};
|
};
|
||||||
|
|
||||||
const commonInvalidateQueries = (query: QueryClient) => {
|
const commonInvalidateQueries = (query: QueryClient) => {
|
||||||
@@ -111,6 +112,10 @@ export function useDeleteBankRule(
|
|||||||
{
|
{
|
||||||
onSuccess: (res, id) => {
|
onSuccess: (res, id) => {
|
||||||
commonInvalidateQueries(queryClient);
|
commonInvalidateQueries(queryClient);
|
||||||
|
|
||||||
|
queryClient.invalidateQueries(
|
||||||
|
QUERY_KEY.RECOGNIZED_BANK_TRANSACTIONS_INFINITY,
|
||||||
|
);
|
||||||
},
|
},
|
||||||
...options,
|
...options,
|
||||||
},
|
},
|
||||||
@@ -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<GetBankAccountSummaryMetaRes, Error>} options
|
||||||
|
* @returns {UseQueryResult<GetBankAccountSummaryMetaRes, Error>}
|
||||||
|
*/
|
||||||
|
export function useGetBankAccountSummaryMeta(
|
||||||
|
bankAccountId: number,
|
||||||
|
options?: UseQueryOptions<GetBankAccountSummaryMetaRes, Error>,
|
||||||
|
): UseQueryResult<GetBankAccountSummaryMetaRes, Error> {
|
||||||
|
const apiRequest = useApiRequest();
|
||||||
|
|
||||||
|
return useQuery<GetBankAccountSummaryMetaRes, Error>(
|
||||||
|
[QUERY_KEY.BANK_ACCOUNT_SUMMARY_META, bankAccountId],
|
||||||
|
() =>
|
||||||
|
apiRequest
|
||||||
|
.get(`/banking/bank_accounts/${bankAccountId}/meta`)
|
||||||
|
.then((res) => transformToCamelCase(res.data?.data)),
|
||||||
|
{ ...options },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
|
|||||||
Reference in New Issue
Block a user