feat: wip multi-select transactions to categorization and matching

This commit is contained in:
Ahmed Bouhuolia
2024-08-03 22:01:21 +02:00
parent 5ce11f192f
commit d74337fb94
29 changed files with 476 additions and 155 deletions

View File

@@ -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;
} }
} }

View File

@@ -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);
}
}
}

View File

@@ -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;

View File

@@ -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,
}; };
} }

View File

@@ -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);
} }
} }

View File

@@ -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 = {

View File

@@ -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),
}
);
}
}

View File

@@ -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
);
}
}

View File

@@ -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';

View File

@@ -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,

View File

@@ -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;
} }

View File

@@ -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);
} }

View File

@@ -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),
});
} }
}; };

View File

@@ -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)`

View File

@@ -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',
}, },

View File

@@ -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} />;

View File

@@ -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;

View File

@@ -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);

View File

@@ -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>

View File

@@ -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,

View File

@@ -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);

View File

@@ -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

View File

@@ -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}

View File

@@ -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 />

View File

@@ -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;

View File

@@ -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 = () => {

View File

@@ -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,

View File

@@ -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.
*/ */

View File

@@ -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
*/ */