From e10c530b4b5644e3069f71028f2674db90f3ac50 Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Tue, 29 Oct 2024 21:14:46 +0200 Subject: [PATCH] feat: wip preview invoice payment mail --- packages/server/src/interfaces/SaleInvoice.ts | 1 + .../services/Sales/Invoices/SaleInvoicePdf.ts | 2 +- .../Invoices/SendInvoiceInvoiceMailCommon.ts | 9 +- .../Sales/Invoices/SendSaleInvoiceMail.ts | 3 +- .../InvoiceMailReceiptPreviewConnected..tsx | 31 ++++--- .../InvoiceSendMailContentBoot.tsx | 18 +++- .../InvoiceSendMailFields.tsx | 91 ++++++++++++++++--- .../InvoiceSendMailForm.tsx | 29 +++--- .../InvoiceSendMailHeaderPreview.tsx | 75 +++++++++++++++ .../InvoiceSendPdfPreviewConnected.tsx | 43 +++++---- .../Invoices/InvoiceSendMailDrawer/_hooks.ts | 43 ++++++++- .../Invoices/InvoiceSendMailDrawer/_types.ts | 4 +- packages/webapp/src/hooks/query/invoices.tsx | 33 +++++-- 13 files changed, 301 insertions(+), 81 deletions(-) create mode 100644 packages/webapp/src/containers/Sales/Invoices/InvoiceSendMailDrawer/InvoiceSendMailHeaderPreview.tsx diff --git a/packages/server/src/interfaces/SaleInvoice.ts b/packages/server/src/interfaces/SaleInvoice.ts index cb77f29fe..f1eeb5256 100644 --- a/packages/server/src/interfaces/SaleInvoice.ts +++ b/packages/server/src/interfaces/SaleInvoice.ts @@ -235,6 +235,7 @@ export enum SaleInvoiceAction { export interface SaleInvoiceMailOptions extends CommonMailOptions { attachInvoice?: boolean; + formatArgs?: Record; } export interface SendInvoiceMailDTO extends CommonMailOptionsDTO { 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/SendInvoiceInvoiceMailCommon.ts b/packages/server/src/services/Sales/Invoices/SendInvoiceInvoiceMailCommon.ts index b7b863c27..10ccedbbb 100644 --- a/packages/server/src/services/Sales/Invoices/SendInvoiceInvoiceMailCommon.ts +++ b/packages/server/src/services/Sales/Invoices/SendInvoiceInvoiceMailCommon.ts @@ -48,11 +48,14 @@ export class SendSaleInvoiceMailCommon { tenantId, saleInvoice.customerId ); + const formatArgs = await this.getInvoiceFormatterArgs(tenantId, invoiceId); + return { ...contactMailDefaultOptions, - attachInvoice: true, subject: defaultSubject, message: defaultMessage, + attachInvoice: true, + formatArgs, }; } @@ -103,7 +106,11 @@ export class SendSaleInvoiceMailCommon { tenantId, invoiceId ); + const commonArgs = + await this.contactMailNotification.getCommonFormatArgs(tenantId); + return { + ...commonArgs, CustomerName: invoice.customer.displayName, InvoiceNumber: invoice.invoiceNo, InvoiceDueAmount: invoice.dueAmountFormatted, diff --git a/packages/server/src/services/Sales/Invoices/SendSaleInvoiceMail.ts b/packages/server/src/services/Sales/Invoices/SendSaleInvoiceMail.ts index 86b664229..f0c4f6e77 100644 --- a/packages/server/src/services/Sales/Invoices/SendSaleInvoiceMail.ts +++ b/packages/server/src/services/Sales/Invoices/SendSaleInvoiceMail.ts @@ -15,9 +15,8 @@ import { parseMailOptions, validateRequiredMailOptions, } from '@/services/MailNotification/utils'; -import events from '@/subscribers/events'; import { EventPublisher } from '@/lib/EventPublisher/EventPublisher'; -import { ParsedNumberSearch } from 'libphonenumber-js'; +import events from '@/subscribers/events'; @Service() export class SendSaleInvoiceMail { diff --git a/packages/webapp/src/containers/Sales/Invoices/InvoiceSendMailDrawer/InvoiceMailReceiptPreviewConnected..tsx b/packages/webapp/src/containers/Sales/Invoices/InvoiceSendMailDrawer/InvoiceMailReceiptPreviewConnected..tsx index f07a341a0..dee0a83c3 100644 --- a/packages/webapp/src/containers/Sales/Invoices/InvoiceSendMailDrawer/InvoiceMailReceiptPreviewConnected..tsx +++ b/packages/webapp/src/containers/Sales/Invoices/InvoiceSendMailDrawer/InvoiceMailReceiptPreviewConnected..tsx @@ -1,11 +1,15 @@ -import { Box } from '@/components'; +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 { InvoiceSendMailPreviewWithHeader } from './InvoiceSendMailHeaderPreview'; +import { useSendInvoiceMailMessage } from './_hooks'; export function InvoiceMailReceiptPreviewConneceted() { const { invoice } = useInvoiceSendMailBoot(); + const mailMessage = useSendInvoiceMailMessage(); const items = useMemo( () => @@ -18,16 +22,19 @@ export function InvoiceMailReceiptPreviewConneceted() { ); return ( - - - + + + + + ); } diff --git a/packages/webapp/src/containers/Sales/Invoices/InvoiceSendMailDrawer/InvoiceSendMailContentBoot.tsx b/packages/webapp/src/containers/Sales/Invoices/InvoiceSendMailDrawer/InvoiceSendMailContentBoot.tsx index b181d771e..9b30ce467 100644 --- a/packages/webapp/src/containers/Sales/Invoices/InvoiceSendMailDrawer/InvoiceSendMailContentBoot.tsx +++ b/packages/webapp/src/containers/Sales/Invoices/InvoiceSendMailDrawer/InvoiceSendMailContentBoot.tsx @@ -1,13 +1,20 @@ // @ts-nocheck import React, { createContext, useContext } from 'react'; import { Spinner } from '@blueprintjs/core'; -import { useInvoice } from '@/hooks/query'; +import { + GetSaleInvoiceDefaultOptionsResponse, + useInvoice, + useSaleInvoiceDefaultOptions, +} from '@/hooks/query'; import { useDrawerContext } from '@/components/Drawer/DrawerProvider'; interface InvoiceSendMailBootValues { invoice: any; invoiceId: number; isInvoiceLoading: boolean; + + invoiceMailOptions: GetSaleInvoiceDefaultOptionsResponse | undefined; + isInvoiceMailOptionsLoading: boolean; } interface InvoiceSendMailBootProps { children: React.ReactNode; @@ -21,10 +28,15 @@ export const InvoiceSendMailBoot = ({ children }: InvoiceSendMailBootProps) => { payload: { invoiceId }, } = useDrawerContext(); + // Invoice details. const { data: invoice, isLoading: isInvoiceLoading } = useInvoice(invoiceId, { enabled: !!invoiceId, }); - const isLoading = isInvoiceLoading; + // Invoice mail options. + const { data: invoiceMailOptions, isLoading: isInvoiceMailOptionsLoading } = + useSaleInvoiceDefaultOptions(invoiceId); + + const isLoading = isInvoiceLoading || isInvoiceMailOptionsLoading; if (isLoading) { return ; @@ -33,6 +45,8 @@ export const InvoiceSendMailBoot = ({ children }: InvoiceSendMailBootProps) => { invoice, isInvoiceLoading, invoiceId, + invoiceMailOptions, + isInvoiceMailOptionsLoading, }; return ( diff --git a/packages/webapp/src/containers/Sales/Invoices/InvoiceSendMailDrawer/InvoiceSendMailFields.tsx b/packages/webapp/src/containers/Sales/Invoices/InvoiceSendMailDrawer/InvoiceSendMailFields.tsx index 4f680f6bd..72fc9cd34 100644 --- a/packages/webapp/src/containers/Sales/Invoices/InvoiceSendMailDrawer/InvoiceSendMailFields.tsx +++ b/packages/webapp/src/containers/Sales/Invoices/InvoiceSendMailDrawer/InvoiceSendMailFields.tsx @@ -1,6 +1,6 @@ // @ts-nocheck -import { useState } from 'react'; -import { Button, Intent, MenuItem } from '@blueprintjs/core'; +import { useRef, useState } from 'react'; +import { Button, Intent, MenuItem, Position } from '@blueprintjs/core'; import { useFormikContext } from 'formik'; import { css } from '@emotion/css'; import { x } from '@xstyled/emotion'; @@ -9,8 +9,10 @@ import { FFormGroup, FInputGroup, FMultiSelect, + FSelect, FTextArea, Group, + Icon, Stack, } from '@/components'; import { useDrawerContext } from '@/components/Drawer/DrawerProvider'; @@ -60,6 +62,7 @@ const fieldsWrapStyle = css` export function InvoiceSendMailFields() { const [showCCField, setShowCCField] = useState(false); const [showBccField, setShowBccField] = useState(false); + const textareaRef = useRef(null); const { values, setFieldValue } = useFormikContext(); const items = useInvoiceMailItems(); @@ -117,6 +120,30 @@ export function InvoiceSendMailFields() { ); + const handleTextareaChange = () => { + const textarea = textareaRef.current; + if (!textarea) return; + + const { selectionStart, selectionEnd } = textarea; + const insertText = '{Variable}'; + + // Insert the text at the cursor position + 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); + }; + return ( {showCCField && ( )} {showBccField && ( @@ -179,25 +208,62 @@ export function InvoiceSendMailFields() { resetOnQuery resetOnSelect fill + fastField /> )} - + - + + + ( + + )} + fill={false} + fastField + /> + + + + @@ -243,4 +309,3 @@ function InvoiceSendMailFooter() { ); } - diff --git a/packages/webapp/src/containers/Sales/Invoices/InvoiceSendMailDrawer/InvoiceSendMailForm.tsx b/packages/webapp/src/containers/Sales/Invoices/InvoiceSendMailDrawer/InvoiceSendMailForm.tsx index 3b108851f..263ab405a 100644 --- a/packages/webapp/src/containers/Sales/Invoices/InvoiceSendMailDrawer/InvoiceSendMailForm.tsx +++ b/packages/webapp/src/containers/Sales/Invoices/InvoiceSendMailDrawer/InvoiceSendMailForm.tsx @@ -1,4 +1,3 @@ -import * as Yup from 'yup'; import { Form, Formik, FormikHelpers } from 'formik'; import { css } from '@emotion/css'; import { Intent } from '@blueprintjs/core'; @@ -9,22 +8,14 @@ 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 = { - subject: 'invoice INV-0002 for AED 0.00', - message: `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`, - to: ['a.bouhuolia@gmail.com'], + subject: '', + message: '', + to: [], + cc: [], + bcc: [], }; interface InvoiceSendMailFormProps { @@ -33,10 +24,14 @@ interface InvoiceSendMailFormProps { export function InvoiceSendMailForm({ children }: InvoiceSendMailFormProps) { const { mutateAsync: sendInvoiceMail } = useSendSaleInvoiceMail(); - const { invoiceId } = useInvoiceSendMailBoot(); + const { invoiceId, invoiceMailOptions } = useInvoiceSendMailBoot(); const { name } = useDrawerContext(); const { closeDrawer } = useDrawerActions(); + const _initialValues = { + ...initialValues, + ...transformToForm(invoiceMailOptions, initialValues), + }; const handleSubmit = ( values: InvoiceSendMailFormValues, { setSubmitting }: FormikHelpers, @@ -62,7 +57,7 @@ export function InvoiceSendMailForm({ children }: InvoiceSendMailFormProps) { return ( diff --git a/packages/webapp/src/containers/Sales/Invoices/InvoiceSendMailDrawer/InvoiceSendMailHeaderPreview.tsx b/packages/webapp/src/containers/Sales/Invoices/InvoiceSendMailDrawer/InvoiceSendMailHeaderPreview.tsx new file mode 100644 index 000000000..cbafd6070 --- /dev/null +++ b/packages/webapp/src/containers/Sales/Invoices/InvoiceSendMailDrawer/InvoiceSendMailHeaderPreview.tsx @@ -0,0 +1,75 @@ +import { css } from '@emotion/css'; +import { x } from '@xstyled/emotion'; +import { Box, Group, Stack } from '@/components'; +import React from 'react'; +import { useSendInvoiceMailSubject } from './_hooks'; + +export function InvoiceSendMailHeaderPreview() { + const mailSubject = useSendInvoiceMailSubject(); + + return ( + + + + {mailSubject} + + + + + + + A + + + + + Ahmed + + <messaging-service@post.xero.com> + + + + + Reply to: Ahmed <a.m.bouhuolia@gmail.com> + + + + + + ); +} + +export function InvoiceSendMailPreviewWithHeader({ + children, +}: { + children: React.ReactNode; +}) { + return ( + + + + {children} + + ); +} diff --git a/packages/webapp/src/containers/Sales/Invoices/InvoiceSendMailDrawer/InvoiceSendPdfPreviewConnected.tsx b/packages/webapp/src/containers/Sales/Invoices/InvoiceSendMailDrawer/InvoiceSendPdfPreviewConnected.tsx index 7a6eaf96c..496a5e80c 100644 --- a/packages/webapp/src/containers/Sales/Invoices/InvoiceSendMailDrawer/InvoiceSendPdfPreviewConnected.tsx +++ b/packages/webapp/src/containers/Sales/Invoices/InvoiceSendMailDrawer/InvoiceSendPdfPreviewConnected.tsx @@ -1,27 +1,30 @@ -import { Box } from "@/components"; -import { InvoicePaperTemplate } from "../InvoiceCustomize/InvoicePaperTemplate"; -import { css } from "@emotion/css"; -import { useInvoiceSendMailBoot } from "./InvoiceSendMailContentBoot"; +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 ( - - - + + + + + ); } diff --git a/packages/webapp/src/containers/Sales/Invoices/InvoiceSendMailDrawer/_hooks.ts b/packages/webapp/src/containers/Sales/Invoices/InvoiceSendMailDrawer/_hooks.ts index 537d985a5..25a8752a4 100644 --- a/packages/webapp/src/containers/Sales/Invoices/InvoiceSendMailDrawer/_hooks.ts +++ b/packages/webapp/src/containers/Sales/Invoices/InvoiceSendMailDrawer/_hooks.ts @@ -1,6 +1,8 @@ import { useFormikContext } from 'formik'; -import { chain } from 'lodash'; +import { camelCase, chain, defaultTo, mapKeys, upperFirst } from 'lodash'; import { InvoiceSendMailFormValues } from './_types'; +import { useInvoiceSendMailBoot } from './InvoiceSendMailContentBoot'; +import { useMemo } from 'react'; export const useInvoiceMailItems = () => { const { values } = useFormikContext(); @@ -16,3 +18,42 @@ export const useInvoiceMailItems = () => { })) .value(); }; + +export const useSendInvoiceMailFormatArgs = () => { + const { invoiceMailOptions } = useInvoiceSendMailBoot(); + + return useMemo(() => { + return mapKeys(invoiceMailOptions?.formatArgs, (_, key) => + upperFirst(camelCase(key)), + ); + }, [invoiceMailOptions]); +}; + +export const useSendInvoiceMailSubject = () => { + const { values } = useFormikContext(); + const formatArgs = useSendInvoiceMailFormatArgs(); + + return formatSmsMessage(values?.subject, formatArgs); +}; + +export const useSendInvoiceMailMessage = () => { + const { values } = useFormikContext(); + const formatArgs = useSendInvoiceMailFormatArgs(); + + return formatSmsMessage(values?.message, formatArgs); +}; + +export const formatSmsMessage = ( + message: string, + args: Record, +) => { + let formattedMessage = message; + + Object.keys(args).forEach((key) => { + const variable = `{${key}}`; + const value = defaultTo(args[key], ''); + + formattedMessage = formattedMessage.replace(variable, value); + }); + return formattedMessage; +}; diff --git a/packages/webapp/src/containers/Sales/Invoices/InvoiceSendMailDrawer/_types.ts b/packages/webapp/src/containers/Sales/Invoices/InvoiceSendMailDrawer/_types.ts index 33987d34a..fbb7dc204 100644 --- a/packages/webapp/src/containers/Sales/Invoices/InvoiceSendMailDrawer/_types.ts +++ b/packages/webapp/src/containers/Sales/Invoices/InvoiceSendMailDrawer/_types.ts @@ -2,6 +2,6 @@ export interface InvoiceSendMailFormValues { subject: string; message: string; to: string[]; - cc?: string[]; - bcc?: string[]; + cc: string[]; + bcc: string[]; } diff --git a/packages/webapp/src/hooks/query/invoices.tsx b/packages/webapp/src/hooks/query/invoices.tsx index 8cf123d6b..9e2c72e8c 100644 --- a/packages/webapp/src/hooks/query/invoices.tsx +++ b/packages/webapp/src/hooks/query/invoices.tsx @@ -5,6 +5,8 @@ import { useQuery, UseMutationOptions, UseMutationResult, + UseQueryOptions, + UseQueryResult, } from 'react-query'; import { useRequestQuery } from '../useQueryRequest'; import { transformPagination, transformToCamelCase } from '@/utils'; @@ -364,17 +366,28 @@ export function useSendSaleInvoiceMail( ); } -export function useSaleInvoiceDefaultOptions(invoiceId, props) { - return useRequestQuery( +export interface GetSaleInvoiceDefaultOptionsResponse { + to: Array; + from: Array; + subject: string; + message: string; + attachInvoice: boolean; + formatArgs: Record; +} + +export function useSaleInvoiceDefaultOptions( + invoiceId: number, + options?: UseQueryOptions, +): UseQueryResult { + const apiRequest = useApiRequest(); + + return useQuery( [t.SALE_INVOICE_DEFAULT_OPTIONS, invoiceId], - { - method: 'get', - url: `sales/invoices/${invoiceId}/mail`, - }, - { - select: (res) => res.data.data, - ...props, - }, + () => + apiRequest + .get(`/sales/invoices/${invoiceId}/mail`) + .then((res) => transformToCamelCase(res.data?.data)), + options, ); }