From d74337fb94a64ab645f313b944b069e27564a7d1 Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Sat, 3 Aug 2024 22:01:21 +0200 Subject: [PATCH] feat: wip multi-select transactions to categorization and matching --- .../controllers/Banking/BankingController.ts | 5 + .../Banking/BankingUncategorizedController.ts | 57 ++++++ .../Cashflow/NewCashflowTransaction.ts | 1 + .../Matching/GetMatchedTransactions.ts | 3 + .../Banking/Matching/MatchTransactions.ts | 2 +- .../src/services/Banking/Matching/types.ts | 1 + .../GetAutofillCategorizeTransaction.ts | 45 +++++ ...utofillCategorizeTransactionTransformer.ts | 174 ++++++++++++++++++ .../RecognizeTranasctionsService.ts | 2 +- .../Cashflow/CategorizeCashflowTransaction.ts | 12 +- .../Cashflow/CommandCasflowValidator.ts | 18 +- ...entUncategorizedTransactionOnCategorize.ts | 5 +- .../server/src/services/Cashflow/utils.ts | 5 +- .../AccountTransactionsUncategorizedTable.tsx | 28 +-- .../UncategorizedTransactions/hooks.tsx | 6 +- .../CategorizeTransactionBoot.tsx | 31 ++-- .../CategorizeTransactionContent.tsx | 17 +- .../CategorizeTransactionForm.tsx | 6 +- .../CategorizeTransactionFormContent.tsx | 9 +- .../CategorizeTransactionDrawer/_utils.ts | 43 +---- .../CategorizeTransactionAside.tsx | 11 +- .../CategorizeTransactionTabs.tsx | 6 +- .../CategorizeTransactionTabsBoot.tsx | 36 ++-- .../MatchingTransaction.tsx | 22 +-- .../MatchingTransactionBoot.tsx | 3 + .../CategorizeTransactionAside/utils.ts | 17 +- .../src/containers/CashFlow/withBanking.ts | 2 +- .../containers/CashFlow/withBankingActions.ts | 20 ++ packages/webapp/src/hooks/query/bank-rules.ts | 44 ++++- 29 files changed, 476 insertions(+), 155 deletions(-) create mode 100644 packages/server/src/api/controllers/Banking/BankingUncategorizedController.ts create mode 100644 packages/server/src/services/Banking/RegonizeTranasctions/GetAutofillCategorizeTransaction.ts create mode 100644 packages/server/src/services/Banking/RegonizeTranasctions/GetAutofillCategorizeTransactionTransformer.ts diff --git a/packages/server/src/api/controllers/Banking/BankingController.ts b/packages/server/src/api/controllers/Banking/BankingController.ts index 30695b19d..69933f16d 100644 --- a/packages/server/src/api/controllers/Banking/BankingController.ts +++ b/packages/server/src/api/controllers/Banking/BankingController.ts @@ -6,6 +6,7 @@ import { BankingRulesController } from './BankingRulesController'; import { BankTransactionsMatchingController } from './BankTransactionsMatchingController'; import { RecognizedTransactionsController } from './RecognizedTransactionsController'; import { BankAccountsController } from './BankAccountsController'; +import { BankingUncategorizedController } from './BankingUncategorizedController'; @Service() export class BankingController extends BaseController { @@ -29,6 +30,10 @@ export class BankingController extends BaseController { '/bank_accounts', Container.get(BankAccountsController).router() ); + router.use( + '/categorize', + Container.get(BankingUncategorizedController).router() + ); return router; } } diff --git a/packages/server/src/api/controllers/Banking/BankingUncategorizedController.ts b/packages/server/src/api/controllers/Banking/BankingUncategorizedController.ts new file mode 100644 index 000000000..396ccbcda --- /dev/null +++ b/packages/server/src/api/controllers/Banking/BankingUncategorizedController.ts @@ -0,0 +1,57 @@ +import { Inject, Service } from 'typedi'; +import { NextFunction, Request, Response, Router } from 'express'; +import { query } from 'express-validator'; +import BaseController from '../BaseController'; +import { GetAutofillCategorizeTransaction } from '@/services/Banking/RegonizeTranasctions/GetAutofillCategorizeTransaction'; + +@Service() +export class BankingUncategorizedController extends BaseController { + @Inject() + private getAutofillCategorizeTransactionService: GetAutofillCategorizeTransaction; + + /** + * Router constructor. + */ + router() { + const router = Router(); + + router.get( + '/autofill', + [ + query('uncategorizedTransactionIds').isArray({ min: 1 }), + query('uncategorizedTransactionIds.*').isNumeric().toInt(), + ], + this.validationResult, + this.getAutofillCategorizeTransaction.bind(this) + ); + return router; + } + + /** + * Retrieves the autofill values of the categorize form of the given + * uncategorized transactions. + * @param {Request} req + * @param {Response} res + * @param {NextFunction} next + * @returns {Promise} + */ + public async getAutofillCategorizeTransaction( + req: Request, + res: Response, + next: NextFunction + ) { + const { tenantId } = req; + const uncategorizedTransactionIds = req.query.uncategorizedTransactionIds; + + try { + const data = + await this.getAutofillCategorizeTransactionService.getAutofillCategorizeTransaction( + tenantId, + uncategorizedTransactionIds + ); + return res.status(200).send({ data }); + } catch (error) { + next(error); + } + } +} diff --git a/packages/server/src/api/controllers/Cashflow/NewCashflowTransaction.ts b/packages/server/src/api/controllers/Cashflow/NewCashflowTransaction.ts index df6c86bcf..51f45396a 100644 --- a/packages/server/src/api/controllers/Cashflow/NewCashflowTransaction.ts +++ b/packages/server/src/api/controllers/Cashflow/NewCashflowTransaction.ts @@ -201,6 +201,7 @@ export default class NewCashflowTransactionController extends BaseController { const categorizeDTO = omit(matchedObject, [ 'uncategorizedTransactionIds', ]) as ICategorizeCashflowTransactioDTO; + const uncategorizedTransactionIds = matchedObject.uncategorizedTransactionIds; diff --git a/packages/server/src/services/Banking/Matching/GetMatchedTransactions.ts b/packages/server/src/services/Banking/Matching/GetMatchedTransactions.ts index 3b5f1339a..c7caf1325 100644 --- a/packages/server/src/services/Banking/Matching/GetMatchedTransactions.ts +++ b/packages/server/src/services/Banking/Matching/GetMatchedTransactions.ts @@ -64,6 +64,8 @@ export class GetMatchedTransactions { .whereIn('id', uncategorizedTransactionIds) .throwIfNotFound(); + const totalPending = Math.abs(sumBy(uncategorizedTransactions, 'amount')); + const filtered = filter.transactionType ? this.registered.filter((item) => item.type === filter.transactionType) : this.registered; @@ -80,6 +82,7 @@ export class GetMatchedTransactions { return { perfectMatches, possibleMatches, + totalPending, }; } diff --git a/packages/server/src/services/Banking/Matching/MatchTransactions.ts b/packages/server/src/services/Banking/Matching/MatchTransactions.ts index af0d41f60..c1604d9c1 100644 --- a/packages/server/src/services/Banking/Matching/MatchTransactions.ts +++ b/packages/server/src/services/Banking/Matching/MatchTransactions.ts @@ -101,7 +101,7 @@ export class MatchBankTransactions { ); // Validates the total given matching transcations whether is not equal // uncategorized transaction amount. - if (totalUncategorizedTransactions === totalMatchedTranasctions) { + if (totalUncategorizedTransactions !== totalMatchedTranasctions) { throw new ServiceError(ERRORS.TOTAL_MATCHING_TRANSACTIONS_INVALID); } } diff --git a/packages/server/src/services/Banking/Matching/types.ts b/packages/server/src/services/Banking/Matching/types.ts index ff295bd9d..707c0fe0c 100644 --- a/packages/server/src/services/Banking/Matching/types.ts +++ b/packages/server/src/services/Banking/Matching/types.ts @@ -58,6 +58,7 @@ export interface MatchedTransactionPOJO { export type MatchedTransactionsPOJO = { perfectMatches: Array; possibleMatches: Array; + totalPending: number; }; export const ERRORS = { diff --git a/packages/server/src/services/Banking/RegonizeTranasctions/GetAutofillCategorizeTransaction.ts b/packages/server/src/services/Banking/RegonizeTranasctions/GetAutofillCategorizeTransaction.ts new file mode 100644 index 000000000..e31402fad --- /dev/null +++ b/packages/server/src/services/Banking/RegonizeTranasctions/GetAutofillCategorizeTransaction.ts @@ -0,0 +1,45 @@ +import { Inject, Service } from 'typedi'; +import { castArray, first, uniq } from 'lodash'; +import HasTenancyService from '@/services/Tenancy/TenancyService'; +import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable'; +import { GetAutofillCategorizeTransctionTransformer } from './GetAutofillCategorizeTransactionTransformer'; + +@Service() +export class GetAutofillCategorizeTransaction { + @Inject() + private tenancy: HasTenancyService; + + @Inject() + private transformer: TransformerInjectable; + + /** + * Retrieves the autofill values of categorize transactions form. + * @param {number} tenantId - Tenant id. + * @param {Array | number} uncategorizeTransactionsId - Uncategorized transactions ids. + */ + public async getAutofillCategorizeTransaction( + tenantId: number, + uncategorizeTransactionsId: Array | number + ) { + const { UncategorizedCashflowTransaction } = this.tenancy.models(tenantId); + const uncategorizeTransactionsIds = uniq( + castArray(uncategorizeTransactionsId) + ); + const uncategorizedTransactions = + await UncategorizedCashflowTransaction.query() + .whereIn('id', uncategorizeTransactionsIds) + .withGraphFetched('recognizedTransaction.assignAccount') + .withGraphFetched('recognizedTransaction.bankRule') + .throwIfNotFound(); + + return this.transformer.transform( + tenantId, + {}, + new GetAutofillCategorizeTransctionTransformer(), + { + uncategorizedTransactions, + firstUncategorizedTransaction: first(uncategorizedTransactions), + } + ); + } +} diff --git a/packages/server/src/services/Banking/RegonizeTranasctions/GetAutofillCategorizeTransactionTransformer.ts b/packages/server/src/services/Banking/RegonizeTranasctions/GetAutofillCategorizeTransactionTransformer.ts new file mode 100644 index 000000000..46a142e48 --- /dev/null +++ b/packages/server/src/services/Banking/RegonizeTranasctions/GetAutofillCategorizeTransactionTransformer.ts @@ -0,0 +1,174 @@ +import { Transformer } from '@/lib/Transformer/Transformer'; +import { sumBy } from 'lodash'; + +export class GetAutofillCategorizeTransctionTransformer extends Transformer { + /** + * Included attributes to the object. + * @returns {Array} + */ + public includeAttributes = (): string[] => { + return [ + 'amount', + 'formattedAmount', + 'isRecognized', + 'date', + 'formattedDate', + 'creditAccountId', + 'debitAccountId', + 'referenceNo', + 'transactionType', + 'recognizedByRuleId', + 'recognizedByRuleName', + 'isWithdrawalTransaction', + 'isDepositTransaction', + ]; + }; + + /** + * Detarmines whether the transaction is recognized. + * @returns {boolean} + */ + public isRecognized() { + return !!this.options.firstUncategorizedTransaction?.recognizedTransaction; + } + + /** + * Retrieves the total amount of uncategorized transactions. + * @returns {number} + */ + public amount() { + return sumBy(this.options.uncategorizedTransactions, 'amount'); + } + + /** + * Retrieves the formatted total amount of uncategorized transactions. + * @returns {string} + */ + public formattedAmount() { + return this.formatNumber(this.amount(), { + currencyCode: 'USD', + money: true, + }); + } + + /** + * Detarmines whether the transaction is deposit. + * @returns {boolean} + */ + public isDepositTransaction() { + const amount = this.amount(); + + return amount > 0; + } + + /** + * Detarmines whether the transaction is withdrawal. + * @returns {boolean} + */ + public isWithdrawalTransaction() { + const amount = this.amount(); + + return amount < 0; + } + + /** + * + * @param {string} + */ + public date() { + return this.options.firstUncategorizedTransaction?.date || null; + } + + /** + * Retrieves the formatted date of uncategorized transaction. + * @returns {string} + */ + public formattedDate() { + return this.formatDate(this.date()); + } + + /** + * + * @param {string} + */ + public referenceNo() { + return this.options.firstUncategorizedTransaction?.referenceNo || null; + } + + /** + * + * @returns {number} + */ + public creditAccountId() { + return ( + this.options.firstUncategorizedTransaction?.recognizedTransaction + ?.assignedAccountId || null + ); + } + + /** + * + * @returns {number} + */ + public debitAccountId() { + return this.options.firstUncategorizedTransaction?.accountId || null; + } + + /** + * + * @returns {string} + */ + public transactionType() { + const assignCategory = + this.options.firstUncategorizedTransaction?.recognizedTransaction + ?.assignCategory || null; + + return assignCategory || this.isDepositTransaction() + ? 'other_income' + : 'other_expense'; + } + + /** + * + * @returns {string} + */ + public payee() { + return ( + this.options.firstUncategorizedTransaction?.recognizedTransaction + ?.assignedPayee || null + ); + } + + /** + * + * @returns {string} + */ + public memo() { + return ( + this.options.firstUncategorizedTransaction?.recognizedTransaction + ?.assignedMemo || null + ); + } + + /** + * Retrieves the rule id the transaction recongized by. + * @returns {string} + */ + public recognizedByRuleId() { + return ( + this.options.firstUncategorizedTransaction?.recognizedTransaction + ?.bankRuleId || null + ); + } + + /** + * Retrieves the rule name the transaction recongized by. + * @returns {string} + */ + public recognizedByRuleName() { + return ( + this.options.firstUncategorizedTransaction?.recognizedTransaction + ?.bankRule?.name || null + ); + } +} diff --git a/packages/server/src/services/Banking/RegonizeTranasctions/RecognizeTranasctionsService.ts b/packages/server/src/services/Banking/RegonizeTranasctions/RecognizeTranasctionsService.ts index 5582652d8..f7e81d4f6 100644 --- a/packages/server/src/services/Banking/RegonizeTranasctions/RecognizeTranasctionsService.ts +++ b/packages/server/src/services/Banking/RegonizeTranasctions/RecognizeTranasctionsService.ts @@ -1,8 +1,8 @@ import { Knex } from 'knex'; +import { Inject, Service } from 'typedi'; import UncategorizedCashflowTransaction from '@/models/UncategorizedCashflowTransaction'; import HasTenancyService from '@/services/Tenancy/TenancyService'; import { transformToMapBy } from '@/utils'; -import { Inject, Service } from 'typedi'; import { PromisePool } from '@supercharge/promise-pool'; import { BankRule } from '@/models/BankRule'; import { bankRulesMatchTransaction } from './_utils'; diff --git a/packages/server/src/services/Cashflow/CategorizeCashflowTransaction.ts b/packages/server/src/services/Cashflow/CategorizeCashflowTransaction.ts index 3a4977f48..8a02556a3 100644 --- a/packages/server/src/services/Cashflow/CategorizeCashflowTransaction.ts +++ b/packages/server/src/services/Cashflow/CategorizeCashflowTransaction.ts @@ -58,14 +58,14 @@ export class CategorizeCashflowTransaction { // Validates the transaction shouldn't be categorized before. this.commandValidators.validateTransactionsShouldNotCategorized( - oldIncategorizedTransactions + oldUncategorizedTransactions ); // Validate the uncateogirzed transaction if it's deposit the transaction direction // should `IN` and the same thing if it's withdrawal the direction should be OUT. - // this.commandValidators.validateUncategorizeTransactionType( - // uncategorizedTransactions, - // categorizeDTO.transactionType - // ); + this.commandValidators.validateUncategorizeTransactionType( + oldUncategorizedTransactions, + categorizeDTO.transactionType + ); // Edits the cashflow transaction under UOW env. return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => { // Triggers `onTransactionCategorizing` event. @@ -88,6 +88,7 @@ export class CategorizeCashflowTransaction { tenantId, cashflowTransactionDTO ); + // Updates the uncategorized transaction as categorized. await UncategorizedCashflowTransaction.query(trx) .whereIn('id', uncategorizedTransactionIds) @@ -102,7 +103,6 @@ export class CategorizeCashflowTransaction { 'id', uncategorizedTransactionIds ); - // Triggers `onCashflowTransactionCategorized` event. await this.eventPublisher.emitAsync( events.cashflow.onTransactionCategorized, diff --git a/packages/server/src/services/Cashflow/CommandCasflowValidator.ts b/packages/server/src/services/Cashflow/CommandCasflowValidator.ts index cec6ac4ca..fc2871c28 100644 --- a/packages/server/src/services/Cashflow/CommandCasflowValidator.ts +++ b/packages/server/src/services/Cashflow/CommandCasflowValidator.ts @@ -1,5 +1,5 @@ import { Service } from 'typedi'; -import { includes, camelCase, upperFirst } from 'lodash'; +import { includes, camelCase, upperFirst, sumBy } from 'lodash'; import { IAccount, IUncategorizedCashflowTransaction } from '@/interfaces'; import { getCashflowTransactionType } from './utils'; import { ServiceError } from '@/exceptions'; @@ -74,7 +74,9 @@ export class CommandCashflowValidator { const categorized = cashflowTransactions.filter((t) => t.categorized); if (categorized?.length > 0) { - throw new ServiceError(ERRORS.TRANSACTION_ALREADY_CATEGORIZED); + throw new ServiceError(ERRORS.TRANSACTION_ALREADY_CATEGORIZED, '', { + ids: categorized.map((t) => t.id), + }); } } @@ -85,17 +87,19 @@ export class CommandCashflowValidator { * @throws {ServiceError(ERRORS.UNCATEGORIZED_TRANSACTION_TYPE_INVALID)} */ public validateUncategorizeTransactionType( - uncategorizeTransaction: IUncategorizedCashflowTransaction, + uncategorizeTransactions: Array, transactionType: string ) { + const amount = sumBy(uncategorizeTransactions, 'amount'); + const isDepositTransaction = amount > 0; + const isWithdrawalTransaction = amount <= 0; + const type = getCashflowTransactionType( transactionType as CASHFLOW_TRANSACTION_TYPE ); if ( - (type.direction === CASHFLOW_DIRECTION.IN && - uncategorizeTransaction.isDepositTransaction) || - (type.direction === CASHFLOW_DIRECTION.OUT && - uncategorizeTransaction.isWithdrawalTransaction) + (type.direction === CASHFLOW_DIRECTION.IN && isDepositTransaction) || + (type.direction === CASHFLOW_DIRECTION.OUT && isWithdrawalTransaction) ) { return; } diff --git a/packages/server/src/services/Cashflow/subscribers/DecrementUncategorizedTransactionOnCategorize.ts b/packages/server/src/services/Cashflow/subscribers/DecrementUncategorizedTransactionOnCategorize.ts index df68c36de..5dd8047fd 100644 --- a/packages/server/src/services/Cashflow/subscribers/DecrementUncategorizedTransactionOnCategorize.ts +++ b/packages/server/src/services/Cashflow/subscribers/DecrementUncategorizedTransactionOnCategorize.ts @@ -34,12 +34,13 @@ export class DecrementUncategorizedTransactionOnCategorize { */ public async decrementUnCategorizedTransactionsOnCategorized({ tenantId, - uncategorizedTransaction, + uncategorizedTransactions, }: ICashflowTransactionCategorizedPayload) { const { Account } = this.tenancy.models(tenantId); + const accountIds = uncategorizedTransactions.map((a) => a.id); await Account.query() - .findById(uncategorizedTransaction.accountId) + .whereIn('id', accountIds) .decrement('uncategorizedTransactions', 1); } diff --git a/packages/server/src/services/Cashflow/utils.ts b/packages/server/src/services/Cashflow/utils.ts index 2cc80c041..a47d0bca3 100644 --- a/packages/server/src/services/Cashflow/utils.ts +++ b/packages/server/src/services/Cashflow/utils.ts @@ -1,6 +1,5 @@ import { upperFirst, camelCase, first, sum, sumBy } from 'lodash'; import { - CASHFLOW_DIRECTION, CASHFLOW_TRANSACTION_TYPE, CASHFLOW_TRANSACTION_TYPE_META, ERRORS, @@ -81,6 +80,8 @@ export const validateUncategorizedTransactionsNotExcluded = ( const excluded = transactions.filter((tran) => tran.excluded); if (excluded?.length > 0) { - throw new ServiceError(ERRORS.CANNOT_CATEGORIZE_EXCLUDED_TRANSACTION); + throw new ServiceError(ERRORS.CANNOT_CATEGORIZE_EXCLUDED_TRANSACTION, '', { + ids: excluded.map((t) => t.id), + }); } }; diff --git a/packages/webapp/src/containers/CashFlow/AccountTransactions/UncategorizedTransactions/AccountTransactionsUncategorizedTable.tsx b/packages/webapp/src/containers/CashFlow/AccountTransactions/UncategorizedTransactions/AccountTransactionsUncategorizedTable.tsx index 715ec1296..d3c575898 100644 --- a/packages/webapp/src/containers/CashFlow/AccountTransactions/UncategorizedTransactions/AccountTransactionsUncategorizedTable.tsx +++ b/packages/webapp/src/containers/CashFlow/AccountTransactions/UncategorizedTransactions/AccountTransactionsUncategorizedTable.tsx @@ -36,10 +36,14 @@ function AccountTransactionsDataTable({ // #withBanking openMatchingTransactionAside, + enableMultipleCategorization, // #withBankingActions setUncategorizedTransactionIdForMatching, setUncategorizedTransactionsSelected, + + addTransactionsToCategorizeSelected, + setTransactionsToCategorizeSelected, }) { // Retrieve table columns. const columns = useAccountUncategorizedTransactionsColumns(); @@ -57,7 +61,11 @@ function AccountTransactionsDataTable({ // Handle cell click. const handleCellClick = (cell) => { - setUncategorizedTransactionIdForMatching(cell.row.original.id); + if (enableMultipleCategorization) { + addTransactionsToCategorizeSelected(cell.row.original.id); + } else { + setTransactionsToCategorizeSelected(cell.row.original.id); + } }; // Handles categorize button click. const handleCategorizeBtnClick = (transaction) => { @@ -80,12 +88,6 @@ function AccountTransactionsDataTable({ }); }; - // Handle selected rows change. - const handleSelectedRowsChange = (selected) => { - const _selectedIds = selected?.map((row) => row.original.id); - setUncategorizedTransactionsSelected(_selectedIds); - }; - return ( ); @@ -129,9 +130,12 @@ export default compose( cashflowTansactionsTableSize: cashflowTransactionsSettings?.tableSize, })), withBankingActions, - withBanking(({ openMatchingTransactionAside }) => ({ - openMatchingTransactionAside, - })), + withBanking( + ({ openMatchingTransactionAside, enableMultipleCategorization }) => ({ + openMatchingTransactionAside, + enableMultipleCategorization, + }), + ), )(AccountTransactionsDataTable); const DashboardConstrantTable = styled(DataTable)` diff --git a/packages/webapp/src/containers/CashFlow/AccountTransactions/UncategorizedTransactions/hooks.tsx b/packages/webapp/src/containers/CashFlow/AccountTransactions/UncategorizedTransactions/hooks.tsx index efe3168af..6bd645144 100644 --- a/packages/webapp/src/containers/CashFlow/AccountTransactions/UncategorizedTransactions/hooks.tsx +++ b/packages/webapp/src/containers/CashFlow/AccountTransactions/UncategorizedTransactions/hooks.tsx @@ -131,9 +131,9 @@ export function useAccountUncategorizedTransactionsColumns() { className={styles.categorizeCheckbox} /> ), - width: 10, - minWidth: 10, - maxWidth: 10, + width: 20, + minWidth: 20, + maxWidth: 20, align: 'right', className: 'categorize_include', }, diff --git a/packages/webapp/src/containers/CashFlow/CategorizeTransaction/drawers/CategorizeTransactionDrawer/CategorizeTransactionBoot.tsx b/packages/webapp/src/containers/CashFlow/CategorizeTransaction/drawers/CategorizeTransactionDrawer/CategorizeTransactionBoot.tsx index 00729183d..812a6681b 100644 --- a/packages/webapp/src/containers/CashFlow/CategorizeTransaction/drawers/CategorizeTransactionDrawer/CategorizeTransactionBoot.tsx +++ b/packages/webapp/src/containers/CashFlow/CategorizeTransaction/drawers/CategorizeTransactionDrawer/CategorizeTransactionBoot.tsx @@ -6,10 +6,13 @@ import { useAccounts, useBranches } from '@/hooks/query'; import { useFeatureCan } from '@/hooks/state'; import { Features } from '@/constants'; import { Spinner } from '@blueprintjs/core'; -import { useGetRecognizedBankTransaction } from '@/hooks/query/bank-rules'; -import { useCategorizeTransactionTabsBoot } from '@/containers/CashFlow/CategorizeTransactionAside/CategorizeTransactionTabsBoot'; +import { + GetAutofillCategorizeTransaction, + useGetAutofillCategorizeTransaction, +} from '@/hooks/query/bank-rules'; interface CategorizeTransactionBootProps { + uncategorizedTransactionsIds: Array; children: React.ReactNode; } @@ -19,8 +22,8 @@ interface CategorizeTransactionBootValue { isBranchesLoading: boolean; isAccountsLoading: boolean; primaryBranch: any; - recognizedTranasction: any; - isRecognizedTransactionLoading: boolean; + autofillCategorizeValues: null | GetAutofillCategorizeTransaction; + isAutofillCategorizeValuesLoading: boolean; } const CategorizeTransactionBootContext = @@ -32,11 +35,9 @@ const CategorizeTransactionBootContext = * Categorize transcation boot. */ function CategorizeTransactionBoot({ + uncategorizedTransactionsIds, ...props }: CategorizeTransactionBootProps) { - const { uncategorizedTransaction, uncategorizedTransactionId } = - useCategorizeTransactionTabsBoot(); - // Detarmines whether the feature is enabled. const { featureCan } = useFeatureCan(); const isBranchFeatureCan = featureCan(Features.Branches); @@ -49,13 +50,11 @@ function CategorizeTransactionBoot({ {}, { enabled: isBranchFeatureCan }, ); - // Fetches the recognized transaction. + // Fetches the autofill values of categorize transaction. const { - data: recognizedTranasction, - isLoading: isRecognizedTransactionLoading, - } = useGetRecognizedBankTransaction(uncategorizedTransactionId, { - enabled: !!uncategorizedTransaction.is_recognized, - }); + data: autofillCategorizeValues, + isLoading: isAutofillCategorizeValuesLoading, + } = useGetAutofillCategorizeTransaction(uncategorizedTransactionsIds, {}); // Retrieves the primary branch. const primaryBranch = useMemo( @@ -69,11 +68,11 @@ function CategorizeTransactionBoot({ isBranchesLoading, isAccountsLoading, primaryBranch, - recognizedTranasction, - isRecognizedTransactionLoading, + autofillCategorizeValues, + isAutofillCategorizeValuesLoading, }; const isLoading = - isBranchesLoading || isAccountsLoading || isRecognizedTransactionLoading; + isBranchesLoading || isAccountsLoading || isAutofillCategorizeValuesLoading; if (isLoading) { ; diff --git a/packages/webapp/src/containers/CashFlow/CategorizeTransaction/drawers/CategorizeTransactionDrawer/CategorizeTransactionContent.tsx b/packages/webapp/src/containers/CashFlow/CategorizeTransaction/drawers/CategorizeTransactionDrawer/CategorizeTransactionContent.tsx index a07004d2b..89513c6b8 100644 --- a/packages/webapp/src/containers/CashFlow/CategorizeTransaction/drawers/CategorizeTransactionDrawer/CategorizeTransactionContent.tsx +++ b/packages/webapp/src/containers/CashFlow/CategorizeTransaction/drawers/CategorizeTransactionDrawer/CategorizeTransactionContent.tsx @@ -1,15 +1,16 @@ // @ts-nocheck import styled from 'styled-components'; +import * as R from 'ramda'; import { CategorizeTransactionBoot } from './CategorizeTransactionBoot'; import { CategorizeTransactionForm } from './CategorizeTransactionForm'; -import { useCategorizeTransactionTabsBoot } from '@/containers/CashFlow/CategorizeTransactionAside/CategorizeTransactionTabsBoot'; - -export function CategorizeTransactionContent() { - const { uncategorizedTransactionId } = useCategorizeTransactionTabsBoot(); +import { withBanking } from '@/containers/CashFlow/withBanking'; +function CategorizeTransactionContentRoot({ + transactionsToCategorizeIdsSelected, +}) { return ( @@ -18,6 +19,12 @@ export function CategorizeTransactionContent() { ); } +export const CategorizeTransactionContent = R.compose( + withBanking(({ transactionsToCategorizeIdsSelected }) => ({ + transactionsToCategorizeIdsSelected, + })), +)(CategorizeTransactionContentRoot); + const CategorizeTransactionDrawerBody = styled.div` display: flex; flex-direction: column; diff --git a/packages/webapp/src/containers/CashFlow/CategorizeTransaction/drawers/CategorizeTransactionDrawer/CategorizeTransactionForm.tsx b/packages/webapp/src/containers/CashFlow/CategorizeTransaction/drawers/CategorizeTransactionDrawer/CategorizeTransactionForm.tsx index 9f328bd7d..b2fe5a59f 100644 --- a/packages/webapp/src/containers/CashFlow/CategorizeTransaction/drawers/CategorizeTransactionDrawer/CategorizeTransactionForm.tsx +++ b/packages/webapp/src/containers/CashFlow/CategorizeTransaction/drawers/CategorizeTransactionDrawer/CategorizeTransactionForm.tsx @@ -22,7 +22,7 @@ function CategorizeTransactionFormRoot({ // #withBankingActions closeMatchingTransactionAside, }) { - const { uncategorizedTransactionId } = useCategorizeTransactionTabsBoot(); + const { uncategorizedTransactionIds } = useCategorizeTransactionTabsBoot(); const { mutateAsync: categorizeTransaction } = useCategorizeTransaction(); // Form initial values in create and edit mode. @@ -30,10 +30,10 @@ function CategorizeTransactionFormRoot({ // Callbacks handles form submit. const handleFormSubmit = (values, { setSubmitting, setErrors }) => { - const transformedValues = tranformToRequest(values); + const _values = tranformToRequest(values, uncategorizedTransactionIds); setSubmitting(true); - categorizeTransaction([uncategorizedTransactionId, transformedValues]) + categorizeTransaction(_values) .then(() => { setSubmitting(false); diff --git a/packages/webapp/src/containers/CashFlow/CategorizeTransaction/drawers/CategorizeTransactionDrawer/CategorizeTransactionFormContent.tsx b/packages/webapp/src/containers/CashFlow/CategorizeTransaction/drawers/CategorizeTransactionDrawer/CategorizeTransactionFormContent.tsx index af3b10400..43322b5f7 100644 --- a/packages/webapp/src/containers/CashFlow/CategorizeTransaction/drawers/CategorizeTransactionDrawer/CategorizeTransactionFormContent.tsx +++ b/packages/webapp/src/containers/CashFlow/CategorizeTransaction/drawers/CategorizeTransactionDrawer/CategorizeTransactionFormContent.tsx @@ -6,6 +6,7 @@ import { Box, FFormGroup, FSelect } from '@/components'; import { getAddMoneyInOptions, getAddMoneyOutOptions } from '@/constants'; import { useFormikContext } from 'formik'; import { useCategorizeTransactionTabsBoot } from '@/containers/CashFlow/CategorizeTransactionAside/CategorizeTransactionTabsBoot'; +import { useCategorizeTransactionBoot } from './CategorizeTransactionBoot'; // Retrieves the add money in button options. const MoneyInOptions = getAddMoneyInOptions(); @@ -18,16 +19,18 @@ const Title = styled('h3')` `; export function CategorizeTransactionFormContent() { - const { uncategorizedTransaction } = useCategorizeTransactionTabsBoot(); + const { autofillCategorizeValues } = useCategorizeTransactionBoot(); - const transactionTypes = uncategorizedTransaction?.is_deposit_transaction + const transactionTypes = autofillCategorizeValues?.isDepositTransaction ? MoneyInOptions : MoneyOutOptions; + const formattedAmount = autofillCategorizeValues?.formattedAmount; + return ( - {uncategorizedTransaction.formatted_amount} + {formattedAmount} diff --git a/packages/webapp/src/containers/CashFlow/CategorizeTransaction/drawers/CategorizeTransactionDrawer/_utils.ts b/packages/webapp/src/containers/CashFlow/CategorizeTransaction/drawers/CategorizeTransactionDrawer/_utils.ts index 340ed16fd..93ebde247 100644 --- a/packages/webapp/src/containers/CashFlow/CategorizeTransaction/drawers/CategorizeTransactionDrawer/_utils.ts +++ b/packages/webapp/src/containers/CashFlow/CategorizeTransaction/drawers/CategorizeTransactionDrawer/_utils.ts @@ -1,8 +1,8 @@ // @ts-nocheck import * as R from 'ramda'; import { transformToForm, transfromToSnakeCase } from '@/utils'; -import { useCategorizeTransactionTabsBoot } from '@/containers/CashFlow/CategorizeTransactionAside/CategorizeTransactionTabsBoot'; import { useCategorizeTransactionBoot } from './CategorizeTransactionBoot'; +import { GetAutofillCategorizeTransaction } from '@/hooks/query/bank-rules'; // Default initial form values. export const defaultInitialValues = { @@ -18,48 +18,28 @@ export const defaultInitialValues = { }; export const transformToCategorizeForm = ( - uncategorizedTransaction: any, - recognizedTransaction?: any, + autofillCategorizeTransaction: GetAutofillCategorizeTransaction, ) => { - let defaultValues = { - debitAccountId: uncategorizedTransaction.account_id, - transactionType: uncategorizedTransaction.is_deposit_transaction - ? 'other_income' - : 'other_expense', - amount: uncategorizedTransaction.amount, - date: uncategorizedTransaction.date, - }; - if (recognizedTransaction) { - const recognizedDefaults = getRecognizedTransactionDefaultValues( - recognizedTransaction, - ); - defaultValues = R.merge(defaultValues, recognizedDefaults); - } - return transformToForm(defaultValues, defaultInitialValues); + return transformToForm(autofillCategorizeTransaction, defaultInitialValues); }; -export const getRecognizedTransactionDefaultValues = ( - recognizedTransaction: any, +export const tranformToRequest = ( + formValues: Record, + uncategorizedTransactionIds: Array, ) => { return { - creditAccountId: recognizedTransaction.assignedAccountId || '', - // transactionType: recognizedTransaction.assignCategory, - referenceNo: recognizedTransaction.referenceNo || '', + uncategorized_transaction_ids: uncategorizedTransactionIds, + ...transfromToSnakeCase(formValues), }; }; -export const tranformToRequest = (formValues: Record) => { - return transfromToSnakeCase(formValues); -}; - /** * Categorize transaction form initial values. * @returns */ export const useCategorizeTransactionFormInitialValues = () => { - const { primaryBranch, recognizedTranasction } = + const { primaryBranch, autofillCategorizeValues } = useCategorizeTransactionBoot(); - const { uncategorizedTransaction } = useCategorizeTransactionTabsBoot(); return { ...defaultInitialValues, @@ -68,10 +48,7 @@ export const useCategorizeTransactionFormInitialValues = () => { * values such as `notes` come back from the API as null, so remove those * as well. */ - ...transformToCategorizeForm( - uncategorizedTransaction, - recognizedTranasction, - ), + ...transformToCategorizeForm(autofillCategorizeValues), /** Assign the primary branch id as default value. */ branchId: primaryBranch?.id || null, diff --git a/packages/webapp/src/containers/CashFlow/CategorizeTransactionAside/CategorizeTransactionAside.tsx b/packages/webapp/src/containers/CashFlow/CategorizeTransactionAside/CategorizeTransactionAside.tsx index 9945ffe04..6c0741ff7 100644 --- a/packages/webapp/src/containers/CashFlow/CategorizeTransactionAside/CategorizeTransactionAside.tsx +++ b/packages/webapp/src/containers/CashFlow/CategorizeTransactionAside/CategorizeTransactionAside.tsx @@ -43,9 +43,8 @@ function CategorizeTransactionAsideRoot({ const handleClose = () => { closeMatchingTransactionAside(); - }; - const uncategorizedTransactionId = selectedUncategorizedTransactionId; - + } + // Cannot continue if there is no selected transactions.; if (!selectedUncategorizedTransactionId) { return null; } @@ -53,7 +52,7 @@ function CategorizeTransactionAsideRoot({