Compare commits

...

20 Commits

Author SHA1 Message Date
Ahmed Bouhuolia
14d1f0bd1d fix: Multi-lines transactions statements 2024-08-12 16:31:36 +02:00
Ahmed Bouhuolia
82f8648c59 Merge pull request #593 from bigcapitalhq/fix-round-pending-matching
fix: Rounding the total amount the pending and matched transactions
2024-08-12 13:01:27 +02:00
Ahmed Bouhuolia
c928940d32 fix: rounding the total amount the pending and matched transactions 2024-08-12 13:01:00 +02:00
Ahmed Bouhuolia
0a78d56015 Merge pull request #592 from bigcapitalhq/matching-reconcile-branches
fix: Should not load branches on reconcile matching form if the branches not enabled
2024-08-12 11:29:48 +02:00
Ahmed Bouhuolia
1a5716873e fix: should not load branches on reconcile matching form if the branches not enabled 2024-08-12 11:28:34 +02:00
Ahmed Bouhuolia
01b7c86ab9 Merge pull request #588 from Champetaman/fix-dev-variable-setting-error
Update `dev` Script in `package.json` to Use `cross-env`
2024-08-12 10:55:45 +02:00
Ahmed Bouhuolia
0ca209b195 Merge pull request #589 from bigcapitalhq/bank-pending-transactions
feat: Pending bank transactions
2024-08-12 10:54:32 +02:00
Ahmed Bouhuolia
be6f6e3c73 fix: function description 2024-08-12 10:54:16 +02:00
Ahmed Bouhuolia
cb016be78c fix: avoid decrement/increment for pending bank transactions 2024-08-12 10:48:36 +02:00
Ahmed Bouhuolia
fc085f2328 Merge pull request #587 from bigcapitalhq/big-244-uncategorize-bank-transactions-in-bulk
feat: Uncategorize bank transactions in bulk
2024-08-12 10:11:55 +02:00
Ahmed Bouhuolia
9a34f3e283 fix: invalidate account cache on bulk uncategorizing 2024-08-12 10:11:40 +02:00
Ahmed Bouhuolia
7054e862d5 feat: pending transactions table 2024-08-11 22:51:58 +02:00
Ahmed Bouhuolia
faa81abee4 feat(banking): uncategorize bank transactions in bulk 2024-08-11 21:26:02 +02:00
Ahmed Bouhuolia
6d01f2a323 Merge pull request #591 from bigcapitalhq/import-export-tax-rates
feat: import and export tax rates
2024-08-11 19:51:50 +02:00
Ahmed Bouhuolia
72678bb936 feat: import and export tax rates 2024-08-11 19:51:16 +02:00
Ahmed Bouhuolia
9ae5644af9 feat: Pending bank transactions 2024-08-11 16:14:13 +02:00
Camilo Oviedo
e8830c5911 Fix dev variable setting causing error on windows for craco start command 2024-08-11 22:14:54 +10:00
Camilo Oviedo
7699889bd6 Fix dev variable setting causing error on windows for craco start command 2024-08-11 21:57:50 +10:00
Ahmed Bouhuolia
35a061d188 feat: Uncategorize bank transactions in bulk 2024-08-11 13:02:38 +02:00
Ahmed Bouhuolia
c7c021c969 fix(banking): detarmine if Plaid item is disabled (#585) 2024-08-11 12:10:15 +02:00
78 changed files with 1458 additions and 140 deletions

View File

@@ -1,9 +1,10 @@
import { Inject, Service } from 'typedi'; import { Inject, Service } from 'typedi';
import { NextFunction, Request, Response, Router } from 'express'; import { NextFunction, Request, Response, Router } from 'express';
import { param, query } from 'express-validator';
import BaseController from '@/api/controllers/BaseController'; import BaseController from '@/api/controllers/BaseController';
import { GetBankAccountSummary } from '@/services/Banking/BankAccounts/GetBankAccountSummary'; import { GetBankAccountSummary } from '@/services/Banking/BankAccounts/GetBankAccountSummary';
import { BankAccountsApplication } from '@/services/Banking/BankAccounts/BankAccountsApplication'; import { BankAccountsApplication } from '@/services/Banking/BankAccounts/BankAccountsApplication';
import { param } from 'express-validator'; import { GetPendingBankAccountTransactions } from '@/services/Cashflow/GetPendingBankAccountTransaction';
@Service() @Service()
export class BankAccountsController extends BaseController { export class BankAccountsController extends BaseController {
@@ -13,6 +14,9 @@ export class BankAccountsController extends BaseController {
@Inject() @Inject()
private bankAccountsApp: BankAccountsApplication; private bankAccountsApp: BankAccountsApplication;
@Inject()
private getPendingTransactionsService: GetPendingBankAccountTransactions;
/** /**
* Router constructor. * Router constructor.
*/ */
@@ -20,6 +24,16 @@ export class BankAccountsController extends BaseController {
const router = Router(); const router = Router();
router.get('/:bankAccountId/meta', this.getBankAccountSummary.bind(this)); router.get('/:bankAccountId/meta', this.getBankAccountSummary.bind(this));
router.get(
'/pending_transactions',
[
query('account_id').optional().isNumeric().toInt(),
query('page').optional().isNumeric().toInt(),
query('page_size').optional().isNumeric().toInt(),
],
this.validationResult,
this.getBankAccountsPendingTransactions.bind(this)
);
router.post( router.post(
'/:bankAccountId/disconnect', '/:bankAccountId/disconnect',
this.disconnectBankAccount.bind(this) this.disconnectBankAccount.bind(this)
@@ -27,17 +41,13 @@ export class BankAccountsController extends BaseController {
router.post('/:bankAccountId/update', this.refreshBankAccount.bind(this)); router.post('/:bankAccountId/update', this.refreshBankAccount.bind(this));
router.post( router.post(
'/:bankAccountId/pause_feeds', '/:bankAccountId/pause_feeds',
[ [param('bankAccountId').exists().isNumeric().toInt()],
param('bankAccountId').exists().isNumeric().toInt(),
],
this.validationResult, this.validationResult,
this.pauseBankAccountFeeds.bind(this) this.pauseBankAccountFeeds.bind(this)
); );
router.post( router.post(
'/:bankAccountId/resume_feeds', '/:bankAccountId/resume_feeds',
[ [param('bankAccountId').exists().isNumeric().toInt()],
param('bankAccountId').exists().isNumeric().toInt(),
],
this.validationResult, this.validationResult,
this.resumeBankAccountFeeds.bind(this) this.resumeBankAccountFeeds.bind(this)
); );
@@ -72,6 +82,32 @@ export class BankAccountsController extends BaseController {
} }
} }
/**
* Retrieves the bank account pending transactions.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
*/
async getBankAccountsPendingTransactions(
req: Request,
res: Response,
next: NextFunction
) {
const { tenantId } = req;
const query = this.matchedQueryData(req);
try {
const data =
await this.getPendingTransactionsService.getPendingTransactions(
tenantId,
query
);
return res.status(200).send(data);
} catch (error) {
next(error);
}
}
/** /**
* Disonnect the given bank account. * Disonnect the given bank account.
* @param {Request} req * @param {Request} req
@@ -128,9 +164,9 @@ export class BankAccountsController extends BaseController {
/** /**
* Resumes the bank account feeds sync. * Resumes the bank account feeds sync.
* @param {Request} req * @param {Request} req
* @param {Response} res * @param {Response} res
* @param {NextFunction} next * @param {NextFunction} next
* @returns {Promise<Response | void>} * @returns {Promise<Response | void>}
*/ */
async resumeBankAccountFeeds( async resumeBankAccountFeeds(
@@ -155,9 +191,9 @@ export class BankAccountsController extends BaseController {
/** /**
* Pauses the bank account feeds sync. * Pauses the bank account feeds sync.
* @param {Request} req * @param {Request} req
* @param {Response} res * @param {Response} res
* @param {NextFunction} next * @param {NextFunction} next
* @returns {Promise<Response | void>} * @returns {Promise<Response | void>}
*/ */
async pauseBankAccountFeeds( async pauseBankAccountFeeds(

View File

@@ -1,5 +1,5 @@
import { Service, Inject } from 'typedi'; import { Service, Inject } from 'typedi';
import { ValidationChain, check, param, query } from 'express-validator'; import { ValidationChain, body, check, param, query } from 'express-validator';
import { Router, Request, Response, NextFunction } from 'express'; import { Router, Request, Response, NextFunction } from 'express';
import { omit } from 'lodash'; import { omit } from 'lodash';
import BaseController from '../BaseController'; import BaseController from '../BaseController';
@@ -43,6 +43,16 @@ export default class NewCashflowTransactionController extends BaseController {
this.asyncMiddleware(this.newCashflowTransaction), this.asyncMiddleware(this.newCashflowTransaction),
this.catchServiceErrors this.catchServiceErrors
); );
router.post(
'/transactions/uncategorize/bulk',
[
body('ids').isArray({ min: 1 }),
body('ids.*').exists().isNumeric().toInt(),
],
this.validationResult,
this.uncategorizeBulkTransactions.bind(this),
this.catchServiceErrors
);
router.post( router.post(
'/transactions/:id/uncategorize', '/transactions/:id/uncategorize',
this.revertCategorizedCashflowTransaction, this.revertCategorizedCashflowTransaction,
@@ -184,6 +194,34 @@ export default class NewCashflowTransactionController extends BaseController {
} }
}; };
/**
* Uncategorize the given transactions in bulk.
* @param {Request<{}>} req
* @param {Response} res
* @param {NextFunction} next
* @returns {Promise<Response | null>}
*/
private uncategorizeBulkTransactions = async (
req: Request<{}>,
res: Response,
next: NextFunction
) => {
const { tenantId } = req;
const { ids: uncategorizedTransactionIds } = this.matchedBodyData(req);
try {
await this.cashflowApplication.uncategorizeTransactions(
tenantId,
uncategorizedTransactionIds
);
return res.status(200).send({
message: 'The given transactions have been uncategorized successfully.',
});
} catch (error) {
next(error);
}
};
/** /**
* Categorize the cashflow transaction. * Categorize the cashflow transaction.
* @param {Request} req * @param {Request} req

View File

@@ -0,0 +1,13 @@
exports.up = function (knex) {
return knex.schema.table('uncategorized_cashflow_transactions', (table) => {
table.boolean('pending').defaultTo(false);
table.string('pending_plaid_transaction_id').nullable();
});
};
exports.down = function (knex) {
return knex.schema.table('uncategorized_cashflow_transactions', (table) => {
table.dropColumn('pending');
table.dropColumn('pending_plaid_transaction_id');
});
};

View File

@@ -268,6 +268,8 @@ export interface CreateUncategorizedTransactionDTO {
description?: string; description?: string;
referenceNo?: string | null; referenceNo?: string | null;
plaidTransactionId?: string | null; plaidTransactionId?: string | null;
pending?: boolean;
pendingPlaidTransactionId?: string | null;
batch?: string; batch?: string;
} }
@@ -283,3 +285,17 @@ export interface IUncategorizedTransactionCreatedEventPayload {
createUncategorizedTransactionDTO: CreateUncategorizedTransactionDTO; createUncategorizedTransactionDTO: CreateUncategorizedTransactionDTO;
trx: Knex.Transaction; trx: Knex.Transaction;
} }
export interface IPendingTransactionRemovingEventPayload {
tenantId: number;
uncategorizedTransactionId: number;
pendingTransaction: IUncategorizedCashflowTransaction;
trx?: Knex.Transaction;
}
export interface IPendingTransactionRemovedEventPayload {
tenantId: number;
uncategorizedTransactionId: number;
pendingTransaction: IUncategorizedCashflowTransaction;
trx?: Knex.Transaction;
}

View File

@@ -0,0 +1,69 @@
export default {
defaultSort: {
sortOrder: 'DESC',
sortField: 'created_at',
},
exportable: true,
importable: true,
print: {
pageTitle: 'Tax Rates',
},
columns: {
name: {
name: 'Tax Rate Name',
type: 'text',
accessor: 'name',
},
code: {
name: 'Code',
type: 'text',
accessor: 'code',
},
rate: {
name: 'Rate',
type: 'text',
},
description: {
name: 'Description',
type: 'text',
},
isNonRecoverable: {
name: 'Is Non Recoverable',
type: 'boolean',
},
active: {
name: 'Active',
type: 'boolean',
},
},
field: {},
fields2: {
name: {
name: 'Tax name',
fieldType: 'name',
required: true,
},
code: {
name: 'Code',
fieldType: 'code',
required: true,
},
rate: {
name: 'Rate',
fieldType: 'number',
required: true,
},
description: {
name: 'Description',
fieldType: 'text',
},
isNonRecoverable: {
name: 'Is Non Recoverable',
fieldType: 'boolean',
},
active: {
name: 'Active',
fieldType: 'boolean',
},
},
};

View File

@@ -2,8 +2,13 @@ import { mixin, Model, raw } from 'objection';
import TenantModel from 'models/TenantModel'; import TenantModel from 'models/TenantModel';
import ModelSearchable from './ModelSearchable'; import ModelSearchable from './ModelSearchable';
import SoftDeleteQueryBuilder from '@/collection/SoftDeleteQueryBuilder'; import SoftDeleteQueryBuilder from '@/collection/SoftDeleteQueryBuilder';
import TaxRateMeta from './TaxRate.settings';
import ModelSetting from './ModelSetting';
export default class TaxRate extends mixin(TenantModel, [ModelSearchable]) { export default class TaxRate extends mixin(TenantModel, [
ModelSetting,
ModelSearchable,
]) {
/** /**
* Table name * Table name
*/ */
@@ -25,6 +30,13 @@ export default class TaxRate extends mixin(TenantModel, [ModelSearchable]) {
return ['createdAt', 'updatedAt']; return ['createdAt', 'updatedAt'];
} }
/**
* Retrieves the tax rate meta.
*/
static get meta() {
return TaxRateMeta;
}
/** /**
* Virtual attributes. * Virtual attributes.
*/ */

View File

@@ -21,6 +21,7 @@ export default class UncategorizedCashflowTransaction extends mixin(
plaidTransactionId!: string; plaidTransactionId!: string;
recognizedTransactionId!: number; recognizedTransactionId!: number;
excludedAt: Date; excludedAt: Date;
pending: boolean;
/** /**
* Table name. * Table name.
@@ -46,7 +47,8 @@ export default class UncategorizedCashflowTransaction extends mixin(
'isDepositTransaction', 'isDepositTransaction',
'isWithdrawalTransaction', 'isWithdrawalTransaction',
'isRecognized', 'isRecognized',
'isExcluded' 'isExcluded',
'isPending',
]; ];
} }
@@ -99,6 +101,14 @@ export default class UncategorizedCashflowTransaction extends mixin(
return !!this.excludedAt; return !!this.excludedAt;
} }
/**
* Detarmines whether the transaction is pending.
* @returns {boolean}
*/
public get isPending(): boolean {
return !!this.pending;
}
/** /**
* Model modifiers. * Model modifiers.
*/ */
@@ -143,6 +153,20 @@ export default class UncategorizedCashflowTransaction extends mixin(
query.whereNull('categorizeRefType'); query.whereNull('categorizeRefType');
query.whereNull('categorizeRefId'); query.whereNull('categorizeRefId');
}, },
/**
* Filters the not pending transactions.
*/
notPending(query) {
query.where('pending', false);
},
/**
* Filters the pending transactions.
*/
pending(query) {
query.where('pending', true);
},
}; };
} }

View File

@@ -52,6 +52,9 @@ export class GetBankAccountSummary {
q.withGraphJoined('matchedBankTransactions'); q.withGraphJoined('matchedBankTransactions');
q.whereNull('matchedBankTransactions.id'); q.whereNull('matchedBankTransactions.id');
// Exclude the pending transactions.
q.modify('notPending');
// Count the results. // Count the results.
q.count('uncategorized_cashflow_transactions.id as total'); q.count('uncategorized_cashflow_transactions.id as total');
q.first(); q.first();
@@ -65,16 +68,32 @@ export class GetBankAccountSummary {
q.withGraphJoined('recognizedTransaction'); q.withGraphJoined('recognizedTransaction');
q.whereNotNull('recognizedTransaction.id'); q.whereNotNull('recognizedTransaction.id');
// Exclude the pending transactions.
q.modify('notPending');
// Count the results. // Count the results.
q.count('uncategorized_cashflow_transactions.id as total'); q.count('uncategorized_cashflow_transactions.id as total');
q.first(); q.first();
}); });
// Retrieves excluded transactions count.
const excludedTransactionsCount = const excludedTransactionsCount =
await UncategorizedCashflowTransaction.query().onBuild((q) => { await UncategorizedCashflowTransaction.query().onBuild((q) => {
q.where('accountId', bankAccountId); q.where('accountId', bankAccountId);
q.modify('excluded'); q.modify('excluded');
// Exclude the pending transactions.
q.modify('notPending');
// Count the results.
q.count('uncategorized_cashflow_transactions.id as total');
q.first();
});
// Retrieves the pending transactions count.
const pendingTransactionsCount =
await UncategorizedCashflowTransaction.query().onBuild((q) => {
q.where('accountId', bankAccountId);
q.modify('pending');
// Count the results. // Count the results.
q.count('uncategorized_cashflow_transactions.id as total'); q.count('uncategorized_cashflow_transactions.id as total');
q.first(); q.first();
@@ -83,14 +102,15 @@ export class GetBankAccountSummary {
const totalUncategorizedTransactions = const totalUncategorizedTransactions =
uncategorizedTranasctionsCount?.total || 0; uncategorizedTranasctionsCount?.total || 0;
const totalRecognizedTransactions = recognizedTransactionsCount?.total || 0; const totalRecognizedTransactions = recognizedTransactionsCount?.total || 0;
const totalExcludedTransactions = excludedTransactionsCount?.total || 0; const totalExcludedTransactions = excludedTransactionsCount?.total || 0;
const totalPendingTransactions = pendingTransactionsCount?.total || 0;
return { return {
name: bankAccount.name, name: bankAccount.name,
totalUncategorizedTransactions, totalUncategorizedTransactions,
totalRecognizedTransactions, totalRecognizedTransactions,
totalExcludedTransactions, totalExcludedTransactions,
totalPendingTransactions,
}; };
} }
} }

View File

@@ -64,7 +64,7 @@ export class GetMatchedTransactions {
.whereIn('id', uncategorizedTransactionIds) .whereIn('id', uncategorizedTransactionIds)
.throwIfNotFound(); .throwIfNotFound();
const totalPending = Math.abs(sumBy(uncategorizedTransactions, 'amount')); const totalPending = 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)

View File

@@ -25,6 +25,7 @@ import { Knex } from 'knex';
import uniqid from 'uniqid'; import uniqid from 'uniqid';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher'; import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import events from '@/subscribers/events'; import events from '@/subscribers/events';
import { RemovePendingUncategorizedTransaction } from '@/services/Cashflow/RemovePendingUncategorizedTransaction';
const CONCURRENCY_ASYNC = 10; const CONCURRENCY_ASYNC = 10;
@@ -40,7 +41,7 @@ export class PlaidSyncDb {
private cashflowApp: CashflowApplication; private cashflowApp: CashflowApplication;
@Inject() @Inject()
private deleteCashflowTransactionService: DeleteCashflowTransaction; private removePendingTransaction: RemovePendingUncategorizedTransaction;
@Inject() @Inject()
private eventPublisher: EventPublisher; private eventPublisher: EventPublisher;
@@ -185,21 +186,22 @@ export class PlaidSyncDb {
plaidTransactionsIds: string[], plaidTransactionsIds: string[],
trx?: Knex.Transaction trx?: Knex.Transaction
) { ) {
const { CashflowTransaction } = this.tenancy.models(tenantId); const { UncategorizedCashflowTransaction } = this.tenancy.models(tenantId);
const cashflowTransactions = await CashflowTransaction.query(trx).whereIn( const uncategorizedTransactions =
'plaidTransactionId', await UncategorizedCashflowTransaction.query(trx).whereIn(
plaidTransactionsIds 'plaidTransactionId',
); plaidTransactionsIds
const cashflowTransactionsIds = cashflowTransactions.map( );
const uncategorizedTransactionsIds = uncategorizedTransactions.map(
(trans) => trans.id (trans) => trans.id
); );
await bluebird.map( await bluebird.map(
cashflowTransactionsIds, uncategorizedTransactionsIds,
(transactionId: number) => (uncategorizedTransactionId: number) =>
this.deleteCashflowTransactionService.deleteCashflowTransaction( this.removePendingTransaction.removePendingTransaction(
tenantId, tenantId,
transactionId, uncategorizedTransactionId,
trx trx
), ),
{ concurrency: CONCURRENCY_ASYNC } { concurrency: CONCURRENCY_ASYNC }

View File

@@ -73,6 +73,12 @@ export class PlaidUpdateTransactions {
item, item,
trx trx
); );
// Sync removed transactions.
await this.plaidSync.syncRemoveTransactions(
tenantId,
removed?.map((r) => r.transaction_id),
trx
);
// Sync bank account transactions. // Sync bank account transactions.
await this.plaidSync.syncAccountsTransactions( await this.plaidSync.syncAccountsTransactions(
tenantId, tenantId,

View File

@@ -83,8 +83,9 @@ export class PlaidWebooks {
webhookCode: string webhookCode: string
): Promise<void> { ): Promise<void> {
const { PlaidItem } = this.tenancy.models(tenantId); const { PlaidItem } = this.tenancy.models(tenantId);
const plaidItem = await PlaidItem.query() const plaidItem = await PlaidItem.query()
.findById(plaidItemId) .findOne({ plaidItemId })
.throwIfNotFound(); .throwIfNotFound();
switch (webhookCode) { switch (webhookCode) {

View File

@@ -3,11 +3,11 @@ import {
Item as PlaidItem, Item as PlaidItem,
Institution as PlaidInstitution, Institution as PlaidInstitution,
AccountBase as PlaidAccount, AccountBase as PlaidAccount,
TransactionBase as PlaidTransactionBase,
} from 'plaid'; } from 'plaid';
import { import {
CreateUncategorizedTransactionDTO, CreateUncategorizedTransactionDTO,
IAccountCreateDTO, IAccountCreateDTO,
PlaidTransaction,
} from '@/interfaces'; } from '@/interfaces';
/** /**
@@ -48,7 +48,7 @@ export const transformPlaidAccountToCreateAccount = R.curry(
export const transformPlaidTrxsToCashflowCreate = R.curry( export const transformPlaidTrxsToCashflowCreate = R.curry(
( (
cashflowAccountId: number, cashflowAccountId: number,
plaidTranasction: PlaidTransaction plaidTranasction: PlaidTransactionBase
): CreateUncategorizedTransactionDTO => { ): CreateUncategorizedTransactionDTO => {
return { return {
date: plaidTranasction.date, date: plaidTranasction.date,
@@ -64,6 +64,8 @@ export const transformPlaidTrxsToCashflowCreate = R.curry(
accountId: cashflowAccountId, accountId: cashflowAccountId,
referenceNo: plaidTranasction.payment_meta?.reference_number, referenceNo: plaidTranasction.payment_meta?.reference_number,
plaidTransactionId: plaidTranasction.transaction_id, plaidTransactionId: plaidTranasction.transaction_id,
pending: plaidTranasction.pending,
pendingPlaidTransactionId: plaidTranasction.pending_transaction_id,
}; };
} }
); );

View File

@@ -21,6 +21,7 @@ import GetCashflowAccountsService from './GetCashflowAccountsService';
import { GetCashflowTransactionService } from './GetCashflowTransactionsService'; import { GetCashflowTransactionService } from './GetCashflowTransactionsService';
import { GetRecognizedTransactionsService } from './GetRecongizedTransactions'; import { GetRecognizedTransactionsService } from './GetRecongizedTransactions';
import { GetRecognizedTransactionService } from './GetRecognizedTransaction'; import { GetRecognizedTransactionService } from './GetRecognizedTransaction';
import { UncategorizeCashflowTransactionsBulk } from './UncategorizeCashflowTransactionsBulk';
@Service() @Service()
export class CashflowApplication { export class CashflowApplication {
@@ -39,6 +40,9 @@ export class CashflowApplication {
@Inject() @Inject()
private uncategorizeTransactionService: UncategorizeCashflowTransaction; private uncategorizeTransactionService: UncategorizeCashflowTransaction;
@Inject()
private uncategorizeTransasctionsService: UncategorizeCashflowTransactionsBulk;
@Inject() @Inject()
private categorizeTransactionService: CategorizeCashflowTransaction; private categorizeTransactionService: CategorizeCashflowTransaction;
@@ -155,6 +159,22 @@ export class CashflowApplication {
); );
} }
/**
* Uncategorize the given transactions in bulk.
* @param {number} tenantId
* @param {number | Array<number>} transactionId
* @returns
*/
public uncategorizeTransactions(
tenantId: number,
transactionId: number | Array<number>
) {
return this.uncategorizeTransasctionsService.uncategorizeBulk(
tenantId,
transactionId
);
}
/** /**
* Categorize the given cashflow transaction. * Categorize the given cashflow transaction.
* @param {number} tenantId * @param {number} tenantId
@@ -241,9 +261,9 @@ export class CashflowApplication {
/** /**
* Retrieves the recognized transaction of the given uncategorized transaction. * Retrieves the recognized transaction of the given uncategorized transaction.
* @param {number} tenantId * @param {number} tenantId
* @param {number} uncategorizedTransactionId * @param {number} uncategorizedTransactionId
* @returns * @returns
*/ */
public getRecognizedTransaction( public getRecognizedTransaction(
tenantId: number, tenantId: number,

View File

@@ -0,0 +1,53 @@
import { Inject } from 'typedi';
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
import HasTenancyService from '../Tenancy/TenancyService';
import { GetPendingBankAccountTransactionTransformer } from './GetPendingBankAccountTransactionTransformer';
export class GetPendingBankAccountTransactions {
@Inject()
private tenancy: HasTenancyService;
@Inject()
private transformer: TransformerInjectable;
/**
* Retrieves the given bank accounts pending transaction.
* @param {number} tenantId - Tenant id.
* @param {GetPendingTransactionsQuery} filter - Pending transactions query.
*/
async getPendingTransactions(
tenantId: number,
filter?: GetPendingTransactionsQuery
) {
const { UncategorizedCashflowTransaction } = this.tenancy.models(tenantId);
const _filter = {
page: 1,
pageSize: 20,
...filter,
};
const { results, pagination } =
await UncategorizedCashflowTransaction.query()
.onBuild((q) => {
q.modify('pending');
if (_filter?.accountId) {
q.where('accountId', _filter.accountId);
}
})
.pagination(_filter.page - 1, _filter.pageSize);
const data = await this.transformer.transform(
tenantId,
results,
new GetPendingBankAccountTransactionTransformer()
);
return { data, pagination };
}
}
interface GetPendingTransactionsQuery {
page?: number;
pageSize?: number;
accountId?: number;
}

View File

@@ -0,0 +1,73 @@
import { Transformer } from '@/lib/Transformer/Transformer';
import { formatNumber } from '@/utils';
export class GetPendingBankAccountTransactionTransformer extends Transformer {
/**
* Include these attributes to sale invoice object.
* @returns {string[]}
*/
public includeAttributes = (): string[] => {
return [
'formattedAmount',
'formattedDate',
'formattedDepositAmount',
'formattedWithdrawalAmount',
];
};
/**
* Exclude all attributes.
* @returns {Array<string>}
*/
public excludeAttributes = (): string[] => {
return [];
};
/**
* Formattes the transaction date.
* @param transaction
* @returns {string}
*/
public formattedDate(transaction) {
return this.formatDate(transaction.date);
}
/**
* Formatted amount.
* @param transaction
* @returns {string}
*/
public formattedAmount(transaction) {
return formatNumber(transaction.amount, {
currencyCode: transaction.currencyCode,
});
}
/**
* Formatted deposit amount.
* @param transaction
* @returns {string}
*/
protected formattedDepositAmount(transaction) {
if (transaction.isDepositTransaction) {
return formatNumber(transaction.deposit, {
currencyCode: transaction.currencyCode,
});
}
return '';
}
/**
* Formatted withdrawal amount.
* @param transaction
* @returns {string}
*/
protected formattedWithdrawalAmount(transaction) {
if (transaction.isWithdrawalTransaction) {
return formatNumber(transaction.withdrawal, {
currencyCode: transaction.currencyCode,
});
}
return '';
}
}

View File

@@ -34,8 +34,13 @@ export class GetRecognizedTransactionsService {
q.withGraphFetched('recognizedTransaction.assignAccount'); q.withGraphFetched('recognizedTransaction.assignAccount');
q.withGraphFetched('recognizedTransaction.bankRule'); q.withGraphFetched('recognizedTransaction.bankRule');
q.whereNotNull('recognizedTransactionId'); q.whereNotNull('recognizedTransactionId');
// Exclude the excluded transactions.
q.modify('notExcluded'); q.modify('notExcluded');
// Exclude the pending transactions.
q.modify('notPending');
if (_filter.accountId) { if (_filter.accountId) {
q.where('accountId', _filter.accountId); q.where('accountId', _filter.accountId);
} }

View File

@@ -51,7 +51,9 @@ export class GetUncategorizedTransactions {
.onBuild((q) => { .onBuild((q) => {
q.where('accountId', accountId); q.where('accountId', accountId);
q.where('categorized', false); q.where('categorized', false);
q.modify('notExcluded'); q.modify('notExcluded');
q.modify('notPending');
q.withGraphFetched('account'); q.withGraphFetched('account');
q.withGraphFetched('recognizedTransaction.assignAccount'); q.withGraphFetched('recognizedTransaction.assignAccount');

View File

@@ -0,0 +1,72 @@
import { Knex } from 'knex';
import { Inject, Service } from 'typedi';
import HasTenancyService from '../Tenancy/TenancyService';
import UnitOfWork from '../UnitOfWork';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import events from '@/subscribers/events';
import { ServiceError } from '@/exceptions';
import { ERRORS } from './constants';
import {
IPendingTransactionRemovedEventPayload,
IPendingTransactionRemovingEventPayload,
} from '@/interfaces';
@Service()
export class RemovePendingUncategorizedTransaction {
@Inject()
private tenancy: HasTenancyService;
@Inject()
private uow: UnitOfWork;
@Inject()
private eventPublisher: EventPublisher;
/**
* REmoves the pending uncategorized transaction.
* @param {number} tenantId -
* @param {number} uncategorizedTransactionId -
* @param {Knex.Transaction} trx -
* @returns {Promise<void>}
*/
public async removePendingTransaction(
tenantId: number,
uncategorizedTransactionId: number,
trx?: Knex.Transaction
): Promise<void> {
const { UncategorizedCashflowTransaction } = this.tenancy.models(tenantId);
const pendingTransaction = await UncategorizedCashflowTransaction.query(trx)
.findById(uncategorizedTransactionId)
.throwIfNotFound();
if (!pendingTransaction.isPending) {
throw new ServiceError(ERRORS.TRANSACTION_NOT_PENDING);
}
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
await this.eventPublisher.emitAsync(
events.bankTransactions.onPendingRemoving,
{
tenantId,
uncategorizedTransactionId,
pendingTransaction,
trx,
} as IPendingTransactionRemovingEventPayload
);
// Removes the pending uncategorized transaction.
await UncategorizedCashflowTransaction.query(trx)
.findById(uncategorizedTransactionId)
.delete();
await this.eventPublisher.emitAsync(
events.bankTransactions.onPendingRemoved,
{
tenantId,
uncategorizedTransactionId,
pendingTransaction,
trx,
} as IPendingTransactionRemovedEventPayload
);
});
}
}

View File

@@ -0,0 +1,37 @@
import PromisePool from '@supercharge/promise-pool';
import { castArray } from 'lodash';
import { Service, Inject } from 'typedi';
import HasTenancyService from '../Tenancy/TenancyService';
import { UncategorizeCashflowTransaction } from './UncategorizeCashflowTransaction';
@Service()
export class UncategorizeCashflowTransactionsBulk {
@Inject()
private tenancy: HasTenancyService;
@Inject()
private uncategorizeTransaction: UncategorizeCashflowTransaction;
/**
* Uncategorize the given bank transactions in bulk.
* @param {number} tenantId
* @param {number} uncategorizedTransactionId
*/
public async uncategorizeBulk(
tenantId: number,
uncategorizedTransactionId: number | Array<number>
) {
const uncategorizedTransactionIds = castArray(uncategorizedTransactionId);
const result = await PromisePool.withConcurrency(MIGRATION_CONCURRENCY)
.for(uncategorizedTransactionIds)
.process(async (_uncategorizedTransactionId: number, index, pool) => {
await this.uncategorizeTransaction.uncategorize(
tenantId,
_uncategorizedTransactionId
);
});
}
}
const MIGRATION_CONCURRENCY = 1;

View File

@@ -15,10 +15,10 @@ export const ERRORS = {
'UNCATEGORIZED_TRANSACTION_TYPE_INVALID', 'UNCATEGORIZED_TRANSACTION_TYPE_INVALID',
CANNOT_DELETE_TRANSACTION_CONVERTED_FROM_UNCATEGORIZED: CANNOT_DELETE_TRANSACTION_CONVERTED_FROM_UNCATEGORIZED:
'CANNOT_DELETE_TRANSACTION_CONVERTED_FROM_UNCATEGORIZED', 'CANNOT_DELETE_TRANSACTION_CONVERTED_FROM_UNCATEGORIZED',
CANNOT_CATEGORIZE_EXCLUDED_TRANSACTION:
CANNOT_CATEGORIZE_EXCLUDED_TRANSACTION: 'CANNOT_CATEGORIZE_EXCLUDED_TRANSACTION', 'CANNOT_CATEGORIZE_EXCLUDED_TRANSACTION',
TRANSACTION_NOT_CATEGORIZED: 'TRANSACTION_NOT_CATEGORIZED' TRANSACTION_NOT_CATEGORIZED: 'TRANSACTION_NOT_CATEGORIZED',
TRANSACTION_NOT_PENDING: 'TRANSACTION_NOT_PENDING',
}; };
export enum CASHFLOW_DIRECTION { export enum CASHFLOW_DIRECTION {

View File

@@ -1,11 +1,11 @@
import { Inject, Service } from 'typedi'; import { Inject, Service } from 'typedi';
import PromisePool from '@supercharge/promise-pool';
import events from '@/subscribers/events'; import events from '@/subscribers/events';
import HasTenancyService from '@/services/Tenancy/TenancyService'; import HasTenancyService from '@/services/Tenancy/TenancyService';
import { import {
ICashflowTransactionCategorizedPayload, ICashflowTransactionCategorizedPayload,
ICashflowTransactionUncategorizedPayload, ICashflowTransactionUncategorizedPayload,
} from '@/interfaces'; } from '@/interfaces';
import PromisePool from '@supercharge/promise-pool';
@Service() @Service()
export class DecrementUncategorizedTransactionOnCategorize { export class DecrementUncategorizedTransactionOnCategorize {
@@ -36,13 +36,17 @@ export class DecrementUncategorizedTransactionOnCategorize {
public async decrementUnCategorizedTransactionsOnCategorized({ public async decrementUnCategorizedTransactionsOnCategorized({
tenantId, tenantId,
uncategorizedTransactions, uncategorizedTransactions,
trx trx,
}: ICashflowTransactionCategorizedPayload) { }: ICashflowTransactionCategorizedPayload) {
const { Account } = this.tenancy.models(tenantId); const { Account } = this.tenancy.models(tenantId);
await PromisePool.withConcurrency(1) await PromisePool.withConcurrency(1)
.for(uncategorizedTransactions) .for(uncategorizedTransactions)
.process(async (uncategorizedTransaction) => { .process(async (uncategorizedTransaction) => {
// Cannot continue if the transaction is still pending.
if (uncategorizedTransaction.isPending) {
return;
}
await Account.query(trx) await Account.query(trx)
.findById(uncategorizedTransaction.accountId) .findById(uncategorizedTransaction.accountId)
.decrement('uncategorizedTransactions', 1); .decrement('uncategorizedTransactions', 1);
@@ -56,13 +60,17 @@ export class DecrementUncategorizedTransactionOnCategorize {
public async incrementUnCategorizedTransactionsOnUncategorized({ public async incrementUnCategorizedTransactionsOnUncategorized({
tenantId, tenantId,
uncategorizedTransactions, uncategorizedTransactions,
trx trx,
}: ICashflowTransactionUncategorizedPayload) { }: ICashflowTransactionUncategorizedPayload) {
const { Account } = this.tenancy.models(tenantId); const { Account } = this.tenancy.models(tenantId);
await PromisePool.withConcurrency(1) await PromisePool.withConcurrency(1)
.for(uncategorizedTransactions) .for(uncategorizedTransactions)
.process(async (uncategorizedTransaction) => { .process(async (uncategorizedTransaction) => {
// Cannot continue if the transaction is still pending.
if (uncategorizedTransaction.isPending) {
return;
}
await Account.query(trx) await Account.query(trx)
.findById(uncategorizedTransaction.accountId) .findById(uncategorizedTransaction.accountId)
.increment('uncategorizedTransactions', 1); .increment('uncategorizedTransactions', 1);
@@ -82,6 +90,9 @@ export class DecrementUncategorizedTransactionOnCategorize {
if (!uncategorizedTransaction.accountId) return; if (!uncategorizedTransaction.accountId) return;
// Cannot continue if the transaction is still pending.
if (uncategorizedTransaction.isPending) return;
await Account.query(trx) await Account.query(trx)
.findById(uncategorizedTransaction.accountId) .findById(uncategorizedTransaction.accountId)
.increment('uncategorizedTransactions', 1); .increment('uncategorizedTransactions', 1);

View File

@@ -15,6 +15,7 @@ import { ManualJournalsExportable } from '../ManualJournals/ManualJournalExporta
import { CreditNotesExportable } from '../CreditNotes/CreditNotesExportable'; import { CreditNotesExportable } from '../CreditNotes/CreditNotesExportable';
import { VendorCreditsExportable } from '../Purchases/VendorCredits/VendorCreditsExportable'; import { VendorCreditsExportable } from '../Purchases/VendorCredits/VendorCreditsExportable';
import { ItemCategoriesExportable } from '../ItemCategories/ItemCategoriesExportable'; import { ItemCategoriesExportable } from '../ItemCategories/ItemCategoriesExportable';
import { TaxRatesExportable } from '../TaxRates/TaxRatesExportable';
@Service() @Service()
export class ExportableResources { export class ExportableResources {
@@ -46,6 +47,7 @@ export class ExportableResources {
{ resource: 'ManualJournal', exportable: ManualJournalsExportable }, { resource: 'ManualJournal', exportable: ManualJournalsExportable },
{ resource: 'CreditNote', exportable: CreditNotesExportable }, { resource: 'CreditNote', exportable: CreditNotesExportable },
{ resource: 'VendorCredit', exportable: VendorCreditsExportable }, { resource: 'VendorCredit', exportable: VendorCreditsExportable },
{ resource: 'TaxRate', exportable: TaxRatesExportable },
]; ];
/** /**

View File

@@ -16,6 +16,7 @@ import { VendorCreditsImportable } from '../Purchases/VendorCredits/VendorCredit
import { PaymentReceivesImportable } from '../Sales/PaymentReceives/PaymentReceivesImportable'; import { PaymentReceivesImportable } from '../Sales/PaymentReceives/PaymentReceivesImportable';
import { CreditNotesImportable } from '../CreditNotes/CreditNotesImportable'; import { CreditNotesImportable } from '../CreditNotes/CreditNotesImportable';
import { SaleReceiptsImportable } from '../Sales/Receipts/SaleReceiptsImportable'; import { SaleReceiptsImportable } from '../Sales/Receipts/SaleReceiptsImportable';
import { TaxRatesImportable } from '../TaxRates/TaxRatesImportable';
@Service() @Service()
export class ImportableResources { export class ImportableResources {
@@ -47,7 +48,8 @@ export class ImportableResources {
{ resource: 'PaymentReceive', importable: PaymentReceivesImportable }, { resource: 'PaymentReceive', importable: PaymentReceivesImportable },
{ resource: 'VendorCredit', importable: VendorCreditsImportable }, { resource: 'VendorCredit', importable: VendorCreditsImportable },
{ resource: 'CreditNote', importable: CreditNotesImportable }, { resource: 'CreditNote', importable: CreditNotesImportable },
{ resource: 'SaleReceipt', importable: SaleReceiptsImportable } { resource: 'SaleReceipt', importable: SaleReceiptsImportable },
{ resource: 'TaxRate', importable: TaxRatesImportable },
]; ];
public get registry() { public get registry() {

View File

@@ -1,9 +1,10 @@
import { ServiceError } from '@/exceptions'; import { Knex } from 'knex';
import { Inject, Service } from 'typedi'; import { Inject, Service } from 'typedi';
import { difference } from 'lodash';
import { ServiceError } from '@/exceptions';
import HasTenancyService from '../Tenancy/TenancyService'; import HasTenancyService from '../Tenancy/TenancyService';
import { IItemEntryDTO, ITaxRate } from '@/interfaces'; import { IItemEntryDTO, ITaxRate } from '@/interfaces';
import { ERRORS } from './constants'; import { ERRORS } from './constants';
import { difference } from 'lodash';
@Service() @Service()
export class CommandTaxRatesValidators { export class CommandTaxRatesValidators {
@@ -44,11 +45,16 @@ export class CommandTaxRatesValidators {
* Validates the tax code uniquiness. * Validates the tax code uniquiness.
* @param {number} tenantId * @param {number} tenantId
* @param {string} taxCode * @param {string} taxCode
* @param {Knex.Transaction} trx -
*/ */
public async validateTaxCodeUnique(tenantId: number, taxCode: string) { public async validateTaxCodeUnique(
tenantId: number,
taxCode: string,
trx?: Knex.Transaction
) {
const { TaxRate } = this.tenancy.models(tenantId); const { TaxRate } = this.tenancy.models(tenantId);
const foundTaxCode = await TaxRate.query().findOne({ code: taxCode }); const foundTaxCode = await TaxRate.query(trx).findOne({ code: taxCode });
if (foundTaxCode) { if (foundTaxCode) {
throw new ServiceError(ERRORS.TAX_CODE_NOT_UNIQUE); throw new ServiceError(ERRORS.TAX_CODE_NOT_UNIQUE);

View File

@@ -1,3 +1,4 @@
import { Inject, Service } from 'typedi';
import { Knex } from 'knex'; import { Knex } from 'knex';
import { import {
ICreateTaxRateDTO, ICreateTaxRateDTO,
@@ -7,7 +8,6 @@ import {
import UnitOfWork from '../UnitOfWork'; import UnitOfWork from '../UnitOfWork';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher'; import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import HasTenancyService from '../Tenancy/TenancyService'; import HasTenancyService from '../Tenancy/TenancyService';
import { Inject, Service } from 'typedi';
import events from '@/subscribers/events'; import events from '@/subscribers/events';
import { CommandTaxRatesValidators } from './CommandTaxRatesValidators'; import { CommandTaxRatesValidators } from './CommandTaxRatesValidators';
@@ -32,36 +32,41 @@ export class CreateTaxRate {
*/ */
public async createTaxRate( public async createTaxRate(
tenantId: number, tenantId: number,
createTaxRateDTO: ICreateTaxRateDTO createTaxRateDTO: ICreateTaxRateDTO,
trx?: Knex.Transaction
) { ) {
const { TaxRate } = this.tenancy.models(tenantId); const { TaxRate } = this.tenancy.models(tenantId);
// Validates the tax code uniquiness. // Validates the tax code uniquiness.
await this.validators.validateTaxCodeUnique( await this.validators.validateTaxCodeUnique(
tenantId, tenantId,
createTaxRateDTO.code createTaxRateDTO.code,
trx
); );
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => { return this.uow.withTransaction(
// Triggers `onTaxRateCreating` event. tenantId,
await this.eventPublisher.emitAsync(events.taxRates.onCreating, { async (trx: Knex.Transaction) => {
createTaxRateDTO, // Triggers `onTaxRateCreating` event.
tenantId, await this.eventPublisher.emitAsync(events.taxRates.onCreating, {
trx, createTaxRateDTO,
} as ITaxRateCreatingPayload); tenantId,
trx,
} as ITaxRateCreatingPayload);
const taxRate = await TaxRate.query(trx).insertAndFetch({ const taxRate = await TaxRate.query(trx).insertAndFetch({
...createTaxRateDTO, ...createTaxRateDTO,
}); });
// Triggers `onTaxRateCreated` event.
await this.eventPublisher.emitAsync(events.taxRates.onCreated, {
createTaxRateDTO,
taxRate,
tenantId,
trx,
} as ITaxRateCreatedPayload);
// Triggers `onTaxRateCreated` event. return taxRate;
await this.eventPublisher.emitAsync(events.taxRates.onCreated, { },
createTaxRateDTO, trx
taxRate, );
tenantId,
trx,
} as ITaxRateCreatedPayload);
return taxRate;
});
} }
} }

View File

@@ -0,0 +1,18 @@
import { Inject, Service } from 'typedi';
import { Exportable } from '../Export/Exportable';
import { TaxRatesApplication } from './TaxRatesApplication';
@Service()
export class TaxRatesExportable extends Exportable {
@Inject()
private taxRatesApplication: TaxRatesApplication;
/**
* Retrieves the accounts data to exportable sheet.
* @param {number} tenantId
* @returns
*/
public exportable(tenantId: number) {
return this.taxRatesApplication.getTaxRates(tenantId);
}
}

View File

@@ -0,0 +1,18 @@
export const TaxRatesSampleData = [
{
'Tax Name': 'Value Added Tax',
Code: 'VAT-STD',
Rate: '20',
Description: 'Standard VAT rate applied to most goods and services.',
'Is Non Recoverable': 'F',
Active: 'T',
},
{
'Tax Name': 'Luxury Goods Tax',
Code: 'TAX-LUXURY',
Rate: '25',
Description: 'Tax imposed on the sale of luxury items.',
'Is Non Recoverable': 'T',
Active: 'T',
},
];

View File

@@ -0,0 +1,46 @@
import { Inject, Service } from 'typedi';
import { Knex } from 'knex';
import { ICreateTaxRateDTO } from '@/interfaces';
import { CreateTaxRate } from './CreateTaxRate';
import { Importable } from '../Import/Importable';
import { TaxRatesSampleData } from './TaxRatesImportable.SampleData';
@Service()
export class TaxRatesImportable extends Importable {
@Inject()
private createTaxRateService: CreateTaxRate;
/**
* Importing to tax rate creating service.
* @param {number} tenantId -
* @param {ICreateTaxRateDTO} ICreateTaxRateDTO -
* @param {Knex.Transaction} trx -
* @returns
*/
public importable(
tenantId: number,
createAccountDTO: ICreateTaxRateDTO,
trx?: Knex.Transaction
) {
return this.createTaxRateService.createTaxRate(
tenantId,
createAccountDTO,
trx
);
}
/**
* Concurrrency controlling of the importing process.
* @returns {number}
*/
public get concurrency() {
return 1;
}
/**
* Retrieves the sample data that used to download accounts sample sheet.
*/
public sampleData(): any[] {
return TaxRatesSampleData;
}
}

View File

@@ -659,6 +659,9 @@ export default {
onUnexcluding: 'onBankTransactionUnexcluding', onUnexcluding: 'onBankTransactionUnexcluding',
onUnexcluded: 'onBankTransactionUnexcluded', onUnexcluded: 'onBankTransactionUnexcluded',
onPendingRemoving: 'onBankTransactionPendingRemoving',
onPendingRemoved: 'onBankTransactionPendingRemoved',
}, },
bankAccount: { bankAccount: {

View File

@@ -121,7 +121,7 @@
"yup": "^0.28.1" "yup": "^0.28.1"
}, },
"scripts": { "scripts": {
"dev": "PORT=4000 craco start", "dev": "cross-env PORT=4000 craco start",
"build": "craco build", "build": "craco build",
"test": "node scripts/test.js", "test": "node scripts/test.js",
"storybook": "start-storybook -p 6006" "storybook": "start-storybook -p 6006"

View File

@@ -1,6 +1,6 @@
// @ts-nocheck // @ts-nocheck
import React from 'react'; import React from 'react';
import classNames from 'classnames'; import clsx from 'classnames';
import '@/style/components/Details.scss'; import '@/style/components/Details.scss';
@@ -24,7 +24,7 @@ export function DetailsMenu({
}) { }) {
return ( return (
<div <div
className={classNames( className={clsx(
'details-menu', 'details-menu',
{ {
'details-menu--vertical': direction === DIRECTION.VERTICAL, 'details-menu--vertical': direction === DIRECTION.VERTICAL,
@@ -44,16 +44,24 @@ export function DetailsMenu({
/** /**
* Detail item. * Detail item.
*/ */
export function DetailItem({ label, children, name, align, className }) { export function DetailItem({
label,
children,
name,
align,
multiline,
className,
}) {
const { minLabelSize } = useDetailsMenuContext(); const { minLabelSize } = useDetailsMenuContext();
return ( return (
<div <div
className={classNames( className={clsx(
'detail-item', 'detail-item',
{ {
[`detail-item--${name}`]: name, [`detail-item--${name}`]: name,
[`align-${align}`]: align, [`align-${align}`]: align,
[`detail-item--multilines`]: multiline,
}, },
className, className,
)} )}
@@ -66,7 +74,7 @@ export function DetailItem({ label, children, name, align, className }) {
> >
{label} {label}
</div> </div>
<div>{children}</div> <div className={clsx('detail-item__content')}>{children}</div>
</div> </div>
); );
} }

View File

@@ -7,4 +7,7 @@ export const BANK_QUERY_KEY = {
'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', AUTOFILL_CATEGORIZE_BANK_TRANSACTION: 'AUTOFILL_CATEGORIZE_BANK_TRANSACTION',
PENDING_BANK_ACCOUNT_TRANSACTIONS: 'PENDING_BANK_ACCOUNT_TRANSACTIONS',
PENDING_BANK_ACCOUNT_TRANSACTIONS_INFINITY:
'PENDING_BANK_ACCOUNT_TRANSACTIONS_INFINITY',
}; };

View File

@@ -15,6 +15,8 @@ export function MakeJournalFormFooterLeft() {
<FEditableText <FEditableText
name={'description'} name={'description'}
placeholder={intl.get('make_jorunal.decscrption.placeholder')} placeholder={intl.get('make_jorunal.decscrption.placeholder')}
multiline
fastField
/> />
</DescriptionFormGroup> </DescriptionFormGroup>
</React.Fragment> </React.Fragment>

View File

@@ -64,6 +64,7 @@ function AccountTransactionsActionsBar({
uncategorizedTransationsIdsSelected, uncategorizedTransationsIdsSelected,
excludedTransactionsIdsSelected, excludedTransactionsIdsSelected,
openMatchingTransactionAside, openMatchingTransactionAside,
categorizedTransactionsSelected,
// #withBankingActions // #withBankingActions
enableMultipleCategorization, enableMultipleCategorization,
@@ -194,7 +195,7 @@ function AccountTransactionsActionsBar({
// Handle multi select transactions for categorization or matching. // Handle multi select transactions for categorization or matching.
const handleMultipleCategorizingSwitch = (event) => { const handleMultipleCategorizingSwitch = (event) => {
enableMultipleCategorization(event.currentTarget.checked); enableMultipleCategorization(event.currentTarget.checked);
} };
// Handle resume bank feeds syncing. // Handle resume bank feeds syncing.
const handleResumeFeedsSyncing = () => { const handleResumeFeedsSyncing = () => {
openAlert('resume-feeds-syncing-bank-accounnt', { openAlert('resume-feeds-syncing-bank-accounnt', {
@@ -208,6 +209,13 @@ function AccountTransactionsActionsBar({
}); });
}; };
// Handles uncategorize the categorized transactions in bulk.
const handleUncategorizeCategorizedBulkBtnClick = () => {
openAlert('uncategorize-transactions-bulk', {
uncategorizeTransactionsIds: categorizedTransactionsSelected,
});
};
return ( return (
<DashboardActionsBar> <DashboardActionsBar>
<NavbarGroup> <NavbarGroup>
@@ -297,6 +305,14 @@ function AccountTransactionsActionsBar({
disabled={isUnexcludingLoading} disabled={isUnexcludingLoading}
/> />
)} )}
{!isEmpty(categorizedTransactionsSelected) && (
<Button
text={'Uncategorize'}
onClick={handleUncategorizeCategorizedBulkBtnClick}
intent={Intent.DANGER}
minimal
/>
)}
</NavbarGroup> </NavbarGroup>
<NavbarGroup align={Alignment.RIGHT}> <NavbarGroup align={Alignment.RIGHT}>
@@ -379,10 +395,12 @@ export default compose(
uncategorizedTransationsIdsSelected, uncategorizedTransationsIdsSelected,
excludedTransactionsIdsSelected, excludedTransactionsIdsSelected,
openMatchingTransactionAside, openMatchingTransactionAside,
categorizedTransactionsSelected,
}) => ({ }) => ({
uncategorizedTransationsIdsSelected, uncategorizedTransationsIdsSelected,
excludedTransactionsIdsSelected, excludedTransactionsIdsSelected,
openMatchingTransactionAside, openMatchingTransactionAside,
categorizedTransactionsSelected,
}), }),
), ),
withBankingActions, withBankingActions,

View File

@@ -22,10 +22,11 @@ import { useMemorizedColumnsWidths } from '@/hooks';
import { useAccountTransactionsColumns, ActionsMenu } from './components'; import { useAccountTransactionsColumns, ActionsMenu } from './components';
import { useAccountTransactionsAllContext } from './AccountTransactionsAllBoot'; import { useAccountTransactionsAllContext } from './AccountTransactionsAllBoot';
import { useUnmatchMatchedUncategorizedTransaction } from '@/hooks/query/bank-rules'; import { useUnmatchMatchedUncategorizedTransaction } from '@/hooks/query/bank-rules';
import { useUncategorizeTransaction } from '@/hooks/query';
import { handleCashFlowTransactionType } from './utils'; import { handleCashFlowTransactionType } from './utils';
import { compose } from '@/utils'; import { compose } from '@/utils';
import { useUncategorizeTransaction } from '@/hooks/query'; import { withBankingActions } from '../withBankingActions';
/** /**
* Account transactions data table. * Account transactions data table.
@@ -39,6 +40,9 @@ function AccountTransactionsDataTable({
// #withDrawerActions // #withDrawerActions
openDrawer, openDrawer,
// #withBankingActions
setCategorizedTransactionsSelected,
}) { }) {
// Retrieve table columns. // Retrieve table columns.
const columns = useAccountTransactionsColumns(); const columns = useAccountTransactionsColumns();
@@ -97,6 +101,15 @@ function AccountTransactionsDataTable({
}); });
}; };
// Handle selected rows change.
const handleSelectedRowsChange = (selected) => {
const selectedIds = selected
?.filter((row) => row.original.uncategorized_transaction_id)
?.map((row) => row.original.uncategorized_transaction_id);
setCategorizedTransactionsSelected(selectedIds);
};
return ( return (
<CashflowTransactionsTable <CashflowTransactionsTable
noInitialFetch={true} noInitialFetch={true}
@@ -119,6 +132,8 @@ function AccountTransactionsDataTable({
vListOverscanRowCount={0} vListOverscanRowCount={0}
initialColumnsWidths={initialColumnsWidths} initialColumnsWidths={initialColumnsWidths}
onColumnResizing={handleColumnResizing} onColumnResizing={handleColumnResizing}
selectionColumn={true}
onSelectedRowsChange={handleSelectedRowsChange}
noResults={<T id={'cash_flow.account_transactions.no_results'} />} noResults={<T id={'cash_flow.account_transactions.no_results'} />}
className="table-constrant" className="table-constrant"
payload={{ payload={{
@@ -136,6 +151,7 @@ export default compose(
})), })),
withAlertsActions, withAlertsActions,
withDrawerActions, withDrawerActions,
withBankingActions,
)(AccountTransactionsDataTable); )(AccountTransactionsDataTable);
const DashboardConstrantTable = styled(DataTable)` const DashboardConstrantTable = styled(DataTable)`

View File

@@ -20,7 +20,8 @@ export function AccountTransactionsFilterTabs() {
const hasUncategorizedTransx = useMemo( const hasUncategorizedTransx = useMemo(
() => () =>
bankAccountMetaSummary?.totalUncategorizedTransactions > 0 || bankAccountMetaSummary?.totalUncategorizedTransactions > 0 ||
bankAccountMetaSummary?.totalExcludedTransactions > 0, bankAccountMetaSummary?.totalExcludedTransactions > 0 ||
bankAccountMetaSummary?.totalPendingTransactions > 0,
[bankAccountMetaSummary], [bankAccountMetaSummary],
); );

View File

@@ -1,4 +1,6 @@
// @ts-nocheck // @ts-nocheck
import * as R from 'ramda';
import { useMemo } from 'react';
import { useAppQueryString } from '@/hooks'; import { useAppQueryString } from '@/hooks';
import { Group } from '@/components'; import { Group } from '@/components';
import { useAccountTransactionsContext } from './AccountTransactionsProvider'; import { useAccountTransactionsContext } from './AccountTransactionsProvider';
@@ -12,31 +14,49 @@ export function AccountTransactionsUncategorizeFilter() {
bankAccountMetaSummary?.totalUncategorizedTransactions; bankAccountMetaSummary?.totalUncategorizedTransactions;
const totalRecognized = bankAccountMetaSummary?.totalRecognizedTransactions; const totalRecognized = bankAccountMetaSummary?.totalRecognizedTransactions;
const totalPending = bankAccountMetaSummary?.totalPendingTransactions;
const handleTabsChange = (value) => { const handleTabsChange = (value) => {
setLocationQuery({ uncategorizedFilter: value }); setLocationQuery({ uncategorizedFilter: value });
}; };
const options = useMemo(
() =>
R.when(
() => totalPending > 0,
R.append({
value: 'pending',
label: (
<>
Pending <strong>({totalPending})</strong>
</>
),
}),
)([
{
value: 'all',
label: (
<>
All <strong>({totalUncategorized})</strong>
</>
),
},
{
value: 'recognized',
label: (
<>
Recognized <strong>({totalRecognized})</strong>
</>
),
},
]),
[totalPending, totalRecognized, totalUncategorized],
);
return ( return (
<Group position={'apart'}> <Group position={'apart'}>
<TagsControl <TagsControl
options={[ options={options}
{
value: 'all',
label: (
<>
All <strong>({totalUncategorized})</strong>
</>
),
},
{
value: 'recognized',
label: (
<>
Recognized <strong>({totalRecognized})</strong>
</>
),
},
]}
value={locationQuery?.uncategorizedFilter || 'all'} value={locationQuery?.uncategorizedFilter || 'all'}
onValueChange={handleTabsChange} onValueChange={handleTabsChange}
/> />

View File

@@ -54,6 +54,12 @@ const AccountUncategorizedTransactions = lazy(() =>
).then((module) => ({ default: module.AccountUncategorizedTransactionsAll })), ).then((module) => ({ default: module.AccountUncategorizedTransactionsAll })),
); );
const PendingTransactions = lazy(() =>
import('./PendingTransactions/PendingTransactions').then((module) => ({
default: module.PendingTransactions,
})),
);
/** /**
* Switches between the account transactions tables. * Switches between the account transactions tables.
* @returns {React.ReactNode} * @returns {React.ReactNode}
@@ -70,6 +76,8 @@ function AccountTransactionsSwitcher() {
case 'all': case 'all':
default: default:
return <AccountUncategorizedTransactions />; return <AccountUncategorizedTransactions />;
case 'pending':
return <PendingTransactions />;
} }
} }

View File

@@ -0,0 +1,14 @@
import React from 'react';
import { AccountTransactionsCard } from '../UncategorizedTransactions/AccountTransactionsCard';
import { PendingTransactionsBoot } from './PendingTransactionsTableBoot';
import { PendingTransactionsDataTable } from './PendingTransactionsTable';
export function PendingTransactions() {
return (
<PendingTransactionsBoot>
<AccountTransactionsCard>
<PendingTransactionsDataTable />
</AccountTransactionsCard>
</PendingTransactionsBoot>
);
}

View File

@@ -0,0 +1,107 @@
// @ts-nocheck
import React from 'react';
import clsx from 'classnames';
import styled from 'styled-components';
import {
DataTable,
TableFastCell,
TableSkeletonRows,
TableSkeletonHeader,
TableVirtualizedListRows,
} from '@/components';
import withSettings from '@/containers/Settings/withSettings';
import { withBankingActions } from '../../withBankingActions';
import { useAccountTransactionsContext } from '../AccountTransactionsProvider';
import { usePendingTransactionsContext } from './PendingTransactionsTableBoot';
import { usePendingTransactionsTableColumns } from './_hooks';
import { compose } from '@/utils';
/**
* Account transactions data table.
*/
function PendingTransactionsDataTableRoot({
// #withSettings
cashflowTansactionsTableSize,
}) {
// Retrieve table columns.
const columns = usePendingTransactionsTableColumns();
const { scrollableRef } = useAccountTransactionsContext();
// Retrieve list context.
const { pendingTransactions, isPendingTransactionsLoading } =
usePendingTransactionsContext();
return (
<CashflowTransactionsTable
noInitialFetch={true}
columns={columns}
data={pendingTransactions || []}
sticky={true}
loading={isPendingTransactionsLoading}
headerLoading={isPendingTransactionsLoading}
TableCellRenderer={TableFastCell}
TableLoadingRenderer={TableSkeletonRows}
TableRowsRenderer={TableVirtualizedListRows}
TableHeaderSkeletonRenderer={TableSkeletonHeader}
// #TableVirtualizedListRows props.
vListrowHeight={cashflowTansactionsTableSize === 'small' ? 32 : 40}
vListOverscanRowCount={0}
noResults={'There is no pending transactions in the current account.'}
windowScrollerProps={{ scrollElement: scrollableRef }}
className={clsx('table-constrant')}
/>
);
}
export const PendingTransactionsDataTable = compose(
withSettings(({ cashflowTransactionsSettings }) => ({
cashflowTansactionsTableSize: cashflowTransactionsSettings?.tableSize,
})),
withBankingActions,
)(PendingTransactionsDataTableRoot);
const DashboardConstrantTable = styled(DataTable)`
.table {
.thead {
.th {
background: #fff;
text-transform: uppercase;
letter-spacing: 1px;
font-size: 13px;i
font-weight: 500;
}
}
.tbody {
.tr:last-child .td {
border-bottom: 0;
}
}
}
`;
const CashflowTransactionsTable = styled(DashboardConstrantTable)`
.table .tbody {
.tbody-inner .tr.no-results {
.td {
padding: 2rem 0;
font-size: 14px;
color: #888;
font-weight: 400;
border-bottom: 0;
}
}
.tbody-inner {
.tr .td:not(:first-child) {
border-left: 1px solid #e6e6e6;
}
.td-description {
color: #5f6b7c;
}
}
}
`;

View File

@@ -0,0 +1,72 @@
// @ts-nocheck
import React from 'react';
import { flatten, map } from 'lodash';
import { IntersectionObserver } from '@/components';
import { useAccountTransactionsContext } from '../AccountTransactionsProvider';
import { usePendingBankTransactionsInfinity } from '@/hooks/query/bank-rules';
const PendingTransactionsContext = React.createContext();
function flattenInfinityPagesData(data) {
return flatten(map(data.pages, (page) => page.data));
}
/**
* Account pending transctions provider.
*/
function PendingTransactionsBoot({ children }) {
const { accountId } = useAccountTransactionsContext();
// Fetches the pending transactions.
const {
data: pendingTransactionsPage,
isFetching: isPendingTransactionFetching,
isLoading: isPendingTransactionsLoading,
isSuccess: isPendingTransactionsSuccess,
isFetchingNextPage: isPendingTransactionFetchNextPage,
fetchNextPage: fetchNextPendingTransactionsPage,
hasNextPage: hasPendingTransactionsNextPage,
} = usePendingBankTransactionsInfinity({
account_id: accountId,
page_size: 50,
});
// Memorized the cashflow account transactions.
const pendingTransactions = React.useMemo(
() =>
isPendingTransactionsSuccess
? flattenInfinityPagesData(pendingTransactionsPage)
: [],
[pendingTransactionsPage, isPendingTransactionsSuccess],
);
// Handle the observer ineraction.
const handleObserverInteract = React.useCallback(() => {
if (!isPendingTransactionFetching && hasPendingTransactionsNextPage) {
fetchNextPendingTransactionsPage();
}
}, [
isPendingTransactionFetching,
hasPendingTransactionsNextPage,
fetchNextPendingTransactionsPage,
]);
// Provider payload.
const provider = {
pendingTransactions,
isPendingTransactionFetching,
isPendingTransactionsLoading,
};
return (
<PendingTransactionsContext.Provider value={provider}>
{children}
<IntersectionObserver
onIntersect={handleObserverInteract}
enabled={!isPendingTransactionFetchNextPage}
/>
</PendingTransactionsContext.Provider>
);
}
const usePendingTransactionsContext = () =>
React.useContext(PendingTransactionsContext);
export { PendingTransactionsBoot, usePendingTransactionsContext };

View File

@@ -0,0 +1,65 @@
import { useMemo } from 'react';
import intl from 'react-intl-universal';
/**
* Retrieve account pending transctions table columns.
*/
export function usePendingTransactionsTableColumns() {
return useMemo(
() => [
{
id: 'date',
Header: intl.get('date'),
accessor: 'formatted_date',
width: 40,
clickable: true,
textOverview: true,
},
{
id: 'description',
Header: 'Description',
accessor: 'description',
width: 160,
textOverview: true,
clickable: true,
},
{
id: 'payee',
Header: 'Payee',
accessor: 'payee',
width: 60,
clickable: true,
textOverview: true,
},
{
id: 'reference_number',
Header: 'Ref.#',
accessor: 'reference_no',
width: 50,
clickable: true,
textOverview: true,
},
{
id: 'deposit',
Header: intl.get('cash_flow.label.deposit'),
accessor: 'formatted_deposit_amount',
width: 40,
className: 'deposit',
textOverview: true,
align: 'right',
clickable: true,
},
{
id: 'withdrawal',
Header: intl.get('cash_flow.label.withdrawal'),
accessor: 'formatted_withdrawal_amount',
className: 'withdrawal',
width: 40,
textOverview: true,
align: 'right',
clickable: true,
},
],
[],
);
}

View File

@@ -0,0 +1,69 @@
// @ts-nocheck
import React from 'react';
import { Intent, Alert } from '@blueprintjs/core';
import { AppToaster, FormattedMessage as T } from '@/components';
import withAlertStoreConnect from '@/containers/Alert/withAlertStoreConnect';
import withAlertActions from '@/containers/Alert/withAlertActions';
import { useUncategorizeTransactionsBulkAction } from '@/hooks/query/bank-transactions';
import { compose } from '@/utils';
/**
* Uncategorize bank account transactions in build alert.
*/
function UncategorizeBankTransactionsBulkAlert({
name,
// #withAlertStoreConnect
isOpen,
payload: { uncategorizeTransactionsIds },
// #withAlertActions
closeAlert,
}) {
const { mutateAsync: uncategorizeTransactions, isLoading } =
useUncategorizeTransactionsBulkAction();
// Handle activate item alert cancel.
const handleCancelActivateItem = () => {
closeAlert(name);
};
// Handle confirm item activated.
const handleConfirmItemActivate = () => {
uncategorizeTransactions({ ids: uncategorizeTransactionsIds })
.then(() => {
AppToaster.show({
message: 'The bank feeds of the bank account has been resumed.',
intent: Intent.SUCCESS,
});
})
.catch((error) => {})
.finally(() => {
closeAlert(name);
});
};
return (
<Alert
cancelButtonText={<T id={'cancel'} />}
confirmButtonText={'Uncategorize Transactions'}
intent={Intent.DANGER}
isOpen={isOpen}
onCancel={handleCancelActivateItem}
loading={isLoading}
onConfirm={handleConfirmItemActivate}
>
<p>
Are you sure want to uncategorize the selected bank transactions, this
action is not reversible but you can always categorize them again?
</p>
</Alert>
);
}
export default compose(
withAlertStoreConnect(),
withAlertActions,
)(UncategorizeBankTransactionsBulkAlert);

View File

@@ -9,6 +9,10 @@ const PauseFeedsBankAccountAlert = React.lazy(
() => import('./PauseFeedsBankAccount'), () => import('./PauseFeedsBankAccount'),
); );
const UncategorizeTransactionsBulkAlert = React.lazy(
() => import('./UncategorizeBankTransactionsBulkAlert'),
);
/** /**
* Bank account alerts. * Bank account alerts.
*/ */
@@ -21,4 +25,8 @@ export const BankAccountAlerts = [
name: 'pause-feeds-syncing-bank-accounnt', name: 'pause-feeds-syncing-bank-accounnt',
component: PauseFeedsBankAccountAlert, component: PauseFeedsBankAccountAlert,
}, },
{
name: 'uncategorize-transactions-bulk',
component: UncategorizeTransactionsBulkAlert,
},
]; ];

View File

@@ -1,6 +1,8 @@
import { useAccounts, useBranches } from '@/hooks/query';
import { Spinner } from '@blueprintjs/core';
import React from 'react'; import React from 'react';
import { Spinner } from '@blueprintjs/core';
import { Features } from '@/constants';
import { useAccounts, useBranches } from '@/hooks/query';
import { useFeatureCan } from '@/hooks/state';
interface MatchingReconcileTransactionBootProps { interface MatchingReconcileTransactionBootProps {
children: React.ReactNode; children: React.ReactNode;
@@ -15,8 +17,17 @@ const MatchingReconcileTransactionBootContext =
export function MatchingReconcileTransactionBoot({ export function MatchingReconcileTransactionBoot({
children, children,
}: MatchingReconcileTransactionBootProps) { }: MatchingReconcileTransactionBootProps) {
// Detarmines whether the feature is enabled.
const { featureCan } = useFeatureCan();
const isBranchFeatureCan = featureCan(Features.Branches);
const { data: accounts, isLoading: isAccountsLoading } = useAccounts({}, {}); const { data: accounts, isLoading: isAccountsLoading } = useAccounts({}, {});
const { data: branches, isLoading: isBranchesLoading } = useBranches({}, {}); const { data: branches, isLoading: isBranchesLoading } = useBranches(
{},
{
enabled: isBranchFeatureCan,
},
);
const provider = { const provider = {
accounts, accounts,

View File

@@ -10,6 +10,7 @@ import {
Box, Box,
BranchSelect, BranchSelect,
FDateInput, FDateInput,
FeatureCan,
FFormGroup, FFormGroup,
FInputGroup, FInputGroup,
FMoneyInputGroup, FMoneyInputGroup,
@@ -30,6 +31,7 @@ import { useAccountTransactionsContext } from '../../AccountTransactions/Account
import { MatchingReconcileFormSchema } from './MatchingReconcileTransactionForm.schema'; import { MatchingReconcileFormSchema } from './MatchingReconcileTransactionForm.schema';
import { initialValues, transformToReq } from './_utils'; import { initialValues, transformToReq } from './_utils';
import { withBanking } from '../../withBanking'; import { withBanking } from '../../withBanking';
import { Features } from '@/constants';
interface MatchingReconcileTransactionFormProps { interface MatchingReconcileTransactionFormProps {
onSubmitSuccess?: (values: any) => void; onSubmitSuccess?: (values: any) => void;
@@ -205,26 +207,28 @@ function CreateReconcileTransactionContent() {
<FInputGroup name={'reference_no'} /> <FInputGroup name={'reference_no'} />
</FFormGroup> </FFormGroup>
<FFormGroup <FeatureCan feature={Features.Branches}>
name={'branchId'} <FFormGroup
label={'Branch'}
labelInfo={<Tag minimal>Required</Tag>}
fastField
>
<BranchSelect
name={'branchId'} name={'branchId'}
branches={branches} label={'Branch'}
popoverProps={{ labelInfo={<Tag minimal>Required</Tag>}
minimal: false,
position: Position.LEFT,
modifiers: {
preventOverflow: { enabled: true },
},
boundary: 'viewport',
}}
fastField fastField
/> >
</FFormGroup> <BranchSelect
name={'branchId'}
branches={branches}
popoverProps={{
minimal: false,
position: Position.LEFT,
modifiers: {
preventOverflow: { enabled: true },
},
boundary: 'viewport',
}}
fastField
/>
</FFormGroup>
</FeatureCan>
</Box> </Box>
); );
} }

View File

@@ -1,8 +1,8 @@
import { useMemo } from 'react';
import { useFormikContext } from 'formik'; import { useFormikContext } from 'formik';
import { round } from 'lodash';
import { MatchingTransactionFormValues } from './types'; import { MatchingTransactionFormValues } from './types';
import { useMatchingTransactionBoot } from './MatchingTransactionBoot'; import { useMatchingTransactionBoot } from './MatchingTransactionBoot';
import { useCategorizeTransactionTabsBoot } from './CategorizeTransactionTabsBoot';
import { useMemo } from 'react';
export const transformToReq = ( export const transformToReq = (
values: MatchingTransactionFormValues, values: MatchingTransactionFormValues,
@@ -38,7 +38,7 @@ export const useGetPendingAmountMatched = () => {
); );
const pendingAmount = totalPending - totalMatchedAmount; const pendingAmount = totalPending - totalMatchedAmount;
return pendingAmount; return round(pendingAmount, 2);
}, [totalPending, perfectMatches, possibleMatches, values]); }, [totalPending, perfectMatches, possibleMatches, values]);
}; };

View File

@@ -22,6 +22,9 @@ export const withBanking = (mapState) => {
transactionsToCategorizeIdsSelected: transactionsToCategorizeIdsSelected:
state.plaid.transactionsToCategorizeSelected, state.plaid.transactionsToCategorizeSelected,
categorizedTransactionsSelected:
state.plaid.categorizedTransactionsSelected,
}; };
return mapState ? mapState(mapped, state, props) : mapped; return mapState ? mapState(mapped, state, props) : mapped;
}; };

View File

@@ -13,6 +13,8 @@ import {
enableMultipleCategorization, enableMultipleCategorization,
addTransactionsToCategorizeSelected, addTransactionsToCategorizeSelected,
removeTransactionsToCategorizeSelected, removeTransactionsToCategorizeSelected,
setCategorizedTransactionsSelected,
resetCategorizedTransactionsSelected,
} from '@/store/banking/banking.reducer'; } from '@/store/banking/banking.reducer';
export interface WithBankingActionsProps { export interface WithBankingActionsProps {
@@ -35,6 +37,9 @@ export interface WithBankingActionsProps {
resetTransactionsToCategorizeSelected: () => void; resetTransactionsToCategorizeSelected: () => void;
enableMultipleCategorization: (enable: boolean) => void; enableMultipleCategorization: (enable: boolean) => void;
setCategorizedTransactionsSelected: (ids: Array<string | number>) => void;
resetCategorizedTransactionsSelected: () => void;
} }
const mapDipatchToProps = (dispatch: any): WithBankingActionsProps => ({ const mapDipatchToProps = (dispatch: any): WithBankingActionsProps => ({
@@ -120,6 +125,19 @@ const mapDipatchToProps = (dispatch: any): WithBankingActionsProps => ({
*/ */
enableMultipleCategorization: (enable: boolean) => enableMultipleCategorization: (enable: boolean) =>
dispatch(enableMultipleCategorization({ enable })), dispatch(enableMultipleCategorization({ enable })),
/**
* Sets the selected ids of the categorized transactions.
* @param {Array<string | number>} ids
*/
setCategorizedTransactionsSelected: (ids: Array<string | number>) =>
dispatch(setCategorizedTransactionsSelected({ ids })),
/**
* Resets the selected categorized transcations.
*/
resetCategorizedTransactionsSelected: () =>
dispatch(resetCategorizedTransactionsSelected()),
}); });
export const withBankingActions = connect< export const withBankingActions = connect<

View File

@@ -14,4 +14,5 @@ export const ExportResources = [
{ value: 'bill', text: 'Bills' }, { value: 'bill', text: 'Bills' },
{ value: 'bill_payment', text: 'Bill Payments' }, { value: 'bill_payment', text: 'Bill Payments' },
{ value: 'vendor_credit', text: 'Vendor Credits' }, { value: 'vendor_credit', text: 'Vendor Credits' },
{ value: 'tax_rate', text: 'Tax Rate' },
]; ];

View File

@@ -20,7 +20,9 @@ export default function BillDetailFooter() {
<CommercialDocFooter> <CommercialDocFooter>
<DetailsMenu direction={'horizantal'} minLabelSize={'180px'}> <DetailsMenu direction={'horizantal'} minLabelSize={'180px'}>
<If condition={bill.note}> <If condition={bill.note}>
<DetailItem label={<T id={'note'} />}>{bill.note}</DetailItem> <DetailItem label={<T id={'note'} />} multiline>
{bill.note}
</DetailItem>
</If> </If>
</DetailsMenu> </DetailsMenu>
</CommercialDocFooter> </CommercialDocFooter>

View File

@@ -9,7 +9,10 @@ export function CashflowTransactionDrawerFooter() {
return ( return (
<CommercialDocFooter> <CommercialDocFooter>
<DetailsMenu direction={'horizantal'} minLabelSize={'180px'}> <DetailsMenu direction={'horizantal'} minLabelSize={'180px'}>
<DetailItem label={<T id={'cash_flow.drawer.label.statement'} />}> <DetailItem
label={<T id={'cash_flow.drawer.label.statement'} />}
multiline
>
{cashflowTransaction.description} {cashflowTransaction.description}
</DetailItem> </DetailItem>
</DetailsMenu> </DetailsMenu>

View File

@@ -21,11 +21,15 @@ export default function CreditNoteDetailFooter() {
<CommercialDocFooter> <CommercialDocFooter>
<DetailsMenu direction={'horizantal'} minLabelSize={'180px'}> <DetailsMenu direction={'horizantal'} minLabelSize={'180px'}>
<If condition={creditNote.terms_conditions}> <If condition={creditNote.terms_conditions}>
<DetailItem label={<T id={'note'} />} children={creditNote.note} /> <DetailItem
label={<T id={'note'} />}
children={creditNote.note}
multiline
/>
</If> </If>
<If condition={creditNote.terms_conditions}> <If condition={creditNote.terms_conditions}>
<DetailItem label={<T id={'terms_conditions'} />}> <DetailItem label={<T id={'terms_conditions'} />} multiline>
{creditNote.terms_conditions} {creditNote.terms_conditions}
</DetailItem> </DetailItem>
</If> </If>

View File

@@ -16,17 +16,20 @@ import { useEstimateDetailDrawerContext } from './EstimateDetailDrawerProvider';
*/ */
export default function EstimateDetailFooter() { export default function EstimateDetailFooter() {
const { estimate } = useEstimateDetailDrawerContext(); const { estimate } = useEstimateDetailDrawerContext();
return ( return (
<CommercialDocFooter> <CommercialDocFooter>
<DetailsMenu direction={'horizantal'} minLabelSize={'180px'}> <DetailsMenu direction={'horizantal'} minLabelSize={'180px'}>
<If condition={estimate.terms_conditions}> <If condition={estimate.terms_conditions}>
<DetailItem label={<T id={'estimate.details.terms_conditions'} />}> <DetailItem
label={<T id={'estimate.details.terms_conditions'} />}
multiline
>
{estimate.terms_conditions} {estimate.terms_conditions}
</DetailItem> </DetailItem>
</If> </If>
<If condition={estimate.note}> <If condition={estimate.note}>
<DetailItem label={<T id={'estimate.details.note'} />}> <DetailItem label={<T id={'estimate.details.note'} />} multiline>
{estimate.note} {estimate.note}
</DetailItem> </DetailItem>
</If> </If>

View File

@@ -23,13 +23,16 @@ export function InvoiceDetailFooter() {
<CommercialDocFooter> <CommercialDocFooter>
<DetailsMenu direction={'horizantal'} minLabelSize={'180px'}> <DetailsMenu direction={'horizantal'} minLabelSize={'180px'}>
<If condition={invoice.terms_conditions}> <If condition={invoice.terms_conditions}>
<DetailItem label={<T id={'terms_conditions'} />}> <DetailItem label={<T id={'terms_conditions'} />} multiline>
{invoice.terms_conditions} {invoice.terms_conditions}
</DetailItem> </DetailItem>
</If> </If>
<If condition={invoice.invoice_message}> <If condition={invoice.invoice_message}>
<DetailItem label={<T id={'invoice.details.invoice_message'} />}> <DetailItem
label={<T id={'invoice.details.invoice_message'} />}
multiline
>
{invoice.invoice_message} {invoice.invoice_message}
</DetailItem> </DetailItem>
</If> </If>

View File

@@ -20,7 +20,10 @@ export function PaymentMadeDetailFooter() {
<CommercialDocFooter> <CommercialDocFooter>
<DetailsMenu direction={'horizantal'} minLabelSize={'180px'}> <DetailsMenu direction={'horizantal'} minLabelSize={'180px'}>
<If condition={paymentMade.statement}> <If condition={paymentMade.statement}>
<DetailItem label={<T id={'payment_made.details.statement'} />}> <DetailItem
label={<T id={'payment_made.details.statement'} />}
multiline
>
{paymentMade.statement} {paymentMade.statement}
</DetailItem> </DetailItem>
</If> </If>

View File

@@ -16,12 +16,15 @@ import { usePaymentReceiveDetailContext } from './PaymentReceiveDetailProvider';
*/ */
export default function PaymentReceiveDetailFooter() { export default function PaymentReceiveDetailFooter() {
const { paymentReceive } = usePaymentReceiveDetailContext(); const { paymentReceive } = usePaymentReceiveDetailContext();
return ( return (
<CommercialDocFooter> <CommercialDocFooter>
<DetailsMenu direction={'horizantal'} minLabelSize={'180px'}> <DetailsMenu direction={'horizantal'} minLabelSize={'180px'}>
<If condition={paymentReceive.statement}> <If condition={paymentReceive.statement}>
<DetailItem label={<T id={'payment_receive.details.statement'} />}> <DetailItem
label={<T id={'payment_receive.details.statement'} />}
multiline
>
{paymentReceive.statement} {paymentReceive.statement}
</DetailItem> </DetailItem>
</If> </If>

View File

@@ -21,12 +21,15 @@ export default function ReceiptDetailFooter() {
<CommercialDocFooter> <CommercialDocFooter>
<DetailsMenu direction={'horizantal'} minLabelSize={'180px'}> <DetailsMenu direction={'horizantal'} minLabelSize={'180px'}>
<If condition={receipt.statement}> <If condition={receipt.statement}>
<DetailItem label={<T id={'receipt.details.statement'} />}> <DetailItem label={<T id={'receipt.details.statement'} />} multiline>
{receipt.statement} {receipt.statement}
</DetailItem> </DetailItem>
</If> </If>
<If condition={receipt.receipt_message}> <If condition={receipt.receipt_message}>
<DetailItem label={<T id={'receipt.details.receipt_message'} />}> <DetailItem
label={<T id={'receipt.details.receipt_message'} />}
multiline
>
{receipt.receipt_message} {receipt.receipt_message}
</DetailItem> </DetailItem>
</If> </If>

View File

@@ -16,7 +16,11 @@ export function VendorCreditDetailFooter() {
<CommercialDocFooter> <CommercialDocFooter>
<DetailsMenu direction={'horizantal'} minLabelSize={'150px'}> <DetailsMenu direction={'horizantal'} minLabelSize={'150px'}>
<If condition={vendorCredit.note}> <If condition={vendorCredit.note}>
<DetailItem label={<T id={'note'} />} children={vendorCredit.note} /> <DetailItem
label={<T id={'note'} />}
children={vendorCredit.note}
multiline
/>
</If> </If>
</DetailsMenu> </DetailsMenu>
</CommercialDocFooter> </CommercialDocFooter>

View File

@@ -14,6 +14,8 @@ export function ExpenseFormFooterLeft() {
<FEditableText <FEditableText
name={'description'} name={'description'}
placeholder={<T id={'expenses.decscrption.placeholder'} />} placeholder={<T id={'expenses.decscrption.placeholder'} />}
multiline
fastField
/> />
</DescriptionFormGroup> </DescriptionFormGroup>
</React.Fragment> </React.Fragment>

View File

@@ -15,6 +15,8 @@ export function BillFormFooterLeft() {
<FEditableText <FEditableText
name={'note'} name={'note'}
placeholder={intl.get('bill_form.label.note.placeholder')} placeholder={intl.get('bill_form.label.note.placeholder')}
fastField
multiline
/> />
</TermsConditsFormGroup> </TermsConditsFormGroup>
</React.Fragment> </React.Fragment>

View File

@@ -15,6 +15,8 @@ export function VendorCreditNoteFormFooterLeft() {
<FEditableText <FEditableText
name={'note'} name={'note'}
placeholder={intl.get('vendor_credit_form.note.placeholder')} placeholder={intl.get('vendor_credit_form.note.placeholder')}
multiline
fastField
/> />
</TermsConditsFormGroup> </TermsConditsFormGroup>
</React.Fragment> </React.Fragment>

View File

@@ -20,7 +20,8 @@ export function PaymentMadeFormFooterLeft() {
<FEditableText <FEditableText
name={'statement'} name={'statement'}
placeholder={intl.get('payment_made.form.internal_note.placeholder')} placeholder={intl.get('payment_made.form.internal_note.placeholder')}
fastField={true} fastField
multiline
/> />
</InternalNoteFormGroup> </InternalNoteFormGroup>
</React.Fragment> </React.Fragment>

View File

@@ -15,6 +15,8 @@ export function CreditNoteFormFooterLeft() {
<FEditableText <FEditableText
name={'note'} name={'note'}
placeholder={intl.get('credit_note.label_customer_note.placeholder')} placeholder={intl.get('credit_note.label_customer_note.placeholder')}
multiline
fastField
/> />
</CreditNoteMsgFormGroup> </CreditNoteMsgFormGroup>
{/* --------- Terms and conditions --------- */} {/* --------- Terms and conditions --------- */}
@@ -27,6 +29,8 @@ export function CreditNoteFormFooterLeft() {
placeholder={intl.get( placeholder={intl.get(
'credit_note.label_terms_and_conditions.placeholder', 'credit_note.label_terms_and_conditions.placeholder',
)} )}
multiline
fastField
/> />
</TermsConditsFormGroup> </TermsConditsFormGroup>
</React.Fragment> </React.Fragment>

View File

@@ -16,6 +16,8 @@ export function EstimateFormFooterLeft() {
<FEditableText <FEditableText
name={'note'} name={'note'}
placeholder={intl.get('estimate_form.customer_note.placeholder')} placeholder={intl.get('estimate_form.customer_note.placeholder')}
multiline
fastField
/> />
</EstimateMsgFormGroup> </EstimateMsgFormGroup>
@@ -26,7 +28,11 @@ export function EstimateFormFooterLeft() {
> >
<FEditableText <FEditableText
name={'terms_conditions'} name={'terms_conditions'}
placeholder={intl.get('estimate_form.terms_and_conditions.placeholder')} placeholder={intl.get(
'estimate_form.terms_and_conditions.placeholder',
)}
multiline
fastField
/> />
</TermsConditsFormGroup> </TermsConditsFormGroup>
</React.Fragment> </React.Fragment>

View File

@@ -15,6 +15,8 @@ export function InvoiceFormFooterLeft() {
<FEditableText <FEditableText
name={'invoice_message'} name={'invoice_message'}
placeholder={intl.get('invoice_form.invoice_message.placeholder')} placeholder={intl.get('invoice_form.invoice_message.placeholder')}
fastField
multiline
/> />
</InvoiceMsgFormGroup> </InvoiceMsgFormGroup>
@@ -28,6 +30,8 @@ export function InvoiceFormFooterLeft() {
placeholder={intl.get( placeholder={intl.get(
'invoice_form.terms_and_conditions.placeholder', 'invoice_form.terms_and_conditions.placeholder',
)} )}
multiline
fastField
/> />
</TermsConditsFormGroup> </TermsConditsFormGroup>
</React.Fragment> </React.Fragment>

View File

@@ -18,7 +18,8 @@ export function PaymentReceiveFormFootetLeft() {
placeholder={intl.get( placeholder={intl.get(
'payment_receive_form.internal_note.placeholder', 'payment_receive_form.internal_note.placeholder',
)} )}
fastField={true} fastField
multiline
/> />
</TermsConditsFormGroup> </TermsConditsFormGroup>
</React.Fragment> </React.Fragment>

View File

@@ -16,6 +16,8 @@ export function ReceiptFormFooterLeft() {
<FEditableText <FEditableText
name={'receipt_message'} name={'receipt_message'}
placeholder={intl.get('receipt_form.receipt_message.placeholder')} placeholder={intl.get('receipt_form.receipt_message.placeholder')}
multiline
fastField
/> />
</ReceiptMsgFormGroup> </ReceiptMsgFormGroup>
@@ -29,6 +31,8 @@ export function ReceiptFormFooterLeft() {
placeholder={intl.get( placeholder={intl.get(
'receipt_form.terms_and_conditions.placeholder', 'receipt_form.terms_and_conditions.placeholder',
)} )}
multiline
fastField
/> />
</TermsConditsFormGroup> </TermsConditsFormGroup>
</React.Fragment> </React.Fragment>

View File

@@ -0,0 +1,25 @@
// @ts-nocheck
import { useHistory } from 'react-router-dom';
import { DashboardInsider } from '@/components';
import { ImportView } from '@/containers/Import';
export default function TaxRatesImport() {
const history = useHistory();
const handleCancelBtnClick = () => {
history.push('/tax-rates');
};
const handleImportSuccess = () => {
history.push('/tax-rates');
};
return (
<DashboardInsider name={'import-tax-rates'}>
<ImportView
resource={'tax-rate'}
onCancelClick={handleCancelBtnClick}
onImportSuccess={handleImportSuccess}
/>
</DashboardInsider>
);
}

View File

@@ -13,6 +13,7 @@ import withDialogActions from '@/containers/Dialog/withDialogActions';
import { DialogsName } from '@/constants/dialogs'; import { DialogsName } from '@/constants/dialogs';
import { compose } from '@/utils'; import { compose } from '@/utils';
import { useHistory } from 'react-router-dom';
/** /**
* Tax rates actions bar. * Tax rates actions bar.
@@ -21,11 +22,21 @@ function TaxRatesActionsBar({
// #withDialogActions // #withDialogActions
openDialog, openDialog,
}) { }) {
const history = useHistory();
// Handle `new item` button click. // Handle `new item` button click.
const onClickNewItem = () => { const onClickNewItem = () => {
openDialog(DialogsName.TaxRateForm); openDialog(DialogsName.TaxRateForm);
}; };
const handleImportBtnClick = () => {
history.push('/tax-rates/import');
};
const handleExportBtnClick = () => {
openDialog(DialogsName.Export, { resource: 'tax_rate' });
};
return ( return (
<DashboardActionsBar> <DashboardActionsBar>
<NavbarGroup> <NavbarGroup>
@@ -43,11 +54,13 @@ function TaxRatesActionsBar({
className={Classes.MINIMAL} className={Classes.MINIMAL}
icon={<Icon icon="file-import-16" iconSize={16} />} icon={<Icon icon="file-import-16" iconSize={16} />}
text={<T id={'import'} />} text={<T id={'import'} />}
onClick={handleImportBtnClick}
/> />
<Button <Button
className={Classes.MINIMAL} className={Classes.MINIMAL}
icon={<Icon icon="file-export-16" iconSize={16} />} icon={<Icon icon="file-export-16" iconSize={16} />}
text={<T id={'export'} />} text={<T id={'export'} />}
onClick={handleExportBtnClick}
/> />
</NavbarGroup> </NavbarGroup>
</DashboardActionsBar> </DashboardActionsBar>

View File

@@ -15,6 +15,8 @@ export function WarehouseTransferFormFooterLeft() {
<FEditableText <FEditableText
name={'reason'} name={'reason'}
placeholder={intl.get('warehouse_transfer.form.reason.placeholder')} placeholder={intl.get('warehouse_transfer.form.reason.placeholder')}
multiline
fastField
/> />
</TermsConditsFormGroup> </TermsConditsFormGroup>
</React.Fragment> </React.Fragment>

View File

@@ -686,3 +686,34 @@ export function useExcludedBankTransactionsInfinity(
}, },
); );
} }
export function usePendingBankTransactionsInfinity(
query,
infinityProps,
axios,
) {
const apiRequest = useApiRequest();
return useInfiniteQuery(
[BANK_QUERY_KEY.PENDING_BANK_ACCOUNT_TRANSACTIONS_INFINITY, query],
async ({ pageParam = 1 }) => {
const response = await apiRequest.http({
...axios,
method: 'get',
url: `/api/banking/bank_accounts/pending_transactions`,
params: { page: pageParam, ...query },
});
return response.data;
},
{
getPreviousPageParam: (firstPage) => firstPage.pagination.page - 1,
getNextPageParam: (lastPage) => {
const { pagination } = lastPage;
return pagination.total > pagination.page_size * pagination.page
? lastPage.pagination.page + 1
: undefined;
},
...infinityProps,
},
);
}

View File

@@ -0,0 +1,28 @@
// @ts-nocheck
import { useQuery, UseQueryOptions, UseQueryResult } from 'react-query';
import useApiRequest from '../useRequest';
import { BANK_QUERY_KEY } from '@/constants/query-keys/banking';
interface GetBankRuleRes {}
/**
* Retrieve the given bank rule.
* @param {number} bankRuleId -
* @param {UseQueryOptions<GetBankRuleRes, Error>} options -
* @returns {UseQueryResult<GetBankRuleRes, Error>}
*/
export function usePendingBankAccountTransactions(
bankRuleId: number,
options?: UseQueryOptions<GetBankRuleRes, Error>,
): UseQueryResult<GetBankRuleRes, Error> {
const apiRequest = useApiRequest();
return useQuery<GetBankRuleRes, Error>(
[BANK_QUERY_KEY.PENDING_BANK_ACCOUNT_TRANSACTIONS],
() =>
apiRequest
.get(`/banking/bank_account/pending_transactions`)
.then((res) => res.data),
{ ...options },
);
}

View File

@@ -0,0 +1,65 @@
// @ts-nocheck
import {
useMutation,
UseMutationOptions,
UseMutationResult,
useQueryClient,
} from 'react-query';
import useApiRequest from '../useRequest';
import { BANK_QUERY_KEY } from '@/constants/query-keys/banking';
import t from './types';
type UncategorizeTransactionsBulkValues = { ids: Array<number> };
interface UncategorizeBankTransactionsBulkResponse {}
/**
* Uncategorize the given categorized transactions in bulk.
* @param {UseMutationResult<PuaseFeedsBankAccountResponse, Error, ExcludeBankTransactionValue>} options
* @returns {UseMutationResult<PuaseFeedsBankAccountResponse, Error, ExcludeBankTransactionValue>}
*/
export function useUncategorizeTransactionsBulkAction(
options?: UseMutationOptions<
UncategorizeBankTransactionsBulkResponse,
Error,
UncategorizeTransactionsBulkValues
>,
): UseMutationResult<
UncategorizeBankTransactionsBulkResponse,
Error,
UncategorizeTransactionsBulkValues
> {
const queryClient = useQueryClient();
const apiRequest = useApiRequest();
return useMutation<
UncategorizeBankTransactionsBulkResponse,
Error,
UncategorizeTransactionsBulkValues
>(
(value) =>
apiRequest.post(`/cashflow/transactions/uncategorize/bulk`, {
ids: value.ids,
}),
{
onSuccess: (res, values) => {
// Invalidate the account uncategorized transactions.
queryClient.invalidateQueries(
t.CASHFLOW_ACCOUNT_UNCATEGORIZED_TRANSACTIONS_INFINITY,
);
// Invalidate the account transactions.
queryClient.invalidateQueries(t.CASHFLOW_ACCOUNT_TRANSACTIONS_INFINITY);
// Invalidate bank account summary.
queryClient.invalidateQueries(BANK_QUERY_KEY.BANK_ACCOUNT_SUMMARY_META);
// Invalidate the recognized transactions.
queryClient.invalidateQueries([
BANK_QUERY_KEY.RECOGNIZED_BANK_TRANSACTIONS_INFINITY,
]);
// Invalidate the account.
queryClient.invalidateQueries(t.ACCOUNT);
},
...options,
},
);
}

View File

@@ -1213,6 +1213,14 @@ export const getDashboardRoutes = () => [
), ),
pageTitle: intl.get('sidebar.projects'), pageTitle: intl.get('sidebar.projects'),
}, },
{
path: '/tax-rates/import',
component: lazy(
() => import('@/containers/TaxRates/containers/TaxRatesImport'),
),
pageTitle: 'Tax Rates',
subscriptionActive: [SUBSCRIPTION_TYPE.MAIN],
},
{ {
path: '/tax-rates', path: '/tax-rates',
component: lazy( component: lazy(

View File

@@ -12,6 +12,8 @@ interface StorePlaidState {
transactionsToCategorizeSelected: Array<number | string>; transactionsToCategorizeSelected: Array<number | string>;
enableMultipleCategorization: boolean; enableMultipleCategorization: boolean;
categorizedTransactionsSelected: Array<number | string>;
} }
export const PlaidSlice = createSlice({ export const PlaidSlice = createSlice({
@@ -28,6 +30,7 @@ export const PlaidSlice = createSlice({
excludedTransactionsSelected: [], excludedTransactionsSelected: [],
transactionsToCategorizeSelected: [], transactionsToCategorizeSelected: [],
enableMultipleCategorization: false, enableMultipleCategorization: false,
categorizedTransactionsSelected: [],
} as StorePlaidState, } as StorePlaidState,
reducers: { reducers: {
setPlaidId: (state: StorePlaidState, action: PayloadAction<string>) => { setPlaidId: (state: StorePlaidState, action: PayloadAction<string>) => {
@@ -176,6 +179,26 @@ export const PlaidSlice = createSlice({
) => { ) => {
state.enableMultipleCategorization = action.payload.enable; state.enableMultipleCategorization = action.payload.enable;
}, },
/**
* Sets the selected ids of the categorized transactions.
* @param {StorePlaidState}
* @param {PayloadAction<{ ids: Array<string | number> }>}
*/
setCategorizedTransactionsSelected: (
state: StorePlaidState,
action: PayloadAction<{ ids: Array<string | number> }>,
) => {
state.categorizedTransactionsSelected = action.payload.ids;
},
/**
* Resets the selected categorized transcations.
* @param {StorePlaidState}
*/
resetCategorizedTransactionsSelected: (state: StorePlaidState) => {
state.categorizedTransactionsSelected = [];
},
}, },
}); });
@@ -195,6 +218,8 @@ export const {
removeTransactionsToCategorizeSelected, removeTransactionsToCategorizeSelected,
resetTransactionsToCategorizeSelected, resetTransactionsToCategorizeSelected,
enableMultipleCategorization, enableMultipleCategorization,
setCategorizedTransactionsSelected,
resetCategorizedTransactionsSelected,
} = PlaidSlice.actions; } = PlaidSlice.actions;
export const getPlaidToken = (state: any) => state.plaid.plaidToken; export const getPlaidToken = (state: any) => state.plaid.plaidToken;

View File

@@ -48,6 +48,13 @@
&__content { &__content {
text-transform: capitalize; text-transform: capitalize;
} }
&.detail-item--multilines{
.detail-item__content{
white-space: pre-line;
}
}
} }
+ .details-menu{ + .details-menu{