feat: send invoice receipt preview

This commit is contained in:
Ahmed Bouhuolia
2024-10-31 12:40:48 +02:00
parent 470bfd32f7
commit dbbaa387bd
27 changed files with 383 additions and 476 deletions

View File

@@ -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 (
<Dialog
name={dialogName}
title={'Invoice Mail'}
isOpen={isOpen}
canEscapeJeyClose={false}
isCloseButtonShown={false}
autoFocus={true}
style={{ width: 600 }}
>
<DialogSuspense>
<InvoiceFormMailDeliverDialogContent
dialogName={dialogName}
invoiceId={invoiceId}
/>
</DialogSuspense>
</Dialog>
);
}
export default compose(withDialogRedux())(InvoiceFormMailDeliverDialog);

View File

@@ -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 (
<InvoiceMailDialogContent
invoiceId={invoiceId}
onFormSubmit={handleSubmit}
onCancelClick={handleCancel}
/>
);
}
export default R.compose(withDialogActions)(
InvoiceFormDeliverDialogContentRoot,
);

View File

@@ -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}
/>
<InvoiceFormMailDeliverDialog
dialogName={DialogsName.InvoiceFormMailDeliver}
/>
</>
);
}

View File

@@ -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 (
<Dialog
name={dialogName}
title={'Invoice Mail'}
isOpen={isOpen}
canEscapeJeyClose={false}
isCloseButtonShown={false}
autoFocus={true}
style={{ width: 600 }}
>
<DialogSuspense>
<InvoiceMailDialogBody invoiceId={invoiceId} />
</DialogSuspense>
</Dialog>
);
}
export default compose(withDialogRedux())(InvoiceMailDialog);

View File

@@ -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 (
<InvoiceMailDialogContent
invoiceId={invoiceId}
onCancelClick={handleCancelClick}
onFormSubmit={handleSubmitClick}
/>
);
}
export default R.compose(withDialogActions)(InvoiceMailDialogBodyRoot);

View File

@@ -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<InvoiceMailDialogBootValues>();
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 (
<DialogContent isLoading={isMailOptionsLoading}>
<InvoiceMailDialagBoot.Provider value={provider} {...props} />
</DialogContent>
);
}
const useInvoiceMailDialogBoot = () =>
React.useContext<InvoiceMailDialogBootValues>(InvoiceMailDialagBoot);
export { InvoiceMailDialogBoot, useInvoiceMailDialogBoot };

View File

@@ -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 (
<InvoiceMailDialogBoot invoiceId={invoiceId}>
<InvoiceMailDialogForm
onFormSubmit={onFormSubmit}
onCancelClick={onCancelClick}
/>
</InvoiceMailDialogBoot>
);
}

View File

@@ -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'),
});

View File

@@ -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 (
<Formik
initialValues={initialValues}
validationSchema={InvoiceMailFormSchema}
onSubmit={handleSubmit}
>
<InvoiceMailDialogFormContent onClose={handleClose} />
</Formik>
);
}

View File

@@ -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 (
<Form>
<div className={Classes.DIALOG_BODY}>
<MailNotificationForm
fromAddresses={mailOptions.from_addresses}
toAddresses={mailOptions.to_addresses}
/>
<AttachFormGroup name={'attachInvoice'} inline>
<FSwitch name={'attachInvoice'} label={'Attach Invoice'} />
</AttachFormGroup>
</div>
<div className={Classes.DIALOG_FOOTER}>
<div className={Classes.DIALOG_FOOTER_ACTIONS}>
<Button
disabled={isSubmitting}
onClick={handleClose}
style={{ minWidth: '65px' }}
>
Close
</Button>
<Button
intent={Intent.PRIMARY}
loading={isSubmitting}
style={{ minWidth: '75px' }}
type="submit"
>
Send
</Button>
</div>
</div>
</Form>
);
}
const AttachFormGroup = styled(FFormGroup)`
background: #f8f9fb;
margin-top: 0.6rem;
padding: 4px 14px;
border-radius: 5px;
border: 1px solid #dcdcdd;
`;

View File

@@ -1,2 +0,0 @@
export * from './InvoiceMailDialog';
export * from './InvoiceMailDialogContent';

View File

@@ -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 (
<InvoiceSendMailPreviewWithHeader>
<Box px={4} pt={8} pb={16}>
<InvoiceMailReceiptPreview
total={invoice.total_formatted}
dueDate={invoice.due_date_formatted}
invoiceNumber={invoice.invoice_no}
companyName={invoiceMailState?.companyName}
// companyLogoUri={invoiceMailState?.companyLogoUri}
primaryColor={invoiceMailState?.primaryColor}
total={invoiceMailState?.totalFormatted}
dueDate={invoiceMailState?.dueDateFormatted}
dueAmount={invoiceMailState?.dueAmountFormatted}
invoiceNumber={invoiceMailState?.invoiceNo}
items={items}
message={mailMessage}
className={css`
@@ -37,4 +42,4 @@ export function InvoiceMailReceiptPreviewConneceted() {
</Box>
</InvoiceSendMailPreviewWithHeader>
);
}
}

View File

@@ -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 <Spinner size={20} />;
}
const value = {
invoice,
isInvoiceLoading,
invoiceId,
invoiceMailOptions,
isInvoiceMailOptionsLoading,
// # Invoice mail options
isInvoiceMailState,
invoiceMailState,
};
return (

View File

@@ -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,

View File

@@ -7,6 +7,7 @@ import { useDrawerActions } from '@/hooks/state';
interface ElementCustomizeHeaderProps {
label?: string;
children?: React.ReactNode;
closeButton?: boolean;
}
export function InvoiceSendMailHeader({

View File

@@ -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 (
<Stack
@@ -43,12 +46,12 @@ export function InvoiceSendMailHeaderPreview() {
<Group spacing={2}>
<Box fontWeight={600}>Ahmed </Box>
<Box color={'#738091'}>
&lt;messaging-service@post.xero.com&gt;
&lt;messaging-service@post.bigcapital.app&gt;
</Box>
</Group>
<Box fontSize={'sm'} color={'#738091'}>
Reply to: Ahmed &lt;a.m.bouhuolia@gmail.com&gt;
Reply to: {invoiceMailState?.companyName} {toAddresses};
</Box>
</Stack>
</Group>
@@ -65,8 +68,15 @@ export function InvoiceSendMailPreviewWithHeader({
return (
<Box>
<InvoiceSendMailHeaderPreview />
<Box>{children}</Box>
</Box>
);
}
export const useMailHeaderToAddresses = () => {
const {
values: { to },
} = useSendInvoiceMailForm();
return useMemo(() => to?.map((email) => '<' + email + '>').join(' '), [to]);
};

View File

@@ -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() {
<Tab
id={'payment-page'}
title={'Payment page'}
panel={<InvoiceMailReceiptPreviewConneceted />}
panel={
<Suspense>
<InvoiceMailReceiptPreviewConneceted />
</Suspense>
}
/>
<Tab
id="pdf-document"
title={'PDF document'}
panel={<InvoiceSendPdfPreviewConnected />}
panel={
<Suspense>
<InvoiceSendPdfPreviewConnected />
</Suspense>
}
/>
</Tabs>
</Stack>

View File

@@ -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 (
<InvoiceSendMailPreviewWithHeader>
<Box px={4} py={6}>
<InvoicePaperTemplate
dueDate={invoice.due_date_formatted}
dateIssue={invoice.invoice_date_formatted}
invoiceNumber={invoice.invoice_no}
total={invoice.total_formatted}
subtotal={invoice.subtotal}
discount={''}
paymentMade={''}
balanceDue={invoice.due_amount_Formatted}
statement={invoice.statement}
className={css`
margin: 0 auto;
`}

View File

@@ -5,6 +5,10 @@ import { chain, defaultTo, mapKeys, snakeCase, startCase } from 'lodash';
import { InvoiceSendMailFormValues } from './_types';
import { useInvoiceSendMailBoot } from './InvoiceSendMailContentBoot';
export const useSendInvoiceMailForm = () => {
return useFormikContext<InvoiceSendMailFormValues>();
};
export const useInvoiceMailItems = () => {
const { values } = useFormikContext<InvoiceSendMailFormValues>();
const cc = values?.cc || [];
@@ -21,13 +25,13 @@ export const useInvoiceMailItems = () => {
};
export const useSendInvoiceMailFormatArgs = (): Record<string, string> => {
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 => {