feat: wip send estimate mail preview

This commit is contained in:
Ahmed Bouhuolia
2024-11-18 15:15:03 +02:00
parent 53ab40a075
commit 7df316aa56
34 changed files with 405 additions and 459 deletions

View File

@@ -140,10 +140,10 @@ export default class SalesEstimatesController extends BaseController {
this.handleServiceErrors this.handleServiceErrors
); );
router.get( router.get(
'/:id/mail', '/:id/mail/state',
[...this.validateSpecificEstimateSchema], [...this.validateSpecificEstimateSchema],
this.validationResult, this.validationResult,
asyncMiddleware(this.getSaleEstimateMail.bind(this)), asyncMiddleware(this.getSaleEstimateMailState.bind(this)),
this.handleServiceErrors this.handleServiceErrors
); );
return router; return router;
@@ -540,18 +540,18 @@ export default class SalesEstimatesController extends BaseController {
* @param {Response} res * @param {Response} res
* @param {NextFunction} next * @param {NextFunction} next
*/ */
private getSaleEstimateMail = async ( private getSaleEstimateMailState = async (
req: Request, req: Request<{ id: number }>,
res: Response, res: Response,
next: NextFunction next: NextFunction
) => { ) => {
const { tenantId } = req; const { tenantId } = req;
const { id: invoiceId } = req.params; const { id: estimateId } = req.params;
try { try {
const data = await this.saleEstimatesApplication.getSaleEstimateMail( const data = await this.saleEstimatesApplication.getSaleEstimateMailState(
tenantId, tenantId,
invoiceId estimateId
); );
return res.status(200).send({ data }); return res.status(200).send({ data });
} catch (error) { } catch (error) {

View File

@@ -184,6 +184,7 @@ export default class SaleEstimate extends mixin(TenantModel, [
const Branch = require('models/Branch'); const Branch = require('models/Branch');
const Warehouse = require('models/Warehouse'); const Warehouse = require('models/Warehouse');
const Document = require('models/Document'); const Document = require('models/Document');
const { PdfTemplate } = require('models/PdfTemplate');
return { return {
customer: { customer: {
@@ -252,6 +253,18 @@ export default class SaleEstimate extends mixin(TenantModel, [
query.where('model_ref', 'SaleEstimate'); query.where('model_ref', 'SaleEstimate');
}, },
}, },
/**
* Sale estimate may belongs to pdf branding template.
*/
pdfTemplate: {
relation: Model.BelongsToOneRelation,
modelClass: PdfTemplate,
join: {
from: 'sales_estimates.pdfTemplateId',
to: 'pdf_templates.id',
},
},
}; };
} }

View File

@@ -0,0 +1,51 @@
import { Inject } from 'typedi';
import { SendSaleEstimateMail } from './SendSaleEstimateMail';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { GetSaleEstimateMailStateTransformer } from './GetSaleEstimateMailStateTransformer';
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
export class GetSaleEstimateMailState {
@Inject()
private estimateMail: SendSaleEstimateMail;
@Inject()
private tenancy: HasTenancyService;
@Inject()
private transformer: TransformerInjectable;
/**
* Retrieves the estimate mail state of the given sale estimate.
* Estimate mail state includes the mail options, branding attributes and the estimate details.
* @param {number} tenantId
* @param {number} saleEstimateId
* @returns {Promise<SaleEstimateMailState>}
*/
async getEstimateMailState(
tenantId: number,
saleEstimateId: number
): Promise<SaleEstimateMailState> {
const { SaleEstimate } = this.tenancy.models(tenantId);
const saleEstimate = await SaleEstimate.query()
.findById(saleEstimateId)
.withGraphFetched('customer')
.withGraphFetched('entries.item')
.withGraphFetched('pdfTemplate')
.throwIfNotFound();
const mailOptions = await this.estimateMail.getMailOptions(
tenantId,
saleEstimateId
);
const transformed = await this.transformer.transform(
tenantId,
saleEstimate,
new GetSaleEstimateMailStateTransformer(),
{
mailOptions,
}
);
return transformed;
}
}

View File

@@ -0,0 +1,105 @@
import { ItemEntryTransformer } from '../Invoices/ItemEntryTransformer';
import { SaleEstimateTransfromer } from './SaleEstimateTransformer';
export class GetSaleEstimateMailStateTransformer extends SaleEstimateTransfromer {
public excludeAttributes = (): string[] => {
return ['*'];
};
public includeAttributes = (): string[] => {
return [
'estimateDate',
'formattedEstimateDate',
'total',
'totalFormatted',
'subtotal',
'subtotalFormatted',
'estimateNo',
'entries',
'companyName',
'companyLogoUri',
'primaryColor',
'customerName',
];
};
/**
* Retrieves the customer name of the invoice.
* @returns {string}
*/
protected customerName = (invoice) => {
return invoice.customer.displayName;
};
/**
* Retrieves the company name.
* @returns {string}
*/
protected companyName = () => {
return this.context.organization.name;
};
/**
* Retrieves the company logo uri.
* @returns {string | null}
*/
protected companyLogoUri = (invoice) => {
return invoice.pdfTemplate?.companyLogoUri;
};
/**
* Retrieves the primary color.
* @returns {string}
*/
protected primaryColor = (invoice) => {
return invoice.pdfTemplate?.attributes?.primaryColor;
};
/**
*
* @param invoice
* @returns
*/
protected entries = (invoice) => {
return this.item(
invoice.entries,
new GetSaleEstimateMailStateEntryTransformer(),
{
currencyCode: invoice.currencyCode,
}
);
};
/**
* Merges the mail options with the invoice object.
*/
public transform = (object: any) => {
return {
...this.options.mailOptions,
...object,
};
};
}
class GetSaleEstimateMailStateEntryTransformer extends ItemEntryTransformer {
public excludeAttributes = (): string[] => {
return ['*'];
};
public includeAttributes = (): string[] => {
return [
'description',
'quantity',
'unitPrice',
'unitPriceFormatted',
'total',
'totalFormatted',
];
};
}

View File

@@ -21,6 +21,7 @@ import { SaleEstimateNotifyBySms } from './SaleEstimateSmsNotify';
import { SaleEstimatesPdf } from './SaleEstimatesPdf'; import { SaleEstimatesPdf } from './SaleEstimatesPdf';
import { SendSaleEstimateMail } from './SendSaleEstimateMail'; import { SendSaleEstimateMail } from './SendSaleEstimateMail';
import { GetSaleEstimateState } from './GetSaleEstimateState'; import { GetSaleEstimateState } from './GetSaleEstimateState';
import { GetSaleEstimateMailState } from './GetSaleEstimateMailState';
@Service() @Service()
export class SaleEstimatesApplication { export class SaleEstimatesApplication {
@@ -57,6 +58,9 @@ export class SaleEstimatesApplication {
@Inject() @Inject()
private sendEstimateMailService: SendSaleEstimateMail; private sendEstimateMailService: SendSaleEstimateMail;
@Inject()
private getSaleEstimateMailStateService: GetSaleEstimateMailState;
@Inject() @Inject()
private getSaleEstimateStateService: GetSaleEstimateState; private getSaleEstimateStateService: GetSaleEstimateState;
@@ -250,6 +254,22 @@ export class SaleEstimatesApplication {
); );
} }
/**
* Retrieves the sale estimate mail state.
* @param {number} tenantId
* @param {number} saleEstimateId
* @returns {Promise<SaleEstimateMailOptions>}
*/
public getSaleEstimateMailState(
tenantId: number,
saleEstimateId: number
): Promise<SaleEstimateMailOptions> {
return this.getSaleEstimateMailStateService.getEstimateMailState(
tenantId,
saleEstimateId
);
}
/** /**
* Retrieves the default mail options of the given sale estimate. * Retrieves the default mail options of the given sale estimate.
* @param {number} tenantId * @param {number} tenantId

View File

@@ -45,7 +45,6 @@ import ProjectBillableEntriesFormDialog from '@/containers/Projects/containers/P
import TaxRateFormDialog from '@/containers/TaxRates/dialogs/TaxRateFormDialog/TaxRateFormDialog'; import TaxRateFormDialog from '@/containers/TaxRates/dialogs/TaxRateFormDialog/TaxRateFormDialog';
import { DialogsName } from '@/constants/dialogs'; import { DialogsName } from '@/constants/dialogs';
import InvoiceExchangeRateChangeDialog from '@/containers/Sales/Invoices/InvoiceForm/Dialogs/InvoiceExchangeRateChangeDialog'; import InvoiceExchangeRateChangeDialog from '@/containers/Sales/Invoices/InvoiceForm/Dialogs/InvoiceExchangeRateChangeDialog';
import EstimateMailDialog from '@/containers/Sales/Estimates/EstimateMailDialog/EstimateMailDialog';
import ReceiptMailDialog from '@/containers/Sales/Receipts/ReceiptMailDialog/ReceiptMailDialog'; import ReceiptMailDialog from '@/containers/Sales/Receipts/ReceiptMailDialog/ReceiptMailDialog';
import PaymentMailDialog from '@/containers/Sales/PaymentsReceived/PaymentMailDialog/PaymentMailDialog'; import PaymentMailDialog from '@/containers/Sales/PaymentsReceived/PaymentMailDialog/PaymentMailDialog';
import { ExportDialog } from '@/containers/Dialogs/ExportDialog'; import { ExportDialog } from '@/containers/Dialogs/ExportDialog';
@@ -143,7 +142,6 @@ export default function DialogsContainer() {
<InvoiceExchangeRateChangeDialog <InvoiceExchangeRateChangeDialog
dialogName={DialogsName.InvoiceExchangeRateChangeNotice} dialogName={DialogsName.InvoiceExchangeRateChangeNotice}
/> />
<EstimateMailDialog dialogName={DialogsName.EstimateMail} />
<ReceiptMailDialog dialogName={DialogsName.ReceiptMail} /> <ReceiptMailDialog dialogName={DialogsName.ReceiptMail} />
<PaymentMailDialog dialogName={DialogsName.PaymentMail} /> <PaymentMailDialog dialogName={DialogsName.PaymentMail} />
<ExportDialog dialogName={DialogsName.Export} /> <ExportDialog dialogName={DialogsName.Export} />

View File

@@ -32,6 +32,7 @@ import { BrandingTemplatesDrawer } from '@/containers/BrandingTemplates/Branding
import { DRAWERS } from '@/constants/drawers'; import { DRAWERS } from '@/constants/drawers';
import { InvoiceSendMailDrawer } from '@/containers/Sales/Invoices/InvoiceSendMailDrawer/InvoiceSendMailDrawer'; import { InvoiceSendMailDrawer } from '@/containers/Sales/Invoices/InvoiceSendMailDrawer/InvoiceSendMailDrawer';
import { EstimateSendMailDrawer } from '@/containers/Sales/Estimates/EstimateSendMailDrawer';
/** /**
* Drawers container of the dashboard. * Drawers container of the dashboard.
@@ -81,6 +82,7 @@ export default function DrawersContainer() {
/> />
<BrandingTemplatesDrawer name={DRAWERS.BRANDING_TEMPLATES} /> <BrandingTemplatesDrawer name={DRAWERS.BRANDING_TEMPLATES} />
<InvoiceSendMailDrawer name={DRAWERS.INVOICE_SEND_MAIL} /> <InvoiceSendMailDrawer name={DRAWERS.INVOICE_SEND_MAIL} />
<EstimateSendMailDrawer name={DRAWERS.ESTIMATE_SEND_MAIL} />
</div> </div>
); );
} }

View File

@@ -34,5 +34,6 @@ export enum DRAWERS {
BRANDING_TEMPLATES = 'BRANDING_TEMPLATES', BRANDING_TEMPLATES = 'BRANDING_TEMPLATES',
PAYMENT_INVOICE_PREVIEW = 'PAYMENT_INVOICE_PREVIEW', PAYMENT_INVOICE_PREVIEW = 'PAYMENT_INVOICE_PREVIEW',
STRIPE_PAYMENT_INTEGRATION_EDIT = 'STRIPE_PAYMENT_INTEGRATION_EDIT', STRIPE_PAYMENT_INTEGRATION_EDIT = 'STRIPE_PAYMENT_INTEGRATION_EDIT',
INVOICE_SEND_MAIL = 'INVOICE_SEND_MAIL' INVOICE_SEND_MAIL = 'INVOICE_SEND_MAIL',
ESTIMATE_SEND_MAIL = 'ESTIMATE_SEND_MAIL',
} }

View File

@@ -1,39 +0,0 @@
// @ts-nocheck
import React from 'react';
import { Dialog, DialogSuspense } from '@/components';
import withDialogRedux from '@/components/DialogReduxConnect';
import { compose } from '@/utils';
const EstimateFormMailDeliverDialogContent = React.lazy(
() => import('./EstimateFormMailDeliverDialogContent'),
);
/**
* Estimate mail dialog.
*/
function EstimateFormMailDeliverDialog({
dialogName,
payload: { estimateId = null },
isOpen,
}) {
return (
<Dialog
name={dialogName}
title={'Estimate Mail'}
isOpen={isOpen}
canEscapeJeyClose={false}
isCloseButtonShown={false}
autoFocus={true}
style={{ width: 600 }}
>
<DialogSuspense>
<EstimateFormMailDeliverDialogContent
dialogName={dialogName}
estimateId={estimateId}
/>
</DialogSuspense>
</Dialog>
);
}
export default compose(withDialogRedux())(EstimateFormMailDeliverDialog);

View File

@@ -1,40 +0,0 @@
// @ts-nocheck
import * as R from 'ramda';
import withDialogActions from '@/containers/Dialog/withDialogActions';
import { useHistory } from 'react-router-dom';
import EstimateMailDialogContent from '../../EstimateMailDialog/EstimateMailDialogContent';
import { DialogsName } from '@/constants/dialogs';
interface EstimateFormDeliverDialogContent {
estimateId: number;
}
function EstimateFormDeliverDialogContentRoot({
estimateId,
// #withDialogActions
closeDialog,
}: EstimateFormDeliverDialogContent) {
const history = useHistory();
const handleSubmit = () => {
closeDialog(DialogsName.EstimateFormMailDeliver);
history.push('/estimates');
};
const handleCancel = () => {
closeDialog(DialogsName.EstimateFormMailDeliver);
history.push('/estimates');
};
return (
<EstimateMailDialogContent
estimateId={estimateId}
onFormSubmit={handleSubmit}
onCancelClick={handleCancel}
/>
);
}
export default R.compose(withDialogActions)(
EstimateFormDeliverDialogContentRoot,
);

View File

@@ -1,9 +1,6 @@
// @ts-nocheck // @ts-nocheck
import React from 'react'; import React from 'react';
import { useFormikContext } from 'formik';
import EstimateNumberDialog from '@/containers/Dialogs/EstimateNumberDialog'; import EstimateNumberDialog from '@/containers/Dialogs/EstimateNumberDialog';
import EstimateFormMailDeliverDialog from './Dialogs/EstimateFormMailDeliverDialog';
import { DialogsName } from '@/constants/dialogs';
/** /**
* Estimate form dialogs. * Estimate form dialogs.
@@ -27,9 +24,6 @@ export default function EstimateFormDialogs() {
dialogName={'estimate-number-form'} dialogName={'estimate-number-form'}
onConfirm={handleEstimateNumberFormConfirm} onConfirm={handleEstimateNumberFormConfirm}
/> />
<EstimateFormMailDeliverDialog
dialogName={DialogsName.EstimateFormMailDeliver}
/>
</> </>
); );
} }

View File

@@ -1,35 +0,0 @@
// @ts-nocheck
import React from 'react';
import { Dialog, DialogSuspense } from '@/components';
import withDialogRedux from '@/components/DialogReduxConnect';
import { compose } from '@/utils';
const EstimateMailDialogBody = React.lazy(
() => import('./EstimateMailDialogBody'),
);
/**
* Estimate 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>
<EstimateMailDialogBody estimateId={estimateId} />
</DialogSuspense>
</Dialog>
);
}
export default compose(withDialogRedux())(EstimateMailDialog);

View File

@@ -1,33 +0,0 @@
// @ts-nocheck
import * as R from 'ramda';
import withDialogActions from '@/containers/Dialog/withDialogActions';
import EstimateMailDialogContent from './EstimateMailDialogContent';
import { DialogsName } from '@/constants/dialogs';
interface EstimateMailDialogBodyProps {
estimateId: number;
}
function EstimateMailDialogBodyRoot({
estimateId,
// #withDialogActions
closeDialog,
}: EstimateMailDialogBodyProps) {
const handleSubmit = () => {
closeDialog(DialogsName.EstimateMail);
};
const handleCancelClick = () => {
closeDialog(DialogsName.EstimateMail);
};
return (
<EstimateMailDialogContent
estimateId={estimateId}
onFormSubmit={handleSubmit}
onCancelClick={handleCancelClick}
/>
);
}
export default R.compose(withDialogActions)(EstimateMailDialogBodyRoot);

View File

@@ -1,48 +0,0 @@
// @ts-nocheck
import React, { createContext } from 'react';
import { useSaleEstimateDefaultOptions } from '@/hooks/query';
import { DialogContent } from '@/components';
interface EstimateMailDialogBootValues {
estimateId: number;
mailOptions: any;
redirectToEstimatesList: boolean;
}
const EstimateMailDialagBoot = createContext<EstimateMailDialogBootValues>();
interface EstimateMailDialogBootProps {
estimateId: number;
redirectToEstimatesList?: boolean;
children: React.ReactNode;
}
/**
* Estimate mail dialog boot provider.
*/
function EstimateMailDialogBoot({
estimateId,
redirectToEstimatesList,
...props
}: EstimateMailDialogBootProps) {
const { data: mailOptions, isLoading: isMailOptionsLoading } =
useSaleEstimateDefaultOptions(estimateId);
const provider = {
saleEstimateId: estimateId,
mailOptions,
isMailOptionsLoading,
redirectToEstimatesList,
};
return (
<DialogContent isLoading={isMailOptionsLoading}>
<EstimateMailDialagBoot.Provider value={provider} {...props} />
</DialogContent>
);
}
const useEstimateMailDialogBoot = () =>
React.useContext<EstimateMailDialogBootValues>(EstimateMailDialagBoot);
export { EstimateMailDialogBoot, useEstimateMailDialogBoot };

View File

@@ -1,22 +0,0 @@
import { EstimateMailDialogBoot } from './EstimateMailDialogBoot';
import { EstimateMailDialogForm } from './EstimateMailDialogForm';
interface EstimateMailDialogContentProps {
estimateId: number;
onFormSubmit?: () => void;
onCancelClick?: () => void;
}
export default function EstimateMailDialogContent({
estimateId,
onFormSubmit,
onCancelClick,
}: EstimateMailDialogContentProps) {
return (
<EstimateMailDialogBoot estimateId={estimateId}>
<EstimateMailDialogForm
onFormSubmit={onFormSubmit}
onCancelClick={onCancelClick}
/>
</EstimateMailDialogBoot>
);
}

View File

@@ -1,81 +0,0 @@
// @ts-nocheck
import { Formik } from 'formik';
import * as R from 'ramda';
import { Intent } from '@blueprintjs/core';
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 { AppToaster } from '@/components';
const initialFormValues = {
...initialMailNotificationValues,
attachEstimate: true,
};
interface EstimateMailFormValues extends MailNotificationFormValues {
attachEstimate: boolean;
}
function EstimateMailDialogFormRoot({
onFormSubmit,
onCancelClick,
// #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);
onFormSubmit && onFormSubmit();
})
.catch(() => {
setSubmitting(false);
closeDialog(DialogsName.EstimateMail);
AppToaster.show({
message: 'Something went wrong.',
intent: Intent.DANGER,
});
onCancelClick && onCancelClick();
});
};
const handleClose = () => {
closeDialog(DialogsName.EstimateMail);
};
return (
<Formik initialValues={initialValues} onSubmit={handleSubmit}>
<EstimateMailDialogFormContent onClose={handleClose} />
</Formik>
);
}
export const EstimateMailDialogForm = R.compose(withDialogActions)(
EstimateMailDialogFormRoot,
);

View File

@@ -1,66 +0,0 @@
// @ts-nocheck
import { Form, useFormikContext } from 'formik';
import { Button, Classes, Intent } from '@blueprintjs/core';
import styled from 'styled-components';
import { FFormGroup, FSwitch } from '@/components';
import { MailNotificationForm } from '@/containers/SendMailNotification';
import { saveInvoke } from '@/utils';
import { 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_options}
toAddresses={mailOptions.to_options}
/>
<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

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

View File

@@ -1,11 +1,12 @@
import React, { createContext, useContext } from 'react'; import React, { createContext, useContext } from 'react';
import { Spinner } from '@blueprintjs/core'; import { Spinner } from '@blueprintjs/core';
import { useDrawerContext } from '@/components/Drawer/DrawerProvider'; import { useDrawerContext } from '@/components/Drawer/DrawerProvider';
import { useSaleEstimateMailState } from '@/hooks/query';
interface EstimateSendMailBootValues { interface EstimateSendMailBootValues {
estimateId: number; estimateId: number;
estimateMailState: GetSaleEstimateDefaultOptionsResponse | undefined; estimateMailState: any;
isEstimateMailState: boolean; isEstimateMailState: boolean;
} }
interface EstimateSendMailBootProps { interface EstimateSendMailBootProps {
@@ -15,7 +16,9 @@ interface EstimateSendMailBootProps {
const EstimateSendMailContentBootContext = const EstimateSendMailContentBootContext =
createContext<EstimateSendMailBootValues>({} as EstimateSendMailBootValues); createContext<EstimateSendMailBootValues>({} as EstimateSendMailBootValues);
export const EstimateSendMailBoot = ({ children }: EstimateSendMailBootProps) => { export const EstimateSendMailBoot = ({
children,
}: EstimateSendMailBootProps) => {
const { const {
payload: { estimateId }, payload: { estimateId },
} = useDrawerContext(); } = useDrawerContext();

View File

@@ -1,9 +1,24 @@
import { Classes } from '@blueprintjs/core';
import { EstimateSendMailBoot } from './EstimateSendMailBoot';
import { Stack } from '@/components';
import { EstimateSendMailForm } from './EstimateSendMailForm';
import { SendMailViewHeader } from '../SendMailViewDrawer/SendMailViewHeader';
import { SendMailViewLayout } from '../SendMailViewDrawer/SendMailViewLayout';
import { EstimateSendMailFields } from './EstimateSnedMailFields';
import { EstimateSendMailPreviewTabs } from './EstimateSendMailPreview';
export function EstimateSendMailContent() { export function EstimateSendMailContent() {
return ( return (
<Stack className={Classes.DRAWER_BODY}>
<EstimateSendMailBoot>
<EstimateSendMailForm>
<SendMailViewLayout
header={<SendMailViewHeader label={'Send Estimate Mail'} />}
fields={<EstimateSendMailFields />}
preview={<EstimateSendMailPreviewTabs />}
/>
</EstimateSendMailForm>
</EstimateSendMailBoot>
</Stack>
); );
} }

View File

@@ -4,9 +4,9 @@ import * as R from 'ramda';
import { Drawer, DrawerSuspense } from '@/components'; import { Drawer, DrawerSuspense } from '@/components';
import withDrawers from '@/containers/Drawer/withDrawers'; import withDrawers from '@/containers/Drawer/withDrawers';
const EstimateSendMailDrawerProps = React.lazy(() => const EstimateSendMailContent = React.lazy(() =>
import('./EstimateSendMailContent').then((module) => ({ import('./EstimateSendMailContent').then((module) => ({
default: module.InvoiceSendMailContent, default: module.EstimateSendMailContent,
})), })),
); );
@@ -31,7 +31,7 @@ function EstimateSendMailDrawerRoot({
size={'calc(100% - 10px)'} size={'calc(100% - 10px)'}
> >
<DrawerSuspense> <DrawerSuspense>
<EstimateSendMailDrawerProps /> <EstimateSendMailContent />
</DrawerSuspense> </DrawerSuspense>
</Drawer> </Drawer>
); );

View File

@@ -1,17 +1,17 @@
// @ts-nocheck
import { Form, Formik, FormikHelpers } from 'formik'; import { Form, Formik, FormikHelpers } from 'formik';
import { css } from '@emotion/css'; import { css } from '@emotion/css';
import { Intent } from '@blueprintjs/core'; import { Intent } from '@blueprintjs/core';
import { InvoiceSendMailFormValues } from './_types'; import { EstimateSendMailFormValues } from './_interfaces';
import { InvoiceSendMailFormSchema } from './InvoiceSendMailForm.schema'; import { EstimateSendMailSchema } from './EstimateSendMail.schema';
import { useSendSaleInvoiceMail } from '@/hooks/query'; import { useSendSaleEstimateMail } from '@/hooks/query';
import { AppToaster } from '@/components'; import { AppToaster } from '@/components';
import { useInvoiceSendMailBoot } from './InvoiceSendMailContentBoot';
import { useDrawerActions } from '@/hooks/state'; import { useDrawerActions } from '@/hooks/state';
import { useDrawerContext } from '@/components/Drawer/DrawerProvider'; import { useDrawerContext } from '@/components/Drawer/DrawerProvider';
import { transformToForm } from '@/utils'; import { transformToForm } from '@/utils';
import { useEstimateSendMailBoot } from './EstimateSendMailBoot'; import { useEstimateSendMailBoot } from './EstimateSendMailBoot';
const initialValues: InvoiceSendMailFormValues = { const initialValues: EstimateSendMailFormValues = {
subject: '', subject: '',
message: '', message: '',
to: [], to: [],
@@ -20,27 +20,27 @@ const initialValues: InvoiceSendMailFormValues = {
attachPdf: true, attachPdf: true,
}; };
interface InvoiceSendMailFormProps { interface EstimateSendMailFormProps {
children: React.ReactNode; children: React.ReactNode;
} }
export function EstimateSendMailForm({ children }: InvoiceSendMailFormProps) { export function EstimateSendMailForm({ children }: EstimateSendMailFormProps) {
const { mutateAsync: sendInvoiceMail } = useSendSaleInvoiceMail(); const { mutateAsync: sendEstimateMail } = useSendSaleEstimateMail();
const { estimateId, estimateMailState } = useEstimateSendMailBoot(); const { estimateId, estimateMailState } = useEstimateSendMailBoot();
const { name } = useDrawerContext(); const { name } = useDrawerContext();
const { closeDrawer } = useDrawerActions(); const { closeDrawer } = useDrawerActions();
const _initialValues: InvoiceSendMailFormValues = { const _initialValues: EstimateSendMailFormValues = {
...initialValues, ...initialValues,
...transformToForm(invoiceMailState, initialValues), ...transformToForm(estimateMailState, initialValues),
}; };
const handleSubmit = ( const handleSubmit = (
values: InvoiceSendMailFormValues, values: EstimateSendMailFormValues,
{ setSubmitting }: FormikHelpers<InvoiceSendMailFormValues>, { setSubmitting }: FormikHelpers<EstimateSendMailFormValues>,
) => { ) => {
setSubmitting(true); setSubmitting(true);
sendInvoiceMail({ id: invoiceId, values: { ...values } }) sendEstimateMail({ id: estimateId, values: { ...values } })
.then(() => { .then(() => {
AppToaster.show({ AppToaster.show({
message: 'The invoice mail has been sent to the customer.', message: 'The invoice mail has been sent to the customer.',
@@ -61,7 +61,7 @@ export function EstimateSendMailForm({ children }: InvoiceSendMailFormProps) {
return ( return (
<Formik <Formik
initialValues={_initialValues} initialValues={_initialValues}
validationSchema={InvoiceSendMailFormSchema} validationSchema={EstimateSendMailSchema}
onSubmit={handleSubmit} onSubmit={handleSubmit}
> >
<Form <Form

View File

@@ -0,0 +1,7 @@
export function EstimateSendMailPreviewTabs() {
return (
<h1>asdfsdf</h1>
)
}

View File

@@ -0,0 +1,74 @@
import { FCheckbox, FFormGroup, FInputGroup, Group, Stack } from "@/components";
import { SendMailViewToAddressField } from "../SendMailViewDrawer/SendMailViewToAddressField";
import { SendMailViewMessageField } from "../SendMailViewDrawer/SendMailViewMessageField";
import { Button, Intent } from "@blueprintjs/core";
import { useFormikContext } from "formik";
import { useDrawerContext } from "@/components/Drawer/DrawerProvider";
import { useDrawerActions } from "@/hooks/state";
const items: Array<any> = [];
const argsOptions: Array<any> = [];
export function EstimateSendMailFields() {
return (
<Stack>
<Stack spacing={0} overflow="auto" flex="1" p={'30px'}>
<SendMailViewToAddressField
toMultiSelectProps={{ items }}
ccMultiSelectProps={{ items }}
bccMultiSelectProps={{ items }}
/>
<FFormGroup label={'Submit'} name={'subject'}>
<FInputGroup name={'subject'} large fastField />
</FFormGroup>
<SendMailViewMessageField argsOptions={argsOptions} />
<Group>
<FCheckbox name={'attachPdf'} label={'Attach PDF'} />
</Group>
</Stack>
<EstimateSendMailFooter />
</Stack>
);
}
function EstimateSendMailFooter() {
const { isSubmitting } = useFormikContext();
const { name } = useDrawerContext();
const { closeDrawer } = useDrawerActions();
const handleClose = () => {
closeDrawer(name);
};
return (
<Group
py={'12px'}
px={'16px'}
borderTop="1px solid #d8d8d9"
position={'apart'}
>
<Group spacing={10} ml={'auto'}>
<Button
disabled={isSubmitting}
onClick={handleClose}
style={{ minWidth: '65px' }}
>
Close
</Button>
<Button
intent={Intent.PRIMARY}
loading={isSubmitting}
style={{ minWidth: '85px' }}
type="submit"
>
Send Mail
</Button>
</Group>
</Group>
);
}

View File

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

View File

@@ -107,7 +107,7 @@ function EstimatesDataTable({
// Handle mail send estimate. // Handle mail send estimate.
const handleMailSendEstimate = ({ id }) => { const handleMailSendEstimate = ({ id }) => {
openDialog(DialogsName.EstimateMail, { estimateId: id }); openDrawer(DRAWERS.ESTIMATE_SEND_MAIL, { estimateId: id });
} }
// Local storage memorizing columns widths. // Local storage memorizing columns widths.

View File

@@ -0,0 +1,37 @@
import { Group, Stack } from '@/components';
import React from 'react';
interface SendMailViewLayoutProps {
header?: React.ReactNode;
fields?: React.ReactNode;
preview?: React.ReactNode;
}
export function SendMailViewLayout({
header,
fields,
preview,
}: SendMailViewLayoutProps) {
return (
<Stack spacing={0} flex={1} overflow="hidden">
{header}
<Group flex={1} overflow="auto" spacing={0} alignItems={'stretch'}>
<Stack
bg="white"
flex={'1'}
maxWidth="720px"
maxHeight="100%"
spacing={0}
borderRight="1px solid #dcdcdd"
>
{fields}
</Stack>
<Stack bg="#F5F5F5" flex={'1'} maxHeight={'100%'} minWidth="850px">
{preview}
</Stack>
</Group>
</Stack>
);
}

View File

@@ -1,14 +1,13 @@
// @ts-nocheck // @ts-nocheck
import { useCallback, useRef } from 'react';
import { useFormikContext } from 'formik'; import { useFormikContext } from 'formik';
import { Button, Icon, Position } from '@blueprintjs/core'; import { Button, Icon, Position } from '@blueprintjs/core';
import { SelectOptionProps } from '@blueprintjs-formik/select'; import { SelectOptionProps } from '@blueprintjs-formik/select';
import { FormGroupProps, TextAreaProps } from '@blueprintjs-formik/core'; import { FormGroupProps, TextAreaProps } from '@blueprintjs-formik/core';
import { css } from '@emotion/css'; import { css } from '@emotion/css';
import { FFormGroup, FSelect, FTextArea, Group, Stack } from '@/components'; import { FFormGroup, FSelect, FTextArea, Group, Stack } from '@/components';
import { useCallback, useRef } from 'react';
import { InvoiceSendMailFormValues } from '../../Invoices/InvoiceSendMailDrawer/_types'; import { InvoiceSendMailFormValues } from '../../Invoices/InvoiceSendMailDrawer/_types';
interface SendMailViewMessageFieldProps { interface SendMailViewMessageFieldProps {
argsOptions?: Array<SelectOptionProps>; argsOptions?: Array<SelectOptionProps>;
formGroupProps?: Partial<FormGroupProps>; formGroupProps?: Partial<FormGroupProps>;

View File

@@ -0,0 +1,9 @@
export function SendMailViewPreview() {
return (
<h1>asdasd</h1>
)
}

View File

@@ -1,6 +1,5 @@
import { css } from '@emotion/css'; import { css } from '@emotion/css';
import { Tabs } from '@blueprintjs/core'; import { Tabs } from '@blueprintjs/core';
import { Stack } from '@/components';
interface SendMailViewPreviewTabsProps { interface SendMailViewPreviewTabsProps {
children: React.ReactNode; children: React.ReactNode;
@@ -10,11 +9,10 @@ export function SendMailViewPreviewTabs({
children, children,
}: SendMailViewPreviewTabsProps) { }: SendMailViewPreviewTabsProps) {
return ( return (
<Stack bg="#F5F5F5" flex={'1'} maxHeight={'100%'} minWidth="850px"> <Tabs
<Tabs id={'preview'}
id={'preview'} defaultSelectedTabId={'payment-page'}
defaultSelectedTabId={'payment-page'} className={css`
className={css`
overflow: hidden; overflow: hidden;
flex: 1 1; flex: 1 1;
display: flex; display: flex;
@@ -39,9 +37,8 @@ export function SendMailViewPreviewTabs({
overflow: auto; overflow: auto;
} }
`} `}
> >
{children} {children}
</Tabs> </Tabs>
</Stack>
); );
} }

View File

@@ -1,3 +1,4 @@
// @ts-nocheck
import { useMemo, useState } from 'react'; import { useMemo, useState } from 'react';
import { Button, MenuItem } from '@blueprintjs/core'; import { Button, MenuItem } from '@blueprintjs/core';
import { SelectOptionProps } from '@blueprintjs-formik/select'; import { SelectOptionProps } from '@blueprintjs-formik/select';

View File

@@ -5,20 +5,18 @@ import { InvoiceSendMailForm } from './InvoiceSendMailForm';
import { InvoiceSendMailPreview } from './InvoiceSendMailPreview'; import { InvoiceSendMailPreview } from './InvoiceSendMailPreview';
import { InvoiceSendMailFields } from './InvoiceSendMailFields'; import { InvoiceSendMailFields } from './InvoiceSendMailFields';
import { SendMailViewHeader } from '../../Estimates/SendMailViewDrawer/SendMailViewHeader'; import { SendMailViewHeader } from '../../Estimates/SendMailViewDrawer/SendMailViewHeader';
import { SendMailViewLayout } from '../../Estimates/SendMailViewDrawer/SendMailViewLayout';
export function InvoiceSendMailContent() { export function InvoiceSendMailContent() {
return ( return (
<Stack className={Classes.DRAWER_BODY}> <Stack className={Classes.DRAWER_BODY}>
<InvoiceSendMailBoot> <InvoiceSendMailBoot>
<InvoiceSendMailForm> <InvoiceSendMailForm>
<Stack spacing={0} flex={1} overflow="hidden"> <SendMailViewLayout
<SendMailViewHeader label={'Send Invoice Mail'} /> header={<SendMailViewHeader label={'Send Invoice Mail'} />}
fields={<InvoiceSendMailFields />}
<Group flex={1} overflow="auto" spacing={0} alignItems={'stretch'}> preview={<InvoiceSendMailPreview />}
<InvoiceSendMailFields /> />
<InvoiceSendMailPreview />
</Group>
</Stack>
</InvoiceSendMailForm> </InvoiceSendMailForm>
</InvoiceSendMailBoot> </InvoiceSendMailBoot>
</Stack> </Stack>

View File

@@ -1,34 +1,19 @@
// @ts-nocheck // @ts-nocheck
import { Button, Intent, MenuItem, Position } from '@blueprintjs/core'; import { Button, Intent } from '@blueprintjs/core';
import { useRef, useState, useMemo, useCallback } from 'react';
import { useFormikContext } from 'formik'; import { useFormikContext } from 'formik';
import { SelectOptionProps } from '@blueprintjs-formik/select'; import { FCheckbox, FFormGroup, FInputGroup, Group, Stack } from '@/components';
import { css } from '@emotion/css';
import {
FCheckbox,
FFormGroup,
FInputGroup,
Group,
Stack,
} from '@/components';
import { useDrawerContext } from '@/components/Drawer/DrawerProvider'; import { useDrawerContext } from '@/components/Drawer/DrawerProvider';
import { useDrawerActions } from '@/hooks/state'; import { useDrawerActions } from '@/hooks/state';
import { useInvoiceMailItems, } from './_hooks'; import { useInvoiceMailItems, useSendInvoiceFormatArgsOptions } from './_hooks';
import { SendMailViewToAddressField } from '../../Estimates/SendMailViewDrawer/SendMailViewToAddressField'; import { SendMailViewToAddressField } from '../../Estimates/SendMailViewDrawer/SendMailViewToAddressField';
import { SendMailViewMessageField } from '../../Estimates/SendMailViewDrawer/SendMailViewMessageField'; import { SendMailViewMessageField } from '../../Estimates/SendMailViewDrawer/SendMailViewMessageField';
export function InvoiceSendMailFields() { export function InvoiceSendMailFields() {
const items = useInvoiceMailItems(); const items = useInvoiceMailItems();
const argsOptions = useSendInvoiceFormatArgsOptions();
return ( return (
<Stack <Stack>
bg="white"
flex={'1'}
maxWidth="720px"
maxHeight="100%"
spacing={0}
borderRight="1px solid #dcdcdd"
>
<Stack spacing={0} overflow="auto" flex="1" p={'30px'}> <Stack spacing={0} overflow="auto" flex="1" p={'30px'}>
<SendMailViewToAddressField <SendMailViewToAddressField
toMultiSelectProps={{ items }} toMultiSelectProps={{ items }}
@@ -39,7 +24,7 @@ export function InvoiceSendMailFields() {
<FInputGroup name={'subject'} large fastField /> <FInputGroup name={'subject'} large fastField />
</FFormGroup> </FFormGroup>
<SendMailViewMessageField /> <SendMailViewMessageField argsOptions={argsOptions} />
<Group> <Group>
<FCheckbox name={'attachPdf'} label={'Attach PDF'} /> <FCheckbox name={'attachPdf'} label={'Attach PDF'} />

View File

@@ -241,7 +241,7 @@ export function useEstimateSMSDetail(estimateId, props, requestProps) {
); );
} }
export function useSendSaleEstimateMail(props) { export function useSendSaleEstimateMail(props = {}) {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const apiRequest = useApiRequest(); const apiRequest = useApiRequest();
@@ -257,17 +257,18 @@ export function useSendSaleEstimateMail(props) {
); );
} }
export function useSaleEstimateDefaultOptions(estimateId, props) { /**
return useRequestQuery( *
[t.SALE_ESTIMATE_MAIL_OPTIONS, estimateId], * @param {number} estimateId
{ * @param props
method: 'get', * @returns
url: `sales/estimates/${estimateId}/mail`, */
}, export function useSaleEstimateMailState(estimateId: number, props?= {}) {
{ const apiRequest = useApiRequest();
select: (res) => res.data.data, return useQuery([t.SALE_ESTIMATE_MAIL_OPTIONS, estimateId], () =>
...props, apiRequest
}, .get(`sales/estimates/${estimateId}/mail/state`)
.then((res) => transformToCamelCase(res.data.data)),
); );
} }