diff --git a/packages/server/package.json b/packages/server/package.json index bf73ce259..427d42d00 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -23,6 +23,7 @@ "@aws-sdk/client-s3": "^3.576.0", "@aws-sdk/s3-request-presigner": "^3.583.0", "@bigcapital/utils": "*", + "@bigcapital/email-components": "*", "@casl/ability": "^5.4.3", "@hapi/boom": "^7.4.3", "@lemonsqueezy/lemonsqueezy.js": "^2.2.0", diff --git a/packages/server/src/api/controllers/Sales/SalesInvoices.ts b/packages/server/src/api/controllers/Sales/SalesInvoices.ts index ba900b786..c0c0bf54c 100644 --- a/packages/server/src/api/controllers/Sales/SalesInvoices.ts +++ b/packages/server/src/api/controllers/Sales/SalesInvoices.ts @@ -179,10 +179,21 @@ export default class SaleInvoicesController extends BaseController { '/:id/mail', [ ...this.specificSaleInvoiceValidation, - body('subject').isString().optional(), + + body('subject').isString().optional({ nullable: true }), + body('message').isString().optional({ nullable: true }), + 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(), ], this.validationResult, @@ -190,7 +201,7 @@ export default class SaleInvoicesController extends BaseController { this.handleServiceErrors ); router.get( - '/:id/mail', + '/:id/mail/state', [...this.specificSaleInvoiceValidation], this.validationResult, 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 {Response} res * @param {NextFunction} next @@ -792,7 +803,7 @@ export default class SaleInvoicesController extends BaseController { const { id: invoiceId } = req.params; try { - const data = await this.saleInvoiceApplication.getSaleInvoiceMail( + const data = await this.saleInvoiceApplication.getSaleInvoiceMailState( tenantId, invoiceId ); diff --git a/packages/server/src/constants/event-tracker.ts b/packages/server/src/constants/event-tracker.ts index dbca8017e..b033abe8c 100644 --- a/packages/server/src/constants/event-tracker.ts +++ b/packages/server/src/constants/event-tracker.ts @@ -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_VIEWED = 'Sale invoice 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_EDITED = 'Sale estimate edited'; diff --git a/packages/server/src/interfaces/Mailable.ts b/packages/server/src/interfaces/Mailable.ts index 5682f2529..7412f2079 100644 --- a/packages/server/src/interfaces/Mailable.ts +++ b/packages/server/src/interfaces/Mailable.ts @@ -30,18 +30,14 @@ export interface AddressItem { } export interface CommonMailOptions { - toAddresses: AddressItem[]; - fromAddresses: AddressItem[]; - from: string; - to: string | string[]; + from: Array; subject: string; - body: string; + message: string; + to: Array; + cc?: Array; + bcc?: Array; data?: Record; } -export interface CommonMailOptionsDTO { - to?: string | string[]; - from?: string; - subject?: string; - body?: string; +export interface CommonMailOptionsDTO extends Partial { } diff --git a/packages/server/src/interfaces/SaleInvoice.ts b/packages/server/src/interfaces/SaleInvoice.ts index 59404b549..01fc8bf26 100644 --- a/packages/server/src/interfaces/SaleInvoice.ts +++ b/packages/server/src/interfaces/SaleInvoice.ts @@ -234,7 +234,32 @@ export enum SaleInvoiceAction { } export interface SaleInvoiceMailOptions extends CommonMailOptions { - attachInvoice: boolean; + attachInvoice?: boolean; + formatArgs?: Record; +} + +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 { @@ -251,6 +276,7 @@ export interface ISaleInvoiceMailSend { tenantId: number; saleInvoiceId: number; messageOptions: SendInvoiceMailDTO; + formattedMessageOptions: SaleInvoiceMailOptions; } export interface ISaleInvoiceMailSent { diff --git a/packages/server/src/services/EventsTracker/events/SaleInvoicesEventsTracker.ts b/packages/server/src/services/EventsTracker/events/SaleInvoicesEventsTracker.ts index 9f3b820d1..ca05a2478 100644 --- a/packages/server/src/services/EventsTracker/events/SaleInvoicesEventsTracker.ts +++ b/packages/server/src/services/EventsTracker/events/SaleInvoicesEventsTracker.ts @@ -10,6 +10,7 @@ import { SALE_INVOICE_CREATED, SALE_INVOICE_DELETED, SALE_INVOICE_EDITED, + SALE_INVOICE_MAIL_SENT, SALE_INVOICE_PDF_VIEWED, SALE_INVOICE_VIEWED, } from '@/constants/event-tracker'; @@ -43,6 +44,10 @@ export class SaleInvoiceEventsTracker extends EventSubscriber { events.saleInvoice.onPdfViewed, this.handleTrackPdfViewedInvoiceEvent ); + bus.subscribe( + events.saleInvoice.onMailSent, + this.handleTrackMailSentInvoiceEvent + ); } private handleTrackInvoiceCreatedEvent = ({ @@ -90,4 +95,12 @@ export class SaleInvoiceEventsTracker extends EventSubscriber { properties: {}, }); }; + + private handleTrackMailSentInvoiceEvent = ({ tenantId }) => { + this.posthog.trackEvent({ + distinctId: `tenant-${tenantId}`, + event: SALE_INVOICE_MAIL_SENT, + properties: {}, + }); + }; } diff --git a/packages/server/src/services/MailNotification/ContactMailNotification.ts b/packages/server/src/services/MailNotification/ContactMailNotification.ts index e1e733a79..725e28821 100644 --- a/packages/server/src/services/MailNotification/ContactMailNotification.ts +++ b/packages/server/src/services/MailNotification/ContactMailNotification.ts @@ -4,6 +4,7 @@ import HasTenancyService from '@/services/Tenancy/TenancyService'; import { MailTenancy } from '@/services/MailTenancy/MailTenancy'; import { formatSmsMessage } from '@/utils'; import { Tenant } from '@/system/models'; +import { castArray } from 'lodash'; @Service() export class ContactMailNotification { @@ -14,76 +15,54 @@ export class ContactMailNotification { private tenancy: HasTenancyService; /** - * Parses the default message options. - * @param {number} tenantId - - * @param {number} invoiceId - - * @param {string} subject - - * @param {string} body - - * @returns {Promise} + * Gets the default mail address of the given contact. + * @param {number} tenantId - Tenant id. + * @param {number} invoiceId - Contact id. + * @returns {Promise>} */ public async getDefaultMailOptions( tenantId: number, - contactId: number, - subject: string = '', - body: string = '' - ): Promise { + customerId: number + ): Promise> { const { Customer } = this.tenancy.models(tenantId); - const contact = await Customer.query() - .findById(contactId) + const customer = await Customer.query() + .findById(customerId) .throwIfNotFound(); - const toAddresses = contact.contactAddresses; + const toAddresses = customer.contactAddresses; const fromAddresses = await this.mailTenancy.senders(tenantId); const toAddress = toAddresses.find((a) => a.primary); const fromAddress = fromAddresses.find((a) => a.primary); - const to = toAddress?.mail || ''; - const from = fromAddress?.mail || ''; + const to = toAddress?.mail ? castArray(toAddress?.mail) : []; + const from = fromAddress?.mail ? castArray(fromAddress?.mail) : []; - return { - subject, - body, - to, - from, - fromAddresses, - toAddresses, - }; + return { to, from }; } /** * Retrieves the mail options of the given contact. * @param {number} tenantId - Tenant id. - * @param {number} invoiceId - Invoice id. - * @param {string} defaultSubject - Default subject text. - * @param {string} defaultBody - Default body text. * @returns {Promise} */ - public async getMailOptions( + public async parseMailOptions( tenantId: number, - contactId: number, - defaultSubject?: string, - defaultBody?: string, - formatterData?: Record + mailOptions: CommonMailOptions, + formatterArgs?: Record ): Promise { - const mailOpts = await this.getDefaultMailOptions( - tenantId, - contactId, - defaultSubject, - defaultBody - ); const commonFormatArgs = await this.getCommonFormatArgs(tenantId); const formatArgs = { ...commonFormatArgs, - ...formatterData, + ...formatterArgs, }; - const subject = formatSmsMessage(mailOpts.subject, formatArgs); - const body = formatSmsMessage(mailOpts.body, formatArgs); + const subjectFormatted = formatSmsMessage(mailOptions?.subject, formatArgs); + const messageFormatted = formatSmsMessage(mailOptions?.message, formatArgs); return { - ...mailOpts, - subject, - body, + ...mailOptions, + subject: subjectFormatted, + message: messageFormatted, }; } @@ -100,7 +79,7 @@ export class ContactMailNotification { .withGraphFetched('metadata'); return { - CompanyName: organization.metadata.name, + ['Company Name']: organization.metadata.name, }; } } diff --git a/packages/server/src/services/MailNotification/utils.ts b/packages/server/src/services/MailNotification/utils.ts index b9e37b297..115a01313 100644 --- a/packages/server/src/services/MailNotification/utils.ts +++ b/packages/server/src/services/MailNotification/utils.ts @@ -1,33 +1,46 @@ -import { isEmpty } from 'lodash'; +import { castArray, isEmpty } from 'lodash'; import { ServiceError } from '@/exceptions'; -import { CommonMailOptions, CommonMailOptionsDTO } from '@/interfaces'; +import { CommonMailOptions } from '@/interfaces'; import { ERRORS } from './constants'; /** * Merges the mail options with incoming options. * @param {Partial} mailOptions * @param {Partial} overridedOptions - * @throws {ServiceError} */ -export function parseAndValidateMailOptions( - mailOptions: Partial, - overridedOptions: Partial -) { +export function parseMailOptions( + mailOptions: CommonMailOptions, + overridedOptions: Partial +): CommonMailOptions { const mergedMessageOptions = { ...mailOptions, ...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 +) { + if (isEmpty(mailOptions.from)) { throw new ServiceError(ERRORS.MAIL_FROM_NOT_FOUND); } - if (isEmpty(mergedMessageOptions.to)) { + if (isEmpty(mailOptions.to)) { throw new ServiceError(ERRORS.MAIL_TO_NOT_FOUND); } - if (isEmpty(mergedMessageOptions.subject)) { + if (isEmpty(mailOptions.subject)) { throw new ServiceError(ERRORS.MAIL_SUBJECT_NOT_FOUND); } - if (isEmpty(mergedMessageOptions.body)) { + if (isEmpty(mailOptions.message)) { throw new ServiceError(ERRORS.MAIL_BODY_NOT_FOUND); } - return mergedMessageOptions; } diff --git a/packages/server/src/services/Sales/Invoices/GetInvoicePaymentMail.ts b/packages/server/src/services/Sales/Invoices/GetInvoicePaymentMail.ts new file mode 100644 index 000000000..4a41b3f26 --- /dev/null +++ b/packages/server/src/services/Sales/Invoices/GetInvoicePaymentMail.ts @@ -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 + ): Promise { + const attributes = await this.getMailTemplateAttributes( + tenantId, + invoiceId + ); + const mergedAttributes = { ...attributes, ...overrideAttributes }; + + return renderInvoicePaymentEmail(mergedAttributes); + } +} diff --git a/packages/server/src/services/Sales/Invoices/GetInvoicePaymentMailAttributesTransformer.ts b/packages/server/src/services/Sales/Invoices/GetInvoicePaymentMailAttributesTransformer.ts new file mode 100644 index 000000000..976b5053f --- /dev/null +++ b/packages/server/src/services/Sales/Invoices/GetInvoicePaymentMailAttributesTransformer.ts @@ -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 { + 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; + } +} diff --git a/packages/server/src/services/Sales/Invoices/GetSaleInvoiceMailState.ts b/packages/server/src/services/Sales/Invoices/GetSaleInvoiceMailState.ts new file mode 100644 index 000000000..c32fa2430 --- /dev/null +++ b/packages/server/src/services/Sales/Invoices/GetSaleInvoiceMailState.ts @@ -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} + */ + async getInvoiceMailState( + tenantId: number, + saleInvoiceId: number + ): Promise { + 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; + } +} diff --git a/packages/server/src/services/Sales/Invoices/GetSaleInvoiceMailStateTransformer.ts b/packages/server/src/services/Sales/Invoices/GetSaleInvoiceMailStateTransformer.ts new file mode 100644 index 000000000..75d3d1f62 --- /dev/null +++ b/packages/server/src/services/Sales/Invoices/GetSaleInvoiceMailStateTransformer.ts @@ -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', + ]; + }; +} diff --git a/packages/server/src/services/Sales/Invoices/SaleInvoicePdf.ts b/packages/server/src/services/Sales/Invoices/SaleInvoicePdf.ts index b7c4e8e14..d2c78a8b4 100644 --- a/packages/server/src/services/Sales/Invoices/SaleInvoicePdf.ts +++ b/packages/server/src/services/Sales/Invoices/SaleInvoicePdf.ts @@ -33,7 +33,7 @@ export class SaleInvoicePdf { * Retrieve sale invoice pdf content. * @param {number} tenantId - Tenant Id. * @param {ISaleInvoice} saleInvoice - - * @returns {Promise} + * @returns {Promise<[Buffer, string]>} */ public async saleInvoicePdf( tenantId: number, diff --git a/packages/server/src/services/Sales/Invoices/SaleInvoicesApplication.ts b/packages/server/src/services/Sales/Invoices/SaleInvoicesApplication.ts index e647d8ef6..e04ce4439 100644 --- a/packages/server/src/services/Sales/Invoices/SaleInvoicesApplication.ts +++ b/packages/server/src/services/Sales/Invoices/SaleInvoicesApplication.ts @@ -11,6 +11,7 @@ import { ISystemUser, ITenantUser, InvoiceNotificationType, + SaleInvoiceMailState, SendInvoiceMailDTO, } from '@/interfaces'; import { Inject, Service } from 'typedi'; @@ -29,6 +30,8 @@ import { SendInvoiceMailReminder } from './SendSaleInvoiceMailReminder'; import { SendSaleInvoiceMail } from './SendSaleInvoiceMail'; import { GetSaleInvoiceMailReminder } from './GetSaleInvoiceMailReminder'; import { GetSaleInvoiceState } from './GetSaleInvoiceState'; +import { GetSaleInvoiceBrandTemplate } from './GetSaleInvoiceBrandTemplate'; +import { GetSaleInvoiceMailState } from './GetSaleInvoiceMailState'; @Service() export class SaleInvoiceApplication { @@ -72,7 +75,7 @@ export class SaleInvoiceApplication { private sendSaleInvoiceMailService: SendSaleInvoiceMail; @Inject() - private getSaleInvoiceReminderService: GetSaleInvoiceMailReminder; + private getSaleInvoiceMailStateService: GetSaleInvoiceMailState; @Inject() private getSaleInvoiceStateService: GetSaleInvoiceState; @@ -361,10 +364,10 @@ export class SaleInvoiceApplication { * Retrieves the default mail options of the given sale invoice. * @param {number} tenantId * @param {number} saleInvoiceid - * @returns {Promise} + * @returns {Promise} */ - public getSaleInvoiceMail(tenantId: number, saleInvoiceid: number) { - return this.sendSaleInvoiceMailService.getMailOption( + public getSaleInvoiceMailState(tenantId: number, saleInvoiceid: number) { + return this.getSaleInvoiceMailStateService.getInvoiceMailState( tenantId, saleInvoiceid ); diff --git a/packages/server/src/services/Sales/Invoices/SendInvoiceInvoiceMailCommon.ts b/packages/server/src/services/Sales/Invoices/SendInvoiceInvoiceMailCommon.ts index 52ef46a59..16ee5bd9f 100644 --- a/packages/server/src/services/Sales/Invoices/SendInvoiceInvoiceMailCommon.ts +++ b/packages/server/src/services/Sales/Invoices/SendInvoiceInvoiceMailCommon.ts @@ -7,6 +7,7 @@ import { DEFAULT_INVOICE_MAIL_CONTENT, DEFAULT_INVOICE_MAIL_SUBJECT, } from './constants'; +import { GetInvoicePaymentMail } from './GetInvoicePaymentMail'; @Service() export class SendSaleInvoiceMailCommon { @@ -19,6 +20,9 @@ export class SendSaleInvoiceMailCommon { @Inject() private contactMailNotification: ContactMailNotification; + @Inject() + private getInvoicePaymentMail: GetInvoicePaymentMail; + /** * Retrieves the mail options. * @param {number} tenantId - Tenant id. @@ -27,11 +31,11 @@ export class SendSaleInvoiceMailCommon { * @param {string} defaultBody - Subject body. * @returns {Promise} */ - public async getMailOption( + public async getInvoiceMailOptions( tenantId: number, invoiceId: number, defaultSubject: string = DEFAULT_INVOICE_MAIL_SUBJECT, - defaultBody: string = DEFAULT_INVOICE_MAIL_CONTENT + defaultMessage: string = DEFAULT_INVOICE_MAIL_CONTENT ): Promise { const { SaleInvoice } = this.tenancy.models(tenantId); @@ -39,21 +43,54 @@ export class SendSaleInvoiceMailCommon { .findById(invoiceId) .throwIfNotFound(); - const formatterData = await this.formatText(tenantId, invoiceId); + const contactMailDefaultOptions = + await this.contactMailNotification.getDefaultMailOptions( + tenantId, + saleInvoice.customerId + ); + const formatArgs = await this.getInvoiceFormatterArgs(tenantId, invoiceId); - const mailOptions = await this.contactMailNotification.getMailOptions( - tenantId, - saleInvoice.customerId, - defaultSubject, - defaultBody, - formatterData - ); return { - ...mailOptions, + ...contactMailDefaultOptions, + subject: defaultSubject, + message: defaultMessage, attachInvoice: true, + formatArgs, }; } + /** + * Formats the given invoice mail options. + * @param {number} tenantId + * @param {number} invoiceId + * @param {SaleInvoiceMailOptions} mailOptions + * @returns {Promise} + */ + public async formatInvoiceMailOptions( + tenantId: number, + invoiceId: number, + mailOptions: SaleInvoiceMailOptions + ): Promise { + 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. * @param {number} tenantId - Tenant id. @@ -61,7 +98,7 @@ export class SendSaleInvoiceMailCommon { * @param {string} text - The given text. * @returns {Promise} */ - public formatText = async ( + public getInvoiceFormatterArgs = async ( tenantId: number, invoiceId: number ): Promise> => { @@ -69,15 +106,18 @@ export class SendSaleInvoiceMailCommon { tenantId, invoiceId ); - + const commonArgs = await this.contactMailNotification.getCommonFormatArgs( + tenantId + ); return { - CustomerName: invoice.customer.displayName, - InvoiceNumber: invoice.invoiceNo, - InvoiceDueAmount: invoice.dueAmountFormatted, - InvoiceDueDate: invoice.dueDateFormatted, - InvoiceDate: invoice.invoiceDateFormatted, - InvoiceAmount: invoice.totalFormatted, - OverdueDays: invoice.overdueDays, + ...commonArgs, + ['Customer Name']: invoice.customer.displayName, + ['Invoice Number']: invoice.invoiceNo, + ['Invoice DueAmount']: invoice.dueAmountFormatted, + ['Invoice DueDate']: invoice.dueDateFormatted, + ['Invoice Date']: invoice.invoiceDateFormatted, + ['Invoice Amount']: invoice.totalFormatted, + ['Overdue Days']: invoice.overdueDays, }; }; } diff --git a/packages/server/src/services/Sales/Invoices/SendSaleInvoiceMail.ts b/packages/server/src/services/Sales/Invoices/SendSaleInvoiceMail.ts index eff8b2603..f95f68fb3 100644 --- a/packages/server/src/services/Sales/Invoices/SendSaleInvoiceMail.ts +++ b/packages/server/src/services/Sales/Invoices/SendSaleInvoiceMail.ts @@ -4,12 +4,11 @@ import { ISaleInvoiceMailSend, SendInvoiceMailDTO } from '@/interfaces'; import { SaleInvoicePdf } from './SaleInvoicePdf'; import { SendSaleInvoiceMailCommon } from './SendInvoiceInvoiceMailCommon'; import { - DEFAULT_INVOICE_MAIL_CONTENT, - DEFAULT_INVOICE_MAIL_SUBJECT, -} from './constants'; -import { parseAndValidateMailOptions } from '@/services/MailNotification/utils'; -import events from '@/subscribers/events'; + parseMailOptions, + validateRequiredMailOptions, +} from '@/services/MailNotification/utils'; import { EventPublisher } from '@/lib/EventPublisher/EventPublisher'; +import events from '@/subscribers/events'; @Service() export class SendSaleInvoiceMail { @@ -51,21 +50,6 @@ export class SendSaleInvoiceMail { } as ISaleInvoiceMailSend); } - /** - * Retrieves the mail options of the given sale invoice. - * @param {number} tenantId - * @param {number} saleInvoiceId - * @returns {Promise} - */ - public async getMailOption(tenantId: number, saleInvoiceId: number) { - return this.invoiceMail.getMailOption( - tenantId, - saleInvoiceId, - DEFAULT_INVOICE_MAIL_SUBJECT, - DEFAULT_INVOICE_MAIL_CONTENT - ); - } - /** * Triggers the mail invoice. * @param {number} tenantId @@ -78,44 +62,58 @@ export class SendSaleInvoiceMail { saleInvoiceId: number, messageOptions: SendInvoiceMailDTO ) { - const defaultMessageOpts = await this.getMailOption( + const defaultMessageOptions = await this.invoiceMail.getInvoiceMailOptions( tenantId, saleInvoiceId ); - // Merge message opts with default options and validate the incoming options. - const messageOpts = parseAndValidateMailOptions( - defaultMessageOpts, + // Merges message options with default options and parses the options values. + const parsedMessageOptions = parseMailOptions( + defaultMessageOptions, messageOptions ); - const mail = new Mail() - .setSubject(messageOpts.subject) - .setTo(messageOpts.to) - .setContent(messageOpts.body); + // Validates the required mail options. + validateRequiredMailOptions(parsedMessageOptions); - if (messageOpts.attachInvoice) { - // Retrieves document buffer of the invoice pdf document. - const invoicePdfBuffer = await this.invoicePdf.saleInvoicePdf( + const formattedMessageOptions = + await this.invoiceMail.formatInvoiceMailOptions( 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([ - { filename: 'invoice.pdf', content: invoicePdfBuffer }, + { filename: `${invoiceFilename}.pdf`, content: invoicePdfBuffer }, ]); } - // Triggers the event `onSaleInvoiceSend`. - await this.eventPublisher.emitAsync(events.saleInvoice.onMailSend, { + + const eventPayload = { tenantId, saleInvoiceId, messageOptions, - } as ISaleInvoiceMailSend); + formattedMessageOptions, + } as ISaleInvoiceMailSend; + // Triggers the event `onSaleInvoiceSend`. + await this.eventPublisher.emitAsync( + events.saleInvoice.onMailSend, + eventPayload + ); await mail.send(); // Triggers the event `onSaleInvoiceSend`. - await this.eventPublisher.emitAsync(events.saleInvoice.onMailSent, { - tenantId, - saleInvoiceId, - messageOptions, - } as ISaleInvoiceMailSend); + await this.eventPublisher.emitAsync( + events.saleInvoice.onMailSent, + eventPayload + ); } } diff --git a/packages/server/src/services/Sales/Invoices/constants.ts b/packages/server/src/services/Sales/Invoices/constants.ts index 4a67b6780..cff791b3c 100644 --- a/packages/server/src/services/Sales/Invoices/constants.ts +++ b/packages/server/src/services/Sales/Invoices/constants.ts @@ -1,19 +1,19 @@ import config from '@/config'; export const DEFAULT_INVOICE_MAIL_SUBJECT = - 'Invoice {InvoiceNumber} from {CompanyName}'; + 'Invoice {Invoice Number} from {Company Name}'; export const DEFAULT_INVOICE_MAIL_CONTENT = ` -

Dear {CustomerName}

+

Dear {Customer Name}

Thank you for your business, You can view or print your invoice from attachements.

-Invoice #{InvoiceNumber}
-Due Date : {InvoiceDueDate}
-Amount : {InvoiceAmount}
+Invoice #{Invoice Number}
+Due Date : {Invoice Due Date}
+Amount : {Invoice Amount}

Regards
-{CompanyName} +{Company Name}

`; diff --git a/packages/server/src/utils/index.ts b/packages/server/src/utils/index.ts index 9edf85b44..aaf236eb5 100644 --- a/packages/server/src/utils/index.ts +++ b/packages/server/src/utils/index.ts @@ -406,7 +406,7 @@ export const runningAmount = (amount: number) => { }; }; -export const formatSmsMessage = (message, args) => { +export const formatSmsMessage = (message: string, args) => { let formattedMessage = message; Object.keys(args).forEach((key) => { diff --git a/packages/webapp/package.json b/packages/webapp/package.json index effb13ba1..a315192d1 100644 --- a/packages/webapp/package.json +++ b/packages/webapp/package.json @@ -37,9 +37,9 @@ "@types/lodash": "^4.14.172", "@types/node": "^14.14.9", "@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-dom": "^16.9.16", "@types/react-helmet": "^6.1.11", "@types/react-redux": "^7.1.24", "@types/react-router-dom": "^5.3.3", @@ -79,6 +79,7 @@ "path-browserify": "^1.0.1", "plaid": "^9.3.0", "plaid-threads": "^11.4.3", + "polished": "^4.3.1", "prop-types": "15.8.1", "query-string": "^7.1.1", "ramda": "^0.27.1", diff --git a/packages/webapp/src/components/DialogsContainer.tsx b/packages/webapp/src/components/DialogsContainer.tsx index cd4a1b9ef..3533cc6bc 100644 --- a/packages/webapp/src/components/DialogsContainer.tsx +++ b/packages/webapp/src/components/DialogsContainer.tsx @@ -45,7 +45,6 @@ import ProjectBillableEntriesFormDialog from '@/containers/Projects/containers/P import TaxRateFormDialog from '@/containers/TaxRates/dialogs/TaxRateFormDialog/TaxRateFormDialog'; import { DialogsName } from '@/constants/dialogs'; 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 ReceiptMailDialog from '@/containers/Sales/Receipts/ReceiptMailDialog/ReceiptMailDialog'; import PaymentMailDialog from '@/containers/Sales/PaymentsReceived/PaymentMailDialog/PaymentMailDialog'; @@ -144,7 +143,6 @@ export default function DialogsContainer() { - diff --git a/packages/webapp/src/components/DrawersContainer.tsx b/packages/webapp/src/components/DrawersContainer.tsx index 89c1a13e5..448f6e676 100644 --- a/packages/webapp/src/components/DrawersContainer.tsx +++ b/packages/webapp/src/components/DrawersContainer.tsx @@ -31,6 +31,7 @@ import { PaymentReceivedCustomizeDrawer } from '@/containers/Sales/PaymentsRecei import { BrandingTemplatesDrawer } from '@/containers/BrandingTemplates/BrandingTemplatesDrawer'; import { DRAWERS } from '@/constants/drawers'; +import { InvoiceSendMailDrawer } from '@/containers/Sales/Invoices/InvoiceSendMailDrawer/InvoiceSendMailDrawer'; /** * Drawers container of the dashboard. @@ -79,6 +80,7 @@ export default function DrawersContainer() { name={DRAWERS.PAYMENT_RECEIVED_CUSTOMIZE} /> + ); } diff --git a/packages/webapp/src/constants/drawers.ts b/packages/webapp/src/constants/drawers.ts index 4c97ef783..cd1d064d4 100644 --- a/packages/webapp/src/constants/drawers.ts +++ b/packages/webapp/src/constants/drawers.ts @@ -33,5 +33,6 @@ export enum DRAWERS { PAYMENT_RECEIVED_CUSTOMIZE = 'PAYMENT_RECEIVED_CUSTOMIZE', BRANDING_TEMPLATES = 'BRANDING_TEMPLATES', 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' } diff --git a/packages/webapp/src/containers/BrandingTemplates/BrandingTemplatesDrawer.tsx b/packages/webapp/src/containers/BrandingTemplates/BrandingTemplatesDrawer.tsx index b4b9679ca..685e7cadb 100644 --- a/packages/webapp/src/containers/BrandingTemplates/BrandingTemplatesDrawer.tsx +++ b/packages/webapp/src/containers/BrandingTemplates/BrandingTemplatesDrawer.tsx @@ -23,8 +23,6 @@ function BrandingTemplatesDrawerRoot({ isOpen={isOpen} name={name} payload={payload} - size={'600px'} - style={{ borderLeftColor: '#cbcbcb' }} > diff --git a/packages/webapp/src/containers/ElementCustomize/ElementCustomizeFields.tsx b/packages/webapp/src/containers/ElementCustomize/ElementCustomizeFields.tsx index 2e0ec0c94..76abd88c1 100644 --- a/packages/webapp/src/containers/ElementCustomize/ElementCustomizeFields.tsx +++ b/packages/webapp/src/containers/ElementCustomize/ElementCustomizeFields.tsx @@ -37,8 +37,10 @@ export function ElementCustomizeFieldsMain() { - - {CustomizeTabPanel} + + + {CustomizeTabPanel} + diff --git a/packages/webapp/src/containers/ElementCustomize/ElementCustomizePreviewContent.tsx b/packages/webapp/src/containers/ElementCustomize/ElementCustomizePreviewContent.tsx index 6f643141d..134f74bba 100644 --- a/packages/webapp/src/containers/ElementCustomize/ElementCustomizePreviewContent.tsx +++ b/packages/webapp/src/containers/ElementCustomize/ElementCustomizePreviewContent.tsx @@ -1,19 +1,12 @@ -import { Box } from '@/components'; +import { Box, Stack } from '@/components'; import { useElementCustomizeContext } from './ElementCustomizeProvider'; export function ElementCustomizePreviewContent() { const { PaperTemplate } = useElementCustomizeContext(); return ( - + {PaperTemplate} - + ); } diff --git a/packages/webapp/src/containers/PaymentPortal/InvoicePaymentPagePreview.tsx b/packages/webapp/src/containers/PaymentPortal/InvoicePaymentPagePreview.tsx new file mode 100644 index 000000000..1802db82c --- /dev/null +++ b/packages/webapp/src/containers/PaymentPortal/InvoicePaymentPagePreview.tsx @@ -0,0 +1,24 @@ +import { InvoicePaymentPage, PaymentPageProps } from './PaymentPage'; + +export interface InvoicePaymentPagePreviewProps + extends Partial { } + +export function InvoicePaymentPagePreview( + props: InvoicePaymentPagePreviewProps, +) { + return ( + + ); +} diff --git a/packages/webapp/src/containers/PaymentPortal/PaymentPage.tsx b/packages/webapp/src/containers/PaymentPortal/PaymentPage.tsx new file mode 100644 index 000000000..64f57a93c --- /dev/null +++ b/packages/webapp/src/containers/PaymentPortal/PaymentPage.tsx @@ -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; + + // # View invoice button + viewInvoiceLabel?: string; + viewInvoiceButtonProps?: Partial; + + // # Invoice number + invoiceNumber: string; + invoiceNumberLabel?: string; + + // # Pay button + showPayButton?: boolean; + payButtonLabel?: string; + payInvoiceButtonProps?: Partial; + + // # Buy note + buyNote?: string; + + // # Copyright + copyrightText?: string; + + classNames?: Record +} + +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.
All rights reserved.`, + + classNames, +}: PaymentPageProps) { + return ( + + + + + {companyLogoUri && ( + + )} + {organizationName} + + + +

+ {organizationName} Sent an Invoice for {total} +

+ + + Invoice due {dueDate}{' '} + + +
+ + + {customerName} + + {customerAddress && ( + + )} + + +

+ {invoiceNumberLabel} {invoiceNumber} +

+ + + + {subtotalLabel} + {subtotal} + + + + {totalLabel} + {total} + + {/* + {sharableLinkMeta?.taxes?.map((tax, key) => ( + + {tax?.name} + {tax?.taxRateAmountFormatted} + + ))} */} + + {paidAmountLabel} + {paidAmount} + + + + {dueAmountLabel} + {dueAmount} + + +
+ + + + + + + {showPayButton && ( + + )} + + + {buyNote && ( + + {buyNote} + + )} +
+ + + + + {copyrightText && ( + + )} + +
+ ); +} diff --git a/packages/webapp/src/containers/PaymentPortal/PaymentPortal.module.scss b/packages/webapp/src/containers/PaymentPortal/PaymentPortal.module.scss index a431cfcfe..9ad45640e 100644 --- a/packages/webapp/src/containers/PaymentPortal/PaymentPortal.module.scss +++ b/packages/webapp/src/containers/PaymentPortal/PaymentPortal.module.scss @@ -7,7 +7,7 @@ border-radius: 10px; box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.1); width: 600px; - margin: 40px auto; + // margin: 40px auto; color: #222; background-color: #fff; } @@ -28,6 +28,7 @@ font-weight: 500; color: #222; font-size: 26px; + line-height: 1.35; } .invoiceDueDate{ diff --git a/packages/webapp/src/containers/Preferences/PaymentMethods/drawers/StripeIntegrationEditBoot.tsx b/packages/webapp/src/containers/Preferences/PaymentMethods/drawers/StripeIntegrationEditBoot.tsx index 524c7c37e..2c1d2ff94 100644 --- a/packages/webapp/src/containers/Preferences/PaymentMethods/drawers/StripeIntegrationEditBoot.tsx +++ b/packages/webapp/src/containers/Preferences/PaymentMethods/drawers/StripeIntegrationEditBoot.tsx @@ -29,7 +29,13 @@ export const useStripeIntegrationEditBoot = () => { return context; }; -export const StripeIntegrationEditBoot: React.FC = ({ children }) => { +interface StripeIntegrationEditBootProps { + children: React.ReactNode; +} + +export const StripeIntegrationEditBoot: React.FC< + StripeIntegrationEditBootProps +> = ({ children }) => { const { payload: { stripePaymentMethodId }, } = useDrawerContext(); diff --git a/packages/webapp/src/containers/Sales/CreditNotes/CreditNoteCustomize/CreditNoteCustomizeDrawer.tsx b/packages/webapp/src/containers/Sales/CreditNotes/CreditNoteCustomize/CreditNoteCustomizeDrawer.tsx index 42159714e..863c8c4ef 100644 --- a/packages/webapp/src/containers/Sales/CreditNotes/CreditNoteCustomize/CreditNoteCustomizeDrawer.tsx +++ b/packages/webapp/src/containers/Sales/CreditNotes/CreditNoteCustomize/CreditNoteCustomizeDrawer.tsx @@ -19,7 +19,12 @@ function CreditNoteCustomizeDrawerRoot({ payload, }) { return ( - + diff --git a/packages/webapp/src/containers/Sales/Estimates/EstimateCustomize/EstimateCustomizeDrawer.tsx b/packages/webapp/src/containers/Sales/Estimates/EstimateCustomize/EstimateCustomizeDrawer.tsx index 35bd969ea..66d0db268 100644 --- a/packages/webapp/src/containers/Sales/Estimates/EstimateCustomize/EstimateCustomizeDrawer.tsx +++ b/packages/webapp/src/containers/Sales/Estimates/EstimateCustomize/EstimateCustomizeDrawer.tsx @@ -20,7 +20,12 @@ function EstimateCustomizeDrawerRoot({ payload, }) { return ( - + diff --git a/packages/webapp/src/containers/Sales/Invoices/InvoiceCustomize/InvoiceCustomizeContent.tsx b/packages/webapp/src/containers/Sales/Invoices/InvoiceCustomize/InvoiceCustomizeContent.tsx index a6960ccff..479f5ed49 100644 --- a/packages/webapp/src/containers/Sales/Invoices/InvoiceCustomize/InvoiceCustomizeContent.tsx +++ b/packages/webapp/src/containers/Sales/Invoices/InvoiceCustomize/InvoiceCustomizeContent.tsx @@ -1,10 +1,5 @@ -import React from 'react'; -import * as R from 'ramda'; -import { useFormikContext } from 'formik'; -import { - InvoicePaperTemplate, - InvoicePaperTemplateProps, -} from './InvoicePaperTemplate'; +import { lazy, Suspense } from 'react'; +import { Spinner, Tab } from '@blueprintjs/core'; import { ElementCustomize, ElementCustomizeContent, @@ -16,9 +11,27 @@ import { InvoiceCustomizeSchema } from './InvoiceCustomizeForm.schema'; import { useDrawerContext } from '@/components/Drawer/DrawerProvider'; import { useDrawerActions } from '@/hooks/state'; import { BrandingTemplateForm } from '@/containers/BrandingTemplates/BrandingTemplateForm'; -import { useElementCustomizeContext } from '@/containers/ElementCustomize/ElementCustomizeProvider'; import { initialValues } from './constants'; 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. @@ -56,7 +69,39 @@ function InvoiceCustomizeFormContent() { return ( - + + }> + + + } + /> + }> + + + } + /> + }> + + + } + /> + @@ -73,28 +118,3 @@ function InvoiceCustomizeFormContent() { ); } - -/** - * Injects the `InvoicePaperTemplate` component props from the form and branding states. - * @param Component - * @returns {JSX.Element} - */ -const withInvoicePreviewTemplateProps =

( - Component: React.ComponentType

, -) => { - return (props: Omit) => { - const { values } = useFormikContext(); - const { brandingState } = useElementCustomizeContext(); - - const mergedProps: InvoicePaperTemplateProps = { - ...brandingState, - ...values, - }; - - return ; - }; -}; - -export const InvoicePaperTemplateFormConnected = R.compose( - withInvoicePreviewTemplateProps, -)(InvoicePaperTemplate); diff --git a/packages/webapp/src/containers/Sales/Invoices/InvoiceCustomize/InvoiceCustomizeDrawer.tsx b/packages/webapp/src/containers/Sales/Invoices/InvoiceCustomize/InvoiceCustomizeDrawer.tsx index 7c6ae6589..8ecf93b5a 100644 --- a/packages/webapp/src/containers/Sales/Invoices/InvoiceCustomize/InvoiceCustomizeDrawer.tsx +++ b/packages/webapp/src/containers/Sales/Invoices/InvoiceCustomize/InvoiceCustomizeDrawer.tsx @@ -17,7 +17,12 @@ function InvoiceCustomizeDrawerRoot({ payload, }) { return ( - + diff --git a/packages/webapp/src/containers/Sales/Invoices/InvoiceCustomize/InvoiceCustomizeGeneralFields.tsx b/packages/webapp/src/containers/Sales/Invoices/InvoiceCustomize/InvoiceCustomizeGeneralFields.tsx index a6a8d46f2..ea53835ec 100644 --- a/packages/webapp/src/containers/Sales/Invoices/InvoiceCustomize/InvoiceCustomizeGeneralFields.tsx +++ b/packages/webapp/src/containers/Sales/Invoices/InvoiceCustomize/InvoiceCustomizeGeneralFields.tsx @@ -1,5 +1,6 @@ // @ts-nocheck import { Classes, Text } from '@blueprintjs/core'; +import { Link } from 'react-router-dom'; import { FFormGroup, FieldRequiredHint, @@ -13,7 +14,6 @@ import { CreditCardIcon } from '@/icons/CreditCardIcon'; import { Overlay } from './Overlay'; import { useIsTemplateNamedFilled } from '@/containers/BrandingTemplates/utils'; import { BrandingCompanyLogoUploadField } from '@/containers/ElementCustomize/components/BrandingCompanyLogoUploadField'; -import { Link } from 'react-router-dom'; import { MANAGE_LINK_URL } from './constants'; import { useDrawerContext } from '@/components/Drawer/DrawerProvider'; import { useDrawerActions } from '@/hooks/state'; diff --git a/packages/webapp/src/containers/Sales/Invoices/InvoiceCustomize/InvoiceCustomizeMailReceiptPreview.tsx b/packages/webapp/src/containers/Sales/Invoices/InvoiceCustomize/InvoiceCustomizeMailReceiptPreview.tsx new file mode 100644 index 000000000..3bafdb5a7 --- /dev/null +++ b/packages/webapp/src/containers/Sales/Invoices/InvoiceCustomize/InvoiceCustomizeMailReceiptPreview.tsx @@ -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 =

( + Component: React.ComponentType

, +) => { + return (props: Omit) => { + const { values } = useFormikContext(); + const { brandingState } = useElementCustomizeContext(); + + const mergedBrandingState = { + ...brandingState, + ...values, + }; + const mergedProps: InvoicePaymentPagePreviewProps = { + companyLogoUri: mergedBrandingState?.companyLogoUri, + primaryColor: mergedBrandingState?.primaryColor, + // organizationAddress: mergedBrandingState, + }; + return ( + + + + ); + }; +}; + +export const InvoiceCustomizeMailReceiptPreview = R.compose( + withInvoiceMailReceiptPreviewConnected, +)(InvoiceMailReceiptPreview); diff --git a/packages/webapp/src/containers/Sales/Invoices/InvoiceCustomize/InvoiceCustomizePaymentPreview.tsx b/packages/webapp/src/containers/Sales/Invoices/InvoiceCustomize/InvoiceCustomizePaymentPreview.tsx new file mode 100644 index 000000000..46e510435 --- /dev/null +++ b/packages/webapp/src/containers/Sales/Invoices/InvoiceCustomize/InvoiceCustomizePaymentPreview.tsx @@ -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 =

( + Component: React.ComponentType

, +) => { + return (props: Omit) => { + const { values } = useFormikContext(); + const { brandingState } = useElementCustomizeContext(); + + const mergedBrandingState = { + ...brandingState, + ...values, + }; + const mergedProps: InvoicePaymentPagePreviewProps = { + companyLogoUri: mergedBrandingState?.companyLogoUri, + primaryColor: mergedBrandingState?.primaryColor, + }; + return ( + + + + ); + }; +}; + +export const InvoiceCustomizePaymentPreview = R.compose( + withInvoicePaymentPreviewPageProps, +)(InvoicePaymentPagePreview); diff --git a/packages/webapp/src/containers/Sales/Invoices/InvoiceCustomize/InvoiceCustomizePdfPreview.tsx b/packages/webapp/src/containers/Sales/Invoices/InvoiceCustomize/InvoiceCustomizePdfPreview.tsx new file mode 100644 index 000000000..44ea38af6 --- /dev/null +++ b/packages/webapp/src/containers/Sales/Invoices/InvoiceCustomize/InvoiceCustomizePdfPreview.tsx @@ -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

} Component + * @returns {JSX.Element} + */ +const withInvoicePreviewTemplateProps =

( + Component: React.ComponentType

, +) => { + return (props: Omit) => { + const { values } = useFormikContext(); + const { brandingState } = useElementCustomizeContext(); + + const mergedProps: InvoicePaperTemplateProps = { + ...brandingState, + ...values, + }; + return ( + + + + ); + }; +}; + +export const InvoiceCustomizePdfPreview = R.compose( + withInvoicePreviewTemplateProps, +)(InvoicePaperTemplate); diff --git a/packages/webapp/src/containers/Sales/Invoices/InvoiceCustomize/InvoiceCustomizeTabs.tsx b/packages/webapp/src/containers/Sales/Invoices/InvoiceCustomize/InvoiceCustomizeTabs.tsx new file mode 100644 index 000000000..7fddc6a48 --- /dev/null +++ b/packages/webapp/src/containers/Sales/Invoices/InvoiceCustomize/InvoiceCustomizeTabs.tsx @@ -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 ( + + ); +} diff --git a/packages/webapp/src/containers/Sales/Invoices/InvoiceCustomize/InvoiceMailReceipt.tsx b/packages/webapp/src/containers/Sales/Invoices/InvoiceCustomize/InvoiceMailReceipt.tsx new file mode 100644 index 000000000..02801698a --- /dev/null +++ b/packages/webapp/src/containers/Sales/Invoices/InvoiceCustomize/InvoiceMailReceipt.tsx @@ -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 ( + + + {companyLogoUri && ( + + )} + + + {companyName} + + + + {total} + + + + {invoiceNumberLabel} {invoiceNumber} + + + + {dueDateLabel} {dueDate} + + + + + + {message} + + + {showViewInvoiceButton && ( + + )} + + + {items?.map((item, key) => ( + + {item.label} + + {item.quantity} x {item.total} + + + ))} + + + {totalLabel} + + {total} + + + + + {dueAmountLabel} + + {dueAmount} + + + + + ); +} diff --git a/packages/webapp/src/containers/Sales/Invoices/InvoiceCustomize/InvoiceMailReceiptPreview.tsx b/packages/webapp/src/containers/Sales/Invoices/InvoiceCustomize/InvoiceMailReceiptPreview.tsx new file mode 100644 index 000000000..fa72e64a8 --- /dev/null +++ b/packages/webapp/src/containers/Sales/Invoices/InvoiceCustomize/InvoiceMailReceiptPreview.tsx @@ -0,0 +1,38 @@ +import { + InvoiceMailReceipt, + InvoiceMailReceiptProps, +} from './InvoiceMailReceipt'; + +export interface InvoiceMailReceiptPreviewProps + extends Partial {} + +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 ; +} diff --git a/packages/webapp/src/containers/Sales/Invoices/InvoiceCustomize/InvoicePaperTemplate.tsx b/packages/webapp/src/containers/Sales/Invoices/InvoiceCustomize/InvoicePaperTemplate.tsx index ea2883658..4f072fc67 100644 --- a/packages/webapp/src/containers/Sales/Invoices/InvoiceCustomize/InvoicePaperTemplate.tsx +++ b/packages/webapp/src/containers/Sales/Invoices/InvoiceCustomize/InvoicePaperTemplate.tsx @@ -1,5 +1,9 @@ import { Classes, Text } from '@blueprintjs/core'; -import { PaperTemplate, PaperTemplateTotalBorder } from './PaperTemplate'; +import { + PaperTemplate, + PaperTemplateProps, + PaperTemplateTotalBorder, +} from './PaperTemplate'; import { Box, Group, Stack } from '@/components'; import { DefaultPdfTemplateTerms, @@ -23,7 +27,7 @@ interface PaperTax { amount: string; } -export interface InvoicePaperTemplateProps { +export interface InvoicePaperTemplateProps extends PaperTemplateProps { primaryColor?: string; secondaryColor?: string; @@ -177,9 +181,14 @@ export function InvoicePaperTemplate({ statementLabel = 'Statement', showStatement = true, statement = DefaultPdfTemplateStatement, + ...props }: InvoicePaperTemplateProps) { return ( - + diff --git a/packages/webapp/src/containers/Sales/Invoices/InvoiceCustomize/PaperTemplate.tsx b/packages/webapp/src/containers/Sales/Invoices/InvoiceCustomize/PaperTemplate.tsx index 3e6465851..7948d6727 100644 --- a/packages/webapp/src/containers/Sales/Invoices/InvoiceCustomize/PaperTemplate.tsx +++ b/packages/webapp/src/containers/Sales/Invoices/InvoiceCustomize/PaperTemplate.tsx @@ -1,10 +1,10 @@ import React from 'react'; import clsx from 'classnames'; import { get, isFunction } from 'lodash'; -import { Box, Group, GroupProps } from '@/components'; +import { Box, BoxProps, Group, GroupProps } from '@/components'; import styles from './InvoicePaperTemplate.module.scss'; -export interface PaperTemplateProps { +export interface PaperTemplateProps extends BoxProps { primaryColor?: string; secondaryColor?: string; children?: React.ReactNode; @@ -14,13 +14,13 @@ export function PaperTemplate({ primaryColor, secondaryColor, children, + ...restProps }: PaperTemplateProps) { return ( -

+ - {children} -
+ ); } @@ -118,9 +118,9 @@ PaperTemplate.TotalLine = ({ ); }; -PaperTemplate.MutedText = () => { }; +PaperTemplate.MutedText = () => {}; -PaperTemplate.Text = () => { }; +PaperTemplate.Text = () => {}; PaperTemplate.AddressesGroup = (props: GroupProps) => { return ( diff --git a/packages/webapp/src/containers/Sales/Invoices/InvoiceForm/Dialogs/InvoiceFormMailDeliverDialog/InvoiceFormMailDeliverDialog.tsx b/packages/webapp/src/containers/Sales/Invoices/InvoiceForm/Dialogs/InvoiceFormMailDeliverDialog/InvoiceFormMailDeliverDialog.tsx deleted file mode 100644 index f6ceb38f8..000000000 --- a/packages/webapp/src/containers/Sales/Invoices/InvoiceForm/Dialogs/InvoiceFormMailDeliverDialog/InvoiceFormMailDeliverDialog.tsx +++ /dev/null @@ -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 ( - - - - - - ); -} - -export default compose(withDialogRedux())(InvoiceFormMailDeliverDialog); diff --git a/packages/webapp/src/containers/Sales/Invoices/InvoiceForm/Dialogs/InvoiceFormMailDeliverDialog/InvoiceFormMailDeliverDialogContent.tsx b/packages/webapp/src/containers/Sales/Invoices/InvoiceForm/Dialogs/InvoiceFormMailDeliverDialog/InvoiceFormMailDeliverDialogContent.tsx deleted file mode 100644 index 8ce5e7c12..000000000 --- a/packages/webapp/src/containers/Sales/Invoices/InvoiceForm/Dialogs/InvoiceFormMailDeliverDialog/InvoiceFormMailDeliverDialogContent.tsx +++ /dev/null @@ -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 ( - - ); -} - -export default R.compose(withDialogActions)( - InvoiceFormDeliverDialogContentRoot, -); diff --git a/packages/webapp/src/containers/Sales/Invoices/InvoiceForm/InvoiceFormDialogs.tsx b/packages/webapp/src/containers/Sales/Invoices/InvoiceForm/InvoiceFormDialogs.tsx index fca6a8bcb..aa29b564c 100644 --- a/packages/webapp/src/containers/Sales/Invoices/InvoiceForm/InvoiceFormDialogs.tsx +++ b/packages/webapp/src/containers/Sales/Invoices/InvoiceForm/InvoiceFormDialogs.tsx @@ -2,7 +2,6 @@ import { useFormikContext } from 'formik'; import InvoiceNumberDialog from '@/containers/Dialogs/InvoiceNumberDialog'; import { DialogsName } from '@/constants/dialogs'; -import InvoiceFormMailDeliverDialog from './Dialogs/InvoiceFormMailDeliverDialog/InvoiceFormMailDeliverDialog'; /** * Invoice form dialogs. @@ -28,9 +27,6 @@ export default function InvoiceFormDialogs() { dialogName={DialogsName.InvoiceNumberSettings} onConfirm={handleInvoiceNumberFormConfirm} /> - ); } diff --git a/packages/webapp/src/containers/Sales/Invoices/InvoiceMailDialog/InvoiceMailDialog.tsx b/packages/webapp/src/containers/Sales/Invoices/InvoiceMailDialog/InvoiceMailDialog.tsx deleted file mode 100644 index 02c629e7c..000000000 --- a/packages/webapp/src/containers/Sales/Invoices/InvoiceMailDialog/InvoiceMailDialog.tsx +++ /dev/null @@ -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 ( - - - - - - ); -} -export default compose(withDialogRedux())(InvoiceMailDialog); diff --git a/packages/webapp/src/containers/Sales/Invoices/InvoiceMailDialog/InvoiceMailDialogBody.tsx b/packages/webapp/src/containers/Sales/Invoices/InvoiceMailDialog/InvoiceMailDialogBody.tsx deleted file mode 100644 index 3728c60ce..000000000 --- a/packages/webapp/src/containers/Sales/Invoices/InvoiceMailDialog/InvoiceMailDialogBody.tsx +++ /dev/null @@ -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 ( - - ); -} - -export default R.compose(withDialogActions)(InvoiceMailDialogBodyRoot); diff --git a/packages/webapp/src/containers/Sales/Invoices/InvoiceMailDialog/InvoiceMailDialogBoot.tsx b/packages/webapp/src/containers/Sales/Invoices/InvoiceMailDialog/InvoiceMailDialogBoot.tsx deleted file mode 100644 index 8c7d5f7e2..000000000 --- a/packages/webapp/src/containers/Sales/Invoices/InvoiceMailDialog/InvoiceMailDialogBoot.tsx +++ /dev/null @@ -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(); - -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 ( - - - - ); -} - -const useInvoiceMailDialogBoot = () => - React.useContext(InvoiceMailDialagBoot); - -export { InvoiceMailDialogBoot, useInvoiceMailDialogBoot }; diff --git a/packages/webapp/src/containers/Sales/Invoices/InvoiceMailDialog/InvoiceMailDialogContent.tsx b/packages/webapp/src/containers/Sales/Invoices/InvoiceMailDialog/InvoiceMailDialogContent.tsx deleted file mode 100644 index dbecb34fc..000000000 --- a/packages/webapp/src/containers/Sales/Invoices/InvoiceMailDialog/InvoiceMailDialogContent.tsx +++ /dev/null @@ -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 ( - - - - ); -} diff --git a/packages/webapp/src/containers/Sales/Invoices/InvoiceMailDialog/InvoiceMailDialogForm.schema.ts b/packages/webapp/src/containers/Sales/Invoices/InvoiceMailDialog/InvoiceMailDialogForm.schema.ts deleted file mode 100644 index 1c365ac4a..000000000 --- a/packages/webapp/src/containers/Sales/Invoices/InvoiceMailDialog/InvoiceMailDialogForm.schema.ts +++ /dev/null @@ -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'), -}); diff --git a/packages/webapp/src/containers/Sales/Invoices/InvoiceMailDialog/InvoiceMailDialogForm.tsx b/packages/webapp/src/containers/Sales/Invoices/InvoiceMailDialog/InvoiceMailDialogForm.tsx deleted file mode 100644 index a91c03466..000000000 --- a/packages/webapp/src/containers/Sales/Invoices/InvoiceMailDialog/InvoiceMailDialogForm.tsx +++ /dev/null @@ -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 ( - - - - ); -} diff --git a/packages/webapp/src/containers/Sales/Invoices/InvoiceMailDialog/InvoiceMailDialogFormContent.tsx b/packages/webapp/src/containers/Sales/Invoices/InvoiceMailDialog/InvoiceMailDialogFormContent.tsx deleted file mode 100644 index 07e104027..000000000 --- a/packages/webapp/src/containers/Sales/Invoices/InvoiceMailDialog/InvoiceMailDialogFormContent.tsx +++ /dev/null @@ -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 ( -
-
- - - - -
- -
-
- - - -
-
-
- ); -} - -const AttachFormGroup = styled(FFormGroup)` - background: #f8f9fb; - margin-top: 0.6rem; - padding: 4px 14px; - border-radius: 5px; - border: 1px solid #dcdcdd; -`; diff --git a/packages/webapp/src/containers/Sales/Invoices/InvoiceMailDialog/index.ts b/packages/webapp/src/containers/Sales/Invoices/InvoiceMailDialog/index.ts deleted file mode 100644 index b40bce27b..000000000 --- a/packages/webapp/src/containers/Sales/Invoices/InvoiceMailDialog/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './InvoiceMailDialog'; -export * from './InvoiceMailDialogContent'; \ No newline at end of file diff --git a/packages/webapp/src/containers/Sales/Invoices/InvoiceSendMailDrawer/InvoiceMailReceiptPreviewConnected..tsx b/packages/webapp/src/containers/Sales/Invoices/InvoiceSendMailDrawer/InvoiceMailReceiptPreviewConnected..tsx new file mode 100644 index 000000000..b93d26696 --- /dev/null +++ b/packages/webapp/src/containers/Sales/Invoices/InvoiceSendMailDrawer/InvoiceMailReceiptPreviewConnected..tsx @@ -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 ( + + + + + + ); +} \ No newline at end of file diff --git a/packages/webapp/src/containers/Sales/Invoices/InvoiceSendMailDrawer/InvoiceSendMailContent.tsx b/packages/webapp/src/containers/Sales/Invoices/InvoiceSendMailDrawer/InvoiceSendMailContent.tsx new file mode 100644 index 000000000..c1cc3d7a6 --- /dev/null +++ b/packages/webapp/src/containers/Sales/Invoices/InvoiceSendMailDrawer/InvoiceSendMailContent.tsx @@ -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 ( + + + + + + + + + + + + + + + ); +} diff --git a/packages/webapp/src/containers/Sales/Invoices/InvoiceSendMailDrawer/InvoiceSendMailContentBoot.tsx b/packages/webapp/src/containers/Sales/Invoices/InvoiceSendMailDrawer/InvoiceSendMailContentBoot.tsx new file mode 100644 index 000000000..dab0e5269 --- /dev/null +++ b/packages/webapp/src/containers/Sales/Invoices/InvoiceSendMailDrawer/InvoiceSendMailContentBoot.tsx @@ -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({} 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 ; + } + const value = { + invoiceId, + + // # Invoice mail options + isInvoiceMailState, + invoiceMailState, + }; + + return ( + + {children} + + ); +}; +InvoiceSendMailBoot.displayName = 'InvoiceSendMailBoot'; + +export const useInvoiceSendMailBoot = () => { + return useContext( + InvoiceSendMailContentBootContext, + ); +}; diff --git a/packages/webapp/src/containers/Sales/Invoices/InvoiceSendMailDrawer/InvoiceSendMailDrawer.tsx b/packages/webapp/src/containers/Sales/Invoices/InvoiceSendMailDrawer/InvoiceSendMailDrawer.tsx new file mode 100644 index 000000000..237f37149 --- /dev/null +++ b/packages/webapp/src/containers/Sales/Invoices/InvoiceSendMailDrawer/InvoiceSendMailDrawer.tsx @@ -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 ( + + + + + + ); +} + +export const InvoiceSendMailDrawer = R.compose(withDrawers())( + InvoiceSendMailDrawerRoot, +); diff --git a/packages/webapp/src/containers/Sales/Invoices/InvoiceSendMailDrawer/InvoiceSendMailFields.tsx b/packages/webapp/src/containers/Sales/Invoices/InvoiceSendMailDrawer/InvoiceSendMailFields.tsx new file mode 100644 index 000000000..226b9c335 --- /dev/null +++ b/packages/webapp/src/containers/Sales/Invoices/InvoiceSendMailDrawer/InvoiceSendMailFields.tsx @@ -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 ( + + ); +}; + +// 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(false); + const [showBccField, setShowBccField] = useState(false); + const textareaRef = useRef(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(() => ( + + + + + + ), []); + + 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 ( + + + + + + {showCCField && ( + + )} + {showBccField && ( + + )} + + + + + + + + + + + ( + + )} + fill={false} + fastField + /> + + + + + + + + + + + + + + ); +} + +function InvoiceSendMailFooter() { + const { isSubmitting } = useFormikContext(); + const { name } = useDrawerContext(); + const { closeDrawer } = useDrawerActions(); + + const handleClose = () => { + closeDrawer(name); + }; + + return ( + + + + + + + + ); +} diff --git a/packages/webapp/src/containers/Sales/Invoices/InvoiceSendMailDrawer/InvoiceSendMailForm.schema.ts b/packages/webapp/src/containers/Sales/Invoices/InvoiceSendMailDrawer/InvoiceSendMailForm.schema.ts new file mode 100644 index 000000000..c40d0d7a9 --- /dev/null +++ b/packages/webapp/src/containers/Sales/Invoices/InvoiceSendMailDrawer/InvoiceSendMailForm.schema.ts @@ -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')), +}); diff --git a/packages/webapp/src/containers/Sales/Invoices/InvoiceSendMailDrawer/InvoiceSendMailForm.tsx b/packages/webapp/src/containers/Sales/Invoices/InvoiceSendMailDrawer/InvoiceSendMailForm.tsx new file mode 100644 index 000000000..ad4722801 --- /dev/null +++ b/packages/webapp/src/containers/Sales/Invoices/InvoiceSendMailDrawer/InvoiceSendMailForm.tsx @@ -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, + ) => { + 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 ( + +
+ {children} +
+
+ ); +} diff --git a/packages/webapp/src/containers/Sales/Invoices/InvoiceSendMailDrawer/InvoiceSendMailHeader.tsx b/packages/webapp/src/containers/Sales/Invoices/InvoiceSendMailDrawer/InvoiceSendMailHeader.tsx new file mode 100644 index 000000000..25d3ab008 --- /dev/null +++ b/packages/webapp/src/containers/Sales/Invoices/InvoiceSendMailDrawer/InvoiceSendMailHeader.tsx @@ -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 ( + + {label && ( + + {label} + + )} + {closeButton && ( + + +
+ {items.map((item, index) => ( + + + {item.label} + + + + + {item.quantity} x {item.rate} + + + + ))} + + + + + {dueAmountLabel} + + + + + + {dueAmount} + + + + + + + {totalLabel} + + + + {total} + + +
+ + + + + + ); +}; + +export const renderInvoicePaymentEmail = (props: InvoicePaymentEmailProps) => { + return render(); +}; + +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', +}; diff --git a/shared/email-components/src/lib/main.ts b/shared/email-components/src/lib/main.ts new file mode 100644 index 000000000..aac012e31 --- /dev/null +++ b/shared/email-components/src/lib/main.ts @@ -0,0 +1 @@ +export * from './InvoicePaymentEmail'; diff --git a/shared/email-components/src/vite-env.d.ts b/shared/email-components/src/vite-env.d.ts new file mode 100644 index 000000000..11f02fe2a --- /dev/null +++ b/shared/email-components/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/shared/email-components/tailwind.config.js b/shared/email-components/tailwind.config.js new file mode 100644 index 000000000..6d07d1bee --- /dev/null +++ b/shared/email-components/tailwind.config.js @@ -0,0 +1,8 @@ +/** @type {import('tailwindcss').Config} */ +module.exports = { + content: ['./src/**/*.{ts,tsx}'], + theme: { + extend: {}, + }, + plugins: [], +}; diff --git a/shared/email-components/tsconfig.json b/shared/email-components/tsconfig.json new file mode 100644 index 000000000..8dbc658b3 --- /dev/null +++ b/shared/email-components/tsconfig.json @@ -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"] +} \ No newline at end of file diff --git a/shared/email-components/tsconfig.node.json b/shared/email-components/tsconfig.node.json new file mode 100644 index 000000000..9dad70185 --- /dev/null +++ b/shared/email-components/tsconfig.node.json @@ -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"] +} diff --git a/shared/email-components/vite.config.ts b/shared/email-components/vite.config.ts new file mode 100644 index 000000000..efb4f959f --- /dev/null +++ b/shared/email-components/vite.config.ts @@ -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 => { + /** + * 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;