diff --git a/packages/server/src/api/controllers/Cashflow/NewCashflowTransaction.ts b/packages/server/src/api/controllers/Cashflow/NewCashflowTransaction.ts index fea9b84c9..0ddf5861a 100644 --- a/packages/server/src/api/controllers/Cashflow/NewCashflowTransaction.ts +++ b/packages/server/src/api/controllers/Cashflow/NewCashflowTransaction.ts @@ -1,5 +1,5 @@ import { Service, Inject } from 'typedi'; -import { ValidationChain, check, param, query } from 'express-validator'; +import { ValidationChain, body, check, param, query } from 'express-validator'; import { Router, Request, Response, NextFunction } from 'express'; import { omit } from 'lodash'; import BaseController from '../BaseController'; @@ -43,6 +43,16 @@ export default class NewCashflowTransactionController extends BaseController { this.asyncMiddleware(this.newCashflowTransaction), this.catchServiceErrors ); + router.post( + '/transactions/uncategorize/bulk', + [ + body('ids').isArray({ min: 1 }), + body('ids.*').exists().isNumeric().toInt(), + ], + this.validationResult, + this.uncategorizeBulkTransactions.bind(this), + this.catchServiceErrors + ); router.post( '/transactions/:id/uncategorize', this.revertCategorizedCashflowTransaction, @@ -184,6 +194,34 @@ export default class NewCashflowTransactionController extends BaseController { } }; + /** + * Uncategorize the given transactions in bulk. + * @param {Request<{}>} req + * @param {Response} res + * @param {NextFunction} next + * @returns {Promise} + */ + private uncategorizeBulkTransactions = async ( + req: Request<{}>, + res: Response, + next: NextFunction + ) => { + const { tenantId } = req; + const { ids: uncategorizedTransactionIds } = this.matchedBodyData(req); + + try { + await this.cashflowApplication.uncategorizeTransactions( + tenantId, + uncategorizedTransactionIds + ); + return res.status(200).send({ + message: 'The given transactions have been uncategorized successfully.', + }); + } catch (error) { + next(error); + } + }; + /** * Categorize the cashflow transaction. * @param {Request} req diff --git a/packages/server/src/services/Cashflow/CashflowApplication.ts b/packages/server/src/services/Cashflow/CashflowApplication.ts index f54935db7..5773c1ee1 100644 --- a/packages/server/src/services/Cashflow/CashflowApplication.ts +++ b/packages/server/src/services/Cashflow/CashflowApplication.ts @@ -21,6 +21,7 @@ import GetCashflowAccountsService from './GetCashflowAccountsService'; import { GetCashflowTransactionService } from './GetCashflowTransactionsService'; import { GetRecognizedTransactionsService } from './GetRecongizedTransactions'; import { GetRecognizedTransactionService } from './GetRecognizedTransaction'; +import { UncategorizeCashflowTransactionsBulk } from './UncategorizeCashflowTransactionsBulk'; @Service() export class CashflowApplication { @@ -39,6 +40,9 @@ export class CashflowApplication { @Inject() private uncategorizeTransactionService: UncategorizeCashflowTransaction; + @Inject() + private uncategorizeTransasctionsService: UncategorizeCashflowTransactionsBulk; + @Inject() private categorizeTransactionService: CategorizeCashflowTransaction; @@ -155,6 +159,22 @@ export class CashflowApplication { ); } + /** + * Uncategorize the given transactions in bulk. + * @param {number} tenantId + * @param {number | Array} transactionId + * @returns + */ + public uncategorizeTransactions( + tenantId: number, + transactionId: number | Array + ) { + return this.uncategorizeTransasctionsService.uncategorizeBulk( + tenantId, + transactionId + ); + } + /** * Categorize the given cashflow transaction. * @param {number} tenantId @@ -241,9 +261,9 @@ export class CashflowApplication { /** * Retrieves the recognized transaction of the given uncategorized transaction. - * @param {number} tenantId - * @param {number} uncategorizedTransactionId - * @returns + * @param {number} tenantId + * @param {number} uncategorizedTransactionId + * @returns */ public getRecognizedTransaction( tenantId: number, diff --git a/packages/server/src/services/Cashflow/UncategorizeCashflowTransactionsBulk.ts b/packages/server/src/services/Cashflow/UncategorizeCashflowTransactionsBulk.ts new file mode 100644 index 000000000..e5f9ceee2 --- /dev/null +++ b/packages/server/src/services/Cashflow/UncategorizeCashflowTransactionsBulk.ts @@ -0,0 +1,37 @@ +import PromisePool from '@supercharge/promise-pool'; +import { castArray } from 'lodash'; +import { Service, Inject } from 'typedi'; +import HasTenancyService from '../Tenancy/TenancyService'; +import { UncategorizeCashflowTransaction } from './UncategorizeCashflowTransaction'; + +@Service() +export class UncategorizeCashflowTransactionsBulk { + @Inject() + private tenancy: HasTenancyService; + + @Inject() + private uncategorizeTransaction: UncategorizeCashflowTransaction; + + /** + * Uncategorize the given bank transactions in bulk. + * @param {number} tenantId + * @param {number} uncategorizedTransactionId + */ + public async uncategorizeBulk( + tenantId: number, + uncategorizedTransactionId: number | Array + ) { + const uncategorizedTransactionIds = castArray(uncategorizedTransactionId); + + const result = await PromisePool.withConcurrency(MIGRATION_CONCURRENCY) + .for(uncategorizedTransactionIds) + .process(async (_uncategorizedTransactionId: number, index, pool) => { + await this.uncategorizeTransaction.uncategorize( + tenantId, + _uncategorizedTransactionId + ); + }); + } +} + +const MIGRATION_CONCURRENCY = 1; diff --git a/packages/webapp/src/containers/CashFlow/AccountTransactions/AccountTransactionsActionsBar.tsx b/packages/webapp/src/containers/CashFlow/AccountTransactions/AccountTransactionsActionsBar.tsx index c5428b9da..9b605a672 100644 --- a/packages/webapp/src/containers/CashFlow/AccountTransactions/AccountTransactionsActionsBar.tsx +++ b/packages/webapp/src/containers/CashFlow/AccountTransactions/AccountTransactionsActionsBar.tsx @@ -210,7 +210,11 @@ function AccountTransactionsActionsBar({ }; // Handles uncategorize the categorized transactions in bulk. - const handleUncategorizeCategorizedBulkBtnClick = () => {}; + const handleUncategorizeCategorizedBulkBtnClick = () => { + openAlert('uncategorize-transactions-bulk', { + uncategorizeTransactionsIds: categorizedTransactionsSelected, + }); + }; return ( diff --git a/packages/webapp/src/containers/CashFlow/AccountTransactions/alerts/UncategorizeBankTransactionsBulkAlert.tsx b/packages/webapp/src/containers/CashFlow/AccountTransactions/alerts/UncategorizeBankTransactionsBulkAlert.tsx new file mode 100644 index 000000000..ae2ad77fd --- /dev/null +++ b/packages/webapp/src/containers/CashFlow/AccountTransactions/alerts/UncategorizeBankTransactionsBulkAlert.tsx @@ -0,0 +1,69 @@ +// @ts-nocheck +import React from 'react'; +import { Intent, Alert } from '@blueprintjs/core'; + +import { AppToaster, FormattedMessage as T } from '@/components'; +import withAlertStoreConnect from '@/containers/Alert/withAlertStoreConnect'; +import withAlertActions from '@/containers/Alert/withAlertActions'; + +import { useUncategorizeTransactionsBulkAction } from '@/hooks/query/bank-transactions'; +import { compose } from '@/utils'; + +/** + * Uncategorize bank account transactions in build alert. + */ +function UncategorizeBankTransactionsBulkAlert({ + name, + + // #withAlertStoreConnect + isOpen, + payload: { uncategorizeTransactionsIds }, + + // #withAlertActions + closeAlert, +}) { + const { mutateAsync: uncategorizeTransactions, isLoading } = + useUncategorizeTransactionsBulkAction(); + + // Handle activate item alert cancel. + const handleCancelActivateItem = () => { + closeAlert(name); + }; + + // Handle confirm item activated. + const handleConfirmItemActivate = () => { + uncategorizeTransactions({ ids: uncategorizeTransactionsIds }) + .then(() => { + AppToaster.show({ + message: 'The bank feeds of the bank account has been resumed.', + intent: Intent.SUCCESS, + }); + }) + .catch((error) => {}) + .finally(() => { + closeAlert(name); + }); + }; + + return ( + } + confirmButtonText={'Uncategorize Transactions'} + intent={Intent.DANGER} + isOpen={isOpen} + onCancel={handleCancelActivateItem} + loading={isLoading} + onConfirm={handleConfirmItemActivate} + > +

+ Are you sure want to uncategorize the selected bank transactions, this + action is not revertable but you can always categorize them again? +

+
+ ); +} + +export default compose( + withAlertStoreConnect(), + withAlertActions, +)(UncategorizeBankTransactionsBulkAlert); diff --git a/packages/webapp/src/containers/CashFlow/AccountTransactions/alerts/index.ts b/packages/webapp/src/containers/CashFlow/AccountTransactions/alerts/index.ts index c8b6e0fcb..007f21706 100644 --- a/packages/webapp/src/containers/CashFlow/AccountTransactions/alerts/index.ts +++ b/packages/webapp/src/containers/CashFlow/AccountTransactions/alerts/index.ts @@ -9,6 +9,10 @@ const PauseFeedsBankAccountAlert = React.lazy( () => import('./PauseFeedsBankAccount'), ); +const UncategorizeTransactionsBulkAlert = React.lazy( + () => import('./UncategorizeBankTransactionsBulkAlert'), +); + /** * Bank account alerts. */ @@ -21,4 +25,8 @@ export const BankAccountAlerts = [ name: 'pause-feeds-syncing-bank-accounnt', component: PauseFeedsBankAccountAlert, }, + { + name: 'uncategorize-transactions-bulk', + component: UncategorizeTransactionsBulkAlert, + }, ]; diff --git a/packages/webapp/src/hooks/query/bank-transactions.ts b/packages/webapp/src/hooks/query/bank-transactions.ts new file mode 100644 index 000000000..8604f75be --- /dev/null +++ b/packages/webapp/src/hooks/query/bank-transactions.ts @@ -0,0 +1,64 @@ +// @ts-nocheck +import { + useMutation, + UseMutationOptions, + UseMutationResult, + useQueryClient, +} from 'react-query'; +import useApiRequest from '../useRequest'; +import { BANK_QUERY_KEY } from '@/constants/query-keys/banking'; +import t from './types'; + +type UncategorizeTransactionsBulkValues = { ids: Array }; +interface UncategorizeBankTransactionsBulkResponse {} + +/** + * Uncategorize the given categorized transactions in bulk. + * @param {UseMutationResult} options + * @returns {UseMutationResult} + */ +export function useUncategorizeTransactionsBulkAction( + options?: UseMutationOptions< + UncategorizeBankTransactionsBulkResponse, + Error, + UncategorizeTransactionsBulkValues + >, +): UseMutationResult< + UncategorizeBankTransactionsBulkResponse, + Error, + UncategorizeTransactionsBulkValues +> { + const queryClient = useQueryClient(); + const apiRequest = useApiRequest(); + + return useMutation< + UncategorizeBankTransactionsBulkResponse, + Error, + UncategorizeTransactionsBulkValues + >( + (value) => + apiRequest.post(`/cashflow/transactions/uncategorize/bulk`, { + ids: value.ids, + }), + { + onSuccess: (res, values) => { + // Invalidate the account uncategorized transactions. + queryClient.invalidateQueries( + t.CASHFLOW_ACCOUNT_UNCATEGORIZED_TRANSACTIONS_INFINITY, + ); + // Invalidate the account transactions. + queryClient.invalidateQueries( + t.CASHFLOW_ACCOUNT_TRANSACTIONS_INFINITY + ); + // invalidate bank account summary. + queryClient.invalidateQueries(BANK_QUERY_KEY.BANK_ACCOUNT_SUMMARY_META); + + // Invalidate the recognized transactions. + queryClient.invalidateQueries([ + BANK_QUERY_KEY.RECOGNIZED_BANK_TRANSACTIONS_INFINITY, + ]); + }, + ...options, + }, + ); +}