mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-15 12:20:31 +00:00
Compare commits
33 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
92d1fc670b | ||
|
|
ab7abfea35 | ||
|
|
0d15c16d40 | ||
|
|
2a85fe2f3c | ||
|
|
dc762567b5 | ||
|
|
a676e09537 | ||
|
|
c46948049c | ||
|
|
de1b7f132c | ||
|
|
90b552d5c9 | ||
|
|
39115b3000 | ||
|
|
3c8c3d8253 | ||
|
|
657400c671 | ||
|
|
6356cb5e63 | ||
|
|
13c6e7a62d | ||
|
|
b6d99b1d4b | ||
|
|
f0e15d43d3 | ||
|
|
50d5ddba8e | ||
|
|
d2c63878ed | ||
|
|
ef52eaf91a | ||
|
|
cd71900bdd | ||
|
|
34e97fc56b | ||
|
|
e6e119682b | ||
|
|
e096db2ab8 | ||
|
|
cdff641cdb | ||
|
|
d0126ffc2c | ||
|
|
fe043eb594 | ||
|
|
eb1ff7c151 | ||
|
|
a862ee9ccf | ||
|
|
6953f7c4a3 | ||
|
|
ad53ddb9dd | ||
|
|
cfd4540a65 | ||
|
|
217321380a | ||
|
|
33e5d1a979 |
@@ -69,6 +69,15 @@
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "cschuijt",
|
||||
"name": "Casper Schuijt",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/5460015?v=4",
|
||||
"profile": "http://cschuijt.nl",
|
||||
"contributions": [
|
||||
"bug"
|
||||
]
|
||||
}
|
||||
],
|
||||
"contributorsPerLine": 7,
|
||||
|
||||
46
CHANGELOG.md
46
CHANGELOG.md
@@ -2,11 +2,43 @@
|
||||
|
||||
All notable changes to Bigcapital server-side will be in this file.
|
||||
|
||||
# [0.10.1] - 25-09-2023
|
||||
## [0.12.1] - 17-11-2023
|
||||
|
||||
* feat: Add default customer message and terms conditions to the transactions by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/291
|
||||
* fix: The currency code of transaction tax rate entry by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/293
|
||||
|
||||
## [0.12.0] - 04-11-2023
|
||||
|
||||
* feat: Export reports via CSV and XLSX by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/286
|
||||
* fix: Axios upgrade by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/288
|
||||
* fix(server): Allow decimal amount in sale/purchase transactions. by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/289
|
||||
* feat: Optimize invoice documents printing by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/280
|
||||
* chore(deps): bump axios from 0.20.0 to 1.6.0 in /packages/server by @dependabot in https://github.com/bigcapitalhq/bigcapital/pull/284
|
||||
* chore(deps): bump axios from 0.20.0 to 1.6.0 by @dependabot in https://github.com/bigcapitalhq/bigcapital/pull/283
|
||||
|
||||
## [0.11.0] - 28-10-2023
|
||||
|
||||
* feat: Migrate to pnpm by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/253
|
||||
* feat: Integrate tax rates to bills by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/260
|
||||
* feat: Assign default sell/purchase tax rates to items by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/261
|
||||
* chore(deps-dev): bump @babel/traverse from 7.23.0 to 7.23.2 in /packages/server by @dependabot in https://github.com/bigcapitalhq/bigcapital/pull/272
|
||||
* feat: Improve financial statements rows color by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/276
|
||||
* fix: Trial balance sheet adjusted balance by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/273
|
||||
* feat: Adds tax numbers to organization and customers by @kochie in https://github.com/bigcapitalhq/bigcapital/pull/269
|
||||
* docs: Add kochie as a contributor for code by @allcontributors in https://github.com/bigcapitalhq/bigcapital/pull/277
|
||||
* feat: Computed Net Income under Equity in Balance Sheet report. by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/271
|
||||
* fix: Change Dockerfile files with new pnpm by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/278
|
||||
|
||||
## [0.10.2] - 02-10-2023
|
||||
|
||||
fix(webapp): Disable tax rates from item entries editor table services do not support tax rates (https://github.com/bigcapitalhq/bigcapital/commit/69afa07e3ba45495a4cab3490c15f2b0c40c4790) by @abouolia
|
||||
fix(server): Add missing method in ItemEntry model (https://github.com/bigcapitalhq/bigcapital/commit/07628ddc37f46c98959ced0323f28752e0a98944) by @abouolia
|
||||
|
||||
## [0.10.1] - 25-09-2023
|
||||
|
||||
* Fix: Running tenants migration on Docker migration container by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/242
|
||||
|
||||
# [0.10.0] - 24-09-2023
|
||||
## [0.10.0] - 24-09-2023
|
||||
|
||||
* Added: Tax rates service by @abouolia @elforjani13 in https://github.com/bigcapitalhq/bigcapital/pull/204
|
||||
* Added: Sales Tax Liability Summary report by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/204
|
||||
@@ -16,7 +48,7 @@ All notable changes to Bigcapital server-side will be in this file.
|
||||
* chore(deps): bump word-wrap from 1.2.3 to 1.2.4 in /packages/webapp by @dependabot in https://github.com/bigcapitalhq/bigcapital/pull/199
|
||||
* chore(deps): bump mongoose from 5.13.15 to 5.13.20 by @dependabot in https://github.com/bigcapitalhq/bigcapital/pull/197
|
||||
|
||||
# [0.9.12] - 29-08-2023
|
||||
## [0.9.12] - 29-08-2023
|
||||
|
||||
* Refactor: split the services to multiple service classes. (by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/202)
|
||||
* Fix: create quick customer/vendor by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/206
|
||||
@@ -30,18 +62,18 @@ All notable changes to Bigcapital server-side will be in this file.
|
||||
* Fix: filter by customers, vendors and items in reports do not work by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/224
|
||||
https://github.com/bigcapitalhq/bigcapital/pull/225
|
||||
|
||||
# [0.9.11] - 23-07-2023
|
||||
## [0.9.11] - 23-07-2023
|
||||
|
||||
* added: Restart policy to docker compose files. by @suhaibaffan in https://github.com/bigcapitalhq/bigcapital/pull/198
|
||||
* fix: Expose and expand the rate limit to the env variables by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/195
|
||||
|
||||
# [0.9.10] - 18-07-2023
|
||||
## [0.9.10] - 18-07-2023
|
||||
|
||||
* feat(e2e): E2E onboarding process by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/176
|
||||
* fix(webapp): Show loading message of cost computing job on financial reports by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/196
|
||||
* fix(webapp): Change the currency code of sales and purchases transactions with foreign contacts.
|
||||
|
||||
# [0.9.9] - 28-06-2023
|
||||
## [0.9.9] - 28-06-2023
|
||||
|
||||
* refactor: Customer and vendor select component by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/171
|
||||
* chore: Move auto-increment components in separate files by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/170
|
||||
@@ -53,7 +85,7 @@ https://github.com/bigcapitalhq/bigcapital/pull/225
|
||||
* fix: No currency in amount field on money in/out dialogs by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/179
|
||||
* fix: No default branch for customer/vendor opening balance branch by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/182
|
||||
|
||||
# [0.9.8] - 19-06-2023
|
||||
## [0.9.8] - 19-06-2023
|
||||
|
||||
`bigcapitalhq/webapp`
|
||||
|
||||
|
||||
@@ -107,6 +107,9 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/KalliopiPliogka"><img src="https://avatars.githubusercontent.com/u/81677549?v=4?s=100" width="100px;" alt="Kalliopi Pliogka"/><br /><sub><b>Kalliopi Pliogka</b></sub></a><br /><a href="https://github.com/bigcapitalhq/bigcapital/issues?q=author%3AKalliopiPliogka" title="Bug reports">🐛</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://me.kochie.io"><img src="https://avatars.githubusercontent.com/u/10809884?v=4?s=100" width="100px;" alt="Robert Koch"/><br /><sub><b>Robert Koch</b></sub></a><br /><a href="https://github.com/bigcapitalhq/bigcapital/commits?author=kochie" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://cschuijt.nl"><img src="https://avatars.githubusercontent.com/u/5460015?v=4?s=100" width="100px;" alt="Casper Schuijt"/><br /><sub><b>Casper Schuijt</b></sub></a><br /><a href="https://github.com/bigcapitalhq/bigcapital/issues?q=author%3Acschuijt" title="Bug reports">🐛</a></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { Router, Request, Response, NextFunction } from 'express';
|
||||
import { check, param, query, ValidationChain } from 'express-validator';
|
||||
import { body, check, param, query, ValidationChain } from 'express-validator';
|
||||
import {
|
||||
AbilitySubject,
|
||||
IPaymentReceiveDTO,
|
||||
PaymentReceiveAction,
|
||||
PaymentReceiveMailOptsDTO,
|
||||
} from '@/interfaces';
|
||||
import BaseController from '@/api/controllers/BaseController';
|
||||
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
|
||||
@@ -117,6 +118,25 @@ export default class PaymentReceivesController extends BaseController {
|
||||
asyncMiddleware(this.deletePaymentReceive.bind(this)),
|
||||
this.handleServiceErrors
|
||||
);
|
||||
router.post(
|
||||
'/:id/mail',
|
||||
[
|
||||
...this.paymentReceiveValidation,
|
||||
body('subject').isString().optional(),
|
||||
body('from').isString().optional(),
|
||||
body('to').isString().optional(),
|
||||
body('body').isString().optional(),
|
||||
body('attach_invoice').optional().isBoolean().toBoolean(),
|
||||
],
|
||||
this.sendPaymentReceiveByMail.bind(this),
|
||||
this.handleServiceErrors
|
||||
);
|
||||
router.get(
|
||||
'/:id/mail',
|
||||
[...this.paymentReceiveValidation],
|
||||
asyncMiddleware(this.getPaymentDefaultMail.bind(this)),
|
||||
this.handleServiceErrors
|
||||
);
|
||||
return router;
|
||||
}
|
||||
|
||||
@@ -416,27 +436,26 @@ export default class PaymentReceivesController extends BaseController {
|
||||
const { id: paymentReceiveId } = req.params;
|
||||
|
||||
try {
|
||||
const paymentReceive =
|
||||
await this.paymentReceiveApplication.getPaymentReceive(
|
||||
tenantId,
|
||||
paymentReceiveId
|
||||
);
|
||||
|
||||
const ACCEPT_TYPE = {
|
||||
APPLICATION_PDF: 'application/pdf',
|
||||
APPLICATION_JSON: 'application/json',
|
||||
};
|
||||
res.format({
|
||||
[ACCEPT_TYPE.APPLICATION_JSON]: () => {
|
||||
[ACCEPT_TYPE.APPLICATION_JSON]: async () => {
|
||||
const paymentReceive =
|
||||
await this.paymentReceiveApplication.getPaymentReceive(
|
||||
tenantId,
|
||||
paymentReceiveId
|
||||
);
|
||||
return res.status(200).send({
|
||||
payment_receive: this.transfromToResponse(paymentReceive),
|
||||
payment_receive: paymentReceive,
|
||||
});
|
||||
},
|
||||
[ACCEPT_TYPE.APPLICATION_PDF]: async () => {
|
||||
const pdfContent =
|
||||
await this.paymentReceiveApplication.getPaymentReceivePdf(
|
||||
tenantId,
|
||||
paymentReceive
|
||||
paymentReceiveId
|
||||
);
|
||||
res.set({
|
||||
'Content-Type': 'application/pdf',
|
||||
@@ -507,6 +526,66 @@ export default class PaymentReceivesController extends BaseController {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Sends mail invoice of the given sale invoice.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
* @returns
|
||||
*/
|
||||
public sendPaymentReceiveByMail = async (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
const { tenantId } = req;
|
||||
const { id: paymentReceiveId } = req.params;
|
||||
const paymentMailDTO: PaymentReceiveMailOptsDTO = this.matchedBodyData(
|
||||
req,
|
||||
{
|
||||
includeOptionals: false,
|
||||
}
|
||||
);
|
||||
try {
|
||||
await this.paymentReceiveApplication.notifyPaymentByMail(
|
||||
tenantId,
|
||||
paymentReceiveId,
|
||||
paymentMailDTO
|
||||
);
|
||||
return res.status(200).send({
|
||||
code: 200,
|
||||
message: 'The payment notification has been sent successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the default mail options of the given payment transaction.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
public getPaymentDefaultMail = async (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
const { tenantId } = req;
|
||||
const { id: paymentReceiveId } = req.params;
|
||||
|
||||
try {
|
||||
const data = await this.paymentReceiveApplication.getPaymentMailOptions(
|
||||
tenantId,
|
||||
paymentReceiveId
|
||||
);
|
||||
return res.status(200).send({ data });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Handles service errors.
|
||||
* @param error
|
||||
@@ -514,7 +593,7 @@ export default class PaymentReceivesController extends BaseController {
|
||||
* @param res
|
||||
* @param next
|
||||
*/
|
||||
handleServiceErrors(
|
||||
private handleServiceErrors(
|
||||
error: Error,
|
||||
req: Request,
|
||||
res: Response,
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { Router, Request, Response, NextFunction } from 'express';
|
||||
import { check, param, query } from 'express-validator';
|
||||
import { body, check, param, query } from 'express-validator';
|
||||
import { Inject, Service } from 'typedi';
|
||||
import {
|
||||
AbilitySubject,
|
||||
ISaleEstimateDTO,
|
||||
SaleEstimateAction,
|
||||
SaleEstimateMailOptionsDTO,
|
||||
} from '@/interfaces';
|
||||
import BaseController from '@/api/controllers/BaseController';
|
||||
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
|
||||
@@ -121,6 +122,27 @@ export default class SalesEstimatesController extends BaseController {
|
||||
this.handleServiceErrors,
|
||||
this.dynamicListService.handlerErrorsToResponse
|
||||
);
|
||||
router.post(
|
||||
'/:id/mail',
|
||||
[
|
||||
...this.validateSpecificEstimateSchema,
|
||||
body('subject').isString().optional(),
|
||||
body('from').isString().optional(),
|
||||
body('to').isString().optional(),
|
||||
body('body').isString().optional(),
|
||||
body('attach_invoice').optional().isBoolean().toBoolean(),
|
||||
],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.sendSaleEstimateMail.bind(this)),
|
||||
this.handleServiceErrors
|
||||
);
|
||||
router.get(
|
||||
'/:id/mail',
|
||||
[...this.validateSpecificEstimateSchema],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.getSaleEstimateMail.bind(this)),
|
||||
this.handleServiceErrors
|
||||
);
|
||||
return router;
|
||||
}
|
||||
|
||||
@@ -362,22 +384,22 @@ export default class SalesEstimatesController extends BaseController {
|
||||
const { tenantId } = req;
|
||||
|
||||
try {
|
||||
const estimate = await this.saleEstimatesApplication.getSaleEstimate(
|
||||
tenantId,
|
||||
estimateId
|
||||
);
|
||||
// Response formatter.
|
||||
res.format({
|
||||
// JSON content type.
|
||||
[ACCEPT_TYPE.APPLICATION_JSON]: () => {
|
||||
return res.status(200).send(this.transfromToResponse({ estimate }));
|
||||
[ACCEPT_TYPE.APPLICATION_JSON]: async () => {
|
||||
const estimate = await this.saleEstimatesApplication.getSaleEstimate(
|
||||
tenantId,
|
||||
estimateId
|
||||
);
|
||||
return res.status(200).send({ estimate });
|
||||
},
|
||||
// PDF content type.
|
||||
[ACCEPT_TYPE.APPLICATION_PDF]: async () => {
|
||||
const pdfContent =
|
||||
await this.saleEstimatesApplication.getSaleEstimatePdf(
|
||||
tenantId,
|
||||
estimate
|
||||
estimateId
|
||||
);
|
||||
res.set({
|
||||
'Content-Type': 'application/pdf',
|
||||
@@ -478,6 +500,65 @@ export default class SalesEstimatesController extends BaseController {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Send the sale estimate mail.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
private sendSaleEstimateMail = async (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
const { tenantId } = req;
|
||||
const { id: invoiceId } = req.params;
|
||||
const saleEstimateDTO: SaleEstimateMailOptionsDTO = this.matchedBodyData(
|
||||
req,
|
||||
{
|
||||
includeOptionals: false,
|
||||
}
|
||||
);
|
||||
try {
|
||||
await this.saleEstimatesApplication.sendSaleEstimateMail(
|
||||
tenantId,
|
||||
invoiceId,
|
||||
saleEstimateDTO
|
||||
);
|
||||
return res.status(200).send({
|
||||
code: 200,
|
||||
message: 'The sale estimate mail has been sent successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the default mail options of the given sale estimate.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
private getSaleEstimateMail = async (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
const { tenantId } = req;
|
||||
const { id: invoiceId } = req.params;
|
||||
|
||||
try {
|
||||
const data = await this.saleEstimatesApplication.getSaleEstimateMail(
|
||||
tenantId,
|
||||
invoiceId
|
||||
);
|
||||
return res.status(200).send({ data });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Handles service errors.
|
||||
* @param {Error} error
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Router, Request, Response, NextFunction } from 'express';
|
||||
import { check, param, query } from 'express-validator';
|
||||
import { body, check, param, query } from 'express-validator';
|
||||
import { Service, Inject } from 'typedi';
|
||||
import BaseController from '../BaseController';
|
||||
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
ISaleInvoiceCreateDTO,
|
||||
SaleInvoiceAction,
|
||||
AbilitySubject,
|
||||
SendInvoiceMailDTO,
|
||||
} from '@/interfaces';
|
||||
import CheckPolicies from '@/api/middleware/CheckPolicies';
|
||||
import { SaleInvoiceApplication } from '@/services/Sales/Invoices/SaleInvoicesApplication';
|
||||
@@ -145,6 +146,48 @@ export default class SaleInvoicesController extends BaseController {
|
||||
this.handleServiceErrors,
|
||||
this.dynamicListService.handlerErrorsToResponse
|
||||
);
|
||||
router.get(
|
||||
'/:id/mail-reminder',
|
||||
this.specificSaleInvoiceValidation,
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.getSaleInvoiceMailReminder.bind(this)),
|
||||
this.handleServiceErrors
|
||||
);
|
||||
router.post(
|
||||
'/:id/mail-reminder',
|
||||
[
|
||||
...this.specificSaleInvoiceValidation,
|
||||
body('subject').isString().optional(),
|
||||
body('from').isString().optional(),
|
||||
body('to').isString().optional(),
|
||||
body('body').isString().optional(),
|
||||
body('attach_invoice').optional().isBoolean().toBoolean(),
|
||||
],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.sendSaleInvoiceMailReminder.bind(this)),
|
||||
this.handleServiceErrors
|
||||
);
|
||||
router.post(
|
||||
'/:id/mail',
|
||||
[
|
||||
...this.specificSaleInvoiceValidation,
|
||||
body('subject').isString().optional(),
|
||||
body('from').isString().optional(),
|
||||
body('to').isString().optional(),
|
||||
body('body').isString().optional(),
|
||||
body('attach_invoice').optional().isBoolean().toBoolean(),
|
||||
],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.sendSaleInvoiceMail.bind(this)),
|
||||
this.handleServiceErrors
|
||||
);
|
||||
router.get(
|
||||
'/:id/mail',
|
||||
[...this.specificSaleInvoiceValidation],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.getSaleInvoiceMail.bind(this)),
|
||||
this.handleServiceErrors
|
||||
);
|
||||
return router;
|
||||
}
|
||||
|
||||
@@ -630,6 +673,119 @@ export default class SaleInvoicesController extends BaseController {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Sends mail invoice of the given sale invoice.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
public async sendSaleInvoiceMail(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
const { tenantId } = req;
|
||||
const { id: invoiceId } = req.params;
|
||||
const invoiceMailDTO: SendInvoiceMailDTO = this.matchedBodyData(req, {
|
||||
includeOptionals: false,
|
||||
});
|
||||
|
||||
try {
|
||||
await this.saleInvoiceApplication.sendSaleInvoiceMail(
|
||||
tenantId,
|
||||
invoiceId,
|
||||
invoiceMailDTO
|
||||
);
|
||||
return res.status(200).send({
|
||||
code: 200,
|
||||
message: 'The sale invoice mail has been sent successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retreivers the sale invoice reminder options.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
public async getSaleInvoiceMailReminder(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
const { tenantId } = req;
|
||||
const { id: invoiceId } = req.params;
|
||||
|
||||
try {
|
||||
const data = await this.saleInvoiceApplication.getSaleInvoiceMailReminder(
|
||||
tenantId,
|
||||
invoiceId
|
||||
);
|
||||
return res.status(200).send(data);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends mail invoice of the given sale invoice.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
public async sendSaleInvoiceMailReminder(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
const { tenantId } = req;
|
||||
const { id: invoiceId } = req.params;
|
||||
const invoiceMailDTO: SendInvoiceMailDTO = this.matchedBodyData(req, {
|
||||
includeOptionals: false,
|
||||
});
|
||||
try {
|
||||
await this.saleInvoiceApplication.sendSaleInvoiceMailReminder(
|
||||
tenantId,
|
||||
invoiceId,
|
||||
invoiceMailDTO
|
||||
);
|
||||
return res.status(200).send({
|
||||
code: 200,
|
||||
message: 'The sale invoice mail reminder has been sent successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the default mail options of the given sale invoice.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
public async getSaleInvoiceMail(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
const { tenantId } = req;
|
||||
const { id: invoiceId } = req.params;
|
||||
|
||||
try {
|
||||
const data = await this.saleInvoiceApplication.getSaleInvoiceMail(
|
||||
tenantId,
|
||||
invoiceId
|
||||
);
|
||||
return res.status(200).send({ data });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles service errors.
|
||||
* @param {Error} error
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Router, Request, Response, NextFunction } from 'express';
|
||||
import { check, param, query } from 'express-validator';
|
||||
import { body, check, param, query } from 'express-validator';
|
||||
import { Inject, Service } from 'typedi';
|
||||
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
|
||||
import BaseController from '../BaseController';
|
||||
import { ISaleReceiptDTO } from '@/interfaces/SaleReceipt';
|
||||
import { ISaleReceiptDTO, SaleReceiptMailOpts, SaleReceiptMailOptsDTO } from '@/interfaces/SaleReceipt';
|
||||
import { ServiceError } from '@/exceptions';
|
||||
import DynamicListingService from '@/services/DynamicListing/DynamicListService';
|
||||
import CheckPolicies from '@/api/middleware/CheckPolicies';
|
||||
@@ -46,6 +46,29 @@ export default class SalesReceiptsController extends BaseController {
|
||||
this.saleReceiptSmsDetails,
|
||||
this.handleServiceErrors
|
||||
);
|
||||
router.post(
|
||||
'/:id/mail',
|
||||
[
|
||||
...this.specificReceiptValidationSchema,
|
||||
body('subject').isString().optional(),
|
||||
body('from').isString().optional(),
|
||||
body('to').isString().optional(),
|
||||
body('body').isString().optional(),
|
||||
body('attach_receipt').optional().isBoolean().toBoolean(),
|
||||
],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.sendSaleReceiptMail.bind(this)),
|
||||
this.handleServiceErrors
|
||||
);
|
||||
router.get(
|
||||
'/:id/mail',
|
||||
[
|
||||
...this.specificReceiptValidationSchema,
|
||||
],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.getSaleReceiptMail.bind(this)),
|
||||
this.handleServiceErrors
|
||||
);
|
||||
router.post(
|
||||
'/:id',
|
||||
CheckPolicies(SaleReceiptAction.Edit, AbilitySubject.SaleReceipt),
|
||||
@@ -314,26 +337,24 @@ export default class SalesReceiptsController extends BaseController {
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
async getSaleReceipt(req: Request, res: Response, next: NextFunction) {
|
||||
public async getSaleReceipt(req: Request, res: Response, next: NextFunction) {
|
||||
const { id: saleReceiptId } = req.params;
|
||||
const { tenantId } = req;
|
||||
|
||||
try {
|
||||
const saleReceipt = await this.saleReceiptsApplication.getSaleReceipt(
|
||||
tenantId,
|
||||
saleReceiptId
|
||||
);
|
||||
res.format({
|
||||
'application/json': () => {
|
||||
return res
|
||||
.status(200)
|
||||
.send(this.transfromToResponse({ saleReceipt }));
|
||||
'application/json': async () => {
|
||||
const saleReceipt = await this.saleReceiptsApplication.getSaleReceipt(
|
||||
tenantId,
|
||||
saleReceiptId
|
||||
);
|
||||
return res.status(200).send({ saleReceipt });
|
||||
},
|
||||
'application/pdf': async () => {
|
||||
const pdfContent =
|
||||
await this.saleReceiptsApplication.getSaleReceiptPdf(
|
||||
tenantId,
|
||||
saleReceipt
|
||||
saleReceiptId
|
||||
);
|
||||
res.set({
|
||||
'Content-Type': 'application/pdf',
|
||||
@@ -405,6 +426,64 @@ export default class SalesReceiptsController extends BaseController {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Sends mail notification of the given sale receipt.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
public sendSaleReceiptMail = async (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
const { tenantId } = req;
|
||||
const { id: receiptId } = req.params;
|
||||
const receiptMailDTO: SaleReceiptMailOptsDTO = this.matchedBodyData(req, {
|
||||
includeOptionals: false,
|
||||
});
|
||||
|
||||
try {
|
||||
await this.saleReceiptsApplication.sendSaleReceiptMail(
|
||||
tenantId,
|
||||
receiptId,
|
||||
receiptMailDTO
|
||||
);
|
||||
return res.status(200).send({
|
||||
code: 200,
|
||||
message:
|
||||
'The sale receipt notification via sms has been sent successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the default mail options of the given sale receipt.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
public getSaleReceiptMail = async (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
const { tenantId } = req;
|
||||
const { id: receiptId } = req.params;
|
||||
|
||||
try {
|
||||
const data = await this.saleReceiptsApplication.getSaleReceiptMail(
|
||||
tenantId,
|
||||
receiptId
|
||||
);
|
||||
return res.status(200).send({ data });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Handles service errors.
|
||||
* @param {Error} error
|
||||
|
||||
@@ -58,6 +58,7 @@ module.exports = {
|
||||
secure: !!parseInt(process.env.MAIL_SECURE, 10),
|
||||
username: process.env.MAIL_USERNAME,
|
||||
password: process.env.MAIL_PASSWORD,
|
||||
from: process.env.MAIL_FROM_ADDRESS,
|
||||
},
|
||||
|
||||
/**
|
||||
|
||||
@@ -59,6 +59,12 @@ export default {
|
||||
auto_increment: {
|
||||
type: 'boolean',
|
||||
},
|
||||
customer_notes: {
|
||||
type: 'string',
|
||||
},
|
||||
terms_conditions: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
sales_receipts: {
|
||||
next_number: {
|
||||
@@ -73,6 +79,12 @@ export default {
|
||||
preferred_deposit_account: {
|
||||
type: 'number',
|
||||
},
|
||||
receipt_message: {
|
||||
type: 'string',
|
||||
},
|
||||
terms_conditions: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
sales_invoices: {
|
||||
next_number: {
|
||||
@@ -84,6 +96,12 @@ export default {
|
||||
auto_increment: {
|
||||
type: 'boolean',
|
||||
},
|
||||
customer_notes: {
|
||||
type: 'string',
|
||||
},
|
||||
terms_conditions: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
payment_receives: {
|
||||
next_number: {
|
||||
@@ -147,6 +165,12 @@ export default {
|
||||
auto_increment: {
|
||||
type: 'boolean',
|
||||
},
|
||||
customer_notes: {
|
||||
type: 'string',
|
||||
},
|
||||
terms_conditions: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
vendor_credit: {
|
||||
next_number: {
|
||||
|
||||
@@ -1,9 +1,17 @@
|
||||
export type IMailAttachment = MailAttachmentPath | MailAttachmentContent;
|
||||
|
||||
export interface MailAttachmentPath {
|
||||
filename: string;
|
||||
path: string;
|
||||
cid: string;
|
||||
}
|
||||
export interface MailAttachmentContent {
|
||||
filename: string;
|
||||
content: Buffer;
|
||||
}
|
||||
|
||||
export interface IMailable {
|
||||
constructor(
|
||||
view: string,
|
||||
data?: { [key: string]: string | number },
|
||||
);
|
||||
constructor(view: string, data?: { [key: string]: string | number });
|
||||
send(): Promise<any>;
|
||||
build(): void;
|
||||
setData(data: { [key: string]: string | number }): IMailable;
|
||||
@@ -13,4 +21,27 @@ export interface IMailable {
|
||||
setView(view: string): IMailable;
|
||||
render(data?: { [key: string]: string | number }): string;
|
||||
getViewContent(): string;
|
||||
}
|
||||
}
|
||||
|
||||
export interface AddressItem {
|
||||
label: string;
|
||||
mail: string;
|
||||
primary?: boolean;
|
||||
}
|
||||
|
||||
export interface CommonMailOptions {
|
||||
toAddresses: AddressItem[];
|
||||
fromAddresses: AddressItem[];
|
||||
from: string;
|
||||
to: string | string[];
|
||||
subject: string;
|
||||
body: string;
|
||||
data?: Record<string, any>;
|
||||
}
|
||||
|
||||
export interface CommonMailOptionsDTO {
|
||||
to?: string | string[];
|
||||
from?: string;
|
||||
subject?: string;
|
||||
body?: string;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import { Knex } from 'knex';
|
||||
import { ISystemUser } from '@/interfaces';
|
||||
import {
|
||||
CommonMailOptions,
|
||||
CommonMailOptionsDTO,
|
||||
ISystemUser,
|
||||
} from '@/interfaces';
|
||||
import { ILedgerEntry } from './Ledger';
|
||||
import { ISaleInvoice } from './SaleInvoice';
|
||||
|
||||
@@ -19,7 +23,7 @@ export interface IPaymentReceive {
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
localAmount?: number;
|
||||
branchId?: number
|
||||
branchId?: number;
|
||||
}
|
||||
export interface IPaymentReceiveCreateDTO {
|
||||
customerId: number;
|
||||
@@ -165,3 +169,7 @@ export type IPaymentReceiveGLCommonEntry = Pick<
|
||||
| 'createdAt'
|
||||
| 'branchId'
|
||||
>;
|
||||
|
||||
export interface PaymentReceiveMailOpts extends CommonMailOptions {}
|
||||
|
||||
export interface PaymentReceiveMailOptsDTO extends CommonMailOptionsDTO {}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Knex } from 'knex';
|
||||
import { IItemEntry, IItemEntryDTO } from './ItemEntry';
|
||||
import { IDynamicListFilterDTO } from '@/interfaces/DynamicFilter';
|
||||
import { CommonMailOptions, CommonMailOptionsDTO } from './Mailable';
|
||||
|
||||
export interface ISaleEstimate {
|
||||
id?: number;
|
||||
@@ -124,3 +125,11 @@ export interface ISaleEstimateApprovedEvent {
|
||||
saleEstimate: ISaleEstimate;
|
||||
trx: Knex.Transaction;
|
||||
}
|
||||
|
||||
export interface SaleEstimateMailOptions extends CommonMailOptions {
|
||||
attachEstimate?: boolean;
|
||||
}
|
||||
|
||||
export interface SaleEstimateMailOptionsDTO extends CommonMailOptionsDTO {
|
||||
attachEstimate?: boolean;
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Knex } from 'knex';
|
||||
import { ISystemUser, IAccount, ITaxTransaction } from '@/interfaces';
|
||||
import { CommonMailOptions, CommonMailOptionsDTO } from './Mailable';
|
||||
import { IDynamicListFilter } from '@/interfaces/DynamicFilter';
|
||||
import { IItemEntry, IItemEntryDTO } from './ItemEntry';
|
||||
|
||||
@@ -186,3 +187,17 @@ export enum SaleInvoiceAction {
|
||||
Writeoff = 'Writeoff',
|
||||
NotifyBySms = 'NotifyBySms',
|
||||
}
|
||||
|
||||
export interface SaleInvoiceMailOptions extends CommonMailOptions {
|
||||
attachInvoice: boolean;
|
||||
}
|
||||
|
||||
export interface SendInvoiceMailDTO extends CommonMailOptionsDTO {
|
||||
attachInvoice?: boolean;
|
||||
}
|
||||
|
||||
export interface ISaleInvoiceNotifyPayload {
|
||||
tenantId: number;
|
||||
saleInvoiceId: number;
|
||||
messageDTO: SendInvoiceMailDTO;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Knex } from 'knex';
|
||||
import { IItemEntry } from './ItemEntry';
|
||||
import { CommonMailOptions, CommonMailOptionsDTO } from './Mailable';
|
||||
|
||||
export interface ISaleReceipt {
|
||||
id?: number;
|
||||
@@ -134,3 +135,11 @@ export interface ISaleReceiptDeletingPayload {
|
||||
oldSaleReceipt: ISaleReceipt;
|
||||
trx: Knex.Transaction;
|
||||
}
|
||||
|
||||
export interface SaleReceiptMailOpts extends CommonMailOptions {
|
||||
attachReceipt: boolean;
|
||||
}
|
||||
|
||||
export interface SaleReceiptMailOptsDTO extends CommonMailOptionsDTO {
|
||||
attachReceipt?: boolean;
|
||||
}
|
||||
|
||||
@@ -2,18 +2,13 @@ import fs from 'fs';
|
||||
import Mustache from 'mustache';
|
||||
import { Container } from 'typedi';
|
||||
import path from 'path';
|
||||
import { IMailable } from '@/interfaces';
|
||||
|
||||
interface IMailAttachment {
|
||||
filename: string;
|
||||
path: string;
|
||||
cid: string;
|
||||
}
|
||||
import { IMailAttachment } from '@/interfaces';
|
||||
|
||||
export default class Mail {
|
||||
view: string;
|
||||
subject: string;
|
||||
to: string;
|
||||
subject: string = '';
|
||||
content: string = '';
|
||||
to: string | string[];
|
||||
from: string = `${process.env.MAIL_FROM_NAME} ${process.env.MAIL_FROM_ADDRESS}`;
|
||||
data: { [key: string]: string | number };
|
||||
attachments: IMailAttachment[];
|
||||
@@ -21,16 +16,24 @@ export default class Mail {
|
||||
/**
|
||||
* Mail options.
|
||||
*/
|
||||
private get mailOptions() {
|
||||
public get mailOptions() {
|
||||
return {
|
||||
to: this.to,
|
||||
from: this.from,
|
||||
subject: this.subject,
|
||||
html: this.render(this.data),
|
||||
html: this.html,
|
||||
attachments: this.attachments,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the html content of the mail.
|
||||
* @returns {string}
|
||||
*/
|
||||
public get html() {
|
||||
return this.view ? Mail.render(this.view, this.data) : this.content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends the given mail to the target address.
|
||||
*/
|
||||
@@ -52,7 +55,7 @@ export default class Mail {
|
||||
* Set send mail to address.
|
||||
* @param {string} to -
|
||||
*/
|
||||
setTo(to: string) {
|
||||
setTo(to: string | string[]) {
|
||||
this.to = to;
|
||||
return this;
|
||||
}
|
||||
@@ -62,11 +65,16 @@ export default class Mail {
|
||||
* @param {string} from
|
||||
* @return {}
|
||||
*/
|
||||
private setFrom(from: string) {
|
||||
setFrom(from: string) {
|
||||
this.from = from;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set attachments to the mail.
|
||||
* @param {IMailAttachment[]} attachments
|
||||
* @returns {Mail}
|
||||
*/
|
||||
setAttachments(attachments: IMailAttachment[]) {
|
||||
this.attachments = attachments;
|
||||
return this;
|
||||
@@ -95,21 +103,26 @@ export default class Mail {
|
||||
return this;
|
||||
}
|
||||
|
||||
setContent(content: string) {
|
||||
this.content = content;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the view template with the given data.
|
||||
* @param {object} data
|
||||
* @return {string}
|
||||
*/
|
||||
render(data): string {
|
||||
const viewContent = this.getViewContent();
|
||||
static render(view: string, data: Record<string, any>): string {
|
||||
const viewContent = Mail.getViewContent(view);
|
||||
return Mustache.render(viewContent, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve view content from the view directory.
|
||||
*/
|
||||
private getViewContent(): string {
|
||||
const filePath = path.join(global.__views_dir, `/${this.view}`);
|
||||
static getViewContent(view: string): string {
|
||||
const filePath = path.join(global.__views_dir, `/${view}`);
|
||||
return fs.readFileSync(filePath, 'utf8');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,11 @@ import RewriteInvoicesJournalEntries from 'jobs/WriteInvoicesJEntries';
|
||||
import UserInviteMailJob from 'jobs/UserInviteMail';
|
||||
import OrganizationSetupJob from 'jobs/OrganizationSetup';
|
||||
import OrganizationUpgrade from 'jobs/OrganizationUpgrade';
|
||||
import { SendSaleInvoiceMailJob } from '@/services/Sales/Invoices/SendSaleInvoiceMailJob';
|
||||
import { SendSaleInvoiceReminderMailJob } from '@/services/Sales/Invoices/SendSaleInvoiceMailReminderJob';
|
||||
import { SendSaleEstimateMailJob } from '@/services/Sales/Estimates/SendSaleEstimateMailJob';
|
||||
import { SaleReceiptMailNotificationJob } from '@/services/Sales/Receipts/SaleReceiptMailNotificationJob';
|
||||
import { PaymentReceiveMailNotificationJob } from '@/services/Sales/PaymentReceives/PaymentReceiveMailNotificationJob';
|
||||
|
||||
export default ({ agenda }: { agenda: Agenda }) => {
|
||||
new ResetPasswordMailJob(agenda);
|
||||
@@ -13,6 +18,11 @@ export default ({ agenda }: { agenda: Agenda }) => {
|
||||
new RewriteInvoicesJournalEntries(agenda);
|
||||
new OrganizationSetupJob(agenda);
|
||||
new OrganizationUpgrade(agenda);
|
||||
new SendSaleInvoiceMailJob(agenda);
|
||||
new SendSaleInvoiceReminderMailJob(agenda);
|
||||
new SendSaleEstimateMailJob(agenda);
|
||||
new SaleReceiptMailNotificationJob(agenda);
|
||||
new PaymentReceiveMailNotificationJob(agenda);
|
||||
|
||||
agenda.start();
|
||||
};
|
||||
|
||||
@@ -24,6 +24,9 @@ export default class Customer extends mixin(TenantModel, [
|
||||
CustomViewBaseModel,
|
||||
ModelSearchable,
|
||||
]) {
|
||||
email: string;
|
||||
displayName: string;
|
||||
|
||||
/**
|
||||
* Query builder.
|
||||
*/
|
||||
@@ -76,6 +79,19 @@ export default class Customer extends mixin(TenantModel, [
|
||||
return 'debit';
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
get contactAddresses() {
|
||||
return [
|
||||
{
|
||||
mail: this.email,
|
||||
label: this.displayName,
|
||||
primary: true
|
||||
},
|
||||
].filter((c) => c.mail);
|
||||
}
|
||||
|
||||
/**
|
||||
* Model modifiers.
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { CommonMailOptions } from '@/interfaces';
|
||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
import { MailTenancy } from '@/services/MailTenancy/MailTenancy';
|
||||
import { formatSmsMessage } from '@/utils';
|
||||
import { Tenant } from '@/system/models';
|
||||
|
||||
@Service()
|
||||
export class ContactMailNotification {
|
||||
@Inject()
|
||||
private mailTenancy: MailTenancy;
|
||||
|
||||
@Inject()
|
||||
private tenancy: HasTenancyService;
|
||||
|
||||
/**
|
||||
* Parses the default message options.
|
||||
* @param {number} tenantId -
|
||||
* @param {number} invoiceId -
|
||||
* @param {string} subject -
|
||||
* @param {string} body -
|
||||
* @returns {Promise<SaleInvoiceMailOptions>}
|
||||
*/
|
||||
public async getDefaultMailOptions(
|
||||
tenantId: number,
|
||||
contactId: number,
|
||||
subject: string = '',
|
||||
body: string = ''
|
||||
): Promise<CommonMailOptions> {
|
||||
const { Customer } = this.tenancy.models(tenantId);
|
||||
const contact = await Customer.query()
|
||||
.findById(contactId)
|
||||
.throwIfNotFound();
|
||||
|
||||
const toAddresses = contact.contactAddresses;
|
||||
const fromAddresses = await this.mailTenancy.senders(tenantId);
|
||||
|
||||
const toAddress = toAddresses.find((a) => a.primary);
|
||||
const fromAddress = fromAddresses.find((a) => a.primary);
|
||||
|
||||
const to = toAddress?.mail || '';
|
||||
const from = fromAddress?.mail || '';
|
||||
|
||||
return {
|
||||
subject,
|
||||
body,
|
||||
to,
|
||||
from,
|
||||
fromAddresses,
|
||||
toAddresses,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the mail options of the given contact.
|
||||
* @param {number} tenantId - Tenant id.
|
||||
* @param {number} invoiceId - Invoice id.
|
||||
* @param {string} defaultSubject - Default subject text.
|
||||
* @param {string} defaultBody - Default body text.
|
||||
* @returns {Promise<CommonMailOptions>}
|
||||
*/
|
||||
public async getMailOptions(
|
||||
tenantId: number,
|
||||
contactId: number,
|
||||
defaultSubject?: string,
|
||||
defaultBody?: string,
|
||||
formatterData?: Record<string, any>
|
||||
): Promise<CommonMailOptions> {
|
||||
const mailOpts = await this.getDefaultMailOptions(
|
||||
tenantId,
|
||||
contactId,
|
||||
defaultSubject,
|
||||
defaultBody
|
||||
);
|
||||
const commonFormatArgs = await this.getCommonFormatArgs(tenantId);
|
||||
const formatArgs = {
|
||||
...commonFormatArgs,
|
||||
...formatterData,
|
||||
};
|
||||
const subject = formatSmsMessage(mailOpts.subject, formatArgs);
|
||||
const body = formatSmsMessage(mailOpts.body, formatArgs);
|
||||
|
||||
return {
|
||||
...mailOpts,
|
||||
subject,
|
||||
body,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the common format args.
|
||||
* @param {number} tenantId
|
||||
* @returns {Promise<Record<string, string>>}
|
||||
*/
|
||||
public async getCommonFormatArgs(
|
||||
tenantId: number
|
||||
): Promise<Record<string, string>> {
|
||||
const organization = await Tenant.query()
|
||||
.findById(tenantId)
|
||||
.withGraphFetched('metadata');
|
||||
|
||||
return {
|
||||
CompanyName: organization.metadata.name,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
export const ERRORS = {
|
||||
MAIL_FROM_NOT_FOUND: 'Mail from address not found',
|
||||
MAIL_TO_NOT_FOUND: 'Mail to address not found',
|
||||
MAIL_SUBJECT_NOT_FOUND: 'Mail subject not found',
|
||||
MAIL_BODY_NOT_FOUND: 'Mail body not found',
|
||||
};
|
||||
33
packages/server/src/services/MailNotification/utils.ts
Normal file
33
packages/server/src/services/MailNotification/utils.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { isEmpty } from 'lodash';
|
||||
import { ServiceError } from '@/exceptions';
|
||||
import { CommonMailOptions, CommonMailOptionsDTO } from '@/interfaces';
|
||||
import { ERRORS } from './constants';
|
||||
|
||||
/**
|
||||
* Merges the mail options with incoming options.
|
||||
* @param {Partial<SaleInvoiceMailOptions>} mailOptions
|
||||
* @param {Partial<SendInvoiceMailDTO>} overridedOptions
|
||||
* @throws {ServiceError}
|
||||
*/
|
||||
export function parseAndValidateMailOptions(
|
||||
mailOptions: Partial<CommonMailOptions>,
|
||||
overridedOptions: Partial<CommonMailOptionsDTO>
|
||||
) {
|
||||
const mergedMessageOptions = {
|
||||
...mailOptions,
|
||||
...overridedOptions,
|
||||
};
|
||||
if (isEmpty(mergedMessageOptions.from)) {
|
||||
throw new ServiceError(ERRORS.MAIL_FROM_NOT_FOUND);
|
||||
}
|
||||
if (isEmpty(mergedMessageOptions.to)) {
|
||||
throw new ServiceError(ERRORS.MAIL_TO_NOT_FOUND);
|
||||
}
|
||||
if (isEmpty(mergedMessageOptions.subject)) {
|
||||
throw new ServiceError(ERRORS.MAIL_SUBJECT_NOT_FOUND);
|
||||
}
|
||||
if (isEmpty(mergedMessageOptions.body)) {
|
||||
throw new ServiceError(ERRORS.MAIL_BODY_NOT_FOUND);
|
||||
}
|
||||
return mergedMessageOptions;
|
||||
}
|
||||
25
packages/server/src/services/MailTenancy/MailTenancy.ts
Normal file
25
packages/server/src/services/MailTenancy/MailTenancy.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import config from '@/config';
|
||||
import { Tenant } from "@/system/models";
|
||||
import { Service } from 'typedi';
|
||||
|
||||
|
||||
@Service()
|
||||
export class MailTenancy {
|
||||
/**
|
||||
* Retrieves the senders mails of the given tenant.
|
||||
* @param {number} tenantId
|
||||
*/
|
||||
public async senders(tenantId: number) {
|
||||
const tenant = await Tenant.query()
|
||||
.findById(tenantId)
|
||||
.withGraphFetched('metadata');
|
||||
|
||||
return [
|
||||
{
|
||||
mail: config.mail.from,
|
||||
label: tenant.metadata.name,
|
||||
primary: true,
|
||||
}
|
||||
].filter((item) => item.mail)
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,8 @@ import {
|
||||
ISaleEstimate,
|
||||
ISaleEstimateDTO,
|
||||
ISalesEstimatesFilter,
|
||||
SaleEstimateMailOptions,
|
||||
SaleEstimateMailOptionsDTO,
|
||||
} from '@/interfaces';
|
||||
import { EditSaleEstimate } from './EditSaleEstimate';
|
||||
import { DeleteSaleEstimate } from './DeleteSaleEstimate';
|
||||
@@ -17,6 +19,7 @@ import { ApproveSaleEstimate } from './ApproveSaleEstimate';
|
||||
import { RejectSaleEstimate } from './RejectSaleEstimate';
|
||||
import { SaleEstimateNotifyBySms } from './SaleEstimateSmsNotify';
|
||||
import { SaleEstimatesPdf } from './SaleEstimatesPdf';
|
||||
import { SendSaleEstimateMail } from './SendSaleEstimateMail';
|
||||
|
||||
@Service()
|
||||
export class SaleEstimatesApplication {
|
||||
@@ -50,6 +53,9 @@ export class SaleEstimatesApplication {
|
||||
@Inject()
|
||||
private saleEstimatesPdfService: SaleEstimatesPdf;
|
||||
|
||||
@Inject()
|
||||
private sendEstimateMailService: SendSaleEstimateMail;
|
||||
|
||||
/**
|
||||
* Create a sale estimate.
|
||||
* @param {number} tenantId - The tenant id.
|
||||
@@ -198,15 +204,49 @@ export class SaleEstimatesApplication {
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* Retrieve the PDF content of the given sale estimate.
|
||||
* @param {number} tenantId
|
||||
* @param {} saleEstimate
|
||||
* @param {number} saleEstimateId
|
||||
* @returns
|
||||
*/
|
||||
public getSaleEstimatePdf(tenantId: number, saleEstimate) {
|
||||
public getSaleEstimatePdf(tenantId: number, saleEstimateId: number) {
|
||||
return this.saleEstimatesPdfService.getSaleEstimatePdf(
|
||||
tenantId,
|
||||
saleEstimate
|
||||
saleEstimateId
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send the reminder mail of the given sale estimate.
|
||||
* @param {number} tenantId
|
||||
* @param {number} saleEstimateId
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
public sendSaleEstimateMail(
|
||||
tenantId: number,
|
||||
saleEstimateId: number,
|
||||
saleEstimateMailOpts: SaleEstimateMailOptionsDTO
|
||||
): Promise<void> {
|
||||
return this.sendEstimateMailService.triggerMail(
|
||||
tenantId,
|
||||
saleEstimateId,
|
||||
saleEstimateMailOpts
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the default mail options of the given sale estimate.
|
||||
* @param {number} tenantId
|
||||
* @param {number} saleEstimateId
|
||||
* @returns {Promise<SaleEstimateMailOptions>}
|
||||
*/
|
||||
public getSaleEstimateMail(
|
||||
tenantId: number,
|
||||
saleEstimateId: number
|
||||
): Promise<SaleEstimateMailOptions> {
|
||||
return this.sendEstimateMailService.getMailOptions(
|
||||
tenantId,
|
||||
saleEstimateId
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { ChromiumlyTenancy } from '@/services/ChromiumlyTenancy/ChromiumlyTenancy';
|
||||
import { TemplateInjectable } from '@/services/TemplateInjectable/TemplateInjectable';
|
||||
import { GetSaleEstimate } from './GetSaleEstimate';
|
||||
|
||||
@Service()
|
||||
export class SaleEstimatesPdf {
|
||||
@@ -10,11 +11,19 @@ export class SaleEstimatesPdf {
|
||||
@Inject()
|
||||
private templateInjectable: TemplateInjectable;
|
||||
|
||||
@Inject()
|
||||
private getSaleEstimate: GetSaleEstimate;
|
||||
|
||||
/**
|
||||
* Retrieve sale invoice pdf content.
|
||||
* @param {} saleInvoice -
|
||||
* @param {number} tenantId -
|
||||
* @param {ISaleInvoice} saleInvoice -
|
||||
*/
|
||||
async getSaleEstimatePdf(tenantId: number, saleEstimate) {
|
||||
public async getSaleEstimatePdf(tenantId: number, saleEstimateId: number) {
|
||||
const saleEstimate = await this.getSaleEstimate.getEstimate(
|
||||
tenantId,
|
||||
saleEstimateId
|
||||
);
|
||||
const htmlContent = await this.templateInjectable.render(
|
||||
tenantId,
|
||||
'modules/estimate-regular',
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import Mail from '@/lib/Mail';
|
||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
import {
|
||||
DEFAULT_ESTIMATE_REMINDER_MAIL_CONTENT,
|
||||
DEFAULT_ESTIMATE_REMINDER_MAIL_SUBJECT,
|
||||
} from './constants';
|
||||
import { SaleEstimatesPdf } from './SaleEstimatesPdf';
|
||||
import { GetSaleEstimate } from './GetSaleEstimate';
|
||||
import {
|
||||
SaleEstimateMailOptions,
|
||||
SaleEstimateMailOptionsDTO,
|
||||
} from '@/interfaces';
|
||||
import { ContactMailNotification } from '@/services/MailNotification/ContactMailNotification';
|
||||
import { parseAndValidateMailOptions } from '@/services/MailNotification/utils';
|
||||
|
||||
@Service()
|
||||
export class SendSaleEstimateMail {
|
||||
@Inject()
|
||||
private tenancy: HasTenancyService;
|
||||
|
||||
@Inject()
|
||||
private estimatePdf: SaleEstimatesPdf;
|
||||
|
||||
@Inject()
|
||||
private getSaleEstimateService: GetSaleEstimate;
|
||||
|
||||
@Inject()
|
||||
private contactMailNotification: ContactMailNotification;
|
||||
|
||||
@Inject('agenda')
|
||||
private agenda: any;
|
||||
|
||||
/**
|
||||
* Triggers the reminder mail of the given sale estimate.
|
||||
* @param {number} tenantId -
|
||||
* @param {number} saleEstimateId -
|
||||
* @param {SaleEstimateMailOptionsDTO} messageOptions -
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
public async triggerMail(
|
||||
tenantId: number,
|
||||
saleEstimateId: number,
|
||||
messageOptions: SaleEstimateMailOptionsDTO
|
||||
): Promise<void> {
|
||||
const payload = {
|
||||
tenantId,
|
||||
saleEstimateId,
|
||||
messageOptions,
|
||||
};
|
||||
await this.agenda.now('sale-estimate-mail-send', payload);
|
||||
}
|
||||
|
||||
/**
|
||||
* Formates the text of the mail.
|
||||
* @param {number} tenantId - Tenant id.
|
||||
* @param {number} estimateId - Estimate id.
|
||||
* @returns {Promise<Record<string, any>>}
|
||||
*/
|
||||
public formatterData = async (tenantId: number, estimateId: number) => {
|
||||
const estimate = await this.getSaleEstimateService.getEstimate(
|
||||
tenantId,
|
||||
estimateId
|
||||
);
|
||||
return {
|
||||
CustomerName: estimate.customer.displayName,
|
||||
EstimateNumber: estimate.estimateNumber,
|
||||
EstimateDate: estimate.formattedEstimateDate,
|
||||
EstimateAmount: estimate.formattedAmount,
|
||||
EstimateExpirationDate: estimate.formattedExpirationDate,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the mail options.
|
||||
* @param {number} tenantId
|
||||
* @param {number} saleEstimateId
|
||||
* @returns {Promise<SaleEstimateMailOptions>}
|
||||
*/
|
||||
public getMailOptions = async (
|
||||
tenantId: number,
|
||||
saleEstimateId: number
|
||||
): Promise<SaleEstimateMailOptions> => {
|
||||
const { SaleEstimate } = this.tenancy.models(tenantId);
|
||||
|
||||
const saleEstimate = await SaleEstimate.query()
|
||||
.findById(saleEstimateId)
|
||||
.throwIfNotFound();
|
||||
|
||||
const formatterData = await this.formatterData(tenantId, saleEstimateId);
|
||||
|
||||
const mailOptions = await this.contactMailNotification.getMailOptions(
|
||||
tenantId,
|
||||
saleEstimate.customerId,
|
||||
DEFAULT_ESTIMATE_REMINDER_MAIL_SUBJECT,
|
||||
DEFAULT_ESTIMATE_REMINDER_MAIL_CONTENT,
|
||||
formatterData
|
||||
);
|
||||
return {
|
||||
...mailOptions,
|
||||
data: formatterData,
|
||||
attachEstimate: true
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Sends the mail notification of the given sale estimate.
|
||||
* @param {number} tenantId
|
||||
* @param {number} saleEstimateId
|
||||
* @param {SaleEstimateMailOptions} messageOptions
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
public async sendMail(
|
||||
tenantId: number,
|
||||
saleEstimateId: number,
|
||||
messageOptions: SaleEstimateMailOptionsDTO
|
||||
): Promise<void> {
|
||||
const localMessageOpts = await this.getMailOptions(
|
||||
tenantId,
|
||||
saleEstimateId
|
||||
);
|
||||
// Overrides and validates the given mail options.
|
||||
const messageOpts = parseAndValidateMailOptions(
|
||||
localMessageOpts,
|
||||
messageOptions
|
||||
);
|
||||
const mail = new Mail()
|
||||
.setSubject(messageOpts.subject)
|
||||
.setTo(messageOpts.to)
|
||||
.setContent(messageOpts.body);
|
||||
|
||||
if (messageOpts.attachEstimate) {
|
||||
const estimatePdfBuffer = await this.estimatePdf.getSaleEstimatePdf(
|
||||
tenantId,
|
||||
saleEstimateId
|
||||
);
|
||||
mail.setAttachments([
|
||||
{
|
||||
filename: messageOpts.data?.EstimateNumber || 'estimate.pdf',
|
||||
content: estimatePdfBuffer,
|
||||
},
|
||||
]);
|
||||
}
|
||||
await mail.send();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import Container, { Service } from 'typedi';
|
||||
import { SendSaleEstimateMail } from './SendSaleEstimateMail';
|
||||
|
||||
@Service()
|
||||
export class SendSaleEstimateMailJob {
|
||||
/**
|
||||
* Constructor method.
|
||||
*/
|
||||
constructor(agenda) {
|
||||
agenda.define(
|
||||
'sale-estimate-mail-send',
|
||||
{ priority: 'high', concurrency: 2 },
|
||||
this.handler
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggers sending invoice mail.
|
||||
*/
|
||||
private handler = async (job, done: Function) => {
|
||||
const { tenantId, saleEstimateId, messageOptions } = job.attrs.data;
|
||||
const sendSaleEstimateMail = Container.get(SendSaleEstimateMail);
|
||||
|
||||
try {
|
||||
await sendSaleEstimateMail.sendMail(
|
||||
tenantId,
|
||||
saleEstimateId,
|
||||
messageOptions
|
||||
);
|
||||
done();
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
done(error);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1,3 +1,18 @@
|
||||
export const DEFAULT_ESTIMATE_REMINDER_MAIL_SUBJECT =
|
||||
'Estimate {EstimateNumber} is awaiting your approval';
|
||||
export const DEFAULT_ESTIMATE_REMINDER_MAIL_CONTENT = `<p>Dear {CustomerName}</p>
|
||||
<p>Thank you for your business, You can view or print your estimate from attachements.</p>
|
||||
<p>
|
||||
Estimate <strong>#{EstimateNumber}</strong><br />
|
||||
Expiration Date : <strong>{EstimateExpirationDate}</strong><br />
|
||||
Amount : <strong>{EstimateAmount}</strong></br />
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<i>Regards</i><br />
|
||||
<i>{CompanyName}</i>
|
||||
</p>
|
||||
`;
|
||||
|
||||
export const ERRORS = {
|
||||
SALE_ESTIMATE_NOT_FOUND: 'SALE_ESTIMATE_NOT_FOUND',
|
||||
@@ -8,7 +23,7 @@ export const ERRORS = {
|
||||
CUSTOMER_HAS_SALES_ESTIMATES: 'CUSTOMER_HAS_SALES_ESTIMATES',
|
||||
SALE_ESTIMATE_NO_IS_REQUIRED: 'SALE_ESTIMATE_NO_IS_REQUIRED',
|
||||
SALE_ESTIMATE_ALREADY_DELIVERED: 'SALE_ESTIMATE_ALREADY_DELIVERED',
|
||||
SALE_ESTIMATE_ALREADY_APPROVED: 'SALE_ESTIMATE_ALREADY_APPROVED'
|
||||
SALE_ESTIMATE_ALREADY_APPROVED: 'SALE_ESTIMATE_ALREADY_APPROVED',
|
||||
};
|
||||
|
||||
export const DEFAULT_VIEW_COLUMNS = [];
|
||||
|
||||
@@ -24,8 +24,7 @@ export class GetSaleInvoice {
|
||||
*/
|
||||
public async getSaleInvoice(
|
||||
tenantId: number,
|
||||
saleInvoiceId: number,
|
||||
authorizedUser: ISystemUser
|
||||
saleInvoiceId: number
|
||||
): Promise<ISaleInvoice> {
|
||||
const { SaleInvoice } = this.tenancy.models(tenantId);
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
export class GetSaleInvoiceMailReminder {
|
||||
public getInvoiceMailReminder(tenantId: number, saleInvoiceId: number) {}
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { ChromiumlyTenancy } from '@/services/ChromiumlyTenancy/ChromiumlyTenancy';
|
||||
import { TemplateInjectable } from '@/services/TemplateInjectable/TemplateInjectable';
|
||||
import { ISaleInvoice } from '@/interfaces';
|
||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
import { CommandSaleInvoiceValidators } from './CommandSaleInvoiceValidators';
|
||||
|
||||
@Service()
|
||||
export class SaleInvoicePdf {
|
||||
@@ -11,16 +12,34 @@ export class SaleInvoicePdf {
|
||||
@Inject()
|
||||
private templateInjectable: TemplateInjectable;
|
||||
|
||||
@Inject()
|
||||
private validators: CommandSaleInvoiceValidators;
|
||||
|
||||
@Inject()
|
||||
private tenancy: HasTenancyService;
|
||||
|
||||
/**
|
||||
* Retrieve sale invoice pdf content.
|
||||
* @param {number} tenantId - Tenant Id.
|
||||
* @param {ISaleInvoice} saleInvoice -
|
||||
* @returns {Promise<Buffer>}
|
||||
*/
|
||||
async saleInvoicePdf(
|
||||
public async saleInvoicePdf(
|
||||
tenantId: number,
|
||||
saleInvoice: ISaleInvoice
|
||||
invoiceId: number
|
||||
): Promise<Buffer> {
|
||||
const { SaleInvoice } = this.tenancy.models(tenantId);
|
||||
|
||||
const saleInvoice = await SaleInvoice.query()
|
||||
.findById(invoiceId)
|
||||
.withGraphFetched('entries.item')
|
||||
.withGraphFetched('entries.tax')
|
||||
.withGraphFetched('customer')
|
||||
.withGraphFetched('taxes.taxRate');
|
||||
|
||||
// Validates the given sale invoice existance.
|
||||
this.validators.validateInvoiceExistance(saleInvoice);
|
||||
|
||||
const htmlContent = await this.templateInjectable.render(
|
||||
tenantId,
|
||||
'modules/invoice-regular',
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
ISystemUser,
|
||||
ITenantUser,
|
||||
InvoiceNotificationType,
|
||||
SendInvoiceMailDTO,
|
||||
} from '@/interfaces';
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { CreateSaleInvoice } from './CreateSaleInvoice';
|
||||
@@ -24,6 +25,9 @@ import { WriteoffSaleInvoice } from './WriteoffSaleInvoice';
|
||||
import { SaleInvoicePdf } from './SaleInvoicePdf';
|
||||
import { GetInvoicePaymentsService } from './GetInvoicePaymentsService';
|
||||
import { SaleInvoiceNotifyBySms } from './SaleInvoiceNotifyBySms';
|
||||
import { SendInvoiceMailReminder } from './SendSaleInvoiceMailReminder';
|
||||
import { SendSaleInvoiceMail } from './SendSaleInvoiceMail';
|
||||
import { GetSaleInvoiceMailReminder } from './GetSaleInvoiceMailReminder';
|
||||
|
||||
@Service()
|
||||
export class SaleInvoiceApplication {
|
||||
@@ -60,6 +64,15 @@ export class SaleInvoiceApplication {
|
||||
@Inject()
|
||||
private invoiceSms: SaleInvoiceNotifyBySms;
|
||||
|
||||
@Inject()
|
||||
private sendInvoiceReminderService: SendInvoiceMailReminder;
|
||||
|
||||
@Inject()
|
||||
private sendSaleInvoiceMailService: SendSaleInvoiceMail;
|
||||
|
||||
@Inject()
|
||||
private getSaleInvoiceReminderService: GetSaleInvoiceMailReminder;
|
||||
|
||||
/**
|
||||
* Creates a new sale invoice with associated GL entries.
|
||||
* @param {number} tenantId
|
||||
@@ -236,13 +249,13 @@ export class SaleInvoiceApplication {
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {number} tenantId ]
|
||||
* @param saleInvoice
|
||||
* @returns
|
||||
* Retrieves the pdf buffer of the given sale invoice.
|
||||
* @param {number} tenantId - Tenant id.
|
||||
* @param {number} saleInvoice
|
||||
* @returns {Promise<Buffer>}
|
||||
*/
|
||||
public saleInvoicePdf(tenantId: number, saleInvoice) {
|
||||
return this.pdfSaleInvoiceService.saleInvoicePdf(tenantId, saleInvoice);
|
||||
public saleInvoicePdf(tenantId: number, saleInvoiceId: number) {
|
||||
return this.pdfSaleInvoiceService.saleInvoicePdf(tenantId, saleInvoiceId);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -279,4 +292,67 @@ export class SaleInvoiceApplication {
|
||||
invoiceSmsDetailsDTO
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the metadata of invoice mail reminder.
|
||||
* @param {number} tenantId
|
||||
* @param {number} saleInvoiceId
|
||||
* @returns {}
|
||||
*/
|
||||
public getSaleInvoiceMailReminder(tenantId: number, saleInvoiceId: number) {
|
||||
return this.sendInvoiceReminderService.getMailOption(
|
||||
tenantId,
|
||||
saleInvoiceId
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends reminder of the given invoice to the invoice's customer.
|
||||
* @param {number} tenantId
|
||||
* @param {number} saleInvoiceId
|
||||
* @returns {}
|
||||
*/
|
||||
public sendSaleInvoiceMailReminder(
|
||||
tenantId: number,
|
||||
saleInvoiceId: number,
|
||||
messageDTO: SendInvoiceMailDTO
|
||||
) {
|
||||
return this.sendInvoiceReminderService.triggerMail(
|
||||
tenantId,
|
||||
saleInvoiceId,
|
||||
messageDTO
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends the invoice mail of the given sale invoice.
|
||||
* @param {number} tenantId
|
||||
* @param {number} saleInvoiceId
|
||||
* @param {SendInvoiceMailDTO} messageDTO
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
public sendSaleInvoiceMail(
|
||||
tenantId: number,
|
||||
saleInvoiceId: number,
|
||||
messageDTO: SendInvoiceMailDTO
|
||||
) {
|
||||
return this.sendSaleInvoiceMailService.triggerMail(
|
||||
tenantId,
|
||||
saleInvoiceId,
|
||||
messageDTO
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the default mail options of the given sale invoice.
|
||||
* @param {number} tenantId
|
||||
* @param {number} saleInvoiceid
|
||||
* @returns {Promise<SendInvoiceMailDTO>}
|
||||
*/
|
||||
public getSaleInvoiceMail(tenantId: number, saleInvoiceid: number) {
|
||||
return this.sendSaleInvoiceMailService.getMailOption(
|
||||
tenantId,
|
||||
saleInvoiceid
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { SaleInvoiceMailOptions } from '@/interfaces';
|
||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
import { GetSaleInvoice } from './GetSaleInvoice';
|
||||
import { ContactMailNotification } from '@/services/MailNotification/ContactMailNotification';
|
||||
import {
|
||||
DEFAULT_INVOICE_MAIL_CONTENT,
|
||||
DEFAULT_INVOICE_MAIL_SUBJECT,
|
||||
} from './constants';
|
||||
|
||||
@Service()
|
||||
export class SendSaleInvoiceMailCommon {
|
||||
@Inject()
|
||||
private tenancy: HasTenancyService;
|
||||
|
||||
@Inject()
|
||||
private getSaleInvoiceService: GetSaleInvoice;
|
||||
|
||||
@Inject()
|
||||
private contactMailNotification: ContactMailNotification;
|
||||
|
||||
/**
|
||||
* Retrieves the mail options.
|
||||
* @param {number} tenantId - Tenant id.
|
||||
* @param {number} invoiceId - Invoice id.
|
||||
* @param {string} defaultSubject - Subject text.
|
||||
* @param {string} defaultBody - Subject body.
|
||||
* @returns {Promise<SaleInvoiceMailOptions>}
|
||||
*/
|
||||
public async getMailOption(
|
||||
tenantId: number,
|
||||
invoiceId: number,
|
||||
defaultSubject: string = DEFAULT_INVOICE_MAIL_SUBJECT,
|
||||
defaultBody: string = DEFAULT_INVOICE_MAIL_CONTENT
|
||||
): Promise<SaleInvoiceMailOptions> {
|
||||
const { SaleInvoice } = this.tenancy.models(tenantId);
|
||||
|
||||
const saleInvoice = await SaleInvoice.query()
|
||||
.findById(invoiceId)
|
||||
.throwIfNotFound();
|
||||
|
||||
const formatterData = await this.formatText(tenantId, invoiceId);
|
||||
|
||||
const mailOptions = await this.contactMailNotification.getMailOptions(
|
||||
tenantId,
|
||||
saleInvoice.customerId,
|
||||
defaultSubject,
|
||||
defaultBody,
|
||||
formatterData
|
||||
);
|
||||
return {
|
||||
...mailOptions,
|
||||
attachInvoice: true,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the formatted text of the given sale invoice.
|
||||
* @param {number} tenantId - Tenant id.
|
||||
* @param {number} invoiceId - Sale invoice id.
|
||||
* @param {string} text - The given text.
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
public formatText = async (
|
||||
tenantId: number,
|
||||
invoiceId: number
|
||||
): Promise<Record<string, string | number>> => {
|
||||
const invoice = await this.getSaleInvoiceService.getSaleInvoice(
|
||||
tenantId,
|
||||
invoiceId
|
||||
);
|
||||
|
||||
return {
|
||||
CustomerName: invoice.customer.displayName,
|
||||
InvoiceNumber: invoice.invoiceNo,
|
||||
InvoiceDueAmount: invoice.dueAmountFormatted,
|
||||
InvoiceDueDate: invoice.dueDateFormatted,
|
||||
InvoiceDate: invoice.invoiceDateFormatted,
|
||||
InvoiceAmount: invoice.totalFormatted,
|
||||
OverdueDays: invoice.overdueDays,
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import Mail from '@/lib/Mail';
|
||||
import { SendInvoiceMailDTO } from '@/interfaces';
|
||||
import { SaleInvoicePdf } from './SaleInvoicePdf';
|
||||
import { SendSaleInvoiceMailCommon } from './SendInvoiceInvoiceMailCommon';
|
||||
import {
|
||||
DEFAULT_INVOICE_MAIL_CONTENT,
|
||||
DEFAULT_INVOICE_MAIL_SUBJECT,
|
||||
} from './constants';
|
||||
import { parseAndValidateMailOptions } from '@/services/MailNotification/utils';
|
||||
|
||||
@Service()
|
||||
export class SendSaleInvoiceMail {
|
||||
@Inject()
|
||||
private invoicePdf: SaleInvoicePdf;
|
||||
|
||||
@Inject()
|
||||
private invoiceMail: SendSaleInvoiceMailCommon;
|
||||
|
||||
@Inject('agenda')
|
||||
private agenda: any;
|
||||
|
||||
/**
|
||||
* Sends the invoice mail of the given sale invoice.
|
||||
* @param {number} tenantId
|
||||
* @param {number} saleInvoiceId
|
||||
* @param {SendInvoiceMailDTO} messageDTO
|
||||
*/
|
||||
public async triggerMail(
|
||||
tenantId: number,
|
||||
saleInvoiceId: number,
|
||||
messageDTO: SendInvoiceMailDTO
|
||||
) {
|
||||
const payload = {
|
||||
tenantId,
|
||||
saleInvoiceId,
|
||||
messageDTO,
|
||||
};
|
||||
await this.agenda.now('sale-invoice-mail-send', payload);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the mail options of the given sale invoice.
|
||||
* @param {number} tenantId
|
||||
* @param {number} saleInvoiceId
|
||||
* @returns {Promise<SaleInvoiceMailOptions>}
|
||||
*/
|
||||
public async getMailOption(tenantId: number, saleInvoiceId: number) {
|
||||
return this.invoiceMail.getMailOption(
|
||||
tenantId,
|
||||
saleInvoiceId,
|
||||
DEFAULT_INVOICE_MAIL_SUBJECT,
|
||||
DEFAULT_INVOICE_MAIL_CONTENT
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggers the mail invoice.
|
||||
* @param {number} tenantId
|
||||
* @param {number} saleInvoiceId
|
||||
* @param {SendInvoiceMailDTO} messageDTO
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
public async sendMail(
|
||||
tenantId: number,
|
||||
saleInvoiceId: number,
|
||||
messageDTO: SendInvoiceMailDTO
|
||||
) {
|
||||
const defaultMessageOpts = await this.getMailOption(
|
||||
tenantId,
|
||||
saleInvoiceId
|
||||
);
|
||||
// Merge message opts with default options and validate the incoming options.
|
||||
const messageOpts = parseAndValidateMailOptions(
|
||||
defaultMessageOpts,
|
||||
messageDTO
|
||||
);
|
||||
const mail = new Mail()
|
||||
.setSubject(messageOpts.subject)
|
||||
.setTo(messageOpts.to)
|
||||
.setContent(messageOpts.body);
|
||||
|
||||
if (messageOpts.attachInvoice) {
|
||||
// Retrieves document buffer of the invoice pdf document.
|
||||
const invoicePdfBuffer = await this.invoicePdf.saleInvoicePdf(
|
||||
tenantId,
|
||||
saleInvoiceId
|
||||
);
|
||||
mail.setAttachments([
|
||||
{ filename: 'invoice.pdf', content: invoicePdfBuffer },
|
||||
]);
|
||||
}
|
||||
await mail.send();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import Container, { Service } from 'typedi';
|
||||
import events from '@/subscribers/events';
|
||||
import { SendSaleInvoiceMail } from './SendSaleInvoiceMail';
|
||||
|
||||
@Service()
|
||||
export class SendSaleInvoiceMailJob {
|
||||
/**
|
||||
* Constructor method.
|
||||
*/
|
||||
constructor(agenda) {
|
||||
agenda.define(
|
||||
'sale-invoice-mail-send',
|
||||
{ priority: 'high', concurrency: 2 },
|
||||
this.handler
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggers sending invoice mail.
|
||||
*/
|
||||
private handler = async (job, done: Function) => {
|
||||
const { tenantId, saleInvoiceId, messageDTO } = job.attrs.data;
|
||||
const sendInvoiceMail = Container.get(SendSaleInvoiceMail);
|
||||
|
||||
try {
|
||||
await sendInvoiceMail.sendMail(tenantId, saleInvoiceId, messageDTO);
|
||||
done();
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
done(error);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { SendInvoiceMailDTO } from '@/interfaces';
|
||||
import Mail from '@/lib/Mail';
|
||||
import { SaleInvoicePdf } from './SaleInvoicePdf';
|
||||
import { SendSaleInvoiceMailCommon } from './SendInvoiceInvoiceMailCommon';
|
||||
import {
|
||||
DEFAULT_INVOICE_REMINDER_MAIL_CONTENT,
|
||||
DEFAULT_INVOICE_REMINDER_MAIL_SUBJECT,
|
||||
} from './constants';
|
||||
|
||||
@Service()
|
||||
export class SendInvoiceMailReminder {
|
||||
@Inject('agenda')
|
||||
private agenda: any;
|
||||
|
||||
@Inject()
|
||||
private invoicePdf: SaleInvoicePdf;
|
||||
|
||||
@Inject()
|
||||
private invoiceCommonMail: SendSaleInvoiceMailCommon;
|
||||
|
||||
/**
|
||||
* Triggers the reminder mail of the given sale invoice.
|
||||
* @param {number} tenantId
|
||||
* @param {number} saleInvoiceId
|
||||
*/
|
||||
public async triggerMail(
|
||||
tenantId: number,
|
||||
saleInvoiceId: number,
|
||||
messageOptions: SendInvoiceMailDTO
|
||||
) {
|
||||
const payload = {
|
||||
tenantId,
|
||||
saleInvoiceId,
|
||||
messageOptions,
|
||||
};
|
||||
await this.agenda.now('sale-invoice-reminder-mail-send', payload);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the mail options of the given sale invoice.
|
||||
* @param {number} tenantId
|
||||
* @param {number} saleInvoiceId
|
||||
* @returns {Promise<SaleInvoiceMailOptions>}
|
||||
*/
|
||||
public async getMailOption(tenantId: number, saleInvoiceId: number) {
|
||||
return this.invoiceCommonMail.getMailOption(
|
||||
tenantId,
|
||||
saleInvoiceId,
|
||||
DEFAULT_INVOICE_REMINDER_MAIL_SUBJECT,
|
||||
DEFAULT_INVOICE_REMINDER_MAIL_CONTENT
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggers the mail invoice.
|
||||
* @param {number} tenantId
|
||||
* @param {number} saleInvoiceId
|
||||
* @param {SendInvoiceMailDTO} messageOptions
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
public async sendMail(
|
||||
tenantId: number,
|
||||
saleInvoiceId: number,
|
||||
messageOptions: SendInvoiceMailDTO
|
||||
) {
|
||||
const localMessageOpts = await this.getMailOption(tenantId, saleInvoiceId);
|
||||
|
||||
const messageOpts = {
|
||||
...localMessageOpts,
|
||||
...messageOptions,
|
||||
};
|
||||
const mail = new Mail()
|
||||
.setSubject(messageOpts.subject)
|
||||
.setTo(messageOpts.to)
|
||||
.setContent(messageOpts.body);
|
||||
|
||||
if (messageOpts.attachInvoice) {
|
||||
// Retrieves document buffer of the invoice pdf document.
|
||||
const invoicePdfBuffer = await this.invoicePdf.saleInvoicePdf(
|
||||
tenantId,
|
||||
saleInvoiceId
|
||||
);
|
||||
mail.setAttachments([
|
||||
{ filename: 'invoice.pdf', content: invoicePdfBuffer },
|
||||
]);
|
||||
}
|
||||
await mail.send();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import Container, { Service } from 'typedi';
|
||||
import { SendInvoiceMailReminder } from './SendSaleInvoiceMailReminder';
|
||||
|
||||
@Service()
|
||||
export class SendSaleInvoiceReminderMailJob {
|
||||
/**
|
||||
* Constructor method.
|
||||
*/
|
||||
constructor(agenda) {
|
||||
agenda.define(
|
||||
'sale-invoice-reminder-mail-send',
|
||||
{ priority: 'high', concurrency: 1 },
|
||||
this.handler
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggers sending invoice mail.
|
||||
*/
|
||||
private handler = async (job, done: Function) => {
|
||||
const { tenantId, saleInvoiceId, messageOptions } = job.attrs.data;
|
||||
const sendInvoiceMail = Container.get(SendInvoiceMailReminder);
|
||||
|
||||
try {
|
||||
await sendInvoiceMail.sendMail(tenantId, saleInvoiceId, messageOptions);
|
||||
done();
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
done(error);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1,3 +1,35 @@
|
||||
export const DEFAULT_INVOICE_MAIL_SUBJECT =
|
||||
'Invoice {InvoiceNumber} from {CompanyName}';
|
||||
export const DEFAULT_INVOICE_MAIL_CONTENT = `
|
||||
<p>Dear {CustomerName}</p>
|
||||
<p>Thank you for your business, You can view or print your invoice from attachements.</p>
|
||||
<p>
|
||||
Invoice <strong>#{InvoiceNumber}</strong><br />
|
||||
Due Date : <strong>{InvoiceDueDate}</strong><br />
|
||||
Amount : <strong>{InvoiceAmount}</strong></br />
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<i>Regards</i><br />
|
||||
<i>{CompanyName}</i>
|
||||
</p>
|
||||
`;
|
||||
|
||||
export const DEFAULT_INVOICE_REMINDER_MAIL_SUBJECT =
|
||||
'Invoice {InvoiceNumber} reminder from {CompanyName}';
|
||||
export const DEFAULT_INVOICE_REMINDER_MAIL_CONTENT = `
|
||||
<p>Dear {CustomerName}</p>
|
||||
<p>You might have missed the payment date and the invoice is now overdue by {OverdueDays} days.</p>
|
||||
<p>Invoice <strong>#{InvoiceNumber}</strong><br />
|
||||
Due Date : <strong>{InvoiceDueDate}</strong><br />
|
||||
Amount : <strong>{InvoiceAmount}</strong></p>
|
||||
|
||||
<p>
|
||||
<i>Regards</i><br />
|
||||
<i>{CompanyName}</i>
|
||||
</p>
|
||||
`;
|
||||
|
||||
export const ERRORS = {
|
||||
INVOICE_NUMBER_NOT_UNIQUE: 'INVOICE_NUMBER_NOT_UNIQUE',
|
||||
SALE_INVOICE_NOT_FOUND: 'SALE_INVOICE_NOT_FOUND',
|
||||
@@ -16,6 +48,7 @@ export const ERRORS = {
|
||||
PAYMENT_ACCOUNT_CURRENCY_INVALID: 'PAYMENT_ACCOUNT_CURRENCY_INVALID',
|
||||
SALE_INVOICE_ALREADY_WRITTEN_OFF: 'SALE_INVOICE_ALREADY_WRITTEN_OFF',
|
||||
SALE_INVOICE_NOT_WRITTEN_OFF: 'SALE_INVOICE_NOT_WRITTEN_OFF',
|
||||
NO_INVOICE_CUSTOMER_EMAIL_ADDR: 'NO_INVOICE_CUSTOMER_EMAIL_ADDR',
|
||||
};
|
||||
|
||||
export const DEFAULT_VIEW_COLUMNS = [];
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { ChromiumlyTenancy } from '@/services/ChromiumlyTenancy/ChromiumlyTenancy';
|
||||
import { TemplateInjectable } from '@/services/TemplateInjectable/TemplateInjectable';
|
||||
import { IPaymentReceive } from '@/interfaces';
|
||||
import { GetPaymentReceive } from './GetPaymentReceive';
|
||||
|
||||
@Service()
|
||||
export default class GetPaymentReceivePdf {
|
||||
@@ -11,6 +11,9 @@ export default class GetPaymentReceivePdf {
|
||||
@Inject()
|
||||
private templateInjectable: TemplateInjectable;
|
||||
|
||||
@Inject()
|
||||
private getPaymentService: GetPaymentReceive;
|
||||
|
||||
/**
|
||||
* Retrieve sale invoice pdf content.
|
||||
* @param {number} tenantId -
|
||||
@@ -19,8 +22,12 @@ export default class GetPaymentReceivePdf {
|
||||
*/
|
||||
async getPaymentReceivePdf(
|
||||
tenantId: number,
|
||||
paymentReceive: IPaymentReceive
|
||||
paymentReceiveId: number
|
||||
): Promise<Buffer> {
|
||||
const paymentReceive = await this.getPaymentService.getPaymentReceive(
|
||||
tenantId,
|
||||
paymentReceiveId
|
||||
);
|
||||
const htmlContent = await this.templateInjectable.render(
|
||||
tenantId,
|
||||
'modules/payment-receive-standard',
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import {
|
||||
PaymentReceiveMailOpts,
|
||||
PaymentReceiveMailOptsDTO,
|
||||
SendInvoiceMailDTO,
|
||||
} from '@/interfaces';
|
||||
import Mail from '@/lib/Mail';
|
||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
import {
|
||||
DEFAULT_PAYMENT_MAIL_CONTENT,
|
||||
DEFAULT_PAYMENT_MAIL_SUBJECT,
|
||||
} from './constants';
|
||||
import { GetPaymentReceive } from './GetPaymentReceive';
|
||||
import { ContactMailNotification } from '@/services/MailNotification/ContactMailNotification';
|
||||
import { parseAndValidateMailOptions } from '@/services/MailNotification/utils';
|
||||
|
||||
@Service()
|
||||
export class SendPaymentReceiveMailNotification {
|
||||
@Inject()
|
||||
private tenancy: HasTenancyService;
|
||||
|
||||
@Inject()
|
||||
private getPaymentService: GetPaymentReceive;
|
||||
|
||||
@Inject()
|
||||
private contactMailNotification: ContactMailNotification;
|
||||
|
||||
@Inject('agenda')
|
||||
private agenda: any;
|
||||
|
||||
/**
|
||||
* Sends the mail of the given payment receive.
|
||||
* @param {number} tenantId
|
||||
* @param {number} paymentReceiveId
|
||||
* @param {PaymentReceiveMailOptsDTO} messageDTO
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
public async triggerMail(
|
||||
tenantId: number,
|
||||
paymentReceiveId: number,
|
||||
messageDTO: PaymentReceiveMailOptsDTO
|
||||
): Promise<void> {
|
||||
const payload = {
|
||||
tenantId,
|
||||
paymentReceiveId,
|
||||
messageDTO,
|
||||
};
|
||||
await this.agenda.now('payment-receive-mail-send', payload);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the default payment mail options.
|
||||
* @param {number} tenantId - Tenant id.
|
||||
* @param {number} paymentReceiveId - Payment receive id.
|
||||
* @returns {Promise<PaymentReceiveMailOpts>}
|
||||
*/
|
||||
public getMailOptions = async (
|
||||
tenantId: number,
|
||||
paymentId: number
|
||||
): Promise<PaymentReceiveMailOpts> => {
|
||||
const { PaymentReceive } = this.tenancy.models(tenantId);
|
||||
|
||||
const paymentReceive = await PaymentReceive.query()
|
||||
.findById(paymentId)
|
||||
.throwIfNotFound();
|
||||
|
||||
const formatterData = await this.textFormatter(tenantId, paymentId);
|
||||
|
||||
return this.contactMailNotification.getMailOptions(
|
||||
tenantId,
|
||||
paymentReceive.customerId,
|
||||
DEFAULT_PAYMENT_MAIL_SUBJECT,
|
||||
DEFAULT_PAYMENT_MAIL_CONTENT,
|
||||
formatterData
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the formatted text of the given sale invoice.
|
||||
* @param {number} tenantId - Tenant id.
|
||||
* @param {number} invoiceId - Sale invoice id.
|
||||
* @param {string} text - The given text.
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
public textFormatter = async (
|
||||
tenantId: number,
|
||||
invoiceId: number
|
||||
): Promise<Record<string, string>> => {
|
||||
const payment = await this.getPaymentService.getPaymentReceive(
|
||||
tenantId,
|
||||
invoiceId
|
||||
);
|
||||
return {
|
||||
CustomerName: payment.customer.displayName,
|
||||
PaymentNumber: payment.payment_receive_no,
|
||||
PaymentDate: payment.formattedPaymentDate,
|
||||
PaymentAmount: payment.formattedAmount,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Triggers the mail invoice.
|
||||
* @param {number} tenantId
|
||||
* @param {number} saleInvoiceId
|
||||
* @param {SendInvoiceMailDTO} messageDTO
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
public async sendMail(
|
||||
tenantId: number,
|
||||
paymentReceiveId: number,
|
||||
messageDTO: SendInvoiceMailDTO
|
||||
): Promise<void> {
|
||||
const defaultMessageOpts = await this.getMailOptions(
|
||||
tenantId,
|
||||
paymentReceiveId
|
||||
);
|
||||
// Parsed message opts with default options.
|
||||
const parsedMessageOpts = parseAndValidateMailOptions(
|
||||
defaultMessageOpts,
|
||||
messageDTO
|
||||
);
|
||||
await new Mail()
|
||||
.setSubject(parsedMessageOpts.subject)
|
||||
.setTo(parsedMessageOpts.to)
|
||||
.setContent(parsedMessageOpts.body)
|
||||
.send();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import Container, { Service } from 'typedi';
|
||||
import events from '@/subscribers/events';
|
||||
import { SendPaymentReceiveMailNotification } from './PaymentReceiveMailNotification';
|
||||
|
||||
@Service()
|
||||
export class PaymentReceiveMailNotificationJob {
|
||||
/**
|
||||
* Constructor method.
|
||||
*/
|
||||
constructor(agenda) {
|
||||
agenda.define(
|
||||
'payment-receive-mail-send',
|
||||
{ priority: 'high', concurrency: 2 },
|
||||
this.handler
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggers sending payment notification via mail.
|
||||
*/
|
||||
private handler = async (job, done: Function) => {
|
||||
const { tenantId, paymentReceiveId, messageDTO } = job.attrs.data;
|
||||
const paymentMail = Container.get(SendPaymentReceiveMailNotification);
|
||||
|
||||
console.log(tenantId, paymentReceiveId, messageDTO);
|
||||
|
||||
try {
|
||||
await paymentMail.sendMail(tenantId, paymentReceiveId, messageDTO);
|
||||
done();
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
done(error);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
IPaymentReceiveSmsDetails,
|
||||
IPaymentReceivesFilter,
|
||||
ISystemUser,
|
||||
PaymentReceiveMailOptsDTO,
|
||||
} from '@/interfaces';
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { CreatePaymentReceive } from './CreatePaymentReceive';
|
||||
@@ -17,7 +18,7 @@ import { GetPaymentReceive } from './GetPaymentReceive';
|
||||
import { GetPaymentReceiveInvoices } from './GetPaymentReceiveInvoices';
|
||||
import { PaymentReceiveNotifyBySms } from './PaymentReceiveSmsNotify';
|
||||
import GetPaymentReceivePdf from './GetPaymentReeceivePdf';
|
||||
import { PaymentReceive } from '@/models';
|
||||
import { SendPaymentReceiveMailNotification } from './PaymentReceiveMailNotification';
|
||||
|
||||
@Service()
|
||||
export class PaymentReceivesApplication {
|
||||
@@ -42,6 +43,9 @@ export class PaymentReceivesApplication {
|
||||
@Inject()
|
||||
private paymentSmsNotify: PaymentReceiveNotifyBySms;
|
||||
|
||||
@Inject()
|
||||
private paymentMailNotify: SendPaymentReceiveMailNotification;
|
||||
|
||||
@Inject()
|
||||
private getPaymentReceivePdfService: GetPaymentReceivePdf;
|
||||
|
||||
@@ -176,18 +180,47 @@ export class PaymentReceivesApplication {
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve PDF content of the given payment receive.
|
||||
* Notify customer via mail about payment receive details.
|
||||
* @param {number} tenantId
|
||||
* @param {number} paymentReceiveId
|
||||
* @param {IPaymentReceiveMailOpts} messageOpts
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
public notifyPaymentByMail(
|
||||
tenantId: number,
|
||||
paymentReceiveId: number,
|
||||
messageOpts: PaymentReceiveMailOptsDTO
|
||||
): Promise<void> {
|
||||
return this.paymentMailNotify.triggerMail(
|
||||
tenantId,
|
||||
paymentReceiveId,
|
||||
messageOpts
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the default mail options of the given payment transaction.
|
||||
* @param {number} tenantId
|
||||
* @param {number} paymentReceiveId
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
public getPaymentMailOptions(tenantId: number, paymentReceiveId: number) {
|
||||
return this.paymentMailNotify.getMailOptions(tenantId, paymentReceiveId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve pdf content of the given payment receive.
|
||||
* @param {number} tenantId
|
||||
* @param {PaymentReceive} paymentReceive
|
||||
* @returns
|
||||
*/
|
||||
public getPaymentReceivePdf = (
|
||||
tenantId: number,
|
||||
paymentReceive: PaymentReceive
|
||||
paymentReceiveId: number
|
||||
) => {
|
||||
return this.getPaymentReceivePdfService.getPaymentReceivePdf(
|
||||
tenantId,
|
||||
paymentReceive
|
||||
paymentReceiveId
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,3 +1,18 @@
|
||||
export const DEFAULT_PAYMENT_MAIL_SUBJECT = 'Payment Received by {CompanyName}';
|
||||
export const DEFAULT_PAYMENT_MAIL_CONTENT = `
|
||||
<p>Dear {CustomerName}</p>
|
||||
<p>Thank you for your payment. It was a pleasure doing business with you. We look forward to work together again!</p>
|
||||
<p>
|
||||
Payment Date : <strong>{PaymentDate}</strong><br />
|
||||
Amount : <strong>{PaymentAmount}</strong></br />
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<i>Regards</i><br />
|
||||
<i>{CompanyName}</i>
|
||||
</p>
|
||||
`;
|
||||
|
||||
export const ERRORS = {
|
||||
PAYMENT_RECEIVE_NO_EXISTS: 'PAYMENT_RECEIVE_NO_EXISTS',
|
||||
PAYMENT_RECEIVE_NOT_EXISTS: 'PAYMENT_RECEIVE_NOT_EXISTS',
|
||||
@@ -12,6 +27,7 @@ export const ERRORS = {
|
||||
PAYMENT_CUSTOMER_SHOULD_NOT_UPDATE: 'PAYMENT_CUSTOMER_SHOULD_NOT_UPDATE',
|
||||
CUSTOMER_HAS_PAYMENT_RECEIVES: 'CUSTOMER_HAS_PAYMENT_RECEIVES',
|
||||
PAYMENT_ACCOUNT_CURRENCY_INVALID: 'PAYMENT_ACCOUNT_CURRENCY_INVALID',
|
||||
NO_INVOICE_CUSTOMER_EMAIL_ADDR: 'NO_INVOICE_CUSTOMER_EMAIL_ADDR',
|
||||
};
|
||||
|
||||
export const DEFAULT_VIEWS = [];
|
||||
|
||||
@@ -5,6 +5,8 @@ import {
|
||||
IPaginationMeta,
|
||||
ISaleReceipt,
|
||||
ISalesReceiptsFilter,
|
||||
SaleReceiptMailOpts,
|
||||
SaleReceiptMailOptsDTO,
|
||||
} from '@/interfaces';
|
||||
import { EditSaleReceipt } from './EditSaleReceipt';
|
||||
import { GetSaleReceipt } from './GetSaleReceipt';
|
||||
@@ -13,6 +15,7 @@ import { GetSaleReceipts } from './GetSaleReceipts';
|
||||
import { CloseSaleReceipt } from './CloseSaleReceipt';
|
||||
import { SaleReceiptsPdf } from './SaleReceiptsPdfService';
|
||||
import { SaleReceiptNotifyBySms } from './SaleReceiptNotifyBySms';
|
||||
import { SaleReceiptMailNotification } from './SaleReceiptMailNotification';
|
||||
|
||||
@Service()
|
||||
export class SaleReceiptApplication {
|
||||
@@ -40,6 +43,9 @@ export class SaleReceiptApplication {
|
||||
@Inject()
|
||||
private saleReceiptNotifyBySmsService: SaleReceiptNotifyBySms;
|
||||
|
||||
@Inject()
|
||||
private saleReceiptNotifyByMailService: SaleReceiptMailNotification;
|
||||
|
||||
/**
|
||||
* Creates a new sale receipt with associated entries.
|
||||
* @param {number} tenantId
|
||||
@@ -166,4 +172,38 @@ export class SaleReceiptApplication {
|
||||
saleReceiptId
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends the receipt mail of the given sale receipt.
|
||||
* @param {number} tenantId
|
||||
* @param {number} saleReceiptId
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
public sendSaleReceiptMail(
|
||||
tenantId: number,
|
||||
saleReceiptId: number,
|
||||
messageOpts: SaleReceiptMailOptsDTO
|
||||
): Promise<void> {
|
||||
return this.saleReceiptNotifyByMailService.triggerMail(
|
||||
tenantId,
|
||||
saleReceiptId,
|
||||
messageOpts
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the default mail options of the given sale receipt.
|
||||
* @param {number} tenantId
|
||||
* @param {number} saleReceiptId
|
||||
* @returns {Promise<SaleReceiptMailOpts>}
|
||||
*/
|
||||
public getSaleReceiptMail(
|
||||
tenantId: number,
|
||||
saleReceiptId: number
|
||||
): Promise<SaleReceiptMailOpts> {
|
||||
return this.saleReceiptNotifyByMailService.getMailOptions(
|
||||
tenantId,
|
||||
saleReceiptId
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
import { Inject, Service } from 'typedi';
|
||||
import Mail from '@/lib/Mail';
|
||||
import { GetSaleReceipt } from './GetSaleReceipt';
|
||||
import { SaleReceiptsPdf } from './SaleReceiptsPdfService';
|
||||
import {
|
||||
DEFAULT_RECEIPT_MAIL_CONTENT,
|
||||
DEFAULT_RECEIPT_MAIL_SUBJECT,
|
||||
} from './constants';
|
||||
import { SaleReceiptMailOpts, SaleReceiptMailOptsDTO } from '@/interfaces';
|
||||
import { ContactMailNotification } from '@/services/MailNotification/ContactMailNotification';
|
||||
import { parseAndValidateMailOptions } from '@/services/MailNotification/utils';
|
||||
|
||||
@Service()
|
||||
export class SaleReceiptMailNotification {
|
||||
@Inject()
|
||||
private tenancy: HasTenancyService;
|
||||
|
||||
@Inject()
|
||||
private getSaleReceiptService: GetSaleReceipt;
|
||||
|
||||
@Inject()
|
||||
private receiptPdfService: SaleReceiptsPdf;
|
||||
|
||||
@Inject()
|
||||
private contactMailNotification: ContactMailNotification;
|
||||
|
||||
@Inject('agenda')
|
||||
private agenda: any;
|
||||
|
||||
/**
|
||||
* Sends the receipt mail of the given sale receipt.
|
||||
* @param {number} tenantId
|
||||
* @param {number} saleReceiptId
|
||||
* @param {SaleReceiptMailOptsDTO} messageDTO
|
||||
*/
|
||||
public async triggerMail(
|
||||
tenantId: number,
|
||||
saleReceiptId: number,
|
||||
messageOpts: SaleReceiptMailOptsDTO
|
||||
) {
|
||||
const payload = {
|
||||
tenantId,
|
||||
saleReceiptId,
|
||||
messageOpts,
|
||||
};
|
||||
await this.agenda.now('sale-receipt-mail-send', payload);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the mail options of the given sale receipt.
|
||||
* @param {number} tenantId
|
||||
* @param {number} saleReceiptId
|
||||
* @returns {Promise<SaleReceiptMailOptsDTO>}
|
||||
*/
|
||||
public async getMailOptions(
|
||||
tenantId: number,
|
||||
saleReceiptId: number
|
||||
): Promise<SaleReceiptMailOpts> {
|
||||
const { SaleReceipt } = this.tenancy.models(tenantId);
|
||||
|
||||
const saleReceipt = await SaleReceipt.query()
|
||||
.findById(saleReceiptId)
|
||||
.throwIfNotFound();
|
||||
|
||||
const formattedData = await this.textFormatter(tenantId, saleReceiptId);
|
||||
|
||||
const mailOpts = await this.contactMailNotification.getMailOptions(
|
||||
tenantId,
|
||||
saleReceipt.customerId,
|
||||
DEFAULT_RECEIPT_MAIL_SUBJECT,
|
||||
DEFAULT_RECEIPT_MAIL_CONTENT,
|
||||
formattedData
|
||||
);
|
||||
return {
|
||||
...mailOpts,
|
||||
attachReceipt: true,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the formatted text of the given sale receipt.
|
||||
* @param {number} tenantId - Tenant id.
|
||||
* @param {number} receiptId - Sale receipt id.
|
||||
* @param {string} text - The given text.
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
public textFormatter = async (
|
||||
tenantId: number,
|
||||
receiptId: number
|
||||
): Promise<Record<string, string>> => {
|
||||
const receipt = await this.getSaleReceiptService.getSaleReceipt(
|
||||
tenantId,
|
||||
receiptId
|
||||
);
|
||||
return {
|
||||
CustomerName: receipt.customer.displayName,
|
||||
ReceiptNumber: receipt.receiptNumber,
|
||||
ReceiptDate: receipt.formattedReceiptDate,
|
||||
ReceiptAmount: receipt.formattedAmount,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Triggers the mail notification of the given sale receipt.
|
||||
* @param {number} tenantId - Tenant id.
|
||||
* @param {number} saleReceiptId - Sale receipt id.
|
||||
* @param {SaleReceiptMailOpts} messageDTO - Overrided message options.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
public async sendMail(
|
||||
tenantId: number,
|
||||
saleReceiptId: number,
|
||||
messageOpts: SaleReceiptMailOptsDTO
|
||||
) {
|
||||
const defaultMessageOpts = await this.getMailOptions(
|
||||
tenantId,
|
||||
saleReceiptId
|
||||
);
|
||||
// Merges message opts with default options.
|
||||
const parsedMessageOpts = parseAndValidateMailOptions(
|
||||
defaultMessageOpts,
|
||||
messageOpts
|
||||
);
|
||||
const mail = new Mail()
|
||||
.setSubject(parsedMessageOpts.subject)
|
||||
.setTo(parsedMessageOpts.to)
|
||||
.setContent(parsedMessageOpts.body);
|
||||
|
||||
if (parsedMessageOpts.attachReceipt) {
|
||||
// Retrieves document buffer of the receipt pdf document.
|
||||
const receiptPdfBuffer = await this.receiptPdfService.saleReceiptPdf(
|
||||
tenantId,
|
||||
saleReceiptId
|
||||
);
|
||||
mail.setAttachments([
|
||||
{ filename: 'receipt.pdf', content: receiptPdfBuffer },
|
||||
]);
|
||||
}
|
||||
await mail.send();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import Container, { Service } from 'typedi';
|
||||
import { SaleReceiptMailNotification } from './SaleReceiptMailNotification';
|
||||
|
||||
@Service()
|
||||
export class SaleReceiptMailNotificationJob {
|
||||
/**
|
||||
* Constructor method.
|
||||
*/
|
||||
constructor(agenda) {
|
||||
agenda.define(
|
||||
'sale-receipt-mail-send',
|
||||
{ priority: 'high', concurrency: 2 },
|
||||
this.handler
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggers sending invoice mail.
|
||||
*/
|
||||
private handler = async (job, done: Function) => {
|
||||
const { tenantId, saleReceiptId, messageOpts } = job.attrs.data;
|
||||
const receiveMailNotification = Container.get(SaleReceiptMailNotification);
|
||||
|
||||
try {
|
||||
await receiveMailNotification.sendMail(
|
||||
tenantId,
|
||||
saleReceiptId,
|
||||
messageOpts
|
||||
);
|
||||
done();
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
done(error);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { TemplateInjectable } from '@/services/TemplateInjectable/TemplateInjectable';
|
||||
import { ChromiumlyTenancy } from '@/services/ChromiumlyTenancy/ChromiumlyTenancy';
|
||||
import { GetSaleReceipt } from './GetSaleReceipt';
|
||||
|
||||
@Service()
|
||||
export class SaleReceiptsPdf {
|
||||
@@ -10,11 +11,20 @@ export class SaleReceiptsPdf {
|
||||
@Inject()
|
||||
private templateInjectable: TemplateInjectable;
|
||||
|
||||
@Inject()
|
||||
private getSaleReceiptService: GetSaleReceipt;
|
||||
|
||||
/**
|
||||
* Retrieve sale invoice pdf content.
|
||||
* @param {} saleInvoice -
|
||||
* Retrieves sale invoice pdf content.
|
||||
* @param {number} tenantId -
|
||||
* @param {number} saleInvoiceId -
|
||||
* @returns {Promise<Buffer>}
|
||||
*/
|
||||
public async saleReceiptPdf(tenantId: number, saleReceipt) {
|
||||
public async saleReceiptPdf(tenantId: number, saleReceiptId: number) {
|
||||
const saleReceipt = await this.getSaleReceiptService.getSaleReceipt(
|
||||
tenantId,
|
||||
saleReceiptId
|
||||
);
|
||||
const htmlContent = await this.templateInjectable.render(
|
||||
tenantId,
|
||||
'modules/receipt-regular',
|
||||
|
||||
@@ -1,3 +1,19 @@
|
||||
export const DEFAULT_RECEIPT_MAIL_SUBJECT =
|
||||
'Receipt {ReceiptNumber} from {CompanyName}';
|
||||
export const DEFAULT_RECEIPT_MAIL_CONTENT = `
|
||||
<p>Dear {CustomerName}</p>
|
||||
<p>Thank you for your business, You can view or print your receipt from attachements.</p>
|
||||
<p>
|
||||
Receipt <strong>#{ReceiptNumber}</strong><br />
|
||||
Amount : <strong>{ReceiptAmount}</strong></br />
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<i>Regards</i><br />
|
||||
<i>{CompanyName}</i>
|
||||
</p>
|
||||
`;
|
||||
|
||||
export const ERRORS = {
|
||||
SALE_RECEIPT_NOT_FOUND: 'SALE_RECEIPT_NOT_FOUND',
|
||||
DEPOSIT_ACCOUNT_NOT_FOUND: 'DEPOSIT_ACCOUNT_NOT_FOUND',
|
||||
@@ -6,6 +22,7 @@ export const ERRORS = {
|
||||
SALE_RECEIPT_IS_ALREADY_CLOSED: 'SALE_RECEIPT_IS_ALREADY_CLOSED',
|
||||
SALE_RECEIPT_NO_IS_REQUIRED: 'SALE_RECEIPT_NO_IS_REQUIRED',
|
||||
CUSTOMER_HAS_SALES_INVOICES: 'CUSTOMER_HAS_SALES_INVOICES',
|
||||
NO_INVOICE_CUSTOMER_EMAIL_ADDR: 'NO_INVOICE_CUSTOMER_EMAIL_ADDR'
|
||||
};
|
||||
|
||||
export const DEFAULT_VIEW_COLUMNS = [];
|
||||
|
||||
@@ -129,6 +129,9 @@ export default {
|
||||
|
||||
onNotifySms: 'onSaleInvoiceNotifySms',
|
||||
onNotifiedSms: 'onSaleInvoiceNotifiedSms',
|
||||
|
||||
onNotifyMail: 'onSaleInvoiceNotifyMail',
|
||||
onNotifyReminderMail: 'onSaleInvoiceNotifyReminderMail'
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -160,6 +163,8 @@ export default {
|
||||
|
||||
onRejecting: 'onSaleEstimateRejecting',
|
||||
onRejected: 'onSaleEstimateRejected',
|
||||
|
||||
onNotifyMail: 'onSaleEstimateNotifyMail'
|
||||
},
|
||||
|
||||
/**
|
||||
|
||||
@@ -20,6 +20,13 @@
|
||||
"@testing-library/jest-dom": "^4.2.4",
|
||||
"@testing-library/react": "^9.4.0",
|
||||
"@testing-library/user-event": "^7.2.1",
|
||||
"@tiptap/extension-color": "latest",
|
||||
"@tiptap/extension-text-style": "2.1.13",
|
||||
"@tiptap/core": "2.1.13",
|
||||
"@tiptap/pm": "2.1.13",
|
||||
"@tiptap/extension-list-item": "2.1.13",
|
||||
"@tiptap/react": "2.1.13",
|
||||
"@tiptap/starter-kit": "2.1.13",
|
||||
"@types/jest": "^26.0.15",
|
||||
"@types/js-money": "^0.6.1",
|
||||
"@types/lodash": "^4.14.172",
|
||||
|
||||
@@ -47,6 +47,10 @@ import ProjectInvoicingFormDialog from '@/containers/Projects/containers/Project
|
||||
import ProjectBillableEntriesFormDialog from '@/containers/Projects/containers/ProjectBillableEntriesFormDialog';
|
||||
import TaxRateFormDialog from '@/containers/TaxRates/dialogs/TaxRateFormDialog/TaxRateFormDialog';
|
||||
import { DialogsName } from '@/constants/dialogs';
|
||||
import InvoiceMailDialog from '@/containers/Sales/Invoices/InvoiceMailDialog/InvoiceMailDialog';
|
||||
import EstimateMailDialog from '@/containers/Sales/Estimates/EstimateMailDialog/EstimateMailDialog';
|
||||
import ReceiptMailDialog from '@/containers/Sales/Receipts/ReceiptMailDialog/ReceiptMailDialog';
|
||||
import PaymentMailDialog from '@/containers/Sales/PaymentReceives/PaymentMailDialog/PaymentMailDialog';
|
||||
|
||||
/**
|
||||
* Dialogs container.
|
||||
@@ -137,6 +141,10 @@ export default function DialogsContainer() {
|
||||
dialogName={DialogsName.ProjectBillableEntriesForm}
|
||||
/>
|
||||
<TaxRateFormDialog dialogName={DialogsName.TaxRateForm} />
|
||||
<InvoiceMailDialog dialogName={DialogsName.InvoiceMail} />
|
||||
<EstimateMailDialog dialogName={DialogsName.EstimateMail} />
|
||||
<ReceiptMailDialog dialogName={DialogsName.ReceiptMail} />
|
||||
<PaymentMailDialog dialogName={DialogsName.PaymentMail} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
52
packages/webapp/src/components/Forms/FRichEditor.tsx
Normal file
52
packages/webapp/src/components/Forms/FRichEditor.tsx
Normal file
@@ -0,0 +1,52 @@
|
||||
import React from 'react';
|
||||
import { FieldConfig, FieldProps } from 'formik';
|
||||
import { Field } from '@blueprintjs-formik/core';
|
||||
import { RichEditor, RichEditorProps } from '../../components/RichEditor';
|
||||
|
||||
export interface FRichEditorProps
|
||||
extends Omit<FieldConfig, 'children' | 'component' | 'as'>,
|
||||
RichEditorProps {
|
||||
name: string;
|
||||
value?: string;
|
||||
}
|
||||
|
||||
interface FieldToRichEditorProps
|
||||
extends FieldProps,
|
||||
Omit<RichEditorProps, 'form'> {}
|
||||
|
||||
/**
|
||||
* Transformes the field props to `RichEditor` props.
|
||||
* @param {FieldToRichEditorProps}
|
||||
* @returns {HTMLSelectProps}
|
||||
*/
|
||||
function fieldToRichEditor({
|
||||
field: { onBlur: onFieldBlur, ...field },
|
||||
form: { touched, errors, ...form },
|
||||
...props
|
||||
}: FieldToRichEditorProps): RichEditorProps {
|
||||
return {
|
||||
...field,
|
||||
...props,
|
||||
onChange: (value: string) => {
|
||||
form.setFieldValue(field.name, value);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Transformes field props to `RichEditor` props.
|
||||
* @param {FieldToRichEditorProps}
|
||||
* @returns {JSX.Element}
|
||||
*/
|
||||
function FieldToRichEditor({ ...props }: FieldToRichEditorProps): JSX.Element {
|
||||
return <RichEditor {...fieldToRichEditor(props)} />;
|
||||
}
|
||||
|
||||
/**
|
||||
* Rich editor wrapper to bind with Formik.
|
||||
* @param {FRichEditorProps} props -
|
||||
* @returns {JSX.Element}
|
||||
*/
|
||||
export function FRichEditor({ ...props }: FRichEditorProps): JSX.Element {
|
||||
return <Field {...props} component={FieldToRichEditor} />;
|
||||
}
|
||||
@@ -4,4 +4,5 @@ export * from './FMoneyInputGroup';
|
||||
export * from './BlueprintFormik';
|
||||
export * from './InputPrependText';
|
||||
export * from './InputPrependButton';
|
||||
export * from './MoneyInputGroup';
|
||||
export * from './MoneyInputGroup';
|
||||
export * from './FRichEditor';
|
||||
@@ -0,0 +1,66 @@
|
||||
/* Basic editor styles */
|
||||
.tiptap {
|
||||
color: #222;
|
||||
|
||||
&:focus-visible {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
>*+* {
|
||||
margin-top: 0.75em;
|
||||
}
|
||||
|
||||
ul,
|
||||
ol {
|
||||
padding: 0 1rem;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
code {
|
||||
background: rgba(#ffffff, 0.1);
|
||||
color: rgba(#ffffff, 0.6);
|
||||
border: 1px solid rgba(#ffffff, 0.1);
|
||||
border-radius: 0.5rem;
|
||||
padding: 0.2rem;
|
||||
}
|
||||
|
||||
pre {
|
||||
background: rgba(#ffffff, 0.1);
|
||||
font-family: "JetBrainsMono", monospace;
|
||||
padding: 0.75rem 1rem;
|
||||
border-radius: 0.5rem;
|
||||
|
||||
code {
|
||||
color: inherit;
|
||||
padding: 0;
|
||||
background: none;
|
||||
font-size: 0.8rem;
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
|
||||
img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
margin-left: 0;
|
||||
padding-left: 1rem;
|
||||
border-left: 2px solid rgba(#ffffff, 0.4);
|
||||
|
||||
hr {
|
||||
border: none;
|
||||
border-top: 2px solid rgba(#ffffff, 0.1);
|
||||
margin: 2rem 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
58
packages/webapp/src/components/RichEditor/RichEditor.tsx
Normal file
58
packages/webapp/src/components/RichEditor/RichEditor.tsx
Normal file
@@ -0,0 +1,58 @@
|
||||
// @ts-nocheck
|
||||
import { Color } from '@tiptap/extension-color';
|
||||
import ListItem from '@tiptap/extension-list-item';
|
||||
import TextStyle from '@tiptap/extension-text-style';
|
||||
import { EditorProvider } from '@tiptap/react';
|
||||
import StarterKit from '@tiptap/starter-kit';
|
||||
import { useUncontrolled } from '@/hooks/useUncontrolled';
|
||||
import { Box } from '../Layout/Box';
|
||||
import './RichEditor.style.scss';
|
||||
|
||||
const extensions = [
|
||||
Color.configure({ types: [TextStyle.name, ListItem.name] }),
|
||||
TextStyle.configure({ types: [ListItem.name] }),
|
||||
StarterKit.configure({
|
||||
bulletList: {
|
||||
keepMarks: true,
|
||||
keepAttributes: false,
|
||||
},
|
||||
orderedList: {
|
||||
keepMarks: true,
|
||||
keepAttributes: false,
|
||||
},
|
||||
}),
|
||||
];
|
||||
|
||||
export interface RichEditorProps {
|
||||
value?: string;
|
||||
initialValue?: string;
|
||||
onChange?: (value: string) => void;
|
||||
className?: string;
|
||||
}
|
||||
export const RichEditor = ({
|
||||
value,
|
||||
initialValue,
|
||||
onChange,
|
||||
className,
|
||||
}: RichEditorProps) => {
|
||||
const [content, handleChange] = useUncontrolled({
|
||||
value,
|
||||
initialValue,
|
||||
onChange,
|
||||
finalValue: '',
|
||||
});
|
||||
|
||||
const handleBlur = ({ editor }) => {
|
||||
handleChange(editor.getHTML());
|
||||
};
|
||||
|
||||
return (
|
||||
<Box className={className}>
|
||||
<EditorProvider
|
||||
extensions={extensions}
|
||||
content={content}
|
||||
onBlur={handleBlur}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
1
packages/webapp/src/components/RichEditor/index.ts
Normal file
1
packages/webapp/src/components/RichEditor/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './RichEditor';
|
||||
@@ -48,4 +48,8 @@ export enum DialogsName {
|
||||
ProjectBillableEntriesForm = 'project-billable-entries',
|
||||
InvoiceNumberSettings = 'InvoiceNumberSettings',
|
||||
TaxRateForm = 'tax-rate-form',
|
||||
InvoiceMail = 'invoice-mail',
|
||||
EstimateMail = 'estimate-mail',
|
||||
ReceiptMail = 'receipt-mail',
|
||||
PaymentMail = 'payment-mail',
|
||||
}
|
||||
|
||||
@@ -12,6 +12,22 @@ export default [
|
||||
text: <T id={'users'} />,
|
||||
href: '/preferences/users',
|
||||
},
|
||||
{
|
||||
text: <T id={'preferences.estimates'} />,
|
||||
href: '/preferences/estimates',
|
||||
},
|
||||
{
|
||||
text: <T id={'preferences.invoices'} />,
|
||||
href: '/preferences/invoices',
|
||||
},
|
||||
{
|
||||
text: <T id={'preferences.receipts'} />,
|
||||
href: '/preferences/receipts',
|
||||
},
|
||||
{
|
||||
text: <T id={'preferences.creditNotes'} />,
|
||||
href: '/preferences/credit-notes',
|
||||
},
|
||||
{
|
||||
text: <T id={'currencies'} />,
|
||||
href: '/preferences/currencies',
|
||||
|
||||
@@ -29,7 +29,7 @@ const Schema = Yup.object().shape({
|
||||
billing_address_2: Yup.string().trim(),
|
||||
billing_address_city: Yup.string().trim(),
|
||||
billing_address_state: Yup.string().trim(),
|
||||
billing_address_postcode: Yup.number().nullable(),
|
||||
billing_address_postcode: Yup.string().nullable(),
|
||||
billing_address_phone: Yup.number(),
|
||||
|
||||
shipping_address_country: Yup.string().trim(),
|
||||
@@ -37,7 +37,7 @@ const Schema = Yup.object().shape({
|
||||
shipping_address_2: Yup.string().trim(),
|
||||
shipping_address_city: Yup.string().trim(),
|
||||
shipping_address_state: Yup.string().trim(),
|
||||
shipping_address_postcode: Yup.number().nullable(),
|
||||
shipping_address_postcode: Yup.string().nullable(),
|
||||
shipping_address_phone: Yup.number(),
|
||||
|
||||
opening_balance: Yup.number().nullable(),
|
||||
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
|
||||
import { compose } from '@/utils';
|
||||
import { DRAWERS } from '@/constants/drawers';
|
||||
import { DialogsName } from '@/constants/dialogs';
|
||||
|
||||
/**
|
||||
* Estimate read-only details actions bar of the drawer.
|
||||
@@ -65,6 +66,10 @@ function EstimateDetailActionsBar({
|
||||
const handleNotifyViaSMS = () => {
|
||||
openDialog('notify-estimate-via-sms', { estimateId });
|
||||
};
|
||||
// Handles the estimate mail dialog.
|
||||
const handleMailEstimate = () => {
|
||||
openDialog(DialogsName.EstimateMail, { estimateId });
|
||||
};
|
||||
|
||||
return (
|
||||
<DrawerActionsBar>
|
||||
@@ -79,12 +84,19 @@ function EstimateDetailActionsBar({
|
||||
<NavbarDivider />
|
||||
</Can>
|
||||
<Can I={SaleEstimateAction.View} a={AbilitySubject.Estimate}>
|
||||
<Button
|
||||
className={Classes.MINIMAL}
|
||||
icon={<Icon icon="envelope" />}
|
||||
text={'Send Mail'}
|
||||
onClick={handleMailEstimate}
|
||||
/>
|
||||
<Button
|
||||
className={Classes.MINIMAL}
|
||||
icon={<Icon icon="print-16" />}
|
||||
text={<T id={'print'} />}
|
||||
onClick={handlePrintEstimate}
|
||||
/>
|
||||
<NavbarDivider />
|
||||
</Can>
|
||||
<Can I={SaleEstimateAction.Delete} a={AbilitySubject.Estimate}>
|
||||
<Button
|
||||
|
||||
@@ -31,6 +31,7 @@ import {
|
||||
import { compose } from '@/utils';
|
||||
import { BadDebtMenuItem } from './utils';
|
||||
import { DRAWERS } from '@/constants/drawers';
|
||||
import { DialogsName } from '@/constants/dialogs';
|
||||
|
||||
/**
|
||||
* Invoice details action bar.
|
||||
@@ -93,6 +94,10 @@ function InvoiceDetailActionsBar({
|
||||
openAlert('cancel-bad-debt', { invoiceId });
|
||||
};
|
||||
|
||||
const handleMailInvoice = () => {
|
||||
openDialog(DialogsName.InvoiceMail, { invoiceId });
|
||||
};
|
||||
|
||||
return (
|
||||
<DrawerActionsBar>
|
||||
<NavbarGroup>
|
||||
@@ -117,12 +122,19 @@ function InvoiceDetailActionsBar({
|
||||
<NavbarDivider />
|
||||
</Can>
|
||||
<Can I={SaleInvoiceAction.View} a={AbilitySubject.Invoice}>
|
||||
<Button
|
||||
text={'Send Mail'}
|
||||
icon={<Icon icon="envelope" />}
|
||||
onClick={handleMailInvoice}
|
||||
className={Classes.MINIMAL}
|
||||
/>
|
||||
<Button
|
||||
className={Classes.MINIMAL}
|
||||
icon={<Icon icon="print-16" />}
|
||||
text={<T id={'print'} />}
|
||||
onClick={handlePrintInvoice}
|
||||
/>
|
||||
<NavbarDivider />
|
||||
</Can>
|
||||
<Can I={SaleInvoiceAction.Delete} a={AbilitySubject.Invoice}>
|
||||
<Button
|
||||
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
|
||||
import { compose } from '@/utils';
|
||||
import { DRAWERS } from '@/constants/drawers';
|
||||
import { DialogsName } from '@/constants/dialogs';
|
||||
|
||||
/**
|
||||
* Payment receive actions bar.
|
||||
@@ -68,6 +69,10 @@ function PaymentReceiveActionsBar({
|
||||
openDialog('payment-pdf-preview', { paymentReceiveId });
|
||||
};
|
||||
|
||||
const handleMailPaymentReceive = () => {
|
||||
openDialog(DialogsName.PaymentMail, { paymentReceiveId });
|
||||
};
|
||||
|
||||
return (
|
||||
<DrawerActionsBar>
|
||||
<NavbarGroup>
|
||||
@@ -81,12 +86,19 @@ function PaymentReceiveActionsBar({
|
||||
<NavbarDivider />
|
||||
</Can>
|
||||
<Can I={PaymentReceiveAction.View} a={AbilitySubject.PaymentReceive}>
|
||||
<Button
|
||||
className={Classes.MINIMAL}
|
||||
text={'Send Mail'}
|
||||
icon={<Icon icon="envelope" />}
|
||||
onClick={handleMailPaymentReceive}
|
||||
/>
|
||||
<Button
|
||||
className={Classes.MINIMAL}
|
||||
icon={<Icon icon="print-16" />}
|
||||
text={<T id={'print'} />}
|
||||
onClick={handlePrintPaymentReceive}
|
||||
/>
|
||||
<NavbarDivider />
|
||||
</Can>
|
||||
<Can I={PaymentReceiveAction.Delete} a={AbilitySubject.PaymentReceive}>
|
||||
<Button
|
||||
|
||||
@@ -24,6 +24,7 @@ import { useReceiptDetailDrawerContext } from './ReceiptDetailDrawerProvider';
|
||||
import { SaleReceiptAction, AbilitySubject } from '@/constants/abilityOption';
|
||||
import { safeCallback, compose } from '@/utils';
|
||||
import { DRAWERS } from '@/constants/drawers';
|
||||
import { DialogsName } from '@/constants/dialogs';
|
||||
|
||||
/**
|
||||
* Receipt details actions bar.
|
||||
@@ -60,6 +61,9 @@ function ReceiptDetailActionBar({
|
||||
const handleNotifyViaSMS = () => {
|
||||
openDialog('notify-receipt-via-sms', { receiptId });
|
||||
};
|
||||
const handleReceiptMail = () => {
|
||||
openDialog(DialogsName.ReceiptMail, { receiptId });
|
||||
};
|
||||
|
||||
return (
|
||||
<DrawerActionsBar>
|
||||
@@ -74,6 +78,12 @@ function ReceiptDetailActionBar({
|
||||
<NavbarDivider />
|
||||
</Can>
|
||||
<Can I={SaleReceiptAction.View} a={AbilitySubject.Receipt}>
|
||||
<Button
|
||||
className={Classes.MINIMAL}
|
||||
text={'Send Mail'}
|
||||
icon={<Icon icon="envelope" />}
|
||||
onClick={handleReceiptMail}
|
||||
/>
|
||||
<Button
|
||||
className={Classes.MINIMAL}
|
||||
icon={<Icon icon="print-16" />}
|
||||
|
||||
@@ -284,26 +284,31 @@ export const useComposeRowsOnRemoveTableRow = () => {
|
||||
|
||||
/**
|
||||
* Retrieves the aggregate tax rates from the given item entries.
|
||||
* @param {string} currencyCode -
|
||||
* @param {any} taxRates -
|
||||
* @param {any} entries -
|
||||
*/
|
||||
export const aggregateItemEntriesTaxRates = R.curry((taxRates, entries) => {
|
||||
const taxRatesById = keyBy(taxRates, 'id');
|
||||
export const aggregateItemEntriesTaxRates = R.curry(
|
||||
(currencyCode, taxRates, entries) => {
|
||||
const taxRatesById = keyBy(taxRates, 'id');
|
||||
|
||||
// Calculate the total tax amount of invoice entries.
|
||||
const filteredEntries = entries.filter((e) => e.tax_rate_id);
|
||||
const groupedTaxRates = groupBy(filteredEntries, 'tax_rate_id');
|
||||
// Calculate the total tax amount of invoice entries.
|
||||
const filteredEntries = entries.filter((e) => e.tax_rate_id);
|
||||
const groupedTaxRates = groupBy(filteredEntries, 'tax_rate_id');
|
||||
|
||||
return Object.keys(groupedTaxRates).map((taxRateId) => {
|
||||
const taxRate = taxRatesById[taxRateId];
|
||||
const taxRates = groupedTaxRates[taxRateId];
|
||||
const totalTaxAmount = sumBy(taxRates, 'tax_amount');
|
||||
const taxAmountFormatted = formattedAmount(totalTaxAmount, 'USD');
|
||||
return Object.keys(groupedTaxRates).map((taxRateId) => {
|
||||
const taxRate = taxRatesById[taxRateId];
|
||||
const taxRates = groupedTaxRates[taxRateId];
|
||||
const totalTaxAmount = sumBy(taxRates, 'tax_amount');
|
||||
const taxAmountFormatted = formattedAmount(totalTaxAmount, currencyCode);
|
||||
|
||||
return {
|
||||
taxRateId,
|
||||
taxRate: taxRate.rate,
|
||||
label: `${taxRate.name} [${taxRate.rate}%]`,
|
||||
taxAmount: totalTaxAmount,
|
||||
taxAmountFormatted,
|
||||
};
|
||||
});
|
||||
});
|
||||
return {
|
||||
taxRateId,
|
||||
taxRate: taxRate.rate,
|
||||
label: `${taxRate.name} [${taxRate.rate}%]`,
|
||||
taxAmount: totalTaxAmount,
|
||||
taxAmountFormatted,
|
||||
};
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
// @ts-nocheck
|
||||
import { PreferencesCreditNotesBoot } from './PreferencesCreditNotesFormBoot';
|
||||
import { PreferencesCreditNotesFormPage } from './PreferencesCreditNotesFormPage';
|
||||
|
||||
/**
|
||||
* Credit notes preferences.
|
||||
*/
|
||||
export function PreferencesCreditNotes() {
|
||||
return (
|
||||
<PreferencesCreditNotesBoot>
|
||||
<PreferencesCreditNotesFormPage />
|
||||
</PreferencesCreditNotesBoot>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
// @ts-nocheck
|
||||
import * as Yup from 'yup';
|
||||
|
||||
const Schema = Yup.object().shape({
|
||||
termsConditions: Yup.string().optional(),
|
||||
customerNotes: Yup.string().optional(),
|
||||
});
|
||||
|
||||
export const PreferencesCreditNotesFormSchema = Schema;
|
||||
@@ -0,0 +1,74 @@
|
||||
// @ts-nocheck
|
||||
import styled from 'styled-components';
|
||||
import { Form } from 'formik';
|
||||
import { Button, Intent } from '@blueprintjs/core';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
|
||||
import { FormattedMessage as T, FFormGroup, FTextArea } from '@/components';
|
||||
|
||||
/**
|
||||
* Preferences credit notes form.
|
||||
*/
|
||||
export function PreferencesCreditNotesForm({ isSubmitting }) {
|
||||
const history = useHistory();
|
||||
|
||||
// Handle close click.
|
||||
const handleCloseClick = () => {
|
||||
history.go(-1);
|
||||
};
|
||||
|
||||
return (
|
||||
<Form>
|
||||
{/* ---------- Customer Notes ---------- */}
|
||||
<FFormGroup
|
||||
name={'customerNotes'}
|
||||
label={<T id={'pref.creditNotes.customerNotes.field'} />}
|
||||
fastField={true}
|
||||
>
|
||||
<FTextArea
|
||||
medium={'true'}
|
||||
name={'customerNotes'}
|
||||
fastField={true}
|
||||
fill={true}
|
||||
/>
|
||||
</FFormGroup>
|
||||
|
||||
{/* ---------- Terms & Conditions ---------- */}
|
||||
<FFormGroup
|
||||
name={'termsConditions'}
|
||||
label={<T id={'pref.creditNotes.termsConditions.field'} />}
|
||||
fastField={true}
|
||||
>
|
||||
<FTextArea
|
||||
medium={'true'}
|
||||
name={'termsConditions'}
|
||||
fastField={true}
|
||||
fill={true}
|
||||
/>
|
||||
</FFormGroup>
|
||||
|
||||
<CardFooterActions>
|
||||
<Button loading={isSubmitting} intent={Intent.PRIMARY} type="submit">
|
||||
<T id={'save'} />
|
||||
</Button>
|
||||
<Button onClick={handleCloseClick}>
|
||||
<T id={'close'} />
|
||||
</Button>
|
||||
</CardFooterActions>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
||||
const CardFooterActions = styled.div`
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid #e0e7ea;
|
||||
margin-top: 30px;
|
||||
|
||||
.bp4-button {
|
||||
min-width: 70px;
|
||||
|
||||
+ .bp4-button {
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -0,0 +1,55 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import classNames from 'classnames';
|
||||
import { CLASSES } from '@/constants/classes';
|
||||
import { useSettings } from '@/hooks/query';
|
||||
import PreferencesPageLoader from '../PreferencesPageLoader';
|
||||
import { Card } from '@/components';
|
||||
|
||||
const PreferencesCreditNotesFormContext = React.createContext();
|
||||
|
||||
function PreferencesCreditNotesBoot({ ...props }) {
|
||||
// Fetches organization settings.
|
||||
const { isLoading: isSettingsLoading } = useSettings();
|
||||
|
||||
// Provider state.
|
||||
const provider = {
|
||||
isSettingsLoading,
|
||||
};
|
||||
// Detarmines whether if any query is loading.
|
||||
const isLoading = isSettingsLoading;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
CLASSES.PREFERENCES_PAGE_INSIDE_CONTENT,
|
||||
CLASSES.PREFERENCES_PAGE_INSIDE_CONTENT_ACCOUNTANT,
|
||||
)}
|
||||
>
|
||||
<PreferencesCreditNotesCard>
|
||||
{isLoading ? (
|
||||
<PreferencesPageLoader />
|
||||
) : (
|
||||
<PreferencesCreditNotesFormContext.Provider
|
||||
value={provider}
|
||||
{...props}
|
||||
/>
|
||||
)}
|
||||
</PreferencesCreditNotesCard>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const PreferencesCreditNotesCard = styled(Card)`
|
||||
padding: 25px;
|
||||
|
||||
.bp4-form-group {
|
||||
max-width: 600px;
|
||||
}
|
||||
`;
|
||||
|
||||
const usePreferencesCreditNotesFormContext = () =>
|
||||
React.useContext(PreferencesCreditNotesFormContext);
|
||||
|
||||
export { PreferencesCreditNotesBoot, usePreferencesCreditNotesFormContext };
|
||||
@@ -0,0 +1,82 @@
|
||||
// @ts-nocheck
|
||||
import { useEffect } from 'react';
|
||||
import intl from 'react-intl-universal';
|
||||
import { Formik } from 'formik';
|
||||
import * as R from 'ramda';
|
||||
import { Intent } from '@blueprintjs/core';
|
||||
|
||||
import { AppToaster } from '@/components';
|
||||
import { PreferencesCreditNotesFormSchema } from './PreferencesCreditNotesForm.schema';
|
||||
import { PreferencesCreditNotesForm } from './PreferencesCreditNotesForm';
|
||||
import withDashboardActions from '@/containers/Dashboard/withDashboardActions';
|
||||
|
||||
import { compose, transformToForm, transfromToSnakeCase } from '@/utils';
|
||||
import withSettings from '@/containers/Settings/withSettings';
|
||||
import { transferObjectOptionsToArray } from '../Accountant/utils';
|
||||
import { useSaveSettings } from '@/hooks/query';
|
||||
|
||||
const defaultValues = {
|
||||
termsConditions: '',
|
||||
customerNotes: '',
|
||||
};
|
||||
|
||||
/**
|
||||
* Preferences - Credit Notes.
|
||||
*/
|
||||
function PreferencesCreditNotesFormPageRoot({
|
||||
// #withDashboardActions
|
||||
changePreferencesPageTitle,
|
||||
|
||||
// #withSettings
|
||||
creditNoteSettings,
|
||||
}) {
|
||||
// Save settings.
|
||||
const { mutateAsync: saveSettingMutate } = useSaveSettings();
|
||||
|
||||
useEffect(() => {
|
||||
changePreferencesPageTitle(intl.get('preferences.creditNotes'));
|
||||
}, [changePreferencesPageTitle]);
|
||||
|
||||
// Initial values.
|
||||
const initialValues = {
|
||||
...defaultValues,
|
||||
...transformToForm(creditNoteSettings, defaultValues),
|
||||
};
|
||||
// Handle the form submit.
|
||||
const handleFormSubmit = (values, { setSubmitting }) => {
|
||||
const options = R.compose(
|
||||
transferObjectOptionsToArray,
|
||||
transfromToSnakeCase,
|
||||
)({ creditNote: { ...values } });
|
||||
|
||||
// Handle request success.
|
||||
const onSuccess = () => {
|
||||
AppToaster.show({
|
||||
message: intl.get('preferences.credit_notes.success_message'),
|
||||
intent: Intent.SUCCESS,
|
||||
});
|
||||
setSubmitting(false);
|
||||
};
|
||||
// Handle request error.
|
||||
const onError = () => {
|
||||
setSubmitting(false);
|
||||
};
|
||||
saveSettingMutate({ options }).then(onSuccess).catch(onError);
|
||||
};
|
||||
|
||||
return (
|
||||
<Formik
|
||||
initialValues={initialValues}
|
||||
validationSchema={PreferencesCreditNotesFormSchema}
|
||||
onSubmit={handleFormSubmit}
|
||||
component={PreferencesCreditNotesForm}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export const PreferencesCreditNotesFormPage = compose(
|
||||
withDashboardActions,
|
||||
withSettings(({ creditNoteSettings }) => ({
|
||||
creditNoteSettings: creditNoteSettings,
|
||||
})),
|
||||
)(PreferencesCreditNotesFormPageRoot);
|
||||
@@ -0,0 +1,14 @@
|
||||
// @ts-nocheck
|
||||
import { PreferencesEstimatesBoot } from './PreferencesEstimatesFormBoot';
|
||||
import { PreferencesEstimatesFormPage } from './PreferencesEstimatesFormPage';
|
||||
|
||||
/**
|
||||
* Estimates preferences.
|
||||
*/
|
||||
export function PreferencesEstimates() {
|
||||
return (
|
||||
<PreferencesEstimatesBoot>
|
||||
<PreferencesEstimatesFormPage />
|
||||
</PreferencesEstimatesBoot>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
// @ts-nocheck
|
||||
import * as Yup from 'yup';
|
||||
|
||||
const Schema = Yup.object().shape({
|
||||
termsConditions: Yup.string().optional(),
|
||||
customerNotes: Yup.string().optional(),
|
||||
});
|
||||
|
||||
export const PreferencesEstimatesFormSchema = Schema;
|
||||
@@ -0,0 +1,74 @@
|
||||
// @ts-nocheck
|
||||
import styled from 'styled-components';
|
||||
import { Form } from 'formik';
|
||||
import { Button, Intent } from '@blueprintjs/core';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
|
||||
import { FormattedMessage as T, FFormGroup, FTextArea } from '@/components';
|
||||
|
||||
/**
|
||||
* Preferences estimates form.
|
||||
*/
|
||||
export function PreferencesEstimatesForm({ isSubmitting }) {
|
||||
const history = useHistory();
|
||||
|
||||
// Handle close click.
|
||||
const handleCloseClick = () => {
|
||||
history.go(-1);
|
||||
};
|
||||
|
||||
return (
|
||||
<Form>
|
||||
{/* ---------- Customer Notes ---------- */}
|
||||
<FFormGroup
|
||||
name={'customerNotes'}
|
||||
label={<T id={'pref.estimates.customerNotes.field'} />}
|
||||
fastField={true}
|
||||
>
|
||||
<FTextArea
|
||||
medium={'true'}
|
||||
name={'customerNotes'}
|
||||
fastField={true}
|
||||
fill={true}
|
||||
/>
|
||||
</FFormGroup>
|
||||
|
||||
{/* ---------- Terms & Conditions ---------- */}
|
||||
<FFormGroup
|
||||
name={'termsConditions'}
|
||||
label={<T id={'pref.estimates.termsConditions.field'} />}
|
||||
fastField={true}
|
||||
>
|
||||
<FTextArea
|
||||
medium={'true'}
|
||||
name={'termsConditions'}
|
||||
fastField={true}
|
||||
fill={true}
|
||||
/>
|
||||
</FFormGroup>
|
||||
|
||||
<CardFooterActions>
|
||||
<Button loading={isSubmitting} intent={Intent.PRIMARY} type="submit">
|
||||
<T id={'save'} />
|
||||
</Button>
|
||||
<Button onClick={handleCloseClick}>
|
||||
<T id={'close'} />
|
||||
</Button>
|
||||
</CardFooterActions>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
||||
const CardFooterActions = styled.div`
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid #e0e7ea;
|
||||
margin-top: 30px;
|
||||
|
||||
.bp4-button {
|
||||
min-width: 70px;
|
||||
|
||||
+ .bp4-button {
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -0,0 +1,55 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { CLASSES } from '@/constants/classes';
|
||||
import { useSettings } from '@/hooks/query';
|
||||
import PreferencesPageLoader from '../PreferencesPageLoader';
|
||||
import styled from 'styled-components';
|
||||
import { Card } from '@/components';
|
||||
|
||||
const PreferencesEstimatesFormContext = React.createContext();
|
||||
|
||||
function PreferencesEstimatesBoot({ ...props }) {
|
||||
// Fetches organization settings.
|
||||
const { isLoading: isSettingsLoading } = useSettings();
|
||||
|
||||
// Provider state.
|
||||
const provider = {
|
||||
isSettingsLoading,
|
||||
};
|
||||
// Detarmines whether if any query is loading.
|
||||
const isLoading = isSettingsLoading;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
CLASSES.PREFERENCES_PAGE_INSIDE_CONTENT,
|
||||
CLASSES.PREFERENCES_PAGE_INSIDE_CONTENT_ACCOUNTANT,
|
||||
)}
|
||||
>
|
||||
<PreferencesEstimatesCard>
|
||||
{isLoading ? (
|
||||
<PreferencesPageLoader />
|
||||
) : (
|
||||
<PreferencesEstimatesFormContext.Provider
|
||||
value={provider}
|
||||
{...props}
|
||||
/>
|
||||
)}
|
||||
</PreferencesEstimatesCard>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const usePreferencesEstimatesFormContext = () =>
|
||||
React.useContext(PreferencesEstimatesFormContext);
|
||||
|
||||
const PreferencesEstimatesCard = styled(Card)`
|
||||
padding: 25px;
|
||||
|
||||
.bp4-form-group {
|
||||
max-width: 600px;
|
||||
}
|
||||
`;
|
||||
|
||||
export { PreferencesEstimatesBoot, usePreferencesEstimatesFormContext };
|
||||
@@ -0,0 +1,82 @@
|
||||
// @ts-nocheck
|
||||
import React, { useEffect } from 'react';
|
||||
import intl from 'react-intl-universal';
|
||||
import { Formik } from 'formik';
|
||||
import { Intent } from '@blueprintjs/core';
|
||||
import * as R from 'ramda';
|
||||
|
||||
import { AppToaster } from '@/components';
|
||||
import { PreferencesEstimatesFormSchema } from './PreferencesEstimatesForm.schema';
|
||||
import { PreferencesEstimatesForm } from './PreferencesEstimatesForm';
|
||||
import withDashboardActions from '@/containers/Dashboard/withDashboardActions';
|
||||
import withSettings from '@/containers/Settings/withSettings';
|
||||
|
||||
import { transferObjectOptionsToArray } from '../Accountant/utils';
|
||||
import { compose, transformToForm, transfromToSnakeCase } from '@/utils';
|
||||
import { useSaveSettings } from '@/hooks/query';
|
||||
|
||||
const defaultValues = {
|
||||
termsConditions: '',
|
||||
customerNotes: '',
|
||||
};
|
||||
|
||||
/**
|
||||
* Preferences estimates form.
|
||||
*/
|
||||
function PreferencesEstimatesFormPageRoot({
|
||||
// #withDashboardActions
|
||||
changePreferencesPageTitle,
|
||||
|
||||
// #withSettings
|
||||
estimatesSettings,
|
||||
}) {
|
||||
// Save Organization Settings.
|
||||
const { mutateAsync: saveSettingMutate } = useSaveSettings();
|
||||
|
||||
useEffect(() => {
|
||||
changePreferencesPageTitle(intl.get('preferences.estimates'));
|
||||
}, [changePreferencesPageTitle]);
|
||||
|
||||
// Initial values.
|
||||
const initialValues = {
|
||||
...defaultValues,
|
||||
...transformToForm(estimatesSettings, defaultValues),
|
||||
};
|
||||
// Handle the form submit.
|
||||
const handleFormSubmit = (values, { setSubmitting }) => {
|
||||
const options = R.compose(
|
||||
transferObjectOptionsToArray,
|
||||
transfromToSnakeCase,
|
||||
)({ salesEstimates: { ...values } });
|
||||
|
||||
// Handle request success.
|
||||
const onSuccess = (response) => {
|
||||
AppToaster.show({
|
||||
message: intl.get('preferences.estimates.success_message'),
|
||||
intent: Intent.SUCCESS,
|
||||
});
|
||||
setSubmitting(false);
|
||||
};
|
||||
// Handle request error.
|
||||
const onError = () => {
|
||||
setSubmitting(false);
|
||||
};
|
||||
saveSettingMutate({ options }).then(onSuccess).catch(onError);
|
||||
};
|
||||
|
||||
return (
|
||||
<Formik
|
||||
initialValues={initialValues}
|
||||
validationSchema={PreferencesEstimatesFormSchema}
|
||||
onSubmit={handleFormSubmit}
|
||||
component={PreferencesEstimatesForm}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export const PreferencesEstimatesFormPage = compose(
|
||||
withDashboardActions,
|
||||
withSettings(({ estimatesSettings }) => ({
|
||||
estimatesSettings: estimatesSettings,
|
||||
})),
|
||||
)(PreferencesEstimatesFormPageRoot);
|
||||
@@ -0,0 +1,9 @@
|
||||
// @ts-nocheck
|
||||
import * as Yup from 'yup';
|
||||
|
||||
const Schema = Yup.object().shape({
|
||||
termsConditions: Yup.string().optional(),
|
||||
customerNotes: Yup.string().optional(),
|
||||
});
|
||||
|
||||
export const PreferencesInvoiceFormSchema = Schema;
|
||||
@@ -0,0 +1,53 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import styled from 'styled-components';
|
||||
import { CLASSES } from '@/constants/classes';
|
||||
import { useSettings } from '@/hooks/query';
|
||||
import PreferencesPageLoader from '../PreferencesPageLoader';
|
||||
import { Card } from '@/components';
|
||||
|
||||
const PreferencesInvoiceFormContext = React.createContext();
|
||||
|
||||
function PreferencesInvoicesBoot({ ...props }) {
|
||||
// Fetches organization settings.
|
||||
const { isLoading: isSettingsLoading } = useSettings();
|
||||
|
||||
// Provider state.
|
||||
const provider = {
|
||||
isSettingsLoading
|
||||
};
|
||||
|
||||
// Detarmines whether if any query is loading.
|
||||
const isLoading = isSettingsLoading;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
CLASSES.PREFERENCES_PAGE_INSIDE_CONTENT,
|
||||
CLASSES.PREFERENCES_PAGE_INSIDE_CONTENT_ACCOUNTANT,
|
||||
)}
|
||||
>
|
||||
<PreferencesInvoicesCard>
|
||||
{isLoading ? (
|
||||
<PreferencesPageLoader />
|
||||
) : (
|
||||
<PreferencesInvoiceFormContext.Provider value={provider} {...props} />
|
||||
)}
|
||||
</PreferencesInvoicesCard>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const PreferencesInvoicesCard = styled(Card)`
|
||||
padding: 25px;
|
||||
|
||||
.bp4-form-group{
|
||||
max-width: 600px;
|
||||
}
|
||||
`;
|
||||
|
||||
const usePreferencesInvoiceFormContext = () =>
|
||||
React.useContext(PreferencesInvoiceFormContext);
|
||||
|
||||
export { PreferencesInvoicesBoot, usePreferencesInvoiceFormContext };
|
||||
@@ -0,0 +1,82 @@
|
||||
// @ts-nocheck
|
||||
import React, { useEffect } from 'react';
|
||||
import intl from 'react-intl-universal';
|
||||
import { Formik } from 'formik';
|
||||
import { Intent } from '@blueprintjs/core';
|
||||
import * as R from 'ramda';
|
||||
|
||||
import { AppToaster } from '@/components';
|
||||
import { PreferencesInvoiceFormSchema } from './PreferencesInvoiceForm.schema';
|
||||
import { PreferencesInvoicesForm } from './PreferencesInvoicesForm';
|
||||
import withDashboardActions from '@/containers/Dashboard/withDashboardActions';
|
||||
|
||||
import { compose, transformToForm, transfromToSnakeCase } from '@/utils';
|
||||
import withSettings from '@/containers/Settings/withSettings';
|
||||
import { transferObjectOptionsToArray } from '../Accountant/utils';
|
||||
import { useSaveSettings } from '@/hooks/query';
|
||||
|
||||
const defaultValues = {
|
||||
termsConditions: '',
|
||||
customerNotes: '',
|
||||
};
|
||||
|
||||
/**
|
||||
* Preferences - Invoices.
|
||||
*/
|
||||
function PreferencesInvoiceFormPage({
|
||||
// #withDashboardActions
|
||||
changePreferencesPageTitle,
|
||||
|
||||
// #withSettings
|
||||
invoiceSettings,
|
||||
}) {
|
||||
// Save settings.
|
||||
const { mutateAsync: saveSettingMutate } = useSaveSettings();
|
||||
|
||||
useEffect(() => {
|
||||
changePreferencesPageTitle(intl.get('preferences.invoices'));
|
||||
}, [changePreferencesPageTitle]);
|
||||
|
||||
// Initial values.
|
||||
const initialValues = {
|
||||
...defaultValues,
|
||||
...transformToForm(invoiceSettings, defaultValues),
|
||||
};
|
||||
// Handle the form submit.
|
||||
const handleFormSubmit = (values, { setSubmitting }) => {
|
||||
const options = R.compose(
|
||||
transferObjectOptionsToArray,
|
||||
transfromToSnakeCase,
|
||||
)({ salesInvoices: { ...values } });
|
||||
|
||||
// Handle request success.
|
||||
const onSuccess = () => {
|
||||
AppToaster.show({
|
||||
message: intl.get('preferences.invoices.success_message'),
|
||||
intent: Intent.SUCCESS,
|
||||
});
|
||||
setSubmitting(false);
|
||||
};
|
||||
// Handle request error.
|
||||
const onError = () => {
|
||||
setSubmitting(false);
|
||||
};
|
||||
saveSettingMutate({ options }).then(onSuccess).catch(onError);
|
||||
};
|
||||
|
||||
return (
|
||||
<Formik
|
||||
initialValues={initialValues}
|
||||
validationSchema={PreferencesInvoiceFormSchema}
|
||||
onSubmit={handleFormSubmit}
|
||||
component={PreferencesInvoicesForm}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default compose(
|
||||
withDashboardActions,
|
||||
withSettings(({ invoiceSettings }) => ({
|
||||
invoiceSettings: invoiceSettings,
|
||||
})),
|
||||
)(PreferencesInvoiceFormPage);
|
||||
@@ -0,0 +1,14 @@
|
||||
// @ts-nocheck
|
||||
import { PreferencesInvoicesBoot } from './PreferencesInvoiceFormBoot';
|
||||
import PreferencesInvoiceFormPage from './PreferencesInvoiceFormPage';
|
||||
|
||||
/**
|
||||
* items preferences.
|
||||
*/
|
||||
export default function PreferencesInvoices() {
|
||||
return (
|
||||
<PreferencesInvoicesBoot>
|
||||
<PreferencesInvoiceFormPage />
|
||||
</PreferencesInvoicesBoot>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
// @ts-nocheck
|
||||
import styled from 'styled-components';
|
||||
import { Form } from 'formik';
|
||||
import { Button, Intent } from '@blueprintjs/core';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
|
||||
import { FormattedMessage as T, FFormGroup, FTextArea } from '@/components';
|
||||
|
||||
/**
|
||||
* Invoices preferences form.
|
||||
*/
|
||||
export function PreferencesInvoicesForm({ isSubmitting }) {
|
||||
const history = useHistory();
|
||||
|
||||
// Handle close click.
|
||||
const handleCloseClick = () => {
|
||||
history.go(-1);
|
||||
};
|
||||
|
||||
return (
|
||||
<Form>
|
||||
{/* ---------- Customer Notes ---------- */}
|
||||
<FFormGroup
|
||||
name={'customerNotes'}
|
||||
label={<T id={'pref.invoices.customerNotes.field'} />}
|
||||
fastField={true}
|
||||
>
|
||||
<FTextArea
|
||||
medium={'true'}
|
||||
name={'customerNotes'}
|
||||
fastField={true}
|
||||
fill={true}
|
||||
/>
|
||||
</FFormGroup>
|
||||
|
||||
{/* ---------- Terms & Conditions ---------- */}
|
||||
<FFormGroup
|
||||
name={'termsConditions'}
|
||||
label={<T id={'pref.invoices.termsConditions.field'} />}
|
||||
fastField={true}
|
||||
>
|
||||
<FTextArea
|
||||
medium={'true'}
|
||||
name={'termsConditions'}
|
||||
fastField={true}
|
||||
fill={true}
|
||||
/>
|
||||
</FFormGroup>
|
||||
|
||||
<CardFooterActions>
|
||||
<Button loading={isSubmitting} intent={Intent.PRIMARY} type="submit">
|
||||
<T id={'save'} />
|
||||
</Button>
|
||||
<Button onClick={handleCloseClick}>
|
||||
<T id={'close'} />
|
||||
</Button>
|
||||
</CardFooterActions>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
||||
const CardFooterActions = styled.div`
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid #e0e7ea;
|
||||
margin-top: 30px;
|
||||
|
||||
.bp4-button {
|
||||
min-width: 70px;
|
||||
|
||||
+ .bp4-button {
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -0,0 +1,14 @@
|
||||
// @ts-nocheck
|
||||
import { PreferencesReceiptsBoot } from './PreferencesReceiptsFormBoot';
|
||||
import { PreferencesReceiptsFormPage } from './PreferencesReceiptsFormPage';
|
||||
|
||||
/**
|
||||
* Preferences - Receipts.
|
||||
*/
|
||||
export function PreferencesReceipts() {
|
||||
return (
|
||||
<PreferencesReceiptsBoot>
|
||||
<PreferencesReceiptsFormPage />
|
||||
</PreferencesReceiptsBoot>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
// @ts-nocheck
|
||||
import * as Yup from 'yup';
|
||||
|
||||
const Schema = Yup.object().shape({
|
||||
termsConditions: Yup.string().optional(),
|
||||
customerNotes: Yup.string().optional(),
|
||||
});
|
||||
|
||||
export const PreferencesReceiptsFormSchema = Schema;
|
||||
@@ -0,0 +1,74 @@
|
||||
// @ts-nocheck
|
||||
import styled from 'styled-components';
|
||||
import { Form } from 'formik';
|
||||
import { Button, Intent } from '@blueprintjs/core';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
|
||||
import { FormattedMessage as T, FFormGroup, FTextArea } from '@/components';
|
||||
|
||||
/**
|
||||
* Preferences general form.
|
||||
*/
|
||||
export function PreferencesReceiptsForm({ isSubmitting }) {
|
||||
const history = useHistory();
|
||||
|
||||
// Handle close click.
|
||||
const handleCloseClick = () => {
|
||||
history.go(-1);
|
||||
};
|
||||
|
||||
return (
|
||||
<Form>
|
||||
{/* ---------- Customer Notes ---------- */}
|
||||
<FFormGroup
|
||||
name={'receiptMessage'}
|
||||
label={<T id={'pref.receipts.receiptMessage.field'} />}
|
||||
fastField={true}
|
||||
>
|
||||
<FTextArea
|
||||
medium={'true'}
|
||||
name={'receiptMessage'}
|
||||
fastField={true}
|
||||
fill={true}
|
||||
/>
|
||||
</FFormGroup>
|
||||
|
||||
{/* ---------- Terms & Conditions ---------- */}
|
||||
<FFormGroup
|
||||
name={'termsConditions'}
|
||||
label={<T id={'pref.receipts.termsConditions.field'} />}
|
||||
fastField={true}
|
||||
>
|
||||
<FTextArea
|
||||
medium={'true'}
|
||||
name={'termsConditions'}
|
||||
fastField={true}
|
||||
fill={true}
|
||||
/>
|
||||
</FFormGroup>
|
||||
|
||||
<CardFooterActions>
|
||||
<Button loading={isSubmitting} intent={Intent.PRIMARY} type="submit">
|
||||
<T id={'save'} />
|
||||
</Button>
|
||||
<Button onClick={handleCloseClick}>
|
||||
<T id={'close'} />
|
||||
</Button>
|
||||
</CardFooterActions>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
||||
const CardFooterActions = styled.div`
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid #e0e7ea;
|
||||
margin-top: 30px;
|
||||
|
||||
.bp4-button {
|
||||
min-width: 70px;
|
||||
|
||||
+ .bp4-button {
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -0,0 +1,56 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import styled from 'styled-components';
|
||||
import { CLASSES } from '@/constants/classes';
|
||||
import { useSettings } from '@/hooks/query';
|
||||
import PreferencesPageLoader from '../PreferencesPageLoader';
|
||||
import { Card } from '@/components';
|
||||
|
||||
const PreferencesReceiptsFormContext = React.createContext();
|
||||
|
||||
function PreferencesReceiptsBoot({ ...props }) {
|
||||
// Fetches organization settings.
|
||||
const { isLoading: isSettingsLoading } = useSettings();
|
||||
|
||||
// Provider state.
|
||||
const provider = {
|
||||
isSettingsLoading,
|
||||
};
|
||||
|
||||
// Detarmines whether if any query is loading.
|
||||
const isLoading = isSettingsLoading;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
CLASSES.PREFERENCES_PAGE_INSIDE_CONTENT,
|
||||
CLASSES.PREFERENCES_PAGE_INSIDE_CONTENT_ACCOUNTANT,
|
||||
)}
|
||||
>
|
||||
<PreferencesReceiptsCard>
|
||||
{isLoading ? (
|
||||
<PreferencesPageLoader />
|
||||
) : (
|
||||
<PreferencesReceiptsFormContext.Provider
|
||||
value={provider}
|
||||
{...props}
|
||||
/>
|
||||
)}
|
||||
</PreferencesReceiptsCard>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const PreferencesReceiptsCard = styled(Card)`
|
||||
padding: 25px;
|
||||
|
||||
.bp4-form-group {
|
||||
max-width: 600px;
|
||||
}
|
||||
`;
|
||||
|
||||
const usePreferencesReceiptsFormContext = () =>
|
||||
React.useContext(PreferencesReceiptsFormContext);
|
||||
|
||||
export { PreferencesReceiptsBoot, usePreferencesReceiptsFormContext };
|
||||
@@ -0,0 +1,82 @@
|
||||
// @ts-nocheck
|
||||
import React, { useEffect } from 'react';
|
||||
import intl from 'react-intl-universal';
|
||||
import { Formik } from 'formik';
|
||||
import { Intent } from '@blueprintjs/core';
|
||||
import * as R from 'ramda';
|
||||
|
||||
import { AppToaster } from '@/components';
|
||||
import { PreferencesReceiptsFormSchema } from './PreferencesReceiptsForm.schema';
|
||||
import { PreferencesReceiptsForm } from './PreferencesReceiptsForm';
|
||||
import withDashboardActions from '@/containers/Dashboard/withDashboardActions';
|
||||
|
||||
import { compose, transformToForm, transfromToSnakeCase } from '@/utils';
|
||||
import withSettings from '@/containers/Settings/withSettings';
|
||||
import { useSaveSettings } from '@/hooks/query';
|
||||
import { transferObjectOptionsToArray } from '../Accountant/utils';
|
||||
|
||||
const defaultValues = {
|
||||
termsConditions: '',
|
||||
receiptMessage: '',
|
||||
};
|
||||
|
||||
/**
|
||||
* Preferences - Receipts.
|
||||
*/
|
||||
function PreferencesReceiptsFormPageRoot({
|
||||
// #withDashboardActions
|
||||
changePreferencesPageTitle,
|
||||
|
||||
// #withSettings
|
||||
receiptSettings,
|
||||
}) {
|
||||
// Save settings.
|
||||
const { mutateAsync: saveSettingMutate } = useSaveSettings();
|
||||
|
||||
useEffect(() => {
|
||||
changePreferencesPageTitle(intl.get('preferences.receipts'));
|
||||
}, [changePreferencesPageTitle]);
|
||||
|
||||
// Initial values.
|
||||
const initialValues = {
|
||||
...defaultValues,
|
||||
...transformToForm(receiptSettings, defaultValues),
|
||||
};
|
||||
// Handle the form submit.
|
||||
const handleFormSubmit = (values, { setSubmitting }) => {
|
||||
const options = R.compose(
|
||||
transferObjectOptionsToArray,
|
||||
transfromToSnakeCase,
|
||||
)({ salesReceipts: { ...values } });
|
||||
|
||||
// Handle request success.
|
||||
const onSuccess = () => {
|
||||
AppToaster.show({
|
||||
message: intl.get('preferences.receipts.success_message'),
|
||||
intent: Intent.SUCCESS,
|
||||
});
|
||||
setSubmitting(false);
|
||||
};
|
||||
// Handle request error.
|
||||
const onError = () => {
|
||||
setSubmitting(false);
|
||||
};
|
||||
saveSettingMutate({ options }).then(onSuccess).catch(onError);
|
||||
};
|
||||
|
||||
return (
|
||||
<Formik
|
||||
initialValues={initialValues}
|
||||
validationSchema={PreferencesReceiptsFormSchema}
|
||||
onSubmit={handleFormSubmit}
|
||||
component={PreferencesReceiptsForm}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export const PreferencesReceiptsFormPage = compose(
|
||||
withDashboardActions,
|
||||
withSettings(({ receiptSettings }) => ({
|
||||
receiptSettings: receiptSettings,
|
||||
})),
|
||||
)(PreferencesReceiptsFormPageRoot);
|
||||
@@ -322,8 +322,8 @@ export const useBillAggregatedTaxRates = () => {
|
||||
const { taxRates } = useBillFormContext();
|
||||
|
||||
const aggregateTaxRates = React.useMemo(
|
||||
() => aggregateItemEntriesTaxRates(taxRates),
|
||||
[taxRates],
|
||||
() => aggregateItemEntriesTaxRates(values.currency_code, taxRates),
|
||||
[values.currency_code, taxRates],
|
||||
);
|
||||
// Calculate the total tax amount of bill entries.
|
||||
return React.useMemo(() => {
|
||||
|
||||
@@ -5,7 +5,7 @@ import classNames from 'classnames';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { Formik, Form } from 'formik';
|
||||
import { Intent } from '@blueprintjs/core';
|
||||
import { isEmpty } from 'lodash';
|
||||
import { defaultTo, isEmpty } from 'lodash';
|
||||
import { CLASSES } from '@/constants/classes';
|
||||
import {
|
||||
CreateCreditNoteFormSchema,
|
||||
@@ -48,6 +48,8 @@ function CreditNoteForm({
|
||||
creditAutoIncrement,
|
||||
creditNumberPrefix,
|
||||
creditNextNumber,
|
||||
creditCustomerNotes,
|
||||
creditTermsConditions,
|
||||
|
||||
// #withCurrentOrganization
|
||||
organization: { base_currency },
|
||||
@@ -68,22 +70,21 @@ function CreditNoteForm({
|
||||
const creditNumber = transactionNumber(creditNumberPrefix, creditNextNumber);
|
||||
|
||||
// Initial values.
|
||||
const initialValues = React.useMemo(
|
||||
() => ({
|
||||
...(!isEmpty(creditNote)
|
||||
? { ...transformToEditForm(creditNote) }
|
||||
: {
|
||||
...defaultCreditNote,
|
||||
...(creditAutoIncrement && {
|
||||
credit_note_number: creditNumber,
|
||||
}),
|
||||
entries: orderingLinesIndexes(defaultCreditNote.entries),
|
||||
currency_code: base_currency,
|
||||
...newCreditNote,
|
||||
const initialValues = {
|
||||
...(!isEmpty(creditNote)
|
||||
? { ...transformToEditForm(creditNote) }
|
||||
: {
|
||||
...defaultCreditNote,
|
||||
...(creditAutoIncrement && {
|
||||
credit_note_number: creditNumber,
|
||||
}),
|
||||
}),
|
||||
[],
|
||||
);
|
||||
entries: orderingLinesIndexes(defaultCreditNote.entries),
|
||||
currency_code: base_currency,
|
||||
terms_conditions: defaultTo(creditTermsConditions, ''),
|
||||
note: defaultTo(creditCustomerNotes, ''),
|
||||
...newCreditNote,
|
||||
}),
|
||||
};
|
||||
|
||||
// Handles form submit.
|
||||
const handleFormSubmit = (
|
||||
@@ -178,6 +179,8 @@ export default compose(
|
||||
creditAutoIncrement: creditNoteSettings?.autoIncrement,
|
||||
creditNextNumber: creditNoteSettings?.nextNumber,
|
||||
creditNumberPrefix: creditNoteSettings?.numberPrefix,
|
||||
creditCustomerNotes: creditNoteSettings?.customerNotes,
|
||||
creditTermsConditions: creditNoteSettings?.termsConditions,
|
||||
})),
|
||||
withCurrentOrganization(),
|
||||
)(CreditNoteForm);
|
||||
|
||||
@@ -4,7 +4,7 @@ import intl from 'react-intl-universal';
|
||||
import classNames from 'classnames';
|
||||
import { Formik, Form } from 'formik';
|
||||
import { Intent } from '@blueprintjs/core';
|
||||
import { sumBy, isEmpty } from 'lodash';
|
||||
import { sumBy, isEmpty, defaultTo } from 'lodash';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { CLASSES } from '@/constants/classes';
|
||||
|
||||
@@ -43,6 +43,8 @@ function EstimateForm({
|
||||
estimateNextNumber,
|
||||
estimateNumberPrefix,
|
||||
estimateAutoIncrementMode,
|
||||
estimateCustomerNotes,
|
||||
estimateTermsConditions,
|
||||
|
||||
// #withCurrentOrganization
|
||||
organization: { base_currency },
|
||||
@@ -60,25 +62,23 @@ function EstimateForm({
|
||||
estimateNumberPrefix,
|
||||
estimateNextNumber,
|
||||
);
|
||||
|
||||
// Initial values in create and edit mode.
|
||||
const initialValues = useMemo(
|
||||
() => ({
|
||||
...(!isEmpty(estimate)
|
||||
? { ...transformToEditForm(estimate) }
|
||||
: {
|
||||
...defaultEstimate,
|
||||
// If the auto-increment mode is enabled, take the next estimate
|
||||
// number from the settings.
|
||||
...(estimateAutoIncrementMode && {
|
||||
estimate_number: estimateNumber,
|
||||
}),
|
||||
entries: orderingLinesIndexes(defaultEstimate.entries),
|
||||
currency_code: base_currency,
|
||||
const initialValues = {
|
||||
...(!isEmpty(estimate)
|
||||
? { ...transformToEditForm(estimate) }
|
||||
: {
|
||||
...defaultEstimate,
|
||||
// If the auto-increment mode is enabled, take the next estimate
|
||||
// number from the settings.
|
||||
...(estimateAutoIncrementMode && {
|
||||
estimate_number: estimateNumber,
|
||||
}),
|
||||
}),
|
||||
[estimate, estimateNumber, estimateAutoIncrementMode, base_currency],
|
||||
);
|
||||
entries: orderingLinesIndexes(defaultEstimate.entries),
|
||||
currency_code: base_currency,
|
||||
terms_conditions: defaultTo(estimateTermsConditions, ''),
|
||||
note: defaultTo(estimateCustomerNotes, ''),
|
||||
}),
|
||||
};
|
||||
|
||||
// Handles form submit.
|
||||
const handleFormSubmit = (
|
||||
@@ -181,6 +181,8 @@ export default compose(
|
||||
estimateNextNumber: estimatesSettings?.nextNumber,
|
||||
estimateNumberPrefix: estimatesSettings?.numberPrefix,
|
||||
estimateAutoIncrementMode: estimatesSettings?.autoIncrement,
|
||||
estimateCustomerNotes: estimatesSettings?.customerNotes,
|
||||
estimateTermsConditions: estimatesSettings?.termsConditions,
|
||||
})),
|
||||
withCurrentOrganization(),
|
||||
)(EstimateForm);
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import { Dialog, DialogSuspense } from '@/components';
|
||||
import withDialogRedux from '@/components/DialogReduxConnect';
|
||||
import { compose } from '@/utils';
|
||||
|
||||
const EstimateMailDialogContent = React.lazy(
|
||||
() => import('./EstimateMailDialogContent'),
|
||||
);
|
||||
|
||||
/**
|
||||
* Invoice mail dialog.
|
||||
*/
|
||||
function EstimateMailDialog({
|
||||
dialogName,
|
||||
payload: { estimateId = null },
|
||||
isOpen,
|
||||
}) {
|
||||
return (
|
||||
<Dialog
|
||||
name={dialogName}
|
||||
title={'Estimate Mail'}
|
||||
isOpen={isOpen}
|
||||
canEscapeJeyClose={true}
|
||||
autoFocus={true}
|
||||
style={{ width: 600 }}
|
||||
>
|
||||
<DialogSuspense>
|
||||
<EstimateMailDialogContent
|
||||
dialogName={dialogName}
|
||||
estimateId={estimateId}
|
||||
/>
|
||||
</DialogSuspense>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
export default compose(withDialogRedux())(EstimateMailDialog);
|
||||
@@ -0,0 +1,44 @@
|
||||
// @ts-nocheck
|
||||
import React, { createContext } from 'react';
|
||||
import { useSaleEstimateDefaultOptions } from '@/hooks/query';
|
||||
import { DialogContent } from '@/components';
|
||||
|
||||
interface EstimateMailDialogBootValues {
|
||||
estimateId: number;
|
||||
mailOptions: any;
|
||||
}
|
||||
|
||||
const EstimateMailDialagBoot = createContext<EstimateMailDialogBootValues>();
|
||||
|
||||
interface EstimateMailDialogBootProps {
|
||||
estimateId: number;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Estimate mail dialog boot provider.
|
||||
*/
|
||||
function EstimateMailDialogBoot({
|
||||
estimateId,
|
||||
...props
|
||||
}: EstimateMailDialogBootProps) {
|
||||
const { data: mailOptions, isLoading: isMailOptionsLoading } =
|
||||
useSaleEstimateDefaultOptions(estimateId);
|
||||
|
||||
const provider = {
|
||||
saleEstimateId: estimateId,
|
||||
mailOptions,
|
||||
isMailOptionsLoading,
|
||||
};
|
||||
|
||||
return (
|
||||
<DialogContent isLoading={isMailOptionsLoading}>
|
||||
<EstimateMailDialagBoot.Provider value={provider} {...props} />
|
||||
</DialogContent>
|
||||
);
|
||||
}
|
||||
|
||||
const useEstimateMailDialogBoot = () =>
|
||||
React.useContext<EstimateMailDialogBootValues>(EstimateMailDialagBoot);
|
||||
|
||||
export { EstimateMailDialogBoot, useEstimateMailDialogBoot };
|
||||
@@ -0,0 +1,17 @@
|
||||
import { EstimateMailDialogBoot } from './EstimateMailDialogBoot';
|
||||
import { EstimateMailDialogForm } from './EstimateMailDialogForm';
|
||||
|
||||
interface EstimateMailDialogContentProps {
|
||||
dialogName: string;
|
||||
estimateId: number;
|
||||
}
|
||||
export default function EstimateMailDialogContent({
|
||||
dialogName,
|
||||
estimateId,
|
||||
}: EstimateMailDialogContentProps) {
|
||||
return (
|
||||
<EstimateMailDialogBoot estimateId={estimateId}>
|
||||
<EstimateMailDialogForm />
|
||||
</EstimateMailDialogBoot>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
// @ts-nocheck
|
||||
import { Formik } from 'formik';
|
||||
import * as R from 'ramda';
|
||||
import { useEstimateMailDialogBoot } from './EstimateMailDialogBoot';
|
||||
import { DialogsName } from '@/constants/dialogs';
|
||||
import withDialogActions from '@/containers/Dialog/withDialogActions';
|
||||
import { useSendSaleEstimateMail } from '@/hooks/query';
|
||||
import { EstimateMailDialogFormContent } from './EstimateMailDialogFormContent';
|
||||
import {
|
||||
initialMailNotificationValues,
|
||||
MailNotificationFormValues,
|
||||
transformMailFormToInitialValues,
|
||||
transformMailFormToRequest,
|
||||
} from '@/containers/SendMailNotification/utils';
|
||||
import { Intent } from '@blueprintjs/core';
|
||||
import { AppToaster } from '@/components';
|
||||
|
||||
const initialFormValues = {
|
||||
...initialMailNotificationValues,
|
||||
attachEstimate: true,
|
||||
};
|
||||
|
||||
interface EstimateMailFormValues extends MailNotificationFormValues {
|
||||
attachEstimate: boolean;
|
||||
}
|
||||
|
||||
function EstimateMailDialogFormRoot({
|
||||
// #withDialogClose
|
||||
closeDialog,
|
||||
}) {
|
||||
const { mutateAsync: sendEstimateMail } = useSendSaleEstimateMail();
|
||||
const { mailOptions, saleEstimateId } = useEstimateMailDialogBoot();
|
||||
|
||||
const initialValues = transformMailFormToInitialValues(
|
||||
mailOptions,
|
||||
initialFormValues,
|
||||
);
|
||||
// Handle the form submitting.
|
||||
const handleSubmit = (values: EstimateMailFormValues, { setSubmitting }) => {
|
||||
const reqValues = transformMailFormToRequest(values);
|
||||
|
||||
setSubmitting(true);
|
||||
sendEstimateMail([saleEstimateId, reqValues])
|
||||
.then(() => {
|
||||
AppToaster.show({
|
||||
message: 'The mail notification has been sent successfully.',
|
||||
intent: Intent.SUCCESS,
|
||||
});
|
||||
closeDialog(DialogsName.EstimateMail);
|
||||
setSubmitting(false);
|
||||
})
|
||||
.catch((error) => {
|
||||
setSubmitting(false);
|
||||
closeDialog(DialogsName.EstimateMail);
|
||||
AppToaster.show({
|
||||
message: 'Something went wrong.',
|
||||
intent: Intent.DANGER,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
closeDialog(DialogsName.EstimateMail);
|
||||
};
|
||||
|
||||
return (
|
||||
<Formik initialValues={initialValues} onSubmit={handleSubmit}>
|
||||
<EstimateMailDialogFormContent onClose={handleClose} />
|
||||
</Formik>
|
||||
);
|
||||
}
|
||||
|
||||
export const EstimateMailDialogForm = R.compose(withDialogActions)(
|
||||
EstimateMailDialogFormRoot,
|
||||
);
|
||||
@@ -0,0 +1,66 @@
|
||||
// @ts-nocheck
|
||||
import { Form, useFormikContext } from 'formik';
|
||||
import { Button, Classes, Intent } from '@blueprintjs/core';
|
||||
import styled from 'styled-components';
|
||||
import { FFormGroup, FSwitch } from '@/components';
|
||||
import { MailNotificationForm } from '@/containers/SendMailNotification';
|
||||
import { saveInvoke } from '@/utils';
|
||||
import { useEstimateMailDialogBoot } from './EstimateMailDialogBoot';
|
||||
|
||||
interface EstimateMailDialogFormContentProps {
|
||||
onClose?: () => void;
|
||||
}
|
||||
|
||||
export function EstimateMailDialogFormContent({
|
||||
onClose,
|
||||
}: EstimateMailDialogFormContentProps) {
|
||||
const { isSubmitting } = useFormikContext();
|
||||
const { mailOptions } = useEstimateMailDialogBoot();
|
||||
|
||||
const handleClose = () => {
|
||||
saveInvoke(onClose);
|
||||
};
|
||||
|
||||
return (
|
||||
<Form>
|
||||
<div className={Classes.DIALOG_BODY}>
|
||||
<MailNotificationForm
|
||||
fromAddresses={mailOptions.from_addresses}
|
||||
toAddresses={mailOptions.to_addresses}
|
||||
/>
|
||||
<AttachFormGroup name={'attachEstimate'} inline>
|
||||
<FSwitch name={'attachEstimate'} label={'Attach Estimate'} />
|
||||
</AttachFormGroup>
|
||||
</div>
|
||||
|
||||
<div className={Classes.DIALOG_FOOTER}>
|
||||
<div className={Classes.DIALOG_FOOTER_ACTIONS}>
|
||||
<Button
|
||||
disabled={isSubmitting}
|
||||
onClick={handleClose}
|
||||
style={{ minWidth: '65px' }}
|
||||
>
|
||||
Close
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
intent={Intent.PRIMARY}
|
||||
loading={isSubmitting}
|
||||
style={{ minWidth: '75px' }}
|
||||
type="submit"
|
||||
>
|
||||
Send
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
||||
const AttachFormGroup = styled(FFormGroup)`
|
||||
background: #f8f9fb;
|
||||
margin-top: 0.6rem;
|
||||
padding: 4px 14px;
|
||||
border-radius: 5px;
|
||||
border: 1px solid #dcdcdd;
|
||||
`;
|
||||
@@ -0,0 +1 @@
|
||||
export * from './EstimateMailDialog';
|
||||
@@ -22,6 +22,7 @@ import { useEstimatesListContext } from './EstimatesListProvider';
|
||||
import { useMemorizedColumnsWidths } from '@/hooks';
|
||||
import { compose } from '@/utils';
|
||||
import { DRAWERS } from '@/constants/drawers';
|
||||
import { DialogsName } from '@/constants/dialogs';
|
||||
|
||||
/**
|
||||
* Estimates datatable.
|
||||
@@ -100,6 +101,11 @@ function EstimatesDataTable({
|
||||
openDrawer(DRAWERS.ESTIMATE_DETAILS, { estimateId: cell.row.original.id });
|
||||
};
|
||||
|
||||
// Handle mail send estimate.
|
||||
const handleMailSendEstimate = ({ id }) => {
|
||||
openDialog(DialogsName.EstimateMail, { estimateId: id });
|
||||
}
|
||||
|
||||
// Local storage memorizing columns widths.
|
||||
const [initialColumnsWidths, , handleColumnResizing] =
|
||||
useMemorizedColumnsWidths(TABLES.ESTIMATES);
|
||||
@@ -153,6 +159,7 @@ function EstimatesDataTable({
|
||||
onConvert: handleConvertToInvoice,
|
||||
onViewDetails: handleViewDetailEstimate,
|
||||
onPrint: handlePrintEstimate,
|
||||
onSendMail: handleMailSendEstimate,
|
||||
}}
|
||||
/>
|
||||
</DashboardContentTable>
|
||||
|
||||
@@ -64,6 +64,7 @@ export function ActionsMenu({
|
||||
onConvert,
|
||||
onViewDetails,
|
||||
onPrint,
|
||||
onSendMail
|
||||
},
|
||||
}) {
|
||||
return (
|
||||
@@ -129,6 +130,11 @@ export function ActionsMenu({
|
||||
</Choose>
|
||||
</Can>
|
||||
<Can I={SaleEstimateAction.View} a={AbilitySubject.Estimate}>
|
||||
<MenuItem
|
||||
icon={<Icon icon={'envelope'} iconSize={16} />}
|
||||
text={'Send Mail'}
|
||||
onClick={safeCallback(onSendMail, original)}
|
||||
/>
|
||||
<MenuItem
|
||||
icon={<Icon icon={'print-16'} iconSize={16} />}
|
||||
text={intl.get('print')}
|
||||
|
||||
@@ -4,7 +4,7 @@ import intl from 'react-intl-universal';
|
||||
import classNames from 'classnames';
|
||||
import { Formik, Form } from 'formik';
|
||||
import { Intent } from '@blueprintjs/core';
|
||||
import { sumBy, isEmpty } from 'lodash';
|
||||
import { sumBy, isEmpty, defaultTo } from 'lodash';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { CLASSES } from '@/constants/classes';
|
||||
import {
|
||||
@@ -44,6 +44,8 @@ function InvoiceForm({
|
||||
invoiceNextNumber,
|
||||
invoiceNumberPrefix,
|
||||
invoiceAutoIncrementMode,
|
||||
invoiceCustomerNotes,
|
||||
invoiceTermsConditions,
|
||||
|
||||
// #withCurrentOrganization
|
||||
organization: { base_currency },
|
||||
@@ -79,6 +81,8 @@ function InvoiceForm({
|
||||
}),
|
||||
entries: orderingLinesIndexes(defaultInvoice.entries),
|
||||
currency_code: base_currency,
|
||||
invoice_message: defaultTo(invoiceCustomerNotes, ''),
|
||||
terms_conditions: defaultTo(invoiceTermsConditions, ''),
|
||||
...newInvoice,
|
||||
}),
|
||||
};
|
||||
@@ -192,6 +196,8 @@ export default compose(
|
||||
invoiceNextNumber: invoiceSettings?.nextNumber,
|
||||
invoiceNumberPrefix: invoiceSettings?.numberPrefix,
|
||||
invoiceAutoIncrementMode: invoiceSettings?.autoIncrement,
|
||||
invoiceCustomerNotes: invoiceSettings?.customerNotes,
|
||||
invoiceTermsConditions: invoiceSettings?.termsConditions,
|
||||
})),
|
||||
withCurrentOrganization(),
|
||||
)(InvoiceForm);
|
||||
|
||||
@@ -333,8 +333,8 @@ export const useInvoiceAggregatedTaxRates = () => {
|
||||
const { taxRates } = useInvoiceFormContext();
|
||||
|
||||
const aggregateTaxRates = React.useMemo(
|
||||
() => aggregateItemEntriesTaxRates(taxRates),
|
||||
[taxRates],
|
||||
() => aggregateItemEntriesTaxRates(values.currency_code, taxRates),
|
||||
[values.currency_code, taxRates],
|
||||
);
|
||||
// Calculate the total tax amount of invoice entries.
|
||||
return React.useMemo(() => {
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import { Dialog, DialogSuspense } from '@/components';
|
||||
import withDialogRedux from '@/components/DialogReduxConnect';
|
||||
import { compose } from '@/utils';
|
||||
|
||||
const InvoiceMailDialogContent = React.lazy(
|
||||
() => import('./InvoiceMailDialogContent'),
|
||||
);
|
||||
|
||||
/**
|
||||
* Invoice mail dialog.
|
||||
*/
|
||||
function InvoiceMailDialog({
|
||||
dialogName,
|
||||
payload: { invoiceId = null },
|
||||
isOpen,
|
||||
}) {
|
||||
return (
|
||||
<Dialog
|
||||
name={dialogName}
|
||||
title={'Invoice Mail'}
|
||||
isOpen={isOpen}
|
||||
canEscapeJeyClose={true}
|
||||
autoFocus={true}
|
||||
style={{ width: 600 }}
|
||||
>
|
||||
<DialogSuspense>
|
||||
<InvoiceMailDialogContent
|
||||
dialogName={dialogName}
|
||||
invoiceId={invoiceId}
|
||||
/>
|
||||
</DialogSuspense>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
export default compose(withDialogRedux())(InvoiceMailDialog);
|
||||
@@ -0,0 +1,44 @@
|
||||
// @ts-nocheck
|
||||
import React, { createContext } from 'react';
|
||||
import { useSaleInvoiceDefaultOptions } from '@/hooks/query';
|
||||
import { DialogContent } from '@/components';
|
||||
|
||||
interface InvoiceMailDialogBootValues {
|
||||
invoiceId: number;
|
||||
mailOptions: any;
|
||||
}
|
||||
|
||||
const InvoiceMailDialagBoot = createContext<InvoiceMailDialogBootValues>();
|
||||
|
||||
interface InvoiceMailDialogBootProps {
|
||||
invoiceId: number;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoice mail dialog boot provider.
|
||||
*/
|
||||
function InvoiceMailDialogBoot({
|
||||
invoiceId,
|
||||
...props
|
||||
}: InvoiceMailDialogBootProps) {
|
||||
const { data: mailOptions, isLoading: isMailOptionsLoading } =
|
||||
useSaleInvoiceDefaultOptions(invoiceId);
|
||||
|
||||
const provider = {
|
||||
saleInvoiceId: invoiceId,
|
||||
mailOptions,
|
||||
isMailOptionsLoading,
|
||||
};
|
||||
|
||||
return (
|
||||
<DialogContent isLoading={isMailOptionsLoading}>
|
||||
<InvoiceMailDialagBoot.Provider value={provider} {...props} />
|
||||
</DialogContent>
|
||||
);
|
||||
}
|
||||
|
||||
const useInvoiceMailDialogBoot = () =>
|
||||
React.useContext<InvoiceMailDialogBootValues>(InvoiceMailDialagBoot);
|
||||
|
||||
export { InvoiceMailDialogBoot, useInvoiceMailDialogBoot };
|
||||
@@ -0,0 +1,17 @@
|
||||
import { InvoiceMailDialogBoot } from './InvoiceMailDialogBoot';
|
||||
import { InvoiceMailDialogForm } from './InvoiceMailDialogForm';
|
||||
|
||||
interface InvoiceMailDialogContentProps {
|
||||
dialogName: string;
|
||||
invoiceId: number;
|
||||
}
|
||||
export default function InvoiceMailDialogContent({
|
||||
dialogName,
|
||||
invoiceId,
|
||||
}: InvoiceMailDialogContentProps) {
|
||||
return (
|
||||
<InvoiceMailDialogBoot invoiceId={invoiceId}>
|
||||
<InvoiceMailDialogForm />
|
||||
</InvoiceMailDialogBoot>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
// @ts-nocheck
|
||||
import * as Yup from 'yup';
|
||||
|
||||
export const InvoiceMailFormSchema = Yup.object().shape({
|
||||
from: Yup.array().required().min(1).max(5).label('From address'),
|
||||
to: Yup.array().required().min(1).max(5).label('To address'),
|
||||
subject: Yup.string().required().label('Mail subject'),
|
||||
body: Yup.string().required().label('Mail body'),
|
||||
});
|
||||
@@ -0,0 +1,79 @@
|
||||
// @ts-nocheck
|
||||
import { Formik } from 'formik';
|
||||
import * as R from 'ramda';
|
||||
import { Intent } from '@blueprintjs/core';
|
||||
import { useInvoiceMailDialogBoot } from './InvoiceMailDialogBoot';
|
||||
import { DialogsName } from '@/constants/dialogs';
|
||||
import { AppToaster } from '@/components';
|
||||
import { useSendSaleInvoiceMail } from '@/hooks/query';
|
||||
import withDialogActions from '@/containers/Dialog/withDialogActions';
|
||||
import { InvoiceMailDialogFormContent } from './InvoiceMailDialogFormContent';
|
||||
import { InvoiceMailFormSchema } from './InvoiceMailDialogForm.schema';
|
||||
import {
|
||||
MailNotificationFormValues,
|
||||
initialMailNotificationValues,
|
||||
transformMailFormToRequest,
|
||||
transformMailFormToInitialValues,
|
||||
} from '@/containers/SendMailNotification/utils';
|
||||
|
||||
const initialFormValues = {
|
||||
...initialMailNotificationValues,
|
||||
attachInvoice: true,
|
||||
};
|
||||
|
||||
interface InvoiceMailFormValues extends MailNotificationFormValues {
|
||||
attachInvoice: boolean;
|
||||
}
|
||||
|
||||
function InvoiceMailDialogFormRoot({
|
||||
// #withDialogActions
|
||||
closeDialog,
|
||||
}) {
|
||||
const { mailOptions, saleInvoiceId } = useInvoiceMailDialogBoot();
|
||||
const { mutateAsync: sendInvoiceMail } = useSendSaleInvoiceMail();
|
||||
|
||||
const initialValues = transformMailFormToInitialValues(
|
||||
mailOptions,
|
||||
initialFormValues,
|
||||
);
|
||||
// Handle the form submitting.
|
||||
const handleSubmit = (values: InvoiceMailFormValues, { setSubmitting }) => {
|
||||
const reqValues = transformMailFormToRequest(values);
|
||||
|
||||
setSubmitting(true);
|
||||
sendInvoiceMail([saleInvoiceId, reqValues])
|
||||
.then(() => {
|
||||
AppToaster.show({
|
||||
message: 'The mail notification has been sent successfully.',
|
||||
intent: Intent.SUCCESS,
|
||||
});
|
||||
closeDialog(DialogsName.InvoiceMail);
|
||||
setSubmitting(false);
|
||||
})
|
||||
.catch(() => {
|
||||
AppToaster.show({
|
||||
message: 'Something went wrong.',
|
||||
intent: Intent.DANGER,
|
||||
});
|
||||
setSubmitting(false);
|
||||
});
|
||||
};
|
||||
// Handle the close button click.
|
||||
const handleClose = () => {
|
||||
closeDialog(DialogsName.InvoiceMail);
|
||||
};
|
||||
|
||||
return (
|
||||
<Formik
|
||||
initialValues={initialValues}
|
||||
validationSchema={InvoiceMailFormSchema}
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
<InvoiceMailDialogFormContent onClose={handleClose} />
|
||||
</Formik>
|
||||
);
|
||||
}
|
||||
|
||||
export const InvoiceMailDialogForm = R.compose(withDialogActions)(
|
||||
InvoiceMailDialogFormRoot,
|
||||
);
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user