mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-17 21:30:31 +00:00
Compare commits
20 Commits
v0.19.2
...
multi-line
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
14d1f0bd1d | ||
|
|
82f8648c59 | ||
|
|
c928940d32 | ||
|
|
0a78d56015 | ||
|
|
1a5716873e | ||
|
|
01b7c86ab9 | ||
|
|
0ca209b195 | ||
|
|
be6f6e3c73 | ||
|
|
cb016be78c | ||
|
|
fc085f2328 | ||
|
|
9a34f3e283 | ||
|
|
7054e862d5 | ||
|
|
faa81abee4 | ||
|
|
6d01f2a323 | ||
|
|
72678bb936 | ||
|
|
9ae5644af9 | ||
|
|
e8830c5911 | ||
|
|
7699889bd6 | ||
|
|
35a061d188 | ||
|
|
c7c021c969 |
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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');
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|||||||
69
packages/server/src/models/TaxRate.settings.ts
Normal file
69
packages/server/src/models/TaxRate.settings.ts
Normal 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',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -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.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -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);
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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 }
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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 '';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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');
|
||||||
|
|||||||
@@ -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
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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 },
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
18
packages/server/src/services/TaxRates/TaxRatesExportable.ts
Normal file
18
packages/server/src/services/TaxRates/TaxRatesExportable.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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',
|
||||||
|
},
|
||||||
|
];
|
||||||
46
packages/server/src/services/TaxRates/TaxRatesImportable.ts
Normal file
46
packages/server/src/services/TaxRates/TaxRatesImportable.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -659,6 +659,9 @@ export default {
|
|||||||
|
|
||||||
onUnexcluding: 'onBankTransactionUnexcluding',
|
onUnexcluding: 'onBankTransactionUnexcluding',
|
||||||
onUnexcluded: 'onBankTransactionUnexcluded',
|
onUnexcluded: 'onBankTransactionUnexcluded',
|
||||||
|
|
||||||
|
onPendingRemoving: 'onBankTransactionPendingRemoving',
|
||||||
|
onPendingRemoved: 'onBankTransactionPendingRemoved',
|
||||||
},
|
},
|
||||||
|
|
||||||
bankAccount: {
|
bankAccount: {
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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',
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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)`
|
||||||
|
|||||||
@@ -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],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -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}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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 />;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
@@ -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 };
|
||||||
@@ -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,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
@@ -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,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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]);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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<
|
||||||
|
|||||||
@@ -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' },
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -21,12 +21,15 @@ export default function EstimateDetailFooter() {
|
|||||||
<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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -21,7 +21,10 @@ export default function PaymentReceiveDetailFooter() {
|
|||||||
<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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
28
packages/webapp/src/hooks/query/bank-transaction.ts
Normal file
28
packages/webapp/src/hooks/query/bank-transaction.ts
Normal 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 },
|
||||||
|
);
|
||||||
|
}
|
||||||
65
packages/webapp/src/hooks/query/bank-transactions.ts
Normal file
65
packages/webapp/src/hooks/query/bank-transactions.ts
Normal 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,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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(
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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{
|
||||||
|
|||||||
Reference in New Issue
Block a user