Merge branch 'develop' into big-44-auto-re-calculate-the-items-rate-once-changing-the-invoice

This commit is contained in:
Ahmed Bouhuolia
2024-01-11 20:27:42 +02:00
403 changed files with 24846 additions and 9622 deletions

View File

@@ -5,7 +5,7 @@ import classNames from 'classnames';
import { useHistory } from 'react-router-dom';
import { Formik, Form } from 'formik';
import { Intent } from '@blueprintjs/core';
import { isEmpty } from 'lodash';
import { defaultTo, isEmpty } from 'lodash';
import { CLASSES } from '@/constants/classes';
import {
CreateCreditNoteFormSchema,
@@ -48,6 +48,8 @@ function CreditNoteForm({
creditAutoIncrement,
creditNumberPrefix,
creditNextNumber,
creditCustomerNotes,
creditTermsConditions,
// #withCurrentOrganization
organization: { base_currency },
@@ -68,22 +70,21 @@ function CreditNoteForm({
const creditNumber = transactionNumber(creditNumberPrefix, creditNextNumber);
// Initial values.
const initialValues = React.useMemo(
() => ({
...(!isEmpty(creditNote)
? { ...transformToEditForm(creditNote) }
: {
...defaultCreditNote,
...(creditAutoIncrement && {
credit_note_number: creditNumber,
}),
entries: orderingLinesIndexes(defaultCreditNote.entries),
currency_code: base_currency,
...newCreditNote,
const initialValues = {
...(!isEmpty(creditNote)
? { ...transformToEditForm(creditNote) }
: {
...defaultCreditNote,
...(creditAutoIncrement && {
credit_note_number: creditNumber,
}),
}),
[],
);
entries: orderingLinesIndexes(defaultCreditNote.entries),
currency_code: base_currency,
terms_conditions: defaultTo(creditTermsConditions, ''),
note: defaultTo(creditCustomerNotes, ''),
...newCreditNote,
}),
};
// Handles form submit.
const handleFormSubmit = (
@@ -178,6 +179,8 @@ export default compose(
creditAutoIncrement: creditNoteSettings?.autoIncrement,
creditNextNumber: creditNoteSettings?.nextNumber,
creditNumberPrefix: creditNoteSettings?.numberPrefix,
creditCustomerNotes: creditNoteSettings?.customerNotes,
creditTermsConditions: creditNoteSettings?.termsConditions,
})),
withCurrentOrganization(),
)(CreditNoteForm);

View File

@@ -4,7 +4,7 @@ import intl from 'react-intl-universal';
import classNames from 'classnames';
import { Formik, Form } from 'formik';
import { Intent } from '@blueprintjs/core';
import { sumBy, isEmpty } from 'lodash';
import { sumBy, isEmpty, defaultTo } from 'lodash';
import { useHistory } from 'react-router-dom';
import { CLASSES } from '@/constants/classes';
@@ -43,6 +43,8 @@ function EstimateForm({
estimateNextNumber,
estimateNumberPrefix,
estimateAutoIncrementMode,
estimateCustomerNotes,
estimateTermsConditions,
// #withCurrentOrganization
organization: { base_currency },
@@ -60,25 +62,23 @@ function EstimateForm({
estimateNumberPrefix,
estimateNextNumber,
);
// Initial values in create and edit mode.
const initialValues = useMemo(
() => ({
...(!isEmpty(estimate)
? { ...transformToEditForm(estimate) }
: {
...defaultEstimate,
// If the auto-increment mode is enabled, take the next estimate
// number from the settings.
...(estimateAutoIncrementMode && {
estimate_number: estimateNumber,
}),
entries: orderingLinesIndexes(defaultEstimate.entries),
currency_code: base_currency,
const initialValues = {
...(!isEmpty(estimate)
? { ...transformToEditForm(estimate) }
: {
...defaultEstimate,
// If the auto-increment mode is enabled, take the next estimate
// number from the settings.
...(estimateAutoIncrementMode && {
estimate_number: estimateNumber,
}),
}),
[estimate, estimateNumber, estimateAutoIncrementMode, base_currency],
);
entries: orderingLinesIndexes(defaultEstimate.entries),
currency_code: base_currency,
terms_conditions: defaultTo(estimateTermsConditions, ''),
note: defaultTo(estimateCustomerNotes, ''),
}),
};
// Handles form submit.
const handleFormSubmit = (
@@ -181,6 +181,8 @@ export default compose(
estimateNextNumber: estimatesSettings?.nextNumber,
estimateNumberPrefix: estimatesSettings?.numberPrefix,
estimateAutoIncrementMode: estimatesSettings?.autoIncrement,
estimateCustomerNotes: estimatesSettings?.customerNotes,
estimateTermsConditions: estimatesSettings?.termsConditions,
})),
withCurrentOrganization(),
)(EstimateForm);

View File

@@ -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);

View File

@@ -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 };

View File

@@ -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>
)
}

View File

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

View File

@@ -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;
`;

View File

@@ -0,0 +1 @@
export * from './EstimateMailDialog';

View File

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

View File

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

View File

@@ -4,7 +4,7 @@ import intl from 'react-intl-universal';
import classNames from 'classnames';
import { Formik, Form } from 'formik';
import { Intent } from '@blueprintjs/core';
import { sumBy, isEmpty } from 'lodash';
import { sumBy, isEmpty, defaultTo } from 'lodash';
import { useHistory } from 'react-router-dom';
import { CLASSES } from '@/constants/classes';
import {
@@ -44,6 +44,8 @@ function InvoiceForm({
invoiceNextNumber,
invoiceNumberPrefix,
invoiceAutoIncrementMode,
invoiceCustomerNotes,
invoiceTermsConditions,
// #withCurrentOrganization
organization: { base_currency },
@@ -79,6 +81,8 @@ function InvoiceForm({
}),
entries: orderingLinesIndexes(defaultInvoice.entries),
currency_code: base_currency,
invoice_message: defaultTo(invoiceCustomerNotes, ''),
terms_conditions: defaultTo(invoiceTermsConditions, ''),
...newInvoice,
}),
};
@@ -193,6 +197,8 @@ export default compose(
invoiceNextNumber: invoiceSettings?.nextNumber,
invoiceNumberPrefix: invoiceSettings?.numberPrefix,
invoiceAutoIncrementMode: invoiceSettings?.autoIncrement,
invoiceCustomerNotes: invoiceSettings?.customerNotes,
invoiceTermsConditions: invoiceSettings?.termsConditions,
})),
withCurrentOrganization(),
)(InvoiceForm);

View File

@@ -333,8 +333,8 @@ export const useInvoiceAggregatedTaxRates = () => {
const { taxRates } = useInvoiceFormContext();
const aggregateTaxRates = React.useMemo(
() => aggregateItemEntriesTaxRates(taxRates),
[taxRates],
() => aggregateItemEntriesTaxRates(values.currency_code, taxRates),
[values.currency_code, taxRates],
);
// Calculate the total tax amount of invoice entries.
return React.useMemo(() => {

View File

@@ -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);

View File

@@ -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 };

View File

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

View File

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

View File

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

View File

@@ -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;
`;

View File

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

View File

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

View File

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

View File

@@ -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);

View File

@@ -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 };

View File

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

View File

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

View File

@@ -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;
`;

View File

@@ -0,0 +1 @@
export * from './PaymentMailDialog';

View File

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

View File

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

View File

@@ -1,5 +1,4 @@
// @ts-nocheck
import React, { useMemo } from 'react';
import intl from 'react-intl-universal';
import classNames from 'classnames';
import { Formik, Form } from 'formik';
@@ -45,6 +44,8 @@ function ReceiptForm({
receiptNextNumber,
receiptNumberPrefix,
receiptAutoIncrement,
receiptTermsConditions,
receiptMessage,
preferredDepositAccount,
// #withCurrentOrganization
@@ -67,23 +68,21 @@ function ReceiptForm({
receiptNextNumber,
);
// Initial values in create and edit mode.
const initialValues = useMemo(
() => ({
...(!isEmpty(receipt)
? { ...transformToEditForm(receipt) }
: {
...defaultReceipt,
...(receiptAutoIncrement && {
receipt_number: nextReceiptNumber,
}),
deposit_account_id: parseInt(preferredDepositAccount),
entries: orderingLinesIndexes(defaultReceipt.entries),
currency_code: base_currency,
const initialValues = {
...(!isEmpty(receipt)
? { ...transformToEditForm(receipt) }
: {
...defaultReceipt,
...(receiptAutoIncrement && {
receipt_number: nextReceiptNumber,
}),
}),
[receipt, preferredDepositAccount, nextReceiptNumber, receiptAutoIncrement],
);
deposit_account_id: parseInt(preferredDepositAccount),
entries: orderingLinesIndexes(defaultReceipt.entries),
currency_code: base_currency,
receipt_message: receiptMessage,
terms_conditions: receiptTermsConditions,
}),
};
// Handle the form submit.
const handleFormSubmit = (
values,
@@ -184,6 +183,8 @@ export default compose(
receiptNextNumber: receiptSettings?.nextNumber,
receiptNumberPrefix: receiptSettings?.numberPrefix,
receiptAutoIncrement: receiptSettings?.autoIncrement,
receiptMessage: receiptSettings?.receiptMessage,
receiptTermsConditions: receiptSettings?.termsConditions,
preferredDepositAccount: receiptSettings?.preferredDepositAccount,
})),
withCurrentOrganization(),

View File

@@ -7,7 +7,6 @@ import { omit, first } from 'lodash';
import { useFormikContext } from 'formik';
import {
defaultFastFieldShouldUpdate,
transactionNumber,
repeatValue,
transformToForm,
formattedAmount,
@@ -50,7 +49,7 @@ export const defaultReceipt = {
receipt_date: moment(new Date()).format('YYYY-MM-DD'),
reference_no: '',
receipt_message: '',
statement: '',
terms_conditions: '',
closed: '',
branch_id: '',
warehouse_id: '',

View File

@@ -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);

View File

@@ -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 };

View File

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

View File

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

View File

@@ -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;
`;

View File

@@ -0,0 +1 @@
export * from './ReceiptMailDialog';

View File

@@ -140,7 +140,6 @@ function ReceiptActionsBar({
icon={<Icon icon={'file-export-16'} iconSize={'16'} />}
text={<T id={'export'} />}
/>
<NavbarDivider />
<DashboardRowsHeightButton
initialValue={receiptsTableSize}

View File

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

View File

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