Merge pull request #292 from bigcapitalhq/big-83-send-an-invoice-to-the-customer-email

feat: Send mail notifications of the sales transactions
This commit is contained in:
Ahmed Bouhuolia
2023-12-30 19:14:31 +02:00
committed by GitHub
99 changed files with 4103 additions and 95 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View 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)
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 = [];

View File

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

View File

@@ -0,0 +1,3 @@
export class GetSaleInvoiceMailReminder {
public getInvoiceMailReminder(tenantId: number, saleInvoiceId: number) {}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 = [];

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 = [];

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 = [];

View File

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

View File

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

View File

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

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

View File

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

View File

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

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

View File

@@ -0,0 +1 @@
export * from './RichEditor';

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1 @@
export * from './EstimateMailDialog';

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 { useInvoiceMailDialogBoot } from './InvoiceMailDialogBoot';
interface SendMailNotificationFormProps {
onClose?: () => void;
}
export function InvoiceMailDialogFormContent({
onClose,
}: SendMailNotificationFormProps) {
const { isSubmitting } = useFormikContext();
const { mailOptions } = useInvoiceMailDialogBoot();
const handleClose = () => {
saveInvoke(onClose);
};
return (
<Form>
<div className={Classes.DIALOG_BODY}>
<MailNotificationForm
fromAddresses={mailOptions.from_addresses}
toAddresses={mailOptions.to_addresses}
/>
<AttachFormGroup name={'attachInvoice'} inline>
<FSwitch name={'attachInvoice'} label={'Attach Invoice'} />
</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;
`;

View File

@@ -0,0 +1 @@
export * from './InvoiceMailDialog';

View File

@@ -26,6 +26,7 @@ import { useInvoicesListContext } from './InvoicesListProvider';
import { compose } from '@/utils';
import { DRAWERS } from '@/constants/drawers';
import { DialogsName } from '@/constants/dialogs';
/**
* Invoices datatable.
@@ -98,6 +99,11 @@ function InvoicesDataTable({
openDialog('invoice-pdf-preview', { invoiceId: id });
};
// Handle send mail invoice.
const handleSendMailInvoice = ({ id }) => {
openDialog(DialogsName.InvoiceMail, { invoiceId: id });
};
// Handle cell click.
const handleCellClick = (cell, event) => {
openDrawer(DRAWERS.INVOICE_DETAILS, { invoiceId: cell.row.original.id });
@@ -157,6 +163,7 @@ function InvoicesDataTable({
onViewDetails: handleViewDetailInvoice,
onPrint: handlePrintInvoice,
onConvert: handleConvertToCreitNote,
onSendMail: handleSendMailInvoice
}}
/>
</DashboardContentTable>

View File

@@ -128,6 +128,7 @@ export function ActionsMenu({
onQuick,
onViewDetails,
onPrint,
onSendMail
},
row: { original },
}) {
@@ -150,7 +151,6 @@ export function ActionsMenu({
text={intl.get('invoice.convert_to_credit_note')}
onClick={safeCallback(onConvert, original)}
/>
<If condition={!original.is_delivered}>
<MenuItem
icon={<Icon icon="send" iconSize={16} />}
@@ -169,6 +169,11 @@ export function ActionsMenu({
</If>
</Can>
<Can I={SaleInvoiceAction.View} a={AbilitySubject.Invoice}>
<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')}

View File

@@ -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 PaymentMailDialogContent = React.lazy(
() => import('./PaymentMailDialogContent'),
);
/**
* Payment mail dialog.
*/
function PaymentMailDialog({
dialogName,
payload: { paymentReceiveId = null },
isOpen,
}) {
return (
<Dialog
name={dialogName}
title={'Payment Mail'}
isOpen={isOpen}
canEscapeJeyClose={true}
autoFocus={true}
style={{ width: 600 }}
>
<DialogSuspense>
<PaymentMailDialogContent
dialogName={dialogName}
paymentReceiveId={paymentReceiveId}
/>
</DialogSuspense>
</Dialog>
);
}
export default compose(withDialogRedux())(PaymentMailDialog);

View File

@@ -0,0 +1,45 @@
// @ts-nocheck
import React, { createContext } from 'react';
import { usePaymentReceiveDefaultOptions } from '@/hooks/query';
import { DialogContent } from '@/components';
interface PaymentMailDialogBootValues {
paymentReceiveId: number;
mailOptions: any;
}
const PaymentMailDialogBootContext =
createContext<PaymentMailDialogBootValues>();
interface PaymentMailDialogBootProps {
paymentReceiveId: number;
children: React.ReactNode;
}
/**
* Payment mail dialog boot provider.
*/
function PaymentMailDialogBoot({
paymentReceiveId,
...props
}: PaymentMailDialogBootProps) {
const { data: mailOptions, isLoading: isMailOptionsLoading } =
usePaymentReceiveDefaultOptions(paymentReceiveId);
const provider = {
mailOptions,
isMailOptionsLoading,
paymentReceiveId
};
return (
<DialogContent isLoading={isMailOptionsLoading}>
<PaymentMailDialogBootContext.Provider value={provider} {...props} />
</DialogContent>
);
}
const usePaymentMailDialogBoot = () =>
React.useContext<PaymentMailDialogBootValues>(PaymentMailDialogBootContext);
export { PaymentMailDialogBoot, usePaymentMailDialogBoot };

View File

@@ -0,0 +1,17 @@
import { PaymentMailDialogBoot } from './PaymentMailDialogBoot';
import { PaymentMailDialogForm } from './PaymentMailDialogForm';
interface PaymentMailDialogContentProps {
dialogName: string;
paymentReceiveId: number;
}
export default function PaymentMailDialogContent({
dialogName,
paymentReceiveId,
}: PaymentMailDialogContentProps) {
return (
<PaymentMailDialogBoot paymentReceiveId={paymentReceiveId}>
<PaymentMailDialogForm />
</PaymentMailDialogBoot>
);
}

View File

@@ -0,0 +1,78 @@
// @ts-nocheck
import { Formik, FormikBag } from 'formik';
import * as R from 'ramda';
import { Intent } from '@blueprintjs/core';
import { usePaymentMailDialogBoot } from './PaymentMailDialogBoot';
import withDialogActions from '@/containers/Dialog/withDialogActions';
import { DialogsName } from '@/constants/dialogs';
import { useSendPaymentReceiveMail } from '@/hooks/query';
import { PaymentMailDialogFormContent } from './PaymentMailDialogFormContent';
import {
MailNotificationFormValues,
initialMailNotificationValues,
transformMailFormToRequest,
transformMailFormToInitialValues,
} from '@/containers/SendMailNotification/utils';
import { AppToaster } from '@/components';
const initialFormValues = {
...initialMailNotificationValues,
attachPayment: true,
};
interface PaymentMailFormValue extends MailNotificationFormValues {
attachPayment: boolean;
}
export function PaymentMailDialogFormRoot({
// #withDialogActions
closeDialog,
}) {
const { mailOptions, paymentReceiveId } = usePaymentMailDialogBoot();
const { mutateAsync: sendPaymentMail } = useSendPaymentReceiveMail();
const initialValues = transformMailFormToInitialValues(
mailOptions,
initialFormValues,
);
// Handles the form submitting.
const handleSubmit = (
values: PaymentMailFormValue,
{ setSubmitting }: FormikBag<PaymentMailFormValue>,
) => {
const reqValues = transformMailFormToRequest(values);
setSubmitting(true);
sendPaymentMail([paymentReceiveId, reqValues])
.then(() => {
AppToaster.show({
message: 'The mail notification has been sent successfully.',
intent: Intent.SUCCESS,
});
setSubmitting(false);
closeDialog(DialogsName.PaymentMail);
})
.catch(() => {
AppToaster.show({
message: 'Something went wrong.',
intent: Intent.DANGER,
});
setSubmitting(false);
closeDialog(DialogsName.PaymentMail);
});
};
const handleClose = () => {
closeDialog(DialogsName.PaymentMail);
};
return (
<Formik initialValues={initialValues} onSubmit={handleSubmit}>
<PaymentMailDialogFormContent onClose={handleClose} />
</Formik>
);
}
export const PaymentMailDialogForm = R.compose(withDialogActions)(
PaymentMailDialogFormRoot,
);

View File

@@ -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 { usePaymentMailDialogBoot } from './PaymentMailDialogBoot';
interface PaymentMailDialogFormContentProps {
onClose?: () => void;
}
export function PaymentMailDialogFormContent({
onClose,
}: PaymentMailDialogFormContentProps) {
const { mailOptions } = usePaymentMailDialogBoot();
const { isSubmitting } = useFormikContext();
const handleClose = () => {
saveInvoke(onClose);
};
return (
<Form>
<div className={Classes.DIALOG_BODY}>
<MailNotificationForm
fromAddresses={mailOptions.from_addresses}
toAddresses={mailOptions.to_addresses}
/>
<AttachFormGroup name={'attachPayment'} inline>
<FSwitch name={'attachPayment'} label={'Attach Payment'} />
</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;
`;

View File

@@ -0,0 +1 @@
export * from './PaymentMailDialog';

View File

@@ -17,12 +17,14 @@ import withPaymentReceives from './withPaymentReceives';
import withPaymentReceivesActions from './withPaymentReceivesActions';
import withAlertsActions from '@/containers/Alert/withAlertActions';
import withDrawerActions from '@/containers/Drawer/withDrawerActions';
import withDialogActions from '@/containers/Dialog/withDialogActions';
import withSettings from '@/containers/Settings/withSettings';
import { usePaymentReceivesColumns, ActionsMenu } from './components';
import { usePaymentReceivesListContext } from './PaymentReceiptsListProvider';
import { useMemorizedColumnsWidths } from '@/hooks';
import { DRAWERS } from '@/constants/drawers';
import { DialogsName } from '@/constants/dialogs';
/**
* Payment receives datatable.
@@ -31,15 +33,15 @@ function PaymentReceivesDataTable({
// #withPaymentReceivesActions
setPaymentReceivesTableState,
// #withPaymentReceives
paymentReceivesTableState,
// #withAlertsActions
openAlert,
// #withDrawerActions
openDrawer,
// #withDialogActions
openDialog,
// #withSettings
paymentReceivesTableSize,
}) {
@@ -73,6 +75,11 @@ function PaymentReceivesDataTable({
openDrawer(DRAWERS.PAYMENT_RECEIVE_DETAILS, { paymentReceiveId: id });
};
// Handle mail send payment receive.
const handleSendMailPayment = ({ id }) => {
openDialog(DialogsName.PaymentMail, { paymentReceiveId: id });
};
// Handle cell click.
const handleCellClick = (cell, event) => {
openDrawer(DRAWERS.PAYMENT_RECEIVE_DETAILS, {
@@ -129,6 +136,7 @@ function PaymentReceivesDataTable({
onDelete: handleDeletePaymentReceive,
onEdit: handleEditPaymentReceive,
onViewDetails: handleViewDetailPaymentReceive,
onSendMail: handleSendMailPayment,
}}
/>
</DashboardContentTable>
@@ -139,6 +147,7 @@ export default compose(
withPaymentReceivesActions,
withAlertsActions,
withDrawerActions,
withDialogActions,
withPaymentReceives(({ paymentReceivesTableState }) => ({
paymentReceivesTableState,
})),

View File

@@ -15,14 +15,17 @@ import {
import { FormatDateCell, Money, Icon, Can } from '@/components';
import { safeCallback } from '@/utils';
import { CLASSES } from '@/constants/classes';
import { PaymentReceiveAction, AbilitySubject } from '@/constants/abilityOption';
import {
PaymentReceiveAction,
AbilitySubject,
} from '@/constants/abilityOption';
/**
* Table actions menu.
*/
export function ActionsMenu({
row: { original: paymentReceive },
payload: { onEdit, onDelete, onViewDetails },
payload: { onEdit, onDelete, onViewDetails, onSendMail },
}) {
return (
<Menu>
@@ -31,6 +34,11 @@ export function ActionsMenu({
text={intl.get('view_details')}
onClick={safeCallback(onViewDetails, paymentReceive)}
/>
<MenuItem
icon={<Icon icon={'envelope'} iconSize={16} />}
text={'Send Mail'}
onClick={safeCallback(onSendMail, paymentReceive)}
/>
<Can I={PaymentReceiveAction.Edit} a={AbilitySubject.PaymentReceive}>
<MenuDivider />
<MenuItem

View File

@@ -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 ReceiptMailDialogContent = React.lazy(
() => import('./ReceiptMailDialogContent'),
);
/**
* Invoice mail dialog.
*/
function ReceiptMailDialog({
dialogName,
payload: { receiptId = null },
isOpen,
}) {
return (
<Dialog
name={dialogName}
title={'Receipt Mail'}
isOpen={isOpen}
canEscapeJeyClose={true}
autoFocus={true}
style={{ width: 600 }}
>
<DialogSuspense>
<ReceiptMailDialogContent
dialogName={dialogName}
receiptId={receiptId}
/>
</DialogSuspense>
</Dialog>
);
}
export default compose(withDialogRedux())(ReceiptMailDialog);

View File

@@ -0,0 +1,45 @@
// @ts-nocheck
import React, { createContext } from 'react';
import { useSaleReceiptDefaultOptions } from '@/hooks/query';
import { DialogContent } from '@/components';
interface ReceiptMailDialogBootValues {
receiptId: number;
mailOptions: any;
}
const ReceiptMailDialogBootContext =
createContext<ReceiptMailDialogBootValues>();
interface ReceiptMailDialogBootProps {
receiptId: number;
children: React.ReactNode;
}
/**
* Receipt mail dialog boot provider.
*/
function ReceiptMailDialogBoot({
receiptId,
...props
}: ReceiptMailDialogBootProps) {
const { data: mailOptions, isLoading: isMailOptionsLoading } =
useSaleReceiptDefaultOptions(receiptId);
const provider = {
saleReceiptId: receiptId,
mailOptions,
isMailOptionsLoading,
};
return (
<DialogContent isLoading={isMailOptionsLoading}>
<ReceiptMailDialogBootContext.Provider value={provider} {...props} />
</DialogContent>
);
}
const useReceiptMailDialogBoot = () =>
React.useContext<ReceiptMailDialogBootValues>(ReceiptMailDialogBootContext);
export { ReceiptMailDialogBoot, useReceiptMailDialogBoot };

View File

@@ -0,0 +1,18 @@
import React from 'react';
import { ReceiptMailDialogBoot } from './ReceiptMailDialogBoot';
import { ReceiptMailDialogForm } from './ReceiptMailDialogForm';
interface ReceiptMailDialogContentProps {
dialogName: string
receiptId: number;
}
export default function ReceiptMailDialogContent({
dialogName,
receiptId,
}: ReceiptMailDialogContentProps) {
return (
<ReceiptMailDialogBoot receiptId={receiptId}>
<ReceiptMailDialogForm />
</ReceiptMailDialogBoot>
);
}

View File

@@ -0,0 +1,74 @@
// @ts-nocheck
import { Formik, FormikBag } from 'formik';
import * as R from 'ramda';
import { Intent } from '@blueprintjs/core';
import { useReceiptMailDialogBoot } from './ReceiptMailDialogBoot';
import withDialogActions from '@/containers/Dialog/withDialogActions';
import { DialogsName } from '@/constants/dialogs';
import { useSendSaleReceiptMail } from '@/hooks/query';
import { ReceiptMailDialogFormContent } from './ReceiptMailDialogFormContent';
import {
initialMailNotificationValues,
MailNotificationFormValues,
transformMailFormToInitialValues,
transformMailFormToRequest,
} from '@/containers/SendMailNotification/utils';
import { AppToaster } from '@/components';
const initialFormValues = {
...initialMailNotificationValues,
attachReceipt: true,
};
interface ReceiptMailFormValues extends MailNotificationFormValues {
attachReceipt: boolean;
}
function ReceiptMailDialogFormRoot({ closeDialog }) {
const { mailOptions, saleReceiptId } = useReceiptMailDialogBoot();
const { mutateAsync: sendReceiptMail } = useSendSaleReceiptMail();
// Transformes mail options to initial form values.
const initialValues = transformMailFormToInitialValues(
mailOptions,
initialFormValues,
);
// Handle the form submitting.
const handleSubmit = (
values: ReceiptMailFormValues,
{ setSubmitting }: FormikBag<ReceiptMailFormValues>,
) => {
const reqValues = transformMailFormToRequest(values);
setSubmitting(true);
sendReceiptMail([saleReceiptId, reqValues])
.then(() => {
AppToaster.show({
message: 'The mail notification has been sent successfully.',
intent: Intent.SUCCESS,
});
closeDialog(DialogsName.ReceiptMail);
setSubmitting(false);
})
.catch(() => {
AppToaster.show({
message: 'Something went wrong.',
intent: Intent.DANGER,
});
setSubmitting(false);
});
};
// Handle the close button click.
const handleClose = () => {
closeDialog(DialogsName.ReceiptMail);
};
return (
<Formik initialValues={initialValues} onSubmit={handleSubmit}>
<ReceiptMailDialogFormContent onClose={handleClose} />
</Formik>
);
}
export const ReceiptMailDialogForm = R.compose(withDialogActions)(
ReceiptMailDialogFormRoot,
);

View File

@@ -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 { useReceiptMailDialogBoot } from './ReceiptMailDialogBoot';
import { saveInvoke } from '@/utils';
interface SendMailNotificationFormProps {
onClose?: () => void;
}
export function ReceiptMailDialogFormContent({
onClose,
}: SendMailNotificationFormProps) {
const { mailOptions } = useReceiptMailDialogBoot();
const { isSubmitting } = useFormikContext();
const handleClose = () => {
saveInvoke(onClose);
};
return (
<Form>
<div className={Classes.DIALOG_BODY}>
<MailNotificationForm
fromAddresses={mailOptions.from_addresses}
toAddresses={mailOptions.to_addresses}
/>
<AttachFormGroup name={'attachReceipt:'} inline>
<FSwitch name={'attachReceipt:'} label={'Attach Receipt'} />
</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;
`;

View File

@@ -0,0 +1 @@
export * from './ReceiptMailDialog';

View File

@@ -140,7 +140,6 @@ function ReceiptActionsBar({
icon={<Icon icon={'file-export-16'} iconSize={'16'} />}
text={<T id={'export'} />}
/>
<NavbarDivider />
<DashboardRowsHeightButton
initialValue={receiptsTableSize}

View File

@@ -24,6 +24,7 @@ import { useReceiptsListContext } from './ReceiptsListProvider';
import { useReceiptsTableColumns, ActionsMenu } from './components';
import { useMemorizedColumnsWidths } from '@/hooks';
import { DRAWERS } from '@/constants/drawers';
import { DialogsName } from '@/constants/dialogs';
/**
* Sale receipts datatable.
@@ -86,6 +87,11 @@ function ReceiptsDataTable({
openDialog('receipt-pdf-preview', { receiptId: id });
};
// Handle send mail receipt.
const handleSendMailReceipt = ({ id }) => {
openDialog(DialogsName.ReceiptMail, { receiptId: id });
};
// Local storage memorizing columns widths.
const [initialColumnsWidths, , handleColumnResizing] =
useMemorizedColumnsWidths(TABLES.RECEIPTS);
@@ -141,6 +147,7 @@ function ReceiptsDataTable({
onClose: handleCloseReceipt,
onViewDetails: handleViewDetailReceipt,
onPrint: handlePrintInvoice,
onSendMail: handleSendMailReceipt,
}}
/>
</DashboardContentTable>

View File

@@ -24,7 +24,7 @@ import { SaleReceiptAction, AbilitySubject } from '@/constants/abilityOption';
* @returns {React.JSX}
*/
export function ActionsMenu({
payload: { onEdit, onDelete, onClose, onDrawer, onViewDetails, onPrint },
payload: { onEdit, onDelete, onClose, onSendMail, onViewDetails, onPrint },
row: { original: receipt },
}) {
return (
@@ -51,6 +51,11 @@ export function ActionsMenu({
</If>
</Can>
<Can I={SaleReceiptAction.View} a={AbilitySubject.Receipt}>
<MenuItem
icon={<Icon icon={'envelope'} iconSize={16} />}
text={'Send Mail'}
onClick={safeCallback(onSendMail, receipt)}
/>
<MenuItem
icon={<Icon icon={'print-16'} iconSize={16} />}
text={intl.get('print')}

View File

@@ -0,0 +1,143 @@
// @ts-nocheck
import {
Box,
FFormGroup,
FInputGroup,
FMultiSelect,
FRichEditor,
Hint,
} from '@/components';
import styled from 'styled-components';
import { Position } from '@blueprintjs/core';
import { SelectOptionProps } from '@blueprintjs-formik/select';
interface MailNotificationFormProps {
fromAddresses: SelectOptionProps[];
toAddresses: SelectOptionProps[];
}
const commonAddressSelect = {
placeholder: '',
labelAccessor: '',
valueAccessor: 'mail',
tagAccessor: (item) => `<${item.label}> (${item.mail})`,
textAccessor: (item) => `<${item.label}> (${item.mail})`,
};
export function MailNotificationForm({
fromAddresses,
toAddresses,
}: MailNotificationFormProps) {
return (
<Box>
<HeaderBox>
<FFormGroup
label={'From'}
labelInfo={
<Hint
content={'asdasd asdasd asdsad'}
position={Position.BOTTOM_LEFT}
/>
}
name={'from'}
inline={true}
fastField={true}
>
<FMultiSelect
items={fromAddresses}
name={'from'}
popoverProps={{ minimal: true, fill: true }}
tagInputProps={{
tagProps: { round: true, minimal: true, large: true },
}}
fill={true}
{...commonAddressSelect}
/>
</FFormGroup>
<FFormGroup label={'To'} name={'to'} inline={true} fastField={true}>
<FMultiSelect
items={toAddresses}
name={'to'}
placeholder=""
popoverProps={{ minimal: true, fill: true }}
tagInputProps={{
tagProps: { round: true, minimal: true, large: true },
}}
fill={true}
{...commonAddressSelect}
/>
</FFormGroup>
<FFormGroup
label={'Subject'}
name={'subject'}
inline={true}
fastField={true}
>
<FInputGroup name={'subject'} fill={true} />
</FFormGroup>
</HeaderBox>
<MailMessageEditor name={'body'} />
</Box>
);
}
const MailMessageEditor = styled(FRichEditor)`
padding: 15px;
border: 1px solid #dedfe9;
border-top: 0;
border-bottom-left-radius: 5px;
border-bottom-right-radius: 5px;
`;
const HeaderBox = styled('div')`
border-top-right-radius: 5px;
border-top-left-radius: 5px;
border: 1px solid #dddfe9;
border-bottom: 2px solid #eaeaef;
padding: 6px 15px;
.bp4-form-group {
margin: 0;
padding-top: 8px;
padding-bottom: 8px;
&:not(:last-of-type) {
border-bottom: 1px solid #dddfe9;
}
&:first-of-type {
padding-top: 0;
}
&:last-of-type {
padding-bottom: 0;
}
}
.bp4-form-content {
flex: 1 0;
}
.bp4-label {
min-width: 65px;
color: #738091;
}
.bp4-input {
border-color: transparent;
padding: 0;
&:focus,
&.bp4-active {
box-shadow: 0 0 0 0;
}
}
.bp4-input-ghost {
margin-top: 5px;
}
.bp4-tag-input-values {
margin: 0;
}
`;

View File

@@ -0,0 +1 @@
export * from './MailNotificationForm';

View File

@@ -0,0 +1,44 @@
import { castArray, first } from 'lodash';
import { transformToForm } from '@/utils';
export const initialMailNotificationValues = {
from: [],
to: [],
subject: '',
body: '',
};
export interface MailNotificationFormValues {
from: string[];
to: string[];
subject: string;
body: string;
}
export const transformMailFormToRequest = (
values: MailNotificationFormValues,
) => {
return {
...values,
from: first(values.from),
to: values.to?.join(', '),
};
};
/**
* Transformes the mail options response values to form initial values.
* @param {any} mailOptions
* @param {MailNotificationFormValues} initialValues
* @returns {MailNotificationFormValues}
*/
export const transformMailFormToInitialValues = (
mailOptions: any,
initialValues: MailNotificationFormValues,
): MailNotificationFormValues => {
return {
...initialValues,
...transformToForm(mailOptions, initialValues),
from: mailOptions.from ? castArray(mailOptions.from) : [],
to: mailOptions.to ? castArray(mailOptions.to) : [],
};
};

View File

@@ -239,3 +239,33 @@ export function useEstimateSMSDetail(estimateId, props, requestProps) {
},
);
}
export function useSendSaleEstimateMail(props) {
const queryClient = useQueryClient();
const apiRequest = useApiRequest();
return useMutation(
([id, values]) => apiRequest.post(`sales/estimates/${id}/mail`, values),
{
onSuccess: (res, [id, values]) => {
// Common invalidate queries.
commonInvalidateQueries(queryClient);
},
...props,
},
);
}
export function useSaleEstimateDefaultOptions(estimateId, props) {
return useRequestQuery(
[t.SALE_ESTIMATE_MAIL_OPTIONS, estimateId],
{
method: 'get',
url: `sales/estimates/${estimateId}/mail`,
},
{
select: (res) => res.data.data,
...props,
},
);
}

View File

@@ -306,3 +306,34 @@ export function useInvoicePaymentTransactions(invoiceId, props) {
},
);
}
export function useSendSaleInvoiceMail(props) {
const queryClient = useQueryClient();
const apiRequest = useApiRequest();
return useMutation(
([id, values]) => apiRequest.post(`sales/invoices/${id}/mail`, values),
{
onSuccess: (res, [id, values]) => {
// Common invalidate queries.
commonInvalidateQueries(queryClient);
},
...props,
},
);
}
export function useSaleInvoiceDefaultOptions(invoiceId, props) {
return useRequestQuery(
[t.SALE_INVOICE_DEFAULT_OPTIONS, invoiceId],
{
method: 'get',
url: `sales/invoices/${invoiceId}/mail`,
},
{
select: (res) => res.data.data,
...props,
},
);
}

View File

@@ -234,3 +234,34 @@ export function usePaymentReceiveSMSDetail(
export function usePdfPaymentReceive(paymentReceiveId) {
return useRequestPdf(`sales/payment_receives/${paymentReceiveId}`);
}
export function useSendPaymentReceiveMail(props) {
const queryClient = useQueryClient();
const apiRequest = useApiRequest();
return useMutation(
([id, values]) =>
apiRequest.post(`sales/payment_receives/${id}/mail`, values),
{
onSuccess: (res, [id, values]) => {
// Common invalidate queries.
commonInvalidateQueries(queryClient);
},
...props,
},
);
}
export function usePaymentReceiveDefaultOptions(paymentReceiveId, props) {
return useRequestQuery(
[t.PAYMENT_RECEIVE_MAIL_OPTIONS, paymentReceiveId],
{
method: 'get',
url: `sales/payment_receives/${paymentReceiveId}/mail`,
},
{
select: (res) => res.data.data,
...props,
},
);
}

View File

@@ -207,3 +207,36 @@ export function useReceiptSMSDetail(receiptId, props, requestProps) {
},
);
}
/**
*
*/
export function useSendSaleReceiptMail(props) {
const queryClient = useQueryClient();
const apiRequest = useApiRequest();
return useMutation(
([id, values]) => apiRequest.post(`sales/receipts/${id}/mail`, values),
{
onSuccess: () => {
// Invalidate queries.
commonInvalidateQueries(queryClient);
},
...props,
},
);
}
export function useSaleReceiptDefaultOptions(invoiceId, props) {
return useRequestQuery(
[t.SALE_RECEIPT_MAIL_OPTIONS, invoiceId],
{
method: 'get',
url: `sales/receipts/${invoiceId}/mail`,
},
{
select: (res) => res.data.data,
...props,
},
);
}

View File

@@ -69,6 +69,7 @@ const SALE_ESTIMATES = {
SALE_ESTIMATE: 'SALE_ESTIMATE',
SALE_ESTIMATE_SMS_DETAIL: 'SALE_ESTIMATE_SMS_DETAIL',
NOTIFY_SALE_ESTIMATE_BY_SMS: 'NOTIFY_SALE_ESTIMATE_BY_SMS',
SALE_ESTIMATE_MAIL_OPTIONS: 'SALE_ESTIMATE_MAIL_OPTIONS',
};
const SALE_RECEIPTS = {
@@ -76,6 +77,7 @@ const SALE_RECEIPTS = {
SALE_RECEIPT: 'SALE_RECEIPT',
SALE_RECEIPT_SMS_DETAIL: 'SALE_RECEIPT_SMS_DETAIL',
NOTIFY_SALE_RECEIPT_BY_SMS: 'NOTIFY_SALE_RECEIPT_BY_SMS',
SALE_RECEIPT_MAIL_OPTIONS: 'SALE_RECEIPT_MAIL_OPTIONS'
};
const INVENTORY_ADJUSTMENTS = {
@@ -101,6 +103,7 @@ const PAYMENT_RECEIVES = {
PAYMENT_RECEIVE_EDIT_PAGE: 'PAYMENT_RECEIVE_EDIT_PAGE',
PAYMENT_RECEIVE_SMS_DETAIL: 'PAYMENT_RECEIVE_SMS_DETAIL',
NOTIFY_PAYMENT_RECEIVE_BY_SMS: 'NOTIFY_PAYMENT_RECEIVE_BY_SMS',
PAYMENT_RECEIVE_MAIL_OPTIONS: 'PAYMENT_RECEIVE_MAIL_OPTIONS',
};
const SALE_INVOICES = {
@@ -112,6 +115,7 @@ const SALE_INVOICES = {
BAD_DEBT: 'BAD_DEBT',
CANCEL_BAD_DEBT: 'CANCEL_BAD_DEBT',
SALE_INVOICE_PAYMENT_TRANSACTIONS: 'SALE_INVOICE_PAYMENT_TRANSACTIONS',
SALE_INVOICE_DEFAULT_OPTIONS: 'SALE_INVOICE_DEFAULT_OPTIONS'
};
const USERS = {

View File

@@ -561,8 +561,14 @@ export default {
},
'content-copy': {
path: [
'M15 0H5c-.55 0-1 .45-1 1v2h2V2h8v7h-1v2h2c.55 0 1-.45 1-1V1c0-.55-.45-1-1-1zm-4 4H1c-.55 0-1 .45-1 1v10c0 .55.45 1 1 1h10c.55 0 1-.45 1-1V5c0-.55-.45-1-1-1zm-1 10H2V6h8v8z'
'M15 0H5c-.55 0-1 .45-1 1v2h2V2h8v7h-1v2h2c.55 0 1-.45 1-1V1c0-.55-.45-1-1-1zm-4 4H1c-.55 0-1 .45-1 1v10c0 .55.45 1 1 1h10c.55 0 1-.45 1-1V5c0-.55-.45-1-1-1zm-1 10H2V6h8v8z',
],
viewBox: '0 0 16 16'
}
viewBox: '0 0 16 16',
},
envelope: {
path: [
'M0 4.01v11.91l6.27-6.27L0 4.01zm18.91-1.03H1.09L10 10.97l8.91-7.99zm-5.18 6.66L20 15.92V4.01l-6.27 5.63zm-3.23 2.9c-.13.12-.31.19-.5.19s-.37-.07-.5-.19l-2.11-1.89-6.33 6.33h17.88l-6.33-6.33-2.11 1.89z',
],
viewBox: '0 0 20 20',
},
};

555
pnpm-lock.yaml generated
View File

@@ -471,6 +471,27 @@ importers:
'@testing-library/user-event':
specifier: ^7.2.1
version: 7.2.1(@testing-library/dom@8.20.0)
'@tiptap/core':
specifier: 2.1.13
version: 2.1.13(@tiptap/pm@2.1.13)
'@tiptap/extension-color':
specifier: latest
version: 2.1.13(@tiptap/core@2.1.13)(@tiptap/extension-text-style@2.1.13)
'@tiptap/extension-list-item':
specifier: 2.1.13
version: 2.1.13(@tiptap/core@2.1.13)
'@tiptap/extension-text-style':
specifier: 2.1.13
version: 2.1.13(@tiptap/core@2.1.13)
'@tiptap/pm':
specifier: 2.1.13
version: 2.1.13
'@tiptap/react':
specifier: 2.1.13
version: 2.1.13(@tiptap/core@2.1.13)(@tiptap/pm@2.1.13)(react-dom@18.2.0)(react@18.2.0)
'@tiptap/starter-kit':
specifier: 2.1.13
version: 2.1.13(@tiptap/pm@2.1.13)
'@types/jest':
specifier: ^26.0.15
version: 26.0.24
@@ -5690,6 +5711,34 @@ packages:
reselect: 4.1.7
dev: false
/@remirror/core-constants@2.0.2:
resolution: {integrity: sha512-dyHY+sMF0ihPus3O27ODd4+agdHMEmuRdyiZJ2CCWjPV5UFmn17ZbElvk6WOGVE4rdCJKZQCrPV2BcikOMLUGQ==}
dev: false
/@remirror/core-helpers@3.0.0:
resolution: {integrity: sha512-tusEgQJIqg4qKj6HSBUFcyRnWnziw3neh4T9wOmsPGHFC3w9kl5KSrDb9UAgE8uX6y32FnS7vJ955mWOl3n50A==}
dependencies:
'@remirror/core-constants': 2.0.2
'@remirror/types': 1.0.1
'@types/object.omit': 3.0.3
'@types/object.pick': 1.3.4
'@types/throttle-debounce': 2.1.0
case-anything: 2.1.13
dash-get: 1.0.2
deepmerge: 4.3.1
fast-deep-equal: 3.1.3
make-error: 1.3.6
object.omit: 3.0.0
object.pick: 1.3.0
throttle-debounce: 3.0.1
dev: false
/@remirror/types@1.0.1:
resolution: {integrity: sha512-VlZQxwGnt1jtQ18D6JqdIF+uFZo525WEqrfp9BOc3COPpK4+AWCgdnAWL+ho6imWcoINlGjR/+3b6y5C1vBVEA==}
dependencies:
type-fest: 2.19.0
dev: false
/@rollup/plugin-babel@5.3.1(@babel/core@7.20.12)(rollup@2.79.1):
resolution: {integrity: sha512-WFfdLWU/xVWKeRQnKmIAQULUI7Il0gZnBIH/ZFO069wYIfPu+8zrfp/KMW0atmELoRDq8FbiP3VCss9MhCut7Q==}
engines: {node: '>= 10.0.0'}
@@ -5968,6 +6017,273 @@ packages:
'@testing-library/dom': 8.20.0
dev: false
/@tiptap/core@2.1.13(@tiptap/pm@2.1.13):
resolution: {integrity: sha512-cMC8bgTN63dj1Mv82iDeeLl6sa9kY0Pug8LSalxVEptRmyFVsVxGgu2/6Y3T+9aCYScxfS06EkA8SdzFMAwYTQ==}
peerDependencies:
'@tiptap/pm': ^2.0.0
dependencies:
'@tiptap/pm': 2.1.13
dev: false
/@tiptap/extension-blockquote@2.1.13(@tiptap/core@2.1.13):
resolution: {integrity: sha512-oe6wSQACmODugoP9XH3Ouffjy4BsOBWfTC+dETHNCG6ZED6ShHN3CB9Vr7EwwRgmm2WLaKAjMO1sVumwH+Z1rg==}
peerDependencies:
'@tiptap/core': ^2.0.0
dependencies:
'@tiptap/core': 2.1.13(@tiptap/pm@2.1.13)
dev: false
/@tiptap/extension-bold@2.1.13(@tiptap/core@2.1.13):
resolution: {integrity: sha512-6cHsQTh/rUiG4jkbJer3vk7g60I5tBwEBSGpdxmEHh83RsvevD8+n92PjA24hYYte5RNlATB011E1wu8PVhSvw==}
peerDependencies:
'@tiptap/core': ^2.0.0
dependencies:
'@tiptap/core': 2.1.13(@tiptap/pm@2.1.13)
dev: false
/@tiptap/extension-bubble-menu@2.1.13(@tiptap/core@2.1.13)(@tiptap/pm@2.1.13):
resolution: {integrity: sha512-Hm7e1GX3AI6lfaUmr6WqsS9MMyXIzCkhh+VQi6K8jj4Q4s8kY4KPoAyD/c3v9pZ/dieUtm2TfqrOCkbHzsJQBg==}
peerDependencies:
'@tiptap/core': ^2.0.0
'@tiptap/pm': ^2.0.0
dependencies:
'@tiptap/core': 2.1.13(@tiptap/pm@2.1.13)
'@tiptap/pm': 2.1.13
tippy.js: 6.3.7
dev: false
/@tiptap/extension-bullet-list@2.1.13(@tiptap/core@2.1.13):
resolution: {integrity: sha512-NkWlQ5bLPUlcROj6G/d4oqAxMf3j3wfndGOPp0z8OoXJtVbVoXl/aMSlLbVgE6n8r6CS8MYxKhXNxrb7Ll2foA==}
peerDependencies:
'@tiptap/core': ^2.0.0
dependencies:
'@tiptap/core': 2.1.13(@tiptap/pm@2.1.13)
dev: false
/@tiptap/extension-code-block@2.1.13(@tiptap/core@2.1.13)(@tiptap/pm@2.1.13):
resolution: {integrity: sha512-E3tweNExPOV+t1ODKX0MDVsS0aeHGWc1ECt+uyp6XwzsN0bdF2A5+pttQqM7sTcMnQkVACGFbn9wDeLRRcfyQg==}
peerDependencies:
'@tiptap/core': ^2.0.0
'@tiptap/pm': ^2.0.0
dependencies:
'@tiptap/core': 2.1.13(@tiptap/pm@2.1.13)
'@tiptap/pm': 2.1.13
dev: false
/@tiptap/extension-code@2.1.13(@tiptap/core@2.1.13):
resolution: {integrity: sha512-f5fLYlSgliVVa44vd7lQGvo49+peC+Z2H0Fn84TKNCH7tkNZzouoJsHYn0/enLaQ9Sq+24YPfqulfiwlxyiT8w==}
peerDependencies:
'@tiptap/core': ^2.0.0
dependencies:
'@tiptap/core': 2.1.13(@tiptap/pm@2.1.13)
dev: false
/@tiptap/extension-color@2.1.13(@tiptap/core@2.1.13)(@tiptap/extension-text-style@2.1.13):
resolution: {integrity: sha512-T3tJXCIfFxzIlGOhvbPVIZa3y36YZRPYIo2TKsgkTz8LiMob6hRXXNFjsrFDp2Fnu3DrBzyvrorsW7767s4eYg==}
peerDependencies:
'@tiptap/core': ^2.0.0
'@tiptap/extension-text-style': ^2.0.0
dependencies:
'@tiptap/core': 2.1.13(@tiptap/pm@2.1.13)
'@tiptap/extension-text-style': 2.1.13(@tiptap/core@2.1.13)
dev: false
/@tiptap/extension-document@2.1.13(@tiptap/core@2.1.13):
resolution: {integrity: sha512-wLwiTWsVmZTGIE5duTcHRmW4ulVxNW4nmgfpk95+mPn1iKyNGtrVhGWleLhBlTj+DWXDtcfNWZgqZkZNzhkqYQ==}
peerDependencies:
'@tiptap/core': ^2.0.0
dependencies:
'@tiptap/core': 2.1.13(@tiptap/pm@2.1.13)
dev: false
/@tiptap/extension-dropcursor@2.1.13(@tiptap/core@2.1.13)(@tiptap/pm@2.1.13):
resolution: {integrity: sha512-NAyJi4BJxH7vl/2LNS1X0ndwFKjEtX+cRgshXCnMyh7qNpIRW6Plczapc/W1OiMncOEhZJfpZfkRSfwG01FWFg==}
peerDependencies:
'@tiptap/core': ^2.0.0
'@tiptap/pm': ^2.0.0
dependencies:
'@tiptap/core': 2.1.13(@tiptap/pm@2.1.13)
'@tiptap/pm': 2.1.13
dev: false
/@tiptap/extension-floating-menu@2.1.13(@tiptap/core@2.1.13)(@tiptap/pm@2.1.13):
resolution: {integrity: sha512-9Oz7pk1Nts2+EyY+rYfnREGbLzQ5UFazAvRhF6zAJdvyuDmAYm0Jp6s0GoTrpV0/dJEISoFaNpPdMJOb9EBNRw==}
peerDependencies:
'@tiptap/core': ^2.0.0
'@tiptap/pm': ^2.0.0
dependencies:
'@tiptap/core': 2.1.13(@tiptap/pm@2.1.13)
'@tiptap/pm': 2.1.13
tippy.js: 6.3.7
dev: false
/@tiptap/extension-gapcursor@2.1.13(@tiptap/core@2.1.13)(@tiptap/pm@2.1.13):
resolution: {integrity: sha512-Cl5apsoTcyPPCgE3ThufxQxZ1wyqqh+9uxUN9VF9AbeTkid6oPZvKXwaILf6AFnkSy+SuKrb9kZD2iaezxpzXw==}
peerDependencies:
'@tiptap/core': ^2.0.0
'@tiptap/pm': ^2.0.0
dependencies:
'@tiptap/core': 2.1.13(@tiptap/pm@2.1.13)
'@tiptap/pm': 2.1.13
dev: false
/@tiptap/extension-hard-break@2.1.13(@tiptap/core@2.1.13):
resolution: {integrity: sha512-TGkMzMQayuKg+vN4du0x1ahEItBLcCT1jdWeRsjdM8gHfzbPLdo4PQhVsvm1I0xaZmbJZelhnVsUwRZcIu1WNA==}
peerDependencies:
'@tiptap/core': ^2.0.0
dependencies:
'@tiptap/core': 2.1.13(@tiptap/pm@2.1.13)
dev: false
/@tiptap/extension-heading@2.1.13(@tiptap/core@2.1.13):
resolution: {integrity: sha512-PEmc19QLmlVUTiHWoF0hpgNTNPNU0nlaFmMKskzO+cx5Df4xvHmv/UqoIwp7/UFbPMkfVJT1ozQU7oD1IWn9Hg==}
peerDependencies:
'@tiptap/core': ^2.0.0
dependencies:
'@tiptap/core': 2.1.13(@tiptap/pm@2.1.13)
dev: false
/@tiptap/extension-history@2.1.13(@tiptap/core@2.1.13)(@tiptap/pm@2.1.13):
resolution: {integrity: sha512-1ouitThGTBUObqw250aDwGLMNESBH5PRXIGybsCFO1bktdmWtEw7m72WY41EuX2BH8iKJpcYPerl3HfY1vmCNw==}
peerDependencies:
'@tiptap/core': ^2.0.0
'@tiptap/pm': ^2.0.0
dependencies:
'@tiptap/core': 2.1.13(@tiptap/pm@2.1.13)
'@tiptap/pm': 2.1.13
dev: false
/@tiptap/extension-horizontal-rule@2.1.13(@tiptap/core@2.1.13)(@tiptap/pm@2.1.13):
resolution: {integrity: sha512-7OgjgNqZXvBejgULNdMSma2M1nzv4bbZG+FT5XMFZmEOxR9IB1x/RzChjPdeicff2ZK2sfhMBc4Y9femF5XkUg==}
peerDependencies:
'@tiptap/core': ^2.0.0
'@tiptap/pm': ^2.0.0
dependencies:
'@tiptap/core': 2.1.13(@tiptap/pm@2.1.13)
'@tiptap/pm': 2.1.13
dev: false
/@tiptap/extension-italic@2.1.13(@tiptap/core@2.1.13):
resolution: {integrity: sha512-HyDJfuDn5hzwGKZiANcvgz6wcum6bEgb4wmJnfej8XanTMJatNVv63TVxCJ10dSc9KGpPVcIkg6W8/joNXIEbw==}
peerDependencies:
'@tiptap/core': ^2.0.0
dependencies:
'@tiptap/core': 2.1.13(@tiptap/pm@2.1.13)
dev: false
/@tiptap/extension-list-item@2.1.13(@tiptap/core@2.1.13):
resolution: {integrity: sha512-6e8iiCWXOiJTl1XOwVW2tc0YG18h70HUtEHFCx2m5HspOGFKsFEaSS3qYxOheM9HxlmQeDt8mTtqftRjEFRxPQ==}
peerDependencies:
'@tiptap/core': ^2.0.0
dependencies:
'@tiptap/core': 2.1.13(@tiptap/pm@2.1.13)
dev: false
/@tiptap/extension-ordered-list@2.1.13(@tiptap/core@2.1.13):
resolution: {integrity: sha512-UO4ZAL5Vrr1WwER5VjgmeNIWHpqy9cnIRo1En07gZ0OWTjs1eITPcu+4TCn1ZG6DhoFvAQzE5DTxxdhIotg+qw==}
peerDependencies:
'@tiptap/core': ^2.0.0
dependencies:
'@tiptap/core': 2.1.13(@tiptap/pm@2.1.13)
dev: false
/@tiptap/extension-paragraph@2.1.13(@tiptap/core@2.1.13):
resolution: {integrity: sha512-cEoZBJrsQn69FPpUMePXG/ltGXtqKISgypj70PEHXt5meKDjpmMVSY4/8cXvFYEYsI9GvIwyAK0OrfAHiSoROA==}
peerDependencies:
'@tiptap/core': ^2.0.0
dependencies:
'@tiptap/core': 2.1.13(@tiptap/pm@2.1.13)
dev: false
/@tiptap/extension-strike@2.1.13(@tiptap/core@2.1.13):
resolution: {integrity: sha512-VN6zlaCNCbyJUCDyBFxavw19XmQ4LkCh8n20M8huNqW77lDGXA2A7UcWLHaNBpqAijBRu9mWI8l4Bftyf2fcAw==}
peerDependencies:
'@tiptap/core': ^2.0.0
dependencies:
'@tiptap/core': 2.1.13(@tiptap/pm@2.1.13)
dev: false
/@tiptap/extension-text-style@2.1.13(@tiptap/core@2.1.13):
resolution: {integrity: sha512-K9/pNHxpZKQoc++crxrsppVUSeHv8YevfY2FkJ4YMaekGcX+q4BRrHR0tOfii4izAUPJF2L0/PexLQaWXtAY1w==}
peerDependencies:
'@tiptap/core': ^2.0.0
dependencies:
'@tiptap/core': 2.1.13(@tiptap/pm@2.1.13)
dev: false
/@tiptap/extension-text@2.1.13(@tiptap/core@2.1.13):
resolution: {integrity: sha512-zzsTTvu5U67a8WjImi6DrmpX2Q/onLSaj+LRWPh36A1Pz2WaxW5asZgaS+xWCnR+UrozlCALWa01r7uv69jq0w==}
peerDependencies:
'@tiptap/core': ^2.0.0
dependencies:
'@tiptap/core': 2.1.13(@tiptap/pm@2.1.13)
dev: false
/@tiptap/pm@2.1.13:
resolution: {integrity: sha512-zNbA7muWsHuVg12GrTgN/j119rLePPq5M8dZgkKxUwdw8VmU3eUyBp1SihPEXJ2U0MGdZhNhFX7Y74g11u66sg==}
dependencies:
prosemirror-changeset: 2.2.1
prosemirror-collab: 1.3.1
prosemirror-commands: 1.5.2
prosemirror-dropcursor: 1.8.1
prosemirror-gapcursor: 1.3.2
prosemirror-history: 1.3.2
prosemirror-inputrules: 1.3.0
prosemirror-keymap: 1.2.2
prosemirror-markdown: 1.12.0
prosemirror-menu: 1.2.4
prosemirror-model: 1.19.4
prosemirror-schema-basic: 1.2.2
prosemirror-schema-list: 1.3.0
prosemirror-state: 1.4.3
prosemirror-tables: 1.3.5
prosemirror-trailing-node: 2.0.7(prosemirror-model@1.19.4)(prosemirror-state@1.4.3)(prosemirror-view@1.32.7)
prosemirror-transform: 1.8.0
prosemirror-view: 1.32.7
dev: false
/@tiptap/react@2.1.13(@tiptap/core@2.1.13)(@tiptap/pm@2.1.13)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-Dq3f8EtJnpImP3iDtJo+7bulnN9SJZRZcVVzxHXccLcC2MxtmDdlPGZjP+wxO800nd8toSIOd5734fPNf/YcfA==}
peerDependencies:
'@tiptap/core': ^2.0.0
'@tiptap/pm': ^2.0.0
react: ^17.0.0 || ^18.0.0
react-dom: ^17.0.0 || ^18.0.0
dependencies:
'@tiptap/core': 2.1.13(@tiptap/pm@2.1.13)
'@tiptap/extension-bubble-menu': 2.1.13(@tiptap/core@2.1.13)(@tiptap/pm@2.1.13)
'@tiptap/extension-floating-menu': 2.1.13(@tiptap/core@2.1.13)(@tiptap/pm@2.1.13)
'@tiptap/pm': 2.1.13
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
dev: false
/@tiptap/starter-kit@2.1.13(@tiptap/pm@2.1.13):
resolution: {integrity: sha512-ph/mUR/OwPtPkZ5rNHINxubpABn8fHnvJSdhXFrY/q6SKoaO11NZXgegRaiG4aL7O6Sz4LsZVw6Sm0Ae+GJmrg==}
dependencies:
'@tiptap/core': 2.1.13(@tiptap/pm@2.1.13)
'@tiptap/extension-blockquote': 2.1.13(@tiptap/core@2.1.13)
'@tiptap/extension-bold': 2.1.13(@tiptap/core@2.1.13)
'@tiptap/extension-bullet-list': 2.1.13(@tiptap/core@2.1.13)
'@tiptap/extension-code': 2.1.13(@tiptap/core@2.1.13)
'@tiptap/extension-code-block': 2.1.13(@tiptap/core@2.1.13)(@tiptap/pm@2.1.13)
'@tiptap/extension-document': 2.1.13(@tiptap/core@2.1.13)
'@tiptap/extension-dropcursor': 2.1.13(@tiptap/core@2.1.13)(@tiptap/pm@2.1.13)
'@tiptap/extension-gapcursor': 2.1.13(@tiptap/core@2.1.13)(@tiptap/pm@2.1.13)
'@tiptap/extension-hard-break': 2.1.13(@tiptap/core@2.1.13)
'@tiptap/extension-heading': 2.1.13(@tiptap/core@2.1.13)
'@tiptap/extension-history': 2.1.13(@tiptap/core@2.1.13)(@tiptap/pm@2.1.13)
'@tiptap/extension-horizontal-rule': 2.1.13(@tiptap/core@2.1.13)(@tiptap/pm@2.1.13)
'@tiptap/extension-italic': 2.1.13(@tiptap/core@2.1.13)
'@tiptap/extension-list-item': 2.1.13(@tiptap/core@2.1.13)
'@tiptap/extension-ordered-list': 2.1.13(@tiptap/core@2.1.13)
'@tiptap/extension-paragraph': 2.1.13(@tiptap/core@2.1.13)
'@tiptap/extension-strike': 2.1.13(@tiptap/core@2.1.13)
'@tiptap/extension-text': 2.1.13(@tiptap/core@2.1.13)
transitivePeerDependencies:
- '@tiptap/pm'
dev: false
/@tootallnate/once@1.1.2:
resolution: {integrity: sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==}
engines: {node: '>= 6'}
@@ -6276,6 +6592,14 @@ packages:
/@types/normalize-package-data@2.4.1:
resolution: {integrity: sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==}
/@types/object.omit@3.0.3:
resolution: {integrity: sha512-xrq4bQTBGYY2cw+gV4PzoG2Lv3L0pjZ1uXStRRDQoATOYW1lCsFQHhQ+OkPhIcQoqLjAq7gYif7D14Qaa6Zbew==}
dev: false
/@types/object.pick@1.3.4:
resolution: {integrity: sha512-5PjwB0uP2XDp3nt5u5NJAG2DORHIRClPzWT/TTZhJ2Ekwe8M5bA9tvPdi9NO/n2uvu2/ictat8kgqvLfcIE1SA==}
dev: false
/@types/parse-json@4.0.0:
resolution: {integrity: sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==}
@@ -6473,6 +6797,10 @@ packages:
pretty-format: 25.5.0
dev: false
/@types/throttle-debounce@2.1.0:
resolution: {integrity: sha512-5eQEtSCoESnh2FsiLTxE121IiE60hnMqcb435fShf4bpLRjEu1Eoekht23y6zXS9Ts3l+Szu3TARnTsA0GkOkQ==}
dev: false
/@types/triple-beam@1.3.2:
resolution: {integrity: sha512-txGIh+0eDFzKGC25zORnswy+br1Ha7hj5cMVwKIU7+s0U2AxxJru/jZSMU6OC9MJWP6+pc/hc6ZjyZShpsyY2g==}
dev: false
@@ -8839,6 +9167,11 @@ packages:
engines: {node: '>=0.10.0'}
dev: false
/case-anything@2.1.13:
resolution: {integrity: sha512-zlOQ80VrQ2Ue+ymH5OuM/DlDq64mEm+B9UTdHULv5osUMD6HalNTblf2b1u/m6QecjsnOkBpqVZ+XPwIVsy7Ng==}
engines: {node: '>=12.13'}
dev: false
/case-sensitive-paths-webpack-plugin@2.4.0:
resolution: {integrity: sha512-roIFONhcxog0JSSWbvVAh3OocukmSgpqOH6YpMkCvav/ySIV3JKg4Dc8vYtQjYi/UxpNE36r/9v+VqTQqgkYmw==}
engines: {node: '>=4'}
@@ -9780,6 +10113,10 @@ packages:
/create-require@1.1.1:
resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==}
/crelt@1.0.6:
resolution: {integrity: sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==}
dev: false
/cron-parser@3.5.0:
resolution: {integrity: sha512-wyVZtbRs6qDfFd8ap457w3XVntdvqcwBGxBoTvJQH9KGVKL/fB+h2k3C8AqiVxvUQKN1Ps/Ns46CNViOpVDhfQ==}
engines: {node: '>=0.8'}
@@ -10174,6 +10511,10 @@ packages:
engines: {node: '>=8'}
dev: true
/dash-get@1.0.2:
resolution: {integrity: sha512-4FbVrHDwfOASx7uQVxeiCTo7ggSdYZbqs8lH+WU6ViypPlDbe9y6IP5VVUDQBv9DcnyaiPT5XT0UWHgJ64zLeQ==}
dev: false
/dashdash@1.14.1:
resolution: {integrity: sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==}
engines: {node: '>=0.10'}
@@ -10945,6 +11286,11 @@ packages:
resolution: {integrity: sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==}
dev: false
/entities@4.5.0:
resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==}
engines: {node: '>=0.12'}
dev: false
/env-paths@2.2.1:
resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==}
engines: {node: '>=6'}
@@ -16314,6 +16660,12 @@ packages:
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
dev: true
/linkify-it@5.0.0:
resolution: {integrity: sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==}
dependencies:
uc.micro: 2.0.0
dev: false
/load-json-file@1.1.0:
resolution: {integrity: sha512-cy7ZdNRXdablkXYNI049pthVeXFurRyb9+hA/dZzerZ0pGTx42z+y+ssxBaVV2l70t1muq5IdKhn4UtcoGUY9A==}
engines: {node: '>=0.10.0'}
@@ -16722,6 +17074,18 @@ packages:
object-visit: 1.0.1
dev: false
/markdown-it@14.0.0:
resolution: {integrity: sha512-seFjF0FIcPt4P9U39Bq1JYblX0KZCjDLFFQPHpL5AzHpqPEKtosxmdq/LTVZnjfH7tjt9BxStm+wXcDBNuYmzw==}
hasBin: true
dependencies:
argparse: 2.0.1
entities: 4.5.0
linkify-it: 5.0.0
mdurl: 2.0.0
punycode.js: 2.3.1
uc.micro: 2.0.0
dev: false
/match-sorter@4.2.1:
resolution: {integrity: sha512-s+3h9TiZU9U1pWhIERHf8/f4LmBN6IXaRgo2CI17+XGByGS1GvG5VvXK9pcGyCjGe3WM3mSYRC3ipGrd5UEVgw==}
dependencies:
@@ -16785,6 +17149,10 @@ packages:
resolution: {integrity: sha512-iV3XNKw06j5Q7mi6h+9vbx23Tv7JkjEVgKHW4pimwyDGWm0OIQntJJ+u1C6mg6mK1EaTv42XQ7w76yuzH7M2cA==}
dev: false
/mdurl@2.0.0:
resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==}
dev: false
/media-typer@0.3.0:
resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==}
engines: {node: '>= 0.6'}
@@ -18069,6 +18437,13 @@ packages:
make-iterator: 1.0.1
dev: false
/object.omit@3.0.0:
resolution: {integrity: sha512-EO+BCv6LJfu+gBIF3ggLicFebFLN5zqzz/WWJlMFfkMyGth+oBkhxzDl0wx2W4GkLzuQs/FsSkXZb2IMWQqmBQ==}
engines: {node: '>=0.10.0'}
dependencies:
is-extendable: 1.0.1
dev: false
/object.pick@1.3.0:
resolution: {integrity: sha512-tqa/UMy/CCoYmj+H5qc07qvSL9dqcs/WZENZ1JbtWBlATP+iVOe778gE6MSijnyCnORzDuX6hU+LA4SZ09YjFQ==}
engines: {node: '>=0.10.0'}
@@ -18245,6 +18620,10 @@ packages:
readable-stream: 2.3.7
dev: false
/orderedmap@2.1.1:
resolution: {integrity: sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g==}
dev: false
/os-browserify@0.3.0:
resolution: {integrity: sha512-gjcpUc3clBf9+210TRaDWbf+rZZZEshZ+DlXMRCeAjp0xhTrnQsKHypIy1J3d5hKdUzj69t708EHtU8P6bUn0A==}
dev: true
@@ -19825,6 +20204,149 @@ packages:
resolution: {integrity: sha512-IJUkICM5dP5znhCckHSv30Q4b5/JA5enCtkRHYaOVOAocnH/1BQEYTC5NMfT3AVl/iXKdr3aqQbQn9DxyWknwA==}
dev: false
/prosemirror-changeset@2.2.1:
resolution: {integrity: sha512-J7msc6wbxB4ekDFj+n9gTW/jav/p53kdlivvuppHsrZXCaQdVgRghoZbSS3kwrRyAstRVQ4/+u5k7YfLgkkQvQ==}
dependencies:
prosemirror-transform: 1.8.0
dev: false
/prosemirror-collab@1.3.1:
resolution: {integrity: sha512-4SnynYR9TTYaQVXd/ieUvsVV4PDMBzrq2xPUWutHivDuOshZXqQ5rGbZM84HEaXKbLdItse7weMGOUdDVcLKEQ==}
dependencies:
prosemirror-state: 1.4.3
dev: false
/prosemirror-commands@1.5.2:
resolution: {integrity: sha512-hgLcPaakxH8tu6YvVAaILV2tXYsW3rAdDR8WNkeKGcgeMVQg3/TMhPdVoh7iAmfgVjZGtcOSjKiQaoeKjzd2mQ==}
dependencies:
prosemirror-model: 1.19.4
prosemirror-state: 1.4.3
prosemirror-transform: 1.8.0
dev: false
/prosemirror-dropcursor@1.8.1:
resolution: {integrity: sha512-M30WJdJZLyXHi3N8vxN6Zh5O8ZBbQCz0gURTfPmTIBNQ5pxrdU7A58QkNqfa98YEjSAL1HUyyU34f6Pm5xBSGw==}
dependencies:
prosemirror-state: 1.4.3
prosemirror-transform: 1.8.0
prosemirror-view: 1.32.7
dev: false
/prosemirror-gapcursor@1.3.2:
resolution: {integrity: sha512-wtjswVBd2vaQRrnYZaBCbyDqr232Ed4p2QPtRIUK5FuqHYKGWkEwl08oQM4Tw7DOR0FsasARV5uJFvMZWxdNxQ==}
dependencies:
prosemirror-keymap: 1.2.2
prosemirror-model: 1.19.4
prosemirror-state: 1.4.3
prosemirror-view: 1.32.7
dev: false
/prosemirror-history@1.3.2:
resolution: {integrity: sha512-/zm0XoU/N/+u7i5zepjmZAEnpvjDtzoPWW6VmKptcAnPadN/SStsBjMImdCEbb3seiNTpveziPTIrXQbHLtU1g==}
dependencies:
prosemirror-state: 1.4.3
prosemirror-transform: 1.8.0
prosemirror-view: 1.32.7
rope-sequence: 1.3.4
dev: false
/prosemirror-inputrules@1.3.0:
resolution: {integrity: sha512-z1GRP2vhh5CihYMQYsJSa1cOwXb3SYxALXOIfAkX8nZserARtl9LiL+CEl+T+OFIsXc3mJIHKhbsmRzC0HDAXA==}
dependencies:
prosemirror-state: 1.4.3
prosemirror-transform: 1.8.0
dev: false
/prosemirror-keymap@1.2.2:
resolution: {integrity: sha512-EAlXoksqC6Vbocqc0GtzCruZEzYgrn+iiGnNjsJsH4mrnIGex4qbLdWWNza3AW5W36ZRrlBID0eM6bdKH4OStQ==}
dependencies:
prosemirror-state: 1.4.3
w3c-keyname: 2.2.8
dev: false
/prosemirror-markdown@1.12.0:
resolution: {integrity: sha512-6F5HS8Z0HDYiS2VQDZzfZP6A0s/I0gbkJy8NCzzDMtcsz3qrfqyroMMeoSjAmOhDITyon11NbXSzztfKi+frSQ==}
dependencies:
markdown-it: 14.0.0
prosemirror-model: 1.19.4
dev: false
/prosemirror-menu@1.2.4:
resolution: {integrity: sha512-S/bXlc0ODQup6aiBbWVsX/eM+xJgCTAfMq/nLqaO5ID/am4wS0tTCIkzwytmao7ypEtjj39i7YbJjAgO20mIqA==}
dependencies:
crelt: 1.0.6
prosemirror-commands: 1.5.2
prosemirror-history: 1.3.2
prosemirror-state: 1.4.3
dev: false
/prosemirror-model@1.19.4:
resolution: {integrity: sha512-RPmVXxUfOhyFdayHawjuZCxiROsm9L4FCUA6pWI+l7n2yCBsWy9VpdE1hpDHUS8Vad661YLY9AzqfjLhAKQ4iQ==}
dependencies:
orderedmap: 2.1.1
dev: false
/prosemirror-schema-basic@1.2.2:
resolution: {integrity: sha512-/dT4JFEGyO7QnNTe9UaKUhjDXbTNkiWTq/N4VpKaF79bBjSExVV2NXmJpcM7z/gD7mbqNjxbmWW5nf1iNSSGnw==}
dependencies:
prosemirror-model: 1.19.4
dev: false
/prosemirror-schema-list@1.3.0:
resolution: {integrity: sha512-Hz/7gM4skaaYfRPNgr421CU4GSwotmEwBVvJh5ltGiffUJwm7C8GfN/Bc6DR1EKEp5pDKhODmdXXyi9uIsZl5A==}
dependencies:
prosemirror-model: 1.19.4
prosemirror-state: 1.4.3
prosemirror-transform: 1.8.0
dev: false
/prosemirror-state@1.4.3:
resolution: {integrity: sha512-goFKORVbvPuAQaXhpbemJFRKJ2aixr+AZMGiquiqKxaucC6hlpHNZHWgz5R7dS4roHiwq9vDctE//CZ++o0W1Q==}
dependencies:
prosemirror-model: 1.19.4
prosemirror-transform: 1.8.0
prosemirror-view: 1.32.7
dev: false
/prosemirror-tables@1.3.5:
resolution: {integrity: sha512-JSZ2cCNlApu/ObAhdPyotrjBe2cimniniTpz60YXzbL0kZ+47nEYk2LWbfKU2lKpBkUNquta2PjteoNi4YCluQ==}
dependencies:
prosemirror-keymap: 1.2.2
prosemirror-model: 1.19.4
prosemirror-state: 1.4.3
prosemirror-transform: 1.8.0
prosemirror-view: 1.32.7
dev: false
/prosemirror-trailing-node@2.0.7(prosemirror-model@1.19.4)(prosemirror-state@1.4.3)(prosemirror-view@1.32.7):
resolution: {integrity: sha512-8zcZORYj/8WEwsGo6yVCRXFMOfBo0Ub3hCUvmoWIZYfMP26WqENU0mpEP27w7mt8buZWuGrydBewr0tOArPb1Q==}
peerDependencies:
prosemirror-model: ^1.19.0
prosemirror-state: ^1.4.2
prosemirror-view: ^1.31.2
dependencies:
'@remirror/core-constants': 2.0.2
'@remirror/core-helpers': 3.0.0
escape-string-regexp: 4.0.0
prosemirror-model: 1.19.4
prosemirror-state: 1.4.3
prosemirror-view: 1.32.7
dev: false
/prosemirror-transform@1.8.0:
resolution: {integrity: sha512-BaSBsIMv52F1BVVMvOmp1yzD3u65uC3HTzCBQV1WDPqJRQ2LuHKcyfn0jwqodo8sR9vVzMzZyI+Dal5W9E6a9A==}
dependencies:
prosemirror-model: 1.19.4
dev: false
/prosemirror-view@1.32.7:
resolution: {integrity: sha512-pvxiOoD4shW41X5bYDjRQk3DSG4fMqxh36yPMt7VYgU3dWRmqFzWJM/R6zeo1KtC8nyk717ZbQND3CC9VNeptw==}
dependencies:
prosemirror-model: 1.19.4
prosemirror-state: 1.4.3
prosemirror-transform: 1.8.0
dev: false
/proto-list@1.2.4:
resolution: {integrity: sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==}
dev: true
@@ -19979,6 +20501,11 @@ packages:
pump: 2.0.1
dev: false
/punycode.js@2.3.1:
resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==}
engines: {node: '>=6'}
dev: false
/punycode@1.3.2:
resolution: {integrity: sha512-RofWgt/7fL5wP1Y7fxE7/EmTLzQVnB0ycyibJ0OOHIlJqTNzglYFxVwETOcIoJqJmpDXJ9xImDv+Fq34F/d4Dw==}
dev: true
@@ -21500,6 +22027,10 @@ packages:
fsevents: 2.3.2
dev: false
/rope-sequence@1.3.4:
resolution: {integrity: sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ==}
dev: false
/rsvp@4.8.5:
resolution: {integrity: sha512-nfMOlASu9OnRJo1mbEk2cz0D56a1MBNrJ7orjRZQG10XDyuvwksKbuXNp6qa+kbn839HwjwhBzhFmdsaEAfauA==}
engines: {node: 6.* || >= 7.*}
@@ -23119,6 +23650,11 @@ packages:
engines: {node: '>=8'}
dev: false
/throttle-debounce@3.0.1:
resolution: {integrity: sha512-dTEWWNu6JmeVXY0ZYoPuH5cRIwc0MeGbJwah9KUNYSJwommQpCzTySTpEe8Gs1J23aeWEuAobe4Ag7EHVt/LOg==}
engines: {node: '>=10'}
dev: false
/through2-filter@3.0.0:
resolution: {integrity: sha512-jaRjI2WxN3W1V8/FMZ9HKIBXixtiqs3SQSX4/YGIiP3gL6djW48VoZq9tDqeCWs3MT8YY5wb/zli8VW8snY1CA==}
dependencies:
@@ -23185,6 +23721,12 @@ packages:
resolution: {integrity: sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==}
dev: false
/tippy.js@6.3.7:
resolution: {integrity: sha512-E1d3oP2emgJ9dRQZdf3Kkn0qJgI6ZLpyS5z6ZkY1DF3kaQaBsGZsndEpHwx+eC+tYM41HaSNvNtLx8tU57FzTQ==}
dependencies:
'@popperjs/core': 2.11.8
dev: false
/tmp-promise@3.0.3:
resolution: {integrity: sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ==}
dependencies:
@@ -23569,6 +24111,11 @@ packages:
resolution: {integrity: sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==}
engines: {node: '>=8'}
/type-fest@2.19.0:
resolution: {integrity: sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==}
engines: {node: '>=12.20'}
dev: false
/type-is@1.6.15:
resolution: {integrity: sha512-0uqZYZDiBICTVXEsNcDLueZLPgZ8FgGe8lmVDQ0FcVFUeaxsPbFWiz60ZChVw8VELIt7iGuCehOrZSYjYteWKQ==}
engines: {node: '>= 0.6'}
@@ -23661,6 +24208,10 @@ packages:
engines: {node: '>=4.2.0'}
hasBin: true
/uc.micro@2.0.0:
resolution: {integrity: sha512-DffL94LsNOccVn4hyfRe5rdKa273swqeA5DJpMOeFmEn1wCDc7nAbbB0gXlgBCL7TNzeTv6G7XVWzan7iJtfig==}
dev: false
/uglify-js@3.17.4:
resolution: {integrity: sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g==}
engines: {node: '>=0.8.0'}
@@ -24125,6 +24676,10 @@ packages:
browser-process-hrtime: 1.0.0
dev: false
/w3c-keyname@2.2.8:
resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==}
dev: false
/w3c-xmlserializer@1.1.2:
resolution: {integrity: sha512-p10l/ayESzrBMYWRID6xbuCKh2Fp77+sA0doRuGn4tTIMrrZVeqfpKjXHY+oDh3K4nLdPgNwMTVP6Vp4pvqbNg==}
dependencies: