feat: Invoice mail receipt preview

This commit is contained in:
Ahmed Bouhuolia
2024-10-21 15:42:12 +02:00
parent ccbb399685
commit dffd818396
7 changed files with 460 additions and 7 deletions

View File

@@ -7,13 +7,14 @@ export function ElementCustomizePreviewContent() {
return (
<Box
style={{
padding: '28px 24px 40px',
backgroundColor: '#F5F5F5',
overflow: 'auto',
flex: '1',
// padding: '28px 24px 40px',
// backgroundColor: '#F5F5F5',
// overflow: 'auto',
// flex: '1',
}}
>
{PaperTemplate}
</Box>
);
}

View File

@@ -0,0 +1,23 @@
import { InvoicePaymentPage, PaymentPageProps } from './PaymentPage';
interface InvoicePaymentPagePreviewProps extends Partial<PaymentPageProps> { }
export function InvoicePaymentPagePreview(
props: InvoicePaymentPagePreviewProps,
) {
return (
<InvoicePaymentPage
paidAmount={'$1,000.00'}
dueDate={'20 Sep 2024'}
total={'$1,000.00'}
subtotal={'$1,000.00'}
dueAmount={'$1,000.00'}
customerName={'Ahmed Bouhuolia'}
organizationName={'Bigcapital Technology, Inc.'}
invoiceNumber={'INV-000001'}
companyLogoUri={' '}
organizationAddress={' '}
{...props}
/>
);
}

View File

@@ -0,0 +1,181 @@
import { Text, Classes, Button, Intent, ButtonProps } from '@blueprintjs/core';
import clsx from 'classnames';
import { Box, Group, Stack } from '@/components';
import styles from './PaymentPortal.module.scss';
export interface PaymentPageProps {
companyLogoUri: string;
organizationName: string;
customerName: string;
subtotal: string;
total: string;
dueDate: string;
viewInvoiceLabel?: string;
invoiceNumber: string;
totalLabel?: string;
subtotalLabel?: string;
customerAddress?: string;
downloadInvoiceBtnLabel?: string;
showPayButton?: boolean;
paidAmount: string;
paidAmountLabel?: string;
organizationAddress: string;
dueAmount: string;
dueAmountLabel?: string;
downloadInvoiceButtonProps?: Partial<ButtonProps>;
payInvoiceButtonProps?: Partial<ButtonProps>;
viewInvoiceButtonProps?: Partial<ButtonProps>;
invoiceNumberLabel?: string;
buyNote?: string;
copyrightText?: string;
}
export function InvoicePaymentPage({
companyLogoUri,
organizationName,
customerName,
subtotal,
total,
dueDate,
paidAmount,
paidAmountLabel = 'Paid Amount (-)',
invoiceNumber,
customerAddress,
totalLabel = 'Total',
subtotalLabel = 'Subtotal',
viewInvoiceLabel = 'View Invoice',
downloadInvoiceBtnLabel = 'Download Invoice',
showPayButton = true,
organizationAddress,
dueAmount,
dueAmountLabel = 'Due Amount',
downloadInvoiceButtonProps,
payInvoiceButtonProps,
viewInvoiceButtonProps,
invoiceNumberLabel = 'Invoice #',
buyNote = 'By confirming your payment, you allow Bigcapital Technology, Inc. to charge you for this payment and save your payment information in accordance with their terms.',
copyrightText = `© 2024 Bigcapital Technology, Inc. <br /> All rights reserved.`,
}: PaymentPageProps) {
return (
<Box className={styles.root}>
<Stack spacing={0} className={styles.body}>
<Stack>
<Group spacing={10}>
{companyLogoUri && (
<Box
className={styles.companyLogoWrap}
style={{
backgroundImage: `url(${companyLogoUri})`,
}}
></Box>
)}
<Text>{organizationName}</Text>
</Group>
<Stack spacing={6}>
<h1 className={styles.bigTitle}>
{organizationName} Sent an Invoice for {total}
</h1>
<Group spacing={10}>
<Text className={clsx(Classes.TEXT_MUTED, styles.invoiceDueDate)}>
Invoice due {dueDate}{' '}
</Text>
</Group>
</Stack>
<Stack className={styles.address} spacing={2}>
<Box className={styles.customerName}>{customerName}</Box>
{customerAddress && (
<Box dangerouslySetInnerHTML={{ __html: customerAddress }} />
)}
</Stack>
<h2 className={styles.invoiceNumber}>Invoice {invoiceNumber}</h2>
<Stack spacing={0} className={styles.totals}>
<Group
position={'apart'}
className={clsx(styles.totalItem, styles.borderBottomGray)}
>
<Text>{subtotalLabel}</Text>
<Text>{subtotal}</Text>
</Group>
<Group position={'apart'} className={styles.totalItem}>
<Text>{totalLabel}</Text>
<Text style={{ fontWeight: 500 }}>{total}</Text>
</Group>
{/*
{sharableLinkMeta?.taxes?.map((tax, key) => (
<Group key={key} position={'apart'} className={styles.totalItem}>
<Text>{tax?.name}</Text>
<Text>{tax?.taxRateAmountFormatted}</Text>
</Group>
))} */}
<Group
position={'apart'}
className={clsx(styles.totalItem, styles.borderBottomGray)}
>
<Text>{paidAmountLabel}</Text>
<Text>{paidAmount}</Text>
</Group>
<Group
position={'apart'}
className={clsx(styles.totalItem, styles.borderBottomDark)}
>
<Text>Due Amount</Text>
<Text style={{ fontWeight: 500 }}>{dueAmount}</Text>
</Group>
</Stack>
</Stack>
<Stack spacing={8} className={styles.footerButtons}>
<Button
minimal
className={clsx(styles.footerButton, styles.downloadInvoiceButton)}
{...downloadInvoiceButtonProps}
>
{downloadInvoiceBtnLabel}
</Button>
<Button
className={clsx(styles.footerButton, styles.viewInvoiceButton)}
{...viewInvoiceButtonProps}
>
{viewInvoiceLabel}
</Button>
{showPayButton && (
<Button
intent={Intent.PRIMARY}
className={clsx(styles.footerButton, styles.buyButton)}
{...payInvoiceButtonProps}
>
Pay {total}
</Button>
)}
</Stack>
{buyNote && (
<Text className={clsx(Classes.TEXT_MUTED, styles.buyNote)}>
{buyNote}
</Text>
)}
</Stack>
<Stack spacing={18} className={styles.footer}>
<Box dangerouslySetInnerHTML={{ __html: organizationAddress }}></Box>
{copyrightText && (
<Stack
spacing={0}
className={styles.footerText}
dangerouslySetInnerHTML={{ __html: copyrightText }}
></Stack>
)}
</Stack>
</Box>
);
}

View File

@@ -1,6 +1,7 @@
import React from 'react';
import React, { lazy, Suspense } from 'react';
import * as R from 'ramda';
import { useFormikContext } from 'formik';
import { Spinner, Tab } from '@blueprintjs/core';
import {
InvoicePaperTemplate,
InvoicePaperTemplateProps,
@@ -19,6 +20,19 @@ import { BrandingTemplateForm } from '@/containers/BrandingTemplates/BrandingTem
import { useElementCustomizeContext } from '@/containers/ElementCustomize/ElementCustomizeProvider';
import { initialValues } from './constants';
import { useIsTemplateNamedFilled } from '@/containers/BrandingTemplates/utils';
import { InvoiceCustomizeTabs } from './InvoiceCustomizeTabs';
const InvoicePaymentPagePreview = lazy(() =>
import('@/containers/PaymentPortal/InvoicePaymentPagePreview').then(
(module) => ({ default: module.InvoicePaymentPagePreview }),
),
);
const InvoiceMailReceiptPreview = lazy(() =>
import(
'@/containers/Sales/Invoices/InvoiceCustomize/InvoiceMailReceiptPreview'
).then((module) => ({ default: module.InvoiceMailReceiptPreview })),
);
/**
* Invoice branding template customize.
@@ -56,7 +70,39 @@ function InvoiceCustomizeFormContent() {
return (
<ElementCustomizeContent>
<ElementCustomize.PaperTemplate>
<InvoicePaperTemplateFormConnected />
<InvoiceCustomizeTabs
defaultSelectedTabId={'pdf-document'}
id={'customize-preview-tabs'}
renderActiveTabPanelOnly
>
<Tab
id="pdf-document"
title={'PDF document'}
panel={
<Suspense fallback={<Spinner />}>
<InvoicePaperTemplateFormConnected />
</Suspense>
}
/>
<Tab
id={'payment-page'}
title={'Payment page'}
panel={
<Suspense fallback={<Spinner />}>
<InvoicePaymentPagePreview />
</Suspense>
}
/>
<Tab
id={'email-receipt'}
title={'Email receipt'}
panel={
<Suspense fallback={<Spinner />}>
<InvoiceMailReceiptPreview mx={'auto'} />
</Suspense>
}
/>
</InvoiceCustomizeTabs>
</ElementCustomize.PaperTemplate>
<ElementCustomize.FieldsTab id={'general'} label={'General'}>
@@ -90,7 +136,6 @@ const withInvoicePreviewTemplateProps = <P extends object>(
...brandingState,
...values,
};
return <Component {...(props as P)} {...mergedProps} />;
};
};

View File

@@ -0,0 +1,28 @@
import { css } from '@emotion/css';
import { Tabs, TabsProps } from '@blueprintjs/core';
interface InvoiceCustomizeTabsProps extends TabsProps { }
export function InvoiceCustomizeTabs(props: InvoiceCustomizeTabsProps) {
return (
<Tabs
className={css`
.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;
}
`}
{...props}
/>
);
}

View File

@@ -0,0 +1,137 @@
import { Button, Intent } from '@blueprintjs/core';
import { css } from '@emotion/css';
import { x } from '@xstyled/emotion';
import { Group, Stack, StackProps } from '@/components';
export interface InvoiceMailReceiptProps extends StackProps {
companyLogoUri?: string;
message: string;
companyName: string;
invoiceNumber: string;
dueDate: string;
items?: Array<{ label: string; total: string; quantity: string | number }>;
total: string;
dueAmount: string;
totalLabel?: string;
dueAmountLabel?: string;
viewInvoiceButtonLabel?: string;
viewInvoiceButtonOnClick?: () => void;
invoiceNumberLabel?: string;
}
export function InvoiceMailReceipt({
companyLogoUri,
message,
companyName,
total,
invoiceNumber,
dueDate,
dueAmount,
items,
viewInvoiceButtonLabel = 'View Invoice',
viewInvoiceButtonOnClick,
totalLabel = 'Total',
dueAmountLabel = 'Due Amount',
invoiceNumberLabel = 'Invoice #',
...restProps
}: InvoiceMailReceiptProps) {
return (
<Stack
bg="white"
w={'100%'}
maxWidth={'500px'}
p={'35px 25px'}
borderRadius={'5px'}
boxShadow={'0 10px 15px rgba(0, 0, 0, 0.05)'}
color={'black'}
{...restProps}
>
<Stack spacing={16} textAlign={'center'}>
{companyLogoUri && (
<x.div h={'90px'} w={'90px'} bg="#F2F2F2" mx="auto"></x.div>
)}
<Stack spacing={8}>
<x.h1 m={0} fontSize={'18px'} fontWeight={500} color="#404854">
{companyName}
</x.h1>
<x.h3 color="#383E47" fontWeight={500}>
{total}
</x.h3>
<x.span fontSize={'13px'} color="#404854">
{invoiceNumberLabel} {invoiceNumber}
</x.span>
<x.span fontSize={'13px'} color="#404854">
Due {dueDate}
</x.span>
</Stack>
</Stack>
<x.p m={0} whiteSpace={'pre-line'} color="#252A31">
{message}
</x.p>
<Button
large
intent={Intent.PRIMARY}
className={css`
&.bp4-large {
min-height: 38px;
}
`}
onClick={viewInvoiceButtonOnClick}
>
{viewInvoiceButtonLabel}
</Button>
<Stack spacing={0}>
{items?.map((item, key) => (
<Group
key={key}
h={'40px'}
position={'apart'}
borderBottomStyle="solid"
borderBottomWidth={'1px'}
borderBottomColor={'#D9D9D9'}
borderTopStyle="solid"
borderTopColor={'#D9D9D9'}
borderTopWidth={'1px'}
>
<x.span>{item.label}</x.span>
<x.span>
{item.quantity} x {item.total}
</x.span>
</Group>
))}
<Group
h={'40px'}
position={'apart'}
borderBottomStyle="solid"
borderBottomWidth={'1px'}
borderColor={'#000'}
>
<x.span fontWeight={500}>{totalLabel}</x.span>
<x.span fontWeight={600} fontSize={15}>
{total}
</x.span>
</Group>
<Group
h={'40px'}
position={'apart'}
borderBottomStyle="solid"
borderBottomWidth={'1px'}
borderBottomColor={'#000'}
>
<x.span fontWeight={500}>{dueAmountLabel}</x.span>
<x.span fontWeight={600} fontSize={15}>
{dueAmount}
</x.span>
</Group>
</Stack>
</Stack>
);
}

View File

@@ -0,0 +1,38 @@
import {
InvoiceMailReceipt,
InvoiceMailReceiptProps,
} from './InvoiceMailReceipt';
export interface InvoiceMailReceiptPreviewProps
extends Partial<InvoiceMailReceiptProps> { }
const receiptMessage = `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
`;
export function InvoiceMailReceiptPreview(
props: InvoiceMailReceiptPreviewProps,
) {
const propsWithDefaults = {
message: receiptMessage,
companyName: 'Bigcapital Technology, Inc.',
total: '$1,000.00',
invoiceNumber: 'INV-0001',
dueDate: '2 Oct 2024',
dueAmount: '$1,000.00',
items: [{ label: 'Line Item #1', total: '$1000.00', quantity: 1 }],
companyLogoUri: ' ',
...props,
};
return <InvoiceMailReceipt {...propsWithDefaults} />;
}