mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-23 16:19:49 +00:00
Merge pull request #723 from bigcapitalhq/invoice-mail-receipt
feat: wip Invoice mail receipt preview
This commit is contained in:
@@ -23,6 +23,7 @@
|
|||||||
"@aws-sdk/client-s3": "^3.576.0",
|
"@aws-sdk/client-s3": "^3.576.0",
|
||||||
"@aws-sdk/s3-request-presigner": "^3.583.0",
|
"@aws-sdk/s3-request-presigner": "^3.583.0",
|
||||||
"@bigcapital/utils": "*",
|
"@bigcapital/utils": "*",
|
||||||
|
"@bigcapital/email-components": "*",
|
||||||
"@casl/ability": "^5.4.3",
|
"@casl/ability": "^5.4.3",
|
||||||
"@hapi/boom": "^7.4.3",
|
"@hapi/boom": "^7.4.3",
|
||||||
"@lemonsqueezy/lemonsqueezy.js": "^2.2.0",
|
"@lemonsqueezy/lemonsqueezy.js": "^2.2.0",
|
||||||
|
|||||||
@@ -179,10 +179,21 @@ export default class SaleInvoicesController extends BaseController {
|
|||||||
'/:id/mail',
|
'/:id/mail',
|
||||||
[
|
[
|
||||||
...this.specificSaleInvoiceValidation,
|
...this.specificSaleInvoiceValidation,
|
||||||
body('subject').isString().optional(),
|
|
||||||
|
body('subject').isString().optional({ nullable: true }),
|
||||||
|
body('message').isString().optional({ nullable: true }),
|
||||||
|
|
||||||
body('from').isString().optional(),
|
body('from').isString().optional(),
|
||||||
body('to').isString().optional(),
|
|
||||||
body('body').isString().optional(),
|
body('to').isArray().exists(),
|
||||||
|
body('to.*').isString().isEmail().optional(),
|
||||||
|
|
||||||
|
body('cc').isArray().optional({ nullable: true }),
|
||||||
|
body('cc.*').isString().isEmail().optional(),
|
||||||
|
|
||||||
|
body('bcc').isArray().optional({ nullable: true }),
|
||||||
|
body('bcc.*').isString().isEmail().optional(),
|
||||||
|
|
||||||
body('attach_invoice').optional().isBoolean().toBoolean(),
|
body('attach_invoice').optional().isBoolean().toBoolean(),
|
||||||
],
|
],
|
||||||
this.validationResult,
|
this.validationResult,
|
||||||
@@ -190,7 +201,7 @@ export default class SaleInvoicesController extends BaseController {
|
|||||||
this.handleServiceErrors
|
this.handleServiceErrors
|
||||||
);
|
);
|
||||||
router.get(
|
router.get(
|
||||||
'/:id/mail',
|
'/:id/mail/state',
|
||||||
[...this.specificSaleInvoiceValidation],
|
[...this.specificSaleInvoiceValidation],
|
||||||
this.validationResult,
|
this.validationResult,
|
||||||
asyncMiddleware(this.getSaleInvoiceMail.bind(this)),
|
asyncMiddleware(this.getSaleInvoiceMail.bind(this)),
|
||||||
@@ -778,7 +789,7 @@ export default class SaleInvoicesController extends BaseController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves the default mail options of the given sale invoice.
|
* Retrieves the mail state of the given sale invoice.
|
||||||
* @param {Request} req
|
* @param {Request} req
|
||||||
* @param {Response} res
|
* @param {Response} res
|
||||||
* @param {NextFunction} next
|
* @param {NextFunction} next
|
||||||
@@ -792,7 +803,7 @@ export default class SaleInvoicesController extends BaseController {
|
|||||||
const { id: invoiceId } = req.params;
|
const { id: invoiceId } = req.params;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const data = await this.saleInvoiceApplication.getSaleInvoiceMail(
|
const data = await this.saleInvoiceApplication.getSaleInvoiceMailState(
|
||||||
tenantId,
|
tenantId,
|
||||||
invoiceId
|
invoiceId
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -4,6 +4,9 @@ export const SALE_INVOICE_DELETED = 'Sale invoice deleted';
|
|||||||
export const SALE_INVOICE_MAIL_DELIVERED = 'Sale invoice mail delivered';
|
export const SALE_INVOICE_MAIL_DELIVERED = 'Sale invoice mail delivered';
|
||||||
export const SALE_INVOICE_VIEWED = 'Sale invoice viewed';
|
export const SALE_INVOICE_VIEWED = 'Sale invoice viewed';
|
||||||
export const SALE_INVOICE_PDF_VIEWED = 'Sale invoice PDF viewed';
|
export const SALE_INVOICE_PDF_VIEWED = 'Sale invoice PDF viewed';
|
||||||
|
export const SALE_INVOICE_MAIL_SENT = 'Sale invoice mail sent';
|
||||||
|
export const SALE_INVOICE_MAIL_REMINDER_SENT =
|
||||||
|
'Sale invoice reminder mail sent';
|
||||||
|
|
||||||
export const SALE_ESTIMATE_CREATED = 'Sale estimate created';
|
export const SALE_ESTIMATE_CREATED = 'Sale estimate created';
|
||||||
export const SALE_ESTIMATE_EDITED = 'Sale estimate edited';
|
export const SALE_ESTIMATE_EDITED = 'Sale estimate edited';
|
||||||
|
|||||||
@@ -30,18 +30,14 @@ export interface AddressItem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface CommonMailOptions {
|
export interface CommonMailOptions {
|
||||||
toAddresses: AddressItem[];
|
from: Array<string>;
|
||||||
fromAddresses: AddressItem[];
|
|
||||||
from: string;
|
|
||||||
to: string | string[];
|
|
||||||
subject: string;
|
subject: string;
|
||||||
body: string;
|
message: string;
|
||||||
|
to: Array<string>;
|
||||||
|
cc?: Array<string>;
|
||||||
|
bcc?: Array<string>;
|
||||||
data?: Record<string, any>;
|
data?: Record<string, any>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CommonMailOptionsDTO {
|
export interface CommonMailOptionsDTO extends Partial<CommonMailOptions> {
|
||||||
to?: string | string[];
|
|
||||||
from?: string;
|
|
||||||
subject?: string;
|
|
||||||
body?: string;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -234,7 +234,32 @@ export enum SaleInvoiceAction {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface SaleInvoiceMailOptions extends CommonMailOptions {
|
export interface SaleInvoiceMailOptions extends CommonMailOptions {
|
||||||
attachInvoice: boolean;
|
attachInvoice?: boolean;
|
||||||
|
formatArgs?: Record<string, any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SaleInvoiceMailState extends SaleInvoiceMailOptions {
|
||||||
|
invoiceNo: string;
|
||||||
|
|
||||||
|
invoiceDate: string;
|
||||||
|
invoiceDateFormatted: string;
|
||||||
|
|
||||||
|
dueDate: string;
|
||||||
|
dueDateFormatted: string;
|
||||||
|
|
||||||
|
total: number;
|
||||||
|
totalFormatted: string;
|
||||||
|
|
||||||
|
subtotal: number;
|
||||||
|
subtotalFormatted: number;
|
||||||
|
|
||||||
|
companyName: string;
|
||||||
|
companyLogoUri: string;
|
||||||
|
|
||||||
|
customerName: string;
|
||||||
|
|
||||||
|
// # Invoice entries
|
||||||
|
entries?: Array<{ label: string; total: string; quantity: string | number }>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SendInvoiceMailDTO extends CommonMailOptionsDTO {
|
export interface SendInvoiceMailDTO extends CommonMailOptionsDTO {
|
||||||
@@ -251,6 +276,7 @@ export interface ISaleInvoiceMailSend {
|
|||||||
tenantId: number;
|
tenantId: number;
|
||||||
saleInvoiceId: number;
|
saleInvoiceId: number;
|
||||||
messageOptions: SendInvoiceMailDTO;
|
messageOptions: SendInvoiceMailDTO;
|
||||||
|
formattedMessageOptions: SaleInvoiceMailOptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ISaleInvoiceMailSent {
|
export interface ISaleInvoiceMailSent {
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
SALE_INVOICE_CREATED,
|
SALE_INVOICE_CREATED,
|
||||||
SALE_INVOICE_DELETED,
|
SALE_INVOICE_DELETED,
|
||||||
SALE_INVOICE_EDITED,
|
SALE_INVOICE_EDITED,
|
||||||
|
SALE_INVOICE_MAIL_SENT,
|
||||||
SALE_INVOICE_PDF_VIEWED,
|
SALE_INVOICE_PDF_VIEWED,
|
||||||
SALE_INVOICE_VIEWED,
|
SALE_INVOICE_VIEWED,
|
||||||
} from '@/constants/event-tracker';
|
} from '@/constants/event-tracker';
|
||||||
@@ -43,6 +44,10 @@ export class SaleInvoiceEventsTracker extends EventSubscriber {
|
|||||||
events.saleInvoice.onPdfViewed,
|
events.saleInvoice.onPdfViewed,
|
||||||
this.handleTrackPdfViewedInvoiceEvent
|
this.handleTrackPdfViewedInvoiceEvent
|
||||||
);
|
);
|
||||||
|
bus.subscribe(
|
||||||
|
events.saleInvoice.onMailSent,
|
||||||
|
this.handleTrackMailSentInvoiceEvent
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private handleTrackInvoiceCreatedEvent = ({
|
private handleTrackInvoiceCreatedEvent = ({
|
||||||
@@ -90,4 +95,12 @@ export class SaleInvoiceEventsTracker extends EventSubscriber {
|
|||||||
properties: {},
|
properties: {},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private handleTrackMailSentInvoiceEvent = ({ tenantId }) => {
|
||||||
|
this.posthog.trackEvent({
|
||||||
|
distinctId: `tenant-${tenantId}`,
|
||||||
|
event: SALE_INVOICE_MAIL_SENT,
|
||||||
|
properties: {},
|
||||||
|
});
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import HasTenancyService from '@/services/Tenancy/TenancyService';
|
|||||||
import { MailTenancy } from '@/services/MailTenancy/MailTenancy';
|
import { MailTenancy } from '@/services/MailTenancy/MailTenancy';
|
||||||
import { formatSmsMessage } from '@/utils';
|
import { formatSmsMessage } from '@/utils';
|
||||||
import { Tenant } from '@/system/models';
|
import { Tenant } from '@/system/models';
|
||||||
|
import { castArray } from 'lodash';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class ContactMailNotification {
|
export class ContactMailNotification {
|
||||||
@@ -14,76 +15,54 @@ export class ContactMailNotification {
|
|||||||
private tenancy: HasTenancyService;
|
private tenancy: HasTenancyService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parses the default message options.
|
* Gets the default mail address of the given contact.
|
||||||
* @param {number} tenantId -
|
* @param {number} tenantId - Tenant id.
|
||||||
* @param {number} invoiceId -
|
* @param {number} invoiceId - Contact id.
|
||||||
* @param {string} subject -
|
* @returns {Promise<Pick<CommonMailOptions, 'to' | 'from'>>}
|
||||||
* @param {string} body -
|
|
||||||
* @returns {Promise<SaleInvoiceMailOptions>}
|
|
||||||
*/
|
*/
|
||||||
public async getDefaultMailOptions(
|
public async getDefaultMailOptions(
|
||||||
tenantId: number,
|
tenantId: number,
|
||||||
contactId: number,
|
customerId: number
|
||||||
subject: string = '',
|
): Promise<Pick<CommonMailOptions, 'to' | 'from'>> {
|
||||||
body: string = ''
|
|
||||||
): Promise<CommonMailOptions> {
|
|
||||||
const { Customer } = this.tenancy.models(tenantId);
|
const { Customer } = this.tenancy.models(tenantId);
|
||||||
const contact = await Customer.query()
|
const customer = await Customer.query()
|
||||||
.findById(contactId)
|
.findById(customerId)
|
||||||
.throwIfNotFound();
|
.throwIfNotFound();
|
||||||
|
|
||||||
const toAddresses = contact.contactAddresses;
|
const toAddresses = customer.contactAddresses;
|
||||||
const fromAddresses = await this.mailTenancy.senders(tenantId);
|
const fromAddresses = await this.mailTenancy.senders(tenantId);
|
||||||
|
|
||||||
const toAddress = toAddresses.find((a) => a.primary);
|
const toAddress = toAddresses.find((a) => a.primary);
|
||||||
const fromAddress = fromAddresses.find((a) => a.primary);
|
const fromAddress = fromAddresses.find((a) => a.primary);
|
||||||
|
|
||||||
const to = toAddress?.mail || '';
|
const to = toAddress?.mail ? castArray(toAddress?.mail) : [];
|
||||||
const from = fromAddress?.mail || '';
|
const from = fromAddress?.mail ? castArray(fromAddress?.mail) : [];
|
||||||
|
|
||||||
return {
|
return { to, from };
|
||||||
subject,
|
|
||||||
body,
|
|
||||||
to,
|
|
||||||
from,
|
|
||||||
fromAddresses,
|
|
||||||
toAddresses,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves the mail options of the given contact.
|
* Retrieves the mail options of the given contact.
|
||||||
* @param {number} tenantId - Tenant id.
|
* @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>}
|
* @returns {Promise<CommonMailOptions>}
|
||||||
*/
|
*/
|
||||||
public async getMailOptions(
|
public async parseMailOptions(
|
||||||
tenantId: number,
|
tenantId: number,
|
||||||
contactId: number,
|
mailOptions: CommonMailOptions,
|
||||||
defaultSubject?: string,
|
formatterArgs?: Record<string, any>
|
||||||
defaultBody?: string,
|
|
||||||
formatterData?: Record<string, any>
|
|
||||||
): Promise<CommonMailOptions> {
|
): Promise<CommonMailOptions> {
|
||||||
const mailOpts = await this.getDefaultMailOptions(
|
|
||||||
tenantId,
|
|
||||||
contactId,
|
|
||||||
defaultSubject,
|
|
||||||
defaultBody
|
|
||||||
);
|
|
||||||
const commonFormatArgs = await this.getCommonFormatArgs(tenantId);
|
const commonFormatArgs = await this.getCommonFormatArgs(tenantId);
|
||||||
const formatArgs = {
|
const formatArgs = {
|
||||||
...commonFormatArgs,
|
...commonFormatArgs,
|
||||||
...formatterData,
|
...formatterArgs,
|
||||||
};
|
};
|
||||||
const subject = formatSmsMessage(mailOpts.subject, formatArgs);
|
const subjectFormatted = formatSmsMessage(mailOptions?.subject, formatArgs);
|
||||||
const body = formatSmsMessage(mailOpts.body, formatArgs);
|
const messageFormatted = formatSmsMessage(mailOptions?.message, formatArgs);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...mailOpts,
|
...mailOptions,
|
||||||
subject,
|
subject: subjectFormatted,
|
||||||
body,
|
message: messageFormatted,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,7 +79,7 @@ export class ContactMailNotification {
|
|||||||
.withGraphFetched('metadata');
|
.withGraphFetched('metadata');
|
||||||
|
|
||||||
return {
|
return {
|
||||||
CompanyName: organization.metadata.name,
|
['Company Name']: organization.metadata.name,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,33 +1,46 @@
|
|||||||
import { isEmpty } from 'lodash';
|
import { castArray, isEmpty } from 'lodash';
|
||||||
import { ServiceError } from '@/exceptions';
|
import { ServiceError } from '@/exceptions';
|
||||||
import { CommonMailOptions, CommonMailOptionsDTO } from '@/interfaces';
|
import { CommonMailOptions } from '@/interfaces';
|
||||||
import { ERRORS } from './constants';
|
import { ERRORS } from './constants';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Merges the mail options with incoming options.
|
* Merges the mail options with incoming options.
|
||||||
* @param {Partial<SaleInvoiceMailOptions>} mailOptions
|
* @param {Partial<SaleInvoiceMailOptions>} mailOptions
|
||||||
* @param {Partial<SendInvoiceMailDTO>} overridedOptions
|
* @param {Partial<SendInvoiceMailDTO>} overridedOptions
|
||||||
* @throws {ServiceError}
|
|
||||||
*/
|
*/
|
||||||
export function parseAndValidateMailOptions(
|
export function parseMailOptions(
|
||||||
mailOptions: Partial<CommonMailOptions>,
|
mailOptions: CommonMailOptions,
|
||||||
overridedOptions: Partial<CommonMailOptionsDTO>
|
overridedOptions: Partial<CommonMailOptions>
|
||||||
) {
|
): CommonMailOptions {
|
||||||
const mergedMessageOptions = {
|
const mergedMessageOptions = {
|
||||||
...mailOptions,
|
...mailOptions,
|
||||||
...overridedOptions,
|
...overridedOptions,
|
||||||
};
|
};
|
||||||
if (isEmpty(mergedMessageOptions.from)) {
|
const parsedMessageOptions = {
|
||||||
|
...mergedMessageOptions,
|
||||||
|
from: mergedMessageOptions?.from
|
||||||
|
? castArray(mergedMessageOptions?.from)
|
||||||
|
: [],
|
||||||
|
to: mergedMessageOptions?.to ? castArray(mergedMessageOptions?.to) : [],
|
||||||
|
cc: mergedMessageOptions?.cc ? castArray(mergedMessageOptions?.cc) : [],
|
||||||
|
bcc: mergedMessageOptions?.bcc ? castArray(mergedMessageOptions?.bcc) : [],
|
||||||
|
};
|
||||||
|
return parsedMessageOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function validateRequiredMailOptions(
|
||||||
|
mailOptions: Partial<CommonMailOptions>
|
||||||
|
) {
|
||||||
|
if (isEmpty(mailOptions.from)) {
|
||||||
throw new ServiceError(ERRORS.MAIL_FROM_NOT_FOUND);
|
throw new ServiceError(ERRORS.MAIL_FROM_NOT_FOUND);
|
||||||
}
|
}
|
||||||
if (isEmpty(mergedMessageOptions.to)) {
|
if (isEmpty(mailOptions.to)) {
|
||||||
throw new ServiceError(ERRORS.MAIL_TO_NOT_FOUND);
|
throw new ServiceError(ERRORS.MAIL_TO_NOT_FOUND);
|
||||||
}
|
}
|
||||||
if (isEmpty(mergedMessageOptions.subject)) {
|
if (isEmpty(mailOptions.subject)) {
|
||||||
throw new ServiceError(ERRORS.MAIL_SUBJECT_NOT_FOUND);
|
throw new ServiceError(ERRORS.MAIL_SUBJECT_NOT_FOUND);
|
||||||
}
|
}
|
||||||
if (isEmpty(mergedMessageOptions.body)) {
|
if (isEmpty(mailOptions.message)) {
|
||||||
throw new ServiceError(ERRORS.MAIL_BODY_NOT_FOUND);
|
throw new ServiceError(ERRORS.MAIL_BODY_NOT_FOUND);
|
||||||
}
|
}
|
||||||
return mergedMessageOptions;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
import { Inject, Service } from 'typedi';
|
||||||
|
import { GetPdfTemplate } from '@/services/PdfTemplate/GetPdfTemplate';
|
||||||
|
import { GetSaleInvoice } from './GetSaleInvoice';
|
||||||
|
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
|
||||||
|
import {
|
||||||
|
InvoicePaymentEmailProps,
|
||||||
|
renderInvoicePaymentEmail,
|
||||||
|
} from '@bigcapital/email-components';
|
||||||
|
import { GetInvoiceMailTemplateAttributesTransformer } from './GetInvoicePaymentMailAttributesTransformer';
|
||||||
|
|
||||||
|
@Service()
|
||||||
|
export class GetInvoicePaymentMail {
|
||||||
|
@Inject()
|
||||||
|
private getSaleInvoiceService: GetSaleInvoice;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
private getBrandingTemplate: GetPdfTemplate;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
private transformer: TransformerInjectable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the mail template attributes of the given invoice.
|
||||||
|
* @param {number} tenantId - Tenant id.
|
||||||
|
* @param {number} invoiceId - Invoice id.
|
||||||
|
*/
|
||||||
|
public async getMailTemplateAttributes(tenantId: number, invoiceId: number) {
|
||||||
|
const invoice = await this.getSaleInvoiceService.getSaleInvoice(
|
||||||
|
tenantId,
|
||||||
|
invoiceId
|
||||||
|
);
|
||||||
|
const brandingTemplate = await this.getBrandingTemplate.getPdfTemplate(
|
||||||
|
tenantId,
|
||||||
|
invoice.pdfTemplateId
|
||||||
|
);
|
||||||
|
const mailTemplateAttributes = await this.transformer.transform(
|
||||||
|
tenantId,
|
||||||
|
invoice,
|
||||||
|
new GetInvoiceMailTemplateAttributesTransformer(),
|
||||||
|
{
|
||||||
|
invoice,
|
||||||
|
brandingTemplate,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return mailTemplateAttributes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the mail template html content.
|
||||||
|
* @param {number} tenantId - Tenant id.
|
||||||
|
* @param {number} invoiceId - Invoice id.
|
||||||
|
*/
|
||||||
|
public async getMailTemplate(
|
||||||
|
tenantId: number,
|
||||||
|
invoiceId: number,
|
||||||
|
overrideAttributes?: Partial<InvoicePaymentEmailProps>
|
||||||
|
): Promise<string> {
|
||||||
|
const attributes = await this.getMailTemplateAttributes(
|
||||||
|
tenantId,
|
||||||
|
invoiceId
|
||||||
|
);
|
||||||
|
const mergedAttributes = { ...attributes, ...overrideAttributes };
|
||||||
|
|
||||||
|
return renderInvoicePaymentEmail(mergedAttributes);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,136 @@
|
|||||||
|
import { Transformer } from '@/lib/Transformer/Transformer';
|
||||||
|
|
||||||
|
export class GetInvoiceMailTemplateAttributesTransformer extends Transformer {
|
||||||
|
/**
|
||||||
|
* Include these attributes to item entry object.
|
||||||
|
* @returns {Array}
|
||||||
|
*/
|
||||||
|
public includeAttributes = (): string[] => {
|
||||||
|
return [
|
||||||
|
'companyLogoUri',
|
||||||
|
'companyName',
|
||||||
|
|
||||||
|
'invoiceAmount',
|
||||||
|
|
||||||
|
'primaryColor',
|
||||||
|
|
||||||
|
'invoiceAmount',
|
||||||
|
'invoiceMessage',
|
||||||
|
|
||||||
|
'dueDate',
|
||||||
|
'dueDateLabel',
|
||||||
|
|
||||||
|
'invoiceNumber',
|
||||||
|
'invoiceNumberLabel',
|
||||||
|
|
||||||
|
'total',
|
||||||
|
'totalLabel',
|
||||||
|
|
||||||
|
'dueAmount',
|
||||||
|
'dueAmountLabel',
|
||||||
|
|
||||||
|
'viewInvoiceButtonLabel',
|
||||||
|
'viewInvoiceButtonUrl',
|
||||||
|
|
||||||
|
'items',
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
public excludeAttributes = (): string[] => {
|
||||||
|
return ['*'];
|
||||||
|
};
|
||||||
|
|
||||||
|
public companyLogoUri(): string {
|
||||||
|
return this.options.brandingTemplate?.attributes?.companyLogoUri;
|
||||||
|
}
|
||||||
|
|
||||||
|
public companyName(): string {
|
||||||
|
return this.context.organization.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public invoiceAmount(): string {
|
||||||
|
return this.options.invoice.totalFormatted;
|
||||||
|
}
|
||||||
|
|
||||||
|
public primaryColor(): string {
|
||||||
|
return this.options?.brandingTemplate?.attributes?.primaryColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
public invoiceMessage(): string {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
public dueDate(): string {
|
||||||
|
return this.options?.invoice?.dueDateFormatted;
|
||||||
|
}
|
||||||
|
|
||||||
|
public dueDateLabel(): string {
|
||||||
|
return 'Due {dueDate}';
|
||||||
|
}
|
||||||
|
|
||||||
|
public invoiceNumber(): string {
|
||||||
|
return this.options?.invoice?.invoiceNo;
|
||||||
|
}
|
||||||
|
|
||||||
|
public invoiceNumberLabel(): string {
|
||||||
|
return 'Invoice # {invoiceNumber}';
|
||||||
|
}
|
||||||
|
|
||||||
|
public total(): string {
|
||||||
|
return this.options.invoice?.totalFormatted;
|
||||||
|
}
|
||||||
|
|
||||||
|
public totalLabel(): string {
|
||||||
|
return 'Total';
|
||||||
|
}
|
||||||
|
|
||||||
|
public dueAmount(): string {
|
||||||
|
return this.options?.invoice.dueAmountFormatted;
|
||||||
|
}
|
||||||
|
|
||||||
|
public dueAmountLabel(): string {
|
||||||
|
return 'Due Amount';
|
||||||
|
}
|
||||||
|
|
||||||
|
public viewInvoiceButtonLabel(): string {
|
||||||
|
return 'View Invoice';
|
||||||
|
}
|
||||||
|
|
||||||
|
public viewInvoiceButtonUrl(): string {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
public items(): Array<any> {
|
||||||
|
return this.item(
|
||||||
|
this.options.invoice?.entries,
|
||||||
|
new GetInvoiceMailTemplateItemAttrsTransformer()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class GetInvoiceMailTemplateItemAttrsTransformer extends Transformer {
|
||||||
|
/**
|
||||||
|
* Include these attributes to item entry object.
|
||||||
|
* @returns {Array}
|
||||||
|
*/
|
||||||
|
public includeAttributes = (): string[] => {
|
||||||
|
return ['quantity', 'label', 'rate'];
|
||||||
|
};
|
||||||
|
|
||||||
|
public excludeAttributes = (): string[] => {
|
||||||
|
return ['*'];
|
||||||
|
};
|
||||||
|
|
||||||
|
public quantity(entry): string {
|
||||||
|
return entry?.quantity;
|
||||||
|
}
|
||||||
|
|
||||||
|
public label(entry): string {
|
||||||
|
console.log(entry);
|
||||||
|
return entry?.item?.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public rate(entry): string {
|
||||||
|
return entry?.rateFormatted;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
import { SaleInvoiceMailOptions, SaleInvoiceMailState } from '@/interfaces';
|
||||||
|
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||||
|
import { Inject } from 'typedi';
|
||||||
|
import { SendSaleInvoiceMailCommon } from './SendInvoiceInvoiceMailCommon';
|
||||||
|
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
|
||||||
|
import { GetSaleInvoiceMailStateTransformer } from './GetSaleInvoiceMailStateTransformer';
|
||||||
|
|
||||||
|
export class GetSaleInvoiceMailState {
|
||||||
|
@Inject()
|
||||||
|
private tenancy: HasTenancyService;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
private invoiceMail: SendSaleInvoiceMailCommon;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
private transformer: TransformerInjectable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the invoice mail state of the given sale invoice.
|
||||||
|
* Invoice mail state includes the mail options, branding attributes and the invoice details.
|
||||||
|
*
|
||||||
|
* @param {number} tenantId
|
||||||
|
* @param {number} saleInvoiceId
|
||||||
|
* @returns {Promise<SaleInvoiceMailState>}
|
||||||
|
*/
|
||||||
|
async getInvoiceMailState(
|
||||||
|
tenantId: number,
|
||||||
|
saleInvoiceId: number
|
||||||
|
): Promise<SaleInvoiceMailState> {
|
||||||
|
const { SaleInvoice } = this.tenancy.models(tenantId);
|
||||||
|
|
||||||
|
const saleInvoice = await SaleInvoice.query()
|
||||||
|
.findById(saleInvoiceId)
|
||||||
|
.withGraphFetched('customer')
|
||||||
|
.withGraphFetched('entries.item')
|
||||||
|
.withGraphFetched('pdfTemplate')
|
||||||
|
.throwIfNotFound();
|
||||||
|
|
||||||
|
const mailOptions = await this.invoiceMail.getInvoiceMailOptions(
|
||||||
|
tenantId,
|
||||||
|
saleInvoiceId
|
||||||
|
);
|
||||||
|
// Transforms the sale invoice mail state.
|
||||||
|
const transformed = await this.transformer.transform(
|
||||||
|
tenantId,
|
||||||
|
saleInvoice,
|
||||||
|
new GetSaleInvoiceMailStateTransformer(),
|
||||||
|
{
|
||||||
|
mailOptions,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return transformed;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
import { Transformer } from '@/lib/Transformer/Transformer';
|
||||||
|
import { SaleInvoiceTransformer } from './SaleInvoiceTransformer';
|
||||||
|
import { ItemEntryTransformer } from './ItemEntryTransformer';
|
||||||
|
|
||||||
|
export class GetSaleInvoiceMailStateTransformer extends SaleInvoiceTransformer {
|
||||||
|
/**
|
||||||
|
* Exclude these attributes from user object.
|
||||||
|
* @returns {Array}
|
||||||
|
*/
|
||||||
|
public excludeAttributes = (): string[] => {
|
||||||
|
return ['*'];
|
||||||
|
};
|
||||||
|
|
||||||
|
public includeAttributes = (): string[] => {
|
||||||
|
return [
|
||||||
|
'invoiceDate',
|
||||||
|
'invoiceDateFormatted',
|
||||||
|
|
||||||
|
'dueDate',
|
||||||
|
'dueDateFormatted',
|
||||||
|
|
||||||
|
'dueAmount',
|
||||||
|
'dueAmountFormatted',
|
||||||
|
|
||||||
|
'total',
|
||||||
|
'totalFormatted',
|
||||||
|
|
||||||
|
'subtotal',
|
||||||
|
'subtotalFormatted',
|
||||||
|
|
||||||
|
'invoiceNo',
|
||||||
|
|
||||||
|
'entries',
|
||||||
|
|
||||||
|
'companyName',
|
||||||
|
'companyLogoUri',
|
||||||
|
|
||||||
|
'primaryColor',
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
protected companyName = () => {
|
||||||
|
return this.context.organization.name;
|
||||||
|
};
|
||||||
|
|
||||||
|
protected companyLogoUri = (invoice) => {
|
||||||
|
return invoice.pdfTemplate?.attributes?.companyLogoUri;
|
||||||
|
};
|
||||||
|
|
||||||
|
protected primaryColor = (invoice) => {
|
||||||
|
return invoice.pdfTemplate?.attributes?.primaryColor;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param invoice
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
protected entries = (invoice) => {
|
||||||
|
return this.item(
|
||||||
|
invoice.entries,
|
||||||
|
new GetSaleInvoiceMailStateEntryTransformer(),
|
||||||
|
{
|
||||||
|
currencyCode: invoice.currencyCode,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Merges the mail options with the invoice object.
|
||||||
|
*/
|
||||||
|
public transform = (object: any) => {
|
||||||
|
return {
|
||||||
|
...this.options.mailOptions,
|
||||||
|
...object,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
class GetSaleInvoiceMailStateEntryTransformer extends ItemEntryTransformer {
|
||||||
|
/**
|
||||||
|
* Exclude these attributes from user object.
|
||||||
|
* @returns {Array}
|
||||||
|
*/
|
||||||
|
public excludeAttributes = (): string[] => {
|
||||||
|
return ['*'];
|
||||||
|
};
|
||||||
|
|
||||||
|
public name = (entry) => {
|
||||||
|
return entry.item.name;
|
||||||
|
};
|
||||||
|
|
||||||
|
public includeAttributes = (): string[] => {
|
||||||
|
return [
|
||||||
|
'name',
|
||||||
|
'quantity',
|
||||||
|
'quantityFormatted',
|
||||||
|
'rate',
|
||||||
|
'rateFormatted',
|
||||||
|
'total',
|
||||||
|
'totalFormatted',
|
||||||
|
];
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -33,7 +33,7 @@ export class SaleInvoicePdf {
|
|||||||
* Retrieve sale invoice pdf content.
|
* Retrieve sale invoice pdf content.
|
||||||
* @param {number} tenantId - Tenant Id.
|
* @param {number} tenantId - Tenant Id.
|
||||||
* @param {ISaleInvoice} saleInvoice -
|
* @param {ISaleInvoice} saleInvoice -
|
||||||
* @returns {Promise<Buffer>}
|
* @returns {Promise<[Buffer, string]>}
|
||||||
*/
|
*/
|
||||||
public async saleInvoicePdf(
|
public async saleInvoicePdf(
|
||||||
tenantId: number,
|
tenantId: number,
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
ISystemUser,
|
ISystemUser,
|
||||||
ITenantUser,
|
ITenantUser,
|
||||||
InvoiceNotificationType,
|
InvoiceNotificationType,
|
||||||
|
SaleInvoiceMailState,
|
||||||
SendInvoiceMailDTO,
|
SendInvoiceMailDTO,
|
||||||
} from '@/interfaces';
|
} from '@/interfaces';
|
||||||
import { Inject, Service } from 'typedi';
|
import { Inject, Service } from 'typedi';
|
||||||
@@ -29,6 +30,8 @@ import { SendInvoiceMailReminder } from './SendSaleInvoiceMailReminder';
|
|||||||
import { SendSaleInvoiceMail } from './SendSaleInvoiceMail';
|
import { SendSaleInvoiceMail } from './SendSaleInvoiceMail';
|
||||||
import { GetSaleInvoiceMailReminder } from './GetSaleInvoiceMailReminder';
|
import { GetSaleInvoiceMailReminder } from './GetSaleInvoiceMailReminder';
|
||||||
import { GetSaleInvoiceState } from './GetSaleInvoiceState';
|
import { GetSaleInvoiceState } from './GetSaleInvoiceState';
|
||||||
|
import { GetSaleInvoiceBrandTemplate } from './GetSaleInvoiceBrandTemplate';
|
||||||
|
import { GetSaleInvoiceMailState } from './GetSaleInvoiceMailState';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class SaleInvoiceApplication {
|
export class SaleInvoiceApplication {
|
||||||
@@ -72,7 +75,7 @@ export class SaleInvoiceApplication {
|
|||||||
private sendSaleInvoiceMailService: SendSaleInvoiceMail;
|
private sendSaleInvoiceMailService: SendSaleInvoiceMail;
|
||||||
|
|
||||||
@Inject()
|
@Inject()
|
||||||
private getSaleInvoiceReminderService: GetSaleInvoiceMailReminder;
|
private getSaleInvoiceMailStateService: GetSaleInvoiceMailState;
|
||||||
|
|
||||||
@Inject()
|
@Inject()
|
||||||
private getSaleInvoiceStateService: GetSaleInvoiceState;
|
private getSaleInvoiceStateService: GetSaleInvoiceState;
|
||||||
@@ -361,10 +364,10 @@ export class SaleInvoiceApplication {
|
|||||||
* Retrieves the default mail options of the given sale invoice.
|
* Retrieves the default mail options of the given sale invoice.
|
||||||
* @param {number} tenantId
|
* @param {number} tenantId
|
||||||
* @param {number} saleInvoiceid
|
* @param {number} saleInvoiceid
|
||||||
* @returns {Promise<SendInvoiceMailDTO>}
|
* @returns {Promise<SaleInvoiceMailState>}
|
||||||
*/
|
*/
|
||||||
public getSaleInvoiceMail(tenantId: number, saleInvoiceid: number) {
|
public getSaleInvoiceMailState(tenantId: number, saleInvoiceid: number) {
|
||||||
return this.sendSaleInvoiceMailService.getMailOption(
|
return this.getSaleInvoiceMailStateService.getInvoiceMailState(
|
||||||
tenantId,
|
tenantId,
|
||||||
saleInvoiceid
|
saleInvoiceid
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
DEFAULT_INVOICE_MAIL_CONTENT,
|
DEFAULT_INVOICE_MAIL_CONTENT,
|
||||||
DEFAULT_INVOICE_MAIL_SUBJECT,
|
DEFAULT_INVOICE_MAIL_SUBJECT,
|
||||||
} from './constants';
|
} from './constants';
|
||||||
|
import { GetInvoicePaymentMail } from './GetInvoicePaymentMail';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class SendSaleInvoiceMailCommon {
|
export class SendSaleInvoiceMailCommon {
|
||||||
@@ -19,6 +20,9 @@ export class SendSaleInvoiceMailCommon {
|
|||||||
@Inject()
|
@Inject()
|
||||||
private contactMailNotification: ContactMailNotification;
|
private contactMailNotification: ContactMailNotification;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
private getInvoicePaymentMail: GetInvoicePaymentMail;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves the mail options.
|
* Retrieves the mail options.
|
||||||
* @param {number} tenantId - Tenant id.
|
* @param {number} tenantId - Tenant id.
|
||||||
@@ -27,11 +31,11 @@ export class SendSaleInvoiceMailCommon {
|
|||||||
* @param {string} defaultBody - Subject body.
|
* @param {string} defaultBody - Subject body.
|
||||||
* @returns {Promise<SaleInvoiceMailOptions>}
|
* @returns {Promise<SaleInvoiceMailOptions>}
|
||||||
*/
|
*/
|
||||||
public async getMailOption(
|
public async getInvoiceMailOptions(
|
||||||
tenantId: number,
|
tenantId: number,
|
||||||
invoiceId: number,
|
invoiceId: number,
|
||||||
defaultSubject: string = DEFAULT_INVOICE_MAIL_SUBJECT,
|
defaultSubject: string = DEFAULT_INVOICE_MAIL_SUBJECT,
|
||||||
defaultBody: string = DEFAULT_INVOICE_MAIL_CONTENT
|
defaultMessage: string = DEFAULT_INVOICE_MAIL_CONTENT
|
||||||
): Promise<SaleInvoiceMailOptions> {
|
): Promise<SaleInvoiceMailOptions> {
|
||||||
const { SaleInvoice } = this.tenancy.models(tenantId);
|
const { SaleInvoice } = this.tenancy.models(tenantId);
|
||||||
|
|
||||||
@@ -39,21 +43,54 @@ export class SendSaleInvoiceMailCommon {
|
|||||||
.findById(invoiceId)
|
.findById(invoiceId)
|
||||||
.throwIfNotFound();
|
.throwIfNotFound();
|
||||||
|
|
||||||
const formatterData = await this.formatText(tenantId, invoiceId);
|
const contactMailDefaultOptions =
|
||||||
|
await this.contactMailNotification.getDefaultMailOptions(
|
||||||
const mailOptions = await this.contactMailNotification.getMailOptions(
|
|
||||||
tenantId,
|
tenantId,
|
||||||
saleInvoice.customerId,
|
saleInvoice.customerId
|
||||||
defaultSubject,
|
|
||||||
defaultBody,
|
|
||||||
formatterData
|
|
||||||
);
|
);
|
||||||
|
const formatArgs = await this.getInvoiceFormatterArgs(tenantId, invoiceId);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...mailOptions,
|
...contactMailDefaultOptions,
|
||||||
|
subject: defaultSubject,
|
||||||
|
message: defaultMessage,
|
||||||
attachInvoice: true,
|
attachInvoice: true,
|
||||||
|
formatArgs,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formats the given invoice mail options.
|
||||||
|
* @param {number} tenantId
|
||||||
|
* @param {number} invoiceId
|
||||||
|
* @param {SaleInvoiceMailOptions} mailOptions
|
||||||
|
* @returns {Promise<SaleInvoiceMailOptions>}
|
||||||
|
*/
|
||||||
|
public async formatInvoiceMailOptions(
|
||||||
|
tenantId: number,
|
||||||
|
invoiceId: number,
|
||||||
|
mailOptions: SaleInvoiceMailOptions
|
||||||
|
): Promise<SaleInvoiceMailOptions> {
|
||||||
|
const formatterArgs = await this.getInvoiceFormatterArgs(
|
||||||
|
tenantId,
|
||||||
|
invoiceId
|
||||||
|
);
|
||||||
|
const parsedOptions = await this.contactMailNotification.parseMailOptions(
|
||||||
|
tenantId,
|
||||||
|
mailOptions,
|
||||||
|
formatterArgs
|
||||||
|
);
|
||||||
|
const message = await this.getInvoicePaymentMail.getMailTemplate(
|
||||||
|
tenantId,
|
||||||
|
invoiceId,
|
||||||
|
{
|
||||||
|
// # Invoice message
|
||||||
|
invoiceMessage: parsedOptions.message,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return { ...parsedOptions, message };
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves the formatted text of the given sale invoice.
|
* Retrieves the formatted text of the given sale invoice.
|
||||||
* @param {number} tenantId - Tenant id.
|
* @param {number} tenantId - Tenant id.
|
||||||
@@ -61,7 +98,7 @@ export class SendSaleInvoiceMailCommon {
|
|||||||
* @param {string} text - The given text.
|
* @param {string} text - The given text.
|
||||||
* @returns {Promise<string>}
|
* @returns {Promise<string>}
|
||||||
*/
|
*/
|
||||||
public formatText = async (
|
public getInvoiceFormatterArgs = async (
|
||||||
tenantId: number,
|
tenantId: number,
|
||||||
invoiceId: number
|
invoiceId: number
|
||||||
): Promise<Record<string, string | number>> => {
|
): Promise<Record<string, string | number>> => {
|
||||||
@@ -69,15 +106,18 @@ export class SendSaleInvoiceMailCommon {
|
|||||||
tenantId,
|
tenantId,
|
||||||
invoiceId
|
invoiceId
|
||||||
);
|
);
|
||||||
|
const commonArgs = await this.contactMailNotification.getCommonFormatArgs(
|
||||||
|
tenantId
|
||||||
|
);
|
||||||
return {
|
return {
|
||||||
CustomerName: invoice.customer.displayName,
|
...commonArgs,
|
||||||
InvoiceNumber: invoice.invoiceNo,
|
['Customer Name']: invoice.customer.displayName,
|
||||||
InvoiceDueAmount: invoice.dueAmountFormatted,
|
['Invoice Number']: invoice.invoiceNo,
|
||||||
InvoiceDueDate: invoice.dueDateFormatted,
|
['Invoice DueAmount']: invoice.dueAmountFormatted,
|
||||||
InvoiceDate: invoice.invoiceDateFormatted,
|
['Invoice DueDate']: invoice.dueDateFormatted,
|
||||||
InvoiceAmount: invoice.totalFormatted,
|
['Invoice Date']: invoice.invoiceDateFormatted,
|
||||||
OverdueDays: invoice.overdueDays,
|
['Invoice Amount']: invoice.totalFormatted,
|
||||||
|
['Overdue Days']: invoice.overdueDays,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,12 +4,11 @@ import { ISaleInvoiceMailSend, SendInvoiceMailDTO } from '@/interfaces';
|
|||||||
import { SaleInvoicePdf } from './SaleInvoicePdf';
|
import { SaleInvoicePdf } from './SaleInvoicePdf';
|
||||||
import { SendSaleInvoiceMailCommon } from './SendInvoiceInvoiceMailCommon';
|
import { SendSaleInvoiceMailCommon } from './SendInvoiceInvoiceMailCommon';
|
||||||
import {
|
import {
|
||||||
DEFAULT_INVOICE_MAIL_CONTENT,
|
parseMailOptions,
|
||||||
DEFAULT_INVOICE_MAIL_SUBJECT,
|
validateRequiredMailOptions,
|
||||||
} from './constants';
|
} from '@/services/MailNotification/utils';
|
||||||
import { parseAndValidateMailOptions } from '@/services/MailNotification/utils';
|
|
||||||
import events from '@/subscribers/events';
|
|
||||||
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
|
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
|
||||||
|
import events from '@/subscribers/events';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class SendSaleInvoiceMail {
|
export class SendSaleInvoiceMail {
|
||||||
@@ -51,21 +50,6 @@ export class SendSaleInvoiceMail {
|
|||||||
} as ISaleInvoiceMailSend);
|
} as ISaleInvoiceMailSend);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 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.
|
* Triggers the mail invoice.
|
||||||
* @param {number} tenantId
|
* @param {number} tenantId
|
||||||
@@ -78,44 +62,58 @@ export class SendSaleInvoiceMail {
|
|||||||
saleInvoiceId: number,
|
saleInvoiceId: number,
|
||||||
messageOptions: SendInvoiceMailDTO
|
messageOptions: SendInvoiceMailDTO
|
||||||
) {
|
) {
|
||||||
const defaultMessageOpts = await this.getMailOption(
|
const defaultMessageOptions = await this.invoiceMail.getInvoiceMailOptions(
|
||||||
tenantId,
|
tenantId,
|
||||||
saleInvoiceId
|
saleInvoiceId
|
||||||
);
|
);
|
||||||
// Merge message opts with default options and validate the incoming options.
|
// Merges message options with default options and parses the options values.
|
||||||
const messageOpts = parseAndValidateMailOptions(
|
const parsedMessageOptions = parseMailOptions(
|
||||||
defaultMessageOpts,
|
defaultMessageOptions,
|
||||||
messageOptions
|
messageOptions
|
||||||
);
|
);
|
||||||
const mail = new Mail()
|
// Validates the required mail options.
|
||||||
.setSubject(messageOpts.subject)
|
validateRequiredMailOptions(parsedMessageOptions);
|
||||||
.setTo(messageOpts.to)
|
|
||||||
.setContent(messageOpts.body);
|
|
||||||
|
|
||||||
if (messageOpts.attachInvoice) {
|
const formattedMessageOptions =
|
||||||
// Retrieves document buffer of the invoice pdf document.
|
await this.invoiceMail.formatInvoiceMailOptions(
|
||||||
const invoicePdfBuffer = await this.invoicePdf.saleInvoicePdf(
|
|
||||||
tenantId,
|
tenantId,
|
||||||
saleInvoiceId
|
saleInvoiceId,
|
||||||
|
parsedMessageOptions
|
||||||
);
|
);
|
||||||
|
const mail = new Mail()
|
||||||
|
.setSubject(formattedMessageOptions.subject)
|
||||||
|
.setTo(formattedMessageOptions.to)
|
||||||
|
.setContent(formattedMessageOptions.message);
|
||||||
|
|
||||||
|
// Attach invoice document.
|
||||||
|
if (formattedMessageOptions.attachInvoice) {
|
||||||
|
// Retrieves document buffer of the invoice pdf document.
|
||||||
|
const [invoicePdfBuffer, invoiceFilename] =
|
||||||
|
await this.invoicePdf.saleInvoicePdf(tenantId, saleInvoiceId);
|
||||||
|
|
||||||
mail.setAttachments([
|
mail.setAttachments([
|
||||||
{ filename: 'invoice.pdf', content: invoicePdfBuffer },
|
{ filename: `${invoiceFilename}.pdf`, content: invoicePdfBuffer },
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
// Triggers the event `onSaleInvoiceSend`.
|
|
||||||
await this.eventPublisher.emitAsync(events.saleInvoice.onMailSend, {
|
const eventPayload = {
|
||||||
tenantId,
|
tenantId,
|
||||||
saleInvoiceId,
|
saleInvoiceId,
|
||||||
messageOptions,
|
messageOptions,
|
||||||
} as ISaleInvoiceMailSend);
|
formattedMessageOptions,
|
||||||
|
} as ISaleInvoiceMailSend;
|
||||||
|
|
||||||
|
// Triggers the event `onSaleInvoiceSend`.
|
||||||
|
await this.eventPublisher.emitAsync(
|
||||||
|
events.saleInvoice.onMailSend,
|
||||||
|
eventPayload
|
||||||
|
);
|
||||||
await mail.send();
|
await mail.send();
|
||||||
|
|
||||||
// Triggers the event `onSaleInvoiceSend`.
|
// Triggers the event `onSaleInvoiceSend`.
|
||||||
await this.eventPublisher.emitAsync(events.saleInvoice.onMailSent, {
|
await this.eventPublisher.emitAsync(
|
||||||
tenantId,
|
events.saleInvoice.onMailSent,
|
||||||
saleInvoiceId,
|
eventPayload
|
||||||
messageOptions,
|
);
|
||||||
} as ISaleInvoiceMailSend);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,19 @@
|
|||||||
import config from '@/config';
|
import config from '@/config';
|
||||||
|
|
||||||
export const DEFAULT_INVOICE_MAIL_SUBJECT =
|
export const DEFAULT_INVOICE_MAIL_SUBJECT =
|
||||||
'Invoice {InvoiceNumber} from {CompanyName}';
|
'Invoice {Invoice Number} from {Company Name}';
|
||||||
export const DEFAULT_INVOICE_MAIL_CONTENT = `
|
export const DEFAULT_INVOICE_MAIL_CONTENT = `
|
||||||
<p>Dear {CustomerName}</p>
|
<p>Dear {Customer Name}</p>
|
||||||
<p>Thank you for your business, You can view or print your invoice from attachements.</p>
|
<p>Thank you for your business, You can view or print your invoice from attachements.</p>
|
||||||
<p>
|
<p>
|
||||||
Invoice <strong>#{InvoiceNumber}</strong><br />
|
Invoice <strong>#{Invoice Number}</strong><br />
|
||||||
Due Date : <strong>{InvoiceDueDate}</strong><br />
|
Due Date : <strong>{Invoice Due Date}</strong><br />
|
||||||
Amount : <strong>{InvoiceAmount}</strong></br />
|
Amount : <strong>{Invoice Amount}</strong></br />
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
<i>Regards</i><br />
|
<i>Regards</i><br />
|
||||||
<i>{CompanyName}</i>
|
<i>{Company Name}</i>
|
||||||
</p>
|
</p>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|||||||
@@ -406,7 +406,7 @@ export const runningAmount = (amount: number) => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const formatSmsMessage = (message, args) => {
|
export const formatSmsMessage = (message: string, args) => {
|
||||||
let formattedMessage = message;
|
let formattedMessage = message;
|
||||||
|
|
||||||
Object.keys(args).forEach((key) => {
|
Object.keys(args).forEach((key) => {
|
||||||
|
|||||||
@@ -37,9 +37,9 @@
|
|||||||
"@types/lodash": "^4.14.172",
|
"@types/lodash": "^4.14.172",
|
||||||
"@types/node": "^14.14.9",
|
"@types/node": "^14.14.9",
|
||||||
"@types/ramda": "^0.28.14",
|
"@types/ramda": "^0.28.14",
|
||||||
"@types/react": "^16.14.28",
|
"@types/react": "18.3.4",
|
||||||
|
"@types/react-dom": "18.3.0",
|
||||||
"@types/react-body-classname": "^1.1.7",
|
"@types/react-body-classname": "^1.1.7",
|
||||||
"@types/react-dom": "^16.9.16",
|
|
||||||
"@types/react-helmet": "^6.1.11",
|
"@types/react-helmet": "^6.1.11",
|
||||||
"@types/react-redux": "^7.1.24",
|
"@types/react-redux": "^7.1.24",
|
||||||
"@types/react-router-dom": "^5.3.3",
|
"@types/react-router-dom": "^5.3.3",
|
||||||
@@ -79,6 +79,7 @@
|
|||||||
"path-browserify": "^1.0.1",
|
"path-browserify": "^1.0.1",
|
||||||
"plaid": "^9.3.0",
|
"plaid": "^9.3.0",
|
||||||
"plaid-threads": "^11.4.3",
|
"plaid-threads": "^11.4.3",
|
||||||
|
"polished": "^4.3.1",
|
||||||
"prop-types": "15.8.1",
|
"prop-types": "15.8.1",
|
||||||
"query-string": "^7.1.1",
|
"query-string": "^7.1.1",
|
||||||
"ramda": "^0.27.1",
|
"ramda": "^0.27.1",
|
||||||
|
|||||||
@@ -45,7 +45,6 @@ import ProjectBillableEntriesFormDialog from '@/containers/Projects/containers/P
|
|||||||
import TaxRateFormDialog from '@/containers/TaxRates/dialogs/TaxRateFormDialog/TaxRateFormDialog';
|
import TaxRateFormDialog from '@/containers/TaxRates/dialogs/TaxRateFormDialog/TaxRateFormDialog';
|
||||||
import { DialogsName } from '@/constants/dialogs';
|
import { DialogsName } from '@/constants/dialogs';
|
||||||
import InvoiceExchangeRateChangeDialog from '@/containers/Sales/Invoices/InvoiceForm/Dialogs/InvoiceExchangeRateChangeDialog';
|
import InvoiceExchangeRateChangeDialog from '@/containers/Sales/Invoices/InvoiceForm/Dialogs/InvoiceExchangeRateChangeDialog';
|
||||||
import InvoiceMailDialog from '@/containers/Sales/Invoices/InvoiceMailDialog/InvoiceMailDialog';
|
|
||||||
import EstimateMailDialog from '@/containers/Sales/Estimates/EstimateMailDialog/EstimateMailDialog';
|
import EstimateMailDialog from '@/containers/Sales/Estimates/EstimateMailDialog/EstimateMailDialog';
|
||||||
import ReceiptMailDialog from '@/containers/Sales/Receipts/ReceiptMailDialog/ReceiptMailDialog';
|
import ReceiptMailDialog from '@/containers/Sales/Receipts/ReceiptMailDialog/ReceiptMailDialog';
|
||||||
import PaymentMailDialog from '@/containers/Sales/PaymentsReceived/PaymentMailDialog/PaymentMailDialog';
|
import PaymentMailDialog from '@/containers/Sales/PaymentsReceived/PaymentMailDialog/PaymentMailDialog';
|
||||||
@@ -144,7 +143,6 @@ export default function DialogsContainer() {
|
|||||||
<InvoiceExchangeRateChangeDialog
|
<InvoiceExchangeRateChangeDialog
|
||||||
dialogName={DialogsName.InvoiceExchangeRateChangeNotice}
|
dialogName={DialogsName.InvoiceExchangeRateChangeNotice}
|
||||||
/>
|
/>
|
||||||
<InvoiceMailDialog dialogName={DialogsName.InvoiceMail} />
|
|
||||||
<EstimateMailDialog dialogName={DialogsName.EstimateMail} />
|
<EstimateMailDialog dialogName={DialogsName.EstimateMail} />
|
||||||
<ReceiptMailDialog dialogName={DialogsName.ReceiptMail} />
|
<ReceiptMailDialog dialogName={DialogsName.ReceiptMail} />
|
||||||
<PaymentMailDialog dialogName={DialogsName.PaymentMail} />
|
<PaymentMailDialog dialogName={DialogsName.PaymentMail} />
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ import { PaymentReceivedCustomizeDrawer } from '@/containers/Sales/PaymentsRecei
|
|||||||
import { BrandingTemplatesDrawer } from '@/containers/BrandingTemplates/BrandingTemplatesDrawer';
|
import { BrandingTemplatesDrawer } from '@/containers/BrandingTemplates/BrandingTemplatesDrawer';
|
||||||
|
|
||||||
import { DRAWERS } from '@/constants/drawers';
|
import { DRAWERS } from '@/constants/drawers';
|
||||||
|
import { InvoiceSendMailDrawer } from '@/containers/Sales/Invoices/InvoiceSendMailDrawer/InvoiceSendMailDrawer';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Drawers container of the dashboard.
|
* Drawers container of the dashboard.
|
||||||
@@ -79,6 +80,7 @@ export default function DrawersContainer() {
|
|||||||
name={DRAWERS.PAYMENT_RECEIVED_CUSTOMIZE}
|
name={DRAWERS.PAYMENT_RECEIVED_CUSTOMIZE}
|
||||||
/>
|
/>
|
||||||
<BrandingTemplatesDrawer name={DRAWERS.BRANDING_TEMPLATES} />
|
<BrandingTemplatesDrawer name={DRAWERS.BRANDING_TEMPLATES} />
|
||||||
|
<InvoiceSendMailDrawer name={DRAWERS.INVOICE_SEND_MAIL} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,5 +33,6 @@ export enum DRAWERS {
|
|||||||
PAYMENT_RECEIVED_CUSTOMIZE = 'PAYMENT_RECEIVED_CUSTOMIZE',
|
PAYMENT_RECEIVED_CUSTOMIZE = 'PAYMENT_RECEIVED_CUSTOMIZE',
|
||||||
BRANDING_TEMPLATES = 'BRANDING_TEMPLATES',
|
BRANDING_TEMPLATES = 'BRANDING_TEMPLATES',
|
||||||
PAYMENT_INVOICE_PREVIEW = 'PAYMENT_INVOICE_PREVIEW',
|
PAYMENT_INVOICE_PREVIEW = 'PAYMENT_INVOICE_PREVIEW',
|
||||||
STRIPE_PAYMENT_INTEGRATION_EDIT = 'STRIPE_PAYMENT_INTEGRATION_EDIT'
|
STRIPE_PAYMENT_INTEGRATION_EDIT = 'STRIPE_PAYMENT_INTEGRATION_EDIT',
|
||||||
|
INVOICE_SEND_MAIL = 'INVOICE_SEND_MAIL'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,8 +23,6 @@ function BrandingTemplatesDrawerRoot({
|
|||||||
isOpen={isOpen}
|
isOpen={isOpen}
|
||||||
name={name}
|
name={name}
|
||||||
payload={payload}
|
payload={payload}
|
||||||
size={'600px'}
|
|
||||||
style={{ borderLeftColor: '#cbcbcb' }}
|
|
||||||
>
|
>
|
||||||
<DrawerSuspense>
|
<DrawerSuspense>
|
||||||
<BrandingTemplatesContent />
|
<BrandingTemplatesContent />
|
||||||
|
|||||||
@@ -37,8 +37,10 @@ export function ElementCustomizeFieldsMain() {
|
|||||||
<Stack spacing={0} className={styles.mainFields}>
|
<Stack spacing={0} className={styles.mainFields}>
|
||||||
<ElementCustomizeHeader label={'Customize'} />
|
<ElementCustomizeHeader label={'Customize'} />
|
||||||
|
|
||||||
<Stack spacing={0} style={{ flex: '1 1 auto', overflow: 'auto' }}>
|
<Stack spacing={0} flex="1 1 auto" overflow="auto">
|
||||||
<Box style={{ flex: '1 1' }}>{CustomizeTabPanel}</Box>
|
<Box flex={'1 1'} overflow="auto">
|
||||||
|
{CustomizeTabPanel}
|
||||||
|
</Box>
|
||||||
<ElementCustomizeFooterActions />
|
<ElementCustomizeFooterActions />
|
||||||
</Stack>
|
</Stack>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|||||||
@@ -1,19 +1,12 @@
|
|||||||
import { Box } from '@/components';
|
import { Box, Stack } from '@/components';
|
||||||
import { useElementCustomizeContext } from './ElementCustomizeProvider';
|
import { useElementCustomizeContext } from './ElementCustomizeProvider';
|
||||||
|
|
||||||
export function ElementCustomizePreviewContent() {
|
export function ElementCustomizePreviewContent() {
|
||||||
const { PaperTemplate } = useElementCustomizeContext();
|
const { PaperTemplate } = useElementCustomizeContext();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Stack backgroundColor="#F5F5F5" overflow="auto" flex="1 1 0%" spacing={0}>
|
||||||
style={{
|
|
||||||
padding: '28px 24px 40px',
|
|
||||||
backgroundColor: '#F5F5F5',
|
|
||||||
overflow: 'auto',
|
|
||||||
flex: '1',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{PaperTemplate}
|
{PaperTemplate}
|
||||||
</Box>
|
</Stack>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import { InvoicePaymentPage, PaymentPageProps } from './PaymentPage';
|
||||||
|
|
||||||
|
export interface InvoicePaymentPagePreviewProps
|
||||||
|
extends Partial<PaymentPageProps> { }
|
||||||
|
|
||||||
|
export function InvoicePaymentPagePreview(
|
||||||
|
props: InvoicePaymentPagePreviewProps,
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
<InvoicePaymentPage
|
||||||
|
paidAmount={'$1,000.00'}
|
||||||
|
dueDate={'20 Sep 2024'}
|
||||||
|
total={'$1,000.00'}
|
||||||
|
subtotal={'$1,000.00'}
|
||||||
|
dueAmount={'$1,000.00'}
|
||||||
|
customerName={'Ahmed Bouhuolia'}
|
||||||
|
organizationName={'Bigcapital Technology, Inc.'}
|
||||||
|
invoiceNumber={'INV-000001'}
|
||||||
|
companyLogoUri={' '}
|
||||||
|
organizationAddress={' '}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
260
packages/webapp/src/containers/PaymentPortal/PaymentPage.tsx
Normal file
260
packages/webapp/src/containers/PaymentPortal/PaymentPage.tsx
Normal file
@@ -0,0 +1,260 @@
|
|||||||
|
import { Text, Classes, Button, Intent, ButtonProps } from '@blueprintjs/core';
|
||||||
|
import clsx from 'classnames';
|
||||||
|
import { css } from '@emotion/css';
|
||||||
|
import { lighten } from 'polished';
|
||||||
|
import { Box, Group, Stack } from '@/components';
|
||||||
|
import styles from './PaymentPortal.module.scss';
|
||||||
|
|
||||||
|
export interface PaymentPageProps {
|
||||||
|
// # Company name
|
||||||
|
companyLogoUri: string;
|
||||||
|
organizationName: string;
|
||||||
|
organizationAddress: string;
|
||||||
|
|
||||||
|
// # Colors
|
||||||
|
primaryColor?: string;
|
||||||
|
|
||||||
|
// # Customer name
|
||||||
|
customerName: string;
|
||||||
|
customerAddress?: string;
|
||||||
|
|
||||||
|
// # Subtotal
|
||||||
|
subtotal: string;
|
||||||
|
subtotalLabel?: string;
|
||||||
|
|
||||||
|
// # Total
|
||||||
|
total: string;
|
||||||
|
totalLabel?: string;
|
||||||
|
|
||||||
|
// # Due date
|
||||||
|
dueDate: string;
|
||||||
|
|
||||||
|
// # Paid amount
|
||||||
|
paidAmount: string;
|
||||||
|
paidAmountLabel?: string;
|
||||||
|
|
||||||
|
// # Due amount
|
||||||
|
dueAmount: string;
|
||||||
|
dueAmountLabel?: string;
|
||||||
|
|
||||||
|
// # Download invoice button
|
||||||
|
downloadInvoiceBtnLabel?: string;
|
||||||
|
downloadInvoiceButtonProps?: Partial<ButtonProps>;
|
||||||
|
|
||||||
|
// # View invoice button
|
||||||
|
viewInvoiceLabel?: string;
|
||||||
|
viewInvoiceButtonProps?: Partial<ButtonProps>;
|
||||||
|
|
||||||
|
// # Invoice number
|
||||||
|
invoiceNumber: string;
|
||||||
|
invoiceNumberLabel?: string;
|
||||||
|
|
||||||
|
// # Pay button
|
||||||
|
showPayButton?: boolean;
|
||||||
|
payButtonLabel?: string;
|
||||||
|
payInvoiceButtonProps?: Partial<ButtonProps>;
|
||||||
|
|
||||||
|
// # Buy note
|
||||||
|
buyNote?: string;
|
||||||
|
|
||||||
|
// # Copyright
|
||||||
|
copyrightText?: string;
|
||||||
|
|
||||||
|
classNames?: Record<string, string>
|
||||||
|
}
|
||||||
|
|
||||||
|
export function InvoicePaymentPage({
|
||||||
|
// # Company
|
||||||
|
companyLogoUri,
|
||||||
|
organizationName,
|
||||||
|
organizationAddress,
|
||||||
|
|
||||||
|
// # Colors
|
||||||
|
primaryColor = 'rgb(0, 82, 204)',
|
||||||
|
|
||||||
|
// # Customer
|
||||||
|
customerName,
|
||||||
|
customerAddress,
|
||||||
|
|
||||||
|
// # Subtotal
|
||||||
|
subtotal,
|
||||||
|
subtotalLabel = 'Subtotal',
|
||||||
|
|
||||||
|
// # Total
|
||||||
|
total,
|
||||||
|
totalLabel = 'Total',
|
||||||
|
|
||||||
|
// # Due date
|
||||||
|
dueDate,
|
||||||
|
|
||||||
|
// # Paid amount
|
||||||
|
paidAmount,
|
||||||
|
paidAmountLabel = 'Paid Amount (-)',
|
||||||
|
|
||||||
|
// # Invoice number
|
||||||
|
invoiceNumber,
|
||||||
|
invoiceNumberLabel = 'Invoice #',
|
||||||
|
|
||||||
|
// # Download invoice button
|
||||||
|
downloadInvoiceBtnLabel = 'Download Invoice',
|
||||||
|
downloadInvoiceButtonProps,
|
||||||
|
|
||||||
|
// # View invoice button
|
||||||
|
viewInvoiceLabel = 'View Invoice',
|
||||||
|
viewInvoiceButtonProps,
|
||||||
|
|
||||||
|
// # Due amount
|
||||||
|
dueAmount,
|
||||||
|
dueAmountLabel = 'Due Amount',
|
||||||
|
|
||||||
|
// # Pay button
|
||||||
|
showPayButton = true,
|
||||||
|
payButtonLabel = 'Pay {total}',
|
||||||
|
payInvoiceButtonProps,
|
||||||
|
|
||||||
|
// # Buy note
|
||||||
|
buyNote = 'By confirming your payment, you allow Bigcapital Technology, Inc. to charge you for this payment and save your payment information in accordance with their terms.',
|
||||||
|
|
||||||
|
// # Copyright
|
||||||
|
copyrightText = `© 2024 Bigcapital Technology, Inc. <br /> All rights reserved.`,
|
||||||
|
|
||||||
|
classNames,
|
||||||
|
}: PaymentPageProps) {
|
||||||
|
return (
|
||||||
|
<Box className={clsx(styles.root, classNames?.root)}>
|
||||||
|
<Stack spacing={0} className={styles.body}>
|
||||||
|
<Stack>
|
||||||
|
<Group spacing={10}>
|
||||||
|
{companyLogoUri && (
|
||||||
|
<Box
|
||||||
|
className={styles.companyLogoWrap}
|
||||||
|
style={{
|
||||||
|
backgroundImage: `url(${companyLogoUri})`,
|
||||||
|
}}
|
||||||
|
></Box>
|
||||||
|
)}
|
||||||
|
<Text>{organizationName}</Text>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<Stack spacing={6}>
|
||||||
|
<h1 className={clsx(styles.bigTitle, classNames?.bigTitle)}>
|
||||||
|
{organizationName} Sent an Invoice for {total}
|
||||||
|
</h1>
|
||||||
|
<Group spacing={10}>
|
||||||
|
<Text className={clsx(Classes.TEXT_MUTED, styles.invoiceDueDate)}>
|
||||||
|
Invoice due {dueDate}{' '}
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
<Stack className={styles.address} spacing={2}>
|
||||||
|
<Box className={styles.customerName}>{customerName}</Box>
|
||||||
|
|
||||||
|
{customerAddress && (
|
||||||
|
<Box dangerouslySetInnerHTML={{ __html: customerAddress }} />
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
<h2 className={styles.invoiceNumber}>
|
||||||
|
{invoiceNumberLabel} {invoiceNumber}
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<Stack spacing={0} className={styles.totals}>
|
||||||
|
<Group
|
||||||
|
position={'apart'}
|
||||||
|
className={clsx(styles.totalItem, styles.borderBottomGray)}
|
||||||
|
>
|
||||||
|
<Text>{subtotalLabel}</Text>
|
||||||
|
<Text>{subtotal}</Text>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<Group position={'apart'} className={styles.totalItem}>
|
||||||
|
<Text>{totalLabel}</Text>
|
||||||
|
<Text style={{ fontWeight: 500 }}>{total}</Text>
|
||||||
|
</Group>
|
||||||
|
{/*
|
||||||
|
{sharableLinkMeta?.taxes?.map((tax, key) => (
|
||||||
|
<Group key={key} position={'apart'} className={styles.totalItem}>
|
||||||
|
<Text>{tax?.name}</Text>
|
||||||
|
<Text>{tax?.taxRateAmountFormatted}</Text>
|
||||||
|
</Group>
|
||||||
|
))} */}
|
||||||
|
<Group
|
||||||
|
position={'apart'}
|
||||||
|
className={clsx(styles.totalItem, styles.borderBottomGray)}
|
||||||
|
>
|
||||||
|
<Text>{paidAmountLabel}</Text>
|
||||||
|
<Text>{paidAmount}</Text>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<Group
|
||||||
|
position={'apart'}
|
||||||
|
className={clsx(styles.totalItem, styles.borderBottomDark)}
|
||||||
|
>
|
||||||
|
<Text>{dueAmountLabel}</Text>
|
||||||
|
<Text style={{ fontWeight: 500 }}>{dueAmount}</Text>
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
<Stack spacing={8} className={styles.footerButtons}>
|
||||||
|
<Button
|
||||||
|
minimal
|
||||||
|
className={clsx(styles.footerButton, styles.downloadInvoiceButton)}
|
||||||
|
{...downloadInvoiceButtonProps}
|
||||||
|
>
|
||||||
|
{downloadInvoiceBtnLabel}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
className={clsx(styles.footerButton, styles.viewInvoiceButton)}
|
||||||
|
{...viewInvoiceButtonProps}
|
||||||
|
>
|
||||||
|
{viewInvoiceLabel}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{showPayButton && (
|
||||||
|
<Button
|
||||||
|
intent={Intent.PRIMARY}
|
||||||
|
className={clsx(
|
||||||
|
styles.footerButton,
|
||||||
|
styles.buyButton,
|
||||||
|
css`
|
||||||
|
&.bp4-intent-primary {
|
||||||
|
background-color: ${primaryColor};
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&:focus {
|
||||||
|
background-color: ${lighten(0.1, primaryColor)};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
)}
|
||||||
|
{...payInvoiceButtonProps}
|
||||||
|
>
|
||||||
|
{payButtonLabel.replace('{total}', total)}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
{buyNote && (
|
||||||
|
<Text className={clsx(Classes.TEXT_MUTED, styles.buyNote)}>
|
||||||
|
{buyNote}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
<Stack spacing={18} className={styles.footer}>
|
||||||
|
<Box dangerouslySetInnerHTML={{ __html: organizationAddress }}></Box>
|
||||||
|
|
||||||
|
{copyrightText && (
|
||||||
|
<Stack
|
||||||
|
spacing={0}
|
||||||
|
className={styles.footerText}
|
||||||
|
dangerouslySetInnerHTML={{ __html: copyrightText }}
|
||||||
|
></Stack>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -7,7 +7,7 @@
|
|||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.1);
|
box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.1);
|
||||||
width: 600px;
|
width: 600px;
|
||||||
margin: 40px auto;
|
// margin: 40px auto;
|
||||||
color: #222;
|
color: #222;
|
||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
}
|
}
|
||||||
@@ -28,6 +28,7 @@
|
|||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
color: #222;
|
color: #222;
|
||||||
font-size: 26px;
|
font-size: 26px;
|
||||||
|
line-height: 1.35;
|
||||||
}
|
}
|
||||||
|
|
||||||
.invoiceDueDate{
|
.invoiceDueDate{
|
||||||
|
|||||||
@@ -29,7 +29,13 @@ export const useStripeIntegrationEditBoot = () => {
|
|||||||
return context;
|
return context;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const StripeIntegrationEditBoot: React.FC = ({ children }) => {
|
interface StripeIntegrationEditBootProps {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const StripeIntegrationEditBoot: React.FC<
|
||||||
|
StripeIntegrationEditBootProps
|
||||||
|
> = ({ children }) => {
|
||||||
const {
|
const {
|
||||||
payload: { stripePaymentMethodId },
|
payload: { stripePaymentMethodId },
|
||||||
} = useDrawerContext();
|
} = useDrawerContext();
|
||||||
|
|||||||
@@ -19,7 +19,12 @@ function CreditNoteCustomizeDrawerRoot({
|
|||||||
payload,
|
payload,
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<Drawer isOpen={isOpen} name={name} payload={payload} size={'100%'}>
|
<Drawer
|
||||||
|
isOpen={isOpen}
|
||||||
|
name={name}
|
||||||
|
payload={payload}
|
||||||
|
size={'calc(100% - 10px)'}
|
||||||
|
>
|
||||||
<DrawerSuspense>
|
<DrawerSuspense>
|
||||||
<CreditNoteCustomizeDrawerBody />
|
<CreditNoteCustomizeDrawerBody />
|
||||||
</DrawerSuspense>
|
</DrawerSuspense>
|
||||||
|
|||||||
@@ -20,7 +20,12 @@ function EstimateCustomizeDrawerRoot({
|
|||||||
payload,
|
payload,
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<Drawer isOpen={isOpen} name={name} payload={payload} size={'100%'}>
|
<Drawer
|
||||||
|
isOpen={isOpen}
|
||||||
|
name={name}
|
||||||
|
payload={payload}
|
||||||
|
size={'calc(100% - 10px)'}
|
||||||
|
>
|
||||||
<DrawerSuspense>
|
<DrawerSuspense>
|
||||||
<EstimateCustomizeDrawerBody />
|
<EstimateCustomizeDrawerBody />
|
||||||
</DrawerSuspense>
|
</DrawerSuspense>
|
||||||
|
|||||||
@@ -1,10 +1,5 @@
|
|||||||
import React from 'react';
|
import { lazy, Suspense } from 'react';
|
||||||
import * as R from 'ramda';
|
import { Spinner, Tab } from '@blueprintjs/core';
|
||||||
import { useFormikContext } from 'formik';
|
|
||||||
import {
|
|
||||||
InvoicePaperTemplate,
|
|
||||||
InvoicePaperTemplateProps,
|
|
||||||
} from './InvoicePaperTemplate';
|
|
||||||
import {
|
import {
|
||||||
ElementCustomize,
|
ElementCustomize,
|
||||||
ElementCustomizeContent,
|
ElementCustomizeContent,
|
||||||
@@ -16,9 +11,27 @@ import { InvoiceCustomizeSchema } from './InvoiceCustomizeForm.schema';
|
|||||||
import { useDrawerContext } from '@/components/Drawer/DrawerProvider';
|
import { useDrawerContext } from '@/components/Drawer/DrawerProvider';
|
||||||
import { useDrawerActions } from '@/hooks/state';
|
import { useDrawerActions } from '@/hooks/state';
|
||||||
import { BrandingTemplateForm } from '@/containers/BrandingTemplates/BrandingTemplateForm';
|
import { BrandingTemplateForm } from '@/containers/BrandingTemplates/BrandingTemplateForm';
|
||||||
import { useElementCustomizeContext } from '@/containers/ElementCustomize/ElementCustomizeProvider';
|
|
||||||
import { initialValues } from './constants';
|
import { initialValues } from './constants';
|
||||||
import { useIsTemplateNamedFilled } from '@/containers/BrandingTemplates/utils';
|
import { useIsTemplateNamedFilled } from '@/containers/BrandingTemplates/utils';
|
||||||
|
import { InvoiceCustomizeTabs } from './InvoiceCustomizeTabs';
|
||||||
|
|
||||||
|
const InvoiceCustomizePaymentPreview = lazy(() =>
|
||||||
|
import('./InvoiceCustomizePaymentPreview').then((module) => ({
|
||||||
|
default: module.InvoiceCustomizePaymentPreview,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
|
||||||
|
const InvoiceCustomizeMailReceiptPreview = lazy(() =>
|
||||||
|
import('./InvoiceCustomizeMailReceiptPreview').then((module) => ({
|
||||||
|
default: module.InvoiceCustomizeMailReceiptPreview,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
|
||||||
|
const InvoiceCustomizePdfPreview = lazy(() =>
|
||||||
|
import('./InvoiceCustomizePdfPreview').then((module) => ({
|
||||||
|
default: module.InvoiceCustomizePdfPreview,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Invoice branding template customize.
|
* Invoice branding template customize.
|
||||||
@@ -56,7 +69,39 @@ function InvoiceCustomizeFormContent() {
|
|||||||
return (
|
return (
|
||||||
<ElementCustomizeContent>
|
<ElementCustomizeContent>
|
||||||
<ElementCustomize.PaperTemplate>
|
<ElementCustomize.PaperTemplate>
|
||||||
<InvoicePaperTemplateFormConnected />
|
<InvoiceCustomizeTabs
|
||||||
|
defaultSelectedTabId={'pdf-document'}
|
||||||
|
id={'customize-preview-tabs'}
|
||||||
|
renderActiveTabPanelOnly
|
||||||
|
>
|
||||||
|
<Tab
|
||||||
|
id="pdf-document"
|
||||||
|
title={'PDF document'}
|
||||||
|
panel={
|
||||||
|
<Suspense fallback={<Spinner />}>
|
||||||
|
<InvoiceCustomizePdfPreview />
|
||||||
|
</Suspense>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Tab
|
||||||
|
id={'payment-page'}
|
||||||
|
title={'Payment page'}
|
||||||
|
panel={
|
||||||
|
<Suspense fallback={<Spinner />}>
|
||||||
|
<InvoiceCustomizePaymentPreview />
|
||||||
|
</Suspense>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Tab
|
||||||
|
id={'email-receipt'}
|
||||||
|
title={'Email receipt'}
|
||||||
|
panel={
|
||||||
|
<Suspense fallback={<Spinner />}>
|
||||||
|
<InvoiceCustomizeMailReceiptPreview mx={'auto'} />
|
||||||
|
</Suspense>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</InvoiceCustomizeTabs>
|
||||||
</ElementCustomize.PaperTemplate>
|
</ElementCustomize.PaperTemplate>
|
||||||
|
|
||||||
<ElementCustomize.FieldsTab id={'general'} label={'General'}>
|
<ElementCustomize.FieldsTab id={'general'} label={'General'}>
|
||||||
@@ -73,28 +118,3 @@ function InvoiceCustomizeFormContent() {
|
|||||||
</ElementCustomizeContent>
|
</ElementCustomizeContent>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Injects the `InvoicePaperTemplate` component props from the form and branding states.
|
|
||||||
* @param Component
|
|
||||||
* @returns {JSX.Element}
|
|
||||||
*/
|
|
||||||
const withInvoicePreviewTemplateProps = <P extends object>(
|
|
||||||
Component: React.ComponentType<P>,
|
|
||||||
) => {
|
|
||||||
return (props: Omit<P, keyof InvoicePaperTemplateProps>) => {
|
|
||||||
const { values } = useFormikContext<InvoiceCustomizeFormValues>();
|
|
||||||
const { brandingState } = useElementCustomizeContext();
|
|
||||||
|
|
||||||
const mergedProps: InvoicePaperTemplateProps = {
|
|
||||||
...brandingState,
|
|
||||||
...values,
|
|
||||||
};
|
|
||||||
|
|
||||||
return <Component {...(props as P)} {...mergedProps} />;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const InvoicePaperTemplateFormConnected = R.compose(
|
|
||||||
withInvoicePreviewTemplateProps,
|
|
||||||
)(InvoicePaperTemplate);
|
|
||||||
|
|||||||
@@ -17,7 +17,12 @@ function InvoiceCustomizeDrawerRoot({
|
|||||||
payload,
|
payload,
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<Drawer isOpen={isOpen} name={name} size={'100%'} payload={payload}>
|
<Drawer
|
||||||
|
isOpen={isOpen}
|
||||||
|
name={name}
|
||||||
|
payload={payload}
|
||||||
|
size={'calc(100% - 10px)'}
|
||||||
|
>
|
||||||
<DrawerSuspense>
|
<DrawerSuspense>
|
||||||
<InvoiceCustomize />
|
<InvoiceCustomize />
|
||||||
</DrawerSuspense>
|
</DrawerSuspense>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
// @ts-nocheck
|
// @ts-nocheck
|
||||||
import { Classes, Text } from '@blueprintjs/core';
|
import { Classes, Text } from '@blueprintjs/core';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
import {
|
import {
|
||||||
FFormGroup,
|
FFormGroup,
|
||||||
FieldRequiredHint,
|
FieldRequiredHint,
|
||||||
@@ -13,7 +14,6 @@ import { CreditCardIcon } from '@/icons/CreditCardIcon';
|
|||||||
import { Overlay } from './Overlay';
|
import { Overlay } from './Overlay';
|
||||||
import { useIsTemplateNamedFilled } from '@/containers/BrandingTemplates/utils';
|
import { useIsTemplateNamedFilled } from '@/containers/BrandingTemplates/utils';
|
||||||
import { BrandingCompanyLogoUploadField } from '@/containers/ElementCustomize/components/BrandingCompanyLogoUploadField';
|
import { BrandingCompanyLogoUploadField } from '@/containers/ElementCustomize/components/BrandingCompanyLogoUploadField';
|
||||||
import { Link } from 'react-router-dom';
|
|
||||||
import { MANAGE_LINK_URL } from './constants';
|
import { MANAGE_LINK_URL } from './constants';
|
||||||
import { useDrawerContext } from '@/components/Drawer/DrawerProvider';
|
import { useDrawerContext } from '@/components/Drawer/DrawerProvider';
|
||||||
import { useDrawerActions } from '@/hooks/state';
|
import { useDrawerActions } from '@/hooks/state';
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import * as R from 'ramda';
|
||||||
|
import { useFormikContext } from 'formik';
|
||||||
|
import { InvoicePaymentPagePreviewProps } from '@/containers/PaymentPortal/InvoicePaymentPagePreview';
|
||||||
|
import { InvoiceCustomizeFormValues } from './types';
|
||||||
|
import { useElementCustomizeContext } from '@/containers/ElementCustomize/ElementCustomizeProvider';
|
||||||
|
import { InvoiceMailReceiptPreview } from './InvoiceMailReceiptPreview';
|
||||||
|
import { Box } from '@/components';
|
||||||
|
|
||||||
|
const withInvoiceMailReceiptPreviewConnected = <P extends Object>(
|
||||||
|
Component: React.ComponentType<P>,
|
||||||
|
) => {
|
||||||
|
return (props: Omit<P, keyof InvoicePaymentPagePreviewProps>) => {
|
||||||
|
const { values } = useFormikContext<InvoiceCustomizeFormValues>();
|
||||||
|
const { brandingState } = useElementCustomizeContext();
|
||||||
|
|
||||||
|
const mergedBrandingState = {
|
||||||
|
...brandingState,
|
||||||
|
...values,
|
||||||
|
};
|
||||||
|
const mergedProps: InvoicePaymentPagePreviewProps = {
|
||||||
|
companyLogoUri: mergedBrandingState?.companyLogoUri,
|
||||||
|
primaryColor: mergedBrandingState?.primaryColor,
|
||||||
|
// organizationAddress: mergedBrandingState,
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<Box px={4} pt={8} pb={16}>
|
||||||
|
<Component {...(props as P)} {...mergedProps} />
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const InvoiceCustomizeMailReceiptPreview = R.compose(
|
||||||
|
withInvoiceMailReceiptPreviewConnected,
|
||||||
|
)(InvoiceMailReceiptPreview);
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import * as R from 'ramda';
|
||||||
|
import { useFormikContext } from 'formik';
|
||||||
|
import { css } from '@emotion/css';
|
||||||
|
import {
|
||||||
|
InvoicePaymentPagePreview,
|
||||||
|
InvoicePaymentPagePreviewProps,
|
||||||
|
} from '@/containers/PaymentPortal/InvoicePaymentPagePreview';
|
||||||
|
import { useElementCustomizeContext } from '@/containers/ElementCustomize/ElementCustomizeProvider';
|
||||||
|
import { InvoiceCustomizeFormValues } from './types';
|
||||||
|
import { Box } from '@/components';
|
||||||
|
|
||||||
|
const withInvoicePaymentPreviewPageProps = <P extends Object>(
|
||||||
|
Component: React.ComponentType<P>,
|
||||||
|
) => {
|
||||||
|
return (props: Omit<P, keyof InvoicePaymentPagePreviewProps>) => {
|
||||||
|
const { values } = useFormikContext<InvoiceCustomizeFormValues>();
|
||||||
|
const { brandingState } = useElementCustomizeContext();
|
||||||
|
|
||||||
|
const mergedBrandingState = {
|
||||||
|
...brandingState,
|
||||||
|
...values,
|
||||||
|
};
|
||||||
|
const mergedProps: InvoicePaymentPagePreviewProps = {
|
||||||
|
companyLogoUri: mergedBrandingState?.companyLogoUri,
|
||||||
|
primaryColor: mergedBrandingState?.primaryColor,
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<Box px={4} pt={8} pb={16}>
|
||||||
|
<Component
|
||||||
|
{...(props as P)}
|
||||||
|
{...mergedProps}
|
||||||
|
classNames={{
|
||||||
|
root: css`
|
||||||
|
margin: 0 auto;
|
||||||
|
border-radius: 5px !important;
|
||||||
|
transform: scale(0.9);
|
||||||
|
transform-origin: top;
|
||||||
|
boxshadow: 0 10px 15px rgba(0, 0, 0, 0.05) !important;
|
||||||
|
`,
|
||||||
|
bigTitle: css`
|
||||||
|
color: #333 !important;
|
||||||
|
`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const InvoiceCustomizePaymentPreview = R.compose(
|
||||||
|
withInvoicePaymentPreviewPageProps,
|
||||||
|
)(InvoicePaymentPagePreview);
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import * as R from 'ramda';
|
||||||
|
import { useFormikContext } from 'formik';
|
||||||
|
import {
|
||||||
|
InvoicePaperTemplate,
|
||||||
|
InvoicePaperTemplateProps,
|
||||||
|
} from './InvoicePaperTemplate';
|
||||||
|
import { useElementCustomizeContext } from '@/containers/ElementCustomize/ElementCustomizeProvider';
|
||||||
|
import { InvoiceCustomizeFormValues } from './types';
|
||||||
|
import { Box } from '@/components';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Injects the `InvoicePaperTemplate` component props from the form and branding states.
|
||||||
|
* @param {React.ComponentType<P>} Component
|
||||||
|
* @returns {JSX.Element}
|
||||||
|
*/
|
||||||
|
const withInvoicePreviewTemplateProps = <P extends object>(
|
||||||
|
Component: React.ComponentType<P>,
|
||||||
|
) => {
|
||||||
|
return (props: Omit<P, keyof InvoicePaperTemplateProps>) => {
|
||||||
|
const { values } = useFormikContext<InvoiceCustomizeFormValues>();
|
||||||
|
const { brandingState } = useElementCustomizeContext();
|
||||||
|
|
||||||
|
const mergedProps: InvoicePaperTemplateProps = {
|
||||||
|
...brandingState,
|
||||||
|
...values,
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<Box px={4} py={6}>
|
||||||
|
<Component {...(props as P)} {...mergedProps} />
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const InvoiceCustomizePdfPreview = R.compose(
|
||||||
|
withInvoicePreviewTemplateProps,
|
||||||
|
)(InvoicePaperTemplate);
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import { css } from '@emotion/css';
|
||||||
|
import { Tabs, TabsProps } from '@blueprintjs/core';
|
||||||
|
|
||||||
|
interface InvoiceCustomizeTabsProps extends TabsProps {}
|
||||||
|
|
||||||
|
export function InvoiceCustomizeTabs(props: InvoiceCustomizeTabsProps) {
|
||||||
|
return (
|
||||||
|
<Tabs
|
||||||
|
className={css`
|
||||||
|
overflow: hidden;
|
||||||
|
flex: 1 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
.bp4-tab-list {
|
||||||
|
padding: 0 20px;
|
||||||
|
background: #fff;
|
||||||
|
border-bottom: 1px solid #dcdcdd;
|
||||||
|
}
|
||||||
|
.bp4-tab {
|
||||||
|
line-height: 40px;
|
||||||
|
}
|
||||||
|
.bp4-tab:not([aria-selected='true']) {
|
||||||
|
color: #5f6b7c;
|
||||||
|
}
|
||||||
|
.bp4-tab-indicator-wrapper .bp4-tab-indicator {
|
||||||
|
height: 2px;
|
||||||
|
}
|
||||||
|
.bp4-tab-panel{
|
||||||
|
margin: 0;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,196 @@
|
|||||||
|
import { Button, Intent } from '@blueprintjs/core';
|
||||||
|
import { css } from '@emotion/css';
|
||||||
|
import { x } from '@xstyled/emotion';
|
||||||
|
import { lighten } from 'polished';
|
||||||
|
import { Group, Stack, StackProps } from '@/components';
|
||||||
|
|
||||||
|
export interface InvoiceMailReceiptProps extends StackProps {
|
||||||
|
// # Company
|
||||||
|
companyName: string;
|
||||||
|
companyLogoUri?: string;
|
||||||
|
|
||||||
|
// # Colors
|
||||||
|
primaryColor?: string;
|
||||||
|
|
||||||
|
// # Due date
|
||||||
|
dueDate: string;
|
||||||
|
dueDateLabel?: string;
|
||||||
|
|
||||||
|
// # Due amount
|
||||||
|
dueAmountLabel?: string;
|
||||||
|
dueAmount: string;
|
||||||
|
|
||||||
|
// # Total
|
||||||
|
total: string;
|
||||||
|
totalLabel?: string;
|
||||||
|
|
||||||
|
// # Invoice number
|
||||||
|
invoiceNumber: string;
|
||||||
|
invoiceNumberLabel?: string;
|
||||||
|
|
||||||
|
// # Mail message
|
||||||
|
message: string;
|
||||||
|
|
||||||
|
// # Invoice items
|
||||||
|
items?: Array<{ label: string; total: string; quantity: string | number }>;
|
||||||
|
|
||||||
|
// # View invoice button
|
||||||
|
showViewInvoiceButton?: boolean;
|
||||||
|
viewInvoiceButtonLabel?: string;
|
||||||
|
viewInvoiceButtonOnClick?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function InvoiceMailReceipt({
|
||||||
|
// Company
|
||||||
|
companyName,
|
||||||
|
companyLogoUri,
|
||||||
|
|
||||||
|
// # Colors
|
||||||
|
primaryColor = 'rgb(0, 82, 204)',
|
||||||
|
|
||||||
|
// Due date
|
||||||
|
dueDate,
|
||||||
|
dueDateLabel = 'Due',
|
||||||
|
|
||||||
|
// Due amount
|
||||||
|
dueAmountLabel = 'Due Amount',
|
||||||
|
dueAmount,
|
||||||
|
|
||||||
|
// Total
|
||||||
|
total,
|
||||||
|
totalLabel = 'Total',
|
||||||
|
|
||||||
|
// Invoice number
|
||||||
|
invoiceNumber,
|
||||||
|
invoiceNumberLabel = 'Invoice #',
|
||||||
|
|
||||||
|
// Invoice message
|
||||||
|
message,
|
||||||
|
|
||||||
|
// Invoice items
|
||||||
|
items,
|
||||||
|
|
||||||
|
// View invoice button
|
||||||
|
showViewInvoiceButton = true,
|
||||||
|
viewInvoiceButtonLabel = 'View Invoice',
|
||||||
|
viewInvoiceButtonOnClick,
|
||||||
|
...restProps
|
||||||
|
}: InvoiceMailReceiptProps) {
|
||||||
|
return (
|
||||||
|
<Stack
|
||||||
|
bg="white"
|
||||||
|
w={'100%'}
|
||||||
|
maxWidth={'500px'}
|
||||||
|
p={'35px 25px'}
|
||||||
|
borderRadius={'5px'}
|
||||||
|
boxShadow={'0 10px 15px rgba(0, 0, 0, 0.05)'}
|
||||||
|
color={'black'}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
<Stack spacing={16} textAlign={'center'}>
|
||||||
|
{companyLogoUri && (
|
||||||
|
<x.div
|
||||||
|
h="90px"
|
||||||
|
w="90px"
|
||||||
|
mx="auto"
|
||||||
|
backgroundRepeat="no-repeat"
|
||||||
|
backgroundPosition="center center"
|
||||||
|
backgroundSize="contain"
|
||||||
|
backgroundImage={`url("${companyLogoUri}")`}
|
||||||
|
></x.div>
|
||||||
|
)}
|
||||||
|
<Stack spacing={8}>
|
||||||
|
<x.h1 m={0} fontSize={'18px'} fontWeight={500} color="#404854">
|
||||||
|
{companyName}
|
||||||
|
</x.h1>
|
||||||
|
|
||||||
|
<x.h3 color="#383E47" fontWeight={500}>
|
||||||
|
{total}
|
||||||
|
</x.h3>
|
||||||
|
|
||||||
|
<x.span fontSize={'13px'} color="#404854">
|
||||||
|
{invoiceNumberLabel} {invoiceNumber}
|
||||||
|
</x.span>
|
||||||
|
|
||||||
|
<x.span fontSize={'13px'} color="#404854">
|
||||||
|
{dueDateLabel} {dueDate}
|
||||||
|
</x.span>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
<x.p m={0} whiteSpace={'pre-line'} color="#252A31">
|
||||||
|
{message}
|
||||||
|
</x.p>
|
||||||
|
|
||||||
|
{showViewInvoiceButton && (
|
||||||
|
<Button
|
||||||
|
large
|
||||||
|
intent={Intent.PRIMARY}
|
||||||
|
className={css`
|
||||||
|
&.bp4-intent-primary {
|
||||||
|
background-color: ${primaryColor};
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&:focus {
|
||||||
|
background-color: ${lighten(0.1, primaryColor)};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&.bp4-large {
|
||||||
|
min-height: 38px;
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
onClick={viewInvoiceButtonOnClick}
|
||||||
|
>
|
||||||
|
{viewInvoiceButtonLabel}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Stack spacing={0}>
|
||||||
|
{items?.map((item, key) => (
|
||||||
|
<Group
|
||||||
|
key={key}
|
||||||
|
h={'40px'}
|
||||||
|
position={'apart'}
|
||||||
|
borderBottomStyle="solid"
|
||||||
|
borderBottomWidth={'1px'}
|
||||||
|
borderBottomColor={'#D9D9D9'}
|
||||||
|
borderTopStyle="solid"
|
||||||
|
borderTopColor={'#D9D9D9'}
|
||||||
|
borderTopWidth={'1px'}
|
||||||
|
>
|
||||||
|
<x.span>{item.label}</x.span>
|
||||||
|
<x.span>
|
||||||
|
{item.quantity} x {item.total}
|
||||||
|
</x.span>
|
||||||
|
</Group>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<Group
|
||||||
|
h={'40px'}
|
||||||
|
position={'apart'}
|
||||||
|
borderBottomStyle="solid"
|
||||||
|
borderBottomWidth={'1px'}
|
||||||
|
borderColor={'#000'}
|
||||||
|
>
|
||||||
|
<x.span fontWeight={500}>{totalLabel}</x.span>
|
||||||
|
<x.span fontWeight={600} fontSize={15}>
|
||||||
|
{total}
|
||||||
|
</x.span>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<Group
|
||||||
|
h={'40px'}
|
||||||
|
position={'apart'}
|
||||||
|
borderBottomStyle="solid"
|
||||||
|
borderBottomWidth={'1px'}
|
||||||
|
borderBottomColor={'#000'}
|
||||||
|
>
|
||||||
|
<x.span fontWeight={500}>{dueAmountLabel}</x.span>
|
||||||
|
<x.span fontWeight={600} fontSize={15}>
|
||||||
|
{dueAmount}
|
||||||
|
</x.span>
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import {
|
||||||
|
InvoiceMailReceipt,
|
||||||
|
InvoiceMailReceiptProps,
|
||||||
|
} from './InvoiceMailReceipt';
|
||||||
|
|
||||||
|
export interface InvoiceMailReceiptPreviewProps
|
||||||
|
extends Partial<InvoiceMailReceiptProps> {}
|
||||||
|
|
||||||
|
const receiptMessage = `Hi Ahmed,
|
||||||
|
|
||||||
|
Here’s invoice INV-0002 for AED 0.00
|
||||||
|
|
||||||
|
The amount outstanding of AED $100,00 is due on 2 October 2024
|
||||||
|
|
||||||
|
View your bill online From your online you can print a PDF or pay your outstanding bills,
|
||||||
|
|
||||||
|
If you have any questions, please let us know,
|
||||||
|
|
||||||
|
Thanks,
|
||||||
|
Mohamed
|
||||||
|
`;
|
||||||
|
|
||||||
|
export function InvoiceMailReceiptPreview(
|
||||||
|
props: InvoiceMailReceiptPreviewProps,
|
||||||
|
) {
|
||||||
|
const propsWithDefaults = {
|
||||||
|
message: receiptMessage,
|
||||||
|
companyName: 'Bigcapital Technology, Inc.',
|
||||||
|
total: '$1,000.00',
|
||||||
|
invoiceNumber: 'INV-0001',
|
||||||
|
dueDate: '2 Oct 2024',
|
||||||
|
dueAmount: '$1,000.00',
|
||||||
|
items: [{ label: 'Web development', total: '$1000.00', quantity: 1 }],
|
||||||
|
companyLogoUri: ' ',
|
||||||
|
...props,
|
||||||
|
};
|
||||||
|
return <InvoiceMailReceipt {...propsWithDefaults} />;
|
||||||
|
}
|
||||||
@@ -1,5 +1,9 @@
|
|||||||
import { Classes, Text } from '@blueprintjs/core';
|
import { Classes, Text } from '@blueprintjs/core';
|
||||||
import { PaperTemplate, PaperTemplateTotalBorder } from './PaperTemplate';
|
import {
|
||||||
|
PaperTemplate,
|
||||||
|
PaperTemplateProps,
|
||||||
|
PaperTemplateTotalBorder,
|
||||||
|
} from './PaperTemplate';
|
||||||
import { Box, Group, Stack } from '@/components';
|
import { Box, Group, Stack } from '@/components';
|
||||||
import {
|
import {
|
||||||
DefaultPdfTemplateTerms,
|
DefaultPdfTemplateTerms,
|
||||||
@@ -23,7 +27,7 @@ interface PaperTax {
|
|||||||
amount: string;
|
amount: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface InvoicePaperTemplateProps {
|
export interface InvoicePaperTemplateProps extends PaperTemplateProps {
|
||||||
primaryColor?: string;
|
primaryColor?: string;
|
||||||
secondaryColor?: string;
|
secondaryColor?: string;
|
||||||
|
|
||||||
@@ -177,9 +181,14 @@ export function InvoicePaperTemplate({
|
|||||||
statementLabel = 'Statement',
|
statementLabel = 'Statement',
|
||||||
showStatement = true,
|
showStatement = true,
|
||||||
statement = DefaultPdfTemplateStatement,
|
statement = DefaultPdfTemplateStatement,
|
||||||
|
...props
|
||||||
}: InvoicePaperTemplateProps) {
|
}: InvoicePaperTemplateProps) {
|
||||||
return (
|
return (
|
||||||
<PaperTemplate primaryColor={primaryColor} secondaryColor={secondaryColor}>
|
<PaperTemplate
|
||||||
|
primaryColor={primaryColor}
|
||||||
|
secondaryColor={secondaryColor}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
<Stack spacing={24}>
|
<Stack spacing={24}>
|
||||||
<Group align="start" spacing={10}>
|
<Group align="start" spacing={10}>
|
||||||
<Stack flex={1}>
|
<Stack flex={1}>
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import clsx from 'classnames';
|
import clsx from 'classnames';
|
||||||
import { get, isFunction } from 'lodash';
|
import { get, isFunction } from 'lodash';
|
||||||
import { Box, Group, GroupProps } from '@/components';
|
import { Box, BoxProps, Group, GroupProps } from '@/components';
|
||||||
import styles from './InvoicePaperTemplate.module.scss';
|
import styles from './InvoicePaperTemplate.module.scss';
|
||||||
|
|
||||||
export interface PaperTemplateProps {
|
export interface PaperTemplateProps extends BoxProps {
|
||||||
primaryColor?: string;
|
primaryColor?: string;
|
||||||
secondaryColor?: string;
|
secondaryColor?: string;
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
@@ -14,13 +14,13 @@ export function PaperTemplate({
|
|||||||
primaryColor,
|
primaryColor,
|
||||||
secondaryColor,
|
secondaryColor,
|
||||||
children,
|
children,
|
||||||
|
...restProps
|
||||||
}: PaperTemplateProps) {
|
}: PaperTemplateProps) {
|
||||||
return (
|
return (
|
||||||
<div className={styles.root}>
|
<Box {...restProps} className={clsx(styles.root, restProps?.className)}>
|
||||||
<style>{`:root { --invoice-primary-color: ${primaryColor}; --invoice-secondary-color: ${secondaryColor}; }`}</style>
|
<style>{`:root { --invoice-primary-color: ${primaryColor}; --invoice-secondary-color: ${secondaryColor}; }`}</style>
|
||||||
|
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -118,9 +118,9 @@ PaperTemplate.TotalLine = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
PaperTemplate.MutedText = () => { };
|
PaperTemplate.MutedText = () => {};
|
||||||
|
|
||||||
PaperTemplate.Text = () => { };
|
PaperTemplate.Text = () => {};
|
||||||
|
|
||||||
PaperTemplate.AddressesGroup = (props: GroupProps) => {
|
PaperTemplate.AddressesGroup = (props: GroupProps) => {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,39 +0,0 @@
|
|||||||
// @ts-nocheck
|
|
||||||
import React from 'react';
|
|
||||||
import { Dialog, DialogSuspense } from '@/components';
|
|
||||||
import withDialogRedux from '@/components/DialogReduxConnect';
|
|
||||||
import { compose } from '@/utils';
|
|
||||||
|
|
||||||
const InvoiceFormMailDeliverDialogContent = React.lazy(
|
|
||||||
() => import('./InvoiceFormMailDeliverDialogContent'),
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Invoice mail dialog.
|
|
||||||
*/
|
|
||||||
function InvoiceFormMailDeliverDialog({
|
|
||||||
dialogName,
|
|
||||||
payload: { invoiceId = null },
|
|
||||||
isOpen,
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<Dialog
|
|
||||||
name={dialogName}
|
|
||||||
title={'Invoice Mail'}
|
|
||||||
isOpen={isOpen}
|
|
||||||
canEscapeJeyClose={false}
|
|
||||||
isCloseButtonShown={false}
|
|
||||||
autoFocus={true}
|
|
||||||
style={{ width: 600 }}
|
|
||||||
>
|
|
||||||
<DialogSuspense>
|
|
||||||
<InvoiceFormMailDeliverDialogContent
|
|
||||||
dialogName={dialogName}
|
|
||||||
invoiceId={invoiceId}
|
|
||||||
/>
|
|
||||||
</DialogSuspense>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default compose(withDialogRedux())(InvoiceFormMailDeliverDialog);
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
// @ts-nocheck
|
|
||||||
import * as R from 'ramda';
|
|
||||||
import { useHistory } from 'react-router-dom';
|
|
||||||
import InvoiceMailDialogContent from '../../../InvoiceMailDialog/InvoiceMailDialogContent';
|
|
||||||
import withDialogActions from '@/containers/Dialog/withDialogActions';
|
|
||||||
import { DialogsName } from '@/constants/dialogs';
|
|
||||||
|
|
||||||
interface InvoiceFormDeliverDialogContent {
|
|
||||||
invoiceId: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
function InvoiceFormDeliverDialogContentRoot({
|
|
||||||
invoiceId,
|
|
||||||
|
|
||||||
// #withDialogActions
|
|
||||||
closeDialog,
|
|
||||||
}: InvoiceFormDeliverDialogContent) {
|
|
||||||
const history = useHistory();
|
|
||||||
|
|
||||||
const handleSubmit = () => {
|
|
||||||
history.push('/invoices');
|
|
||||||
closeDialog(DialogsName.InvoiceFormMailDeliver);
|
|
||||||
};
|
|
||||||
const handleCancel = () => {
|
|
||||||
history.push('/invoices');
|
|
||||||
closeDialog(DialogsName.InvoiceFormMailDeliver);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<InvoiceMailDialogContent
|
|
||||||
invoiceId={invoiceId}
|
|
||||||
onFormSubmit={handleSubmit}
|
|
||||||
onCancelClick={handleCancel}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default R.compose(withDialogActions)(
|
|
||||||
InvoiceFormDeliverDialogContentRoot,
|
|
||||||
);
|
|
||||||
@@ -2,7 +2,6 @@
|
|||||||
import { useFormikContext } from 'formik';
|
import { useFormikContext } from 'formik';
|
||||||
import InvoiceNumberDialog from '@/containers/Dialogs/InvoiceNumberDialog';
|
import InvoiceNumberDialog from '@/containers/Dialogs/InvoiceNumberDialog';
|
||||||
import { DialogsName } from '@/constants/dialogs';
|
import { DialogsName } from '@/constants/dialogs';
|
||||||
import InvoiceFormMailDeliverDialog from './Dialogs/InvoiceFormMailDeliverDialog/InvoiceFormMailDeliverDialog';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Invoice form dialogs.
|
* Invoice form dialogs.
|
||||||
@@ -28,9 +27,6 @@ export default function InvoiceFormDialogs() {
|
|||||||
dialogName={DialogsName.InvoiceNumberSettings}
|
dialogName={DialogsName.InvoiceNumberSettings}
|
||||||
onConfirm={handleInvoiceNumberFormConfirm}
|
onConfirm={handleInvoiceNumberFormConfirm}
|
||||||
/>
|
/>
|
||||||
<InvoiceFormMailDeliverDialog
|
|
||||||
dialogName={DialogsName.InvoiceFormMailDeliver}
|
|
||||||
/>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,35 +0,0 @@
|
|||||||
// @ts-nocheck
|
|
||||||
import React from 'react';
|
|
||||||
import { Dialog, DialogSuspense } from '@/components';
|
|
||||||
import withDialogRedux from '@/components/DialogReduxConnect';
|
|
||||||
import { compose } from '@/utils';
|
|
||||||
|
|
||||||
const InvoiceMailDialogBody = React.lazy(
|
|
||||||
() => import('./InvoiceMailDialogBody'),
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Invoice mail dialog.
|
|
||||||
*/
|
|
||||||
function InvoiceMailDialog({
|
|
||||||
dialogName,
|
|
||||||
payload: { invoiceId = null },
|
|
||||||
isOpen,
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<Dialog
|
|
||||||
name={dialogName}
|
|
||||||
title={'Invoice Mail'}
|
|
||||||
isOpen={isOpen}
|
|
||||||
canEscapeJeyClose={false}
|
|
||||||
isCloseButtonShown={false}
|
|
||||||
autoFocus={true}
|
|
||||||
style={{ width: 600 }}
|
|
||||||
>
|
|
||||||
<DialogSuspense>
|
|
||||||
<InvoiceMailDialogBody invoiceId={invoiceId} />
|
|
||||||
</DialogSuspense>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
export default compose(withDialogRedux())(InvoiceMailDialog);
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
// @ts-nocheck
|
|
||||||
import * as R from 'ramda';
|
|
||||||
import withDialogActions from '@/containers/Dialog/withDialogActions';
|
|
||||||
import InvoiceMailDialogContent, {
|
|
||||||
InvoiceMailDialogContentProps,
|
|
||||||
} from './InvoiceMailDialogContent';
|
|
||||||
import { DialogsName } from '@/constants/dialogs';
|
|
||||||
|
|
||||||
export interface InvoiceMailDialogBodyProps
|
|
||||||
extends InvoiceMailDialogContentProps {}
|
|
||||||
|
|
||||||
function InvoiceMailDialogBodyRoot({
|
|
||||||
invoiceId,
|
|
||||||
onCancelClick,
|
|
||||||
onFormSubmit,
|
|
||||||
|
|
||||||
// #withDialogActions
|
|
||||||
closeDialog,
|
|
||||||
}: InvoiceMailDialogBodyProps) {
|
|
||||||
const handleCancelClick = () => {
|
|
||||||
closeDialog(DialogsName.InvoiceMail);
|
|
||||||
};
|
|
||||||
const handleSubmitClick = () => {
|
|
||||||
closeDialog(DialogsName.InvoiceMail);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<InvoiceMailDialogContent
|
|
||||||
invoiceId={invoiceId}
|
|
||||||
onCancelClick={handleCancelClick}
|
|
||||||
onFormSubmit={handleSubmitClick}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default R.compose(withDialogActions)(InvoiceMailDialogBodyRoot);
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
// @ts-nocheck
|
|
||||||
import React, { createContext } from 'react';
|
|
||||||
import { useSaleInvoiceDefaultOptions } from '@/hooks/query';
|
|
||||||
import { DialogContent } from '@/components';
|
|
||||||
|
|
||||||
interface InvoiceMailDialogBootValues {
|
|
||||||
invoiceId: number;
|
|
||||||
mailOptions: any;
|
|
||||||
redirectToInvoicesList: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
const InvoiceMailDialagBoot = createContext<InvoiceMailDialogBootValues>();
|
|
||||||
|
|
||||||
interface InvoiceMailDialogBootProps {
|
|
||||||
invoiceId: number;
|
|
||||||
redirectToInvoicesList?: boolean;
|
|
||||||
children: React.ReactNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Invoice mail dialog boot provider.
|
|
||||||
*/
|
|
||||||
function InvoiceMailDialogBoot({
|
|
||||||
invoiceId,
|
|
||||||
redirectToInvoicesList,
|
|
||||||
...props
|
|
||||||
}: InvoiceMailDialogBootProps) {
|
|
||||||
const { data: mailOptions, isLoading: isMailOptionsLoading } =
|
|
||||||
useSaleInvoiceDefaultOptions(invoiceId);
|
|
||||||
|
|
||||||
const provider = {
|
|
||||||
saleInvoiceId: invoiceId,
|
|
||||||
mailOptions,
|
|
||||||
isMailOptionsLoading,
|
|
||||||
redirectToInvoicesList,
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DialogContent isLoading={isMailOptionsLoading}>
|
|
||||||
<InvoiceMailDialagBoot.Provider value={provider} {...props} />
|
|
||||||
</DialogContent>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const useInvoiceMailDialogBoot = () =>
|
|
||||||
React.useContext<InvoiceMailDialogBootValues>(InvoiceMailDialagBoot);
|
|
||||||
|
|
||||||
export { InvoiceMailDialogBoot, useInvoiceMailDialogBoot };
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
import { InvoiceMailDialogBoot } from './InvoiceMailDialogBoot';
|
|
||||||
import { InvoiceMailDialogForm } from './InvoiceMailDialogForm';
|
|
||||||
|
|
||||||
export interface InvoiceMailDialogContentProps {
|
|
||||||
invoiceId: number;
|
|
||||||
onFormSubmit?: () => void;
|
|
||||||
onCancelClick?: () => void;
|
|
||||||
}
|
|
||||||
export default function InvoiceMailDialogContent({
|
|
||||||
invoiceId,
|
|
||||||
onFormSubmit,
|
|
||||||
onCancelClick,
|
|
||||||
}: InvoiceMailDialogContentProps) {
|
|
||||||
return (
|
|
||||||
<InvoiceMailDialogBoot invoiceId={invoiceId}>
|
|
||||||
<InvoiceMailDialogForm
|
|
||||||
onFormSubmit={onFormSubmit}
|
|
||||||
onCancelClick={onCancelClick}
|
|
||||||
/>
|
|
||||||
</InvoiceMailDialogBoot>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
// @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'),
|
|
||||||
});
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
// @ts-nocheck
|
|
||||||
import { Formik } from 'formik';
|
|
||||||
import { Intent } from '@blueprintjs/core';
|
|
||||||
import { useInvoiceMailDialogBoot } from './InvoiceMailDialogBoot';
|
|
||||||
import { AppToaster } from '@/components';
|
|
||||||
import { useSendSaleInvoiceMail } from '@/hooks/query';
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function InvoiceMailDialogForm({ onFormSubmit, onCancelClick }) {
|
|
||||||
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,
|
|
||||||
});
|
|
||||||
setSubmitting(false);
|
|
||||||
onFormSubmit && onFormSubmit(values);
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
AppToaster.show({
|
|
||||||
message: 'Something went wrong.',
|
|
||||||
intent: Intent.DANGER,
|
|
||||||
});
|
|
||||||
setSubmitting(false);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
// Handle the close button click.
|
|
||||||
const handleClose = () => {
|
|
||||||
onCancelClick && onCancelClick();
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Formik
|
|
||||||
initialValues={initialValues}
|
|
||||||
validationSchema={InvoiceMailFormSchema}
|
|
||||||
onSubmit={handleSubmit}
|
|
||||||
>
|
|
||||||
<InvoiceMailDialogFormContent onClose={handleClose} />
|
|
||||||
</Formik>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
// @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;
|
|
||||||
`;
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
export * from './InvoiceMailDialog';
|
|
||||||
export * from './InvoiceMailDialogContent';
|
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import { useMemo } from 'react';
|
||||||
|
import { css } from '@emotion/css';
|
||||||
|
import { Box, } from '@/components';
|
||||||
|
import { InvoiceMailReceiptPreview } from '../InvoiceCustomize/InvoiceMailReceiptPreview';
|
||||||
|
import { useInvoiceSendMailBoot } from './InvoiceSendMailContentBoot';
|
||||||
|
import { InvoiceSendMailPreviewWithHeader } from './InvoiceSendMailHeaderPreview';
|
||||||
|
import { useSendInvoiceMailMessage } from './_hooks';
|
||||||
|
|
||||||
|
export function InvoiceMailReceiptPreviewConneceted() {
|
||||||
|
const mailMessage = useSendInvoiceMailMessage();
|
||||||
|
const { invoiceMailState } = useInvoiceSendMailBoot();
|
||||||
|
|
||||||
|
const items = useMemo(
|
||||||
|
() =>
|
||||||
|
invoiceMailState?.entries?.map((entry: any) => ({
|
||||||
|
quantity: entry.quantity,
|
||||||
|
total: entry.totalFormatted,
|
||||||
|
label: entry.name,
|
||||||
|
})),
|
||||||
|
[invoiceMailState?.entries],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<InvoiceSendMailPreviewWithHeader>
|
||||||
|
<Box px={4} pt={8} pb={16}>
|
||||||
|
<InvoiceMailReceiptPreview
|
||||||
|
companyName={invoiceMailState?.companyName}
|
||||||
|
// companyLogoUri={invoiceMailState?.companyLogoUri}
|
||||||
|
|
||||||
|
primaryColor={invoiceMailState?.primaryColor}
|
||||||
|
total={invoiceMailState?.totalFormatted}
|
||||||
|
dueDate={invoiceMailState?.dueDateFormatted}
|
||||||
|
dueAmount={invoiceMailState?.dueAmountFormatted}
|
||||||
|
|
||||||
|
invoiceNumber={invoiceMailState?.invoiceNo}
|
||||||
|
items={items}
|
||||||
|
message={mailMessage}
|
||||||
|
className={css`
|
||||||
|
margin: 0 auto;
|
||||||
|
`}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</InvoiceSendMailPreviewWithHeader>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import { Group, Stack } from '@/components';
|
||||||
|
import { Classes } from '@blueprintjs/core';
|
||||||
|
import { InvoiceSendMailBoot } from './InvoiceSendMailContentBoot';
|
||||||
|
import { InvoiceSendMailForm } from './InvoiceSendMailForm';
|
||||||
|
import { InvoiceSendMailHeader } from './InvoiceSendMailHeader';
|
||||||
|
import { InvoiceSendMailPreview } from './InvoiceSendMailPreview';
|
||||||
|
import { InvoiceSendMailFields } from './InvoiceSendMailFields';
|
||||||
|
|
||||||
|
export function InvoiceSendMailContent() {
|
||||||
|
return (
|
||||||
|
<Stack className={Classes.DRAWER_BODY}>
|
||||||
|
<InvoiceSendMailBoot>
|
||||||
|
<InvoiceSendMailForm>
|
||||||
|
<Stack spacing={0} flex={1} overflow="hidden">
|
||||||
|
<InvoiceSendMailHeader label={'Send Invoice Mail'} />
|
||||||
|
|
||||||
|
<Group flex={1} overflow="auto" spacing={0} alignItems={'stretch'}>
|
||||||
|
<InvoiceSendMailFields />
|
||||||
|
<InvoiceSendMailPreview />
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
|
</InvoiceSendMailForm>
|
||||||
|
</InvoiceSendMailBoot>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
// @ts-nocheck
|
||||||
|
import React, { createContext, useContext } from 'react';
|
||||||
|
import { Spinner } from '@blueprintjs/core';
|
||||||
|
import {
|
||||||
|
GetSaleInvoiceDefaultOptionsResponse,
|
||||||
|
useSaleInvoiceMailState,
|
||||||
|
} from '@/hooks/query';
|
||||||
|
import { useDrawerContext } from '@/components/Drawer/DrawerProvider';
|
||||||
|
|
||||||
|
interface InvoiceSendMailBootValues {
|
||||||
|
invoiceId: number;
|
||||||
|
|
||||||
|
invoiceMailState: GetSaleInvoiceDefaultOptionsResponse | undefined;
|
||||||
|
isInvoiceMailState: boolean;
|
||||||
|
}
|
||||||
|
interface InvoiceSendMailBootProps {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
const InvoiceSendMailContentBootContext =
|
||||||
|
createContext<InvoiceSendMailBootValues>({} as InvoiceSendMailBootValues);
|
||||||
|
|
||||||
|
export const InvoiceSendMailBoot = ({ children }: InvoiceSendMailBootProps) => {
|
||||||
|
const {
|
||||||
|
payload: { invoiceId },
|
||||||
|
} = useDrawerContext();
|
||||||
|
|
||||||
|
// Invoice mail options.
|
||||||
|
const { data: invoiceMailState, isLoading: isInvoiceMailState } =
|
||||||
|
useSaleInvoiceMailState(invoiceId);
|
||||||
|
|
||||||
|
const isLoading = isInvoiceMailState;
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return <Spinner size={20} />;
|
||||||
|
}
|
||||||
|
const value = {
|
||||||
|
invoiceId,
|
||||||
|
|
||||||
|
// # Invoice mail options
|
||||||
|
isInvoiceMailState,
|
||||||
|
invoiceMailState,
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<InvoiceSendMailContentBootContext.Provider value={value}>
|
||||||
|
{children}
|
||||||
|
</InvoiceSendMailContentBootContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
InvoiceSendMailBoot.displayName = 'InvoiceSendMailBoot';
|
||||||
|
|
||||||
|
export const useInvoiceSendMailBoot = () => {
|
||||||
|
return useContext<InvoiceSendMailBootValues>(
|
||||||
|
InvoiceSendMailContentBootContext,
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import * as R from 'ramda';
|
||||||
|
import { Drawer, DrawerSuspense } from '@/components';
|
||||||
|
import withDrawers from '@/containers/Drawer/withDrawers';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
const InvoiceSendMailContent = React.lazy(() =>
|
||||||
|
import('./InvoiceSendMailContent').then((module) => ({
|
||||||
|
default: module.InvoiceSendMailContent,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
|
||||||
|
interface InvoiceSendMailDrawerProps {
|
||||||
|
name: string;
|
||||||
|
isOpen?: boolean;
|
||||||
|
payload?: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
function InvoiceSendMailDrawerRoot({
|
||||||
|
name,
|
||||||
|
|
||||||
|
// #withDrawer
|
||||||
|
isOpen,
|
||||||
|
payload,
|
||||||
|
}: InvoiceSendMailDrawerProps) {
|
||||||
|
return (
|
||||||
|
<Drawer
|
||||||
|
isOpen={isOpen}
|
||||||
|
name={name}
|
||||||
|
payload={payload}
|
||||||
|
size={'calc(100% - 10px)'}
|
||||||
|
>
|
||||||
|
<DrawerSuspense>
|
||||||
|
<InvoiceSendMailContent />
|
||||||
|
</DrawerSuspense>
|
||||||
|
</Drawer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const InvoiceSendMailDrawer = R.compose(withDrawers())(
|
||||||
|
InvoiceSendMailDrawerRoot,
|
||||||
|
);
|
||||||
@@ -0,0 +1,312 @@
|
|||||||
|
// @ts-nocheck
|
||||||
|
import { Button, Intent, MenuItem, Position } from '@blueprintjs/core';
|
||||||
|
import { useRef, useState, useMemo, useCallback } from 'react';
|
||||||
|
import { useFormikContext } from 'formik';
|
||||||
|
import { SelectOptionProps } from '@blueprintjs-formik/select';
|
||||||
|
import { css } from '@emotion/css';
|
||||||
|
import {
|
||||||
|
FCheckbox,
|
||||||
|
FFormGroup,
|
||||||
|
FInputGroup,
|
||||||
|
FMultiSelect,
|
||||||
|
FSelect,
|
||||||
|
FTextArea,
|
||||||
|
Group,
|
||||||
|
Icon,
|
||||||
|
Stack,
|
||||||
|
} from '@/components';
|
||||||
|
import { useDrawerContext } from '@/components/Drawer/DrawerProvider';
|
||||||
|
import { useDrawerActions } from '@/hooks/state';
|
||||||
|
import { useInvoiceMailItems, useSendInvoiceFormatArgsOptions } from './_hooks';
|
||||||
|
|
||||||
|
// Create new account renderer.
|
||||||
|
const createNewItemRenderer = (query, active, handleClick) => {
|
||||||
|
return (
|
||||||
|
<MenuItem
|
||||||
|
icon="add"
|
||||||
|
text={'Now contact address'}
|
||||||
|
active={active}
|
||||||
|
onClick={handleClick}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create new item from the given query string.
|
||||||
|
const createNewItemFromQuery = (name) => ({ name });
|
||||||
|
|
||||||
|
const styleEmailButton = css`
|
||||||
|
&.bp4-button.bp4-small {
|
||||||
|
width: auto;
|
||||||
|
margin: 0;
|
||||||
|
min-height: 26px;
|
||||||
|
line-height: 26px;
|
||||||
|
padding-top: 0;
|
||||||
|
padding-bottom: 0;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const fieldsWrapStyle = css`
|
||||||
|
> :not(:first-of-type) .bp4-input {
|
||||||
|
border-top-color: transparent;
|
||||||
|
border-top-right-radius: 0;
|
||||||
|
border-top-left-radius: 0;
|
||||||
|
}
|
||||||
|
> :not(:last-of-type) .bp4-input {
|
||||||
|
border-bottom-left-radius: 0;
|
||||||
|
border-bottom-right-radius: 0;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export function InvoiceSendMailFields() {
|
||||||
|
const [showCCField, setShowCCField] = useState<boolean>(false);
|
||||||
|
const [showBccField, setShowBccField] = useState<boolean>(false);
|
||||||
|
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||||
|
|
||||||
|
const { values, setFieldValue } = useFormikContext();
|
||||||
|
const items = useInvoiceMailItems();
|
||||||
|
const argsOptions = useSendInvoiceFormatArgsOptions();
|
||||||
|
|
||||||
|
const handleClickCcBtn = (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
|
||||||
|
setShowCCField(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClickBccBtn = (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
|
||||||
|
setShowBccField(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCreateToItemSelect = (value: SelectOptionProps) => {
|
||||||
|
setFieldValue('to', [...values?.to, value?.name]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCreateCcItemSelect = (value: SelectOptionProps) => {
|
||||||
|
setFieldValue('cc', [...values?.cc, value?.name]);
|
||||||
|
};
|
||||||
|
const handleCreateBccItemSelect = (value: SelectOptionProps) => {
|
||||||
|
setFieldValue('bcc', [...values?.bcc, value?.name]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const rightElementsToField = useMemo(() => (
|
||||||
|
<Group
|
||||||
|
spacing={0}
|
||||||
|
paddingRight={'7px'}
|
||||||
|
paddingTop={'7px'}
|
||||||
|
fontWeight={500}
|
||||||
|
color={'#000'}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
onClick={handleClickCcBtn}
|
||||||
|
minimal
|
||||||
|
small
|
||||||
|
className={styleEmailButton}
|
||||||
|
>
|
||||||
|
CC
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
onClick={handleClickBccBtn}
|
||||||
|
minimal
|
||||||
|
small
|
||||||
|
className={styleEmailButton}
|
||||||
|
>
|
||||||
|
BCC
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
), []);
|
||||||
|
|
||||||
|
const handleTextareaChange = useCallback((item: SelectOptionProps) => {
|
||||||
|
const textarea = textareaRef.current;
|
||||||
|
if (!textarea) return;
|
||||||
|
|
||||||
|
const { selectionStart, selectionEnd, value: text } = textarea;
|
||||||
|
const insertText = `{${item.value}}`;
|
||||||
|
const message =
|
||||||
|
text.substring(0, selectionStart) +
|
||||||
|
insertText +
|
||||||
|
text.substring(selectionEnd);
|
||||||
|
|
||||||
|
setFieldValue('message', message);
|
||||||
|
|
||||||
|
// Move the cursor to the end of the inserted text
|
||||||
|
setTimeout(() => {
|
||||||
|
textarea.selectionStart = textarea.selectionEnd =
|
||||||
|
selectionStart + insertText.length;
|
||||||
|
textarea.focus();
|
||||||
|
}, 0);
|
||||||
|
}, [setFieldValue]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack
|
||||||
|
bg="white"
|
||||||
|
flex={'1'}
|
||||||
|
maxHeight="100%"
|
||||||
|
spacing={0}
|
||||||
|
borderRight="1px solid #dcdcdd"
|
||||||
|
>
|
||||||
|
<Stack spacing={0} overflow="auto" flex="1" p={'30px'}>
|
||||||
|
<FFormGroup label={'To'} name={'to'}>
|
||||||
|
<Stack spacing={0} className={fieldsWrapStyle}>
|
||||||
|
<FMultiSelect
|
||||||
|
items={items}
|
||||||
|
name={'to'}
|
||||||
|
placeholder={'To'}
|
||||||
|
popoverProps={{ minimal: true, fill: true }}
|
||||||
|
tagInputProps={{
|
||||||
|
tagProps: { round: true, minimal: true, large: true },
|
||||||
|
rightElement: rightElementsToField,
|
||||||
|
large: true,
|
||||||
|
}}
|
||||||
|
createNewItemRenderer={createNewItemRenderer}
|
||||||
|
createNewItemFromQuery={createNewItemFromQuery}
|
||||||
|
onCreateItemSelect={handleCreateToItemSelect}
|
||||||
|
resetOnQuery
|
||||||
|
resetOnSelect
|
||||||
|
fill
|
||||||
|
fastField
|
||||||
|
/>
|
||||||
|
{showCCField && (
|
||||||
|
<FMultiSelect
|
||||||
|
items={items}
|
||||||
|
name={'cc'}
|
||||||
|
placeholder={'Cc'}
|
||||||
|
popoverProps={{ minimal: true, fill: true }}
|
||||||
|
tagInputProps={{
|
||||||
|
tagProps: { round: true, minimal: true, large: true },
|
||||||
|
large: true,
|
||||||
|
}}
|
||||||
|
createNewItemRenderer={createNewItemRenderer}
|
||||||
|
createNewItemFromQuery={createNewItemFromQuery}
|
||||||
|
onCreateItemSelect={handleCreateCcItemSelect}
|
||||||
|
resetOnQuery
|
||||||
|
resetOnSelect
|
||||||
|
fill
|
||||||
|
fastField
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{showBccField && (
|
||||||
|
<FMultiSelect
|
||||||
|
items={items}
|
||||||
|
name={'bcc'}
|
||||||
|
placeholder={'Bcc'}
|
||||||
|
popoverProps={{ minimal: true, fill: true }}
|
||||||
|
tagInputProps={{
|
||||||
|
tagProps: { round: true, minimal: true, large: true },
|
||||||
|
large: true,
|
||||||
|
}}
|
||||||
|
createNewItemRenderer={createNewItemRenderer}
|
||||||
|
createNewItemFromQuery={createNewItemFromQuery}
|
||||||
|
onCreateItemSelect={handleCreateBccItemSelect}
|
||||||
|
resetOnQuery
|
||||||
|
resetOnSelect
|
||||||
|
fill
|
||||||
|
fastField
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
</FFormGroup>
|
||||||
|
|
||||||
|
<FFormGroup label={'Submit'} name={'subject'}>
|
||||||
|
<FInputGroup name={'subject'} large fastField />
|
||||||
|
</FFormGroup>
|
||||||
|
|
||||||
|
<FFormGroup label={'Message'} name={'message'}>
|
||||||
|
<Stack spacing={0}>
|
||||||
|
<Group
|
||||||
|
border={'1px solid #ced4da'}
|
||||||
|
borderBottom={0}
|
||||||
|
borderRadius={'3px 3px 0 0'}
|
||||||
|
>
|
||||||
|
<FSelect
|
||||||
|
selectedItem={'customerName'}
|
||||||
|
name={'item'}
|
||||||
|
items={argsOptions}
|
||||||
|
onItemChange={handleTextareaChange}
|
||||||
|
popoverProps={{
|
||||||
|
fill: false,
|
||||||
|
position: Position.BOTTOM_LEFT,
|
||||||
|
minimal: true,
|
||||||
|
}}
|
||||||
|
input={({ activeItem, text, label, value }) => (
|
||||||
|
<Button
|
||||||
|
minimal
|
||||||
|
rightIcon={
|
||||||
|
<Icon icon={'caret-down-16'} color={'#8F99A8'} />
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Insert Variable
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
fill={false}
|
||||||
|
fastField
|
||||||
|
/>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<FTextArea
|
||||||
|
inputRef={textareaRef}
|
||||||
|
name={'message'}
|
||||||
|
large
|
||||||
|
fill
|
||||||
|
fastField
|
||||||
|
className={css`
|
||||||
|
resize: vertical;
|
||||||
|
min-height: 300px;
|
||||||
|
border-top-right-radius: 0px;
|
||||||
|
border-top-left-radius: 0px;
|
||||||
|
`}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
</FFormGroup>
|
||||||
|
|
||||||
|
<Group>
|
||||||
|
<FCheckbox name={'attachPdf'} label={'Attach PDF'} />
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
<InvoiceSendMailFooter />
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function InvoiceSendMailFooter() {
|
||||||
|
const { isSubmitting } = useFormikContext();
|
||||||
|
const { name } = useDrawerContext();
|
||||||
|
const { closeDrawer } = useDrawerActions();
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
closeDrawer(name);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Group
|
||||||
|
py={'12px'}
|
||||||
|
px={'16px'}
|
||||||
|
borderTop="1px solid #d8d8d9"
|
||||||
|
position={'apart'}
|
||||||
|
>
|
||||||
|
<Group spacing={10} ml={'auto'}>
|
||||||
|
<Button
|
||||||
|
disabled={isSubmitting}
|
||||||
|
onClick={handleClose}
|
||||||
|
style={{ minWidth: '65px' }}
|
||||||
|
>
|
||||||
|
Close
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
intent={Intent.PRIMARY}
|
||||||
|
loading={isSubmitting}
|
||||||
|
style={{ minWidth: '85px' }}
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
|
Send Mail
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
</Group>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import * as Yup from 'yup';
|
||||||
|
|
||||||
|
export const InvoiceSendMailFormSchema = Yup.object().shape({
|
||||||
|
subject: Yup.string().required('Subject is required'),
|
||||||
|
message: Yup.string().required('Message is required'),
|
||||||
|
to: Yup.array()
|
||||||
|
.of(Yup.string().email('Invalid email'))
|
||||||
|
.required('To address is required'),
|
||||||
|
cc: Yup.array().of(Yup.string().email('Invalid email')),
|
||||||
|
bcc: Yup.array().of(Yup.string().email('Invalid email')),
|
||||||
|
});
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
import { Form, Formik, FormikHelpers } from 'formik';
|
||||||
|
import { css } from '@emotion/css';
|
||||||
|
import { Intent } from '@blueprintjs/core';
|
||||||
|
import { InvoiceSendMailFormValues } from './_types';
|
||||||
|
import { InvoiceSendMailFormSchema } from './InvoiceSendMailForm.schema';
|
||||||
|
import { useSendSaleInvoiceMail } from '@/hooks/query';
|
||||||
|
import { AppToaster } from '@/components';
|
||||||
|
import { useInvoiceSendMailBoot } from './InvoiceSendMailContentBoot';
|
||||||
|
import { useDrawerActions } from '@/hooks/state';
|
||||||
|
import { useDrawerContext } from '@/components/Drawer/DrawerProvider';
|
||||||
|
import { transformToForm } from '@/utils';
|
||||||
|
|
||||||
|
const initialValues: InvoiceSendMailFormValues = {
|
||||||
|
subject: '',
|
||||||
|
message: '',
|
||||||
|
to: [],
|
||||||
|
cc: [],
|
||||||
|
bcc: [],
|
||||||
|
attachPdf: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
interface InvoiceSendMailFormProps {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function InvoiceSendMailForm({ children }: InvoiceSendMailFormProps) {
|
||||||
|
const { mutateAsync: sendInvoiceMail } = useSendSaleInvoiceMail();
|
||||||
|
const { invoiceId, invoiceMailState } = useInvoiceSendMailBoot();
|
||||||
|
|
||||||
|
const { name } = useDrawerContext();
|
||||||
|
const { closeDrawer } = useDrawerActions();
|
||||||
|
|
||||||
|
const _initialValues: InvoiceSendMailFormValues = {
|
||||||
|
...initialValues,
|
||||||
|
...transformToForm(invoiceMailState, initialValues),
|
||||||
|
};
|
||||||
|
const handleSubmit = (
|
||||||
|
values: InvoiceSendMailFormValues,
|
||||||
|
{ setSubmitting }: FormikHelpers<InvoiceSendMailFormValues>,
|
||||||
|
) => {
|
||||||
|
setSubmitting(true);
|
||||||
|
sendInvoiceMail({ id: invoiceId, values: { ...values } })
|
||||||
|
.then(() => {
|
||||||
|
AppToaster.show({
|
||||||
|
message: 'The invoice mail has been sent to the customer.',
|
||||||
|
intent: Intent.SUCCESS,
|
||||||
|
});
|
||||||
|
setSubmitting(false);
|
||||||
|
closeDrawer(name);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
setSubmitting(false);
|
||||||
|
AppToaster.show({
|
||||||
|
message: 'Something went wrong!',
|
||||||
|
intent: Intent.SUCCESS,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Formik
|
||||||
|
initialValues={_initialValues}
|
||||||
|
validationSchema={InvoiceSendMailFormSchema}
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
>
|
||||||
|
<Form
|
||||||
|
className={css`
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Form>
|
||||||
|
</Formik>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import { Button, Classes } from '@blueprintjs/core';
|
||||||
|
import { x } from '@xstyled/emotion';
|
||||||
|
import { Group, Icon } from '@/components';
|
||||||
|
import { useDrawerContext } from '@/components/Drawer/DrawerProvider';
|
||||||
|
import { useDrawerActions } from '@/hooks/state';
|
||||||
|
|
||||||
|
interface ElementCustomizeHeaderProps {
|
||||||
|
label?: string;
|
||||||
|
children?: React.ReactNode;
|
||||||
|
closeButton?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function InvoiceSendMailHeader({
|
||||||
|
label,
|
||||||
|
closeButton = true,
|
||||||
|
}: ElementCustomizeHeaderProps) {
|
||||||
|
const { name } = useDrawerContext();
|
||||||
|
const { closeDrawer } = useDrawerActions();
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
closeDrawer(name);
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<Group
|
||||||
|
p={'10px'}
|
||||||
|
pl={'30px'}
|
||||||
|
bg="white"
|
||||||
|
alignItems={'center'}
|
||||||
|
boxShadow={'0 1px 0 rgba(17, 20, 24, .15)'}
|
||||||
|
zIndex={1}
|
||||||
|
style={{ position: 'relative' }}
|
||||||
|
>
|
||||||
|
{label && (
|
||||||
|
<x.h1 margin={0} fontSize={20} fontWeight={500} color={'#666'}>
|
||||||
|
{label}
|
||||||
|
</x.h1>
|
||||||
|
)}
|
||||||
|
{closeButton && (
|
||||||
|
<Button
|
||||||
|
aria-label="Close"
|
||||||
|
className={Classes.DIALOG_CLOSE_BUTTON}
|
||||||
|
icon={<Icon icon={'smallCross'} color={'#000'} />}
|
||||||
|
minimal={true}
|
||||||
|
onClick={handleClose}
|
||||||
|
style={{ marginLeft: 'auto' }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Group>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
import React, { useMemo } from 'react';
|
||||||
|
import { x } from '@xstyled/emotion';
|
||||||
|
import { Box, Group, Stack } from '@/components';
|
||||||
|
import { useSendInvoiceMailForm, useSendInvoiceMailSubject } from './_hooks';
|
||||||
|
import { useInvoiceSendMailBoot } from './InvoiceSendMailContentBoot';
|
||||||
|
|
||||||
|
export function InvoiceSendMailHeaderPreview() {
|
||||||
|
const mailSubject = useSendInvoiceMailSubject();
|
||||||
|
const { invoiceMailState } = useInvoiceSendMailBoot();
|
||||||
|
const toAddresses = useMailHeaderToAddresses();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack
|
||||||
|
bg={'white'}
|
||||||
|
borderBottom={'1px solid #dcdcdd'}
|
||||||
|
padding={'22px 30px'}
|
||||||
|
spacing={8}
|
||||||
|
position={'sticky'}
|
||||||
|
top={0}
|
||||||
|
zIndex={1}
|
||||||
|
>
|
||||||
|
<Box>
|
||||||
|
<x.h2 fontWeight={600} fontSize={16}>
|
||||||
|
{mailSubject}
|
||||||
|
</x.h2>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Group display="flex" gap={2}>
|
||||||
|
<Group display="flex" alignItems="center" gap={15}>
|
||||||
|
<x.abbr
|
||||||
|
role="presentation"
|
||||||
|
w={'40px'}
|
||||||
|
h={'40px'}
|
||||||
|
bg={'#daa3e4'}
|
||||||
|
fill={'#daa3e4'}
|
||||||
|
color={'#3f1946'}
|
||||||
|
lineHeight={'40px'}
|
||||||
|
textAlign={'center'}
|
||||||
|
borderRadius={'40px'}
|
||||||
|
fontSize={'14px'}
|
||||||
|
>
|
||||||
|
A
|
||||||
|
</x.abbr>
|
||||||
|
|
||||||
|
<Stack spacing={2}>
|
||||||
|
<Group spacing={2}>
|
||||||
|
<Box fontWeight={600}>Ahmed </Box>
|
||||||
|
<Box color={'#738091'}>
|
||||||
|
<messaging-service@post.bigcapital.app>
|
||||||
|
</Box>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<Box fontSize={'sm'} color={'#738091'}>
|
||||||
|
Reply to: {invoiceMailState?.companyName} {toAddresses};
|
||||||
|
</Box>
|
||||||
|
</Stack>
|
||||||
|
</Group>
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function InvoiceSendMailPreviewWithHeader({
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
<InvoiceSendMailHeaderPreview />
|
||||||
|
<Box>{children}</Box>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useMailHeaderToAddresses = () => {
|
||||||
|
const {
|
||||||
|
values: { to },
|
||||||
|
} = useSendInvoiceMailForm();
|
||||||
|
|
||||||
|
return useMemo(() => to?.map((email) => '<' + email + '>').join(' '), [to]);
|
||||||
|
};
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
import { lazy, Suspense } from 'react';
|
||||||
|
import { css } from '@emotion/css';
|
||||||
|
import { Tab, Tabs } from '@blueprintjs/core';
|
||||||
|
import { Stack } from '@/components';
|
||||||
|
|
||||||
|
const InvoiceMailReceiptPreviewConneceted = lazy(() =>
|
||||||
|
import('./InvoiceMailReceiptPreviewConnected.').then((module) => ({
|
||||||
|
default: module.InvoiceMailReceiptPreviewConneceted,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
const InvoiceSendPdfPreviewConnected = lazy(() =>
|
||||||
|
import('./InvoiceSendPdfPreviewConnected').then((module) => ({
|
||||||
|
default: module.InvoiceSendPdfPreviewConnected,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
|
||||||
|
export function InvoiceSendMailPreview() {
|
||||||
|
return (
|
||||||
|
<Stack bg="#F5F5F5" flex={'1'} maxHeight={'100%'} minWidth="850px">
|
||||||
|
<Tabs
|
||||||
|
id={'preview'}
|
||||||
|
defaultSelectedTabId={'payment-page'}
|
||||||
|
className={css`
|
||||||
|
overflow: hidden;
|
||||||
|
flex: 1 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
.bp4-tab-list {
|
||||||
|
padding: 0 20px;
|
||||||
|
background: #fff;
|
||||||
|
border-bottom: 1px solid #dcdcdd;
|
||||||
|
}
|
||||||
|
.bp4-tab {
|
||||||
|
line-height: 40px;
|
||||||
|
}
|
||||||
|
.bp4-tab:not([aria-selected='true']) {
|
||||||
|
color: #5f6b7c;
|
||||||
|
}
|
||||||
|
.bp4-tab-indicator-wrapper .bp4-tab-indicator {
|
||||||
|
height: 2px;
|
||||||
|
}
|
||||||
|
.bp4-tab-panel {
|
||||||
|
margin: 0;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
<Tab
|
||||||
|
id={'payment-page'}
|
||||||
|
title={'Payment page'}
|
||||||
|
panel={
|
||||||
|
<Suspense>
|
||||||
|
<InvoiceMailReceiptPreviewConneceted />
|
||||||
|
</Suspense>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Tab
|
||||||
|
id="pdf-document"
|
||||||
|
title={'PDF document'}
|
||||||
|
panel={
|
||||||
|
<Suspense>
|
||||||
|
<InvoiceSendPdfPreviewConnected />
|
||||||
|
</Suspense>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Tabs>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { css } from '@emotion/css';
|
||||||
|
import { Box } from '@/components';
|
||||||
|
import { InvoicePaperTemplate } from '../InvoiceCustomize/InvoicePaperTemplate';
|
||||||
|
import { InvoiceSendMailPreviewWithHeader } from './InvoiceSendMailHeaderPreview';
|
||||||
|
|
||||||
|
export function InvoiceSendPdfPreviewConnected() {
|
||||||
|
return (
|
||||||
|
<InvoiceSendMailPreviewWithHeader>
|
||||||
|
<Box px={4} py={6}>
|
||||||
|
<InvoicePaperTemplate
|
||||||
|
className={css`
|
||||||
|
margin: 0 auto;
|
||||||
|
`}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</InvoiceSendMailPreviewWithHeader>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
import { useFormikContext } from 'formik';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
import { SelectOptionProps } from '@blueprintjs-formik/select';
|
||||||
|
import { chain, defaultTo, mapKeys, snakeCase, startCase } from 'lodash';
|
||||||
|
import { InvoiceSendMailFormValues } from './_types';
|
||||||
|
import { useInvoiceSendMailBoot } from './InvoiceSendMailContentBoot';
|
||||||
|
|
||||||
|
export const useSendInvoiceMailForm = () => {
|
||||||
|
return useFormikContext<InvoiceSendMailFormValues>();
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useInvoiceMailItems = () => {
|
||||||
|
const { values } = useFormikContext<InvoiceSendMailFormValues>();
|
||||||
|
const cc = values?.cc || [];
|
||||||
|
const bcc = values?.bcc || [];
|
||||||
|
|
||||||
|
return chain([...values?.to, ...cc, ...bcc])
|
||||||
|
.filter((email) => !!email?.trim())
|
||||||
|
.uniq()
|
||||||
|
.map((email) => ({
|
||||||
|
value: email,
|
||||||
|
text: email,
|
||||||
|
}))
|
||||||
|
.value();
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useSendInvoiceMailFormatArgs = (): Record<string, string> => {
|
||||||
|
const { invoiceMailState } = useInvoiceSendMailBoot();
|
||||||
|
|
||||||
|
return useMemo(() => {
|
||||||
|
return mapKeys(invoiceMailState?.formatArgs, (_, key) =>
|
||||||
|
startCase(snakeCase(key).replace('_', ' ')),
|
||||||
|
);
|
||||||
|
}, [invoiceMailState]);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useSendInvoiceMailSubject = (): string => {
|
||||||
|
const { values } = useFormikContext<InvoiceSendMailFormValues>();
|
||||||
|
const formatArgs = useSendInvoiceMailFormatArgs();
|
||||||
|
|
||||||
|
return formatSmsMessage(values?.subject, formatArgs);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useSendInvoiceFormatArgsOptions = (): Array<SelectOptionProps> => {
|
||||||
|
const formatArgs = useSendInvoiceMailFormatArgs();
|
||||||
|
|
||||||
|
return Object.keys(formatArgs).map((key) => ({
|
||||||
|
value: key,
|
||||||
|
text: key,
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useSendInvoiceMailMessage = (): string => {
|
||||||
|
const { values } = useFormikContext<InvoiceSendMailFormValues>();
|
||||||
|
const formatArgs = useSendInvoiceMailFormatArgs();
|
||||||
|
|
||||||
|
return formatSmsMessage(values?.message, formatArgs);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const formatSmsMessage = (
|
||||||
|
message: string,
|
||||||
|
args: Record<string, any>,
|
||||||
|
) => {
|
||||||
|
let formattedMessage = message;
|
||||||
|
|
||||||
|
Object.keys(args).forEach((key) => {
|
||||||
|
const variable = `{${key}}`;
|
||||||
|
const value = defaultTo(args[key], '');
|
||||||
|
|
||||||
|
formattedMessage = formattedMessage.replace(variable, value);
|
||||||
|
});
|
||||||
|
return formattedMessage;
|
||||||
|
};
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
export interface InvoiceSendMailFormValues {
|
||||||
|
subject: string;
|
||||||
|
message: string;
|
||||||
|
to: string[];
|
||||||
|
cc: string[];
|
||||||
|
bcc: string[];
|
||||||
|
attachPdf: boolean;
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from './InvoiceSendMailDrawer';
|
||||||
@@ -101,7 +101,7 @@ function InvoicesDataTable({
|
|||||||
|
|
||||||
// Handle send mail invoice.
|
// Handle send mail invoice.
|
||||||
const handleSendMailInvoice = ({ id }) => {
|
const handleSendMailInvoice = ({ id }) => {
|
||||||
openDialog(DialogsName.InvoiceMail, { invoiceId: id });
|
openDrawer(DRAWERS.INVOICE_SEND_MAIL, { invoiceId: id });
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handle cell click.
|
// Handle cell click.
|
||||||
|
|||||||
@@ -19,7 +19,12 @@ function ReceiptCustomizeDrawerRoot({
|
|||||||
payload,
|
payload,
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<Drawer isOpen={isOpen} name={name} size={'100%'} payload={payload}>
|
<Drawer
|
||||||
|
isOpen={isOpen}
|
||||||
|
name={name}
|
||||||
|
payload={payload}
|
||||||
|
size={'calc(100% - 10px)'}
|
||||||
|
>
|
||||||
<DrawerSuspense>
|
<DrawerSuspense>
|
||||||
<ReceiptCustomizeDrawerBody />
|
<ReceiptCustomizeDrawerBody />
|
||||||
</DrawerSuspense>
|
</DrawerSuspense>
|
||||||
|
|||||||
@@ -1,5 +1,13 @@
|
|||||||
// @ts-nocheck
|
// @ts-nocheck
|
||||||
import { useQueryClient, useMutation, useQuery } from 'react-query';
|
import {
|
||||||
|
useQueryClient,
|
||||||
|
useMutation,
|
||||||
|
useQuery,
|
||||||
|
UseMutationOptions,
|
||||||
|
UseMutationResult,
|
||||||
|
UseQueryOptions,
|
||||||
|
UseQueryResult,
|
||||||
|
} from 'react-query';
|
||||||
import { useRequestQuery } from '../useQueryRequest';
|
import { useRequestQuery } from '../useQueryRequest';
|
||||||
import { transformPagination, transformToCamelCase } from '@/utils';
|
import { transformPagination, transformToCamelCase } from '@/utils';
|
||||||
import useApiRequest from '../useRequest';
|
import useApiRequest from '../useRequest';
|
||||||
@@ -312,36 +320,114 @@ export function useInvoicePaymentTransactions(invoiceId, props) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useSendSaleInvoiceMail(props) {
|
// # Send sale invoice mail.
|
||||||
|
// ------------------------------
|
||||||
|
export interface SendSaleInvoiceMailValues {
|
||||||
|
id: number;
|
||||||
|
values: {
|
||||||
|
subject: string;
|
||||||
|
message: string;
|
||||||
|
to: Array<string>;
|
||||||
|
cc?: Array<string>;
|
||||||
|
bcc?: Array<string>;
|
||||||
|
attachInvoice?: boolean;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
export interface SendSaleInvoiceMailResponse { }
|
||||||
|
/**
|
||||||
|
* Sends the sale invoice mail.
|
||||||
|
* @param {UseMutationOptions<SendSaleInvoiceMailValues, Error, SendSaleInvoiceMailResponse>}
|
||||||
|
* @returns {UseMutationResult<SendSaleInvoiceMailResponse, Error, SendSaleInvoiceMailValues>}
|
||||||
|
*/
|
||||||
|
export function useSendSaleInvoiceMail(
|
||||||
|
options?: UseMutationOptions<
|
||||||
|
SendSaleInvoiceMailResponse,
|
||||||
|
Error,
|
||||||
|
SendSaleInvoiceMailValues
|
||||||
|
>,
|
||||||
|
): UseMutationResult<
|
||||||
|
SendSaleInvoiceMailResponse,
|
||||||
|
Error,
|
||||||
|
SendSaleInvoiceMailValues
|
||||||
|
> {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const apiRequest = useApiRequest();
|
const apiRequest = useApiRequest();
|
||||||
|
|
||||||
return useMutation(
|
return useMutation<
|
||||||
([id, values]) => apiRequest.post(`sales/invoices/${id}/mail`, values),
|
SendSaleInvoiceMailResponse,
|
||||||
|
Error,
|
||||||
|
SendSaleInvoiceMailValues
|
||||||
|
>(
|
||||||
|
(value) => apiRequest.post(`sales/invoices/${value.id}/mail`, value.values),
|
||||||
{
|
{
|
||||||
onSuccess: (res, [id, values]) => {
|
onSuccess: (res) => {
|
||||||
// Common invalidate queries.
|
|
||||||
commonInvalidateQueries(queryClient);
|
commonInvalidateQueries(queryClient);
|
||||||
},
|
},
|
||||||
...props,
|
...options,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useSaleInvoiceDefaultOptions(invoiceId, props) {
|
// # Get sale invoice default options.
|
||||||
return useRequestQuery(
|
// --------------------------------------
|
||||||
|
export interface GetSaleInvoiceDefaultOptionsResponse {
|
||||||
|
companyName: string;
|
||||||
|
|
||||||
|
dueDate: string;
|
||||||
|
dueDateFormatted: string;
|
||||||
|
|
||||||
|
dueAmount: number;
|
||||||
|
dueAmountFormatted: string;
|
||||||
|
|
||||||
|
entries: Array<{
|
||||||
|
quantity: number;
|
||||||
|
quantityFormatted: string;
|
||||||
|
rate: number;
|
||||||
|
rateFormatted: string;
|
||||||
|
total: number;
|
||||||
|
totalFormatted: string;
|
||||||
|
}>;
|
||||||
|
formatArgs: Record<string, string>;
|
||||||
|
|
||||||
|
from: string[];
|
||||||
|
to: string[];
|
||||||
|
|
||||||
|
invoiceDate: string;
|
||||||
|
invoiceDateFormatted: string;
|
||||||
|
|
||||||
|
invoiceNo: string;
|
||||||
|
|
||||||
|
message: string;
|
||||||
|
subject: string;
|
||||||
|
|
||||||
|
subtotal: number;
|
||||||
|
subtotalFormatted: string;
|
||||||
|
|
||||||
|
total: number;
|
||||||
|
totalFormatted: string;
|
||||||
|
|
||||||
|
attachInvoice: boolean;
|
||||||
|
primaryColor: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useSaleInvoiceMailState(
|
||||||
|
invoiceId: number,
|
||||||
|
options?: UseQueryOptions<GetSaleInvoiceDefaultOptionsResponse>,
|
||||||
|
): UseQueryResult<GetSaleInvoiceDefaultOptionsResponse> {
|
||||||
|
const apiRequest = useApiRequest();
|
||||||
|
|
||||||
|
return useQuery<GetSaleInvoiceDefaultOptionsResponse>(
|
||||||
[t.SALE_INVOICE_DEFAULT_OPTIONS, invoiceId],
|
[t.SALE_INVOICE_DEFAULT_OPTIONS, invoiceId],
|
||||||
{
|
() =>
|
||||||
method: 'get',
|
apiRequest
|
||||||
url: `sales/invoices/${invoiceId}/mail`,
|
.get(`/sales/invoices/${invoiceId}/mail/state`)
|
||||||
},
|
.then((res) => transformToCamelCase(res.data?.data)),
|
||||||
{
|
options,
|
||||||
select: (res) => res.data.data,
|
|
||||||
...props,
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// # Get sale invoice state.
|
||||||
|
// -------------------------------------
|
||||||
export interface GetSaleInvoiceStateResponse {
|
export interface GetSaleInvoiceStateResponse {
|
||||||
defaultTemplateId: number;
|
defaultTemplateId: number;
|
||||||
}
|
}
|
||||||
@@ -360,3 +446,68 @@ export function useGetSaleInvoiceState(
|
|||||||
{ ...options },
|
{ ...options },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// # Get sale invoice branding template.
|
||||||
|
// --------------------------------------
|
||||||
|
export interface GetSaleInvoiceBrandingTemplateResponse {
|
||||||
|
id: number;
|
||||||
|
default: number;
|
||||||
|
predefined: number;
|
||||||
|
resource: string;
|
||||||
|
resourceFormatted: string;
|
||||||
|
templateName: string;
|
||||||
|
updatedAt: string;
|
||||||
|
createdAt: string;
|
||||||
|
createdAtFormatted: string;
|
||||||
|
attributes: {
|
||||||
|
billedToLabel?: string;
|
||||||
|
companyLogoKey?: string | null;
|
||||||
|
companyLogoUri?: string;
|
||||||
|
dateIssueLabel?: string;
|
||||||
|
discountLabel?: string;
|
||||||
|
dueAmountLabel?: string;
|
||||||
|
dueDateLabel?: string;
|
||||||
|
invoiceNumberLabel?: string;
|
||||||
|
itemDescriptionLabel?: string;
|
||||||
|
itemNameLabel?: string;
|
||||||
|
itemRateLabel?: string;
|
||||||
|
itemTotalLabel?: string;
|
||||||
|
paymentMadeLabel?: string;
|
||||||
|
primaryColor?: string;
|
||||||
|
secondaryColor?: string;
|
||||||
|
showCompanyAddress?: boolean;
|
||||||
|
showCompanyLogo?: boolean;
|
||||||
|
showCustomerAddress?: boolean;
|
||||||
|
showDateIssue?: boolean;
|
||||||
|
showDiscount?: boolean;
|
||||||
|
showDueAmount?: boolean;
|
||||||
|
showDueDate?: boolean;
|
||||||
|
showInvoiceNumber?: boolean;
|
||||||
|
showPaymentMade?: boolean;
|
||||||
|
showStatement?: boolean;
|
||||||
|
showSubtotal?: boolean;
|
||||||
|
showTaxes?: boolean;
|
||||||
|
showTermsConditions?: boolean;
|
||||||
|
showTotal?: boolean;
|
||||||
|
statementLabel?: string;
|
||||||
|
subtotalLabel?: string;
|
||||||
|
termsConditionsLabel?: string;
|
||||||
|
totalLabel?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useGetSaleInvoiceBrandingTemplate(
|
||||||
|
invoiceId: number,
|
||||||
|
options?: UseQueryOptions<GetSaleInvoiceBrandingTemplateResponse, Error>,
|
||||||
|
): UseQueryResult<GetSaleInvoiceBrandingTemplateResponse, Error> {
|
||||||
|
const apiRequest = useApiRequest();
|
||||||
|
|
||||||
|
return useQuery<GetSaleInvoiceBrandingTemplateResponse, Error>(
|
||||||
|
['SALE_INVOICE_BRANDING_TEMPLATE', invoiceId],
|
||||||
|
() =>
|
||||||
|
apiRequest
|
||||||
|
.get(`/sales/invoices/${invoiceId}/template`)
|
||||||
|
.then((res) => transformToCamelCase(res.data?.data)),
|
||||||
|
{ ...options },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
7892
pnpm-lock.yaml
generated
7892
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
24
shared/email-components/.gitignore
vendored
Normal file
24
shared/email-components/.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
23
shared/email-components/.storybook/main.tsx
Normal file
23
shared/email-components/.storybook/main.tsx
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import type { StorybookConfig } from '@storybook/react-vite';
|
||||||
|
|
||||||
|
const config: StorybookConfig = {
|
||||||
|
stories: ['../src/**/*.stories.@(js|jsx|ts|tsx)'],
|
||||||
|
addons: [
|
||||||
|
'@storybook/addon-links',
|
||||||
|
'@storybook/addon-essentials',
|
||||||
|
'@storybook/addon-interactions',
|
||||||
|
'@storybook/addon-styling',
|
||||||
|
],
|
||||||
|
framework: {
|
||||||
|
name: '@storybook/react-vite',
|
||||||
|
options: {
|
||||||
|
builder: {
|
||||||
|
viteConfigPath: '.storybook/vite.config.ts',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
docs: {
|
||||||
|
autodocs: 'tag',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
export default config;
|
||||||
15
shared/email-components/.storybook/preview.tsx
Normal file
15
shared/email-components/.storybook/preview.tsx
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import type { Preview } from '@storybook/react';
|
||||||
|
|
||||||
|
const preview: Preview = {
|
||||||
|
parameters: {
|
||||||
|
actions: { argTypesRegex: '^on[A-Z].*' },
|
||||||
|
controls: {
|
||||||
|
matchers: {
|
||||||
|
color: /(background|color)$/i,
|
||||||
|
date: /Date$/,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default preview;
|
||||||
17
shared/email-components/.storybook/vite.config.ts
Normal file
17
shared/email-components/.storybook/vite.config.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import react from '@vitejs/plugin-react';
|
||||||
|
import { defineConfig } from 'vitest/config';
|
||||||
|
import tailwindcss from 'tailwindcss';
|
||||||
|
import { UserConfigExport } from 'vite';
|
||||||
|
|
||||||
|
const app = async (): Promise<UserConfigExport> => {
|
||||||
|
return defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
css: {
|
||||||
|
postcss: {
|
||||||
|
plugins: [tailwindcss],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
// https://vitejs.dev/config/
|
||||||
|
export default app;
|
||||||
28
shared/email-components/eslint.config.js
Normal file
28
shared/email-components/eslint.config.js
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import js from '@eslint/js'
|
||||||
|
import globals from 'globals'
|
||||||
|
import reactHooks from 'eslint-plugin-react-hooks'
|
||||||
|
import reactRefresh from 'eslint-plugin-react-refresh'
|
||||||
|
import tseslint from 'typescript-eslint'
|
||||||
|
|
||||||
|
export default tseslint.config(
|
||||||
|
{ ignores: ['dist'] },
|
||||||
|
{
|
||||||
|
extends: [js.configs.recommended, ...tseslint.configs.recommended],
|
||||||
|
files: ['**/*.{ts,tsx}'],
|
||||||
|
languageOptions: {
|
||||||
|
ecmaVersion: 2020,
|
||||||
|
globals: globals.browser,
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
'react-hooks': reactHooks,
|
||||||
|
'react-refresh': reactRefresh,
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
...reactHooks.configs.recommended.rules,
|
||||||
|
'react-refresh/only-export-components': [
|
||||||
|
'warn',
|
||||||
|
{ allowConstantExport: true },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
54
shared/email-components/package.json
Normal file
54
shared/email-components/package.json
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
{
|
||||||
|
"name": "@bigcapital/email-components",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsc && vite build",
|
||||||
|
"lint": "eslint .",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"storybook:dev": "storybook dev -p 6006",
|
||||||
|
"storybook:build": "storybook build"
|
||||||
|
},
|
||||||
|
"main": "./dist/email-components.umd.js",
|
||||||
|
"module": "./dist/email-components.es.js",
|
||||||
|
"types": "./dist/main.d.ts",
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"types": "./dist/main.d.ts",
|
||||||
|
"import": "./dist/email-components.es.js",
|
||||||
|
"require": "./dist/email-components.umd.js"
|
||||||
|
},
|
||||||
|
"./dist/style.css": "./dist/style.css"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@react-email/components": "0.0.25",
|
||||||
|
"tailwindcss": "^3.4.14",
|
||||||
|
"vite-plugin-dts": "^4.3.0",
|
||||||
|
"vitest": "^2.1.3",
|
||||||
|
"react-dom": "18.3.1",
|
||||||
|
"react": "18.3.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@eslint/js": "^9.13.0",
|
||||||
|
"@storybook/addon-essentials": "7.2.2",
|
||||||
|
"@storybook/addon-interactions": "7.2.2",
|
||||||
|
"@storybook/addon-links": "7.2.2",
|
||||||
|
"@storybook/addon-styling": "1.3.6",
|
||||||
|
"@storybook/blocks": "7.2.2",
|
||||||
|
"@storybook/react": "7.2.2",
|
||||||
|
"@storybook/react-vite": "7.2.2",
|
||||||
|
"@storybook/testing-library": "0.2.0",
|
||||||
|
"@types/react": "18.3.4",
|
||||||
|
"@types/react-dom": "18.3.0",
|
||||||
|
"@vitejs/plugin-react": "^4.3.3",
|
||||||
|
"eslint": "^9.13.0",
|
||||||
|
"eslint-plugin-react-hooks": "^5.0.0",
|
||||||
|
"eslint-plugin-react-refresh": "^0.4.13",
|
||||||
|
"eslint-plugin-storybook": "0.6.13",
|
||||||
|
"globals": "^15.11.0",
|
||||||
|
"storybook": "7.2.2",
|
||||||
|
"typescript": "~5.6.2",
|
||||||
|
"typescript-eslint": "^8.10.0",
|
||||||
|
"vite": "^5.4.9"
|
||||||
|
}
|
||||||
|
}
|
||||||
6
shared/email-components/postman.config.js
Normal file
6
shared/email-components/postman.config.js
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
module.exports = {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import { Meta } from '@storybook/react';
|
||||||
|
import { StoryFn } from '@storybook/react';
|
||||||
|
import {
|
||||||
|
InvoicePaymentEmail,
|
||||||
|
InvoicePaymentEmailProps,
|
||||||
|
} from './InvoicePaymentEmail';
|
||||||
|
|
||||||
|
const meta: Meta<typeof InvoicePaymentEmail> = {
|
||||||
|
title: 'Invoice Payment Email',
|
||||||
|
component: InvoicePaymentEmail,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
|
||||||
|
const Template: StoryFn<typeof InvoicePaymentEmail> = (
|
||||||
|
args: InvoicePaymentEmailProps
|
||||||
|
) => <InvoicePaymentEmail {...args} />;
|
||||||
|
|
||||||
|
export const PreviewInvoicePaymentMail = Template.bind({});
|
||||||
|
|
||||||
|
PreviewInvoicePaymentMail.args = {
|
||||||
|
preview: 'Preview text',
|
||||||
|
companyName: 'ABC Company',
|
||||||
|
companyLogoUri: 'https://example.com/logo.png',
|
||||||
|
invoiceAmount: '100.00',
|
||||||
|
dueDate: '2022-12-31',
|
||||||
|
invoiceMessage: 'Thank you for your purchase!',
|
||||||
|
invoiceNumber: 'INV-001',
|
||||||
|
dueAmount: '100.00',
|
||||||
|
total: '100.00',
|
||||||
|
viewInvoiceButtonUrl: 'https://example.com/invoice',
|
||||||
|
items: [
|
||||||
|
{ label: 'Item 1', quantity: '1', rate: '50.00' },
|
||||||
|
{ label: 'Item 2', quantity: '2', rate: '25.00' },
|
||||||
|
],
|
||||||
|
};
|
||||||
334
shared/email-components/src/lib/InvoicePaymentEmail.tsx
Normal file
334
shared/email-components/src/lib/InvoicePaymentEmail.tsx
Normal file
@@ -0,0 +1,334 @@
|
|||||||
|
import {
|
||||||
|
Html,
|
||||||
|
Button,
|
||||||
|
Head,
|
||||||
|
Body,
|
||||||
|
Container,
|
||||||
|
Section,
|
||||||
|
Heading,
|
||||||
|
Text,
|
||||||
|
Preview,
|
||||||
|
Tailwind,
|
||||||
|
Row,
|
||||||
|
Column,
|
||||||
|
render,
|
||||||
|
} from '@react-email/components';
|
||||||
|
import { CSSProperties } from 'react';
|
||||||
|
|
||||||
|
export interface InvoicePaymentEmailProps {
|
||||||
|
preview: string;
|
||||||
|
|
||||||
|
// # Company
|
||||||
|
companyName?: string;
|
||||||
|
companyLogoUri?: string;
|
||||||
|
|
||||||
|
// # Colors
|
||||||
|
primaryColor?: string;
|
||||||
|
|
||||||
|
// # Invoice amount
|
||||||
|
invoiceAmount: string;
|
||||||
|
|
||||||
|
// # Invoice message
|
||||||
|
invoiceMessage: string;
|
||||||
|
|
||||||
|
// # Invoice total
|
||||||
|
total: string;
|
||||||
|
totalLabel?: string;
|
||||||
|
|
||||||
|
// # Invoice due amount
|
||||||
|
dueAmount: string;
|
||||||
|
dueAmountLabel?: string;
|
||||||
|
|
||||||
|
// # Due date
|
||||||
|
dueDate: string;
|
||||||
|
dueDateLabel?: string;
|
||||||
|
|
||||||
|
// # Invoice number
|
||||||
|
invoiceNumberLabel?: string;
|
||||||
|
invoiceNumber: string;
|
||||||
|
|
||||||
|
// # View invoice button
|
||||||
|
viewInvoiceButtonLabel?: string;
|
||||||
|
viewInvoiceButtonUrl: string;
|
||||||
|
|
||||||
|
// # Items
|
||||||
|
items: Array<{ label: string; quantity: string; rate: string }>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const InvoicePaymentEmail: React.FC<
|
||||||
|
Readonly<InvoicePaymentEmailProps>
|
||||||
|
> = ({
|
||||||
|
preview,
|
||||||
|
|
||||||
|
// # Company
|
||||||
|
companyName,
|
||||||
|
companyLogoUri,
|
||||||
|
|
||||||
|
// # Colors
|
||||||
|
primaryColor = 'rgb(0, 82, 204)',
|
||||||
|
|
||||||
|
// # Invoice amount
|
||||||
|
invoiceAmount,
|
||||||
|
|
||||||
|
// # Invoice message
|
||||||
|
invoiceMessage,
|
||||||
|
|
||||||
|
// # Due date
|
||||||
|
dueDate,
|
||||||
|
dueDateLabel = 'Due {dueDate}',
|
||||||
|
|
||||||
|
// # Invoice number
|
||||||
|
invoiceNumber,
|
||||||
|
invoiceNumberLabel = 'Invoice # {invoiceNumber}',
|
||||||
|
|
||||||
|
// # invoice total
|
||||||
|
total,
|
||||||
|
totalLabel = 'Total',
|
||||||
|
|
||||||
|
// # Invoice due amount
|
||||||
|
dueAmountLabel = 'Due Amount',
|
||||||
|
dueAmount,
|
||||||
|
|
||||||
|
// # View invoice button
|
||||||
|
viewInvoiceButtonLabel = 'View Invoice',
|
||||||
|
viewInvoiceButtonUrl,
|
||||||
|
|
||||||
|
items,
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<Html lang="en">
|
||||||
|
<Head />
|
||||||
|
<Preview>{preview}</Preview>
|
||||||
|
|
||||||
|
<Tailwind>
|
||||||
|
<Body style={bodyStyle}>
|
||||||
|
<Container style={containerStyle}>
|
||||||
|
<Section style={mainSectionStyle}>
|
||||||
|
{companyLogoUri && (
|
||||||
|
<Section style={logoSectionStyle}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
...companyLogoStyle,
|
||||||
|
backgroundImage: `url("${companyLogoUri}")`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Image
|
||||||
|
</div>
|
||||||
|
</Section>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Section style={headerInfoStyle}>
|
||||||
|
<Row>
|
||||||
|
<Heading style={invoiceCompanyNameStyle}>
|
||||||
|
{companyName}
|
||||||
|
</Heading>
|
||||||
|
</Row>
|
||||||
|
<Row>
|
||||||
|
<Text style={invoiceAmountStyle}>{invoiceAmount}</Text>
|
||||||
|
</Row>
|
||||||
|
<Row>
|
||||||
|
<Text style={invoiceNumberStyle}>
|
||||||
|
{invoiceNumberLabel?.replace(
|
||||||
|
'{invoiceNumber}',
|
||||||
|
invoiceNumber
|
||||||
|
)}
|
||||||
|
</Text>
|
||||||
|
</Row>
|
||||||
|
<Row>
|
||||||
|
<Text style={invoiceDateStyle}>
|
||||||
|
{dueDateLabel.replace('{dueDate}', dueDate)}
|
||||||
|
</Text>
|
||||||
|
</Row>
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
<Text style={invoiceMessageStyle}>{invoiceMessage}</Text>
|
||||||
|
<Button
|
||||||
|
href={viewInvoiceButtonUrl}
|
||||||
|
style={{
|
||||||
|
...viewInvoiceButtonStyle,
|
||||||
|
backgroundColor: primaryColor,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{viewInvoiceButtonLabel}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Section style={totalsSectionStyle}>
|
||||||
|
{items.map((item, index) => (
|
||||||
|
<Row key={index} style={itemLineRowStyle}>
|
||||||
|
<Column width={'50%'}>
|
||||||
|
<Text style={listItemLabelStyle}>{item.label}</Text>
|
||||||
|
</Column>
|
||||||
|
|
||||||
|
<Column width={'50%'}>
|
||||||
|
<Text style={listItemAmountStyle}>
|
||||||
|
{item.quantity} x {item.rate}
|
||||||
|
</Text>
|
||||||
|
</Column>
|
||||||
|
</Row>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<Row style={dueAmounLineRowStyle}>
|
||||||
|
<Column width={'50%'}>
|
||||||
|
<Text style={dueAmountLineItemLabelStyle}>
|
||||||
|
{dueAmountLabel}
|
||||||
|
</Text>
|
||||||
|
</Column>
|
||||||
|
|
||||||
|
<Column width={'50%'}>
|
||||||
|
<Text style={dueAmountLineItemAmountStyle}>
|
||||||
|
{dueAmount}
|
||||||
|
</Text>
|
||||||
|
</Column>
|
||||||
|
</Row>
|
||||||
|
|
||||||
|
<Row style={totalLineRowStyle}>
|
||||||
|
<Column width={'50%'}>
|
||||||
|
<Text style={totalLineItemLabelStyle}>{totalLabel}</Text>
|
||||||
|
</Column>
|
||||||
|
|
||||||
|
<Column width={'50%'}>
|
||||||
|
<Text style={totalLineItemAmountStyle}>{total}</Text>
|
||||||
|
</Column>
|
||||||
|
</Row>
|
||||||
|
</Section>
|
||||||
|
</Section>
|
||||||
|
</Container>
|
||||||
|
</Body>
|
||||||
|
</Tailwind>
|
||||||
|
</Html>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const renderInvoicePaymentEmail = (props: InvoicePaymentEmailProps) => {
|
||||||
|
return render(<InvoicePaymentEmail {...props} />);
|
||||||
|
};
|
||||||
|
|
||||||
|
const bodyStyle: CSSProperties = {
|
||||||
|
backgroundColor: '#F5F5F5',
|
||||||
|
fontFamily:
|
||||||
|
'-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Ubuntu,sans-serif',
|
||||||
|
|
||||||
|
padding: '40px 0',
|
||||||
|
};
|
||||||
|
|
||||||
|
const containerStyle: CSSProperties = {
|
||||||
|
backgroundColor: '#fff',
|
||||||
|
width: '100%',
|
||||||
|
maxWidth: '500px',
|
||||||
|
padding: '35px 25px',
|
||||||
|
color: '#000',
|
||||||
|
borderRadius: '5px',
|
||||||
|
};
|
||||||
|
|
||||||
|
const headerInfoStyle: CSSProperties = {
|
||||||
|
textAlign: 'center',
|
||||||
|
marginBottom: 20,
|
||||||
|
};
|
||||||
|
const mainSectionStyle: CSSProperties = {};
|
||||||
|
|
||||||
|
const invoiceAmountStyle: CSSProperties = {
|
||||||
|
margin: 0,
|
||||||
|
color: '#383E47',
|
||||||
|
fontWeight: 500,
|
||||||
|
};
|
||||||
|
const invoiceNumberStyle: CSSProperties = {
|
||||||
|
margin: 0,
|
||||||
|
fontSize: '13px',
|
||||||
|
color: '#404854',
|
||||||
|
};
|
||||||
|
const invoiceDateStyle: CSSProperties = {
|
||||||
|
margin: 0,
|
||||||
|
fontSize: '13px',
|
||||||
|
color: '#404854',
|
||||||
|
};
|
||||||
|
|
||||||
|
const invoiceCompanyNameStyle: CSSProperties = {
|
||||||
|
margin: 0,
|
||||||
|
fontSize: '18px',
|
||||||
|
fontWeight: 500,
|
||||||
|
color: '#404854',
|
||||||
|
};
|
||||||
|
|
||||||
|
const viewInvoiceButtonStyle: CSSProperties = {
|
||||||
|
display: 'block',
|
||||||
|
cursor: 'pointer',
|
||||||
|
textAlign: 'center',
|
||||||
|
fontSize: 16,
|
||||||
|
padding: '10px 15px',
|
||||||
|
lineHeight: '1',
|
||||||
|
backgroundColor: 'rgb(0, 82, 204)',
|
||||||
|
color: '#fff',
|
||||||
|
borderRadius: '5px',
|
||||||
|
};
|
||||||
|
|
||||||
|
const listItemLabelStyle: CSSProperties = {
|
||||||
|
margin: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
const listItemAmountStyle: CSSProperties = {
|
||||||
|
margin: 0,
|
||||||
|
textAlign: 'right',
|
||||||
|
};
|
||||||
|
|
||||||
|
const invoiceMessageStyle: CSSProperties = {
|
||||||
|
whiteSpace: 'pre-line',
|
||||||
|
color: '#252A31',
|
||||||
|
margin: '0 0 20px 0',
|
||||||
|
};
|
||||||
|
|
||||||
|
const dueAmounLineRowStyle: CSSProperties = {
|
||||||
|
borderBottom: '1px solid #000',
|
||||||
|
height: 40,
|
||||||
|
};
|
||||||
|
|
||||||
|
const totalLineRowStyle: CSSProperties = {
|
||||||
|
borderBottom: '1px solid #000',
|
||||||
|
height: 40,
|
||||||
|
};
|
||||||
|
|
||||||
|
const totalLineItemLabelStyle: CSSProperties = {
|
||||||
|
...listItemLabelStyle,
|
||||||
|
fontWeight: 500,
|
||||||
|
};
|
||||||
|
|
||||||
|
const totalLineItemAmountStyle: CSSProperties = {
|
||||||
|
...listItemAmountStyle,
|
||||||
|
fontWeight: 600,
|
||||||
|
};
|
||||||
|
|
||||||
|
const dueAmountLineItemLabelStyle: CSSProperties = {
|
||||||
|
...listItemLabelStyle,
|
||||||
|
fontWeight: 500,
|
||||||
|
};
|
||||||
|
|
||||||
|
const dueAmountLineItemAmountStyle: CSSProperties = {
|
||||||
|
...listItemAmountStyle,
|
||||||
|
fontWeight: 600,
|
||||||
|
};
|
||||||
|
|
||||||
|
const itemLineRowStyle: CSSProperties = {
|
||||||
|
borderBottom: '1px solid #D9D9D9',
|
||||||
|
height: 40,
|
||||||
|
};
|
||||||
|
|
||||||
|
const totalsSectionStyle = {
|
||||||
|
marginTop: '20px',
|
||||||
|
borderTop: '1px solid #D9D9D9',
|
||||||
|
};
|
||||||
|
|
||||||
|
const logoSectionStyle = {
|
||||||
|
marginBottom: '15px',
|
||||||
|
};
|
||||||
|
|
||||||
|
const companyLogoStyle = {
|
||||||
|
height: 90,
|
||||||
|
width: 90,
|
||||||
|
borderRadius: '3px',
|
||||||
|
marginLeft: 'auto',
|
||||||
|
marginRight: 'auto',
|
||||||
|
textIndent: '-999999px',
|
||||||
|
overflow: 'hidden',
|
||||||
|
backgroundRepeat: 'no-repeat',
|
||||||
|
backgroundPosition: 'center center',
|
||||||
|
backgroundSize: 'contain',
|
||||||
|
};
|
||||||
1
shared/email-components/src/lib/main.ts
Normal file
1
shared/email-components/src/lib/main.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from './InvoicePaymentEmail';
|
||||||
1
shared/email-components/src/vite-env.d.ts
vendored
Normal file
1
shared/email-components/src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
8
shared/email-components/tailwind.config.js
Normal file
8
shared/email-components/tailwind.config.js
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
module.exports = {
|
||||||
|
content: ['./src/**/*.{ts,tsx}'],
|
||||||
|
theme: {
|
||||||
|
extend: {},
|
||||||
|
},
|
||||||
|
plugins: [],
|
||||||
|
};
|
||||||
27
shared/email-components/tsconfig.json
Normal file
27
shared/email-components/tsconfig.json
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"noEmit": true,
|
||||||
|
"composite": false,
|
||||||
|
"declaration": true,
|
||||||
|
"declarationMap": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"inlineSources": false,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"noUnusedLocals": false,
|
||||||
|
"noUnusedParameters": false,
|
||||||
|
"preserveWatchOutput": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"strict": true,
|
||||||
|
"strictNullChecks": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"lib": ["ESNext", "DOM", "DOM.Iterable"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"target": "ESNext",
|
||||||
|
"types": ["vitest/globals"],
|
||||||
|
"resolveJsonModule": true
|
||||||
|
},
|
||||||
|
"include": ["."],
|
||||||
|
"exclude": ["dist", "build", "node_modules"]
|
||||||
|
}
|
||||||
23
shared/email-components/tsconfig.node.json
Normal file
23
shared/email-components/tsconfig.node.json
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"lib": ["ES2023"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "Bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"noEmit": true,
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noUncheckedSideEffectImports": true
|
||||||
|
},
|
||||||
|
"include": ["vite.config.ts"]
|
||||||
|
}
|
||||||
55
shared/email-components/vite.config.ts
Normal file
55
shared/email-components/vite.config.ts
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import react from '@vitejs/plugin-react';
|
||||||
|
import path from 'node:path';
|
||||||
|
import { defineConfig } from 'vitest/config';
|
||||||
|
import dts from 'vite-plugin-dts';
|
||||||
|
import tailwindcss from 'tailwindcss';
|
||||||
|
import { UserConfigExport } from 'vite';
|
||||||
|
import { name } from './package.json';
|
||||||
|
|
||||||
|
const app = async (): Promise<UserConfigExport> => {
|
||||||
|
/**
|
||||||
|
* Removes everything before the last
|
||||||
|
* @octocat/library-repo -> library-repo
|
||||||
|
* vite-component-library-template -> vite-component-library-template
|
||||||
|
*/
|
||||||
|
const formattedName = name.match(/[^/]+$/)?.[0] ?? name;
|
||||||
|
|
||||||
|
return defineConfig({
|
||||||
|
plugins: [
|
||||||
|
react(),
|
||||||
|
dts({
|
||||||
|
insertTypesEntry: true,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
css: {
|
||||||
|
postcss: {
|
||||||
|
plugins: [tailwindcss],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
build: {
|
||||||
|
lib: {
|
||||||
|
entry: path.resolve(__dirname, 'src/lib/main.ts'),
|
||||||
|
name: formattedName,
|
||||||
|
formats: ['es', 'umd'],
|
||||||
|
fileName: (format: string) => `${formattedName}.${format}.js`,
|
||||||
|
},
|
||||||
|
rollupOptions: {
|
||||||
|
external: ['react', 'react/jsx-runtime', 'react-dom', 'tailwindcss'],
|
||||||
|
output: {
|
||||||
|
globals: {
|
||||||
|
react: 'React',
|
||||||
|
'react/jsx-runtime': 'react/jsx-runtime',
|
||||||
|
'react-dom': 'ReactDOM',
|
||||||
|
tailwindcss: 'tailwindcss',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
test: {
|
||||||
|
globals: true,
|
||||||
|
environment: 'jsdom',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
// https://vitejs.dev/config/
|
||||||
|
export default app;
|
||||||
Reference in New Issue
Block a user