feat: wip preview invoice payment mail

This commit is contained in:
Ahmed Bouhuolia
2024-10-29 21:14:46 +02:00
parent 12189f018d
commit e10c530b4b
13 changed files with 301 additions and 81 deletions

View File

@@ -1,11 +1,15 @@
import { Box } from '@/components';
import { Box, Group, Stack } from '@/components';
import { InvoiceMailReceiptPreview } from '../InvoiceCustomize/InvoiceMailReceiptPreview';
import { css } from '@emotion/css';
import { useInvoiceSendMailBoot } from './InvoiceSendMailContentBoot';
import { useMemo } from 'react';
import { x } from '@xstyled/emotion';
import { InvoiceSendMailPreviewWithHeader } from './InvoiceSendMailHeaderPreview';
import { useSendInvoiceMailMessage } from './_hooks';
export function InvoiceMailReceiptPreviewConneceted() {
const { invoice } = useInvoiceSendMailBoot();
const mailMessage = useSendInvoiceMailMessage();
const items = useMemo(
() =>
@@ -18,16 +22,19 @@ export function InvoiceMailReceiptPreviewConneceted() {
);
return (
<Box px={4} pt={8} pb={16}>
<InvoiceMailReceiptPreview
total={invoice.total_formatted}
dueDate={invoice.due_date_formatted}
invoiceNumber={invoice.invoice_no}
items={items}
className={css`
margin: 0 auto;
`}
/>
</Box>
<InvoiceSendMailPreviewWithHeader>
<Box px={4} pt={8} pb={16}>
<InvoiceMailReceiptPreview
total={invoice.total_formatted}
dueDate={invoice.due_date_formatted}
invoiceNumber={invoice.invoice_no}
items={items}
message={mailMessage}
className={css`
margin: 0 auto;
`}
/>
</Box>
</InvoiceSendMailPreviewWithHeader>
);
}

View File

@@ -1,13 +1,20 @@
// @ts-nocheck
import React, { createContext, useContext } from 'react';
import { Spinner } from '@blueprintjs/core';
import { useInvoice } from '@/hooks/query';
import {
GetSaleInvoiceDefaultOptionsResponse,
useInvoice,
useSaleInvoiceDefaultOptions,
} from '@/hooks/query';
import { useDrawerContext } from '@/components/Drawer/DrawerProvider';
interface InvoiceSendMailBootValues {
invoice: any;
invoiceId: number;
isInvoiceLoading: boolean;
invoiceMailOptions: GetSaleInvoiceDefaultOptionsResponse | undefined;
isInvoiceMailOptionsLoading: boolean;
}
interface InvoiceSendMailBootProps {
children: React.ReactNode;
@@ -21,10 +28,15 @@ export const InvoiceSendMailBoot = ({ children }: InvoiceSendMailBootProps) => {
payload: { invoiceId },
} = useDrawerContext();
// Invoice details.
const { data: invoice, isLoading: isInvoiceLoading } = useInvoice(invoiceId, {
enabled: !!invoiceId,
});
const isLoading = isInvoiceLoading;
// Invoice mail options.
const { data: invoiceMailOptions, isLoading: isInvoiceMailOptionsLoading } =
useSaleInvoiceDefaultOptions(invoiceId);
const isLoading = isInvoiceLoading || isInvoiceMailOptionsLoading;
if (isLoading) {
return <Spinner size={20} />;
@@ -33,6 +45,8 @@ export const InvoiceSendMailBoot = ({ children }: InvoiceSendMailBootProps) => {
invoice,
isInvoiceLoading,
invoiceId,
invoiceMailOptions,
isInvoiceMailOptionsLoading,
};
return (

View File

@@ -1,6 +1,6 @@
// @ts-nocheck
import { useState } from 'react';
import { Button, Intent, MenuItem } from '@blueprintjs/core';
import { useRef, useState } from 'react';
import { Button, Intent, MenuItem, Position } from '@blueprintjs/core';
import { useFormikContext } from 'formik';
import { css } from '@emotion/css';
import { x } from '@xstyled/emotion';
@@ -9,8 +9,10 @@ import {
FFormGroup,
FInputGroup,
FMultiSelect,
FSelect,
FTextArea,
Group,
Icon,
Stack,
} from '@/components';
import { useDrawerContext } from '@/components/Drawer/DrawerProvider';
@@ -60,6 +62,7 @@ const fieldsWrapStyle = css`
export function InvoiceSendMailFields() {
const [showCCField, setShowCCField] = useState<boolean>(false);
const [showBccField, setShowBccField] = useState<boolean>(false);
const textareaRef = useRef(null);
const { values, setFieldValue } = useFormikContext();
const items = useInvoiceMailItems();
@@ -117,6 +120,30 @@ export function InvoiceSendMailFields() {
</Group>
);
const handleTextareaChange = () => {
const textarea = textareaRef.current;
if (!textarea) return;
const { selectionStart, selectionEnd } = textarea;
const insertText = '{Variable}';
// Insert the text at the cursor position
const message =
text.substring(0, selectionStart) +
insertText +
text.substring(selectionEnd);
setFieldValue('message', message);
// Move the cursor to the end of the inserted text
setTimeout(() => {
textarea.selectionStart = textarea.selectionEnd =
selectionStart + insertText.length;
textarea.focus();
}, 0);
};
return (
<Stack
bg="white"
@@ -144,6 +171,7 @@ export function InvoiceSendMailFields() {
resetOnQuery
resetOnSelect
fill
fastField
/>
{showCCField && (
<FMultiSelect
@@ -161,6 +189,7 @@ export function InvoiceSendMailFields() {
resetOnQuery
resetOnSelect
fill
fastField
/>
)}
{showBccField && (
@@ -179,25 +208,62 @@ export function InvoiceSendMailFields() {
resetOnQuery
resetOnSelect
fill
fastField
/>
)}
</Stack>
</FFormGroup>
<FFormGroup label={'Submit'} name={'subject'}>
<FInputGroup name={'subject'} large />
<FInputGroup name={'subject'} large fastField />
</FFormGroup>
<FFormGroup label={'Message'} name={'message'}>
<FTextArea
name={'message'}
large
fill
className={css`
resize: vertical;
min-height: 200px;
`}
/>
<Stack spacing={0}>
<Group
border={'1px solid #ced4da'}
borderBottom={0}
borderRadius={'3px 3px 0 0'}
>
<FSelect
selectedItem={'customerName'}
name={'item'}
items={[{ value: 'customerName', text: 'Customer Name' }]}
onItemChange={handleTextareaChange}
popoverProps={{
fill: false,
position: Position.BOTTOM_LEFT,
minimal: true,
}}
input={({ activeItem, text, label, value }) => (
<Button
minimal
rightIcon={
<Icon icon={'caret-down-16'} color={'#8F99A8'} />
}
>
{text}
</Button>
)}
fill={false}
fastField
/>
</Group>
<FTextArea
ref={textareaRef}
name={'message'}
large
fill
fastField
className={css`
resize: vertical;
min-height: 200px;
border-top-right-radius: 0px;
border-top-left-radius: 0px;
`}
/>
</Stack>
</FFormGroup>
</Stack>
@@ -243,4 +309,3 @@ function InvoiceSendMailFooter() {
</Group>
);
}

View File

@@ -1,4 +1,3 @@
import * as Yup from 'yup';
import { Form, Formik, FormikHelpers } from 'formik';
import { css } from '@emotion/css';
import { Intent } from '@blueprintjs/core';
@@ -9,22 +8,14 @@ import { AppToaster } from '@/components';
import { useInvoiceSendMailBoot } from './InvoiceSendMailContentBoot';
import { useDrawerActions } from '@/hooks/state';
import { useDrawerContext } from '@/components/Drawer/DrawerProvider';
import { transformToForm } from '@/utils';
const initialValues = {
subject: 'invoice INV-0002 for AED 0.00',
message: `Hi Ahmed,
Heres invoice INV-0002 for AED 0.00
The amount outstanding of AED $100,00 is due on 2 October 2024
View your bill online From your online you can print a PDF or pay your outstanding bills,
If you have any questions, please let us know,
Thanks,
Mohamed`,
to: ['a.bouhuolia@gmail.com'],
subject: '',
message: '',
to: [],
cc: [],
bcc: [],
};
interface InvoiceSendMailFormProps {
@@ -33,10 +24,14 @@ interface InvoiceSendMailFormProps {
export function InvoiceSendMailForm({ children }: InvoiceSendMailFormProps) {
const { mutateAsync: sendInvoiceMail } = useSendSaleInvoiceMail();
const { invoiceId } = useInvoiceSendMailBoot();
const { invoiceId, invoiceMailOptions } = useInvoiceSendMailBoot();
const { name } = useDrawerContext();
const { closeDrawer } = useDrawerActions();
const _initialValues = {
...initialValues,
...transformToForm(invoiceMailOptions, initialValues),
};
const handleSubmit = (
values: InvoiceSendMailFormValues,
{ setSubmitting }: FormikHelpers<InvoiceSendMailFormValues>,
@@ -62,7 +57,7 @@ export function InvoiceSendMailForm({ children }: InvoiceSendMailFormProps) {
return (
<Formik
initialValues={initialValues}
initialValues={_initialValues}
validationSchema={InvoiceSendMailFormSchema}
onSubmit={handleSubmit}
>

View File

@@ -0,0 +1,75 @@
import { css } from '@emotion/css';
import { x } from '@xstyled/emotion';
import { Box, Group, Stack } from '@/components';
import React from 'react';
import { useSendInvoiceMailSubject } from './_hooks';
export function InvoiceSendMailHeaderPreview() {
const mailSubject = useSendInvoiceMailSubject();
return (
<Stack
bg={'white'}
borderBottom={'1px solid #dcdcdd'}
padding={'22px 30px'}
spacing={8}
position={'sticky'}
top={0}
zIndex={1}
>
<Box>
<x.h2 fontWeight={600} fontSize={16}>
{mailSubject}
</x.h2>
</Box>
<Group display="flex" gap={2}>
<Group display="flex" alignItems="center" gap={15}>
<x.abbr
role="presentation"
className={css`
background-color: #daa3e4;
color: #3f1946;
fill: #daa3e4;
height: 40px;
width: 40px;
line-height: 40px;
text-align: center;
border-radius: 40px;
font-size: 14px;
`}
>
A
</x.abbr>
<Stack spacing={2}>
<Group spacing={2}>
<Box fontWeight={600}>Ahmed </Box>
<Box color={'#738091'}>
&lt;messaging-service@post.xero.com&gt;
</Box>
</Group>
<Box fontSize={'sm'} color={'#738091'}>
Reply to: Ahmed &lt;a.m.bouhuolia@gmail.com&gt;
</Box>
</Stack>
</Group>
</Group>
</Stack>
);
}
export function InvoiceSendMailPreviewWithHeader({
children,
}: {
children: React.ReactNode;
}) {
return (
<Box>
<InvoiceSendMailHeaderPreview />
<Box>{children}</Box>
</Box>
);
}

View File

@@ -1,27 +1,30 @@
import { Box } from "@/components";
import { InvoicePaperTemplate } from "../InvoiceCustomize/InvoicePaperTemplate";
import { css } from "@emotion/css";
import { useInvoiceSendMailBoot } from "./InvoiceSendMailContentBoot";
import { Box } from '@/components';
import { InvoicePaperTemplate } from '../InvoiceCustomize/InvoicePaperTemplate';
import { css } from '@emotion/css';
import { useInvoiceSendMailBoot } from './InvoiceSendMailContentBoot';
import { InvoiceSendMailPreviewWithHeader } from './InvoiceSendMailHeaderPreview';
export function InvoiceSendPdfPreviewConnected() {
const { invoice } = useInvoiceSendMailBoot();
return (
<Box px={4} py={6}>
<InvoicePaperTemplate
dueDate={invoice.due_date_formatted}
dateIssue={invoice.invoice_date_formatted}
invoiceNumber={invoice.invoice_no}
total={invoice.total_formatted}
subtotal={invoice.subtotal}
discount={''}
paymentMade={''}
balanceDue={invoice.due_amount_Formatted}
statement={invoice.statement}
className={css`
margin: 0 auto;
`}
/>
</Box>
<InvoiceSendMailPreviewWithHeader>
<Box px={4} py={6}>
<InvoicePaperTemplate
dueDate={invoice.due_date_formatted}
dateIssue={invoice.invoice_date_formatted}
invoiceNumber={invoice.invoice_no}
total={invoice.total_formatted}
subtotal={invoice.subtotal}
discount={''}
paymentMade={''}
balanceDue={invoice.due_amount_Formatted}
statement={invoice.statement}
className={css`
margin: 0 auto;
`}
/>
</Box>
</InvoiceSendMailPreviewWithHeader>
);
}

View File

@@ -1,6 +1,8 @@
import { useFormikContext } from 'formik';
import { chain } from 'lodash';
import { camelCase, chain, defaultTo, mapKeys, upperFirst } from 'lodash';
import { InvoiceSendMailFormValues } from './_types';
import { useInvoiceSendMailBoot } from './InvoiceSendMailContentBoot';
import { useMemo } from 'react';
export const useInvoiceMailItems = () => {
const { values } = useFormikContext<InvoiceSendMailFormValues>();
@@ -16,3 +18,42 @@ export const useInvoiceMailItems = () => {
}))
.value();
};
export const useSendInvoiceMailFormatArgs = () => {
const { invoiceMailOptions } = useInvoiceSendMailBoot();
return useMemo(() => {
return mapKeys(invoiceMailOptions?.formatArgs, (_, key) =>
upperFirst(camelCase(key)),
);
}, [invoiceMailOptions]);
};
export const useSendInvoiceMailSubject = () => {
const { values } = useFormikContext<InvoiceSendMailFormValues>();
const formatArgs = useSendInvoiceMailFormatArgs();
return formatSmsMessage(values?.subject, formatArgs);
};
export const useSendInvoiceMailMessage = () => {
const { values } = useFormikContext<InvoiceSendMailFormValues>();
const formatArgs = useSendInvoiceMailFormatArgs();
return formatSmsMessage(values?.message, formatArgs);
};
export const formatSmsMessage = (
message: string,
args: Record<string, any>,
) => {
let formattedMessage = message;
Object.keys(args).forEach((key) => {
const variable = `{${key}}`;
const value = defaultTo(args[key], '');
formattedMessage = formattedMessage.replace(variable, value);
});
return formattedMessage;
};

View File

@@ -2,6 +2,6 @@ export interface InvoiceSendMailFormValues {
subject: string;
message: string;
to: string[];
cc?: string[];
bcc?: string[];
cc: string[];
bcc: string[];
}