feat: invoice customize paper preview

This commit is contained in:
Ahmed Bouhuolia
2024-09-10 13:19:11 +02:00
parent 67904f52af
commit f0dfc3d1b0
11 changed files with 474 additions and 171 deletions

View File

@@ -11,6 +11,7 @@ import {
import { HexColorPicker } from 'react-colorful';
import { useUncontrolled } from '@/hooks/useUncontrolled';
import { Box, BoxProps } from '@/components';
import { sanitizeToHexColor } from '@/utils/sanitize-hex-color';
import styles from './ColorInput.module.scss';
export interface ColorInputProps {
@@ -72,6 +73,10 @@ export function ColorInput({
/>
</Box>
}
onChange={(e) => {
const value = sanitizeToHexColor(e.currentTarget.value);
handleChange(value);
}}
{...inputProps}
className={clsx(styles.field, inputProps?.className)}
/>

View File

@@ -56,7 +56,7 @@ export interface ElementCustomizePaperTemplateProps {
ElementCustomize.PaperTemplate = ({
children,
}: ElementCustomizePaperTemplateProps) => {
return <Box>{children}</Box>;
return <>{children}</>;
};
export interface ElementCustomizeContentProps {
@@ -70,5 +70,5 @@ ElementCustomize.FieldsTab = ({
label,
children,
}: ElementCustomizeContentProps) => {
return <Box>{children}</Box>;
return <>{children}</>;
};

View File

@@ -3,7 +3,7 @@ import React from 'react';
import * as R from 'ramda';
import { Button, Intent } from '@blueprintjs/core';
import { useFormikContext } from 'formik';
import { Group, Stack } from '@/components';
import { Box, Group, Stack } from '@/components';
import { ElementCustomizeHeader } from './ElementCustomizeHeader';
import { ElementCustomizeTabs } from './ElementCustomizeTabs';
import { useElementCustomizeTabsController } from './ElementCustomizeTabsController';
@@ -14,7 +14,7 @@ import styles from './ElementCustomize.module.scss';
export function ElementCustomizeFields() {
return (
<Group spacing={0} align={'stretch'} className={styles.root}>
<Group spacing={0} align={'stretch'} className={styles.root}>
<ElementCustomizeTabs />
<ElementCustomizeFieldsMain />
</Group>
@@ -38,7 +38,7 @@ export function ElementCustomizeFieldsMain() {
<ElementCustomizeHeader label={'Customize'} />
<Stack spacing={0} style={{ flex: '1 1 auto', overflow: 'auto' }}>
{CustomizeTabPanel}
<Box style={{ flex: '1 1' }}>{CustomizeTabPanel}</Box>
<ElementCustomizeFooterActions />
</Stack>
</Stack>

View File

@@ -5,45 +5,18 @@ import { InvoicePaperTemplate } from './InvoicePaperTemplate';
import { ElementCustomize } from '../../../ElementCustomize/ElementCustomize';
import { InvoiceCustomizeGeneralField } from './InvoiceCustomizeGeneralFields';
import { InvoiceCustomizeContentFields } from './InvoiceCutomizeContentFields';
interface InvoiceCustomizeValues {
invoiceNumber?: string;
invoiceNumberLabel?: string;
dateIssue?: string;
dateIssueLabel?: string;
dueDate?: string;
dueDateLabel?: string;
companyName?: string;
bigtitle?: string;
itemRateLabel?: string;
itemQuantityLabel?: string;
itemTotalLabel?: string;
// Totals
showDueAmount?: boolean;
showDiscount?: boolean;
showPaymentMade?: boolean;
showTaxes?: boolean;
showSubtotal?: boolean;
showTotal?: boolean;
showBalanceDue?: boolean;
paymentMadeLabel?: string;
discountLabel?: string;
subtotalLabel?: string;
totalLabel?: string;
balanceDueLabel?: string;
}
import { InvoiceCustomizeValues } from './types';
import { initialValues } from './constants';
export default function InvoiceCustomizeContent() {
const handleFormSubmit = (values: InvoiceCustomizeValues) => {};
return (
<Box className={Classes.DRAWER_BODY}>
<ElementCustomize<InvoiceCustomizeValues>>
<ElementCustomize<InvoiceCustomizeValues>
initialValues={initialValues}
onSubmit={handleFormSubmit}
>
<ElementCustomize.PaperTemplate>
<InvoicePaperTemplate />
</ElementCustomize.PaperTemplate>

View File

@@ -20,33 +20,38 @@ export function InvoiceCustomizeGeneralField() {
<FFormGroup
name={'primaryColor'}
label={'Primary Color'}
inline
className={styles.fieldGroup}
inline
fastField
>
<FColorInput
name={'primaryColor'}
inputProps={{ style: { maxWidth: 120 } }}
fastField
/>
</FFormGroup>
<FFormGroup
name={'secondaryColor'}
label={'Secondary Color'}
inline
className={styles.fieldGroup}
inline
fastField
>
<FColorInput
name={'secondaryColor'}
inputProps={{ style: { maxWidth: 120 } }}
fastField
/>
</FFormGroup>
<FFormGroup name={'showLogo'} label={'Logo'}>
<FFormGroup name={'showCompanyLogo'} label={'Logo'} fastField>
<FSwitch
name={'showLogo'}
name={'showCompanyLogo'}
label={'Display company logo in the paper'}
className={styles.showCompanyLogoField}
large
fastField
/>
</FFormGroup>
</Stack>

View File

@@ -1,35 +1,44 @@
import { Classes } from '@blueprintjs/core';
// @ts-nocheck
import { FInputGroup, FSwitch, Group, Stack } from '@/components';
const items = [
{ key: 'dueAmount', label: 'Due Amount' },
{ key: 'billedTo', label: 'Billed To' },
{ key: 'balanceDue', label: 'Balance Due' },
{ key: 'termsConditions', label: 'Terms & Conditions' },
];
import { CLASSES } from '@/constants';
import { Classes } from '@blueprintjs/core';
import { fieldsGroups } from './constants';
export function InvoiceCustomizeContentFields() {
return (
<Stack style={{ padding: 20, flex: '1 1 auto' }}>
<Stack>
<h2>General Branding</h2>
<Stack
spacing={10}
style={{ padding: 20, paddingBottom: 40, flex: '1 1 auto' }}
>
<Stack spacing={10}>
<h3>General Branding</h3>
<p className={Classes.TEXT_MUTED}>
Set your invoice details to be automatically applied every timeyou
create a new invoice.
</p>
</Stack>
<h1>Header</h1>
<Stack>
{items.map((item, index) => (
<Group position={'apart'} key={index}>
<FSwitch name={`item.${item.key}.enabled`} label={item.label} />
<FInputGroup
name={'item.dueAmount.text'}
placeholder={item.label}
/>
</Group>
{fieldsGroups.map((group) => (
<>
<h4 className={CLASSES.TEXT_MUTED} style={{ fontWeight: 600 }}>
{group.label}
</h4>
<Stack spacing={14}>
{group.fields.map((item, index) => (
<Group spacing={14} position={'apart'} key={index}>
<FSwitch name={item.enableKey} label={item.label} fastField />
{item.labelKey && (
<FInputGroup
name={item.labelKey}
style={{ maxWidth: 150 }}
fastField
/>
)}
</Group>
))}
</Stack>
</>
))}
</Stack>
</Stack>

View File

@@ -3,7 +3,7 @@
border-radius: 5px;
background-color: #fff;
color: #111;
box-shadow: inset 0 4px 0px 0 #002762, 0 10px 15px rgba(0, 0, 0, 0.05);
box-shadow: inset 0 4px 0px 0 var(--invoice-primary-color), 0 10px 15px rgba(0, 0, 0, 0.05);
padding: 24px 30px;
font-size: 12px;
position: relative;

View File

@@ -1,60 +1,127 @@
import clsx from 'classnames';
import * as R from 'ramda';
import { useFormikContext } from 'formik';
import styles from './InvoicePaperTemplate.module.scss';
interface PapaerLine {
item?: string;
description?: string;
quantity?: string;
rate?: string;
total?: string;
}
interface PaperTax {
label: string;
amount: string;
}
interface PaperTemplateProps {
primaryColor?: string;
secondaryColor?: string;
showCompanyLogo?: boolean;
companyLogo?: string;
showInvoiceNumber?: boolean;
invoiceNumber?: string;
invoiceNumberLabel?: string;
showDateIssue?: boolean;
dateIssue?: string;
dateIssueLabel?: string;
showDueDate?: boolean;
dueDate?: string;
dueDateLabel?: string;
companyName?: string;
bigtitle?: string;
itemRateLabel?: string;
itemQuantityLabel?: string;
itemTotalLabel?: string;
// Address
showBillingToAddress?: boolean;
showBilledFromAddress?: boolean;
billedToLabel?: string;
// Entries
lineItemLabel?: string;
lineDescriptionLabel?: string;
lineRateLabel?: string;
lineTotalLabel?: string;
// Totals
showDueAmount?: boolean;
showDiscount?: boolean;
showPaymentMade?: boolean;
showTaxes?: boolean;
showSubtotal?: boolean;
showTotal?: boolean;
showBalanceDue?: boolean;
paymentMadeLabel?: string;
discountLabel?: string;
subtotalLabel?: string;
totalLabel?: string;
total?: string;
showDiscount?: boolean;
discountLabel?: string;
discount?: string;
showSubtotal?: boolean;
subtotalLabel?: string;
subtotal?: string;
showPaymentMade?: boolean;
paymentMadeLabel?: string;
paymentMade?: string;
showTaxes?: boolean;
showDueAmount?: boolean;
showBalanceDue?: boolean;
balanceDueLabel?: string;
balanceDue?: string;
// Footer
termsConditionsLabel: string;
showTermsConditions: boolean;
termsConditions: string;
statementLabel: string;
showStatement: boolean;
statement: string;
lines?: Array<PapaerLine>;
taxes?: Array<PaperTax>;
billedFromAddres?: Array<string>;
billedToAddress?: Array<string>;
}
export function InvoicePaperTemplate({
bigtitle = 'Invoice',
function InvoicePaperTemplateRoot({
primaryColor,
secondaryColor,
bigtitle = 'Invoice',
companyName = 'Bigcapital Technology, Inc.',
// dueDateLabel,
showCompanyLogo = true,
companyLogo,
dueDate = 'September 3, 2024',
dueDateLabel = 'Date due',
showDueDate,
dateIssue = 'September 3, 2024',
dateIssueLabel = 'Date of issue',
showDateIssue,
// dateIssue,
invoiceNumberLabel = 'Invoice number',
invoiceNumber = '346D3D40-0001',
showInvoiceNumber,
// Address
showBillingToAddress = true,
showBilledFromAddress = true,
billedToLabel = 'Billed To',
// Entries
itemQuantityLabel = 'Quantity',
itemRateLabel = 'Rate',
itemTotalLabel = 'Total',
lineItemLabel = 'Item',
lineDescriptionLabel = 'Description',
lineRateLabel = 'Rate',
lineTotalLabel = 'Total',
totalLabel = 'Total',
subtotalLabel = 'Subtotal',
@@ -70,82 +137,127 @@ export function InvoicePaperTemplate({
showPaymentMade = true,
showDueAmount = true,
showBalanceDue = true,
total = '$662.75',
subtotal = '630.00',
discount = '0.00',
paymentMade = '100.00',
balanceDue = '$562.75',
// Footer paragraphs.
termsConditionsLabel = 'Terms & Conditions',
showTermsConditions = true,
termsConditions = 'It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout.',
lines = [
{
item: 'Simply dummy text',
description: 'Simply dummy text of the printing and typesetting',
rate: '1',
quantity: '1000',
total: '$1000.00',
},
],
taxes = [
{ label: 'Sample Tax1 (4.70%)', amount: '11.75' },
{ label: 'Sample Tax2 (7.00%)', amount: '21.74' },
],
statementLabel = 'Statement',
showStatement = true,
statement = 'It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout.',
billedToAddress = [
'Bigcapital Technology, Inc.',
'131 Continental Dr Suite 305 Newark,',
'Delaware 19713',
'United States',
'+1 762-339-5634',
'ahmed@bigcapital.app',
],
billedFromAddres = [
'131 Continental Dr Suite 305 Newark,',
'Delaware 19713',
'United States',
'+1 762-339-5634',
'ahmed@bigcapital.app',
],
}: PaperTemplateProps) {
return (
<div className={styles.root}>
<style>{`:root { --invoice-primary-color: ${primaryColor}; --invoice-secondary-color: ${secondaryColor}; }`}</style>
<div>
<h1 className={styles.bigTitle}>{bigtitle}</h1>
<div className={styles.logoWrap}>
<img
alt=""
src="https://cdn-development.mercury.com/demo-assets/avatars/mercury-demo-dark.png"
/>
</div>
{showCompanyLogo && (
<div className={styles.logoWrap}>
<img alt="" src={companyLogo} />
</div>
)}
</div>
<div className={styles.details}>
<div className={styles.detail}>
<div className={styles.detailLabel}>{invoiceNumberLabel}</div>
<div>{invoiceNumber}</div>
</div>
<div className={styles.detail}>
<div className={styles.detailLabel}>{dateIssueLabel}</div>
<div>{dateIssue}</div>
</div>
<div className={styles.detail}>
<div className={styles.detailLabel}>{dueDateLabel}</div>
<div>{dueDate}</div>
</div>
{showInvoiceNumber && (
<div className={styles.detail}>
<div className={styles.detailLabel}>{invoiceNumberLabel}</div>
<div>{invoiceNumber}</div>
</div>
)}
{showDateIssue && (
<div className={styles.detail}>
<div className={styles.detailLabel}>{dateIssueLabel}</div>
<div>{dateIssue}</div>
</div>
)}
{showDueDate && (
<div className={styles.detail}>
<div className={styles.detailLabel}>{dueDateLabel}</div>
<div>{dueDate}</div>
</div>
)}
</div>
<div className={styles.addressRoot}>
<div className={styles.addressBillTo}>
<strong>{companyName}</strong> <br />
131 Continental Dr Suite 305 Newark,
<br />
Delaware 19713
<br />
United States
<br />
+1 762-339-5634
<br />
ahmed@bigcapital.app
</div>
{showBilledFromAddress && (
<div className={styles.addressBillTo}>
<strong>{companyName}</strong> <br />
{billedFromAddres.map((text, index) => (
<div key={index}>{text}</div>
))}
</div>
)}
<div className={styles.addressFrom}>
<strong>Billed To</strong> <br />
Bigcapital Technology, Inc. <br />
131 Continental Dr Suite 305 Newark,
<br />
Delaware 19713
<br />
United States
<br />
+1 762-339-5634
<br />
ahmed@bigcapital.app
</div>
{showBillingToAddress && (
<div className={styles.addressFrom}>
<strong>{billedToLabel}</strong> <br />
{billedToAddress.map((text, index) => (
<div key={index}>{text}</div>
))}
</div>
)}
</div>
<table className={styles.table}>
<thead>
<tr>
<th>Item</th>
<th>Description</th>
<th className={styles.rate}>{itemRateLabel}</th>
<th className={styles.total}>{itemTotalLabel}</th>
<th>{lineItemLabel}</th>
<th>{lineDescriptionLabel}</th>
<th className={styles.rate}>{lineRateLabel}</th>
<th className={styles.total}>{lineTotalLabel}</th>
</tr>
</thead>
<tbody className={styles.tableBody}>
<tr>
<td>Simply dummy text</td>
<td>Simply dummy text of the printing and typesetting</td>
<td className={styles.rate}>1 X $100,00</td>
<td className={styles.total}>$100,00</td>
</tr>
{lines.map((line, index) => (
<tr key={index}>
<td>{line.item}</td>
<td>{line.description}</td>
<td className={styles.rate}>
{line.quantity} X {line.rate}
</td>
<td className={styles.total}>{line.total}</td>
</tr>
))}
</tbody>
</table>
@@ -159,32 +271,25 @@ export function InvoicePaperTemplate({
)}
>
<div className={styles.totalsItemLabel}>{subtotalLabel}</div>
<div className={styles.totalsItemAmount}>630.00</div>
<div className={styles.totalsItemAmount}>{subtotal}</div>
</div>
)}
{showDiscount && (
<div className={styles.totalsItem}>
<div className={styles.totalsItemLabel}>{discountLabel}</div>
<div className={styles.totalsItemAmount}>0.00</div>
<div className={styles.totalsItemAmount}>{discount}</div>
</div>
)}
{showTaxes && (
<>
<div className={styles.totalsItem}>
<div className={styles.totalsItemLabel}>
Sample Tax1 (4.70%)
{taxes.map((tax, index) => (
<div key={index} className={styles.totalsItem}>
<div className={styles.totalsItemLabel}>{tax.label}</div>
<div className={styles.totalsItemAmount}>{tax.amount}</div>
</div>
<div className={styles.totalsItemAmount}>11.75</div>
</div>
<div className={styles.totalsItem}>
<div className={styles.totalsItemLabel}>
Sample Tax2 (7.00%)
</div>
<div className={styles.totalsItemAmount}>21.00</div>
</div>
))}
</>
)}
@@ -193,14 +298,14 @@ export function InvoicePaperTemplate({
className={clsx(styles.totalsItem, styles.totalBottomBordered)}
>
<div className={styles.totalsItemLabel}>{totalLabel}</div>
<div className={styles.totalsItemAmount}>$662.75</div>
<div className={styles.totalsItemAmount}>{total}</div>
</div>
)}
{showPaymentMade && (
<div className={styles.totalsItem}>
<div className={styles.totalsItemLabel}>{paymentMadeLabel}</div>
<div className={styles.totalsItemAmount}>100.00</div>
<div className={styles.totalsItemAmount}>{paymentMade}</div>
</div>
)}
@@ -209,27 +314,38 @@ export function InvoicePaperTemplate({
className={clsx(styles.totalsItem, styles.totalBottomBordered)}
>
<div className={styles.totalsItemLabel}>{balanceDueLabel}</div>
<div className={styles.totalsItemAmount}>$562.75</div>
<div className={styles.totalsItemAmount}>{balanceDue}</div>
</div>
)}
</div>
</div>
<div className={styles.paragraph}>
<div className={styles.paragraphLabel}>Terms & Conditions</div>
<div>
It is a long established fact that a reader will be distracted by the
readable content of a page when looking at its layout.
{showTermsConditions && (
<div className={styles.paragraph}>
<div className={styles.paragraphLabel}>{termsConditionsLabel}</div>
<div>{termsConditions}</div>
</div>
</div>
<div className={styles.paragraph}>
<div className={styles.paragraphLabel}>Statement</div>
<div>
It is a long established fact that a reader will be distracted by the
readable content of a page when looking at its layout.
)}
{showStatement && (
<div className={styles.paragraph}>
<div className={styles.paragraphLabel}>{statementLabel}</div>
<div>{statement}</div>
</div>
</div>
)}
</div>
);
}
const withFormikProps = <P extends object>(
Component: React.ComponentType<P>,
) => {
return (props: Omit<P, keyof PaperTemplateProps>) => {
const { values } = useFormikContext<PaperTemplateProps>();
return <Component {...(props as P)} {...values} />;
};
};
export const InvoicePaperTemplate = R.compose(withFormikProps)(
InvoicePaperTemplateRoot,
);

View File

@@ -0,0 +1,134 @@
export const initialValues = {
// Colors
primaryColor: '#2c3dd8',
secondaryColor: '#2c3dd8',
// Company logo.
showCompanyLogo: true,
companyLogo:
'https://cdn-development.mercury.com/demo-assets/avatars/mercury-demo-dark.png',
// Top details.
showInvoiceNumber: true,
invoiceNumberLabel: 'Invoice number',
showDateIssue: true,
dateIssueLabel: 'Date of Issue',
showDueDate: true,
dueDateLabel: 'Due Date',
// Company name
companyName: 'Bigcapital Technology, Inc.',
// Addresses
showBilledFromAddress: true,
showBillingToAddress: true,
billedToLabel: 'Billed To',
// Entries
itemNameLabel: 'Item',
itemDescriptionLabel: 'Description',
itemRateLabel: 'Rate',
itemTotalLabel: 'Total',
// Totals
showSubtotal: true,
subtotalLabel: 'Subtotal',
showDiscount: true,
discountLabel: 'Discount',
showTaxes: true,
showTotal: true,
totalLabel: 'Total',
paymentMadeLabel: 'Payment Made',
showPaymentMade: true,
dueAmountLabel: 'Due Amount',
showDueAmount: true,
// Footer paragraphs.
termsConditionsLabel: 'Terms & Conditions',
showTermsConditions: true,
statementLabel: 'Statement',
showStatement: true,
};
export const fieldsGroups = [
{
label: 'Header',
fields: [
{
labelKey: 'invoiceNumberLabel',
enableKey: 'showInvoiceNumber',
label: 'Invoice No.',
},
{
labelKey: 'dateIssueLabel',
enableKey: 'showDateIssue',
label: 'Issue Date',
},
{
labelKey: 'dueDateLabel',
enableKey: 'showDueDate',
label: 'Due Date',
},
{
enableKey: 'showBillingToAddress',
labelKey: 'billedToLabel',
label: 'Bill To',
},
{
enableKey: 'showBilledFromAddress',
label: 'Billed From',
},
],
},
{
label: 'Totals',
fields: [
{
labelKey: 'subtotalLabel',
enableKey: 'showSubtotal',
label: 'Subtotal',
},
{
labelKey: 'discountLabel',
enableKey: 'showDiscount',
label: 'Discount',
},
{ enableKey: 'showTaxes', label: 'Taxes' },
{ labelKey: 'totalLabel', enableKey: 'showTotal', label: 'Total' },
{
labelKey: 'paymentMadeLabel',
enableKey: 'showPaymentMade',
label: 'Payment Made',
},
{
labelKey: 'dueAmountLabel',
enableKey: 'showDueAmount',
label: 'Due Amount',
},
],
},
{
label: 'Footer',
fields: [
{
labelKey: 'termsConditionsLabel',
enableKey: 'showTermsConditions',
label: 'Terms & Conditions',
},
{
labelKey: 'statementLabel',
enableKey: 'showStatement',
label: 'Statement',
labelPlaceholder: 'Statement',
},
],
},
];

View File

@@ -0,0 +1,58 @@
export interface InvoiceCustomizeValues {
// Colors
primaryColor?: string;
secondaryColor?: string;
// Company Logo
showCompanyLogo?: boolean;
companyLogo?: string;
// Top details.
showInvoiceNumber?: boolean;
invoiceNumberLabel?: string;
showDateIssue?: boolean;
dateIssueLabel?: string;
showDueDate?: boolean;
dueDateLabel?: string;
// Company name
companyName?: string;
// Addresses
showBilledFromAddress?: boolean;
showBillingToAddress?: boolean;
billedToLabel?: string;
// Entries
itemNameLabel?: string;
itemDescriptionLabel?: string;
itemRateLabel?: string;
itemTotalLabel?: string;
// Totals
showSubtotal?: boolean;
subtotalLabel?: string;
showDiscount?: boolean;
discountLabel?: string;
showTaxes?: boolean;
showTotal?: boolean;
totalLabel?: string;
paymentMadeLabel?: string;
showPaymentMade?: boolean;
dueAmountLabel?: string;
showDueAmount?: boolean;
// Footer paragraphs.
termsConditionsLabel?: string;
showTermsConditions?: boolean;
statementLabel?: string;
showStatement?: boolean;
}

View File

@@ -0,0 +1,3 @@
export function sanitizeToHexColor(input: string) {
return input;
}