feat: estimate, receipt, credit note mail preview

This commit is contained in:
Ahmed Bouhuolia
2024-11-17 15:45:55 +02:00
parent d115ebde12
commit 53ab40a075
37 changed files with 1531 additions and 396 deletions

View File

@@ -411,9 +411,8 @@ export default class PaymentReceivesController extends BaseController {
const { tenantId } = req;
try {
const data = await this.paymentReceiveApplication.getPaymentReceivedState(
tenantId
);
const data =
await this.paymentReceiveApplication.getPaymentReceivedState(tenantId);
return res.status(200).send({ data });
} catch (error) {
next(error);
@@ -471,7 +470,7 @@ export default class PaymentReceivesController extends BaseController {
ACCEPT_TYPE.APPLICATION_JSON,
ACCEPT_TYPE.APPLICATION_PDF,
]);
// Response in pdf format.
// Responses pdf format.
if (ACCEPT_TYPE.APPLICATION_PDF === acceptType) {
const [pdfContent, filename] =
await this.paymentReceiveApplication.getPaymentReceivePdf(
@@ -484,7 +483,14 @@ export default class PaymentReceivesController extends BaseController {
'Content-Disposition': `attachment; filename="${filename}"`,
});
res.send(pdfContent);
// Response in json format.
// Responses html format.
} else if (ACCEPT_TYPE.APPLICATION_TEXT_HTML === acceptType) {
const htmlContent = this.paymentReceiveApplication.getPaymentReceivedHtml(
tenantId,
paymentReceiveId
);
return res.status(200).send({ htmlContent });
// Responses json format.
} else {
const paymentReceive =
await this.paymentReceiveApplication.getPaymentReceive(

View File

@@ -13,11 +13,8 @@ import DynamicListingService from '@/services/DynamicListing/DynamicListService'
import { ServiceError } from '@/exceptions';
import CheckPolicies from '@/api/middleware/CheckPolicies';
import { SaleEstimatesApplication } from '@/services/Sales/Estimates/SaleEstimatesApplication';
import { ACCEPT_TYPE } from '@/interfaces/Http';
const ACCEPT_TYPE = {
APPLICATION_PDF: 'application/pdf',
APPLICATION_JSON: 'application/json',
};
@Service()
export default class SalesEstimatesController extends BaseController {
@Inject()
@@ -395,6 +392,7 @@ export default class SalesEstimatesController extends BaseController {
const acceptType = accept.types([
ACCEPT_TYPE.APPLICATION_JSON,
ACCEPT_TYPE.APPLICATION_PDF,
ACCEPT_TYPE.APPLICATION_TEXT_HTML,
]);
// Retrieves estimate in pdf format.
if (ACCEPT_TYPE.APPLICATION_PDF == acceptType) {
@@ -410,7 +408,14 @@ export default class SalesEstimatesController extends BaseController {
});
res.send(pdfContent);
// Retrieves estimates in json format.
} else {
} else if (ACCEPT_TYPE.APPLICATION_TEXT_HTML === acceptType) {
const htmlContent =
await this.saleEstimatesApplication.getSaleEstimateHtml(
tenantId,
estimateId
);
return res.status(200).send({ htmlContent });
} else if (ACCEPT_TYPE.APPLICATION_JSON) {
const estimate = await this.saleEstimatesApplication.getSaleEstimate(
tenantId,
estimateId

View File

@@ -353,6 +353,7 @@ export default class SalesReceiptsController extends BaseController {
const acceptType = accept.types([
ACCEPT_TYPE.APPLICATION_JSON,
ACCEPT_TYPE.APPLICATION_PDF,
ACCEPT_TYPE.APPLICATION_TEXT_HTML,
]);
// Retrieves receipt in pdf format.
if (ACCEPT_TYPE.APPLICATION_PDF == acceptType) {
@@ -368,6 +369,12 @@ export default class SalesReceiptsController extends BaseController {
});
res.send(pdfContent);
// Retrieves receipt in json format.
} else if (ACCEPT_TYPE.APPLICATION_TEXT_HTML === acceptType) {
const htmlContent = await this.saleReceiptsApplication.getSaleReceiptHtml(
tenantId,
saleReceiptId
);
res.send({ htmlContent });
} else {
const saleReceipt = await this.saleReceiptsApplication.getSaleReceipt(
tenantId,

View File

@@ -220,6 +220,18 @@ export class SaleEstimatesApplication {
);
}
/**
* Retrieve the HTML content of the given sale estimate.
* @param {number} tenantId
* @param {number} saleEstimateId
*/
public getSaleEstimateHtml(tenantId: number, saleEstimateId: number) {
return this.saleEstimatesPdfService.saleEstimateHtml(
tenantId,
saleEstimateId
);
}
/**
* Send the reminder mail of the given sale estimate.
* @param {number} tenantId

View File

@@ -8,6 +8,7 @@ import { transformEstimateToPdfTemplate } from './utils';
import { EstimatePdfBrandingAttributes } from './constants';
import events from '@/subscribers/events';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import { renderEstimatePaperTemplateHtml } from '@bigcapital/pdf-templates';
@Service()
export class SaleEstimatesPdf {
@@ -29,6 +30,22 @@ export class SaleEstimatesPdf {
@Inject()
private eventPublisher: EventPublisher;
/**
* Retrieve sale estimate html content.
* @param {number} tenantId -
* @param {number} invoiceId -
*/
public async saleEstimateHtml(
tenantId: number,
estimateId: number
): Promise<string> {
const brandingAttributes = await this.getEstimateBrandingAttributes(
tenantId,
estimateId
);
return renderEstimatePaperTemplateHtml({ ...brandingAttributes });
}
/**
* Retrieve sale invoice pdf content.
* @param {number} tenantId -
@@ -42,15 +59,9 @@ export class SaleEstimatesPdf {
tenantId,
saleEstimateId
);
const brandingAttributes = await this.getEstimateBrandingAttributes(
tenantId,
saleEstimateId
);
const htmlContent = await this.templateInjectable.render(
tenantId,
'modules/estimate-regular',
brandingAttributes
);
// Retireves the sale estimate html.
const htmlContent = await this.saleEstimateHtml(tenantId, saleEstimateId);
const content = await this.chromiumlyTenancy.convertHtmlContent(
tenantId,
htmlContent

View File

@@ -1,13 +1,13 @@
import { Inject, Service } from 'typedi';
import { renderPaymentReceivedPaperTemplateHtml } from '@bigcapital/pdf-templates';
import { ChromiumlyTenancy } from '@/services/ChromiumlyTenancy/ChromiumlyTenancy';
import { TemplateInjectable } from '@/services/TemplateInjectable/TemplateInjectable';
import { GetPaymentReceived } from './GetPaymentReceived';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { PaymentReceivedBrandingTemplate } from './PaymentReceivedBrandingTemplate';
import { transformPaymentReceivedToPdfTemplate } from './utils';
import { PaymentReceivedPdfTemplateAttributes } from '@/interfaces';
import events from '@/subscribers/events';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import events from '@/subscribers/events';
@Service()
export default class GetPaymentReceivedPdf {
@@ -17,9 +17,6 @@ export default class GetPaymentReceivedPdf {
@Inject()
private chromiumlyTenancy: ChromiumlyTenancy;
@Inject()
private templateInjectable: TemplateInjectable;
@Inject()
private getPaymentService: GetPaymentReceived;
@@ -29,6 +26,23 @@ export default class GetPaymentReceivedPdf {
@Inject()
private eventPublisher: EventPublisher;
/**
* Retrieves payment received html content.
* @param {number} tenantId
* @param {number} paymentReceivedId
* @returns {Promise<string>}
*/
public async getPaymentReceivedHtml(
tenantId: number,
paymentReceivedId: number
): Promise<string> {
const brandingAttributes = await this.getPaymentBrandingAttributes(
tenantId,
paymentReceivedId
);
return renderPaymentReceivedPaperTemplateHtml(brandingAttributes);
}
/**
* Retrieve sale invoice pdf content.
* @param {number} tenantId -
@@ -39,15 +53,10 @@ export default class GetPaymentReceivedPdf {
tenantId: number,
paymentReceivedId: number
): Promise<[Buffer, string]> {
const brandingAttributes = await this.getPaymentBrandingAttributes(
const htmlContent = await this.getPaymentReceivedHtml(
tenantId,
paymentReceivedId
);
const htmlContent = await this.templateInjectable.render(
tenantId,
'modules/payment-receive-standard',
brandingAttributes
);
const filename = await this.getPaymentReceivedFilename(
tenantId,
paymentReceivedId

View File

@@ -228,6 +228,22 @@ export class PaymentReceivesApplication {
);
};
/**
* Retrieves the given payment receive html document.
* @param {number} tenantId
* @param {number} paymentReceiveId
* @returns {Promise<string>}
*/
public getPaymentReceivedHtml = (
tenantId: number,
paymentReceiveId: number
) => {
return this.getPaymentReceivePdfService.getPaymentReceivedHtml(
tenantId,
paymentReceiveId
);
};
/**
* Retrieves the create/edit initial state of the payment received.
* @param {number} tenantId - The ID of the tenant.

View File

@@ -152,6 +152,19 @@ export class SaleReceiptApplication {
);
}
/**
* Retrieves the given sale receipt html.
* @param {number} tenantId
* @param {number} saleReceiptId
* @returns {Promise<string>}
*/
public getSaleReceiptHtml(tenantId: number, saleReceiptId: number) {
return this.getSaleReceiptPdfService.saleReceiptHtml(
tenantId,
saleReceiptId
);
}
/**
* Notify receipt customer by SMS of the given sale receipt.
* @param {number} tenantId

View File

@@ -8,6 +8,7 @@ import { transformReceiptToBrandingTemplateAttributes } from './utils';
import { ISaleReceiptBrandingTemplateAttributes } from '@/interfaces';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import events from '@/subscribers/events';
import { renderReceiptPaperTemplateHtml } from '@bigcapital/pdf-templates';
@Service()
export class SaleReceiptsPdf {
@@ -29,6 +30,19 @@ export class SaleReceiptsPdf {
@Inject()
private eventPublisher: EventPublisher;
/**
* Retrieves sale receipt html content.
* @param {number} tennatId
* @param {number} saleReceiptId
*/
public async saleReceiptHtml(tennatId: number, saleReceiptId: number) {
const brandingAttributes = await this.getReceiptBrandingAttributes(
tennatId,
saleReceiptId
);
return renderReceiptPaperTemplateHtml(brandingAttributes);
}
/**
* Retrieves sale invoice pdf content.
* @param {number} tenantId -
@@ -41,16 +55,9 @@ export class SaleReceiptsPdf {
): Promise<[Buffer, string]> {
const filename = await this.getSaleReceiptFilename(tenantId, saleReceiptId);
const brandingAttributes = await this.getReceiptBrandingAttributes(
tenantId,
saleReceiptId
);
// Converts the receipt template to html content.
const htmlContent = await this.templateInjectable.render(
tenantId,
'modules/receipt-regular',
brandingAttributes
);
const htmlContent = await this.saleReceiptHtml(tenantId, saleReceiptId);
// Renders the html content to pdf document.
const content = await this.chromiumlyTenancy.convertHtmlContent(
tenantId,

View File

@@ -0,0 +1,3 @@
import * as Yup from 'yup';
export const EstimateSendMailSchema = Yup.object().shape({});

View File

@@ -0,0 +1,52 @@
import React, { createContext, useContext } from 'react';
import { Spinner } from '@blueprintjs/core';
import { useDrawerContext } from '@/components/Drawer/DrawerProvider';
interface EstimateSendMailBootValues {
estimateId: number;
estimateMailState: GetSaleEstimateDefaultOptionsResponse | undefined;
isEstimateMailState: boolean;
}
interface EstimateSendMailBootProps {
children: React.ReactNode;
}
const EstimateSendMailContentBootContext =
createContext<EstimateSendMailBootValues>({} as EstimateSendMailBootValues);
export const EstimateSendMailBoot = ({ children }: EstimateSendMailBootProps) => {
const {
payload: { estimateId },
} = useDrawerContext();
// Estimate mail options.
const { data: estimateMailState, isLoading: isEstimateMailState } =
useSaleEstimateMailState(estimateId);
const isLoading = isEstimateMailState;
if (isLoading) {
return <Spinner size={20} />;
}
const value = {
estimateId,
// # Estimate mail options
isEstimateMailState,
estimateMailState,
};
return (
<EstimateSendMailContentBootContext.Provider value={value}>
{children}
</EstimateSendMailContentBootContext.Provider>
);
};
EstimateSendMailBoot.displayName = 'EstimateSendMailBoot';
export const useEstimateSendMailBoot = () => {
return useContext<EstimateSendMailBootValues>(
EstimateSendMailContentBootContext,
);
};

View File

@@ -0,0 +1,9 @@
export function EstimateSendMailContent() {
return (
);
}

View File

@@ -0,0 +1,42 @@
// @ts-nocheck
import React from 'react';
import * as R from 'ramda';
import { Drawer, DrawerSuspense } from '@/components';
import withDrawers from '@/containers/Drawer/withDrawers';
const EstimateSendMailDrawerProps = React.lazy(() =>
import('./EstimateSendMailContent').then((module) => ({
default: module.InvoiceSendMailContent,
})),
);
interface EstimateSendMailDrawerProps {
name: string;
isOpen?: boolean;
payload?: any;
}
function EstimateSendMailDrawerRoot({
name,
// #withDrawer
isOpen,
payload,
}: EstimateSendMailDrawerProps) {
return (
<Drawer
isOpen={isOpen}
name={name}
payload={payload}
size={'calc(100% - 10px)'}
>
<DrawerSuspense>
<EstimateSendMailDrawerProps />
</DrawerSuspense>
</Drawer>
);
}
export const EstimateSendMailDrawer = R.compose(withDrawers())(
EstimateSendMailDrawerRoot,
);

View File

@@ -0,0 +1,79 @@
import { Form, Formik, FormikHelpers } from 'formik';
import { css } from '@emotion/css';
import { Intent } from '@blueprintjs/core';
import { InvoiceSendMailFormValues } from './_types';
import { InvoiceSendMailFormSchema } from './InvoiceSendMailForm.schema';
import { useSendSaleInvoiceMail } from '@/hooks/query';
import { AppToaster } from '@/components';
import { useInvoiceSendMailBoot } from './InvoiceSendMailContentBoot';
import { useDrawerActions } from '@/hooks/state';
import { useDrawerContext } from '@/components/Drawer/DrawerProvider';
import { transformToForm } from '@/utils';
import { useEstimateSendMailBoot } from './EstimateSendMailBoot';
const initialValues: InvoiceSendMailFormValues = {
subject: '',
message: '',
to: [],
cc: [],
bcc: [],
attachPdf: true,
};
interface InvoiceSendMailFormProps {
children: React.ReactNode;
}
export function EstimateSendMailForm({ children }: InvoiceSendMailFormProps) {
const { mutateAsync: sendInvoiceMail } = useSendSaleInvoiceMail();
const { estimateId, estimateMailState } = useEstimateSendMailBoot();
const { name } = useDrawerContext();
const { closeDrawer } = useDrawerActions();
const _initialValues: InvoiceSendMailFormValues = {
...initialValues,
...transformToForm(invoiceMailState, initialValues),
};
const handleSubmit = (
values: InvoiceSendMailFormValues,
{ setSubmitting }: FormikHelpers<InvoiceSendMailFormValues>,
) => {
setSubmitting(true);
sendInvoiceMail({ id: invoiceId, values: { ...values } })
.then(() => {
AppToaster.show({
message: 'The invoice mail has been sent to the customer.',
intent: Intent.SUCCESS,
});
setSubmitting(false);
closeDrawer(name);
})
.catch((error) => {
setSubmitting(false);
AppToaster.show({
message: 'Something went wrong!',
intent: Intent.SUCCESS,
});
});
};
return (
<Formik
initialValues={_initialValues}
validationSchema={InvoiceSendMailFormSchema}
onSubmit={handleSubmit}
>
<Form
className={css`
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
`}
>
{children}
</Form>
</Formik>
);
}

View File

@@ -0,0 +1 @@
export interface EstimateSendMailFormValues {}

View File

@@ -18,20 +18,8 @@ const EstimateRejectAlert = React.lazy(
* Estimates alert.
*/
export default [
{
name: 'estimate-delete',
component: EstimateDeleteAlert,
},
{
name: 'estimate-deliver',
component: EstimateDeliveredAlert,
},
{
name: 'estimate-Approve',
component: EstimateApproveAlert,
},
{
name: 'estimate-reject',
component: EstimateRejectAlert,
},
{ name: 'estimate-delete', component: EstimateDeleteAlert },
{ name: 'estimate-deliver', component: EstimateDeliveredAlert },
{ name: 'estimate-Approve', component: EstimateApproveAlert },
{ name: 'estimate-reject', component: EstimateRejectAlert },
];

View File

@@ -4,16 +4,16 @@ import { Group, Icon } from '@/components';
import { useDrawerContext } from '@/components/Drawer/DrawerProvider';
import { useDrawerActions } from '@/hooks/state';
interface ElementCustomizeHeaderProps {
interface SendMailViewHeaderProps {
label?: string;
children?: React.ReactNode;
closeButton?: boolean;
}
export function InvoiceSendMailHeader({
export function SendMailViewHeader({
label,
closeButton = true,
}: ElementCustomizeHeaderProps) {
}: SendMailViewHeaderProps) {
const { name } = useDrawerContext();
const { closeDrawer } = useDrawerActions();

View File

@@ -0,0 +1,109 @@
// @ts-nocheck
import { useFormikContext } from 'formik';
import { Button, Icon, Position } from '@blueprintjs/core';
import { SelectOptionProps } from '@blueprintjs-formik/select';
import { FormGroupProps, TextAreaProps } from '@blueprintjs-formik/core';
import { css } from '@emotion/css';
import { FFormGroup, FSelect, FTextArea, Group, Stack } from '@/components';
import { useCallback, useRef } from 'react';
import { InvoiceSendMailFormValues } from '../../Invoices/InvoiceSendMailDrawer/_types';
interface SendMailViewMessageFieldProps {
argsOptions?: Array<SelectOptionProps>;
formGroupProps?: Partial<FormGroupProps>;
selectProps?: Partial<any>;
textareaProps?: Partial<TextAreaProps>;
}
export function SendMailViewMessageField({
argsOptions,
formGroupProps,
textareaProps,
}: SendMailViewMessageFieldProps) {
const textareaRef = useRef<HTMLTextAreaElement>(null);
const { setFieldValue } = useFormikContext<InvoiceSendMailFormValues>();
const handleTextareaChange = useCallback(
(item: SelectOptionProps) => {
const textarea = textareaRef.current;
if (!textarea) return;
const { selectionStart, selectionEnd, value: text } = textarea;
const insertText = `{${item.value}}`;
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);
},
[setFieldValue],
);
const handleTagInputKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
// Prevent the form from submitting when the user presses the Enter key
if (e.key === 'Enter') {
e.preventDefault();
}
};
return (
<FFormGroup label={'Message'} name={'message'} {...formGroupProps}>
<Stack spacing={0}>
<Group
border={'1px solid #ced4da'}
borderBottom={0}
borderRadius={'3px 3px 0 0'}
>
<FSelect
selectedItem={'customerName'}
name={'item'}
items={argsOptions}
onItemChange={handleTextareaChange}
popoverProps={{
fill: false,
position: Position.BOTTOM_LEFT,
minimal: true,
inputProps: {
onKeyDown: handleTagInputKeyDown,
},
}}
input={() => (
<Button
minimal
rightIcon={<Icon icon={'caret-down-16'} color={'#8F99A8'} />}
>
Insert Variable
</Button>
)}
fill={false}
fastField
/>
</Group>
<FTextArea
inputRef={textareaRef}
name={'message'}
large
fill
fastField
className={css`
resize: vertical;
min-height: 300px;
border-top-right-radius: 0px;
border-top-left-radius: 0px;
`}
{...textareaProps}
/>
</Stack>
</FFormGroup>
);
}

View File

@@ -0,0 +1,76 @@
import { useMemo } from 'react';
import { x } from '@xstyled/emotion';
import { Box, Group, Stack } from '@/components';
interface SendViewPreviewHeaderProps {
companyName?: string;
customerName?: string;
subject: string;
from?: Array<string>;
to?: Array<string>;
}
export function SendViewPreviewHeader({
companyName,
subject,
customerName,
from,
to,
}: SendViewPreviewHeaderProps) {
const formatedFromAddresses = useMemo(
() => formatAddresses(from || []),
[from],
);
const formattedToAddresses = useMemo(() => formatAddresses(to || []), [to]);
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}>
{subject}
</x.h2>
</Box>
<Group display="flex" gap={2}>
<Group display="flex" alignItems="center" gap={15}>
<x.abbr
role="presentation"
w={'40px'}
h={'40px'}
bg={'#daa3e4'}
fill={'#daa3e4'}
color={'#3f1946'}
lineHeight={'40px'}
textAlign={'center'}
borderRadius={'40px'}
fontSize={'14px'}
>
A
</x.abbr>
<Stack spacing={2}>
<Group spacing={2}>
<Box fontWeight={600}>{companyName} </Box>
<Box color={'#738091'}>{formatedFromAddresses}</Box>
</Group>
<Box fontSize={'sm'} color={'#738091'}>
Send to: {customerName} {formattedToAddresses};
</Box>
</Stack>
</Group>
</Group>
</Stack>
);
}
const formatAddresses = (addresses: Array<string>) =>
addresses?.map((email) => '<' + email + '>').join(' ');

View File

@@ -0,0 +1,27 @@
import { css } from '@emotion/css';
import clsx from 'classnames';
interface SendMailViewPreviewPdfIframeProps
extends React.IframeHTMLAttributes<HTMLIFrameElement> {}
export const SendMailViewPreviewPdfIframe = ({
...props
}: SendMailViewPreviewPdfIframeProps) => {
return (
<iframe
title={'invoice-pdf-preview'}
{...props}
className={clsx(
css`
height: 1123px;
width: 794px;
border: 0;
border-radius: 5px;
box-shadow: 0 10px 15px rgba(0, 0, 0, 0.05);
margin: 0 auto;
`,
props.className,
)}
/>
);
};

View File

@@ -0,0 +1,47 @@
import { css } from '@emotion/css';
import { Tabs } from '@blueprintjs/core';
import { Stack } from '@/components';
interface SendMailViewPreviewTabsProps {
children: React.ReactNode;
}
export function SendMailViewPreviewTabs({
children,
}: SendMailViewPreviewTabsProps) {
return (
<Stack bg="#F5F5F5" flex={'1'} maxHeight={'100%'} minWidth="850px">
<Tabs
id={'preview'}
defaultSelectedTabId={'payment-page'}
className={css`
overflow: hidden;
flex: 1 1;
display: flex;
flex-direction: column;
.bp4-tab-list {
padding: 0 20px;
background: #fff;
border-bottom: 1px solid #dcdcdd;
}
.bp4-tab {
line-height: 40px;
}
.bp4-tab:not([aria-selected='true']) {
color: #5f6b7c;
}
.bp4-tab-indicator-wrapper .bp4-tab-indicator {
height: 2px;
}
.bp4-tab-panel {
margin: 0;
overflow: auto;
}
`}
>
{children}
</Tabs>
</Stack>
);
}

View File

@@ -0,0 +1,209 @@
import { useMemo, useState } from 'react';
import { Button, MenuItem } from '@blueprintjs/core';
import { SelectOptionProps } from '@blueprintjs-formik/select';
import { useFormikContext } from 'formik';
import { css } from '@emotion/css';
import { FormGroupProps } from '@blueprintjs-formik/core';
import { FFormGroup, FMultiSelect, Group, Stack } from '@/components';
import { SendMailViewFormValues } from './_types';
const fieldsWrapStyle = css`
> :not(:first-of-type) .bp4-input {
border-top-color: transparent;
border-top-right-radius: 0;
border-top-left-radius: 0;
}
> :not(:last-of-type) .bp4-input {
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
}
`;
const styleEmailButton = css`
&.bp4-button.bp4-small {
width: auto;
margin: 0;
min-height: 26px;
line-height: 26px;
padding-top: 0;
padding-bottom: 0;
font-size: 12px;
}
`;
// Create new account renderer.
const createNewItemRenderer = (
query: string,
active: boolean,
handleClick: React.MouseEventHandler<HTMLElement>,
) => {
return (
<MenuItem
icon="add"
text={'Now contact address'}
active={active}
onClick={handleClick}
/>
);
};
// Create new item from the given query string.
const createNewItemFromQuery = (text: string): SelectOptionProps => ({ text });
interface SendMailViewToAddressFieldProps {
formGroupProps?: Partial<FormGroupProps>;
toMultiSelectProps?: Partial<any>;
fromMultiSelectProps?: Partial<any>;
ccMultiSelectProps?: Partial<any>;
bccMultiSelectProps?: Partial<any>;
}
export function SendMailViewToAddressField({
formGroupProps,
toMultiSelectProps,
ccMultiSelectProps,
bccMultiSelectProps,
}: SendMailViewToAddressFieldProps) {
const { values, setFieldValue } = useFormikContext<SendMailViewFormValues>();
const [showCCField, setShowCCField] = useState<boolean>(false);
const [showBccField, setShowBccField] = useState<boolean>(false);
const handleClickCcBtn = (event: React.MouseEvent<HTMLElement>) => {
event.preventDefault();
event.stopPropagation();
setShowCCField(true);
};
const handleClickBccBtn = (event: React.MouseEvent<HTMLElement>) => {
event.preventDefault();
event.stopPropagation();
setShowBccField(true);
};
const handleCreateToItemSelect = (value: SelectOptionProps) => {
setFieldValue('to', [...values?.to, value?.text]);
};
const handleCreateCcItemSelect = (value: SelectOptionProps) => {
setFieldValue('cc', [...values?.cc, value?.text]);
};
const handleCreateBccItemSelect = (value: SelectOptionProps) => {
setFieldValue('bcc', [...values?.bcc, value?.text]);
};
const handleTagInputKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
// Prevent the form from submitting when the user presses the Enter key
if (e.key === 'Enter') {
e.preventDefault();
}
};
const rightElementsToField = useMemo(
() => (
<Group
spacing={0}
paddingRight={'7px'}
paddingTop={'7px'}
fontWeight={500}
color={'#000'}
>
<Button
onClick={handleClickCcBtn}
minimal
small
className={styleEmailButton}
>
CC
</Button>
<Button
onClick={handleClickBccBtn}
minimal
small
className={styleEmailButton}
>
BCC
</Button>
</Group>
),
[],
);
return (
<FFormGroup label={'To'} name={'to'} {...formGroupProps}>
<Stack spacing={0} className={fieldsWrapStyle}>
<FMultiSelect
items={[]}
name={'to'}
placeholder={'To'}
popoverProps={{ minimal: true, fill: true }}
tagInputProps={{
tagProps: { round: true, minimal: true, large: true },
rightElement: rightElementsToField,
large: true,
inputProps: {
onKeyDown: handleTagInputKeyDown,
},
}}
createNewItemRenderer={createNewItemRenderer}
createNewItemFromQuery={createNewItemFromQuery}
onCreateItemSelect={handleCreateToItemSelect}
resetOnQuery
resetOnSelect
fill
fastField
{...toMultiSelectProps}
/>
{showCCField && (
<FMultiSelect
items={[]}
name={'cc'}
placeholder={'Cc'}
popoverProps={{ minimal: true, fill: true }}
tagInputProps={{
tagProps: { round: true, minimal: true, large: true },
large: true,
inputProps: {
onKeyDown: handleTagInputKeyDown,
},
}}
createNewItemRenderer={createNewItemRenderer}
createNewItemFromQuery={createNewItemFromQuery}
onCreateItemSelect={handleCreateCcItemSelect}
resetOnQuery
resetOnSelect
fill
fastField
{...ccMultiSelectProps}
/>
)}
{showBccField && (
<FMultiSelect
items={[]}
name={'bcc'}
placeholder={'Bcc'}
popoverProps={{ minimal: true, fill: true }}
tagInputProps={{
tagProps: { round: true, minimal: true, large: true },
large: true,
inputProps: {
onKeyDown: handleTagInputKeyDown,
},
}}
createNewItemRenderer={createNewItemRenderer}
createNewItemFromQuery={createNewItemFromQuery}
onCreateItemSelect={handleCreateBccItemSelect}
resetOnQuery
resetOnSelect
fill
fastField
{...bccMultiSelectProps}
/>
)}
</Stack>
</FFormGroup>
);
}

View File

@@ -0,0 +1 @@
export interface SendMailViewFormValues {}

View File

@@ -2,9 +2,9 @@ import { Group, Stack } from '@/components';
import { Classes } from '@blueprintjs/core';
import { InvoiceSendMailBoot } from './InvoiceSendMailContentBoot';
import { InvoiceSendMailForm } from './InvoiceSendMailForm';
import { InvoiceSendMailHeader } from './InvoiceSendMailHeader';
import { InvoiceSendMailPreview } from './InvoiceSendMailPreview';
import { InvoiceSendMailFields } from './InvoiceSendMailFields';
import { SendMailViewHeader } from '../../Estimates/SendMailViewDrawer/SendMailViewHeader';
export function InvoiceSendMailContent() {
return (
@@ -12,7 +12,7 @@ export function InvoiceSendMailContent() {
<InvoiceSendMailBoot>
<InvoiceSendMailForm>
<Stack spacing={0} flex={1} overflow="hidden">
<InvoiceSendMailHeader label={'Send Invoice Mail'} />
<SendMailViewHeader label={'Send Invoice Mail'} />
<Group flex={1} overflow="auto" spacing={0} alignItems={'stretch'}>
<InvoiceSendMailFields />

View File

@@ -8,157 +8,17 @@ import {
FCheckbox,
FFormGroup,
FInputGroup,
FMultiSelect,
FSelect,
FTextArea,
Group,
Icon,
Stack,
} from '@/components';
import { useDrawerContext } from '@/components/Drawer/DrawerProvider';
import { useDrawerActions } from '@/hooks/state';
import { useInvoiceMailItems, useSendInvoiceFormatArgsOptions } from './_hooks';
import { InvoiceSendMailFormValues } from './_types';
// Create new account renderer.
const createNewItemRenderer = (
query: string,
active: boolean,
handleClick: React.MouseEventHandler<HTMLElement>,
) => {
return (
<MenuItem
icon="add"
text={'Now contact address'}
active={active}
onClick={handleClick}
/>
);
};
// Create new item from the given query string.
const createNewItemFromQuery = (text: string): SelectOptionProps => ({ text });
const styleEmailButton = css`
&.bp4-button.bp4-small {
width: auto;
margin: 0;
min-height: 26px;
line-height: 26px;
padding-top: 0;
padding-bottom: 0;
font-size: 12px;
}
`;
const fieldsWrapStyle = css`
> :not(:first-of-type) .bp4-input {
border-top-color: transparent;
border-top-right-radius: 0;
border-top-left-radius: 0;
}
> :not(:last-of-type) .bp4-input {
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
}
`;
import { useInvoiceMailItems, } from './_hooks';
import { SendMailViewToAddressField } from '../../Estimates/SendMailViewDrawer/SendMailViewToAddressField';
import { SendMailViewMessageField } from '../../Estimates/SendMailViewDrawer/SendMailViewMessageField';
export function InvoiceSendMailFields() {
const [showCCField, setShowCCField] = useState<boolean>(false);
const [showBccField, setShowBccField] = useState<boolean>(false);
const textareaRef = useRef<HTMLTextAreaElement>(null);
const { values, setFieldValue } =
useFormikContext<InvoiceSendMailFormValues>();
const items = useInvoiceMailItems();
const argsOptions = useSendInvoiceFormatArgsOptions();
const handleClickCcBtn = (event: React.MouseEvent<HTMLElement>) => {
event.preventDefault();
event.stopPropagation();
setShowCCField(true);
};
const handleClickBccBtn = (event: React.MouseEvent<HTMLElement>) => {
event.preventDefault();
event.stopPropagation();
setShowBccField(true);
};
const handleCreateToItemSelect = (value: SelectOptionProps) => {
setFieldValue('to', [...values?.to, value?.text]);
};
const handleCreateCcItemSelect = (value: SelectOptionProps) => {
setFieldValue('cc', [...values?.cc, value?.text]);
};
const handleCreateBccItemSelect = (value: SelectOptionProps) => {
setFieldValue('bcc', [...values?.bcc, value?.text]);
};
const rightElementsToField = useMemo(
() => (
<Group
spacing={0}
paddingRight={'7px'}
paddingTop={'7px'}
fontWeight={500}
color={'#000'}
>
<Button
onClick={handleClickCcBtn}
minimal
small
className={styleEmailButton}
>
CC
</Button>
<Button
onClick={handleClickBccBtn}
minimal
small
className={styleEmailButton}
>
BCC
</Button>
</Group>
),
[],
);
const handleTextareaChange = useCallback(
(item: SelectOptionProps) => {
const textarea = textareaRef.current;
if (!textarea) return;
const { selectionStart, selectionEnd, value: text } = textarea;
const insertText = `{${item.value}}`;
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);
},
[setFieldValue],
);
const handleTagInputKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
// Prevent the form from submitting when the user presses the Enter key
if (e.key === 'Enter') {
e.preventDefault();
}
};
return (
<Stack
@@ -170,130 +30,16 @@ export function InvoiceSendMailFields() {
borderRight="1px solid #dcdcdd"
>
<Stack spacing={0} overflow="auto" flex="1" p={'30px'}>
<FFormGroup label={'To'} name={'to'}>
<Stack spacing={0} className={fieldsWrapStyle}>
<FMultiSelect
items={items}
name={'to'}
placeholder={'To'}
popoverProps={{ minimal: true, fill: true }}
tagInputProps={{
tagProps: { round: true, minimal: true, large: true },
rightElement: rightElementsToField,
large: true,
inputProps: {
onKeyDown: handleTagInputKeyDown,
},
}}
createNewItemRenderer={createNewItemRenderer}
createNewItemFromQuery={createNewItemFromQuery}
onCreateItemSelect={handleCreateToItemSelect}
resetOnQuery
resetOnSelect
fill
fastField
/>
{showCCField && (
<FMultiSelect
items={items}
name={'cc'}
placeholder={'Cc'}
popoverProps={{ minimal: true, fill: true }}
tagInputProps={{
tagProps: { round: true, minimal: true, large: true },
large: true,
inputProps: {
onKeyDown: handleTagInputKeyDown,
},
}}
createNewItemRenderer={createNewItemRenderer}
createNewItemFromQuery={createNewItemFromQuery}
onCreateItemSelect={handleCreateCcItemSelect}
resetOnQuery
resetOnSelect
fill
fastField
/>
)}
{showBccField && (
<FMultiSelect
items={items}
name={'bcc'}
placeholder={'Bcc'}
popoverProps={{ minimal: true, fill: true }}
tagInputProps={{
tagProps: { round: true, minimal: true, large: true },
large: true,
inputProps: {
onKeyDown: handleTagInputKeyDown,
},
}}
createNewItemRenderer={createNewItemRenderer}
createNewItemFromQuery={createNewItemFromQuery}
onCreateItemSelect={handleCreateBccItemSelect}
resetOnQuery
resetOnSelect
fill
fastField
/>
)}
</Stack>
</FFormGroup>
<SendMailViewToAddressField
toMultiSelectProps={{ items }}
ccMultiSelectProps={{ items }}
bccMultiSelectProps={{ items }}
/>
<FFormGroup label={'Submit'} name={'subject'}>
<FInputGroup name={'subject'} large fastField />
</FFormGroup>
<FFormGroup label={'Message'} name={'message'}>
<Stack spacing={0}>
<Group
border={'1px solid #ced4da'}
borderBottom={0}
borderRadius={'3px 3px 0 0'}
>
<FSelect
selectedItem={'customerName'}
name={'item'}
items={argsOptions}
onItemChange={handleTextareaChange}
popoverProps={{
fill: false,
position: Position.BOTTOM_LEFT,
minimal: true,
inputProps: {
onKeyDown: handleTagInputKeyDown,
},
}}
input={() => (
<Button
minimal
rightIcon={
<Icon icon={'caret-down-16'} color={'#8F99A8'} />
}
>
Insert Variable
</Button>
)}
fill={false}
fastField
/>
</Group>
<FTextArea
inputRef={textareaRef}
name={'message'}
large
fill
fastField
className={css`
resize: vertical;
min-height: 300px;
border-top-right-radius: 0px;
border-top-left-radius: 0px;
`}
/>
</Stack>
</FFormGroup>
<SendMailViewMessageField />
<Group>
<FCheckbox name={'attachPdf'} label={'Attach PDF'} />

View File

@@ -1,7 +1,6 @@
import { lazy, Suspense } from 'react';
import { css } from '@emotion/css';
import { Tab, Tabs } from '@blueprintjs/core';
import { Stack } from '@/components';
import { Tab, } from '@blueprintjs/core';
import { SendMailViewPreviewTabs } from '../../Estimates/SendMailViewDrawer/SendMailViewPreviewTabs';
const InvoiceMailReceiptPreviewConnected = lazy(() =>
import('./InvoiceMailReceiptPreviewConnected').then((module) => ({
@@ -16,55 +15,25 @@ const InvoiceSendPdfPreviewConnected = lazy(() =>
export function InvoiceSendMailPreview() {
return (
<Stack bg="#F5F5F5" flex={'1'} maxHeight={'100%'} minWidth="850px">
<Tabs
id={'preview'}
defaultSelectedTabId={'payment-page'}
className={css`
overflow: hidden;
flex: 1 1;
display: flex;
flex-direction: column;
.bp4-tab-list {
padding: 0 20px;
background: #fff;
border-bottom: 1px solid #dcdcdd;
}
.bp4-tab {
line-height: 40px;
}
.bp4-tab:not([aria-selected='true']) {
color: #5f6b7c;
}
.bp4-tab-indicator-wrapper .bp4-tab-indicator {
height: 2px;
}
.bp4-tab-panel {
margin: 0;
overflow: auto;
}
`}
>
<Tab
id={'payment-page'}
title={'Payment page'}
panel={
<Suspense>
<InvoiceMailReceiptPreviewConnected />
</Suspense>
}
/>
<Tab
id="pdf-document"
title={'PDF document'}
panel={
<Suspense>
<InvoiceSendPdfPreviewConnected />
</Suspense>
}
/>
</Tabs>
</Stack>
<SendMailViewPreviewTabs>
<Tab
id={'payment-page'}
title={'Payment page'}
panel={
<Suspense>
<InvoiceMailReceiptPreviewConnected />
</Suspense>
}
/>
<Tab
id="pdf-document"
title={'PDF document'}
panel={
<Suspense>
<InvoiceSendPdfPreviewConnected />
</Suspense>
}
/>
</SendMailViewPreviewTabs>
);
}

View File

@@ -4,6 +4,7 @@ import { Stack } from '@/components';
import { InvoiceSendMailPreviewWithHeader } from './InvoiceSendMailHeaderPreview';
import { useInvoiceHtml } from '@/hooks/query';
import { useDrawerContext } from '@/components/Drawer/DrawerProvider';
import { SendMailViewPreviewPdfIframe } from '../../Estimates/SendMailViewDrawer/SendMailViewPreviewPdfIframe';
export function InvoiceSendPdfPreviewConnected() {
return (
@@ -24,18 +25,5 @@ function InvoiceSendPdfPreviewIframe() {
}
const iframeSrcDoc = data?.htmlContent;
return (
<iframe
title={'invoice-pdf-preview'}
srcDoc={iframeSrcDoc}
className={css`
height: 1123px;
width: 794px;
border: 0;
border-radius: 5px;
box-shadow: 0 10px 15px rgba(0, 0, 0, 0.05);
margin: 0 auto;
`}
/>
);
return <SendMailViewPreviewPdfIframe srcDoc={iframeSrcDoc} />;
}

View File

@@ -0,0 +1,247 @@
import { Box } from '../lib/layout/Box';
import { Text } from '../lib/text/Text';
import { Stack } from '../lib/layout/Stack';
import { Group } from '../lib/layout/Group';
import {
DefaultPdfTemplateTerms,
DefaultPdfTemplateItemDescription,
DefaultPdfTemplateStatement,
DefaultPdfTemplateItemName,
DefaultPdfTemplateAddressBilledTo,
DefaultPdfTemplateAddressBilledFrom,
} from './_constants';
import { PaperTemplate, PaperTemplateProps } from './PaperTemplate';
export interface EstimatePaperTemplateProps extends PaperTemplateProps {
// # Company
showCompanyLogo?: boolean;
companyLogoUri?: string;
// # Estimate number
estimateNumebr?: string;
estimateNumberLabel?: string;
showEstimateNumber?: boolean;
// # Expiration date
expirationDate?: string;
showExpirationDate?: boolean;
expirationDateLabel?: string;
// # Estimate date
estimateDateLabel?: string;
showEstimateDate?: boolean;
estimateDate?: string;
// # Customer name
companyName?: string;
// Address
showCustomerAddress?: boolean;
customerAddress?: string;
showCompanyAddress?: boolean;
companyAddress?: string;
billedToLabel?: string;
// Totals
total?: string;
showTotal?: boolean;
totalLabel?: string;
subtotal?: string;
showSubtotal?: boolean;
subtotalLabel?: string;
// # Statements
showCustomerNote?: boolean;
customerNote?: string;
customerNoteLabel?: string;
// # Terms & conditions
showTermsConditions?: boolean;
termsConditions?: string;
termsConditionsLabel?: string;
lines?: Array<{
item: string;
description: string;
rate: string;
quantity: string;
total: string;
}>;
// Lines
lineItemLabel?: string;
lineQuantityLabel?: string;
lineRateLabel?: string;
lineTotalLabel?: string;
}
export function EstimatePaperTemplate({
primaryColor,
secondaryColor,
// # Company logo
showCompanyLogo = true,
companyLogoUri = '',
companyName,
// # Company address
companyAddress = DefaultPdfTemplateAddressBilledFrom,
showCompanyAddress = true,
// # Customer address
customerAddress = DefaultPdfTemplateAddressBilledTo,
showCustomerAddress = true,
billedToLabel = 'Billed To',
// # Total
total = '$1000.00',
totalLabel = 'Total',
showTotal = true,
// # Subtotal
subtotal = '1000/00',
subtotalLabel = 'Subtotal',
showSubtotal = true,
// # Customer Note
showCustomerNote = true,
customerNote = DefaultPdfTemplateStatement,
customerNoteLabel = 'Customer Note',
// # Terms & Conditions
showTermsConditions = true,
termsConditions = DefaultPdfTemplateTerms,
termsConditionsLabel = 'Terms & Conditions',
lines = [
{
item: DefaultPdfTemplateItemName,
description: DefaultPdfTemplateItemDescription,
rate: '1',
quantity: '1000',
total: '$1000.00',
},
],
// Estimate number
showEstimateNumber = true,
estimateNumberLabel = 'Estimate Number',
estimateNumebr = '346D3D40-0001',
// Estimate date
estimateDate = 'September 3, 2024',
showEstimateDate = true,
estimateDateLabel = 'Estimate Date',
// Expiration date
expirationDateLabel = 'Expiration Date',
showExpirationDate = true,
expirationDate = 'September 3, 2024',
// Entries
lineItemLabel = 'Item',
lineQuantityLabel = 'Qty',
lineRateLabel = 'Rate',
lineTotalLabel = 'Total',
}: EstimatePaperTemplateProps) {
return (
<PaperTemplate primaryColor={primaryColor} secondaryColor={secondaryColor}>
<Stack spacing={24}>
<Group align={'start'} spacing={10}>
<Stack flex={1}>
<PaperTemplate.BigTitle title={'Estimate'} />
<PaperTemplate.TermsList>
{showEstimateNumber && (
<PaperTemplate.TermsItem label={estimateNumberLabel}>
{estimateNumebr}
</PaperTemplate.TermsItem>
)}
{showEstimateDate && (
<PaperTemplate.TermsItem label={estimateDateLabel}>
{estimateDate}
</PaperTemplate.TermsItem>
)}
{showExpirationDate && (
<PaperTemplate.TermsItem label={expirationDateLabel}>
{expirationDate}
</PaperTemplate.TermsItem>
)}
</PaperTemplate.TermsList>
</Stack>
{companyLogoUri && showCompanyLogo && (
<PaperTemplate.Logo logoUri={companyLogoUri} />
)}
</Group>
<PaperTemplate.AddressesGroup>
{showCompanyAddress && (
<PaperTemplate.Address>
<Box dangerouslySetInnerHTML={{ __html: companyAddress }} />
</PaperTemplate.Address>
)}
{showCustomerAddress && (
<PaperTemplate.Address>
<strong>{billedToLabel}</strong>
<Box dangerouslySetInnerHTML={{ __html: customerAddress }} />
</PaperTemplate.Address>
)}
</PaperTemplate.AddressesGroup>
<Stack spacing={0}>
<PaperTemplate.Table
columns={[
{
label: lineItemLabel,
accessor: (data) => (
<Stack spacing={2}>
<Text>{data.item}</Text>
<Text
fontSize={'12px'}
// className={Classes.TEXT_MUTED}
// style={{ fontSize: 12 }}
>
{data.description}
</Text>
</Stack>
),
},
{ label: lineQuantityLabel, accessor: 'quantity' },
{ label: lineRateLabel, accessor: 'rate', align: 'right' },
{ label: lineTotalLabel, accessor: 'total', align: 'right' },
]}
data={lines}
/>
<PaperTemplate.Totals>
{showSubtotal && (
<PaperTemplate.TotalLine
label={subtotalLabel}
amount={subtotal}
/>
)}
{showTotal && (
<PaperTemplate.TotalLine label={totalLabel} amount={total} />
)}
</PaperTemplate.Totals>
</Stack>
<Stack spacing={0}>
{showCustomerNote && (
<PaperTemplate.Statement label={customerNoteLabel}>
{customerNote}
</PaperTemplate.Statement>
)}
{showTermsConditions && (
<PaperTemplate.Statement label={termsConditionsLabel}>
{termsConditions}
</PaperTemplate.Statement>
)}
</Stack>
</Stack>
</PaperTemplate>
);
}

View File

@@ -0,0 +1,180 @@
import { Box } from '../lib/layout/Box';
import { Stack } from '../lib/layout/Stack';
import { Group } from '../lib/layout/Group';
import {
PaperTemplate,
PaperTemplateProps,
PaperTemplateTotalBorder,
} from './PaperTemplate';
import {
DefaultPdfTemplateAddressBilledFrom,
DefaultPdfTemplateAddressBilledTo,
} from './_constants';
export interface PaymentReceivedPaperTemplateProps extends PaperTemplateProps {
// # Company logo
showCompanyLogo?: boolean;
companyLogoUri?: string;
// # Company name
companyName?: string;
// Customer address
showCustomerAddress?: boolean;
customerAddress?: string;
// Company address
showCompanyAddress?: boolean;
companyAddress?: string;
billedToLabel?: string;
// Total.
total?: string;
showTotal?: boolean;
totalLabel?: string;
// Subtotal.
subtotal?: string;
showSubtotal?: boolean;
subtotalLabel?: string;
lines?: Array<{
paidAmount: string;
invoiceAmount: string;
invoiceNumber: string;
}>;
// Issue date.
paymentReceivedDateLabel?: string;
showPaymentReceivedDate?: boolean;
paymentReceivedDate?: string;
// Payment received number.
paymentReceivedNumebr?: string;
paymentReceivedNumberLabel?: string;
showPaymentReceivedNumber?: boolean;
}
export function PaymentReceivedPaperTemplate({
// # Colors
primaryColor,
secondaryColor,
// # Company logo
showCompanyLogo = true,
companyLogoUri,
// # Company name
companyName = 'Bigcapital Technology, Inc.',
// # Customer address
showCustomerAddress = true,
customerAddress = DefaultPdfTemplateAddressBilledTo,
// # Company address
showCompanyAddress = true,
companyAddress = DefaultPdfTemplateAddressBilledFrom,
billedToLabel = 'Billed To',
total = '$1000.00',
totalLabel = 'Total',
showTotal = true,
subtotal = '1000/00',
subtotalLabel = 'Subtotal',
showSubtotal = true,
lines = [
{
invoiceNumber: 'INV-00001',
invoiceAmount: '$1000.00',
paidAmount: '$1000.00',
},
],
showPaymentReceivedNumber = true,
paymentReceivedNumberLabel = 'Payment Number',
paymentReceivedNumebr = '346D3D40-0001',
paymentReceivedDate = 'September 3, 2024',
showPaymentReceivedDate = true,
paymentReceivedDateLabel = 'Payment Date',
}: PaymentReceivedPaperTemplateProps) {
return (
<PaperTemplate primaryColor={primaryColor} secondaryColor={secondaryColor}>
<Stack spacing={24}>
<Group align={'start'} spacing={10}>
<Stack flex={1}>
<PaperTemplate.BigTitle title={'Payment'} />
<PaperTemplate.TermsList>
{showPaymentReceivedNumber && (
<PaperTemplate.TermsItem label={paymentReceivedNumberLabel}>
{paymentReceivedNumebr}
</PaperTemplate.TermsItem>
)}
{showPaymentReceivedDate && (
<PaperTemplate.TermsItem label={paymentReceivedDateLabel}>
{paymentReceivedDate}
</PaperTemplate.TermsItem>
)}
</PaperTemplate.TermsList>
</Stack>
{companyLogoUri && showCompanyLogo && (
<PaperTemplate.Logo logoUri={companyLogoUri} />
)}
</Group>
<PaperTemplate.AddressesGroup>
{showCompanyAddress && (
<PaperTemplate.Address>
<Box dangerouslySetInnerHTML={{ __html: companyAddress }} />
</PaperTemplate.Address>
)}
{showCustomerAddress && (
<PaperTemplate.Address>
<strong>{billedToLabel}</strong>
<Box dangerouslySetInnerHTML={{ __html: customerAddress }} />
</PaperTemplate.Address>
)}
</PaperTemplate.AddressesGroup>
<Stack spacing={0}>
<PaperTemplate.Table
columns={[
{ label: 'Invoice #', accessor: 'invoiceNumber' },
{
label: 'Invoice Amount',
accessor: 'invoiceAmount',
align: 'right',
},
{ label: 'Paid Amount', accessor: 'paidAmount', align: 'right' },
]}
data={lines}
/>
<PaperTemplate.Totals>
{showSubtotal && (
<PaperTemplate.TotalLine
label={subtotalLabel}
amount={subtotal}
border={PaperTemplateTotalBorder.Gray}
/>
)}
{showTotal && (
<PaperTemplate.TotalLine
label={totalLabel}
amount={total}
border={PaperTemplateTotalBorder.Dark}
style={{ fontWeight: 500 }}
/>
)}
</PaperTemplate.Totals>
</Stack>
</Stack>
</PaperTemplate>
);
}

View File

@@ -0,0 +1,237 @@
import { Box } from '../lib/layout/Box';
import { Text } from '../lib/text/Text';
import { Stack } from '../lib/layout/Stack';
import { Group } from '../lib/layout/Group';
import {
PaperTemplate,
PaperTemplateProps,
} from './PaperTemplate';
import {
DefaultPdfTemplateTerms,
DefaultPdfTemplateItemDescription,
DefaultPdfTemplateStatement,
DefaultPdfTemplateItemName,
DefaultPdfTemplateAddressBilledTo,
DefaultPdfTemplateAddressBilledFrom,
} from './_constants';
export interface ReceiptPaperTemplateProps extends PaperTemplateProps {
// # Company logo
showCompanyLogo?: boolean;
companyLogoUri?: string;
// # Company name
companyName?: string;
// Addresses
showCustomerAddress?: boolean;
customerAddress?: string;
showCompanyAddress?: boolean;
companyAddress?: string;
billedToLabel?: string;
// Total
total?: string;
showTotal?: boolean;
totalLabel?: string;
// Subtotal
subtotal?: string;
showSubtotal?: boolean;
subtotalLabel?: string;
// Customer Note
showCustomerNote?: boolean;
customerNote?: string;
customerNoteLabel?: string;
// Terms & Conditions
showTermsConditions?: boolean;
termsConditions?: string;
termsConditionsLabel?: string;
// Lines
lines?: Array<{
item: string;
description: string;
rate: string;
quantity: string;
total: string;
}>;
// Receipt Date.
receiptDateLabel?: string;
showReceiptDate?: boolean;
receiptDate?: string;
// Receipt Number
receiptNumebr?: string;
receiptNumberLabel?: string;
showReceiptNumber?: boolean;
// Entries
lineItemLabel?: string;
lineQuantityLabel?: string;
lineRateLabel?: string;
lineTotalLabel?: string;
}
export function ReceiptPaperTemplate({
// # Colors
primaryColor,
secondaryColor,
// # Company logo
showCompanyLogo = true,
companyLogoUri,
// # Company name
companyName = 'Bigcapital Technology, Inc.',
// # Address
showCustomerAddress = true,
customerAddress = DefaultPdfTemplateAddressBilledTo,
showCompanyAddress = true,
companyAddress = DefaultPdfTemplateAddressBilledFrom,
billedToLabel = 'Billed To',
total = '$1000.00',
totalLabel = 'Total',
showTotal = true,
subtotal = '1000/00',
subtotalLabel = 'Subtotal',
showSubtotal = true,
showCustomerNote = true,
customerNoteLabel = 'Customer Note',
customerNote = DefaultPdfTemplateStatement,
showTermsConditions = true,
termsConditionsLabel = 'Terms & Conditions',
termsConditions = DefaultPdfTemplateTerms,
lines = [
{
item: DefaultPdfTemplateItemName,
description: DefaultPdfTemplateItemDescription,
rate: '1',
quantity: '1000',
total: '$1000.00',
},
],
// Receipt Number
showReceiptNumber = true,
receiptNumberLabel = 'Receipt Number',
receiptNumebr = '346D3D40-0001',
// Receipt Date
receiptDate = 'September 3, 2024',
showReceiptDate = true,
receiptDateLabel = 'Receipt Date',
// Entries
lineItemLabel = 'Item',
lineQuantityLabel = 'Qty',
lineRateLabel = 'Rate',
lineTotalLabel = 'Total',
}: ReceiptPaperTemplateProps) {
return (
<PaperTemplate primaryColor={primaryColor} secondaryColor={secondaryColor}>
<Stack spacing={24}>
<Group align={'start'} spacing={10}>
<Stack flex={1}>
<PaperTemplate.BigTitle title={'Receipt'} />
<PaperTemplate.TermsList>
{showReceiptNumber && (
<PaperTemplate.TermsItem label={receiptNumberLabel}>
{receiptNumebr}
</PaperTemplate.TermsItem>
)}
{showReceiptDate && (
<PaperTemplate.TermsItem label={receiptDateLabel}>
{receiptDate}
</PaperTemplate.TermsItem>
)}
</PaperTemplate.TermsList>
</Stack>
{companyLogoUri && showCompanyLogo && (
<PaperTemplate.Logo logoUri={companyLogoUri} />
)}
</Group>
<PaperTemplate.AddressesGroup>
{showCompanyAddress && (
<PaperTemplate.Address>
<Box dangerouslySetInnerHTML={{ __html: companyAddress }} />
</PaperTemplate.Address>
)}
{showCustomerAddress && (
<PaperTemplate.Address>
<strong>{billedToLabel}</strong>
<Box dangerouslySetInnerHTML={{ __html: customerAddress }} />
</PaperTemplate.Address>
)}
</PaperTemplate.AddressesGroup>
<Stack spacing={0}>
<PaperTemplate.Table
columns={[
{
label: lineItemLabel,
accessor: (data) => (
<Stack spacing={2}>
<Text>{data.item}</Text>
<Text
fontSize={'12px'}
// className={Classes.TEXT_MUTED}
// style={{ fontSize: 12 }}
>
{data.description}
</Text>
</Stack>
),
},
{ label: lineQuantityLabel, accessor: 'quantity' },
{ label: lineRateLabel, accessor: 'rate', align: 'right' },
{ label: lineTotalLabel, accessor: 'total', align: 'right' },
]}
data={lines}
/>
<PaperTemplate.Totals>
{showSubtotal && (
<PaperTemplate.TotalLine
label={subtotalLabel}
amount={subtotal}
/>
)}
{showTotal && (
<PaperTemplate.TotalLine label={totalLabel} amount={total} />
)}
</PaperTemplate.Totals>
</Stack>
<Stack spacing={0}>
{showCustomerNote && (
<PaperTemplate.Statement label={customerNoteLabel}>
{customerNote}
</PaperTemplate.Statement>
)}
{showTermsConditions && (
<PaperTemplate.Statement label={termsConditionsLabel}>
{termsConditions}
</PaperTemplate.Statement>
)}
</Stack>
</Stack>
</PaperTemplate>
);
}

View File

@@ -1,3 +1,10 @@
export * from './components/PaperTemplate';
export * from './components/InvoicePaperTemplate';
export * from './components/EstimatePaperTemplate';
export * from './components/ReceiptPaperTemplate';
export * from './components/PaymentReceivedPaperTemplate';
export * from './renders/render-invoice-paper-template';
export * from './renders/render-estimate-paper-template';
export * from './renders/render-receipt-paper-template';
export * from './renders/render-payment-received-paper-template';

View File

@@ -0,0 +1,16 @@
import {
EstimatePaperTemplate,
EstimatePaperTemplateProps,
} from '../components/EstimatePaperTemplate';
import { renderSSR } from './render-ssr';
/**
* Renders estimate paper template html.
* @param {EstimatePaperTemplateProps} props
* @returns {string}
*/
export const renderEstimatePaperTemplateHtml = (
props: EstimatePaperTemplateProps
) => {
return renderSSR(<EstimatePaperTemplate {...props} />);
};

View File

@@ -1,13 +1,7 @@
import { renderToString } from 'react-dom/server';
import createCache from '@emotion/cache';
import { css } from '@emotion/css';
import {
InvoicePaperTemplate,
InvoicePaperTemplateProps,
} from '../components/InvoicePaperTemplate';
import { PaperTemplateLayout } from '../components/PaperTemplateLayout';
import { extractCritical } from '@emotion/server';
import { OpenSansFontLink } from '../constants';
import { renderSSR } from './render-ssr';
export const renderInvoicePaperTemplateHtml = (

View File

@@ -0,0 +1,11 @@
import {
PaymentReceivedPaperTemplateProps,
PaymentReceivedPaperTemplate,
} from '../components/PaymentReceivedPaperTemplate';
import { renderSSR } from './render-ssr';
export const renderPaymentReceivedPaperTemplateHtml = (
props: PaymentReceivedPaperTemplateProps
) => {
return renderSSR(<PaymentReceivedPaperTemplate {...props} />);
};

View File

@@ -0,0 +1,11 @@
import {
ReceiptPaperTemplate,
ReceiptPaperTemplateProps,
} from '../components/ReceiptPaperTemplate';
import { renderSSR } from './render-ssr';
export const renderReceiptPaperTemplateHtml = (
props: ReceiptPaperTemplateProps
) => {
return renderSSR(<ReceiptPaperTemplate {...props} />);
};