mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-16 04:40:32 +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:
@@ -47,6 +47,10 @@ import ProjectInvoicingFormDialog from '@/containers/Projects/containers/Project
|
||||
import ProjectBillableEntriesFormDialog from '@/containers/Projects/containers/ProjectBillableEntriesFormDialog';
|
||||
import TaxRateFormDialog from '@/containers/TaxRates/dialogs/TaxRateFormDialog/TaxRateFormDialog';
|
||||
import { DialogsName } from '@/constants/dialogs';
|
||||
import InvoiceMailDialog from '@/containers/Sales/Invoices/InvoiceMailDialog/InvoiceMailDialog';
|
||||
import EstimateMailDialog from '@/containers/Sales/Estimates/EstimateMailDialog/EstimateMailDialog';
|
||||
import ReceiptMailDialog from '@/containers/Sales/Receipts/ReceiptMailDialog/ReceiptMailDialog';
|
||||
import PaymentMailDialog from '@/containers/Sales/PaymentReceives/PaymentMailDialog/PaymentMailDialog';
|
||||
|
||||
/**
|
||||
* Dialogs container.
|
||||
@@ -137,6 +141,10 @@ export default function DialogsContainer() {
|
||||
dialogName={DialogsName.ProjectBillableEntriesForm}
|
||||
/>
|
||||
<TaxRateFormDialog dialogName={DialogsName.TaxRateForm} />
|
||||
<InvoiceMailDialog dialogName={DialogsName.InvoiceMail} />
|
||||
<EstimateMailDialog dialogName={DialogsName.EstimateMail} />
|
||||
<ReceiptMailDialog dialogName={DialogsName.ReceiptMail} />
|
||||
<PaymentMailDialog dialogName={DialogsName.PaymentMail} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
52
packages/webapp/src/components/Forms/FRichEditor.tsx
Normal file
52
packages/webapp/src/components/Forms/FRichEditor.tsx
Normal file
@@ -0,0 +1,52 @@
|
||||
import React from 'react';
|
||||
import { FieldConfig, FieldProps } from 'formik';
|
||||
import { Field } from '@blueprintjs-formik/core';
|
||||
import { RichEditor, RichEditorProps } from '../../components/RichEditor';
|
||||
|
||||
export interface FRichEditorProps
|
||||
extends Omit<FieldConfig, 'children' | 'component' | 'as'>,
|
||||
RichEditorProps {
|
||||
name: string;
|
||||
value?: string;
|
||||
}
|
||||
|
||||
interface FieldToRichEditorProps
|
||||
extends FieldProps,
|
||||
Omit<RichEditorProps, 'form'> {}
|
||||
|
||||
/**
|
||||
* Transformes the field props to `RichEditor` props.
|
||||
* @param {FieldToRichEditorProps}
|
||||
* @returns {HTMLSelectProps}
|
||||
*/
|
||||
function fieldToRichEditor({
|
||||
field: { onBlur: onFieldBlur, ...field },
|
||||
form: { touched, errors, ...form },
|
||||
...props
|
||||
}: FieldToRichEditorProps): RichEditorProps {
|
||||
return {
|
||||
...field,
|
||||
...props,
|
||||
onChange: (value: string) => {
|
||||
form.setFieldValue(field.name, value);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Transformes field props to `RichEditor` props.
|
||||
* @param {FieldToRichEditorProps}
|
||||
* @returns {JSX.Element}
|
||||
*/
|
||||
function FieldToRichEditor({ ...props }: FieldToRichEditorProps): JSX.Element {
|
||||
return <RichEditor {...fieldToRichEditor(props)} />;
|
||||
}
|
||||
|
||||
/**
|
||||
* Rich editor wrapper to bind with Formik.
|
||||
* @param {FRichEditorProps} props -
|
||||
* @returns {JSX.Element}
|
||||
*/
|
||||
export function FRichEditor({ ...props }: FRichEditorProps): JSX.Element {
|
||||
return <Field {...props} component={FieldToRichEditor} />;
|
||||
}
|
||||
@@ -4,4 +4,5 @@ export * from './FMoneyInputGroup';
|
||||
export * from './BlueprintFormik';
|
||||
export * from './InputPrependText';
|
||||
export * from './InputPrependButton';
|
||||
export * from './MoneyInputGroup';
|
||||
export * from './MoneyInputGroup';
|
||||
export * from './FRichEditor';
|
||||
@@ -0,0 +1,66 @@
|
||||
/* Basic editor styles */
|
||||
.tiptap {
|
||||
color: #222;
|
||||
|
||||
&:focus-visible {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
>*+* {
|
||||
margin-top: 0.75em;
|
||||
}
|
||||
|
||||
ul,
|
||||
ol {
|
||||
padding: 0 1rem;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
code {
|
||||
background: rgba(#ffffff, 0.1);
|
||||
color: rgba(#ffffff, 0.6);
|
||||
border: 1px solid rgba(#ffffff, 0.1);
|
||||
border-radius: 0.5rem;
|
||||
padding: 0.2rem;
|
||||
}
|
||||
|
||||
pre {
|
||||
background: rgba(#ffffff, 0.1);
|
||||
font-family: "JetBrainsMono", monospace;
|
||||
padding: 0.75rem 1rem;
|
||||
border-radius: 0.5rem;
|
||||
|
||||
code {
|
||||
color: inherit;
|
||||
padding: 0;
|
||||
background: none;
|
||||
font-size: 0.8rem;
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
|
||||
img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
margin-left: 0;
|
||||
padding-left: 1rem;
|
||||
border-left: 2px solid rgba(#ffffff, 0.4);
|
||||
|
||||
hr {
|
||||
border: none;
|
||||
border-top: 2px solid rgba(#ffffff, 0.1);
|
||||
margin: 2rem 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
58
packages/webapp/src/components/RichEditor/RichEditor.tsx
Normal file
58
packages/webapp/src/components/RichEditor/RichEditor.tsx
Normal file
@@ -0,0 +1,58 @@
|
||||
// @ts-nocheck
|
||||
import { Color } from '@tiptap/extension-color';
|
||||
import ListItem from '@tiptap/extension-list-item';
|
||||
import TextStyle from '@tiptap/extension-text-style';
|
||||
import { EditorProvider } from '@tiptap/react';
|
||||
import StarterKit from '@tiptap/starter-kit';
|
||||
import { useUncontrolled } from '@/hooks/useUncontrolled';
|
||||
import { Box } from '../Layout/Box';
|
||||
import './RichEditor.style.scss';
|
||||
|
||||
const extensions = [
|
||||
Color.configure({ types: [TextStyle.name, ListItem.name] }),
|
||||
TextStyle.configure({ types: [ListItem.name] }),
|
||||
StarterKit.configure({
|
||||
bulletList: {
|
||||
keepMarks: true,
|
||||
keepAttributes: false,
|
||||
},
|
||||
orderedList: {
|
||||
keepMarks: true,
|
||||
keepAttributes: false,
|
||||
},
|
||||
}),
|
||||
];
|
||||
|
||||
export interface RichEditorProps {
|
||||
value?: string;
|
||||
initialValue?: string;
|
||||
onChange?: (value: string) => void;
|
||||
className?: string;
|
||||
}
|
||||
export const RichEditor = ({
|
||||
value,
|
||||
initialValue,
|
||||
onChange,
|
||||
className,
|
||||
}: RichEditorProps) => {
|
||||
const [content, handleChange] = useUncontrolled({
|
||||
value,
|
||||
initialValue,
|
||||
onChange,
|
||||
finalValue: '',
|
||||
});
|
||||
|
||||
const handleBlur = ({ editor }) => {
|
||||
handleChange(editor.getHTML());
|
||||
};
|
||||
|
||||
return (
|
||||
<Box className={className}>
|
||||
<EditorProvider
|
||||
extensions={extensions}
|
||||
content={content}
|
||||
onBlur={handleBlur}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
1
packages/webapp/src/components/RichEditor/index.ts
Normal file
1
packages/webapp/src/components/RichEditor/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './RichEditor';
|
||||
@@ -48,4 +48,8 @@ export enum DialogsName {
|
||||
ProjectBillableEntriesForm = 'project-billable-entries',
|
||||
InvoiceNumberSettings = 'InvoiceNumberSettings',
|
||||
TaxRateForm = 'tax-rate-form',
|
||||
InvoiceMail = 'invoice-mail',
|
||||
EstimateMail = 'estimate-mail',
|
||||
ReceiptMail = 'receipt-mail',
|
||||
PaymentMail = 'payment-mail',
|
||||
}
|
||||
|
||||
@@ -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) : [],
|
||||
};
|
||||
};
|
||||
@@ -239,3 +239,33 @@ export function useEstimateSMSDetail(estimateId, props, requestProps) {
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
export function useSendSaleEstimateMail(props) {
|
||||
const queryClient = useQueryClient();
|
||||
const apiRequest = useApiRequest();
|
||||
|
||||
return useMutation(
|
||||
([id, values]) => apiRequest.post(`sales/estimates/${id}/mail`, values),
|
||||
{
|
||||
onSuccess: (res, [id, values]) => {
|
||||
// Common invalidate queries.
|
||||
commonInvalidateQueries(queryClient);
|
||||
},
|
||||
...props,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
export function useSaleEstimateDefaultOptions(estimateId, props) {
|
||||
return useRequestQuery(
|
||||
[t.SALE_ESTIMATE_MAIL_OPTIONS, estimateId],
|
||||
{
|
||||
method: 'get',
|
||||
url: `sales/estimates/${estimateId}/mail`,
|
||||
},
|
||||
{
|
||||
select: (res) => res.data.data,
|
||||
...props,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -306,3 +306,34 @@ export function useInvoicePaymentTransactions(invoiceId, props) {
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
export function useSendSaleInvoiceMail(props) {
|
||||
const queryClient = useQueryClient();
|
||||
const apiRequest = useApiRequest();
|
||||
|
||||
return useMutation(
|
||||
([id, values]) => apiRequest.post(`sales/invoices/${id}/mail`, values),
|
||||
{
|
||||
onSuccess: (res, [id, values]) => {
|
||||
// Common invalidate queries.
|
||||
commonInvalidateQueries(queryClient);
|
||||
},
|
||||
...props,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
export function useSaleInvoiceDefaultOptions(invoiceId, props) {
|
||||
return useRequestQuery(
|
||||
[t.SALE_INVOICE_DEFAULT_OPTIONS, invoiceId],
|
||||
{
|
||||
method: 'get',
|
||||
url: `sales/invoices/${invoiceId}/mail`,
|
||||
},
|
||||
{
|
||||
select: (res) => res.data.data,
|
||||
...props,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -234,3 +234,34 @@ export function usePaymentReceiveSMSDetail(
|
||||
export function usePdfPaymentReceive(paymentReceiveId) {
|
||||
return useRequestPdf(`sales/payment_receives/${paymentReceiveId}`);
|
||||
}
|
||||
|
||||
export function useSendPaymentReceiveMail(props) {
|
||||
const queryClient = useQueryClient();
|
||||
const apiRequest = useApiRequest();
|
||||
|
||||
return useMutation(
|
||||
([id, values]) =>
|
||||
apiRequest.post(`sales/payment_receives/${id}/mail`, values),
|
||||
{
|
||||
onSuccess: (res, [id, values]) => {
|
||||
// Common invalidate queries.
|
||||
commonInvalidateQueries(queryClient);
|
||||
},
|
||||
...props,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
export function usePaymentReceiveDefaultOptions(paymentReceiveId, props) {
|
||||
return useRequestQuery(
|
||||
[t.PAYMENT_RECEIVE_MAIL_OPTIONS, paymentReceiveId],
|
||||
{
|
||||
method: 'get',
|
||||
url: `sales/payment_receives/${paymentReceiveId}/mail`,
|
||||
},
|
||||
{
|
||||
select: (res) => res.data.data,
|
||||
...props,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -207,3 +207,36 @@ export function useReceiptSMSDetail(receiptId, props, requestProps) {
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
export function useSendSaleReceiptMail(props) {
|
||||
const queryClient = useQueryClient();
|
||||
const apiRequest = useApiRequest();
|
||||
|
||||
return useMutation(
|
||||
([id, values]) => apiRequest.post(`sales/receipts/${id}/mail`, values),
|
||||
{
|
||||
onSuccess: () => {
|
||||
// Invalidate queries.
|
||||
commonInvalidateQueries(queryClient);
|
||||
},
|
||||
...props,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
export function useSaleReceiptDefaultOptions(invoiceId, props) {
|
||||
return useRequestQuery(
|
||||
[t.SALE_RECEIPT_MAIL_OPTIONS, invoiceId],
|
||||
{
|
||||
method: 'get',
|
||||
url: `sales/receipts/${invoiceId}/mail`,
|
||||
},
|
||||
{
|
||||
select: (res) => res.data.data,
|
||||
...props,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -69,6 +69,7 @@ const SALE_ESTIMATES = {
|
||||
SALE_ESTIMATE: 'SALE_ESTIMATE',
|
||||
SALE_ESTIMATE_SMS_DETAIL: 'SALE_ESTIMATE_SMS_DETAIL',
|
||||
NOTIFY_SALE_ESTIMATE_BY_SMS: 'NOTIFY_SALE_ESTIMATE_BY_SMS',
|
||||
SALE_ESTIMATE_MAIL_OPTIONS: 'SALE_ESTIMATE_MAIL_OPTIONS',
|
||||
};
|
||||
|
||||
const SALE_RECEIPTS = {
|
||||
@@ -76,6 +77,7 @@ const SALE_RECEIPTS = {
|
||||
SALE_RECEIPT: 'SALE_RECEIPT',
|
||||
SALE_RECEIPT_SMS_DETAIL: 'SALE_RECEIPT_SMS_DETAIL',
|
||||
NOTIFY_SALE_RECEIPT_BY_SMS: 'NOTIFY_SALE_RECEIPT_BY_SMS',
|
||||
SALE_RECEIPT_MAIL_OPTIONS: 'SALE_RECEIPT_MAIL_OPTIONS'
|
||||
};
|
||||
|
||||
const INVENTORY_ADJUSTMENTS = {
|
||||
@@ -101,6 +103,7 @@ const PAYMENT_RECEIVES = {
|
||||
PAYMENT_RECEIVE_EDIT_PAGE: 'PAYMENT_RECEIVE_EDIT_PAGE',
|
||||
PAYMENT_RECEIVE_SMS_DETAIL: 'PAYMENT_RECEIVE_SMS_DETAIL',
|
||||
NOTIFY_PAYMENT_RECEIVE_BY_SMS: 'NOTIFY_PAYMENT_RECEIVE_BY_SMS',
|
||||
PAYMENT_RECEIVE_MAIL_OPTIONS: 'PAYMENT_RECEIVE_MAIL_OPTIONS',
|
||||
};
|
||||
|
||||
const SALE_INVOICES = {
|
||||
@@ -112,6 +115,7 @@ const SALE_INVOICES = {
|
||||
BAD_DEBT: 'BAD_DEBT',
|
||||
CANCEL_BAD_DEBT: 'CANCEL_BAD_DEBT',
|
||||
SALE_INVOICE_PAYMENT_TRANSACTIONS: 'SALE_INVOICE_PAYMENT_TRANSACTIONS',
|
||||
SALE_INVOICE_DEFAULT_OPTIONS: 'SALE_INVOICE_DEFAULT_OPTIONS'
|
||||
};
|
||||
|
||||
const USERS = {
|
||||
|
||||
@@ -561,8 +561,14 @@ export default {
|
||||
},
|
||||
'content-copy': {
|
||||
path: [
|
||||
'M15 0H5c-.55 0-1 .45-1 1v2h2V2h8v7h-1v2h2c.55 0 1-.45 1-1V1c0-.55-.45-1-1-1zm-4 4H1c-.55 0-1 .45-1 1v10c0 .55.45 1 1 1h10c.55 0 1-.45 1-1V5c0-.55-.45-1-1-1zm-1 10H2V6h8v8z'
|
||||
'M15 0H5c-.55 0-1 .45-1 1v2h2V2h8v7h-1v2h2c.55 0 1-.45 1-1V1c0-.55-.45-1-1-1zm-4 4H1c-.55 0-1 .45-1 1v10c0 .55.45 1 1 1h10c.55 0 1-.45 1-1V5c0-.55-.45-1-1-1zm-1 10H2V6h8v8z',
|
||||
],
|
||||
viewBox: '0 0 16 16'
|
||||
}
|
||||
viewBox: '0 0 16 16',
|
||||
},
|
||||
envelope: {
|
||||
path: [
|
||||
'M0 4.01v11.91l6.27-6.27L0 4.01zm18.91-1.03H1.09L10 10.97l8.91-7.99zm-5.18 6.66L20 15.92V4.01l-6.27 5.63zm-3.23 2.9c-.13.12-.31.19-.5.19s-.37-.07-.5-.19l-2.11-1.89-6.33 6.33h17.88l-6.33-6.33-2.11 1.89z',
|
||||
],
|
||||
viewBox: '0 0 20 20',
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user