diff --git a/packages/server/src/api/controllers/Banking/ExcludeBankTransactionsController.ts b/packages/server/src/api/controllers/Banking/ExcludeBankTransactionsController.ts index 79a6c7336..9d4ddec22 100644 --- a/packages/server/src/api/controllers/Banking/ExcludeBankTransactionsController.ts +++ b/packages/server/src/api/controllers/Banking/ExcludeBankTransactionsController.ts @@ -1,8 +1,9 @@ import { Inject, Service } from 'typedi'; -import { param } from 'express-validator'; -import { NextFunction, Request, Response, Router, query } from 'express'; +import { body, param, query } from 'express-validator'; +import { NextFunction, Request, Response, Router } from 'express'; import BaseController from '../BaseController'; import { ExcludeBankTransactionsApplication } from '@/services/Banking/Exclude/ExcludeBankTransactionsApplication'; +import { map, parseInt, trim } from 'lodash'; @Service() export class ExcludeBankTransactionsController extends BaseController { @@ -15,9 +16,21 @@ export class ExcludeBankTransactionsController extends BaseController { public router() { const router = Router(); + router.put( + '/transactions/exclude', + [body('ids').exists()], + this.validationResult, + this.excludeBulkBankTransactions.bind(this) + ); + router.put( + '/transactions/unexclude', + [body('ids').exists()], + this.validationResult, + this.unexcludeBulkBankTransactins.bind(this) + ); router.put( '/transactions/:transactionId/exclude', - [param('transactionId').exists()], + [param('transactionId').exists().toInt()], this.validationResult, this.excludeBankTransaction.bind(this) ); @@ -94,6 +107,63 @@ export class ExcludeBankTransactionsController extends BaseController { } } + /** + * Exclude bank transactions in bulk. + * @param {Request} req + * @param {Response} res + * @param {NextFunction} next + */ + private async excludeBulkBankTransactions( + req: Request, + res: Response, + next: NextFunction + ) { + const { tenantId } = req; + const { ids } = this.matchedBodyData(req); + + try { + await this.excludeBankTransactionApp.excludeBankTransactions( + tenantId, + ids + ); + return res.status(200).send({ + message: 'The given bank transactions have been excluded', + ids, + }); + } catch (error) { + next(error); + } + } + + /** + * Unexclude the given bank transactions in bulk. + * @param {Request} req + * @param {Response} res + * @param {NextFunction} next + * @returns {Promise} + */ + private async unexcludeBulkBankTransactins( + req: Request, + res: Response, + next: NextFunction + ): Promise { + const { tenantId } = req; + const { ids } = this.matchedBodyData(req); + + try { + await this.excludeBankTransactionApp.unexcludeBankTransactions( + tenantId, + ids + ); + return res.status(200).send({ + message: 'The given bank transactions have been excluded', + ids, + }); + } catch (error) { + next(error); + } + } + /** * Retrieves the excluded uncategorized bank transactions. * @param {Request} req @@ -109,7 +179,6 @@ export class ExcludeBankTransactionsController extends BaseController { const { tenantId } = req; const filter = this.matchedBodyData(req); - console.log('123'); try { const data = await this.excludeBankTransactionApp.getExcludedBankTransactions( diff --git a/packages/server/src/services/Banking/Exclude/ExcludeBankTransactions.ts b/packages/server/src/services/Banking/Exclude/ExcludeBankTransactions.ts new file mode 100644 index 000000000..abf6bd434 --- /dev/null +++ b/packages/server/src/services/Banking/Exclude/ExcludeBankTransactions.ts @@ -0,0 +1,31 @@ +import { Inject, Service } from 'typedi'; +import { ExcludeBankTransaction } from './ExcludeBankTransaction'; +import PromisePool from '@supercharge/promise-pool'; +import { castArray } from 'lodash'; + +@Service() +export class ExcludeBankTransactions { + @Inject() + private excludeBankTransaction: ExcludeBankTransaction; + + /** + * Exclude bank transactions in bulk. + * @param {number} tenantId + * @param {number} bankTransactionIds + */ + public async excludeBankTransactions( + tenantId: number, + bankTransactionIds: Array | number + ) { + const _bankTransactionIds = castArray(bankTransactionIds); + + await PromisePool.withConcurrency(1) + .for(_bankTransactionIds) + .process(async (bankTransactionId: number) => { + return this.excludeBankTransaction.excludeBankTransaction( + tenantId, + bankTransactionId + ); + }); + } +} diff --git a/packages/server/src/services/Banking/Exclude/ExcludeBankTransactionsApplication.ts b/packages/server/src/services/Banking/Exclude/ExcludeBankTransactionsApplication.ts index a87b63815..621652d86 100644 --- a/packages/server/src/services/Banking/Exclude/ExcludeBankTransactionsApplication.ts +++ b/packages/server/src/services/Banking/Exclude/ExcludeBankTransactionsApplication.ts @@ -3,6 +3,8 @@ import { ExcludeBankTransaction } from './ExcludeBankTransaction'; import { UnexcludeBankTransaction } from './UnexcludeBankTransaction'; import { GetExcludedBankTransactionsService } from './GetExcludedBankTransactions'; import { ExcludedBankTransactionsQuery } from './_types'; +import { UnexcludeBankTransactions } from './UnexcludeBankTransactions'; +import { ExcludeBankTransactions } from './ExcludeBankTransactions'; @Service() export class ExcludeBankTransactionsApplication { @@ -15,6 +17,12 @@ export class ExcludeBankTransactionsApplication { @Inject() private getExcludedBankTransactionsService: GetExcludedBankTransactionsService; + @Inject() + private excludeBankTransactionsService: ExcludeBankTransactions; + + @Inject() + private unexcludeBankTransactionsService: UnexcludeBankTransactions; + /** * Marks a bank transaction as excluded. * @param {number} tenantId - The ID of the tenant. @@ -56,4 +64,36 @@ export class ExcludeBankTransactionsApplication { filter ); } + + /** + * Exclude the given bank transactions in bulk. + * @param {number} tenantId + * @param {Array | number} bankTransactionIds + * @returns {Promise} + */ + public excludeBankTransactions( + tenantId: number, + bankTransactionIds: Array | number + ): Promise { + return this.excludeBankTransactionsService.excludeBankTransactions( + tenantId, + bankTransactionIds + ); + } + + /** + * Exclude the given bank transactions in bulk. + * @param {number} tenantId + * @param {Array | number} bankTransactionIds + * @returns {Promise} + */ + public unexcludeBankTransactions( + tenantId: number, + bankTransactionIds: Array | number + ): Promise { + return this.unexcludeBankTransactionsService.unexcludeBankTransactions( + tenantId, + bankTransactionIds + ); + } } diff --git a/packages/server/src/services/Banking/Exclude/UnexcludeBankTransactions.ts b/packages/server/src/services/Banking/Exclude/UnexcludeBankTransactions.ts new file mode 100644 index 000000000..840eb6259 --- /dev/null +++ b/packages/server/src/services/Banking/Exclude/UnexcludeBankTransactions.ts @@ -0,0 +1,31 @@ +import { Inject, Service } from 'typedi'; +import PromisePool from '@supercharge/promise-pool'; +import { UnexcludeBankTransaction } from './UnexcludeBankTransaction'; +import { castArray } from 'lodash'; + +@Service() +export class UnexcludeBankTransactions { + @Inject() + private unexcludeBankTransaction: UnexcludeBankTransaction; + + /** + * Unexclude bank transactions in bulk. + * @param {number} tenantId + * @param {number} bankTransactionIds + */ + public async unexcludeBankTransactions( + tenantId: number, + bankTransactionIds: Array | number + ) { + const _bankTransactionIds = castArray(bankTransactionIds); + + await PromisePool.withConcurrency(1) + .for(_bankTransactionIds) + .process(async (bankTransactionId: number) => { + return this.unexcludeBankTransaction.unexcludeBankTransaction( + tenantId, + bankTransactionId + ); + }); + } +} diff --git a/packages/webapp/src/containers/CashFlow/AccountTransactions/AccountTransactionsActionsBar.tsx b/packages/webapp/src/containers/CashFlow/AccountTransactions/AccountTransactionsActionsBar.tsx index b5db2eb78..7ca9abc37 100644 --- a/packages/webapp/src/containers/CashFlow/AccountTransactions/AccountTransactionsActionsBar.tsx +++ b/packages/webapp/src/containers/CashFlow/AccountTransactions/AccountTransactionsActionsBar.tsx @@ -11,6 +11,7 @@ import { MenuItem, PopoverInteractionKind, Position, + Intent, } from '@blueprintjs/core'; import { useHistory } from 'react-router-dom'; import { @@ -18,6 +19,7 @@ import { DashboardActionsBar, DashboardRowsHeightButton, FormattedMessage as T, + AppToaster, } from '@/components'; import { CashFlowMenuItems } from './utils'; @@ -33,6 +35,13 @@ import withSettings from '@/containers/Settings/withSettings'; import withSettingsActions from '@/containers/Settings/withSettingsActions'; import { compose } from '@/utils'; +import { withBanking } from '../withBanking'; +import { isEmpty } from 'lodash'; +import { + useExcludeUncategorizedTransactions, + useUnexcludeUncategorizedTransaction, + useUnexcludeUncategorizedTransactions, +} from '@/hooks/query/bank-rules'; function AccountTransactionsActionsBar({ // #withDialogActions @@ -43,6 +52,10 @@ function AccountTransactionsActionsBar({ // #withSettingsActions addSetting, + + // #withBanking + uncategorizedTransationsIdsSelected, + excludedTransactionsIdsSelected, }) { const history = useHistory(); const { accountId } = useAccountTransactionsContext(); @@ -87,6 +100,54 @@ function AccountTransactionsActionsBar({ refresh(); }; + const { + mutateAsync: excludeUncategorizedTransactions, + isLoading: isExcludingLoading, + } = useExcludeUncategorizedTransactions(); + + const { + mutateAsync: unexcludeUncategorizedTransactions, + isLoading: isUnexcludingLoading, + } = useUnexcludeUncategorizedTransactions(); + + // Handles the exclude uncategorized transactions in bulk. + const handleExcludeUncategorizedBtnClick = () => { + excludeUncategorizedTransactions({ + ids: uncategorizedTransationsIdsSelected, + }) + .then(() => { + AppToaster.show({ + message: 'The selected transactions have been excluded.', + intent: Intent.SUCCESS, + }); + }) + .catch(() => { + AppToaster.show({ + message: 'Something went wrong', + intent: Intent.DANGER, + }); + }); + }; + + // Handles the unexclude categorized button click. + const handleUnexcludeUncategorizedBtnClick = () => { + unexcludeUncategorizedTransactions({ + ids: excludedTransactionsIdsSelected, + }) + .then(() => { + AppToaster.show({ + message: 'The selected transactions have been unexcluded.', + intent: Intent.SUCCESS, + }); + }) + .catch((error) => { + AppToaster.show({ + message: 'Something went wrong', + intent: Intent.DANGER, + }); + }); + }; + return ( @@ -129,6 +190,28 @@ function AccountTransactionsActionsBar({ onChange={handleTableRowSizeChange} /> + + {!isEmpty(uncategorizedTransationsIdsSelected) && ( +