From dbbaa387bdfcbd8f5651e468b88e25089f83610b Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Thu, 31 Oct 2024 12:40:48 +0200 Subject: [PATCH] feat: send invoice receipt preview --- .../api/controllers/Sales/SalesInvoices.ts | 8 +- packages/server/src/interfaces/SaleInvoice.ts | 24 ++++ .../Sales/Invoices/GetSaleInvoiceMailState.ts | 54 ++++++++ .../GetSaleInvoiceMailStateTransformer.ts | 104 ++++++++++++++++ .../Sales/Invoices/SaleInvoicesApplication.ts | 11 +- .../Sales/Invoices/SendSaleInvoiceMail.ts | 32 +---- .../src/components/DialogsContainer.tsx | 2 - .../InvoiceFormMailDeliverDialog.tsx | 39 ------ .../InvoiceFormMailDeliverDialogContent.tsx | 40 ------ .../InvoiceForm/InvoiceFormDialogs.tsx | 4 - .../InvoiceMailDialog/InvoiceMailDialog.tsx | 35 ------ .../InvoiceMailDialogBody.tsx | 36 ------ .../InvoiceMailDialogBoot.tsx | 48 -------- .../InvoiceMailDialogContent.tsx | 22 ---- .../InvoiceMailDialogForm.schema.ts | 9 -- .../InvoiceMailDialogForm.tsx | 69 ----------- .../InvoiceMailDialogFormContent.tsx | 66 ---------- .../Sales/Invoices/InvoiceMailDialog/index.ts | 2 - .../InvoiceMailReceiptPreviewConnected..tsx | 33 ++--- .../InvoiceSendMailContentBoot.tsx | 27 ++-- .../InvoiceSendMailForm.tsx | 5 +- .../InvoiceSendMailHeader.tsx | 1 + .../InvoiceSendMailHeaderPreview.tsx | 20 ++- .../InvoiceSendMailPreview.tsx | 28 ++++- .../InvoiceSendPdfPreviewConnected.tsx | 14 +-- .../Invoices/InvoiceSendMailDrawer/_hooks.ts | 10 +- packages/webapp/src/hooks/query/invoices.tsx | 116 ++++++++++++++++-- 27 files changed, 383 insertions(+), 476 deletions(-) create mode 100644 packages/server/src/services/Sales/Invoices/GetSaleInvoiceMailState.ts create mode 100644 packages/server/src/services/Sales/Invoices/GetSaleInvoiceMailStateTransformer.ts delete mode 100644 packages/webapp/src/containers/Sales/Invoices/InvoiceForm/Dialogs/InvoiceFormMailDeliverDialog/InvoiceFormMailDeliverDialog.tsx delete mode 100644 packages/webapp/src/containers/Sales/Invoices/InvoiceForm/Dialogs/InvoiceFormMailDeliverDialog/InvoiceFormMailDeliverDialogContent.tsx delete mode 100644 packages/webapp/src/containers/Sales/Invoices/InvoiceMailDialog/InvoiceMailDialog.tsx delete mode 100644 packages/webapp/src/containers/Sales/Invoices/InvoiceMailDialog/InvoiceMailDialogBody.tsx delete mode 100644 packages/webapp/src/containers/Sales/Invoices/InvoiceMailDialog/InvoiceMailDialogBoot.tsx delete mode 100644 packages/webapp/src/containers/Sales/Invoices/InvoiceMailDialog/InvoiceMailDialogContent.tsx delete mode 100644 packages/webapp/src/containers/Sales/Invoices/InvoiceMailDialog/InvoiceMailDialogForm.schema.ts delete mode 100644 packages/webapp/src/containers/Sales/Invoices/InvoiceMailDialog/InvoiceMailDialogForm.tsx delete mode 100644 packages/webapp/src/containers/Sales/Invoices/InvoiceMailDialog/InvoiceMailDialogFormContent.tsx delete mode 100644 packages/webapp/src/containers/Sales/Invoices/InvoiceMailDialog/index.ts diff --git a/packages/server/src/api/controllers/Sales/SalesInvoices.ts b/packages/server/src/api/controllers/Sales/SalesInvoices.ts index 55abcb6c0..c0c0bf54c 100644 --- a/packages/server/src/api/controllers/Sales/SalesInvoices.ts +++ b/packages/server/src/api/controllers/Sales/SalesInvoices.ts @@ -179,7 +179,7 @@ export default class SaleInvoicesController extends BaseController { '/:id/mail', [ ...this.specificSaleInvoiceValidation, - + body('subject').isString().optional({ nullable: true }), body('message').isString().optional({ nullable: true }), @@ -201,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)), @@ -789,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 @@ -803,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/interfaces/SaleInvoice.ts b/packages/server/src/interfaces/SaleInvoice.ts index f1eeb5256..01fc8bf26 100644 --- a/packages/server/src/interfaces/SaleInvoice.ts +++ b/packages/server/src/interfaces/SaleInvoice.ts @@ -238,6 +238,30 @@ export interface SaleInvoiceMailOptions extends CommonMailOptions { 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 { attachInvoice?: boolean; } 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/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/SendSaleInvoiceMail.ts b/packages/server/src/services/Sales/Invoices/SendSaleInvoiceMail.ts index f0c4f6e77..f95f68fb3 100644 --- a/packages/server/src/services/Sales/Invoices/SendSaleInvoiceMail.ts +++ b/packages/server/src/services/Sales/Invoices/SendSaleInvoiceMail.ts @@ -1,16 +1,8 @@ import { Inject, Service } from 'typedi'; import Mail from '@/lib/Mail'; -import { - ISaleInvoiceMailSend, - SaleInvoiceMailOptions, - SendInvoiceMailDTO, -} from '@/interfaces'; +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 { parseMailOptions, validateRequiredMailOptions, @@ -58,26 +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, - defaultSubject: string = DEFAULT_INVOICE_MAIL_SUBJECT, - defaultMessage: string = DEFAULT_INVOICE_MAIL_CONTENT - ): Promise { - return this.invoiceMail.getInvoiceMailOptions( - tenantId, - saleInvoiceId, - defaultSubject, - defaultMessage - ); - } - /** * Triggers the mail invoice. * @param {number} tenantId @@ -90,7 +62,7 @@ export class SendSaleInvoiceMail { saleInvoiceId: number, messageOptions: SendInvoiceMailDTO ) { - const defaultMessageOptions = await this.getMailOption( + const defaultMessageOptions = await this.invoiceMail.getInvoiceMailOptions( tenantId, saleInvoiceId ); 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/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 index dee0a83c3..b93d26696 100644 --- a/packages/webapp/src/containers/Sales/Invoices/InvoiceSendMailDrawer/InvoiceMailReceiptPreviewConnected..tsx +++ b/packages/webapp/src/containers/Sales/Invoices/InvoiceSendMailDrawer/InvoiceMailReceiptPreviewConnected..tsx @@ -1,33 +1,38 @@ -import { Box, Group, Stack } from '@/components'; -import { InvoiceMailReceiptPreview } from '../InvoiceCustomize/InvoiceMailReceiptPreview'; -import { css } from '@emotion/css'; -import { useInvoiceSendMailBoot } from './InvoiceSendMailContentBoot'; import { useMemo } from 'react'; -import { x } from '@xstyled/emotion'; +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 { invoice } = useInvoiceSendMailBoot(); const mailMessage = useSendInvoiceMailMessage(); + const { invoiceMailState } = useInvoiceSendMailBoot(); const items = useMemo( () => - invoice.entries.map((entry: any) => ({ + invoiceMailState?.entries?.map((entry: any) => ({ quantity: entry.quantity, - total: entry.rate_formatted, - label: entry.item.name, + total: entry.totalFormatted, + label: entry.name, })), - [invoice.entries], + [invoiceMailState?.entries], ); return ( ); -} +} \ No newline at end of file diff --git a/packages/webapp/src/containers/Sales/Invoices/InvoiceSendMailDrawer/InvoiceSendMailContentBoot.tsx b/packages/webapp/src/containers/Sales/Invoices/InvoiceSendMailDrawer/InvoiceSendMailContentBoot.tsx index 9b30ce467..dab0e5269 100644 --- a/packages/webapp/src/containers/Sales/Invoices/InvoiceSendMailDrawer/InvoiceSendMailContentBoot.tsx +++ b/packages/webapp/src/containers/Sales/Invoices/InvoiceSendMailDrawer/InvoiceSendMailContentBoot.tsx @@ -3,18 +3,15 @@ import React, { createContext, useContext } from 'react'; import { Spinner } from '@blueprintjs/core'; import { GetSaleInvoiceDefaultOptionsResponse, - useInvoice, - useSaleInvoiceDefaultOptions, + useSaleInvoiceMailState, } from '@/hooks/query'; import { useDrawerContext } from '@/components/Drawer/DrawerProvider'; interface InvoiceSendMailBootValues { - invoice: any; invoiceId: number; - isInvoiceLoading: boolean; - invoiceMailOptions: GetSaleInvoiceDefaultOptionsResponse | undefined; - isInvoiceMailOptionsLoading: boolean; + invoiceMailState: GetSaleInvoiceDefaultOptionsResponse | undefined; + isInvoiceMailState: boolean; } interface InvoiceSendMailBootProps { children: React.ReactNode; @@ -28,25 +25,21 @@ export const InvoiceSendMailBoot = ({ children }: InvoiceSendMailBootProps) => { payload: { invoiceId }, } = useDrawerContext(); - // Invoice details. - const { data: invoice, isLoading: isInvoiceLoading } = useInvoice(invoiceId, { - enabled: !!invoiceId, - }); // Invoice mail options. - const { data: invoiceMailOptions, isLoading: isInvoiceMailOptionsLoading } = - useSaleInvoiceDefaultOptions(invoiceId); + const { data: invoiceMailState, isLoading: isInvoiceMailState } = + useSaleInvoiceMailState(invoiceId); - const isLoading = isInvoiceLoading || isInvoiceMailOptionsLoading; + const isLoading = isInvoiceMailState; if (isLoading) { return ; } const value = { - invoice, - isInvoiceLoading, invoiceId, - invoiceMailOptions, - isInvoiceMailOptionsLoading, + + // # Invoice mail options + isInvoiceMailState, + invoiceMailState, }; return ( diff --git a/packages/webapp/src/containers/Sales/Invoices/InvoiceSendMailDrawer/InvoiceSendMailForm.tsx b/packages/webapp/src/containers/Sales/Invoices/InvoiceSendMailDrawer/InvoiceSendMailForm.tsx index c938d34e5..ad4722801 100644 --- a/packages/webapp/src/containers/Sales/Invoices/InvoiceSendMailDrawer/InvoiceSendMailForm.tsx +++ b/packages/webapp/src/containers/Sales/Invoices/InvoiceSendMailDrawer/InvoiceSendMailForm.tsx @@ -25,13 +25,14 @@ interface InvoiceSendMailFormProps { export function InvoiceSendMailForm({ children }: InvoiceSendMailFormProps) { const { mutateAsync: sendInvoiceMail } = useSendSaleInvoiceMail(); - const { invoiceId, invoiceMailOptions } = useInvoiceSendMailBoot(); + const { invoiceId, invoiceMailState } = useInvoiceSendMailBoot(); + const { name } = useDrawerContext(); const { closeDrawer } = useDrawerActions(); const _initialValues: InvoiceSendMailFormValues = { ...initialValues, - ...transformToForm(invoiceMailOptions, initialValues), + ...transformToForm(invoiceMailState, initialValues), }; const handleSubmit = ( values: InvoiceSendMailFormValues, diff --git a/packages/webapp/src/containers/Sales/Invoices/InvoiceSendMailDrawer/InvoiceSendMailHeader.tsx b/packages/webapp/src/containers/Sales/Invoices/InvoiceSendMailDrawer/InvoiceSendMailHeader.tsx index 400347f7a..25d3ab008 100644 --- a/packages/webapp/src/containers/Sales/Invoices/InvoiceSendMailDrawer/InvoiceSendMailHeader.tsx +++ b/packages/webapp/src/containers/Sales/Invoices/InvoiceSendMailDrawer/InvoiceSendMailHeader.tsx @@ -7,6 +7,7 @@ import { useDrawerActions } from '@/hooks/state'; interface ElementCustomizeHeaderProps { label?: string; children?: React.ReactNode; + closeButton?: boolean; } export function InvoiceSendMailHeader({ diff --git a/packages/webapp/src/containers/Sales/Invoices/InvoiceSendMailDrawer/InvoiceSendMailHeaderPreview.tsx b/packages/webapp/src/containers/Sales/Invoices/InvoiceSendMailDrawer/InvoiceSendMailHeaderPreview.tsx index 0cf30d3ea..680e3d897 100644 --- a/packages/webapp/src/containers/Sales/Invoices/InvoiceSendMailDrawer/InvoiceSendMailHeaderPreview.tsx +++ b/packages/webapp/src/containers/Sales/Invoices/InvoiceSendMailDrawer/InvoiceSendMailHeaderPreview.tsx @@ -1,10 +1,13 @@ +import React, { useMemo } from 'react'; import { x } from '@xstyled/emotion'; import { Box, Group, Stack } from '@/components'; -import React from 'react'; -import { useSendInvoiceMailSubject } from './_hooks'; +import { useSendInvoiceMailForm, useSendInvoiceMailSubject } from './_hooks'; +import { useInvoiceSendMailBoot } from './InvoiceSendMailContentBoot'; export function InvoiceSendMailHeaderPreview() { const mailSubject = useSendInvoiceMailSubject(); + const { invoiceMailState } = useInvoiceSendMailBoot(); + const toAddresses = useMailHeaderToAddresses(); return ( Ahmed - <messaging-service@post.xero.com> + <messaging-service@post.bigcapital.app> - Reply to: Ahmed <a.m.bouhuolia@gmail.com> + Reply to: {invoiceMailState?.companyName} {toAddresses}; @@ -65,8 +68,15 @@ export function InvoiceSendMailPreviewWithHeader({ return ( - {children} ); } + +export const useMailHeaderToAddresses = () => { + const { + values: { to }, + } = useSendInvoiceMailForm(); + + return useMemo(() => to?.map((email) => '<' + email + '>').join(' '), [to]); +}; diff --git a/packages/webapp/src/containers/Sales/Invoices/InvoiceSendMailDrawer/InvoiceSendMailPreview.tsx b/packages/webapp/src/containers/Sales/Invoices/InvoiceSendMailDrawer/InvoiceSendMailPreview.tsx index 41034569d..a8fb9d2dc 100644 --- a/packages/webapp/src/containers/Sales/Invoices/InvoiceSendMailDrawer/InvoiceSendMailPreview.tsx +++ b/packages/webapp/src/containers/Sales/Invoices/InvoiceSendMailDrawer/InvoiceSendMailPreview.tsx @@ -1,8 +1,18 @@ -import { Tab, Tabs } from '@blueprintjs/core'; +import { lazy, Suspense } from 'react'; import { css } from '@emotion/css'; +import { Tab, Tabs } from '@blueprintjs/core'; import { Stack } from '@/components'; -import { InvoiceMailReceiptPreviewConneceted } from './InvoiceMailReceiptPreviewConnected.'; -import { InvoiceSendPdfPreviewConnected } from './InvoiceSendPdfPreviewConnected'; + +const InvoiceMailReceiptPreviewConneceted = lazy(() => + import('./InvoiceMailReceiptPreviewConnected.').then((module) => ({ + default: module.InvoiceMailReceiptPreviewConneceted, + })), +); +const InvoiceSendPdfPreviewConnected = lazy(() => + import('./InvoiceSendPdfPreviewConnected').then((module) => ({ + default: module.InvoiceSendPdfPreviewConnected, + })), +); export function InvoiceSendMailPreview() { return ( @@ -39,12 +49,20 @@ export function InvoiceSendMailPreview() { } + panel={ + + + + } /> } + panel={ + + + + } /> diff --git a/packages/webapp/src/containers/Sales/Invoices/InvoiceSendMailDrawer/InvoiceSendPdfPreviewConnected.tsx b/packages/webapp/src/containers/Sales/Invoices/InvoiceSendMailDrawer/InvoiceSendPdfPreviewConnected.tsx index 496a5e80c..abf4678c0 100644 --- a/packages/webapp/src/containers/Sales/Invoices/InvoiceSendMailDrawer/InvoiceSendPdfPreviewConnected.tsx +++ b/packages/webapp/src/containers/Sales/Invoices/InvoiceSendMailDrawer/InvoiceSendPdfPreviewConnected.tsx @@ -1,25 +1,13 @@ +import { css } from '@emotion/css'; import { Box } from '@/components'; import { InvoicePaperTemplate } from '../InvoiceCustomize/InvoicePaperTemplate'; -import { css } from '@emotion/css'; -import { useInvoiceSendMailBoot } from './InvoiceSendMailContentBoot'; import { InvoiceSendMailPreviewWithHeader } from './InvoiceSendMailHeaderPreview'; export function InvoiceSendPdfPreviewConnected() { - const { invoice } = useInvoiceSendMailBoot(); - return ( { + return useFormikContext(); +}; + export const useInvoiceMailItems = () => { const { values } = useFormikContext(); const cc = values?.cc || []; @@ -21,13 +25,13 @@ export const useInvoiceMailItems = () => { }; export const useSendInvoiceMailFormatArgs = (): Record => { - const { invoiceMailOptions } = useInvoiceSendMailBoot(); + const { invoiceMailState } = useInvoiceSendMailBoot(); return useMemo(() => { - return mapKeys(invoiceMailOptions?.formatArgs, (_, key) => + return mapKeys(invoiceMailState?.formatArgs, (_, key) => startCase(snakeCase(key).replace('_', ' ')), ); - }, [invoiceMailOptions]); + }, [invoiceMailState]); }; export const useSendInvoiceMailSubject = (): string => { diff --git a/packages/webapp/src/hooks/query/invoices.tsx b/packages/webapp/src/hooks/query/invoices.tsx index dcd24ba52..7cc69d150 100644 --- a/packages/webapp/src/hooks/query/invoices.tsx +++ b/packages/webapp/src/hooks/query/invoices.tsx @@ -320,6 +320,8 @@ export function useInvoicePaymentTransactions(invoiceId, props) { ); } +// # Send sale invoice mail. +// ------------------------------ export interface SendSaleInvoiceMailValues { id: number; values: { @@ -366,16 +368,49 @@ export function useSendSaleInvoiceMail( ); } +// # Get sale invoice default options. +// -------------------------------------- export interface GetSaleInvoiceDefaultOptionsResponse { - to: Array; - from: Array; - subject: string; - message: string; - attachInvoice: boolean; + companyName: string; + + dueDate: string; + dueDateFormatted: string; + + dueAmount: number; + dueAmountFormatted: string; + + entries: Array<{ + quantity: number; + quantityFormatted: string; + rate: number; + rateFormatted: string; + total: number; + totalFormatted: string; + }>; formatArgs: Record; + + from: string[]; + to: string[]; + + invoiceDate: string; + invoiceDateFormatted: string; + + invoiceNo: string; + + message: string; + subject: string; + + subtotal: number; + subtotalFormatted: string; + + total: number; + totalFormatted: string; + + attachInvoice: boolean; + primaryColor: string; } -export function useSaleInvoiceDefaultOptions( +export function useSaleInvoiceMailState( invoiceId: number, options?: UseQueryOptions, ): UseQueryResult { @@ -385,12 +420,14 @@ export function useSaleInvoiceDefaultOptions( [t.SALE_INVOICE_DEFAULT_OPTIONS, invoiceId], () => apiRequest - .get(`/sales/invoices/${invoiceId}/mail`) + .get(`/sales/invoices/${invoiceId}/mail/state`) .then((res) => transformToCamelCase(res.data?.data)), options, ); } +// # Get sale invoice state. +// ------------------------------------- export interface GetSaleInvoiceStateResponse { defaultTemplateId: number; } @@ -409,3 +446,68 @@ export function useGetSaleInvoiceState( { ...options }, ); } + +// # Get sale invoice branding template. +// -------------------------------------- +export interface GetSaleInvoiceBrandingTemplateResponse { + id: number; + default: number; + predefined: number; + resource: string; + resourceFormatted: string; + templateName: string; + updatedAt: string; + createdAt: string; + createdAtFormatted: string; + attributes: { + billedToLabel?: string; + companyLogoKey?: string | null; + companyLogoUri?: string; + dateIssueLabel?: string; + discountLabel?: string; + dueAmountLabel?: string; + dueDateLabel?: string; + invoiceNumberLabel?: string; + itemDescriptionLabel?: string; + itemNameLabel?: string; + itemRateLabel?: string; + itemTotalLabel?: string; + paymentMadeLabel?: string; + primaryColor?: string; + secondaryColor?: string; + showCompanyAddress?: boolean; + showCompanyLogo?: boolean; + showCustomerAddress?: boolean; + showDateIssue?: boolean; + showDiscount?: boolean; + showDueAmount?: boolean; + showDueDate?: boolean; + showInvoiceNumber?: boolean; + showPaymentMade?: boolean; + showStatement?: boolean; + showSubtotal?: boolean; + showTaxes?: boolean; + showTermsConditions?: boolean; + showTotal?: boolean; + statementLabel?: string; + subtotalLabel?: string; + termsConditionsLabel?: string; + totalLabel?: string; + }; +} + +export function useGetSaleInvoiceBrandingTemplate( + invoiceId: number, + options?: UseQueryOptions, +): UseQueryResult { + const apiRequest = useApiRequest(); + + return useQuery( + ['SALE_INVOICE_BRANDING_TEMPLATE', invoiceId], + () => + apiRequest + .get(`/sales/invoices/${invoiceId}/template`) + .then((res) => transformToCamelCase(res.data?.data)), + { ...options }, + ); +}