mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-21 15:20:34 +00:00
feat: wip multi-select transactions to categorization and matching
This commit is contained in:
@@ -6,6 +6,7 @@ import { BankingRulesController } from './BankingRulesController';
|
|||||||
import { BankTransactionsMatchingController } from './BankTransactionsMatchingController';
|
import { BankTransactionsMatchingController } from './BankTransactionsMatchingController';
|
||||||
import { RecognizedTransactionsController } from './RecognizedTransactionsController';
|
import { RecognizedTransactionsController } from './RecognizedTransactionsController';
|
||||||
import { BankAccountsController } from './BankAccountsController';
|
import { BankAccountsController } from './BankAccountsController';
|
||||||
|
import { BankingUncategorizedController } from './BankingUncategorizedController';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class BankingController extends BaseController {
|
export class BankingController extends BaseController {
|
||||||
@@ -29,6 +30,10 @@ export class BankingController extends BaseController {
|
|||||||
'/bank_accounts',
|
'/bank_accounts',
|
||||||
Container.get(BankAccountsController).router()
|
Container.get(BankAccountsController).router()
|
||||||
);
|
);
|
||||||
|
router.use(
|
||||||
|
'/categorize',
|
||||||
|
Container.get(BankingUncategorizedController).router()
|
||||||
|
);
|
||||||
return router;
|
return router;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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<Response | null>}
|
||||||
|
*/
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -201,6 +201,7 @@ export default class NewCashflowTransactionController extends BaseController {
|
|||||||
const categorizeDTO = omit(matchedObject, [
|
const categorizeDTO = omit(matchedObject, [
|
||||||
'uncategorizedTransactionIds',
|
'uncategorizedTransactionIds',
|
||||||
]) as ICategorizeCashflowTransactioDTO;
|
]) as ICategorizeCashflowTransactioDTO;
|
||||||
|
|
||||||
const uncategorizedTransactionIds =
|
const uncategorizedTransactionIds =
|
||||||
matchedObject.uncategorizedTransactionIds;
|
matchedObject.uncategorizedTransactionIds;
|
||||||
|
|
||||||
|
|||||||
@@ -64,6 +64,8 @@ export class GetMatchedTransactions {
|
|||||||
.whereIn('id', uncategorizedTransactionIds)
|
.whereIn('id', uncategorizedTransactionIds)
|
||||||
.throwIfNotFound();
|
.throwIfNotFound();
|
||||||
|
|
||||||
|
const totalPending = Math.abs(sumBy(uncategorizedTransactions, 'amount'));
|
||||||
|
|
||||||
const filtered = filter.transactionType
|
const filtered = filter.transactionType
|
||||||
? this.registered.filter((item) => item.type === filter.transactionType)
|
? this.registered.filter((item) => item.type === filter.transactionType)
|
||||||
: this.registered;
|
: this.registered;
|
||||||
@@ -80,6 +82,7 @@ export class GetMatchedTransactions {
|
|||||||
return {
|
return {
|
||||||
perfectMatches,
|
perfectMatches,
|
||||||
possibleMatches,
|
possibleMatches,
|
||||||
|
totalPending,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -101,7 +101,7 @@ export class MatchBankTransactions {
|
|||||||
);
|
);
|
||||||
// Validates the total given matching transcations whether is not equal
|
// Validates the total given matching transcations whether is not equal
|
||||||
// uncategorized transaction amount.
|
// uncategorized transaction amount.
|
||||||
if (totalUncategorizedTransactions === totalMatchedTranasctions) {
|
if (totalUncategorizedTransactions !== totalMatchedTranasctions) {
|
||||||
throw new ServiceError(ERRORS.TOTAL_MATCHING_TRANSACTIONS_INVALID);
|
throw new ServiceError(ERRORS.TOTAL_MATCHING_TRANSACTIONS_INVALID);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ export interface MatchedTransactionPOJO {
|
|||||||
export type MatchedTransactionsPOJO = {
|
export type MatchedTransactionsPOJO = {
|
||||||
perfectMatches: Array<MatchedTransactionPOJO>;
|
perfectMatches: Array<MatchedTransactionPOJO>;
|
||||||
possibleMatches: Array<MatchedTransactionPOJO>;
|
possibleMatches: Array<MatchedTransactionPOJO>;
|
||||||
|
totalPending: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ERRORS = {
|
export const ERRORS = {
|
||||||
|
|||||||
@@ -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> | number} uncategorizeTransactionsId - Uncategorized transactions ids.
|
||||||
|
*/
|
||||||
|
public async getAutofillCategorizeTransaction(
|
||||||
|
tenantId: number,
|
||||||
|
uncategorizeTransactionsId: Array<number> | 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),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
import { Knex } from 'knex';
|
import { Knex } from 'knex';
|
||||||
|
import { Inject, Service } from 'typedi';
|
||||||
import UncategorizedCashflowTransaction from '@/models/UncategorizedCashflowTransaction';
|
import UncategorizedCashflowTransaction from '@/models/UncategorizedCashflowTransaction';
|
||||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||||
import { transformToMapBy } from '@/utils';
|
import { transformToMapBy } from '@/utils';
|
||||||
import { Inject, Service } from 'typedi';
|
|
||||||
import { PromisePool } from '@supercharge/promise-pool';
|
import { PromisePool } from '@supercharge/promise-pool';
|
||||||
import { BankRule } from '@/models/BankRule';
|
import { BankRule } from '@/models/BankRule';
|
||||||
import { bankRulesMatchTransaction } from './_utils';
|
import { bankRulesMatchTransaction } from './_utils';
|
||||||
|
|||||||
@@ -58,14 +58,14 @@ export class CategorizeCashflowTransaction {
|
|||||||
|
|
||||||
// Validates the transaction shouldn't be categorized before.
|
// Validates the transaction shouldn't be categorized before.
|
||||||
this.commandValidators.validateTransactionsShouldNotCategorized(
|
this.commandValidators.validateTransactionsShouldNotCategorized(
|
||||||
oldIncategorizedTransactions
|
oldUncategorizedTransactions
|
||||||
);
|
);
|
||||||
// Validate the uncateogirzed transaction if it's deposit the transaction direction
|
// 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.
|
// should `IN` and the same thing if it's withdrawal the direction should be OUT.
|
||||||
// this.commandValidators.validateUncategorizeTransactionType(
|
this.commandValidators.validateUncategorizeTransactionType(
|
||||||
// uncategorizedTransactions,
|
oldUncategorizedTransactions,
|
||||||
// categorizeDTO.transactionType
|
categorizeDTO.transactionType
|
||||||
// );
|
);
|
||||||
// Edits the cashflow transaction under UOW env.
|
// Edits the cashflow transaction under UOW env.
|
||||||
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
|
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
|
||||||
// Triggers `onTransactionCategorizing` event.
|
// Triggers `onTransactionCategorizing` event.
|
||||||
@@ -88,6 +88,7 @@ export class CategorizeCashflowTransaction {
|
|||||||
tenantId,
|
tenantId,
|
||||||
cashflowTransactionDTO
|
cashflowTransactionDTO
|
||||||
);
|
);
|
||||||
|
|
||||||
// Updates the uncategorized transaction as categorized.
|
// Updates the uncategorized transaction as categorized.
|
||||||
await UncategorizedCashflowTransaction.query(trx)
|
await UncategorizedCashflowTransaction.query(trx)
|
||||||
.whereIn('id', uncategorizedTransactionIds)
|
.whereIn('id', uncategorizedTransactionIds)
|
||||||
@@ -102,7 +103,6 @@ export class CategorizeCashflowTransaction {
|
|||||||
'id',
|
'id',
|
||||||
uncategorizedTransactionIds
|
uncategorizedTransactionIds
|
||||||
);
|
);
|
||||||
|
|
||||||
// Triggers `onCashflowTransactionCategorized` event.
|
// Triggers `onCashflowTransactionCategorized` event.
|
||||||
await this.eventPublisher.emitAsync(
|
await this.eventPublisher.emitAsync(
|
||||||
events.cashflow.onTransactionCategorized,
|
events.cashflow.onTransactionCategorized,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Service } from 'typedi';
|
import { Service } from 'typedi';
|
||||||
import { includes, camelCase, upperFirst } from 'lodash';
|
import { includes, camelCase, upperFirst, sumBy } from 'lodash';
|
||||||
import { IAccount, IUncategorizedCashflowTransaction } from '@/interfaces';
|
import { IAccount, IUncategorizedCashflowTransaction } from '@/interfaces';
|
||||||
import { getCashflowTransactionType } from './utils';
|
import { getCashflowTransactionType } from './utils';
|
||||||
import { ServiceError } from '@/exceptions';
|
import { ServiceError } from '@/exceptions';
|
||||||
@@ -74,7 +74,9 @@ export class CommandCashflowValidator {
|
|||||||
const categorized = cashflowTransactions.filter((t) => t.categorized);
|
const categorized = cashflowTransactions.filter((t) => t.categorized);
|
||||||
|
|
||||||
if (categorized?.length > 0) {
|
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)}
|
* @throws {ServiceError(ERRORS.UNCATEGORIZED_TRANSACTION_TYPE_INVALID)}
|
||||||
*/
|
*/
|
||||||
public validateUncategorizeTransactionType(
|
public validateUncategorizeTransactionType(
|
||||||
uncategorizeTransaction: IUncategorizedCashflowTransaction,
|
uncategorizeTransactions: Array<IUncategorizedCashflowTransaction>,
|
||||||
transactionType: string
|
transactionType: string
|
||||||
) {
|
) {
|
||||||
|
const amount = sumBy(uncategorizeTransactions, 'amount');
|
||||||
|
const isDepositTransaction = amount > 0;
|
||||||
|
const isWithdrawalTransaction = amount <= 0;
|
||||||
|
|
||||||
const type = getCashflowTransactionType(
|
const type = getCashflowTransactionType(
|
||||||
transactionType as CASHFLOW_TRANSACTION_TYPE
|
transactionType as CASHFLOW_TRANSACTION_TYPE
|
||||||
);
|
);
|
||||||
if (
|
if (
|
||||||
(type.direction === CASHFLOW_DIRECTION.IN &&
|
(type.direction === CASHFLOW_DIRECTION.IN && isDepositTransaction) ||
|
||||||
uncategorizeTransaction.isDepositTransaction) ||
|
(type.direction === CASHFLOW_DIRECTION.OUT && isWithdrawalTransaction)
|
||||||
(type.direction === CASHFLOW_DIRECTION.OUT &&
|
|
||||||
uncategorizeTransaction.isWithdrawalTransaction)
|
|
||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,12 +34,13 @@ export class DecrementUncategorizedTransactionOnCategorize {
|
|||||||
*/
|
*/
|
||||||
public async decrementUnCategorizedTransactionsOnCategorized({
|
public async decrementUnCategorizedTransactionsOnCategorized({
|
||||||
tenantId,
|
tenantId,
|
||||||
uncategorizedTransaction,
|
uncategorizedTransactions,
|
||||||
}: ICashflowTransactionCategorizedPayload) {
|
}: ICashflowTransactionCategorizedPayload) {
|
||||||
const { Account } = this.tenancy.models(tenantId);
|
const { Account } = this.tenancy.models(tenantId);
|
||||||
|
const accountIds = uncategorizedTransactions.map((a) => a.id);
|
||||||
|
|
||||||
await Account.query()
|
await Account.query()
|
||||||
.findById(uncategorizedTransaction.accountId)
|
.whereIn('id', accountIds)
|
||||||
.decrement('uncategorizedTransactions', 1);
|
.decrement('uncategorizedTransactions', 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { upperFirst, camelCase, first, sum, sumBy } from 'lodash';
|
import { upperFirst, camelCase, first, sum, sumBy } from 'lodash';
|
||||||
import {
|
import {
|
||||||
CASHFLOW_DIRECTION,
|
|
||||||
CASHFLOW_TRANSACTION_TYPE,
|
CASHFLOW_TRANSACTION_TYPE,
|
||||||
CASHFLOW_TRANSACTION_TYPE_META,
|
CASHFLOW_TRANSACTION_TYPE_META,
|
||||||
ERRORS,
|
ERRORS,
|
||||||
@@ -81,6 +80,8 @@ export const validateUncategorizedTransactionsNotExcluded = (
|
|||||||
const excluded = transactions.filter((tran) => tran.excluded);
|
const excluded = transactions.filter((tran) => tran.excluded);
|
||||||
|
|
||||||
if (excluded?.length > 0) {
|
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),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -36,10 +36,14 @@ function AccountTransactionsDataTable({
|
|||||||
|
|
||||||
// #withBanking
|
// #withBanking
|
||||||
openMatchingTransactionAside,
|
openMatchingTransactionAside,
|
||||||
|
enableMultipleCategorization,
|
||||||
|
|
||||||
// #withBankingActions
|
// #withBankingActions
|
||||||
setUncategorizedTransactionIdForMatching,
|
setUncategorizedTransactionIdForMatching,
|
||||||
setUncategorizedTransactionsSelected,
|
setUncategorizedTransactionsSelected,
|
||||||
|
|
||||||
|
addTransactionsToCategorizeSelected,
|
||||||
|
setTransactionsToCategorizeSelected,
|
||||||
}) {
|
}) {
|
||||||
// Retrieve table columns.
|
// Retrieve table columns.
|
||||||
const columns = useAccountUncategorizedTransactionsColumns();
|
const columns = useAccountUncategorizedTransactionsColumns();
|
||||||
@@ -57,7 +61,11 @@ function AccountTransactionsDataTable({
|
|||||||
|
|
||||||
// Handle cell click.
|
// Handle cell click.
|
||||||
const handleCellClick = (cell) => {
|
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.
|
// Handles categorize button click.
|
||||||
const handleCategorizeBtnClick = (transaction) => {
|
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 (
|
return (
|
||||||
<CashflowTransactionsTable
|
<CashflowTransactionsTable
|
||||||
noInitialFetch={true}
|
noInitialFetch={true}
|
||||||
@@ -112,13 +114,12 @@ function AccountTransactionsDataTable({
|
|||||||
noResults={
|
noResults={
|
||||||
'There is no uncategorized transactions in the current account.'
|
'There is no uncategorized transactions in the current account.'
|
||||||
}
|
}
|
||||||
onSelectedRowsChange={handleSelectedRowsChange}
|
|
||||||
payload={{
|
payload={{
|
||||||
onExclude: handleExcludeTransaction,
|
onExclude: handleExcludeTransaction,
|
||||||
onCategorize: handleCategorizeBtnClick,
|
onCategorize: handleCategorizeBtnClick,
|
||||||
}}
|
}}
|
||||||
className={clsx('table-constrant', styles.table, {
|
className={clsx('table-constrant', styles.table, {
|
||||||
[styles.showCategorizeColumn]: openMatchingTransactionAside,
|
[styles.showCategorizeColumn]: enableMultipleCategorization,
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -129,9 +130,12 @@ export default compose(
|
|||||||
cashflowTansactionsTableSize: cashflowTransactionsSettings?.tableSize,
|
cashflowTansactionsTableSize: cashflowTransactionsSettings?.tableSize,
|
||||||
})),
|
})),
|
||||||
withBankingActions,
|
withBankingActions,
|
||||||
withBanking(({ openMatchingTransactionAside }) => ({
|
withBanking(
|
||||||
openMatchingTransactionAside,
|
({ openMatchingTransactionAside, enableMultipleCategorization }) => ({
|
||||||
})),
|
openMatchingTransactionAside,
|
||||||
|
enableMultipleCategorization,
|
||||||
|
}),
|
||||||
|
),
|
||||||
)(AccountTransactionsDataTable);
|
)(AccountTransactionsDataTable);
|
||||||
|
|
||||||
const DashboardConstrantTable = styled(DataTable)`
|
const DashboardConstrantTable = styled(DataTable)`
|
||||||
|
|||||||
@@ -131,9 +131,9 @@ export function useAccountUncategorizedTransactionsColumns() {
|
|||||||
className={styles.categorizeCheckbox}
|
className={styles.categorizeCheckbox}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
width: 10,
|
width: 20,
|
||||||
minWidth: 10,
|
minWidth: 20,
|
||||||
maxWidth: 10,
|
maxWidth: 20,
|
||||||
align: 'right',
|
align: 'right',
|
||||||
className: 'categorize_include',
|
className: 'categorize_include',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -6,10 +6,13 @@ import { useAccounts, useBranches } from '@/hooks/query';
|
|||||||
import { useFeatureCan } from '@/hooks/state';
|
import { useFeatureCan } from '@/hooks/state';
|
||||||
import { Features } from '@/constants';
|
import { Features } from '@/constants';
|
||||||
import { Spinner } from '@blueprintjs/core';
|
import { Spinner } from '@blueprintjs/core';
|
||||||
import { useGetRecognizedBankTransaction } from '@/hooks/query/bank-rules';
|
import {
|
||||||
import { useCategorizeTransactionTabsBoot } from '@/containers/CashFlow/CategorizeTransactionAside/CategorizeTransactionTabsBoot';
|
GetAutofillCategorizeTransaction,
|
||||||
|
useGetAutofillCategorizeTransaction,
|
||||||
|
} from '@/hooks/query/bank-rules';
|
||||||
|
|
||||||
interface CategorizeTransactionBootProps {
|
interface CategorizeTransactionBootProps {
|
||||||
|
uncategorizedTransactionsIds: Array<number>;
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -19,8 +22,8 @@ interface CategorizeTransactionBootValue {
|
|||||||
isBranchesLoading: boolean;
|
isBranchesLoading: boolean;
|
||||||
isAccountsLoading: boolean;
|
isAccountsLoading: boolean;
|
||||||
primaryBranch: any;
|
primaryBranch: any;
|
||||||
recognizedTranasction: any;
|
autofillCategorizeValues: null | GetAutofillCategorizeTransaction;
|
||||||
isRecognizedTransactionLoading: boolean;
|
isAutofillCategorizeValuesLoading: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const CategorizeTransactionBootContext =
|
const CategorizeTransactionBootContext =
|
||||||
@@ -32,11 +35,9 @@ const CategorizeTransactionBootContext =
|
|||||||
* Categorize transcation boot.
|
* Categorize transcation boot.
|
||||||
*/
|
*/
|
||||||
function CategorizeTransactionBoot({
|
function CategorizeTransactionBoot({
|
||||||
|
uncategorizedTransactionsIds,
|
||||||
...props
|
...props
|
||||||
}: CategorizeTransactionBootProps) {
|
}: CategorizeTransactionBootProps) {
|
||||||
const { uncategorizedTransaction, uncategorizedTransactionId } =
|
|
||||||
useCategorizeTransactionTabsBoot();
|
|
||||||
|
|
||||||
// Detarmines whether the feature is enabled.
|
// Detarmines whether the feature is enabled.
|
||||||
const { featureCan } = useFeatureCan();
|
const { featureCan } = useFeatureCan();
|
||||||
const isBranchFeatureCan = featureCan(Features.Branches);
|
const isBranchFeatureCan = featureCan(Features.Branches);
|
||||||
@@ -49,13 +50,11 @@ function CategorizeTransactionBoot({
|
|||||||
{},
|
{},
|
||||||
{ enabled: isBranchFeatureCan },
|
{ enabled: isBranchFeatureCan },
|
||||||
);
|
);
|
||||||
// Fetches the recognized transaction.
|
// Fetches the autofill values of categorize transaction.
|
||||||
const {
|
const {
|
||||||
data: recognizedTranasction,
|
data: autofillCategorizeValues,
|
||||||
isLoading: isRecognizedTransactionLoading,
|
isLoading: isAutofillCategorizeValuesLoading,
|
||||||
} = useGetRecognizedBankTransaction(uncategorizedTransactionId, {
|
} = useGetAutofillCategorizeTransaction(uncategorizedTransactionsIds, {});
|
||||||
enabled: !!uncategorizedTransaction.is_recognized,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Retrieves the primary branch.
|
// Retrieves the primary branch.
|
||||||
const primaryBranch = useMemo(
|
const primaryBranch = useMemo(
|
||||||
@@ -69,11 +68,11 @@ function CategorizeTransactionBoot({
|
|||||||
isBranchesLoading,
|
isBranchesLoading,
|
||||||
isAccountsLoading,
|
isAccountsLoading,
|
||||||
primaryBranch,
|
primaryBranch,
|
||||||
recognizedTranasction,
|
autofillCategorizeValues,
|
||||||
isRecognizedTransactionLoading,
|
isAutofillCategorizeValuesLoading,
|
||||||
};
|
};
|
||||||
const isLoading =
|
const isLoading =
|
||||||
isBranchesLoading || isAccountsLoading || isRecognizedTransactionLoading;
|
isBranchesLoading || isAccountsLoading || isAutofillCategorizeValuesLoading;
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
<Spinner size={30} />;
|
<Spinner size={30} />;
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
// @ts-nocheck
|
// @ts-nocheck
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
import * as R from 'ramda';
|
||||||
import { CategorizeTransactionBoot } from './CategorizeTransactionBoot';
|
import { CategorizeTransactionBoot } from './CategorizeTransactionBoot';
|
||||||
import { CategorizeTransactionForm } from './CategorizeTransactionForm';
|
import { CategorizeTransactionForm } from './CategorizeTransactionForm';
|
||||||
import { useCategorizeTransactionTabsBoot } from '@/containers/CashFlow/CategorizeTransactionAside/CategorizeTransactionTabsBoot';
|
import { withBanking } from '@/containers/CashFlow/withBanking';
|
||||||
|
|
||||||
export function CategorizeTransactionContent() {
|
|
||||||
const { uncategorizedTransactionId } = useCategorizeTransactionTabsBoot();
|
|
||||||
|
|
||||||
|
function CategorizeTransactionContentRoot({
|
||||||
|
transactionsToCategorizeIdsSelected,
|
||||||
|
}) {
|
||||||
return (
|
return (
|
||||||
<CategorizeTransactionBoot
|
<CategorizeTransactionBoot
|
||||||
uncategorizedTransactionId={uncategorizedTransactionId}
|
uncategorizedTransactionsIds={transactionsToCategorizeIdsSelected}
|
||||||
>
|
>
|
||||||
<CategorizeTransactionDrawerBody>
|
<CategorizeTransactionDrawerBody>
|
||||||
<CategorizeTransactionForm />
|
<CategorizeTransactionForm />
|
||||||
@@ -18,6 +19,12 @@ export function CategorizeTransactionContent() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const CategorizeTransactionContent = R.compose(
|
||||||
|
withBanking(({ transactionsToCategorizeIdsSelected }) => ({
|
||||||
|
transactionsToCategorizeIdsSelected,
|
||||||
|
})),
|
||||||
|
)(CategorizeTransactionContentRoot);
|
||||||
|
|
||||||
const CategorizeTransactionDrawerBody = styled.div`
|
const CategorizeTransactionDrawerBody = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ function CategorizeTransactionFormRoot({
|
|||||||
// #withBankingActions
|
// #withBankingActions
|
||||||
closeMatchingTransactionAside,
|
closeMatchingTransactionAside,
|
||||||
}) {
|
}) {
|
||||||
const { uncategorizedTransactionId } = useCategorizeTransactionTabsBoot();
|
const { uncategorizedTransactionIds } = useCategorizeTransactionTabsBoot();
|
||||||
const { mutateAsync: categorizeTransaction } = useCategorizeTransaction();
|
const { mutateAsync: categorizeTransaction } = useCategorizeTransaction();
|
||||||
|
|
||||||
// Form initial values in create and edit mode.
|
// Form initial values in create and edit mode.
|
||||||
@@ -30,10 +30,10 @@ function CategorizeTransactionFormRoot({
|
|||||||
|
|
||||||
// Callbacks handles form submit.
|
// Callbacks handles form submit.
|
||||||
const handleFormSubmit = (values, { setSubmitting, setErrors }) => {
|
const handleFormSubmit = (values, { setSubmitting, setErrors }) => {
|
||||||
const transformedValues = tranformToRequest(values);
|
const _values = tranformToRequest(values, uncategorizedTransactionIds);
|
||||||
|
|
||||||
setSubmitting(true);
|
setSubmitting(true);
|
||||||
categorizeTransaction([uncategorizedTransactionId, transformedValues])
|
categorizeTransaction(_values)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
setSubmitting(false);
|
setSubmitting(false);
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { Box, FFormGroup, FSelect } from '@/components';
|
|||||||
import { getAddMoneyInOptions, getAddMoneyOutOptions } from '@/constants';
|
import { getAddMoneyInOptions, getAddMoneyOutOptions } from '@/constants';
|
||||||
import { useFormikContext } from 'formik';
|
import { useFormikContext } from 'formik';
|
||||||
import { useCategorizeTransactionTabsBoot } from '@/containers/CashFlow/CategorizeTransactionAside/CategorizeTransactionTabsBoot';
|
import { useCategorizeTransactionTabsBoot } from '@/containers/CashFlow/CategorizeTransactionAside/CategorizeTransactionTabsBoot';
|
||||||
|
import { useCategorizeTransactionBoot } from './CategorizeTransactionBoot';
|
||||||
|
|
||||||
// Retrieves the add money in button options.
|
// Retrieves the add money in button options.
|
||||||
const MoneyInOptions = getAddMoneyInOptions();
|
const MoneyInOptions = getAddMoneyInOptions();
|
||||||
@@ -18,16 +19,18 @@ const Title = styled('h3')`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
export function CategorizeTransactionFormContent() {
|
export function CategorizeTransactionFormContent() {
|
||||||
const { uncategorizedTransaction } = useCategorizeTransactionTabsBoot();
|
const { autofillCategorizeValues } = useCategorizeTransactionBoot();
|
||||||
|
|
||||||
const transactionTypes = uncategorizedTransaction?.is_deposit_transaction
|
const transactionTypes = autofillCategorizeValues?.isDepositTransaction
|
||||||
? MoneyInOptions
|
? MoneyInOptions
|
||||||
: MoneyOutOptions;
|
: MoneyOutOptions;
|
||||||
|
|
||||||
|
const formattedAmount = autofillCategorizeValues?.formattedAmount;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box style={{ flex: 1, margin: 20 }}>
|
<Box style={{ flex: 1, margin: 20 }}>
|
||||||
<FormGroup label={'Amount'} inline>
|
<FormGroup label={'Amount'} inline>
|
||||||
<Title>{uncategorizedTransaction.formatted_amount}</Title>
|
<Title>{formattedAmount}</Title>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
|
||||||
<FFormGroup name={'category'} label={'Category'} fastField inline>
|
<FFormGroup name={'category'} label={'Category'} fastField inline>
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
// @ts-nocheck
|
// @ts-nocheck
|
||||||
import * as R from 'ramda';
|
import * as R from 'ramda';
|
||||||
import { transformToForm, transfromToSnakeCase } from '@/utils';
|
import { transformToForm, transfromToSnakeCase } from '@/utils';
|
||||||
import { useCategorizeTransactionTabsBoot } from '@/containers/CashFlow/CategorizeTransactionAside/CategorizeTransactionTabsBoot';
|
|
||||||
import { useCategorizeTransactionBoot } from './CategorizeTransactionBoot';
|
import { useCategorizeTransactionBoot } from './CategorizeTransactionBoot';
|
||||||
|
import { GetAutofillCategorizeTransaction } from '@/hooks/query/bank-rules';
|
||||||
|
|
||||||
// Default initial form values.
|
// Default initial form values.
|
||||||
export const defaultInitialValues = {
|
export const defaultInitialValues = {
|
||||||
@@ -18,48 +18,28 @@ export const defaultInitialValues = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const transformToCategorizeForm = (
|
export const transformToCategorizeForm = (
|
||||||
uncategorizedTransaction: any,
|
autofillCategorizeTransaction: GetAutofillCategorizeTransaction,
|
||||||
recognizedTransaction?: any,
|
|
||||||
) => {
|
) => {
|
||||||
let defaultValues = {
|
return transformToForm(autofillCategorizeTransaction, defaultInitialValues);
|
||||||
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);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getRecognizedTransactionDefaultValues = (
|
export const tranformToRequest = (
|
||||||
recognizedTransaction: any,
|
formValues: Record<string, any>,
|
||||||
|
uncategorizedTransactionIds: Array<number>,
|
||||||
) => {
|
) => {
|
||||||
return {
|
return {
|
||||||
creditAccountId: recognizedTransaction.assignedAccountId || '',
|
uncategorized_transaction_ids: uncategorizedTransactionIds,
|
||||||
// transactionType: recognizedTransaction.assignCategory,
|
...transfromToSnakeCase(formValues),
|
||||||
referenceNo: recognizedTransaction.referenceNo || '',
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const tranformToRequest = (formValues: Record<string, any>) => {
|
|
||||||
return transfromToSnakeCase(formValues);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Categorize transaction form initial values.
|
* Categorize transaction form initial values.
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const useCategorizeTransactionFormInitialValues = () => {
|
export const useCategorizeTransactionFormInitialValues = () => {
|
||||||
const { primaryBranch, recognizedTranasction } =
|
const { primaryBranch, autofillCategorizeValues } =
|
||||||
useCategorizeTransactionBoot();
|
useCategorizeTransactionBoot();
|
||||||
const { uncategorizedTransaction } = useCategorizeTransactionTabsBoot();
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...defaultInitialValues,
|
...defaultInitialValues,
|
||||||
@@ -68,10 +48,7 @@ export const useCategorizeTransactionFormInitialValues = () => {
|
|||||||
* values such as `notes` come back from the API as null, so remove those
|
* values such as `notes` come back from the API as null, so remove those
|
||||||
* as well.
|
* as well.
|
||||||
*/
|
*/
|
||||||
...transformToCategorizeForm(
|
...transformToCategorizeForm(autofillCategorizeValues),
|
||||||
uncategorizedTransaction,
|
|
||||||
recognizedTranasction,
|
|
||||||
),
|
|
||||||
|
|
||||||
/** Assign the primary branch id as default value. */
|
/** Assign the primary branch id as default value. */
|
||||||
branchId: primaryBranch?.id || null,
|
branchId: primaryBranch?.id || null,
|
||||||
|
|||||||
@@ -43,9 +43,8 @@ function CategorizeTransactionAsideRoot({
|
|||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
closeMatchingTransactionAside();
|
closeMatchingTransactionAside();
|
||||||
};
|
}
|
||||||
const uncategorizedTransactionId = selectedUncategorizedTransactionId;
|
// Cannot continue if there is no selected transactions.;
|
||||||
|
|
||||||
if (!selectedUncategorizedTransactionId) {
|
if (!selectedUncategorizedTransactionId) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -53,7 +52,7 @@ function CategorizeTransactionAsideRoot({
|
|||||||
<Aside title={'Categorize Bank Transaction'} onClose={handleClose}>
|
<Aside title={'Categorize Bank Transaction'} onClose={handleClose}>
|
||||||
<Aside.Body>
|
<Aside.Body>
|
||||||
<CategorizeTransactionTabsBoot
|
<CategorizeTransactionTabsBoot
|
||||||
uncategorizedTransactionId={uncategorizedTransactionId}
|
uncategorizedTransactionId={selectedUncategorizedTransactionId}
|
||||||
>
|
>
|
||||||
<CategorizeTransactionTabs />
|
<CategorizeTransactionTabs />
|
||||||
</CategorizeTransactionTabsBoot>
|
</CategorizeTransactionTabsBoot>
|
||||||
@@ -64,7 +63,7 @@ function CategorizeTransactionAsideRoot({
|
|||||||
|
|
||||||
export const CategorizeTransactionAside = R.compose(
|
export const CategorizeTransactionAside = R.compose(
|
||||||
withBankingActions,
|
withBankingActions,
|
||||||
withBanking(({ selectedUncategorizedTransactionId }) => ({
|
withBanking(({ transactionsToCategorizeIdsSelected }) => ({
|
||||||
selectedUncategorizedTransactionId,
|
selectedUncategorizedTransactionId: transactionsToCategorizeIdsSelected,
|
||||||
})),
|
})),
|
||||||
)(CategorizeTransactionAsideRoot);
|
)(CategorizeTransactionAsideRoot);
|
||||||
|
|||||||
@@ -2,14 +2,10 @@
|
|||||||
import { Tab, Tabs } from '@blueprintjs/core';
|
import { Tab, Tabs } from '@blueprintjs/core';
|
||||||
import { MatchingBankTransaction } from './MatchingTransaction';
|
import { MatchingBankTransaction } from './MatchingTransaction';
|
||||||
import { CategorizeTransactionContent } from '../CategorizeTransaction/drawers/CategorizeTransactionDrawer/CategorizeTransactionContent';
|
import { CategorizeTransactionContent } from '../CategorizeTransaction/drawers/CategorizeTransactionDrawer/CategorizeTransactionContent';
|
||||||
import { useCategorizeTransactionTabsBoot } from './CategorizeTransactionTabsBoot';
|
|
||||||
import styles from './CategorizeTransactionTabs.module.scss';
|
import styles from './CategorizeTransactionTabs.module.scss';
|
||||||
|
|
||||||
export function CategorizeTransactionTabs() {
|
export function CategorizeTransactionTabs() {
|
||||||
const { uncategorizedTransaction } = useCategorizeTransactionTabsBoot();
|
const defaultSelectedTabId = 'categorize';
|
||||||
const defaultSelectedTabId = uncategorizedTransaction?.is_recognized
|
|
||||||
? 'categorize'
|
|
||||||
: 'matching';
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tabs
|
<Tabs
|
||||||
|
|||||||
@@ -1,16 +1,13 @@
|
|||||||
// @ts-nocheck
|
// @ts-nocheck
|
||||||
import React from 'react';
|
import React, { useMemo } from 'react';
|
||||||
import { Spinner } from '@blueprintjs/core';
|
import { castArray, uniq } from 'lodash';
|
||||||
import { useUncategorizedTransaction } from '@/hooks/query';
|
|
||||||
|
|
||||||
interface CategorizeTransactionTabsValue {
|
interface CategorizeTransactionTabsValue {
|
||||||
uncategorizedTransactionId: number;
|
uncategorizedTransactionIds: Array<number>;
|
||||||
isUncategorizedTransactionLoading: boolean;
|
|
||||||
uncategorizedTransaction: any;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface CategorizeTransactionTabsBootProps {
|
interface CategorizeTransactionTabsBootProps {
|
||||||
uncategorizedTransactionId: number;
|
uncategorizedTransactionIds: number | Array<number>;
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -26,28 +23,23 @@ export function CategorizeTransactionTabsBoot({
|
|||||||
uncategorizedTransactionId,
|
uncategorizedTransactionId,
|
||||||
children,
|
children,
|
||||||
}: CategorizeTransactionTabsBootProps) {
|
}: CategorizeTransactionTabsBootProps) {
|
||||||
const {
|
const uncategorizedTransactionIds = useMemo(
|
||||||
data: uncategorizedTransaction,
|
() => uniq(castArray(uncategorizedTransactionId)),
|
||||||
isLoading: isUncategorizedTransactionLoading,
|
[uncategorizedTransactionId],
|
||||||
} = useUncategorizedTransaction(uncategorizedTransactionId);
|
);
|
||||||
|
|
||||||
const provider = {
|
const provider = {
|
||||||
uncategorizedTransactionId,
|
uncategorizedTransactionIds,
|
||||||
uncategorizedTransaction,
|
|
||||||
isUncategorizedTransactionLoading,
|
|
||||||
};
|
};
|
||||||
const isLoading = isUncategorizedTransactionLoading;
|
// Use a key prop to force re-render of children when `uncategorizedTransactionIds` changes
|
||||||
|
|
||||||
// Use a key prop to force re-render of children when uncategorizedTransactionId changes
|
|
||||||
const childrenPerKey = React.useMemo(() => {
|
const childrenPerKey = React.useMemo(() => {
|
||||||
return React.Children.map(children, (child) =>
|
return React.Children.map(children, (child) =>
|
||||||
React.cloneElement(child, { key: uncategorizedTransactionId }),
|
React.cloneElement(child, {
|
||||||
|
key: uncategorizedTransactionIds?.join(','),
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
}, [children, uncategorizedTransactionId]);
|
}, [children, uncategorizedTransactionIds]);
|
||||||
|
|
||||||
if (isLoading) {
|
|
||||||
return <Spinner size={30} />;
|
|
||||||
}
|
|
||||||
return (
|
return (
|
||||||
<CategorizeTransactionTabsBootContext.Provider value={provider}>
|
<CategorizeTransactionTabsBootContext.Provider value={provider}>
|
||||||
{childrenPerKey}
|
{childrenPerKey}
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
// @ts-nocheck
|
// @ts-nocheck
|
||||||
import { isEmpty } from 'lodash';
|
import { isEmpty } from 'lodash';
|
||||||
import * as R from 'ramda';
|
import * as R from 'ramda';
|
||||||
import { useEffect, useState, useMemo } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { uniq } from 'lodash';
|
|
||||||
import { AnchorButton, Button, Intent, Tag, Text } from '@blueprintjs/core';
|
import { AnchorButton, Button, Intent, Tag, Text } from '@blueprintjs/core';
|
||||||
import { FastField, FastFieldProps, Formik, useFormikContext } from 'formik';
|
import { FastField, FastFieldProps, Formik, useFormikContext } from 'formik';
|
||||||
import { AppToaster, Box, FormatNumber, Group, Stack } from '@/components';
|
import { AppToaster, Box, FormatNumber, Group, Stack } from '@/components';
|
||||||
@@ -45,24 +44,15 @@ function MatchingBankTransactionRoot({
|
|||||||
// #withBanking
|
// #withBanking
|
||||||
transactionsToCategorizeIdsSelected,
|
transactionsToCategorizeIdsSelected,
|
||||||
}) {
|
}) {
|
||||||
const { uncategorizedTransactionId } = useCategorizeTransactionTabsBoot();
|
const { uncategorizedTransactionIds } = useCategorizeTransactionTabsBoot();
|
||||||
const { mutateAsync: matchTransaction } = useMatchUncategorizedTransaction();
|
const { mutateAsync: matchTransaction } = useMatchUncategorizedTransaction();
|
||||||
|
|
||||||
const selectedTransactionsIds = useMemo(
|
|
||||||
() =>
|
|
||||||
uniq([
|
|
||||||
...transactionsToCategorizeIdsSelected,
|
|
||||||
uncategorizedTransactionId,
|
|
||||||
]),
|
|
||||||
[uncategorizedTransactionId, transactionsToCategorizeIdsSelected],
|
|
||||||
);
|
|
||||||
|
|
||||||
// Handles the form submitting.
|
// Handles the form submitting.
|
||||||
const handleSubmit = (
|
const handleSubmit = (
|
||||||
values: MatchingTransactionFormValues,
|
values: MatchingTransactionFormValues,
|
||||||
{ setSubmitting }: FormikHelpers<MatchingTransactionFormValues>,
|
{ setSubmitting }: FormikHelpers<MatchingTransactionFormValues>,
|
||||||
) => {
|
) => {
|
||||||
const _values = transformToReq(values);
|
const _values = transformToReq(values, uncategorizedTransactionIds);
|
||||||
|
|
||||||
if (_values.matchedTransactions?.length === 0) {
|
if (_values.matchedTransactions?.length === 0) {
|
||||||
AppToaster.show({
|
AppToaster.show({
|
||||||
@@ -72,7 +62,7 @@ function MatchingBankTransactionRoot({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setSubmitting(true);
|
setSubmitting(true);
|
||||||
matchTransaction({ id: uncategorizedTransactionId, value: _values })
|
matchTransaction(_values)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
AppToaster.show({
|
AppToaster.show({
|
||||||
intent: Intent.SUCCESS,
|
intent: Intent.SUCCESS,
|
||||||
@@ -91,7 +81,7 @@ function MatchingBankTransactionRoot({
|
|||||||
message: `The total amount does not equal the uncategorized transaction.`,
|
message: `The total amount does not equal the uncategorized transaction.`,
|
||||||
intent: Intent.DANGER,
|
intent: Intent.DANGER,
|
||||||
});
|
});
|
||||||
|
setSubmitting(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
AppToaster.show({
|
AppToaster.show({
|
||||||
@@ -104,7 +94,7 @@ function MatchingBankTransactionRoot({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<MatchingTransactionBoot
|
<MatchingTransactionBoot
|
||||||
uncategorizedTransactionsIds={selectedTransactionsIds}
|
uncategorizedTransactionsIds={uncategorizedTransactionIds}
|
||||||
>
|
>
|
||||||
<Formik initialValues={initialValues} onSubmit={handleSubmit}>
|
<Formik initialValues={initialValues} onSubmit={handleSubmit}>
|
||||||
<MatchingBankTransactionFormContent />
|
<MatchingBankTransactionFormContent />
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ interface MatchingTransactionBootValues {
|
|||||||
possibleMatches: Array<any>;
|
possibleMatches: Array<any>;
|
||||||
perfectMatchesCount: number;
|
perfectMatchesCount: number;
|
||||||
perfectMatches: Array<any>;
|
perfectMatches: Array<any>;
|
||||||
|
totalPending: number;
|
||||||
matches: Array<any>;
|
matches: Array<any>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,6 +37,7 @@ function MatchingTransactionBoot({
|
|||||||
const possibleMatches = defaultTo(matchingTransactions?.possibleMatches, []);
|
const possibleMatches = defaultTo(matchingTransactions?.possibleMatches, []);
|
||||||
const perfectMatchesCount = matchingTransactions?.perfectMatches?.length || 0;
|
const perfectMatchesCount = matchingTransactions?.perfectMatches?.length || 0;
|
||||||
const perfectMatches = defaultTo(matchingTransactions?.perfectMatches, []);
|
const perfectMatches = defaultTo(matchingTransactions?.perfectMatches, []);
|
||||||
|
const totalPending = defaultTo(matchingTransactions?.totalPending, 0);
|
||||||
|
|
||||||
const matches = R.concat(perfectMatches, possibleMatches);
|
const matches = R.concat(perfectMatches, possibleMatches);
|
||||||
|
|
||||||
@@ -46,6 +48,7 @@ function MatchingTransactionBoot({
|
|||||||
possibleMatches,
|
possibleMatches,
|
||||||
perfectMatchesCount,
|
perfectMatchesCount,
|
||||||
perfectMatches,
|
perfectMatches,
|
||||||
|
totalPending,
|
||||||
matches,
|
matches,
|
||||||
} as MatchingTransactionBootValues;
|
} as MatchingTransactionBootValues;
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,10 @@ import { useMatchingTransactionBoot } from './MatchingTransactionBoot';
|
|||||||
import { useCategorizeTransactionTabsBoot } from './CategorizeTransactionTabsBoot';
|
import { useCategorizeTransactionTabsBoot } from './CategorizeTransactionTabsBoot';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
export const transformToReq = (values: MatchingTransactionFormValues) => {
|
export const transformToReq = (
|
||||||
|
values: MatchingTransactionFormValues,
|
||||||
|
uncategorizedTransactions: Array<number>,
|
||||||
|
) => {
|
||||||
const matchedTransactions = Object.entries(values.matched)
|
const matchedTransactions = Object.entries(values.matched)
|
||||||
.filter(([key, value]) => value)
|
.filter(([key, value]) => value)
|
||||||
.map(([key]) => {
|
.map(([key]) => {
|
||||||
@@ -12,14 +15,13 @@ export const transformToReq = (values: MatchingTransactionFormValues) => {
|
|||||||
|
|
||||||
return { reference_type, reference_id: parseInt(reference_id, 10) };
|
return { reference_type, reference_id: parseInt(reference_id, 10) };
|
||||||
});
|
});
|
||||||
|
return { matchedTransactions, uncategorizedTransactions };
|
||||||
return { matchedTransactions };
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useGetPendingAmountMatched = () => {
|
export const useGetPendingAmountMatched = () => {
|
||||||
const { values } = useFormikContext<MatchingTransactionFormValues>();
|
const { values } = useFormikContext<MatchingTransactionFormValues>();
|
||||||
const { perfectMatches, possibleMatches } = useMatchingTransactionBoot();
|
const { perfectMatches, possibleMatches, totalPending } =
|
||||||
const { uncategorizedTransaction } = useCategorizeTransactionTabsBoot();
|
useMatchingTransactionBoot();
|
||||||
|
|
||||||
return useMemo(() => {
|
return useMemo(() => {
|
||||||
const matchedItems = [...perfectMatches, ...possibleMatches].filter(
|
const matchedItems = [...perfectMatches, ...possibleMatches].filter(
|
||||||
@@ -34,11 +36,10 @@ export const useGetPendingAmountMatched = () => {
|
|||||||
(item.transactionNormal === 'debit' ? 1 : -1) * parseFloat(item.amount),
|
(item.transactionNormal === 'debit' ? 1 : -1) * parseFloat(item.amount),
|
||||||
0,
|
0,
|
||||||
);
|
);
|
||||||
const amount = uncategorizedTransaction.amount;
|
const pendingAmount = totalPending - totalMatchedAmount;
|
||||||
const pendingAmount = amount - totalMatchedAmount;
|
|
||||||
|
|
||||||
return pendingAmount;
|
return pendingAmount;
|
||||||
}, [uncategorizedTransaction, perfectMatches, possibleMatches, values]);
|
}, [totalPending, perfectMatches, possibleMatches, values]);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useAtleastOneMatchedSelected = () => {
|
export const useAtleastOneMatchedSelected = () => {
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ export const withBanking = (mapState) => {
|
|||||||
state.plaid.uncategorizedTransactionsSelected,
|
state.plaid.uncategorizedTransactionsSelected,
|
||||||
|
|
||||||
excludedTransactionsIdsSelected: state.plaid.excludedTransactionsSelected,
|
excludedTransactionsIdsSelected: state.plaid.excludedTransactionsSelected,
|
||||||
isMultipleCategorization: state.plaid.isMultipleCategorization,
|
enableMultipleCategorization: state.plaid.enableMultipleCategorization,
|
||||||
|
|
||||||
transactionsToCategorizeIdsSelected:
|
transactionsToCategorizeIdsSelected:
|
||||||
state.plaid.transactionsToCategorizeSelected,
|
state.plaid.transactionsToCategorizeSelected,
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ import {
|
|||||||
resetTransactionsToCategorizeSelected,
|
resetTransactionsToCategorizeSelected,
|
||||||
setTransactionsToCategorizeSelected,
|
setTransactionsToCategorizeSelected,
|
||||||
enableMultipleCategorization,
|
enableMultipleCategorization,
|
||||||
|
addTransactionsToCategorizeSelected,
|
||||||
|
removeTransactionsToCategorizeSelected,
|
||||||
} from '@/store/banking/banking.reducer';
|
} from '@/store/banking/banking.reducer';
|
||||||
|
|
||||||
export interface WithBankingActionsProps {
|
export interface WithBankingActionsProps {
|
||||||
@@ -28,6 +30,8 @@ export interface WithBankingActionsProps {
|
|||||||
resetExcludedTransactionsSelected: () => void;
|
resetExcludedTransactionsSelected: () => void;
|
||||||
|
|
||||||
setTransactionsToCategorizeSelected: (ids: Array<string | number>) => void;
|
setTransactionsToCategorizeSelected: (ids: Array<string | number>) => void;
|
||||||
|
addTransactionsToCategorizeSelected: (id: string | number) => void;
|
||||||
|
removeTransactionsToCategorizeSelected: (id: string | number) => void;
|
||||||
resetTransactionsToCategorizeSelected: () => void;
|
resetTransactionsToCategorizeSelected: () => void;
|
||||||
|
|
||||||
enableMultipleCategorization: (enable: boolean) => void;
|
enableMultipleCategorization: (enable: boolean) => void;
|
||||||
@@ -88,6 +92,22 @@ const mapDipatchToProps = (dispatch: any): WithBankingActionsProps => ({
|
|||||||
setTransactionsToCategorizeSelected: (ids: Array<string | number>) =>
|
setTransactionsToCategorizeSelected: (ids: Array<string | number>) =>
|
||||||
dispatch(setTransactionsToCategorizeSelected({ ids })),
|
dispatch(setTransactionsToCategorizeSelected({ ids })),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds selected transactions to categorize.
|
||||||
|
* @param {string | number} id
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
addTransactionsToCategorizeSelected: (id: string | number) =>
|
||||||
|
dispatch(addTransactionsToCategorizeSelected({ id })),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes the selected transactions.
|
||||||
|
* @param {string | number} id
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
removeTransactionsToCategorizeSelected: (id: string | number) =>
|
||||||
|
dispatch(removeTransactionsToCategorizeSelected({ id })),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resets the selected transactions to categorize or match.
|
* Resets the selected transactions to categorize or match.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ const QUERY_KEY = {
|
|||||||
RECOGNIZED_BANK_TRANSACTIONS_INFINITY:
|
RECOGNIZED_BANK_TRANSACTIONS_INFINITY:
|
||||||
'RECOGNIZED_BANK_TRANSACTIONS_INFINITY',
|
'RECOGNIZED_BANK_TRANSACTIONS_INFINITY',
|
||||||
BANK_ACCOUNT_SUMMARY_META: 'BANK_ACCOUNT_SUMMARY_META',
|
BANK_ACCOUNT_SUMMARY_META: 'BANK_ACCOUNT_SUMMARY_META',
|
||||||
|
AUTOFILL_CATEGORIZE_BANK_TRANSACTION: 'AUTOFILL_CATEGORIZE_BANK_TRANSACTION',
|
||||||
};
|
};
|
||||||
|
|
||||||
const commonInvalidateQueries = (query: QueryClient) => {
|
const commonInvalidateQueries = (query: QueryClient) => {
|
||||||
@@ -244,6 +245,7 @@ interface GetBankTransactionsMatchesValue {
|
|||||||
interface GetBankTransactionsMatchesResponse {
|
interface GetBankTransactionsMatchesResponse {
|
||||||
perfectMatches: Array<any>;
|
perfectMatches: Array<any>;
|
||||||
possibleMatches: Array<any>;
|
possibleMatches: Array<any>;
|
||||||
|
totalPending: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -441,8 +443,8 @@ export function useUnexcludeUncategorizedTransactions(
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface MatchUncategorizedTransactionValues {
|
interface MatchUncategorizedTransactionValues {
|
||||||
id: number;
|
uncategorizedTransactions: Array<number>;
|
||||||
value: any;
|
matchedTransactions: Array<{ reference_type: string; reference_id: number }>;
|
||||||
}
|
}
|
||||||
interface MatchUncategorizedTransactionRes {}
|
interface MatchUncategorizedTransactionRes {}
|
||||||
|
|
||||||
@@ -469,7 +471,7 @@ export function useMatchUncategorizedTransaction(
|
|||||||
MatchUncategorizedTransactionRes,
|
MatchUncategorizedTransactionRes,
|
||||||
Error,
|
Error,
|
||||||
MatchUncategorizedTransactionValues
|
MatchUncategorizedTransactionValues
|
||||||
>(({ id, value }) => apiRequest.post(`/banking/matches/${id}`, value), {
|
>((value) => apiRequest.post('/banking/matches/match', value), {
|
||||||
onSuccess: (res, id) => {
|
onSuccess: (res, id) => {
|
||||||
queryClient.invalidateQueries(
|
queryClient.invalidateQueries(
|
||||||
t.CASHFLOW_ACCOUNT_UNCATEGORIZED_TRANSACTIONS_INFINITY,
|
t.CASHFLOW_ACCOUNT_UNCATEGORIZED_TRANSACTIONS_INFINITY,
|
||||||
@@ -585,6 +587,42 @@ export function useGetBankAccountSummaryMeta(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface GetAutofillCategorizeTransaction {
|
||||||
|
accountId: number | null;
|
||||||
|
amount: number;
|
||||||
|
category: string | null;
|
||||||
|
date: Date;
|
||||||
|
formattedAmount: string;
|
||||||
|
formattedDate: string;
|
||||||
|
isRecognized: boolean;
|
||||||
|
recognizedByRuleId: number | null;
|
||||||
|
recognizedByRuleName: string | null;
|
||||||
|
referenceNo: null | string;
|
||||||
|
isDepositTransaction: boolean;
|
||||||
|
isWithdrawalTransaction: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useGetAutofillCategorizeTransaction(
|
||||||
|
uncategorizedTransactionIds: number[],
|
||||||
|
options: any,
|
||||||
|
) {
|
||||||
|
const apiRequest = useApiRequest();
|
||||||
|
|
||||||
|
return useQuery<GetAutofillCategorizeTransaction, Error>(
|
||||||
|
[
|
||||||
|
QUERY_KEY.AUTOFILL_CATEGORIZE_BANK_TRANSACTION,
|
||||||
|
uncategorizedTransactionIds,
|
||||||
|
],
|
||||||
|
() =>
|
||||||
|
apiRequest
|
||||||
|
.get(`/banking/categorize/autofill`, {
|
||||||
|
params: { uncategorizedTransactionIds },
|
||||||
|
})
|
||||||
|
.then((res) => transformToCamelCase(res.data?.data)),
|
||||||
|
{ ...options },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
|
|||||||
Reference in New Issue
Block a user