diff --git a/packages/server/src/api/controllers/Sales/PaymentReceives.ts b/packages/server/src/api/controllers/Sales/PaymentReceives.ts index 5979d30f0..0fb903de4 100644 --- a/packages/server/src/api/controllers/Sales/PaymentReceives.ts +++ b/packages/server/src/api/controllers/Sales/PaymentReceives.ts @@ -411,8 +411,9 @@ 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); @@ -469,6 +470,7 @@ export default class PaymentReceivesController extends BaseController { const acceptType = accept.types([ ACCEPT_TYPE.APPLICATION_JSON, ACCEPT_TYPE.APPLICATION_PDF, + ACCEPT_TYPE.APPLICATION_TEXT_HTML, ]); // Responses pdf format. if (ACCEPT_TYPE.APPLICATION_PDF === acceptType) { @@ -485,10 +487,11 @@ export default class PaymentReceivesController extends BaseController { res.send(pdfContent); // Responses html format. } else if (ACCEPT_TYPE.APPLICATION_TEXT_HTML === acceptType) { - const htmlContent = this.paymentReceiveApplication.getPaymentReceivedHtml( - tenantId, - paymentReceiveId - ); + const htmlContent = + await this.paymentReceiveApplication.getPaymentReceivedHtml( + tenantId, + paymentReceiveId + ); return res.status(200).send({ htmlContent }); // Responses json format. } else { diff --git a/packages/webapp/src/components/DialogsContainer.tsx b/packages/webapp/src/components/DialogsContainer.tsx index 5fcd3f07c..a97200483 100644 --- a/packages/webapp/src/components/DialogsContainer.tsx +++ b/packages/webapp/src/components/DialogsContainer.tsx @@ -44,7 +44,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 PaymentMailDialog from '@/containers/Sales/PaymentsReceived/PaymentMailDialog/PaymentMailDialog'; import { ExportDialog } from '@/containers/Dialogs/ExportDialog'; import { RuleFormDialog } from '@/containers/Banking/Rules/RuleFormDialog/RuleFormDialog'; import { DisconnectBankAccountDialog } from '@/containers/CashFlow/AccountTransactions/dialogs/DisconnectBankAccountDialog/DisconnectBankAccountDialog'; @@ -139,7 +138,6 @@ export default function DialogsContainer() { - + ); } diff --git a/packages/webapp/src/constants/drawers.ts b/packages/webapp/src/constants/drawers.ts index 9afb191b2..6c75d8b2b 100644 --- a/packages/webapp/src/constants/drawers.ts +++ b/packages/webapp/src/constants/drawers.ts @@ -37,4 +37,5 @@ export enum DRAWERS { INVOICE_SEND_MAIL = 'INVOICE_SEND_MAIL', ESTIMATE_SEND_MAIL = 'ESTIMATE_SEND_MAIL', RECEIPT_SEND_MAIL = 'RECEIPT_SEND_MAIL', + PAYMENT_RECEIVED_SEND_MAIL = 'PAYMENT_RECEIVED_SEND_MAIL', } diff --git a/packages/webapp/src/containers/Sales/PaymentsReceived/PaymentReceivedMailDrawer/PaymentReceivedMailBoot.tsx b/packages/webapp/src/containers/Sales/PaymentsReceived/PaymentReceivedMailDrawer/PaymentReceivedMailBoot.tsx new file mode 100644 index 000000000..c7323b8f4 --- /dev/null +++ b/packages/webapp/src/containers/Sales/PaymentsReceived/PaymentReceivedMailDrawer/PaymentReceivedMailBoot.tsx @@ -0,0 +1,61 @@ +import React, { createContext, useContext } from 'react'; +import { Spinner } from '@blueprintjs/core'; +import { + PaymentReceivedStateResponse, + usePaymentReceivedState, +} from '@/hooks/query'; +import { useDrawerContext } from '@/components/Drawer/DrawerProvider'; + +interface PaymentReceivedSendMailBootValues { + paymentReceivedId: number; + + paymentReceivedMailState: PaymentReceivedStateResponse | undefined; + isPaymentReceivedStateLoading: boolean; +} +interface InvoiceSendMailBootProps { + children: React.ReactNode; +} + +const PaymentReceivedSendMailBootContext = + createContext( + {} as PaymentReceivedSendMailBootValues, + ); + +export const PaymentReceivedSendMailBoot = ({ + children, +}: InvoiceSendMailBootProps) => { + const { + payload: { paymentReceivedId }, + } = useDrawerContext(); + + const { + data: paymentReceivedMailState, + isLoading: isPaymentReceivedStateLoading, + } = usePaymentReceivedState(paymentReceivedId); + + const isLoading = isPaymentReceivedStateLoading; + + if (isLoading) { + return ; + } + const value = { + paymentReceivedId, + + // # mail options + isPaymentReceivedStateLoading, + paymentReceivedMailState, + }; + + return ( + + {children} + + ); +}; +PaymentReceivedSendMailBoot.displayName = 'PaymentReceivedSendMailBoot'; + +export const usePaymentReceivedSendMailBoot = () => { + return useContext( + PaymentReceivedSendMailBootContext, + ); +}; diff --git a/packages/webapp/src/containers/Sales/PaymentsReceived/PaymentReceivedMailDrawer/PaymentReceivedMailContent.tsx b/packages/webapp/src/containers/Sales/PaymentsReceived/PaymentReceivedMailDrawer/PaymentReceivedMailContent.tsx new file mode 100644 index 000000000..bfc536f2a --- /dev/null +++ b/packages/webapp/src/containers/Sales/PaymentsReceived/PaymentReceivedMailDrawer/PaymentReceivedMailContent.tsx @@ -0,0 +1,25 @@ +import { Stack } from '@/components'; +import { Classes } from '@blueprintjs/core'; +import { PaymentReceivedSendMailBoot } from './PaymentReceivedMailBoot'; +import { PaymentReceivedSendMailForm } from './PaymentReceivedMailForm'; +import { PaymentReceivedSendMailPreview } from './PaymentReceivedMailPreviewTabs'; +// import { InvoiceSendMailFields } from './InvoiceSendMailFields'; +import { SendMailViewHeader } from '../../Estimates/SendMailViewDrawer/SendMailViewHeader'; +import { SendMailViewLayout } from '../../Estimates/SendMailViewDrawer/SendMailViewLayout'; +import { PaymentReceivedSendMailFields } from './PaymentReceivedMailFields'; + +export function PaymentReceivedSendMailContent() { + return ( + + + + } + fields={} + preview={} + /> + + + + ); +} diff --git a/packages/webapp/src/containers/Sales/PaymentsReceived/PaymentReceivedMailDrawer/PaymentReceivedMailDrawer.tsx b/packages/webapp/src/containers/Sales/PaymentsReceived/PaymentReceivedMailDrawer/PaymentReceivedMailDrawer.tsx new file mode 100644 index 000000000..04d35e121 --- /dev/null +++ b/packages/webapp/src/containers/Sales/PaymentsReceived/PaymentReceivedMailDrawer/PaymentReceivedMailDrawer.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 PaymentReceivedMailContent = React.lazy(() => + import('./PaymentReceivedMailContent').then((module) => ({ + default: module.PaymentReceivedSendMailContent, + })), +); + +interface PaymentReceivedSendMailDrawerProps { + name: string; + isOpen?: boolean; + payload?: any; +} + +function PaymentReceivedSendMailDrawerRoot({ + name, + + // #withDrawer + isOpen, + payload, +}: PaymentReceivedSendMailDrawerProps) { + return ( + + + + + + ); +} + +export const PaymentReceivedSendMailDrawer = R.compose(withDrawers())( + PaymentReceivedSendMailDrawerRoot, +); diff --git a/packages/webapp/src/containers/Sales/PaymentsReceived/PaymentReceivedMailDrawer/PaymentReceivedMailFields.tsx b/packages/webapp/src/containers/Sales/PaymentsReceived/PaymentReceivedMailDrawer/PaymentReceivedMailFields.tsx new file mode 100644 index 000000000..5dde36570 --- /dev/null +++ b/packages/webapp/src/containers/Sales/PaymentsReceived/PaymentReceivedMailDrawer/PaymentReceivedMailFields.tsx @@ -0,0 +1,77 @@ +// @ts-nocheck +import { Button, Intent } from '@blueprintjs/core'; +import { useFormikContext } from 'formik'; +import { FCheckbox, FFormGroup, FInputGroup, Group, Stack } from '@/components'; +import { useDrawerContext } from '@/components/Drawer/DrawerProvider'; +import { useDrawerActions } from '@/hooks/state'; +import { SendMailViewToAddressField } from '../../Estimates/SendMailViewDrawer/SendMailViewToAddressField'; +import { SendMailViewMessageField } from '../../Estimates/SendMailViewDrawer/SendMailViewMessageField'; + +const items = []; +const argsOptions = []; + +export function PaymentReceivedSendMailFields() { + // const items = useInvoiceMailItems(); + // const argsOptions = useSendInvoiceFormatArgsOptions(); + + return ( + + + + + + + + + + + + + + + + + ); +} + +function PaymentReceivedSendMailFooter() { + const { isSubmitting } = useFormikContext(); + const { name } = useDrawerContext(); + const { closeDrawer } = useDrawerActions(); + + const handleClose = () => { + closeDrawer(name); + }; + + return ( + + + + + + + + ); +} diff --git a/packages/webapp/src/containers/Sales/PaymentsReceived/PaymentReceivedMailDrawer/PaymentReceivedMailForm.tsx b/packages/webapp/src/containers/Sales/PaymentsReceived/PaymentReceivedMailDrawer/PaymentReceivedMailForm.tsx new file mode 100644 index 000000000..23ba08eae --- /dev/null +++ b/packages/webapp/src/containers/Sales/PaymentsReceived/PaymentReceivedMailDrawer/PaymentReceivedMailForm.tsx @@ -0,0 +1,81 @@ +import { Form, Formik, FormikHelpers } from 'formik'; +import { css } from '@emotion/css'; +import { Intent } from '@blueprintjs/core'; +import { PaymentReceivedSendMailFormSchema } from './_types'; +import { AppToaster } from '@/components'; +import { useSendSaleInvoiceMail } from '@/hooks/query'; +import { usePaymentReceivedSendMailBoot } from './PaymentReceivedMailBoot'; +import { useDrawerActions } from '@/hooks/state'; +import { useDrawerContext } from '@/components/Drawer/DrawerProvider'; +import { transformToForm } from '@/utils'; +import { PaymentReceivedSendMailFormValues } from './_types'; + +const initialValues: PaymentReceivedSendMailFormValues = { + subject: '', + message: '', + to: [], + cc: [], + bcc: [], + attachPdf: true, +}; + +interface PaymentReceivedSendMailFormProps { + children: React.ReactNode; +} + +export function PaymentReceivedSendMailForm({ + children, +}: PaymentReceivedSendMailFormProps) { + const { mutateAsync: sendInvoiceMail } = useSendSaleInvoiceMail(); + const { paymentReceivedId, paymentReceivedMailState } = + usePaymentReceivedSendMailBoot(); + + const { name } = useDrawerContext(); + const { closeDrawer } = useDrawerActions(); + + const _initialValues: PaymentReceivedSendMailFormValues = { + ...initialValues, + ...transformToForm(paymentReceivedMailState, initialValues), + }; + const handleSubmit = ( + values: PaymentReceivedSendMailFormValues, + { setSubmitting }: FormikHelpers, + ) => { + setSubmitting(true); + sendInvoiceMail({ id: paymentReceivedId, values: { ...values } }) + .then(() => { + AppToaster.show({ + message: 'The invoice mail has been sent to the customer.', + intent: Intent.SUCCESS, + }); + setSubmitting(false); + closeDrawer(name); + }) + .catch(() => { + setSubmitting(false); + AppToaster.show({ + message: 'Something went wrong!', + intent: Intent.SUCCESS, + }); + }); + }; + + return ( + +
+ {children} +
+
+ ); +} diff --git a/packages/webapp/src/containers/Sales/PaymentsReceived/PaymentReceivedMailDrawer/PaymentReceivedMailPreviewHeader.tsx b/packages/webapp/src/containers/Sales/PaymentsReceived/PaymentReceivedMailDrawer/PaymentReceivedMailPreviewHeader.tsx new file mode 100644 index 000000000..020dc5a98 --- /dev/null +++ b/packages/webapp/src/containers/Sales/PaymentsReceived/PaymentReceivedMailDrawer/PaymentReceivedMailPreviewHeader.tsx @@ -0,0 +1,13 @@ +import { SendViewPreviewHeader } from "../../Estimates/SendMailViewDrawer/SendMailViewPreviewHeader"; + +export function PaymentReceivedMailPreviewHeader() { + return ( + + ); +} diff --git a/packages/webapp/src/containers/Sales/PaymentsReceived/PaymentReceivedMailDrawer/PaymentReceivedMailPreviewPdf.tsx b/packages/webapp/src/containers/Sales/PaymentsReceived/PaymentReceivedMailDrawer/PaymentReceivedMailPreviewPdf.tsx new file mode 100644 index 000000000..0dd19ecae --- /dev/null +++ b/packages/webapp/src/containers/Sales/PaymentsReceived/PaymentReceivedMailDrawer/PaymentReceivedMailPreviewPdf.tsx @@ -0,0 +1,32 @@ +import { Spinner } from '@blueprintjs/core'; +import { Stack } from '@/components'; +import { useGetPaymentReceiveHtml } from '@/hooks/query'; +import { useDrawerContext } from '@/components/Drawer/DrawerProvider'; +import { SendMailViewPreviewPdfIframe } from '../../Estimates/SendMailViewDrawer/SendMailViewPreviewPdfIframe'; +import { PaymentReceivedMailPreviewHeader } from './PaymentReceivedMailPreviewHeader'; + +export function PaymentReceivedSendMailPreviewPdf() { + return ( + + + + + + + + ); +} + +function PaymentReceivedSendPdfPreviewIframe() { + const { payload } = useDrawerContext(); + const { data, isLoading } = useGetPaymentReceiveHtml( + payload?.paymentReceivedId, + ); + + if (isLoading && data) { + return ; + } + const iframeSrcDoc = data?.htmlContent; + + return ; +} diff --git a/packages/webapp/src/containers/Sales/PaymentsReceived/PaymentReceivedMailDrawer/PaymentReceivedMailPreviewReceipt.tsx b/packages/webapp/src/containers/Sales/PaymentsReceived/PaymentReceivedMailDrawer/PaymentReceivedMailPreviewReceipt.tsx new file mode 100644 index 000000000..45b23db46 --- /dev/null +++ b/packages/webapp/src/containers/Sales/PaymentsReceived/PaymentReceivedMailDrawer/PaymentReceivedMailPreviewReceipt.tsx @@ -0,0 +1,41 @@ +import { css } from '@emotion/css'; +import { PaymentReceivedMailReceipt } from './PaymentReceivedMailReceipt'; +import { PaymentReceivedMailPreviewHeader } from './PaymentReceivedMailPreviewHeader'; +import { Stack } from '@/components'; + +const defaultPaymentReceiptMailProps = { + companyName: 'Company Name', + companyLogoUri: 'https://via.placeholder.com/150', + primaryColor: 'rgb(0, 82, 204)', + paymentDate: '2021-01-01', + paymentDateLabel: 'Payment Date', + total: '100.00', + totalLabel: 'Total', + paymentNumber: '123456', + paymentNumberLabel: 'Payment #', + message: 'Thank you for your payment!', + subtotal: '100.00', + subtotalLabel: 'Subtotal', + items: [{ label: 'Invoice 1', total: '100.00' }], +}; + +export function PaymentReceivedMailPreviewReceipt() { + return ( + + + + + + + + ); +} diff --git a/packages/webapp/src/containers/Sales/PaymentsReceived/PaymentReceivedMailDrawer/PaymentReceivedMailPreviewTabs.tsx b/packages/webapp/src/containers/Sales/PaymentsReceived/PaymentReceivedMailDrawer/PaymentReceivedMailPreviewTabs.tsx new file mode 100644 index 000000000..687970d76 --- /dev/null +++ b/packages/webapp/src/containers/Sales/PaymentsReceived/PaymentReceivedMailDrawer/PaymentReceivedMailPreviewTabs.tsx @@ -0,0 +1,39 @@ +import { lazy, Suspense } from 'react'; +import { Tab } from '@blueprintjs/core'; +import { SendMailViewPreviewTabs } from '../../Estimates/SendMailViewDrawer/SendMailViewPreviewTabs'; + +const PaymentReceivedMailPreviewReceipt = lazy(() => + import('./PaymentReceivedMailPreviewReceipt').then((module) => ({ + default: module.PaymentReceivedMailPreviewReceipt, + })), +); +const PaymentReceivedSendMailPreviewPdf = lazy(() => + import('./PaymentReceivedMailPreviewPdf').then((module) => ({ + default: module.PaymentReceivedSendMailPreviewPdf, + })), +); + +export function PaymentReceivedSendMailPreview() { + return ( + + + + + } + /> + + + + } + /> + + ); +} diff --git a/packages/webapp/src/containers/Sales/PaymentsReceived/PaymentReceivedMailDrawer/PaymentReceivedMailReceipt.tsx b/packages/webapp/src/containers/Sales/PaymentsReceived/PaymentReceivedMailDrawer/PaymentReceivedMailReceipt.tsx new file mode 100644 index 000000000..0353b82da --- /dev/null +++ b/packages/webapp/src/containers/Sales/PaymentsReceived/PaymentReceivedMailDrawer/PaymentReceivedMailReceipt.tsx @@ -0,0 +1,145 @@ +import { x } from '@xstyled/emotion'; +import { Group, Stack } from '@/components'; +import { + SendMailReceipt, + SendMailReceiptProps, +} from '../../Estimates/SendMailViewDrawer/SendMailViewReceiptPreview'; + +export interface PaymentReceivedMailReceiptProps extends SendMailReceiptProps { + // # Company + companyName: string; + companyLogoUri?: string; + + // # Colors + primaryColor?: string; + + // # Payment date + paymentDate: string; + paymentDateLabel?: string; + + // # Total + total: string; + totalLabel?: string; + + // # Subtotal + subtotal: string; + subtotalLabel?: string; + + // # Invoice number + paymentNumber: string; + paymentNumberLabel?: string; + + // # Mail message + message: string; + + // # Paid Invoices + items?: Array<{ label: string; total: string }>; +} + +export function PaymentReceivedMailReceipt({ + // # Company + companyName, + companyLogoUri, + + // # Colors + primaryColor = 'rgb(0, 82, 204)', + + // # Payment date + paymentDate, + paymentDateLabel = 'Payment Date', + + // # Total + total, + totalLabel = 'Total', + + // # Payment number + paymentNumber, + paymentNumberLabel = 'Payment #', + + // # Mail message + message, + + // # Subtotal + subtotal, + subtotalLabel = 'Subtotal', + + // # Paid Invoices + items = [], + + ...restProps +}: PaymentReceivedMailReceiptProps) { + return ( + + + {companyLogoUri && } + + + + {companyName} + + + + {total} + + + + {paymentNumberLabel} {paymentNumber} + + + + {paymentDateLabel} {paymentDate} + + + + + + {message} + + + + {items?.map((item, key) => ( + + {item.label} + {item.total} + + ))} + + + {subtotalLabel} + + {subtotal} + + + + + {totalLabel} + + {total} + + + + + ); +} diff --git a/packages/webapp/src/containers/Sales/PaymentsReceived/PaymentReceivedMailDrawer/_types.ts b/packages/webapp/src/containers/Sales/PaymentsReceived/PaymentReceivedMailDrawer/_types.ts new file mode 100644 index 000000000..0813c4888 --- /dev/null +++ b/packages/webapp/src/containers/Sales/PaymentsReceived/PaymentReceivedMailDrawer/_types.ts @@ -0,0 +1,20 @@ +import * as Yup from 'yup'; + +export const PaymentReceivedSendMailFormSchema = 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 address')) + .required('To address is required'), + cc: Yup.array().of(Yup.string().email('Invalid email address')), + bcc: Yup.array().of(Yup.string().email('Invalid email address')), +}); + +export interface PaymentReceivedSendMailFormValues { + subject: string; + message: string; + to: string[]; + cc: string[]; + bcc: string[]; + attachPdf: boolean; +} diff --git a/packages/webapp/src/containers/Sales/PaymentsReceived/PaymentReceivedMailDrawer/index.ts b/packages/webapp/src/containers/Sales/PaymentsReceived/PaymentReceivedMailDrawer/index.ts new file mode 100644 index 000000000..0e24cac5f --- /dev/null +++ b/packages/webapp/src/containers/Sales/PaymentsReceived/PaymentReceivedMailDrawer/index.ts @@ -0,0 +1 @@ +export * from './PaymentReceivedMailDrawer'; diff --git a/packages/webapp/src/containers/Sales/PaymentsReceived/PaymentsLanding/PaymentsReceivedTable.tsx b/packages/webapp/src/containers/Sales/PaymentsReceived/PaymentsLanding/PaymentsReceivedTable.tsx index aa7bc18f3..073f22c75 100644 --- a/packages/webapp/src/containers/Sales/PaymentsReceived/PaymentsLanding/PaymentsReceivedTable.tsx +++ b/packages/webapp/src/containers/Sales/PaymentsReceived/PaymentsLanding/PaymentsReceivedTable.tsx @@ -80,7 +80,7 @@ function PaymentsReceivedDataTable({ // Handle mail send payment receive. const handleSendMailPayment = ({ id }) => { - openDialog(DialogsName.PaymentMail, { paymentReceiveId: id }); + openDrawer(DRAWERS.PAYMENT_RECEIVED_SEND_MAIL, { paymentReceivedId: id }); }; // Handle cell click. diff --git a/packages/webapp/src/hooks/query/paymentReceives.tsx b/packages/webapp/src/hooks/query/paymentReceives.tsx index 8a40cc818..1d6270a71 100644 --- a/packages/webapp/src/hooks/query/paymentReceives.tsx +++ b/packages/webapp/src/hooks/query/paymentReceives.tsx @@ -301,3 +301,35 @@ export function usePaymentReceivedState( }, ); } + +interface PaymentReceivedHtmlResponse { + htmlContent: string; +} + +/** + * Retrieves the html content of the given payment receive. + * @param {number} paymentReceivedId + * @param {UseQueryOptions} options + * @returns {UseQueryResult} + */ +export function useGetPaymentReceiveHtml( + paymentReceivedId: number, + options?: UseQueryOptions, +): UseQueryResult { + const apiRequest = useApiRequest(); + + return useQuery( + ['PAYMENT_RECEIVED_HTML', paymentReceivedId], + () => + apiRequest + .get(`/sales/payment_receives/${paymentReceivedId}`, { + headers: { + Accept: 'application/json+html', + }, + }) + .then((res) => transformToCamelCase(res.data)), + { + ...options, + }, + ); +}