mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-15 20:30:33 +00:00
Merge pull request #292 from bigcapitalhq/big-83-send-an-invoice-to-the-customer-email
feat: Send mail notifications of the sales transactions
This commit is contained in:
@@ -26,6 +26,7 @@ import {
|
||||
|
||||
import { compose } from '@/utils';
|
||||
import { DRAWERS } from '@/constants/drawers';
|
||||
import { DialogsName } from '@/constants/dialogs';
|
||||
|
||||
/**
|
||||
* Estimate read-only details actions bar of the drawer.
|
||||
@@ -65,6 +66,10 @@ function EstimateDetailActionsBar({
|
||||
const handleNotifyViaSMS = () => {
|
||||
openDialog('notify-estimate-via-sms', { estimateId });
|
||||
};
|
||||
// Handles the estimate mail dialog.
|
||||
const handleMailEstimate = () => {
|
||||
openDialog(DialogsName.EstimateMail, { estimateId });
|
||||
};
|
||||
|
||||
return (
|
||||
<DrawerActionsBar>
|
||||
@@ -79,12 +84,19 @@ function EstimateDetailActionsBar({
|
||||
<NavbarDivider />
|
||||
</Can>
|
||||
<Can I={SaleEstimateAction.View} a={AbilitySubject.Estimate}>
|
||||
<Button
|
||||
className={Classes.MINIMAL}
|
||||
icon={<Icon icon="envelope" />}
|
||||
text={'Send Mail'}
|
||||
onClick={handleMailEstimate}
|
||||
/>
|
||||
<Button
|
||||
className={Classes.MINIMAL}
|
||||
icon={<Icon icon="print-16" />}
|
||||
text={<T id={'print'} />}
|
||||
onClick={handlePrintEstimate}
|
||||
/>
|
||||
<NavbarDivider />
|
||||
</Can>
|
||||
<Can I={SaleEstimateAction.Delete} a={AbilitySubject.Estimate}>
|
||||
<Button
|
||||
|
||||
@@ -31,6 +31,7 @@ import {
|
||||
import { compose } from '@/utils';
|
||||
import { BadDebtMenuItem } from './utils';
|
||||
import { DRAWERS } from '@/constants/drawers';
|
||||
import { DialogsName } from '@/constants/dialogs';
|
||||
|
||||
/**
|
||||
* Invoice details action bar.
|
||||
@@ -93,6 +94,10 @@ function InvoiceDetailActionsBar({
|
||||
openAlert('cancel-bad-debt', { invoiceId });
|
||||
};
|
||||
|
||||
const handleMailInvoice = () => {
|
||||
openDialog(DialogsName.InvoiceMail, { invoiceId });
|
||||
};
|
||||
|
||||
return (
|
||||
<DrawerActionsBar>
|
||||
<NavbarGroup>
|
||||
@@ -117,12 +122,19 @@ function InvoiceDetailActionsBar({
|
||||
<NavbarDivider />
|
||||
</Can>
|
||||
<Can I={SaleInvoiceAction.View} a={AbilitySubject.Invoice}>
|
||||
<Button
|
||||
text={'Send Mail'}
|
||||
icon={<Icon icon="envelope" />}
|
||||
onClick={handleMailInvoice}
|
||||
className={Classes.MINIMAL}
|
||||
/>
|
||||
<Button
|
||||
className={Classes.MINIMAL}
|
||||
icon={<Icon icon="print-16" />}
|
||||
text={<T id={'print'} />}
|
||||
onClick={handlePrintInvoice}
|
||||
/>
|
||||
<NavbarDivider />
|
||||
</Can>
|
||||
<Can I={SaleInvoiceAction.Delete} a={AbilitySubject.Invoice}>
|
||||
<Button
|
||||
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
|
||||
import { compose } from '@/utils';
|
||||
import { DRAWERS } from '@/constants/drawers';
|
||||
import { DialogsName } from '@/constants/dialogs';
|
||||
|
||||
/**
|
||||
* Payment receive actions bar.
|
||||
@@ -68,6 +69,10 @@ function PaymentReceiveActionsBar({
|
||||
openDialog('payment-pdf-preview', { paymentReceiveId });
|
||||
};
|
||||
|
||||
const handleMailPaymentReceive = () => {
|
||||
openDialog(DialogsName.PaymentMail, { paymentReceiveId });
|
||||
};
|
||||
|
||||
return (
|
||||
<DrawerActionsBar>
|
||||
<NavbarGroup>
|
||||
@@ -81,12 +86,19 @@ function PaymentReceiveActionsBar({
|
||||
<NavbarDivider />
|
||||
</Can>
|
||||
<Can I={PaymentReceiveAction.View} a={AbilitySubject.PaymentReceive}>
|
||||
<Button
|
||||
className={Classes.MINIMAL}
|
||||
text={'Send Mail'}
|
||||
icon={<Icon icon="envelope" />}
|
||||
onClick={handleMailPaymentReceive}
|
||||
/>
|
||||
<Button
|
||||
className={Classes.MINIMAL}
|
||||
icon={<Icon icon="print-16" />}
|
||||
text={<T id={'print'} />}
|
||||
onClick={handlePrintPaymentReceive}
|
||||
/>
|
||||
<NavbarDivider />
|
||||
</Can>
|
||||
<Can I={PaymentReceiveAction.Delete} a={AbilitySubject.PaymentReceive}>
|
||||
<Button
|
||||
|
||||
@@ -24,6 +24,7 @@ import { useReceiptDetailDrawerContext } from './ReceiptDetailDrawerProvider';
|
||||
import { SaleReceiptAction, AbilitySubject } from '@/constants/abilityOption';
|
||||
import { safeCallback, compose } from '@/utils';
|
||||
import { DRAWERS } from '@/constants/drawers';
|
||||
import { DialogsName } from '@/constants/dialogs';
|
||||
|
||||
/**
|
||||
* Receipt details actions bar.
|
||||
@@ -60,6 +61,9 @@ function ReceiptDetailActionBar({
|
||||
const handleNotifyViaSMS = () => {
|
||||
openDialog('notify-receipt-via-sms', { receiptId });
|
||||
};
|
||||
const handleReceiptMail = () => {
|
||||
openDialog(DialogsName.ReceiptMail, { receiptId });
|
||||
};
|
||||
|
||||
return (
|
||||
<DrawerActionsBar>
|
||||
@@ -74,6 +78,12 @@ function ReceiptDetailActionBar({
|
||||
<NavbarDivider />
|
||||
</Can>
|
||||
<Can I={SaleReceiptAction.View} a={AbilitySubject.Receipt}>
|
||||
<Button
|
||||
className={Classes.MINIMAL}
|
||||
text={'Send Mail'}
|
||||
icon={<Icon icon="envelope" />}
|
||||
onClick={handleReceiptMail}
|
||||
/>
|
||||
<Button
|
||||
className={Classes.MINIMAL}
|
||||
icon={<Icon icon="print-16" />}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import { Dialog, DialogSuspense } from '@/components';
|
||||
import withDialogRedux from '@/components/DialogReduxConnect';
|
||||
import { compose } from '@/utils';
|
||||
|
||||
const EstimateMailDialogContent = React.lazy(
|
||||
() => import('./EstimateMailDialogContent'),
|
||||
);
|
||||
|
||||
/**
|
||||
* Invoice mail dialog.
|
||||
*/
|
||||
function EstimateMailDialog({
|
||||
dialogName,
|
||||
payload: { estimateId = null },
|
||||
isOpen,
|
||||
}) {
|
||||
return (
|
||||
<Dialog
|
||||
name={dialogName}
|
||||
title={'Estimate Mail'}
|
||||
isOpen={isOpen}
|
||||
canEscapeJeyClose={true}
|
||||
autoFocus={true}
|
||||
style={{ width: 600 }}
|
||||
>
|
||||
<DialogSuspense>
|
||||
<EstimateMailDialogContent
|
||||
dialogName={dialogName}
|
||||
estimateId={estimateId}
|
||||
/>
|
||||
</DialogSuspense>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
export default compose(withDialogRedux())(EstimateMailDialog);
|
||||
@@ -0,0 +1,44 @@
|
||||
// @ts-nocheck
|
||||
import React, { createContext } from 'react';
|
||||
import { useSaleEstimateDefaultOptions } from '@/hooks/query';
|
||||
import { DialogContent } from '@/components';
|
||||
|
||||
interface EstimateMailDialogBootValues {
|
||||
estimateId: number;
|
||||
mailOptions: any;
|
||||
}
|
||||
|
||||
const EstimateMailDialagBoot = createContext<EstimateMailDialogBootValues>();
|
||||
|
||||
interface EstimateMailDialogBootProps {
|
||||
estimateId: number;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Estimate mail dialog boot provider.
|
||||
*/
|
||||
function EstimateMailDialogBoot({
|
||||
estimateId,
|
||||
...props
|
||||
}: EstimateMailDialogBootProps) {
|
||||
const { data: mailOptions, isLoading: isMailOptionsLoading } =
|
||||
useSaleEstimateDefaultOptions(estimateId);
|
||||
|
||||
const provider = {
|
||||
saleEstimateId: estimateId,
|
||||
mailOptions,
|
||||
isMailOptionsLoading,
|
||||
};
|
||||
|
||||
return (
|
||||
<DialogContent isLoading={isMailOptionsLoading}>
|
||||
<EstimateMailDialagBoot.Provider value={provider} {...props} />
|
||||
</DialogContent>
|
||||
);
|
||||
}
|
||||
|
||||
const useEstimateMailDialogBoot = () =>
|
||||
React.useContext<EstimateMailDialogBootValues>(EstimateMailDialagBoot);
|
||||
|
||||
export { EstimateMailDialogBoot, useEstimateMailDialogBoot };
|
||||
@@ -0,0 +1,17 @@
|
||||
import { EstimateMailDialogBoot } from './EstimateMailDialogBoot';
|
||||
import { EstimateMailDialogForm } from './EstimateMailDialogForm';
|
||||
|
||||
interface EstimateMailDialogContentProps {
|
||||
dialogName: string;
|
||||
estimateId: number;
|
||||
}
|
||||
export default function EstimateMailDialogContent({
|
||||
dialogName,
|
||||
estimateId,
|
||||
}: EstimateMailDialogContentProps) {
|
||||
return (
|
||||
<EstimateMailDialogBoot estimateId={estimateId}>
|
||||
<EstimateMailDialogForm />
|
||||
</EstimateMailDialogBoot>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
// @ts-nocheck
|
||||
import { Formik } from 'formik';
|
||||
import * as R from 'ramda';
|
||||
import { useEstimateMailDialogBoot } from './EstimateMailDialogBoot';
|
||||
import { DialogsName } from '@/constants/dialogs';
|
||||
import withDialogActions from '@/containers/Dialog/withDialogActions';
|
||||
import { useSendSaleEstimateMail } from '@/hooks/query';
|
||||
import { EstimateMailDialogFormContent } from './EstimateMailDialogFormContent';
|
||||
import {
|
||||
initialMailNotificationValues,
|
||||
MailNotificationFormValues,
|
||||
transformMailFormToInitialValues,
|
||||
transformMailFormToRequest,
|
||||
} from '@/containers/SendMailNotification/utils';
|
||||
import { Intent } from '@blueprintjs/core';
|
||||
import { AppToaster } from '@/components';
|
||||
|
||||
const initialFormValues = {
|
||||
...initialMailNotificationValues,
|
||||
attachEstimate: true,
|
||||
};
|
||||
|
||||
interface EstimateMailFormValues extends MailNotificationFormValues {
|
||||
attachEstimate: boolean;
|
||||
}
|
||||
|
||||
function EstimateMailDialogFormRoot({
|
||||
// #withDialogClose
|
||||
closeDialog,
|
||||
}) {
|
||||
const { mutateAsync: sendEstimateMail } = useSendSaleEstimateMail();
|
||||
const { mailOptions, saleEstimateId } = useEstimateMailDialogBoot();
|
||||
|
||||
const initialValues = transformMailFormToInitialValues(
|
||||
mailOptions,
|
||||
initialFormValues,
|
||||
);
|
||||
// Handle the form submitting.
|
||||
const handleSubmit = (values: EstimateMailFormValues, { setSubmitting }) => {
|
||||
const reqValues = transformMailFormToRequest(values);
|
||||
|
||||
setSubmitting(true);
|
||||
sendEstimateMail([saleEstimateId, reqValues])
|
||||
.then(() => {
|
||||
AppToaster.show({
|
||||
message: 'The mail notification has been sent successfully.',
|
||||
intent: Intent.SUCCESS,
|
||||
});
|
||||
closeDialog(DialogsName.EstimateMail);
|
||||
setSubmitting(false);
|
||||
})
|
||||
.catch((error) => {
|
||||
setSubmitting(false);
|
||||
closeDialog(DialogsName.EstimateMail);
|
||||
AppToaster.show({
|
||||
message: 'Something went wrong.',
|
||||
intent: Intent.DANGER,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
closeDialog(DialogsName.EstimateMail);
|
||||
};
|
||||
|
||||
return (
|
||||
<Formik initialValues={initialValues} onSubmit={handleSubmit}>
|
||||
<EstimateMailDialogFormContent onClose={handleClose} />
|
||||
</Formik>
|
||||
);
|
||||
}
|
||||
|
||||
export const EstimateMailDialogForm = R.compose(withDialogActions)(
|
||||
EstimateMailDialogFormRoot,
|
||||
);
|
||||
@@ -0,0 +1,66 @@
|
||||
// @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 { useEstimateMailDialogBoot } from './EstimateMailDialogBoot';
|
||||
|
||||
interface EstimateMailDialogFormContentProps {
|
||||
onClose?: () => void;
|
||||
}
|
||||
|
||||
export function EstimateMailDialogFormContent({
|
||||
onClose,
|
||||
}: EstimateMailDialogFormContentProps) {
|
||||
const { isSubmitting } = useFormikContext();
|
||||
const { mailOptions } = useEstimateMailDialogBoot();
|
||||
|
||||
const handleClose = () => {
|
||||
saveInvoke(onClose);
|
||||
};
|
||||
|
||||
return (
|
||||
<Form>
|
||||
<div className={Classes.DIALOG_BODY}>
|
||||
<MailNotificationForm
|
||||
fromAddresses={mailOptions.from_addresses}
|
||||
toAddresses={mailOptions.to_addresses}
|
||||
/>
|
||||
<AttachFormGroup name={'attachEstimate'} inline>
|
||||
<FSwitch name={'attachEstimate'} label={'Attach Estimate'} />
|
||||
</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;
|
||||
`;
|
||||
@@ -0,0 +1 @@
|
||||
export * from './EstimateMailDialog';
|
||||
@@ -22,6 +22,7 @@ import { useEstimatesListContext } from './EstimatesListProvider';
|
||||
import { useMemorizedColumnsWidths } from '@/hooks';
|
||||
import { compose } from '@/utils';
|
||||
import { DRAWERS } from '@/constants/drawers';
|
||||
import { DialogsName } from '@/constants/dialogs';
|
||||
|
||||
/**
|
||||
* Estimates datatable.
|
||||
@@ -100,6 +101,11 @@ function EstimatesDataTable({
|
||||
openDrawer(DRAWERS.ESTIMATE_DETAILS, { estimateId: cell.row.original.id });
|
||||
};
|
||||
|
||||
// Handle mail send estimate.
|
||||
const handleMailSendEstimate = ({ id }) => {
|
||||
openDialog(DialogsName.EstimateMail, { estimateId: id });
|
||||
}
|
||||
|
||||
// Local storage memorizing columns widths.
|
||||
const [initialColumnsWidths, , handleColumnResizing] =
|
||||
useMemorizedColumnsWidths(TABLES.ESTIMATES);
|
||||
@@ -153,6 +159,7 @@ function EstimatesDataTable({
|
||||
onConvert: handleConvertToInvoice,
|
||||
onViewDetails: handleViewDetailEstimate,
|
||||
onPrint: handlePrintEstimate,
|
||||
onSendMail: handleMailSendEstimate,
|
||||
}}
|
||||
/>
|
||||
</DashboardContentTable>
|
||||
|
||||
@@ -64,6 +64,7 @@ export function ActionsMenu({
|
||||
onConvert,
|
||||
onViewDetails,
|
||||
onPrint,
|
||||
onSendMail
|
||||
},
|
||||
}) {
|
||||
return (
|
||||
@@ -129,6 +130,11 @@ export function ActionsMenu({
|
||||
</Choose>
|
||||
</Can>
|
||||
<Can I={SaleEstimateAction.View} a={AbilitySubject.Estimate}>
|
||||
<MenuItem
|
||||
icon={<Icon icon={'envelope'} iconSize={16} />}
|
||||
text={'Send Mail'}
|
||||
onClick={safeCallback(onSendMail, original)}
|
||||
/>
|
||||
<MenuItem
|
||||
icon={<Icon icon={'print-16'} iconSize={16} />}
|
||||
text={intl.get('print')}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import { Dialog, DialogSuspense } from '@/components';
|
||||
import withDialogRedux from '@/components/DialogReduxConnect';
|
||||
import { compose } from '@/utils';
|
||||
|
||||
const InvoiceMailDialogContent = React.lazy(
|
||||
() => import('./InvoiceMailDialogContent'),
|
||||
);
|
||||
|
||||
/**
|
||||
* Invoice mail dialog.
|
||||
*/
|
||||
function InvoiceMailDialog({
|
||||
dialogName,
|
||||
payload: { invoiceId = null },
|
||||
isOpen,
|
||||
}) {
|
||||
return (
|
||||
<Dialog
|
||||
name={dialogName}
|
||||
title={'Invoice Mail'}
|
||||
isOpen={isOpen}
|
||||
canEscapeJeyClose={true}
|
||||
autoFocus={true}
|
||||
style={{ width: 600 }}
|
||||
>
|
||||
<DialogSuspense>
|
||||
<InvoiceMailDialogContent
|
||||
dialogName={dialogName}
|
||||
invoiceId={invoiceId}
|
||||
/>
|
||||
</DialogSuspense>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
export default compose(withDialogRedux())(InvoiceMailDialog);
|
||||
@@ -0,0 +1,44 @@
|
||||
// @ts-nocheck
|
||||
import React, { createContext } from 'react';
|
||||
import { useSaleInvoiceDefaultOptions } from '@/hooks/query';
|
||||
import { DialogContent } from '@/components';
|
||||
|
||||
interface InvoiceMailDialogBootValues {
|
||||
invoiceId: number;
|
||||
mailOptions: any;
|
||||
}
|
||||
|
||||
const InvoiceMailDialagBoot = createContext<InvoiceMailDialogBootValues>();
|
||||
|
||||
interface InvoiceMailDialogBootProps {
|
||||
invoiceId: number;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoice mail dialog boot provider.
|
||||
*/
|
||||
function InvoiceMailDialogBoot({
|
||||
invoiceId,
|
||||
...props
|
||||
}: InvoiceMailDialogBootProps) {
|
||||
const { data: mailOptions, isLoading: isMailOptionsLoading } =
|
||||
useSaleInvoiceDefaultOptions(invoiceId);
|
||||
|
||||
const provider = {
|
||||
saleInvoiceId: invoiceId,
|
||||
mailOptions,
|
||||
isMailOptionsLoading,
|
||||
};
|
||||
|
||||
return (
|
||||
<DialogContent isLoading={isMailOptionsLoading}>
|
||||
<InvoiceMailDialagBoot.Provider value={provider} {...props} />
|
||||
</DialogContent>
|
||||
);
|
||||
}
|
||||
|
||||
const useInvoiceMailDialogBoot = () =>
|
||||
React.useContext<InvoiceMailDialogBootValues>(InvoiceMailDialagBoot);
|
||||
|
||||
export { InvoiceMailDialogBoot, useInvoiceMailDialogBoot };
|
||||
@@ -0,0 +1,17 @@
|
||||
import { InvoiceMailDialogBoot } from './InvoiceMailDialogBoot';
|
||||
import { InvoiceMailDialogForm } from './InvoiceMailDialogForm';
|
||||
|
||||
interface InvoiceMailDialogContentProps {
|
||||
dialogName: string;
|
||||
invoiceId: number;
|
||||
}
|
||||
export default function InvoiceMailDialogContent({
|
||||
dialogName,
|
||||
invoiceId,
|
||||
}: InvoiceMailDialogContentProps) {
|
||||
return (
|
||||
<InvoiceMailDialogBoot invoiceId={invoiceId}>
|
||||
<InvoiceMailDialogForm />
|
||||
</InvoiceMailDialogBoot>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
// @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'),
|
||||
});
|
||||
@@ -0,0 +1,79 @@
|
||||
// @ts-nocheck
|
||||
import { Formik } from 'formik';
|
||||
import * as R from 'ramda';
|
||||
import { Intent } from '@blueprintjs/core';
|
||||
import { useInvoiceMailDialogBoot } from './InvoiceMailDialogBoot';
|
||||
import { DialogsName } from '@/constants/dialogs';
|
||||
import { AppToaster } from '@/components';
|
||||
import { useSendSaleInvoiceMail } from '@/hooks/query';
|
||||
import withDialogActions from '@/containers/Dialog/withDialogActions';
|
||||
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;
|
||||
}
|
||||
|
||||
function InvoiceMailDialogFormRoot({
|
||||
// #withDialogActions
|
||||
closeDialog,
|
||||
}) {
|
||||
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,
|
||||
});
|
||||
closeDialog(DialogsName.InvoiceMail);
|
||||
setSubmitting(false);
|
||||
})
|
||||
.catch(() => {
|
||||
AppToaster.show({
|
||||
message: 'Something went wrong.',
|
||||
intent: Intent.DANGER,
|
||||
});
|
||||
setSubmitting(false);
|
||||
});
|
||||
};
|
||||
// Handle the close button click.
|
||||
const handleClose = () => {
|
||||
closeDialog(DialogsName.InvoiceMail);
|
||||
};
|
||||
|
||||
return (
|
||||
<Formik
|
||||
initialValues={initialValues}
|
||||
validationSchema={InvoiceMailFormSchema}
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
<InvoiceMailDialogFormContent onClose={handleClose} />
|
||||
</Formik>
|
||||
);
|
||||
}
|
||||
|
||||
export const InvoiceMailDialogForm = R.compose(withDialogActions)(
|
||||
InvoiceMailDialogFormRoot,
|
||||
);
|
||||
@@ -0,0 +1,66 @@
|
||||
// @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;
|
||||
`;
|
||||
@@ -0,0 +1 @@
|
||||
export * from './InvoiceMailDialog';
|
||||
@@ -26,6 +26,7 @@ import { useInvoicesListContext } from './InvoicesListProvider';
|
||||
|
||||
import { compose } from '@/utils';
|
||||
import { DRAWERS } from '@/constants/drawers';
|
||||
import { DialogsName } from '@/constants/dialogs';
|
||||
|
||||
/**
|
||||
* Invoices datatable.
|
||||
@@ -98,6 +99,11 @@ function InvoicesDataTable({
|
||||
openDialog('invoice-pdf-preview', { invoiceId: id });
|
||||
};
|
||||
|
||||
// Handle send mail invoice.
|
||||
const handleSendMailInvoice = ({ id }) => {
|
||||
openDialog(DialogsName.InvoiceMail, { invoiceId: id });
|
||||
};
|
||||
|
||||
// Handle cell click.
|
||||
const handleCellClick = (cell, event) => {
|
||||
openDrawer(DRAWERS.INVOICE_DETAILS, { invoiceId: cell.row.original.id });
|
||||
@@ -157,6 +163,7 @@ function InvoicesDataTable({
|
||||
onViewDetails: handleViewDetailInvoice,
|
||||
onPrint: handlePrintInvoice,
|
||||
onConvert: handleConvertToCreitNote,
|
||||
onSendMail: handleSendMailInvoice
|
||||
}}
|
||||
/>
|
||||
</DashboardContentTable>
|
||||
|
||||
@@ -128,6 +128,7 @@ export function ActionsMenu({
|
||||
onQuick,
|
||||
onViewDetails,
|
||||
onPrint,
|
||||
onSendMail
|
||||
},
|
||||
row: { original },
|
||||
}) {
|
||||
@@ -150,7 +151,6 @@ export function ActionsMenu({
|
||||
text={intl.get('invoice.convert_to_credit_note')}
|
||||
onClick={safeCallback(onConvert, original)}
|
||||
/>
|
||||
|
||||
<If condition={!original.is_delivered}>
|
||||
<MenuItem
|
||||
icon={<Icon icon="send" iconSize={16} />}
|
||||
@@ -169,6 +169,11 @@ export function ActionsMenu({
|
||||
</If>
|
||||
</Can>
|
||||
<Can I={SaleInvoiceAction.View} a={AbilitySubject.Invoice}>
|
||||
<MenuItem
|
||||
icon={<Icon icon={'envelope'} iconSize={16} />}
|
||||
text={'Send Mail'}
|
||||
onClick={safeCallback(onSendMail, original)}
|
||||
/>
|
||||
<MenuItem
|
||||
icon={<Icon icon={'print-16'} iconSize={16} />}
|
||||
text={intl.get('print')}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import { Dialog, DialogSuspense } from '@/components';
|
||||
import withDialogRedux from '@/components/DialogReduxConnect';
|
||||
import { compose } from '@/utils';
|
||||
|
||||
const PaymentMailDialogContent = React.lazy(
|
||||
() => import('./PaymentMailDialogContent'),
|
||||
);
|
||||
|
||||
/**
|
||||
* Payment mail dialog.
|
||||
*/
|
||||
function PaymentMailDialog({
|
||||
dialogName,
|
||||
payload: { paymentReceiveId = null },
|
||||
isOpen,
|
||||
}) {
|
||||
return (
|
||||
<Dialog
|
||||
name={dialogName}
|
||||
title={'Payment Mail'}
|
||||
isOpen={isOpen}
|
||||
canEscapeJeyClose={true}
|
||||
autoFocus={true}
|
||||
style={{ width: 600 }}
|
||||
>
|
||||
<DialogSuspense>
|
||||
<PaymentMailDialogContent
|
||||
dialogName={dialogName}
|
||||
paymentReceiveId={paymentReceiveId}
|
||||
/>
|
||||
</DialogSuspense>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
export default compose(withDialogRedux())(PaymentMailDialog);
|
||||
@@ -0,0 +1,45 @@
|
||||
// @ts-nocheck
|
||||
import React, { createContext } from 'react';
|
||||
import { usePaymentReceiveDefaultOptions } from '@/hooks/query';
|
||||
import { DialogContent } from '@/components';
|
||||
|
||||
interface PaymentMailDialogBootValues {
|
||||
paymentReceiveId: number;
|
||||
mailOptions: any;
|
||||
}
|
||||
|
||||
const PaymentMailDialogBootContext =
|
||||
createContext<PaymentMailDialogBootValues>();
|
||||
|
||||
interface PaymentMailDialogBootProps {
|
||||
paymentReceiveId: number;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Payment mail dialog boot provider.
|
||||
*/
|
||||
function PaymentMailDialogBoot({
|
||||
paymentReceiveId,
|
||||
...props
|
||||
}: PaymentMailDialogBootProps) {
|
||||
const { data: mailOptions, isLoading: isMailOptionsLoading } =
|
||||
usePaymentReceiveDefaultOptions(paymentReceiveId);
|
||||
|
||||
const provider = {
|
||||
mailOptions,
|
||||
isMailOptionsLoading,
|
||||
paymentReceiveId
|
||||
};
|
||||
|
||||
return (
|
||||
<DialogContent isLoading={isMailOptionsLoading}>
|
||||
<PaymentMailDialogBootContext.Provider value={provider} {...props} />
|
||||
</DialogContent>
|
||||
);
|
||||
}
|
||||
|
||||
const usePaymentMailDialogBoot = () =>
|
||||
React.useContext<PaymentMailDialogBootValues>(PaymentMailDialogBootContext);
|
||||
|
||||
export { PaymentMailDialogBoot, usePaymentMailDialogBoot };
|
||||
@@ -0,0 +1,17 @@
|
||||
import { PaymentMailDialogBoot } from './PaymentMailDialogBoot';
|
||||
import { PaymentMailDialogForm } from './PaymentMailDialogForm';
|
||||
|
||||
interface PaymentMailDialogContentProps {
|
||||
dialogName: string;
|
||||
paymentReceiveId: number;
|
||||
}
|
||||
export default function PaymentMailDialogContent({
|
||||
dialogName,
|
||||
paymentReceiveId,
|
||||
}: PaymentMailDialogContentProps) {
|
||||
return (
|
||||
<PaymentMailDialogBoot paymentReceiveId={paymentReceiveId}>
|
||||
<PaymentMailDialogForm />
|
||||
</PaymentMailDialogBoot>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
// @ts-nocheck
|
||||
import { Formik, FormikBag } from 'formik';
|
||||
import * as R from 'ramda';
|
||||
import { Intent } from '@blueprintjs/core';
|
||||
import { usePaymentMailDialogBoot } from './PaymentMailDialogBoot';
|
||||
import withDialogActions from '@/containers/Dialog/withDialogActions';
|
||||
import { DialogsName } from '@/constants/dialogs';
|
||||
import { useSendPaymentReceiveMail } from '@/hooks/query';
|
||||
import { PaymentMailDialogFormContent } from './PaymentMailDialogFormContent';
|
||||
import {
|
||||
MailNotificationFormValues,
|
||||
initialMailNotificationValues,
|
||||
transformMailFormToRequest,
|
||||
transformMailFormToInitialValues,
|
||||
} from '@/containers/SendMailNotification/utils';
|
||||
import { AppToaster } from '@/components';
|
||||
|
||||
const initialFormValues = {
|
||||
...initialMailNotificationValues,
|
||||
attachPayment: true,
|
||||
};
|
||||
|
||||
interface PaymentMailFormValue extends MailNotificationFormValues {
|
||||
attachPayment: boolean;
|
||||
}
|
||||
|
||||
export function PaymentMailDialogFormRoot({
|
||||
// #withDialogActions
|
||||
closeDialog,
|
||||
}) {
|
||||
const { mailOptions, paymentReceiveId } = usePaymentMailDialogBoot();
|
||||
const { mutateAsync: sendPaymentMail } = useSendPaymentReceiveMail();
|
||||
|
||||
const initialValues = transformMailFormToInitialValues(
|
||||
mailOptions,
|
||||
initialFormValues,
|
||||
);
|
||||
// Handles the form submitting.
|
||||
const handleSubmit = (
|
||||
values: PaymentMailFormValue,
|
||||
{ setSubmitting }: FormikBag<PaymentMailFormValue>,
|
||||
) => {
|
||||
const reqValues = transformMailFormToRequest(values);
|
||||
|
||||
setSubmitting(true);
|
||||
sendPaymentMail([paymentReceiveId, reqValues])
|
||||
.then(() => {
|
||||
AppToaster.show({
|
||||
message: 'The mail notification has been sent successfully.',
|
||||
intent: Intent.SUCCESS,
|
||||
});
|
||||
setSubmitting(false);
|
||||
closeDialog(DialogsName.PaymentMail);
|
||||
})
|
||||
.catch(() => {
|
||||
AppToaster.show({
|
||||
message: 'Something went wrong.',
|
||||
intent: Intent.DANGER,
|
||||
});
|
||||
setSubmitting(false);
|
||||
closeDialog(DialogsName.PaymentMail);
|
||||
});
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
closeDialog(DialogsName.PaymentMail);
|
||||
};
|
||||
|
||||
return (
|
||||
<Formik initialValues={initialValues} onSubmit={handleSubmit}>
|
||||
<PaymentMailDialogFormContent onClose={handleClose} />
|
||||
</Formik>
|
||||
);
|
||||
}
|
||||
|
||||
export const PaymentMailDialogForm = R.compose(withDialogActions)(
|
||||
PaymentMailDialogFormRoot,
|
||||
);
|
||||
@@ -0,0 +1,66 @@
|
||||
// @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 { usePaymentMailDialogBoot } from './PaymentMailDialogBoot';
|
||||
|
||||
interface PaymentMailDialogFormContentProps {
|
||||
onClose?: () => void;
|
||||
}
|
||||
|
||||
export function PaymentMailDialogFormContent({
|
||||
onClose,
|
||||
}: PaymentMailDialogFormContentProps) {
|
||||
const { mailOptions } = usePaymentMailDialogBoot();
|
||||
const { isSubmitting } = useFormikContext();
|
||||
|
||||
const handleClose = () => {
|
||||
saveInvoke(onClose);
|
||||
};
|
||||
|
||||
return (
|
||||
<Form>
|
||||
<div className={Classes.DIALOG_BODY}>
|
||||
<MailNotificationForm
|
||||
fromAddresses={mailOptions.from_addresses}
|
||||
toAddresses={mailOptions.to_addresses}
|
||||
/>
|
||||
<AttachFormGroup name={'attachPayment'} inline>
|
||||
<FSwitch name={'attachPayment'} label={'Attach Payment'} />
|
||||
</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;
|
||||
`;
|
||||
@@ -0,0 +1 @@
|
||||
export * from './PaymentMailDialog';
|
||||
@@ -17,12 +17,14 @@ import withPaymentReceives from './withPaymentReceives';
|
||||
import withPaymentReceivesActions from './withPaymentReceivesActions';
|
||||
import withAlertsActions from '@/containers/Alert/withAlertActions';
|
||||
import withDrawerActions from '@/containers/Drawer/withDrawerActions';
|
||||
import withDialogActions from '@/containers/Dialog/withDialogActions';
|
||||
import withSettings from '@/containers/Settings/withSettings';
|
||||
|
||||
import { usePaymentReceivesColumns, ActionsMenu } from './components';
|
||||
import { usePaymentReceivesListContext } from './PaymentReceiptsListProvider';
|
||||
import { useMemorizedColumnsWidths } from '@/hooks';
|
||||
import { DRAWERS } from '@/constants/drawers';
|
||||
import { DialogsName } from '@/constants/dialogs';
|
||||
|
||||
/**
|
||||
* Payment receives datatable.
|
||||
@@ -31,15 +33,15 @@ function PaymentReceivesDataTable({
|
||||
// #withPaymentReceivesActions
|
||||
setPaymentReceivesTableState,
|
||||
|
||||
// #withPaymentReceives
|
||||
paymentReceivesTableState,
|
||||
|
||||
// #withAlertsActions
|
||||
openAlert,
|
||||
|
||||
// #withDrawerActions
|
||||
openDrawer,
|
||||
|
||||
// #withDialogActions
|
||||
openDialog,
|
||||
|
||||
// #withSettings
|
||||
paymentReceivesTableSize,
|
||||
}) {
|
||||
@@ -73,6 +75,11 @@ function PaymentReceivesDataTable({
|
||||
openDrawer(DRAWERS.PAYMENT_RECEIVE_DETAILS, { paymentReceiveId: id });
|
||||
};
|
||||
|
||||
// Handle mail send payment receive.
|
||||
const handleSendMailPayment = ({ id }) => {
|
||||
openDialog(DialogsName.PaymentMail, { paymentReceiveId: id });
|
||||
};
|
||||
|
||||
// Handle cell click.
|
||||
const handleCellClick = (cell, event) => {
|
||||
openDrawer(DRAWERS.PAYMENT_RECEIVE_DETAILS, {
|
||||
@@ -129,6 +136,7 @@ function PaymentReceivesDataTable({
|
||||
onDelete: handleDeletePaymentReceive,
|
||||
onEdit: handleEditPaymentReceive,
|
||||
onViewDetails: handleViewDetailPaymentReceive,
|
||||
onSendMail: handleSendMailPayment,
|
||||
}}
|
||||
/>
|
||||
</DashboardContentTable>
|
||||
@@ -139,6 +147,7 @@ export default compose(
|
||||
withPaymentReceivesActions,
|
||||
withAlertsActions,
|
||||
withDrawerActions,
|
||||
withDialogActions,
|
||||
withPaymentReceives(({ paymentReceivesTableState }) => ({
|
||||
paymentReceivesTableState,
|
||||
})),
|
||||
|
||||
@@ -15,14 +15,17 @@ import {
|
||||
import { FormatDateCell, Money, Icon, Can } from '@/components';
|
||||
import { safeCallback } from '@/utils';
|
||||
import { CLASSES } from '@/constants/classes';
|
||||
import { PaymentReceiveAction, AbilitySubject } from '@/constants/abilityOption';
|
||||
import {
|
||||
PaymentReceiveAction,
|
||||
AbilitySubject,
|
||||
} from '@/constants/abilityOption';
|
||||
|
||||
/**
|
||||
* Table actions menu.
|
||||
*/
|
||||
export function ActionsMenu({
|
||||
row: { original: paymentReceive },
|
||||
payload: { onEdit, onDelete, onViewDetails },
|
||||
payload: { onEdit, onDelete, onViewDetails, onSendMail },
|
||||
}) {
|
||||
return (
|
||||
<Menu>
|
||||
@@ -31,6 +34,11 @@ export function ActionsMenu({
|
||||
text={intl.get('view_details')}
|
||||
onClick={safeCallback(onViewDetails, paymentReceive)}
|
||||
/>
|
||||
<MenuItem
|
||||
icon={<Icon icon={'envelope'} iconSize={16} />}
|
||||
text={'Send Mail'}
|
||||
onClick={safeCallback(onSendMail, paymentReceive)}
|
||||
/>
|
||||
<Can I={PaymentReceiveAction.Edit} a={AbilitySubject.PaymentReceive}>
|
||||
<MenuDivider />
|
||||
<MenuItem
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import { Dialog, DialogSuspense } from '@/components';
|
||||
import withDialogRedux from '@/components/DialogReduxConnect';
|
||||
import { compose } from '@/utils';
|
||||
|
||||
const ReceiptMailDialogContent = React.lazy(
|
||||
() => import('./ReceiptMailDialogContent'),
|
||||
);
|
||||
|
||||
/**
|
||||
* Invoice mail dialog.
|
||||
*/
|
||||
function ReceiptMailDialog({
|
||||
dialogName,
|
||||
payload: { receiptId = null },
|
||||
isOpen,
|
||||
}) {
|
||||
return (
|
||||
<Dialog
|
||||
name={dialogName}
|
||||
title={'Receipt Mail'}
|
||||
isOpen={isOpen}
|
||||
canEscapeJeyClose={true}
|
||||
autoFocus={true}
|
||||
style={{ width: 600 }}
|
||||
>
|
||||
<DialogSuspense>
|
||||
<ReceiptMailDialogContent
|
||||
dialogName={dialogName}
|
||||
receiptId={receiptId}
|
||||
/>
|
||||
</DialogSuspense>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
export default compose(withDialogRedux())(ReceiptMailDialog);
|
||||
@@ -0,0 +1,45 @@
|
||||
// @ts-nocheck
|
||||
import React, { createContext } from 'react';
|
||||
import { useSaleReceiptDefaultOptions } from '@/hooks/query';
|
||||
import { DialogContent } from '@/components';
|
||||
|
||||
interface ReceiptMailDialogBootValues {
|
||||
receiptId: number;
|
||||
mailOptions: any;
|
||||
}
|
||||
|
||||
const ReceiptMailDialogBootContext =
|
||||
createContext<ReceiptMailDialogBootValues>();
|
||||
|
||||
interface ReceiptMailDialogBootProps {
|
||||
receiptId: number;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Receipt mail dialog boot provider.
|
||||
*/
|
||||
function ReceiptMailDialogBoot({
|
||||
receiptId,
|
||||
...props
|
||||
}: ReceiptMailDialogBootProps) {
|
||||
const { data: mailOptions, isLoading: isMailOptionsLoading } =
|
||||
useSaleReceiptDefaultOptions(receiptId);
|
||||
|
||||
const provider = {
|
||||
saleReceiptId: receiptId,
|
||||
mailOptions,
|
||||
isMailOptionsLoading,
|
||||
};
|
||||
|
||||
return (
|
||||
<DialogContent isLoading={isMailOptionsLoading}>
|
||||
<ReceiptMailDialogBootContext.Provider value={provider} {...props} />
|
||||
</DialogContent>
|
||||
);
|
||||
}
|
||||
|
||||
const useReceiptMailDialogBoot = () =>
|
||||
React.useContext<ReceiptMailDialogBootValues>(ReceiptMailDialogBootContext);
|
||||
|
||||
export { ReceiptMailDialogBoot, useReceiptMailDialogBoot };
|
||||
@@ -0,0 +1,18 @@
|
||||
import React from 'react';
|
||||
import { ReceiptMailDialogBoot } from './ReceiptMailDialogBoot';
|
||||
import { ReceiptMailDialogForm } from './ReceiptMailDialogForm';
|
||||
|
||||
interface ReceiptMailDialogContentProps {
|
||||
dialogName: string
|
||||
receiptId: number;
|
||||
}
|
||||
export default function ReceiptMailDialogContent({
|
||||
dialogName,
|
||||
receiptId,
|
||||
}: ReceiptMailDialogContentProps) {
|
||||
return (
|
||||
<ReceiptMailDialogBoot receiptId={receiptId}>
|
||||
<ReceiptMailDialogForm />
|
||||
</ReceiptMailDialogBoot>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
// @ts-nocheck
|
||||
import { Formik, FormikBag } from 'formik';
|
||||
import * as R from 'ramda';
|
||||
import { Intent } from '@blueprintjs/core';
|
||||
import { useReceiptMailDialogBoot } from './ReceiptMailDialogBoot';
|
||||
import withDialogActions from '@/containers/Dialog/withDialogActions';
|
||||
import { DialogsName } from '@/constants/dialogs';
|
||||
import { useSendSaleReceiptMail } from '@/hooks/query';
|
||||
import { ReceiptMailDialogFormContent } from './ReceiptMailDialogFormContent';
|
||||
import {
|
||||
initialMailNotificationValues,
|
||||
MailNotificationFormValues,
|
||||
transformMailFormToInitialValues,
|
||||
transformMailFormToRequest,
|
||||
} from '@/containers/SendMailNotification/utils';
|
||||
import { AppToaster } from '@/components';
|
||||
|
||||
const initialFormValues = {
|
||||
...initialMailNotificationValues,
|
||||
attachReceipt: true,
|
||||
};
|
||||
interface ReceiptMailFormValues extends MailNotificationFormValues {
|
||||
attachReceipt: boolean;
|
||||
}
|
||||
|
||||
function ReceiptMailDialogFormRoot({ closeDialog }) {
|
||||
const { mailOptions, saleReceiptId } = useReceiptMailDialogBoot();
|
||||
const { mutateAsync: sendReceiptMail } = useSendSaleReceiptMail();
|
||||
|
||||
// Transformes mail options to initial form values.
|
||||
const initialValues = transformMailFormToInitialValues(
|
||||
mailOptions,
|
||||
initialFormValues,
|
||||
);
|
||||
// Handle the form submitting.
|
||||
const handleSubmit = (
|
||||
values: ReceiptMailFormValues,
|
||||
{ setSubmitting }: FormikBag<ReceiptMailFormValues>,
|
||||
) => {
|
||||
const reqValues = transformMailFormToRequest(values);
|
||||
|
||||
setSubmitting(true);
|
||||
sendReceiptMail([saleReceiptId, reqValues])
|
||||
.then(() => {
|
||||
AppToaster.show({
|
||||
message: 'The mail notification has been sent successfully.',
|
||||
intent: Intent.SUCCESS,
|
||||
});
|
||||
closeDialog(DialogsName.ReceiptMail);
|
||||
setSubmitting(false);
|
||||
})
|
||||
.catch(() => {
|
||||
AppToaster.show({
|
||||
message: 'Something went wrong.',
|
||||
intent: Intent.DANGER,
|
||||
});
|
||||
setSubmitting(false);
|
||||
});
|
||||
};
|
||||
// Handle the close button click.
|
||||
const handleClose = () => {
|
||||
closeDialog(DialogsName.ReceiptMail);
|
||||
};
|
||||
|
||||
return (
|
||||
<Formik initialValues={initialValues} onSubmit={handleSubmit}>
|
||||
<ReceiptMailDialogFormContent onClose={handleClose} />
|
||||
</Formik>
|
||||
);
|
||||
}
|
||||
|
||||
export const ReceiptMailDialogForm = R.compose(withDialogActions)(
|
||||
ReceiptMailDialogFormRoot,
|
||||
);
|
||||
@@ -0,0 +1,66 @@
|
||||
// @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 { useReceiptMailDialogBoot } from './ReceiptMailDialogBoot';
|
||||
import { saveInvoke } from '@/utils';
|
||||
|
||||
interface SendMailNotificationFormProps {
|
||||
onClose?: () => void;
|
||||
}
|
||||
|
||||
export function ReceiptMailDialogFormContent({
|
||||
onClose,
|
||||
}: SendMailNotificationFormProps) {
|
||||
const { mailOptions } = useReceiptMailDialogBoot();
|
||||
const { isSubmitting } = useFormikContext();
|
||||
|
||||
const handleClose = () => {
|
||||
saveInvoke(onClose);
|
||||
};
|
||||
|
||||
return (
|
||||
<Form>
|
||||
<div className={Classes.DIALOG_BODY}>
|
||||
<MailNotificationForm
|
||||
fromAddresses={mailOptions.from_addresses}
|
||||
toAddresses={mailOptions.to_addresses}
|
||||
/>
|
||||
<AttachFormGroup name={'attachReceipt:'} inline>
|
||||
<FSwitch name={'attachReceipt:'} label={'Attach Receipt'} />
|
||||
</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;
|
||||
`;
|
||||
@@ -0,0 +1 @@
|
||||
export * from './ReceiptMailDialog';
|
||||
@@ -140,7 +140,6 @@ function ReceiptActionsBar({
|
||||
icon={<Icon icon={'file-export-16'} iconSize={'16'} />}
|
||||
text={<T id={'export'} />}
|
||||
/>
|
||||
|
||||
<NavbarDivider />
|
||||
<DashboardRowsHeightButton
|
||||
initialValue={receiptsTableSize}
|
||||
|
||||
@@ -24,6 +24,7 @@ import { useReceiptsListContext } from './ReceiptsListProvider';
|
||||
import { useReceiptsTableColumns, ActionsMenu } from './components';
|
||||
import { useMemorizedColumnsWidths } from '@/hooks';
|
||||
import { DRAWERS } from '@/constants/drawers';
|
||||
import { DialogsName } from '@/constants/dialogs';
|
||||
|
||||
/**
|
||||
* Sale receipts datatable.
|
||||
@@ -86,6 +87,11 @@ function ReceiptsDataTable({
|
||||
openDialog('receipt-pdf-preview', { receiptId: id });
|
||||
};
|
||||
|
||||
// Handle send mail receipt.
|
||||
const handleSendMailReceipt = ({ id }) => {
|
||||
openDialog(DialogsName.ReceiptMail, { receiptId: id });
|
||||
};
|
||||
|
||||
// Local storage memorizing columns widths.
|
||||
const [initialColumnsWidths, , handleColumnResizing] =
|
||||
useMemorizedColumnsWidths(TABLES.RECEIPTS);
|
||||
@@ -141,6 +147,7 @@ function ReceiptsDataTable({
|
||||
onClose: handleCloseReceipt,
|
||||
onViewDetails: handleViewDetailReceipt,
|
||||
onPrint: handlePrintInvoice,
|
||||
onSendMail: handleSendMailReceipt,
|
||||
}}
|
||||
/>
|
||||
</DashboardContentTable>
|
||||
|
||||
@@ -24,7 +24,7 @@ import { SaleReceiptAction, AbilitySubject } from '@/constants/abilityOption';
|
||||
* @returns {React.JSX}
|
||||
*/
|
||||
export function ActionsMenu({
|
||||
payload: { onEdit, onDelete, onClose, onDrawer, onViewDetails, onPrint },
|
||||
payload: { onEdit, onDelete, onClose, onSendMail, onViewDetails, onPrint },
|
||||
row: { original: receipt },
|
||||
}) {
|
||||
return (
|
||||
@@ -51,6 +51,11 @@ export function ActionsMenu({
|
||||
</If>
|
||||
</Can>
|
||||
<Can I={SaleReceiptAction.View} a={AbilitySubject.Receipt}>
|
||||
<MenuItem
|
||||
icon={<Icon icon={'envelope'} iconSize={16} />}
|
||||
text={'Send Mail'}
|
||||
onClick={safeCallback(onSendMail, receipt)}
|
||||
/>
|
||||
<MenuItem
|
||||
icon={<Icon icon={'print-16'} iconSize={16} />}
|
||||
text={intl.get('print')}
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
// @ts-nocheck
|
||||
import {
|
||||
Box,
|
||||
FFormGroup,
|
||||
FInputGroup,
|
||||
FMultiSelect,
|
||||
FRichEditor,
|
||||
Hint,
|
||||
} from '@/components';
|
||||
import styled from 'styled-components';
|
||||
import { Position } from '@blueprintjs/core';
|
||||
import { SelectOptionProps } from '@blueprintjs-formik/select';
|
||||
|
||||
interface MailNotificationFormProps {
|
||||
fromAddresses: SelectOptionProps[];
|
||||
toAddresses: SelectOptionProps[];
|
||||
}
|
||||
|
||||
const commonAddressSelect = {
|
||||
placeholder: '',
|
||||
labelAccessor: '',
|
||||
valueAccessor: 'mail',
|
||||
tagAccessor: (item) => `<${item.label}> (${item.mail})`,
|
||||
textAccessor: (item) => `<${item.label}> (${item.mail})`,
|
||||
};
|
||||
|
||||
export function MailNotificationForm({
|
||||
fromAddresses,
|
||||
toAddresses,
|
||||
}: MailNotificationFormProps) {
|
||||
return (
|
||||
<Box>
|
||||
<HeaderBox>
|
||||
<FFormGroup
|
||||
label={'From'}
|
||||
labelInfo={
|
||||
<Hint
|
||||
content={'asdasd asdasd asdsad'}
|
||||
position={Position.BOTTOM_LEFT}
|
||||
/>
|
||||
}
|
||||
name={'from'}
|
||||
inline={true}
|
||||
fastField={true}
|
||||
>
|
||||
<FMultiSelect
|
||||
items={fromAddresses}
|
||||
name={'from'}
|
||||
popoverProps={{ minimal: true, fill: true }}
|
||||
tagInputProps={{
|
||||
tagProps: { round: true, minimal: true, large: true },
|
||||
}}
|
||||
fill={true}
|
||||
{...commonAddressSelect}
|
||||
/>
|
||||
</FFormGroup>
|
||||
|
||||
<FFormGroup label={'To'} name={'to'} inline={true} fastField={true}>
|
||||
<FMultiSelect
|
||||
items={toAddresses}
|
||||
name={'to'}
|
||||
placeholder=""
|
||||
popoverProps={{ minimal: true, fill: true }}
|
||||
tagInputProps={{
|
||||
tagProps: { round: true, minimal: true, large: true },
|
||||
}}
|
||||
fill={true}
|
||||
{...commonAddressSelect}
|
||||
/>
|
||||
</FFormGroup>
|
||||
|
||||
<FFormGroup
|
||||
label={'Subject'}
|
||||
name={'subject'}
|
||||
inline={true}
|
||||
fastField={true}
|
||||
>
|
||||
<FInputGroup name={'subject'} fill={true} />
|
||||
</FFormGroup>
|
||||
</HeaderBox>
|
||||
|
||||
<MailMessageEditor name={'body'} />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
const MailMessageEditor = styled(FRichEditor)`
|
||||
padding: 15px;
|
||||
border: 1px solid #dedfe9;
|
||||
border-top: 0;
|
||||
border-bottom-left-radius: 5px;
|
||||
border-bottom-right-radius: 5px;
|
||||
`;
|
||||
|
||||
const HeaderBox = styled('div')`
|
||||
border-top-right-radius: 5px;
|
||||
border-top-left-radius: 5px;
|
||||
border: 1px solid #dddfe9;
|
||||
border-bottom: 2px solid #eaeaef;
|
||||
padding: 6px 15px;
|
||||
|
||||
.bp4-form-group {
|
||||
margin: 0;
|
||||
padding-top: 8px;
|
||||
padding-bottom: 8px;
|
||||
|
||||
&:not(:last-of-type) {
|
||||
border-bottom: 1px solid #dddfe9;
|
||||
}
|
||||
&:first-of-type {
|
||||
padding-top: 0;
|
||||
}
|
||||
&:last-of-type {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.bp4-form-content {
|
||||
flex: 1 0;
|
||||
}
|
||||
|
||||
.bp4-label {
|
||||
min-width: 65px;
|
||||
color: #738091;
|
||||
}
|
||||
|
||||
.bp4-input {
|
||||
border-color: transparent;
|
||||
padding: 0;
|
||||
|
||||
&:focus,
|
||||
&.bp4-active {
|
||||
box-shadow: 0 0 0 0;
|
||||
}
|
||||
}
|
||||
|
||||
.bp4-input-ghost {
|
||||
margin-top: 5px;
|
||||
}
|
||||
.bp4-tag-input-values {
|
||||
margin: 0;
|
||||
}
|
||||
`;
|
||||
@@ -0,0 +1 @@
|
||||
export * from './MailNotificationForm';
|
||||
44
packages/webapp/src/containers/SendMailNotification/utils.ts
Normal file
44
packages/webapp/src/containers/SendMailNotification/utils.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { castArray, first } from 'lodash';
|
||||
import { transformToForm } from '@/utils';
|
||||
|
||||
export const initialMailNotificationValues = {
|
||||
from: [],
|
||||
to: [],
|
||||
subject: '',
|
||||
body: '',
|
||||
};
|
||||
|
||||
export interface MailNotificationFormValues {
|
||||
from: string[];
|
||||
to: string[];
|
||||
subject: string;
|
||||
body: string;
|
||||
}
|
||||
|
||||
export const transformMailFormToRequest = (
|
||||
values: MailNotificationFormValues,
|
||||
) => {
|
||||
return {
|
||||
...values,
|
||||
from: first(values.from),
|
||||
to: values.to?.join(', '),
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Transformes the mail options response values to form initial values.
|
||||
* @param {any} mailOptions
|
||||
* @param {MailNotificationFormValues} initialValues
|
||||
* @returns {MailNotificationFormValues}
|
||||
*/
|
||||
export const transformMailFormToInitialValues = (
|
||||
mailOptions: any,
|
||||
initialValues: MailNotificationFormValues,
|
||||
): MailNotificationFormValues => {
|
||||
return {
|
||||
...initialValues,
|
||||
...transformToForm(mailOptions, initialValues),
|
||||
from: mailOptions.from ? castArray(mailOptions.from) : [],
|
||||
to: mailOptions.to ? castArray(mailOptions.to) : [],
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user