feat: wip invoice customizer

This commit is contained in:
Ahmed Bouhuolia
2024-09-07 21:39:05 +02:00
parent 6d24474162
commit e6bad27771
23 changed files with 648 additions and 1 deletions

View File

@@ -0,0 +1,14 @@
.field{
height: 28px;
line-height: 28px;
border-radius: 5px;
}
.colorPicker{
background-color: rgb(103, 114, 229);
border-radius: 3px;
height: 14px;
width: 14px;
cursor: pointer;
}

View File

@@ -0,0 +1,30 @@
import {
InputGroup,
Popover,
PopoverInteractionKind,
Position,
} from '@blueprintjs/core';
import { useState } from 'react';
import { HexColorPicker } from 'react-colorful';
import styles from './ColorField.module.scss';
export function ColorField() {
const [color, setColor] = useState('#aabbcc');
return (
<Popover
content={<HexColorPicker color={color} onChange={setColor} />}
position={Position.BOTTOM}
interactionKind={PopoverInteractionKind.CLICK}
modifiers={{
offset: { offset: '0, 4' },
}}
minimal
>
<InputGroup
leftElement={<div className={styles.colorPicker}></div>}
className={styles.field}
/>
</Popover>
);
}

View File

@@ -0,0 +1,9 @@
import { ColorField } from './ColorField';
interface FColorFieldProps {
name: string;
}
export function FColorField({ name }: FColorFieldProps) {
return <ColorField />;
}

View File

@@ -0,0 +1,21 @@
import { Box, Group } from '@/components';
import { InvoiceCustomizePreview } from './InvoiceCustomizePreview';
import { InvoiceCustomizeFields } from './InvoiceCustomizeFields';
import { InvoiceCustomizeForm } from './InvoiceCustomizerForm';
import { Classes } from '@blueprintjs/core';
import { InvoiceCustomizeTabsControllerProvider } from './InvoiceCustomizeTabsController';
export default function InvoiceCustomizeContent() {
return (
<Box className={Classes.DRAWER_BODY}>
<InvoiceCustomizeForm>
<Group spacing={0} align="flex-start">
<InvoiceCustomizeTabsControllerProvider>
<InvoiceCustomizeFields />
<InvoiceCustomizePreview />
</InvoiceCustomizeTabsControllerProvider>
</Group>
</InvoiceCustomizeForm>
</Box>
);
}

View File

@@ -0,0 +1,37 @@
// @ts-nocheck
import React from 'react';
import * as R from 'ramda';
import { Drawer, DrawerSuspense } from '@/components';
import withDrawers from '@/containers/Drawer/withDrawers';
const InvoiceCustomizeContent = React.lazy(
() => import('./InvoiceCustomizeContent'),
);
/**
* Refund credit note detail.
* @returns
*/
function InvoiceCustomizeDrawerRoot({
name,
// #withDrawer
isOpen,
payload: {},
}) {
return (
<Drawer
isOpen={isOpen}
name={name}
size={'100%'}
>
<DrawerSuspense>
<InvoiceCustomizeContent />
</DrawerSuspense>
</Drawer>
);
}
export const InvoiceCustomizeDrawer = R.compose(withDrawers())(
InvoiceCustomizeDrawerRoot,
);

View File

@@ -0,0 +1,7 @@
.root {
flex: 1;
}
.mainFields{
flex: 1;
}

View File

@@ -0,0 +1,26 @@
import { Box, Group } from '@/components';
import { InvoiceCustomizeHeader } from './InvoiceCustomizeHeader';
import { InvoiceCustomizeTabs } from './InvoiceCustomizeTabs';
import styles from './InvoiceCustomizeFields.module.scss';
import { InvoiceCustomizeGeneralField } from './InvoiceCustomizeGeneralFields';
import { useInvoiceCustomizeTabsController } from './InvoiceCustomizeTabsController';
export function InvoiceCustomizeFields() {
return (
<Group spacing={0} align={'stretch'} className={styles.root}>
<InvoiceCustomizeTabs />
<InvoiceCustomizeFieldsMain />
</Group>
);
}
export function InvoiceCustomizeFieldsMain() {
const { currentTabId } = useInvoiceCustomizeTabsController();
return (
<Box className={styles.mainFields}>
<InvoiceCustomizeHeader label={'Customize'} />
{currentTabId === 'general' && <InvoiceCustomizeGeneralField />}
</Box>
);
}

View File

@@ -0,0 +1,16 @@
import { Box, FFormGroup } from '@/components';
import { FColorField } from './FColorField';
export function InvoiceCustomizeGeneralField() {
return (
<Box>
<FFormGroup name={'primaryColor'} label={'Primary Color'} inline>
<FColorField name={'primaryColor'} />
</FFormGroup>
<FFormGroup name={'secondaryColor'} label={'Secondary Color'} inline>
<FColorField name={'secondaryColor'} />
</FFormGroup>
</Box>
);
}

View File

@@ -0,0 +1,18 @@
.root {
align-items: center;
border-radius: 0;
box-shadow: 0 1px 0 rgba(17, 20, 24, .15);
display: flex;
flex: 0 0 auto;
min-height: 40px;
padding: 5px 5px 5px 20px;
position: relative;
background-color: #fff;
}
.title{
margin: 0;
font-size: 19px;
font-weight: 500;
}

View File

@@ -0,0 +1,36 @@
import { Group, Icon } from '@/components';
import styles from './InvoiceCustomizeHeader.module.scss';
import { Button, Classes } from '@blueprintjs/core';
interface InvoiceCustomizeHeaderProps {
label?: string;
children?: React.ReactNode;
closeButton?: boolean;
onClose?: () => void;
}
export function InvoiceCustomizeHeader({
label,
closeButton,
onClose,
children,
}: InvoiceCustomizeHeaderProps) {
const handleClose = () => {
onClose && onClose();
};
return (
<Group className={styles.root}>
{label && <h1 className={styles.title}>{label}</h1>}
{closeButton && (
<Button
aria-label="Close"
className={Classes.DIALOG_CLOSE_BUTTON}
icon={<Icon icon={'smallCross'} color={'#000'} />}
minimal={true}
onClick={handleClose}
style={{ marginLeft: 'auto' }}
/>
)}
</Group>
);
}

View File

@@ -0,0 +1,12 @@
import { Stack } from '@/components';
import { InvoiceCustomizeHeader } from './InvoiceCustomizeHeader';
import { InvoiceCustomizePreviewContent } from './InvoiceCustomizePreviewContent';
export function InvoiceCustomizePreview() {
return (
<Stack spacing={0} style={{ borderLeft: '1px solid #D9D9D9' }}>
<InvoiceCustomizeHeader label={'Preview'} closeButton />
<InvoiceCustomizePreviewContent />
</Stack>
);
}

View File

@@ -0,0 +1,10 @@
import { Box } from '@/components';
import { PaperTemplate } from './PaperTemplate';
export function InvoiceCustomizePreviewContent() {
return (
<Box style={{ padding: 20, backgroundColor: '#F5F5F5' }}>
<PaperTemplate />
</Box>
);
}

View File

@@ -0,0 +1,14 @@
.root {
flex: 1;
min-width: 165px;
max-width: 185px;
}
.content{
padding: 5px;
}
.tabsList{
width: 100%;
}

View File

@@ -0,0 +1,29 @@
import { Box } from '@/components';
import { Tab, Tabs } from '@blueprintjs/core';
import { InvoiceCustomizeHeader } from './InvoiceCustomizeHeader';
import styles from './InvoiceCustomizeTabs.module.scss';
import {
InvoiceCustomizeTabsEnum,
useInvoiceCustomizeTabsController,
} from './InvoiceCustomizeTabsController';
export function InvoiceCustomizeTabs() {
const { setCurrentTabId } = useInvoiceCustomizeTabsController();
const handleChange = (value: InvoiceCustomizeTabsEnum) => {
setCurrentTabId(value);
};
return (
<Box className={styles.root}>
<InvoiceCustomizeHeader label={''} />
<Box className={styles.content}>
<Tabs vertical fill onChange={handleChange} className={styles.tabsList}>
<Tab id="general" title="General" />
<Tab id="content" title="Content" />
<Tab id="total" title="Total" />
</Tabs>
</Box>
</Box>
);
}

View File

@@ -0,0 +1,46 @@
import React, { createContext, useContext, useState } from 'react';
export enum InvoiceCustomizeTabsEnum {
General = 'general',
Items = 'items',
Totals = 'totals'
}
const DEFAULT_TAB_ID = InvoiceCustomizeTabsEnum.General;
interface InvoiceCustomizeTabsControllerValue {
currentTabId: InvoiceCustomizeTabsEnum;
setCurrentTabId: React.Dispatch<
React.SetStateAction<InvoiceCustomizeTabsEnum>
>;
}
const InvoiceCustomizeTabsController = createContext(
{} as InvoiceCustomizeTabsControllerValue,
);
export const useInvoiceCustomizeTabsController = () => {
return useContext(InvoiceCustomizeTabsController);
};
interface InvoiceCustomizeTabsControllerProps {
children: React.ReactNode;
}
export const InvoiceCustomizeTabsControllerProvider = ({
children,
}: InvoiceCustomizeTabsControllerProps) => {
const [currentTabId, setCurrentTabId] =
useState<InvoiceCustomizeTabsEnum>(DEFAULT_TAB_ID);
const value = {
currentTabId,
setCurrentTabId,
};
return (
<InvoiceCustomizeTabsController.Provider value={value}>
{children}
</InvoiceCustomizeTabsController.Provider>
);
};

View File

@@ -0,0 +1,27 @@
import { Formik, Form } from 'formik';
import * as Yup from 'yup';
import React from 'react';
const validationSchema = Yup.object().shape({
invoiceNumber: Yup.string().required('Invoice number is required'),
customerName: Yup.string().required('Customer name is required'),
amount: Yup.number()
.required('Amount is required')
.positive('Amount must be positive'),
});
interface InvoiceCustomizeFormProps {
children: React.ReactNode;
}
export function InvoiceCustomizeForm({ children }: InvoiceCustomizeFormProps) {
return (
<Formik
initialValues={{ invoiceNumber: '', customerName: '', amount: '' }}
validationSchema={validationSchema}
onSubmit={(values) => {}}
>
<Form>{children}</Form>
</Formik>
);
}

View File

@@ -0,0 +1,105 @@
.root {
border-radius: 5px;
background-color: #fff;
box-shadow: inset 0 4px 0px 0 #002762, 0 10px 15px rgba(0, 0, 0, 0.15);
padding: 22px;
position: relative;
}
.bigTitle{
font-size: 60px;
margin: 0;
LINE-HEIGHT: 1;
MARGIN-BOTTOM: 20px;
}
.details {
margin-bottom: 20px;
display: flex;
flex-direction: column;
gap: 4px;
}
.detail {
display: flex;
flex-direction: row;
gap: 10px;
}
.detailLabel {
min-width: 120px;
}
.addressRoot{
display: flex;
flex-direction: row;
}
.addressBillTo{
flex: 1;
}
.addressFrom{
flex: 1;
}
.table :global {
margin-top: 40px;
width: 100%;
border-collapse: collapse;
text-align: left;
thead th{
font-weight: 400;
border-bottom: 1px solid #000;
}
tbody{
tr {
}
td{
border: 1px solid #F6F6F6;
padding: 10px 0;
}
}
}
.totals{
display: flex;
flex-direction: column;
margin-bottom: 40px;
margin-left: auto;
}
.totalsItem{
display: flex;
padding: 6px 0;
}
.totalsItemLabel{
min-width: 160px;
}
.logoWrap{
height: 120px;
width: 120px;
position: absolute;
right: 20px;
top: 20px;
img{
max-width: 100%;
}
}
.footer{
}
.paragraph{
margin-bottom: 20px;
}
.paragraphLabel{
color: #333333;
}

View File

@@ -0,0 +1,135 @@
import styles from './PaperTemplate.module.scss';
export function PaperTemplate() {
return (
<div className={styles.root}>
<div>
<h1 className={styles.bigTitle}>Invoice</h1>
<div className={styles.logoWrap}>
<img alt="" src="https://cdn-development.mercury.com/demo-assets/avatars/mercury-demo-dark.png" />
</div>
</div>
<div className={styles.details}>
<div className={styles.detail}>
<div className={styles.detailLabel}>Invoice number</div>
<div>346D3D40-0001</div>
</div>
<div className={styles.detail}>
<div className={styles.detailLabel}>Date of issue</div>
<div>September 3, 2024</div>
</div>
<div className={styles.detail}>
<div className={styles.detailLabel}>Date due</div>
<div>October 3, 2024</div>
</div>
</div>
<div className={styles.addressRoot}>
<div className={styles.addressBillTo}>
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>
<div className={styles.addressFrom}>
Billed To <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>
</div>
<table className={styles.table}>
<thead>
<tr>
<th>Item</th>
<th>Description</th>
<th>Rate</th>
<th>Total</th>
</tr>
</thead>
<tbody>
<tr>
<td>Simply dummy text</td>
<td>Simply dummy text of the printing and typesetting</td>
<td>1 X $100,00</td>
<td>$100,00</td>
</tr>
</tbody>
</table>
<div style={{ display: 'flex' }}>
<div className={styles.totals}>
<div className={styles.totalsItem}>
<div className={styles.totalsItemLabel}>Sub Total</div>
<div>630.00</div>
</div>
<div className={styles.totalsItem}>
<div className={styles.totalsItemLabel}>Discount</div>
<div>0.00</div>
</div>
<div className={styles.totalsItem}>
<div className={styles.totalsItemLabel}>Sample Tax1 (4.70%)</div>
<div>11.75</div>
</div>
<div className={styles.totalsItem}>
<div className={styles.totalsItemLabel}>Sample Tax2 (7.00%)</div>
<div>21.00</div>
</div>
<div className={styles.totalsItem}>
<div className={styles.totalsItemLabel}>Total</div>
<div>$662.75</div>
</div>
<div className={styles.totalsItem}>
<div className={styles.totalsItemLabel}>Payment Made</div>
<div>100.00</div>
</div>
<div className={styles.totalsItem}>
<div className={styles.totalsItemLabel}>Balance Due</div>
<div className={styles.totalsItemLabel}>$562.75</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.
</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.
</div>
</div>
</div>
);
}

View File

@@ -7,6 +7,11 @@ import {
NavbarGroup,
Intent,
Alignment,
Menu,
MenuItem,
Popover,
PopoverInteractionKind,
Position,
} from '@blueprintjs/core';
import { useHistory } from 'react-router-dom';
import {
@@ -32,6 +37,8 @@ import withSettingsActions from '@/containers/Settings/withSettingsActions';
import { compose } from '@/utils';
import withDialogActions from '@/containers/Dialog/withDialogActions';
import { DialogsName } from '@/constants/dialogs';
import withDrawerActions from '@/containers/Drawer/withDrawerActions';
import { DRAWERS } from '@/constants/drawers';
/**
* Invoices table actions bar.
@@ -51,6 +58,9 @@ function InvoiceActionsBar({
// #withDialogsActions
openDialog,
// #withDrawerActions
openDrawer,
}) {
const history = useHistory();
@@ -97,6 +107,11 @@ function InvoiceActionsBar({
downloadExportPdf({ resource: 'SaleInvoice' });
};
// Handles the invoice customize button click.
const handleCustomizeBtnClick = () => {
openDrawer(DRAWERS.INVOICE_CUSTOMIZE);
};
return (
<DashboardActionsBar>
<NavbarGroup>
@@ -164,6 +179,25 @@ function InvoiceActionsBar({
<NavbarDivider />
</NavbarGroup>
<NavbarGroup align={Alignment.RIGHT}>
<Popover
minimal={true}
interactionKind={PopoverInteractionKind.CLICK}
position={Position.BOTTOM_RIGHT}
modifiers={{
offset: { offset: '0, 4' },
}}
content={
<Menu>
<MenuItem
onClick={handleCustomizeBtnClick}
text={'Customize Invoice'}
/>
</Menu>
}
>
<Button icon={<Icon icon="cog-16" iconSize={16} />} minimal={true} />
</Popover>
<NavbarDivider />
<Button
className={Classes.MINIMAL}
icon={<Icon icon="refresh-16" iconSize={14} />}
@@ -184,4 +218,5 @@ export default compose(
invoicesTableSize: invoiceSettings?.tableSize,
})),
withDialogActions,
withDrawerActions,
)(InvoiceActionsBar);