From 53ab40a075d703ce982032b1f35d76307c19688e Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Sun, 17 Nov 2024 15:45:55 +0200 Subject: [PATCH] feat: estimate, receipt, credit note mail preview --- .../api/controllers/Sales/PaymentReceives.ts | 16 +- .../api/controllers/Sales/SalesEstimates.ts | 15 +- .../api/controllers/Sales/SalesReceipts.ts | 7 + .../Estimates/SaleEstimatesApplication.ts | 12 + .../Sales/Estimates/SaleEstimatesPdf.ts | 29 +- .../PaymentReceived/GetPaymentReceivedPdf.ts | 31 +- .../PaymentReceivedApplication.ts | 16 ++ .../Sales/Receipts/SaleReceiptApplication.ts | 13 + .../Sales/Receipts/SaleReceiptsPdfService.ts | 25 +- .../EstimateSendMail.schema.ts | 3 + .../EstimateSendMailBoot.tsx | 52 ++++ .../EstimateSendMailContent.tsx | 9 + .../EstimateSendMailDrawer.tsx | 42 +++ .../EstimateSendMailForm.tsx | 79 +++++ .../EstimateSendMailDrawer/_interfaces.ts | 1 + .../Estimates/EstimateSendMailDrawer/index.ts | 0 .../Sales/Estimates/EstimatesAlerts.tsx | 20 +- .../SendMailViewHeader.tsx} | 6 +- .../SendMailViewMessageField.tsx | 109 +++++++ .../SendMailViewPreview.tsx | 0 .../SendMailViewPreviewHeader.tsx | 76 +++++ .../SendMailViewPreviewPdfIframe.tsx | 27 ++ .../SendMailViewPreviewTabs.tsx | 47 +++ .../SendMailViewToAddressField.tsx | 209 ++++++++++++++ .../Estimates/SendMailViewDrawer/_types.ts | 1 + .../InvoiceSendMailContent.tsx | 4 +- .../InvoiceSendMailFields.tsx | 272 +----------------- .../InvoiceSendMailPreview.tsx | 75 ++--- .../InvoiceSendPdfPreviewConnected.tsx | 16 +- .../src/components/EstimatePaperTemplate.tsx | 247 ++++++++++++++++ .../PaymentReceivedPaperTemplate.tsx | 180 ++++++++++++ .../src/components/ReceiptPaperTemplate.tsx | 237 +++++++++++++++ shared/pdf-templates/src/index.ts | 7 + .../render-estimate-paper-template.tsx | 16 ++ .../renders/render-invoice-paper-template.tsx | 6 - ...render-payment-received-paper-template.tsx | 11 + .../renders/render-receipt-paper-template.tsx | 11 + 37 files changed, 1531 insertions(+), 396 deletions(-) create mode 100644 packages/webapp/src/containers/Sales/Estimates/EstimateSendMailDrawer/EstimateSendMail.schema.ts create mode 100644 packages/webapp/src/containers/Sales/Estimates/EstimateSendMailDrawer/EstimateSendMailBoot.tsx create mode 100644 packages/webapp/src/containers/Sales/Estimates/EstimateSendMailDrawer/EstimateSendMailContent.tsx create mode 100644 packages/webapp/src/containers/Sales/Estimates/EstimateSendMailDrawer/EstimateSendMailDrawer.tsx create mode 100644 packages/webapp/src/containers/Sales/Estimates/EstimateSendMailDrawer/EstimateSendMailForm.tsx create mode 100644 packages/webapp/src/containers/Sales/Estimates/EstimateSendMailDrawer/_interfaces.ts create mode 100644 packages/webapp/src/containers/Sales/Estimates/EstimateSendMailDrawer/index.ts rename packages/webapp/src/containers/Sales/{Invoices/InvoiceSendMailDrawer/InvoiceSendMailHeader.tsx => Estimates/SendMailViewDrawer/SendMailViewHeader.tsx} (91%) create mode 100644 packages/webapp/src/containers/Sales/Estimates/SendMailViewDrawer/SendMailViewMessageField.tsx create mode 100644 packages/webapp/src/containers/Sales/Estimates/SendMailViewDrawer/SendMailViewPreview.tsx create mode 100644 packages/webapp/src/containers/Sales/Estimates/SendMailViewDrawer/SendMailViewPreviewHeader.tsx create mode 100644 packages/webapp/src/containers/Sales/Estimates/SendMailViewDrawer/SendMailViewPreviewPdfIframe.tsx create mode 100644 packages/webapp/src/containers/Sales/Estimates/SendMailViewDrawer/SendMailViewPreviewTabs.tsx create mode 100644 packages/webapp/src/containers/Sales/Estimates/SendMailViewDrawer/SendMailViewToAddressField.tsx create mode 100644 packages/webapp/src/containers/Sales/Estimates/SendMailViewDrawer/_types.ts create mode 100644 shared/pdf-templates/src/components/EstimatePaperTemplate.tsx create mode 100644 shared/pdf-templates/src/components/PaymentReceivedPaperTemplate.tsx create mode 100644 shared/pdf-templates/src/components/ReceiptPaperTemplate.tsx create mode 100644 shared/pdf-templates/src/renders/render-estimate-paper-template.tsx create mode 100644 shared/pdf-templates/src/renders/render-payment-received-paper-template.tsx create mode 100644 shared/pdf-templates/src/renders/render-receipt-paper-template.tsx diff --git a/packages/server/src/api/controllers/Sales/PaymentReceives.ts b/packages/server/src/api/controllers/Sales/PaymentReceives.ts index b0f17bfac..5979d30f0 100644 --- a/packages/server/src/api/controllers/Sales/PaymentReceives.ts +++ b/packages/server/src/api/controllers/Sales/PaymentReceives.ts @@ -411,9 +411,8 @@ export default class PaymentReceivesController extends BaseController { const { tenantId } = req; try { - const data = await this.paymentReceiveApplication.getPaymentReceivedState( - tenantId - ); + const data = + await this.paymentReceiveApplication.getPaymentReceivedState(tenantId); return res.status(200).send({ data }); } catch (error) { next(error); @@ -471,7 +470,7 @@ export default class PaymentReceivesController extends BaseController { ACCEPT_TYPE.APPLICATION_JSON, ACCEPT_TYPE.APPLICATION_PDF, ]); - // Response in pdf format. + // Responses pdf format. if (ACCEPT_TYPE.APPLICATION_PDF === acceptType) { const [pdfContent, filename] = await this.paymentReceiveApplication.getPaymentReceivePdf( @@ -484,7 +483,14 @@ export default class PaymentReceivesController extends BaseController { 'Content-Disposition': `attachment; filename="${filename}"`, }); res.send(pdfContent); - // Response in json format. + // Responses html format. + } else if (ACCEPT_TYPE.APPLICATION_TEXT_HTML === acceptType) { + const htmlContent = this.paymentReceiveApplication.getPaymentReceivedHtml( + tenantId, + paymentReceiveId + ); + return res.status(200).send({ htmlContent }); + // Responses json format. } else { const paymentReceive = await this.paymentReceiveApplication.getPaymentReceive( diff --git a/packages/server/src/api/controllers/Sales/SalesEstimates.ts b/packages/server/src/api/controllers/Sales/SalesEstimates.ts index 5a2a80673..68f7e593e 100644 --- a/packages/server/src/api/controllers/Sales/SalesEstimates.ts +++ b/packages/server/src/api/controllers/Sales/SalesEstimates.ts @@ -13,11 +13,8 @@ import DynamicListingService from '@/services/DynamicListing/DynamicListService' import { ServiceError } from '@/exceptions'; import CheckPolicies from '@/api/middleware/CheckPolicies'; import { SaleEstimatesApplication } from '@/services/Sales/Estimates/SaleEstimatesApplication'; +import { ACCEPT_TYPE } from '@/interfaces/Http'; -const ACCEPT_TYPE = { - APPLICATION_PDF: 'application/pdf', - APPLICATION_JSON: 'application/json', -}; @Service() export default class SalesEstimatesController extends BaseController { @Inject() @@ -395,6 +392,7 @@ export default class SalesEstimatesController extends BaseController { const acceptType = accept.types([ ACCEPT_TYPE.APPLICATION_JSON, ACCEPT_TYPE.APPLICATION_PDF, + ACCEPT_TYPE.APPLICATION_TEXT_HTML, ]); // Retrieves estimate in pdf format. if (ACCEPT_TYPE.APPLICATION_PDF == acceptType) { @@ -410,7 +408,14 @@ export default class SalesEstimatesController extends BaseController { }); res.send(pdfContent); // Retrieves estimates in json format. - } else { + } else if (ACCEPT_TYPE.APPLICATION_TEXT_HTML === acceptType) { + const htmlContent = + await this.saleEstimatesApplication.getSaleEstimateHtml( + tenantId, + estimateId + ); + return res.status(200).send({ htmlContent }); + } else if (ACCEPT_TYPE.APPLICATION_JSON) { const estimate = await this.saleEstimatesApplication.getSaleEstimate( tenantId, estimateId diff --git a/packages/server/src/api/controllers/Sales/SalesReceipts.ts b/packages/server/src/api/controllers/Sales/SalesReceipts.ts index 6b7d6ef7a..3b185634f 100644 --- a/packages/server/src/api/controllers/Sales/SalesReceipts.ts +++ b/packages/server/src/api/controllers/Sales/SalesReceipts.ts @@ -353,6 +353,7 @@ export default class SalesReceiptsController extends BaseController { const acceptType = accept.types([ ACCEPT_TYPE.APPLICATION_JSON, ACCEPT_TYPE.APPLICATION_PDF, + ACCEPT_TYPE.APPLICATION_TEXT_HTML, ]); // Retrieves receipt in pdf format. if (ACCEPT_TYPE.APPLICATION_PDF == acceptType) { @@ -368,6 +369,12 @@ export default class SalesReceiptsController extends BaseController { }); res.send(pdfContent); // Retrieves receipt in json format. + } else if (ACCEPT_TYPE.APPLICATION_TEXT_HTML === acceptType) { + const htmlContent = await this.saleReceiptsApplication.getSaleReceiptHtml( + tenantId, + saleReceiptId + ); + res.send({ htmlContent }); } else { const saleReceipt = await this.saleReceiptsApplication.getSaleReceipt( tenantId, diff --git a/packages/server/src/services/Sales/Estimates/SaleEstimatesApplication.ts b/packages/server/src/services/Sales/Estimates/SaleEstimatesApplication.ts index f9972e282..233d40eac 100644 --- a/packages/server/src/services/Sales/Estimates/SaleEstimatesApplication.ts +++ b/packages/server/src/services/Sales/Estimates/SaleEstimatesApplication.ts @@ -220,6 +220,18 @@ export class SaleEstimatesApplication { ); } + /** + * Retrieve the HTML content of the given sale estimate. + * @param {number} tenantId + * @param {number} saleEstimateId + */ + public getSaleEstimateHtml(tenantId: number, saleEstimateId: number) { + return this.saleEstimatesPdfService.saleEstimateHtml( + tenantId, + saleEstimateId + ); + } + /** * Send the reminder mail of the given sale estimate. * @param {number} tenantId diff --git a/packages/server/src/services/Sales/Estimates/SaleEstimatesPdf.ts b/packages/server/src/services/Sales/Estimates/SaleEstimatesPdf.ts index f0750b43c..906da6de6 100644 --- a/packages/server/src/services/Sales/Estimates/SaleEstimatesPdf.ts +++ b/packages/server/src/services/Sales/Estimates/SaleEstimatesPdf.ts @@ -8,6 +8,7 @@ import { transformEstimateToPdfTemplate } from './utils'; import { EstimatePdfBrandingAttributes } from './constants'; import events from '@/subscribers/events'; import { EventPublisher } from '@/lib/EventPublisher/EventPublisher'; +import { renderEstimatePaperTemplateHtml } from '@bigcapital/pdf-templates'; @Service() export class SaleEstimatesPdf { @@ -29,6 +30,22 @@ export class SaleEstimatesPdf { @Inject() private eventPublisher: EventPublisher; + /** + * Retrieve sale estimate html content. + * @param {number} tenantId - + * @param {number} invoiceId - + */ + public async saleEstimateHtml( + tenantId: number, + estimateId: number + ): Promise { + const brandingAttributes = await this.getEstimateBrandingAttributes( + tenantId, + estimateId + ); + return renderEstimatePaperTemplateHtml({ ...brandingAttributes }); + } + /** * Retrieve sale invoice pdf content. * @param {number} tenantId - @@ -42,15 +59,9 @@ export class SaleEstimatesPdf { tenantId, saleEstimateId ); - const brandingAttributes = await this.getEstimateBrandingAttributes( - tenantId, - saleEstimateId - ); - const htmlContent = await this.templateInjectable.render( - tenantId, - 'modules/estimate-regular', - brandingAttributes - ); + // Retireves the sale estimate html. + const htmlContent = await this.saleEstimateHtml(tenantId, saleEstimateId); + const content = await this.chromiumlyTenancy.convertHtmlContent( tenantId, htmlContent diff --git a/packages/server/src/services/Sales/PaymentReceived/GetPaymentReceivedPdf.ts b/packages/server/src/services/Sales/PaymentReceived/GetPaymentReceivedPdf.ts index ed88b7ebf..36533961c 100644 --- a/packages/server/src/services/Sales/PaymentReceived/GetPaymentReceivedPdf.ts +++ b/packages/server/src/services/Sales/PaymentReceived/GetPaymentReceivedPdf.ts @@ -1,13 +1,13 @@ import { Inject, Service } from 'typedi'; +import { renderPaymentReceivedPaperTemplateHtml } from '@bigcapital/pdf-templates'; import { ChromiumlyTenancy } from '@/services/ChromiumlyTenancy/ChromiumlyTenancy'; -import { TemplateInjectable } from '@/services/TemplateInjectable/TemplateInjectable'; import { GetPaymentReceived } from './GetPaymentReceived'; import HasTenancyService from '@/services/Tenancy/TenancyService'; import { PaymentReceivedBrandingTemplate } from './PaymentReceivedBrandingTemplate'; import { transformPaymentReceivedToPdfTemplate } from './utils'; import { PaymentReceivedPdfTemplateAttributes } from '@/interfaces'; -import events from '@/subscribers/events'; import { EventPublisher } from '@/lib/EventPublisher/EventPublisher'; +import events from '@/subscribers/events'; @Service() export default class GetPaymentReceivedPdf { @@ -17,9 +17,6 @@ export default class GetPaymentReceivedPdf { @Inject() private chromiumlyTenancy: ChromiumlyTenancy; - @Inject() - private templateInjectable: TemplateInjectable; - @Inject() private getPaymentService: GetPaymentReceived; @@ -29,6 +26,23 @@ export default class GetPaymentReceivedPdf { @Inject() private eventPublisher: EventPublisher; + /** + * Retrieves payment received html content. + * @param {number} tenantId + * @param {number} paymentReceivedId + * @returns {Promise} + */ + public async getPaymentReceivedHtml( + tenantId: number, + paymentReceivedId: number + ): Promise { + const brandingAttributes = await this.getPaymentBrandingAttributes( + tenantId, + paymentReceivedId + ); + return renderPaymentReceivedPaperTemplateHtml(brandingAttributes); + } + /** * Retrieve sale invoice pdf content. * @param {number} tenantId - @@ -39,15 +53,10 @@ export default class GetPaymentReceivedPdf { tenantId: number, paymentReceivedId: number ): Promise<[Buffer, string]> { - const brandingAttributes = await this.getPaymentBrandingAttributes( + const htmlContent = await this.getPaymentReceivedHtml( tenantId, paymentReceivedId ); - const htmlContent = await this.templateInjectable.render( - tenantId, - 'modules/payment-receive-standard', - brandingAttributes - ); const filename = await this.getPaymentReceivedFilename( tenantId, paymentReceivedId diff --git a/packages/server/src/services/Sales/PaymentReceived/PaymentReceivedApplication.ts b/packages/server/src/services/Sales/PaymentReceived/PaymentReceivedApplication.ts index f88e784dd..95edd334b 100644 --- a/packages/server/src/services/Sales/PaymentReceived/PaymentReceivedApplication.ts +++ b/packages/server/src/services/Sales/PaymentReceived/PaymentReceivedApplication.ts @@ -228,6 +228,22 @@ export class PaymentReceivesApplication { ); }; + /** + * Retrieves the given payment receive html document. + * @param {number} tenantId + * @param {number} paymentReceiveId + * @returns {Promise} + */ + public getPaymentReceivedHtml = ( + tenantId: number, + paymentReceiveId: number + ) => { + return this.getPaymentReceivePdfService.getPaymentReceivedHtml( + tenantId, + paymentReceiveId + ); + }; + /** * Retrieves the create/edit initial state of the payment received. * @param {number} tenantId - The ID of the tenant. diff --git a/packages/server/src/services/Sales/Receipts/SaleReceiptApplication.ts b/packages/server/src/services/Sales/Receipts/SaleReceiptApplication.ts index b68f195f0..5d8a134b8 100644 --- a/packages/server/src/services/Sales/Receipts/SaleReceiptApplication.ts +++ b/packages/server/src/services/Sales/Receipts/SaleReceiptApplication.ts @@ -152,6 +152,19 @@ export class SaleReceiptApplication { ); } + /** + * Retrieves the given sale receipt html. + * @param {number} tenantId + * @param {number} saleReceiptId + * @returns {Promise} + */ + public getSaleReceiptHtml(tenantId: number, saleReceiptId: number) { + return this.getSaleReceiptPdfService.saleReceiptHtml( + tenantId, + saleReceiptId + ); + } + /** * Notify receipt customer by SMS of the given sale receipt. * @param {number} tenantId diff --git a/packages/server/src/services/Sales/Receipts/SaleReceiptsPdfService.ts b/packages/server/src/services/Sales/Receipts/SaleReceiptsPdfService.ts index 9c9c6132b..3c4e42060 100644 --- a/packages/server/src/services/Sales/Receipts/SaleReceiptsPdfService.ts +++ b/packages/server/src/services/Sales/Receipts/SaleReceiptsPdfService.ts @@ -8,6 +8,7 @@ import { transformReceiptToBrandingTemplateAttributes } from './utils'; import { ISaleReceiptBrandingTemplateAttributes } from '@/interfaces'; import { EventPublisher } from '@/lib/EventPublisher/EventPublisher'; import events from '@/subscribers/events'; +import { renderReceiptPaperTemplateHtml } from '@bigcapital/pdf-templates'; @Service() export class SaleReceiptsPdf { @@ -29,6 +30,19 @@ export class SaleReceiptsPdf { @Inject() private eventPublisher: EventPublisher; + /** + * Retrieves sale receipt html content. + * @param {number} tennatId + * @param {number} saleReceiptId + */ + public async saleReceiptHtml(tennatId: number, saleReceiptId: number) { + const brandingAttributes = await this.getReceiptBrandingAttributes( + tennatId, + saleReceiptId + ); + return renderReceiptPaperTemplateHtml(brandingAttributes); + } + /** * Retrieves sale invoice pdf content. * @param {number} tenantId - @@ -41,16 +55,9 @@ export class SaleReceiptsPdf { ): Promise<[Buffer, string]> { const filename = await this.getSaleReceiptFilename(tenantId, saleReceiptId); - const brandingAttributes = await this.getReceiptBrandingAttributes( - tenantId, - saleReceiptId - ); // Converts the receipt template to html content. - const htmlContent = await this.templateInjectable.render( - tenantId, - 'modules/receipt-regular', - brandingAttributes - ); + const htmlContent = await this.saleReceiptHtml(tenantId, saleReceiptId); + // Renders the html content to pdf document. const content = await this.chromiumlyTenancy.convertHtmlContent( tenantId, diff --git a/packages/webapp/src/containers/Sales/Estimates/EstimateSendMailDrawer/EstimateSendMail.schema.ts b/packages/webapp/src/containers/Sales/Estimates/EstimateSendMailDrawer/EstimateSendMail.schema.ts new file mode 100644 index 000000000..02134112c --- /dev/null +++ b/packages/webapp/src/containers/Sales/Estimates/EstimateSendMailDrawer/EstimateSendMail.schema.ts @@ -0,0 +1,3 @@ +import * as Yup from 'yup'; + +export const EstimateSendMailSchema = Yup.object().shape({}); diff --git a/packages/webapp/src/containers/Sales/Estimates/EstimateSendMailDrawer/EstimateSendMailBoot.tsx b/packages/webapp/src/containers/Sales/Estimates/EstimateSendMailDrawer/EstimateSendMailBoot.tsx new file mode 100644 index 000000000..5c1072ee5 --- /dev/null +++ b/packages/webapp/src/containers/Sales/Estimates/EstimateSendMailDrawer/EstimateSendMailBoot.tsx @@ -0,0 +1,52 @@ +import React, { createContext, useContext } from 'react'; +import { Spinner } from '@blueprintjs/core'; +import { useDrawerContext } from '@/components/Drawer/DrawerProvider'; + +interface EstimateSendMailBootValues { + estimateId: number; + + estimateMailState: GetSaleEstimateDefaultOptionsResponse | undefined; + isEstimateMailState: boolean; +} +interface EstimateSendMailBootProps { + children: React.ReactNode; +} + +const EstimateSendMailContentBootContext = + createContext({} as EstimateSendMailBootValues); + +export const EstimateSendMailBoot = ({ children }: EstimateSendMailBootProps) => { + const { + payload: { estimateId }, + } = useDrawerContext(); + + // Estimate mail options. + const { data: estimateMailState, isLoading: isEstimateMailState } = + useSaleEstimateMailState(estimateId); + + const isLoading = isEstimateMailState; + + if (isLoading) { + return ; + } + const value = { + estimateId, + + // # Estimate mail options + isEstimateMailState, + estimateMailState, + }; + + return ( + + {children} + + ); +}; +EstimateSendMailBoot.displayName = 'EstimateSendMailBoot'; + +export const useEstimateSendMailBoot = () => { + return useContext( + EstimateSendMailContentBootContext, + ); +}; diff --git a/packages/webapp/src/containers/Sales/Estimates/EstimateSendMailDrawer/EstimateSendMailContent.tsx b/packages/webapp/src/containers/Sales/Estimates/EstimateSendMailDrawer/EstimateSendMailContent.tsx new file mode 100644 index 000000000..ae30ab402 --- /dev/null +++ b/packages/webapp/src/containers/Sales/Estimates/EstimateSendMailDrawer/EstimateSendMailContent.tsx @@ -0,0 +1,9 @@ + + +export function EstimateSendMailContent() { + + return ( + + + ); +} \ No newline at end of file diff --git a/packages/webapp/src/containers/Sales/Estimates/EstimateSendMailDrawer/EstimateSendMailDrawer.tsx b/packages/webapp/src/containers/Sales/Estimates/EstimateSendMailDrawer/EstimateSendMailDrawer.tsx new file mode 100644 index 000000000..752d6b81f --- /dev/null +++ b/packages/webapp/src/containers/Sales/Estimates/EstimateSendMailDrawer/EstimateSendMailDrawer.tsx @@ -0,0 +1,42 @@ +// @ts-nocheck +import React from 'react'; +import * as R from 'ramda'; +import { Drawer, DrawerSuspense } from '@/components'; +import withDrawers from '@/containers/Drawer/withDrawers'; + +const EstimateSendMailDrawerProps = React.lazy(() => + import('./EstimateSendMailContent').then((module) => ({ + default: module.InvoiceSendMailContent, + })), +); + +interface EstimateSendMailDrawerProps { + name: string; + isOpen?: boolean; + payload?: any; +} + +function EstimateSendMailDrawerRoot({ + name, + + // #withDrawer + isOpen, + payload, +}: EstimateSendMailDrawerProps) { + return ( + + + + + + ); +} + +export const EstimateSendMailDrawer = R.compose(withDrawers())( + EstimateSendMailDrawerRoot, +); diff --git a/packages/webapp/src/containers/Sales/Estimates/EstimateSendMailDrawer/EstimateSendMailForm.tsx b/packages/webapp/src/containers/Sales/Estimates/EstimateSendMailDrawer/EstimateSendMailForm.tsx new file mode 100644 index 000000000..545fc9405 --- /dev/null +++ b/packages/webapp/src/containers/Sales/Estimates/EstimateSendMailDrawer/EstimateSendMailForm.tsx @@ -0,0 +1,79 @@ +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'; +import { useEstimateSendMailBoot } from './EstimateSendMailBoot'; + +const initialValues: InvoiceSendMailFormValues = { + subject: '', + message: '', + to: [], + cc: [], + bcc: [], + attachPdf: true, +}; + +interface InvoiceSendMailFormProps { + children: React.ReactNode; +} + +export function EstimateSendMailForm({ children }: InvoiceSendMailFormProps) { + const { mutateAsync: sendInvoiceMail } = useSendSaleInvoiceMail(); + const { estimateId, estimateMailState } = useEstimateSendMailBoot(); + + 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/Estimates/EstimateSendMailDrawer/_interfaces.ts b/packages/webapp/src/containers/Sales/Estimates/EstimateSendMailDrawer/_interfaces.ts new file mode 100644 index 000000000..b302170db --- /dev/null +++ b/packages/webapp/src/containers/Sales/Estimates/EstimateSendMailDrawer/_interfaces.ts @@ -0,0 +1 @@ +export interface EstimateSendMailFormValues {} diff --git a/packages/webapp/src/containers/Sales/Estimates/EstimateSendMailDrawer/index.ts b/packages/webapp/src/containers/Sales/Estimates/EstimateSendMailDrawer/index.ts new file mode 100644 index 000000000..e69de29bb diff --git a/packages/webapp/src/containers/Sales/Estimates/EstimatesAlerts.tsx b/packages/webapp/src/containers/Sales/Estimates/EstimatesAlerts.tsx index d5931793c..730ad3043 100644 --- a/packages/webapp/src/containers/Sales/Estimates/EstimatesAlerts.tsx +++ b/packages/webapp/src/containers/Sales/Estimates/EstimatesAlerts.tsx @@ -18,20 +18,8 @@ const EstimateRejectAlert = React.lazy( * Estimates alert. */ export default [ - { - name: 'estimate-delete', - component: EstimateDeleteAlert, - }, - { - name: 'estimate-deliver', - component: EstimateDeliveredAlert, - }, - { - name: 'estimate-Approve', - component: EstimateApproveAlert, - }, - { - name: 'estimate-reject', - component: EstimateRejectAlert, - }, + { name: 'estimate-delete', component: EstimateDeleteAlert }, + { name: 'estimate-deliver', component: EstimateDeliveredAlert }, + { name: 'estimate-Approve', component: EstimateApproveAlert }, + { name: 'estimate-reject', component: EstimateRejectAlert }, ]; diff --git a/packages/webapp/src/containers/Sales/Invoices/InvoiceSendMailDrawer/InvoiceSendMailHeader.tsx b/packages/webapp/src/containers/Sales/Estimates/SendMailViewDrawer/SendMailViewHeader.tsx similarity index 91% rename from packages/webapp/src/containers/Sales/Invoices/InvoiceSendMailDrawer/InvoiceSendMailHeader.tsx rename to packages/webapp/src/containers/Sales/Estimates/SendMailViewDrawer/SendMailViewHeader.tsx index 25d3ab008..98a9c6584 100644 --- a/packages/webapp/src/containers/Sales/Invoices/InvoiceSendMailDrawer/InvoiceSendMailHeader.tsx +++ b/packages/webapp/src/containers/Sales/Estimates/SendMailViewDrawer/SendMailViewHeader.tsx @@ -4,16 +4,16 @@ import { Group, Icon } from '@/components'; import { useDrawerContext } from '@/components/Drawer/DrawerProvider'; import { useDrawerActions } from '@/hooks/state'; -interface ElementCustomizeHeaderProps { +interface SendMailViewHeaderProps { label?: string; children?: React.ReactNode; closeButton?: boolean; } -export function InvoiceSendMailHeader({ +export function SendMailViewHeader({ label, closeButton = true, -}: ElementCustomizeHeaderProps) { +}: SendMailViewHeaderProps) { const { name } = useDrawerContext(); const { closeDrawer } = useDrawerActions(); diff --git a/packages/webapp/src/containers/Sales/Estimates/SendMailViewDrawer/SendMailViewMessageField.tsx b/packages/webapp/src/containers/Sales/Estimates/SendMailViewDrawer/SendMailViewMessageField.tsx new file mode 100644 index 000000000..f5226d427 --- /dev/null +++ b/packages/webapp/src/containers/Sales/Estimates/SendMailViewDrawer/SendMailViewMessageField.tsx @@ -0,0 +1,109 @@ +// @ts-nocheck +import { useFormikContext } from 'formik'; +import { Button, Icon, Position } from '@blueprintjs/core'; +import { SelectOptionProps } from '@blueprintjs-formik/select'; +import { FormGroupProps, TextAreaProps } from '@blueprintjs-formik/core'; +import { css } from '@emotion/css'; +import { FFormGroup, FSelect, FTextArea, Group, Stack } from '@/components'; +import { useCallback, useRef } from 'react'; +import { InvoiceSendMailFormValues } from '../../Invoices/InvoiceSendMailDrawer/_types'; + + +interface SendMailViewMessageFieldProps { + argsOptions?: Array; + formGroupProps?: Partial; + selectProps?: Partial; + textareaProps?: Partial; +} + +export function SendMailViewMessageField({ + argsOptions, + formGroupProps, + textareaProps, +}: SendMailViewMessageFieldProps) { + const textareaRef = useRef(null); + const { setFieldValue } = useFormikContext(); + + 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], + ); + + const handleTagInputKeyDown = (e: React.KeyboardEvent) => { + // Prevent the form from submitting when the user presses the Enter key + if (e.key === 'Enter') { + e.preventDefault(); + } + }; + + return ( + + + + ( + + )} + fill={false} + fastField + /> + + + + + + ); +} diff --git a/packages/webapp/src/containers/Sales/Estimates/SendMailViewDrawer/SendMailViewPreview.tsx b/packages/webapp/src/containers/Sales/Estimates/SendMailViewDrawer/SendMailViewPreview.tsx new file mode 100644 index 000000000..e69de29bb diff --git a/packages/webapp/src/containers/Sales/Estimates/SendMailViewDrawer/SendMailViewPreviewHeader.tsx b/packages/webapp/src/containers/Sales/Estimates/SendMailViewDrawer/SendMailViewPreviewHeader.tsx new file mode 100644 index 000000000..c33c9fb68 --- /dev/null +++ b/packages/webapp/src/containers/Sales/Estimates/SendMailViewDrawer/SendMailViewPreviewHeader.tsx @@ -0,0 +1,76 @@ +import { useMemo } from 'react'; +import { x } from '@xstyled/emotion'; +import { Box, Group, Stack } from '@/components'; + +interface SendViewPreviewHeaderProps { + companyName?: string; + customerName?: string; + subject: string; + from?: Array; + to?: Array; +} + +export function SendViewPreviewHeader({ + companyName, + subject, + customerName, + from, + to, +}: SendViewPreviewHeaderProps) { + const formatedFromAddresses = useMemo( + () => formatAddresses(from || []), + [from], + ); + const formattedToAddresses = useMemo(() => formatAddresses(to || []), [to]); + + return ( + + + + {subject} + + + + + + + A + + + + + {companyName} + {formatedFromAddresses} + + + + Send to: {customerName} {formattedToAddresses}; + + + + + + ); +} + +const formatAddresses = (addresses: Array) => + addresses?.map((email) => '<' + email + '>').join(' '); \ No newline at end of file diff --git a/packages/webapp/src/containers/Sales/Estimates/SendMailViewDrawer/SendMailViewPreviewPdfIframe.tsx b/packages/webapp/src/containers/Sales/Estimates/SendMailViewDrawer/SendMailViewPreviewPdfIframe.tsx new file mode 100644 index 000000000..441d46e10 --- /dev/null +++ b/packages/webapp/src/containers/Sales/Estimates/SendMailViewDrawer/SendMailViewPreviewPdfIframe.tsx @@ -0,0 +1,27 @@ +import { css } from '@emotion/css'; +import clsx from 'classnames'; + +interface SendMailViewPreviewPdfIframeProps + extends React.IframeHTMLAttributes {} + +export const SendMailViewPreviewPdfIframe = ({ + ...props +}: SendMailViewPreviewPdfIframeProps) => { + return ( +