mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-17 13:20:31 +00:00
Merge branch 'develop' into stripe-integrate
This commit is contained in:
@@ -2,8 +2,12 @@
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
export function Card({ className, children }) {
|
||||
return <CardRoot className={className}>{children}</CardRoot>;
|
||||
export function Card({ className, style, children }) {
|
||||
return (
|
||||
<CardRoot className={className} style={style}>
|
||||
{children}
|
||||
</CardRoot>
|
||||
);
|
||||
}
|
||||
|
||||
const CardRoot = styled.div`
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
// @ts-nocheck
|
||||
import React, { createContext, useContext } from 'react';
|
||||
|
||||
const DrawerContext = createContext();
|
||||
interface DrawerContextValue {
|
||||
name: string;
|
||||
payload: Record<string, any>;
|
||||
}
|
||||
|
||||
const DrawerContext = createContext<DrawerContextValue>(
|
||||
{} as DrawerContextValue,
|
||||
);
|
||||
|
||||
/**
|
||||
* Account form provider.
|
||||
|
||||
@@ -23,6 +23,12 @@ import WarehouseTransferDetailDrawer from '@/containers/Drawers/WarehouseTransfe
|
||||
import TaxRateDetailsDrawer from '@/containers/TaxRates/drawers/TaxRateDetailsDrawer/TaxRateDetailsDrawer';
|
||||
import CategorizeTransactionDrawer from '@/containers/CashFlow/CategorizeTransaction/drawers/CategorizeTransactionDrawer/CategorizeTransactionDrawer';
|
||||
import ChangeSubscriptionPlanDrawer from '@/containers/Subscriptions/drawers/ChangeSubscriptionPlanDrawer/ChangeSubscriptionPlanDrawer';
|
||||
import { InvoiceCustomizeDrawer } from '@/containers/Sales/Invoices/InvoiceCustomize/InvoiceCustomizeDrawer';
|
||||
import { EstimateCustomizeDrawer } from '@/containers/Sales/Estimates/EstimateCustomize/EstimateCustomizeDrawer';
|
||||
import { ReceiptCustomizeDrawer } from '@/containers/Sales/Receipts/ReceiptCustomize/ReceiptCustomizeDrawer';
|
||||
import { CreditNoteCustomizeDrawer } from '@/containers/Sales/CreditNotes/CreditNoteCustomize/CreditNoteCustomizeDrawer';
|
||||
import { PaymentReceivedCustomizeDrawer } from '@/containers/Sales/PaymentsReceived/PaymentReceivedCustomize/PaymentReceivedCustomizeDrawer';
|
||||
import { BrandingTemplatesDrawer } from '@/containers/BrandingTemplates/BrandingTemplatesDrawer';
|
||||
|
||||
import { DRAWERS } from '@/constants/drawers';
|
||||
|
||||
@@ -65,6 +71,14 @@ export default function DrawersContainer() {
|
||||
<TaxRateDetailsDrawer name={DRAWERS.TAX_RATE_DETAILS} />
|
||||
<CategorizeTransactionDrawer name={DRAWERS.CATEGORIZE_TRANSACTION} />
|
||||
<ChangeSubscriptionPlanDrawer name={DRAWERS.CHANGE_SUBSCARIPTION_PLAN} />
|
||||
<InvoiceCustomizeDrawer name={DRAWERS.INVOICE_CUSTOMIZE} />
|
||||
<EstimateCustomizeDrawer name={DRAWERS.ESTIMATE_CUSTOMIZE} />
|
||||
<ReceiptCustomizeDrawer name={DRAWERS.RECEIPT_CUSTOMIZE} />
|
||||
<CreditNoteCustomizeDrawer name={DRAWERS.CREDIT_NOTE_CUSTOMIZE} />
|
||||
<PaymentReceivedCustomizeDrawer
|
||||
name={DRAWERS.PAYMENT_RECEIVED_CUSTOMIZE}
|
||||
/>
|
||||
<BrandingTemplatesDrawer name={DRAWERS.BRANDING_TEMPLATES} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
15
packages/webapp/src/components/Forms/ColorInput.module.scss
Normal file
15
packages/webapp/src/components/Forms/ColorInput.module.scss
Normal file
@@ -0,0 +1,15 @@
|
||||
|
||||
.field{
|
||||
height: 28px;
|
||||
line-height: 28px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.colorPicker{
|
||||
background-color: rgb(103, 114, 229);
|
||||
border-radius: 3px;
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.1);
|
||||
cursor: pointer;
|
||||
}
|
||||
85
packages/webapp/src/components/Forms/ColorInput.tsx
Normal file
85
packages/webapp/src/components/Forms/ColorInput.tsx
Normal file
@@ -0,0 +1,85 @@
|
||||
import { useState } from 'react';
|
||||
import clsx from 'classnames';
|
||||
import {
|
||||
IInputGroupProps,
|
||||
InputGroup,
|
||||
IPopoverProps,
|
||||
Popover,
|
||||
PopoverInteractionKind,
|
||||
Position,
|
||||
} from '@blueprintjs/core';
|
||||
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 {
|
||||
value?: string;
|
||||
initialValue?: string;
|
||||
onChange?: (value: string) => void;
|
||||
popoverProps?: Partial<IPopoverProps>;
|
||||
inputProps?: Partial<IInputGroupProps>;
|
||||
pickerProps?: Partial<BoxProps>;
|
||||
pickerWrapProps?: Partial<BoxProps>;
|
||||
}
|
||||
|
||||
export function ColorInput({
|
||||
value,
|
||||
initialValue,
|
||||
onChange,
|
||||
popoverProps,
|
||||
inputProps,
|
||||
pickerWrapProps,
|
||||
pickerProps,
|
||||
}: ColorInputProps) {
|
||||
const [_value, handleChange] = useUncontrolled({
|
||||
value,
|
||||
initialValue,
|
||||
onChange,
|
||||
finalValue: '',
|
||||
});
|
||||
const [isOpen, setIsOpen] = useState<boolean>(false);
|
||||
|
||||
const handleClose = () => {
|
||||
setIsOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<Popover
|
||||
content={<HexColorPicker color={_value} onChange={handleChange} />}
|
||||
position={Position.BOTTOM}
|
||||
interactionKind={PopoverInteractionKind.CLICK}
|
||||
modifiers={{
|
||||
offset: { offset: '0, 4' },
|
||||
}}
|
||||
onClose={handleClose}
|
||||
isOpen={isOpen}
|
||||
minimal
|
||||
{...popoverProps}
|
||||
>
|
||||
<InputGroup
|
||||
value={_value}
|
||||
leftElement={
|
||||
<Box
|
||||
{...pickerWrapProps}
|
||||
style={{ padding: 8, ...pickerWrapProps?.style }}
|
||||
>
|
||||
<Box
|
||||
onClick={() => setIsOpen((oldValue) => !oldValue)}
|
||||
style={{ backgroundColor: _value }}
|
||||
className={clsx(styles.colorPicker, pickerProps?.className)}
|
||||
{...pickerProps}
|
||||
/>
|
||||
</Box>
|
||||
}
|
||||
onChange={(e) => {
|
||||
const value = sanitizeToHexColor(e.currentTarget.value);
|
||||
handleChange(value);
|
||||
}}
|
||||
{...inputProps}
|
||||
className={clsx(styles.field, inputProps?.className)}
|
||||
/>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
64
packages/webapp/src/components/Forms/FColorInput.tsx
Normal file
64
packages/webapp/src/components/Forms/FColorInput.tsx
Normal file
@@ -0,0 +1,64 @@
|
||||
import React from 'react';
|
||||
import { getIn, FieldConfig, FieldProps } from 'formik';
|
||||
import { Intent } from '@blueprintjs/core';
|
||||
import { Field } from '@blueprintjs-formik/core';
|
||||
import { ColorInput, ColorInputProps } from './ColorInput';
|
||||
|
||||
interface ColorInputInputGroupProps
|
||||
extends Omit<FieldConfig, 'children' | 'component' | 'as' | 'value'>,
|
||||
ColorInputProps {}
|
||||
|
||||
export interface ColorInputToInputProps
|
||||
extends Omit<FieldProps, 'onChange'>,
|
||||
ColorInputProps {}
|
||||
|
||||
/**
|
||||
* Transforms field props to input group props for ColorInput.
|
||||
* @param {ColorInputToInputProps}
|
||||
* @returns {ColorInputProps}
|
||||
*/
|
||||
function fieldToColorInputInputGroup({
|
||||
field: { onBlur: onFieldBlur, onChange: onFieldChange, value, ...field },
|
||||
form: { touched, errors, setFieldValue },
|
||||
onChange,
|
||||
...props
|
||||
}: ColorInputToInputProps): ColorInputProps {
|
||||
const fieldError = getIn(errors, field.name);
|
||||
const showError = getIn(touched, field.name) && !!fieldError;
|
||||
|
||||
return {
|
||||
inputProps: {
|
||||
intent: showError ? Intent.DANGER : Intent.NONE,
|
||||
},
|
||||
value,
|
||||
onChange:
|
||||
onChange ??
|
||||
function (value: string) {
|
||||
setFieldValue(field.name, value);
|
||||
},
|
||||
...field,
|
||||
...props,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms field props to input group props for ColorInput.
|
||||
* @param {ColorInputToInputProps} props -
|
||||
* @returns {JSX.Element}
|
||||
*/
|
||||
function ColorInputToInputGroup({
|
||||
...props
|
||||
}: ColorInputToInputProps): JSX.Element {
|
||||
return <ColorInput {...fieldToColorInputInputGroup(props)} />;
|
||||
}
|
||||
|
||||
/**
|
||||
* Input group Blueprint component binded with Formik for ColorInput.
|
||||
* @param {ColorInputInputGroupProps}
|
||||
* @returns {JSX.Element}
|
||||
*/
|
||||
export function FColorInput({
|
||||
...props
|
||||
}: ColorInputInputGroupProps): JSX.Element {
|
||||
return <Field {...props} component={ColorInputToInputGroup} />;
|
||||
}
|
||||
@@ -6,16 +6,14 @@ import styled from 'styled-components';
|
||||
import clsx from 'classnames';
|
||||
|
||||
export function FSelect({ ...props }) {
|
||||
const input = ({ activeItem, text, label, value }) => {
|
||||
return (
|
||||
<SelectButton
|
||||
text={text || props.placeholder || 'Select an item ...'}
|
||||
disabled={props.disabled || false}
|
||||
{...props.buttonProps}
|
||||
className={clsx({ 'is-selected': !!text }, props.className)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
const input = ({ activeItem, text, label, value }) => (
|
||||
<SelectButton
|
||||
text={text || props.placeholder || 'Select an item ...'}
|
||||
disabled={props.disabled || false}
|
||||
{...props.buttonProps}
|
||||
className={clsx({ 'is-selected': !!text }, props.className)}
|
||||
/>
|
||||
);
|
||||
return <Select input={input} fill={true} {...props} />;
|
||||
}
|
||||
|
||||
|
||||
@@ -24,5 +24,12 @@ export enum DRAWERS {
|
||||
WAREHOUSE_TRANSFER_DETAILS = 'warehouse-transfer-detail-drawer',
|
||||
TAX_RATE_DETAILS = 'tax-rate-detail-drawer',
|
||||
CATEGORIZE_TRANSACTION = 'categorize-transaction',
|
||||
CHANGE_SUBSCARIPTION_PLAN = 'change-subscription-plan'
|
||||
CHANGE_SUBSCARIPTION_PLAN = 'change-subscription-plan',
|
||||
INVOICE_CUSTOMIZE = 'INVOICE_CUSTOMIZE',
|
||||
ESTIMATE_CUSTOMIZE = 'ESTIMATE_CUSTOMIZE',
|
||||
PAYMENT_RECEIPT_CUSTOMIZE = 'PAYMENT_RECEIPT_CUSTOMIZE',
|
||||
RECEIPT_CUSTOMIZE = 'RECEIPT_CUSTOMIZE',
|
||||
CREDIT_NOTE_CUSTOMIZE = 'CREDIT_NOTE_CUSTOMIZE',
|
||||
PAYMENT_RECEIVED_CUSTOMIZE = 'PAYMENT_RECEIVED_CUSTOMIZE',
|
||||
BRANDING_TEMPLATES = 'BRANDING_TEMPLATES'
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ import { CashflowAlerts } from '../CashFlow/CashflowAlerts';
|
||||
import { BankRulesAlerts } from '../Banking/Rules/RulesList/BankRulesAlerts';
|
||||
import { SubscriptionAlerts } from '../Subscriptions/alerts/alerts';
|
||||
import { BankAccountAlerts } from '@/containers/CashFlow/AccountTransactions/alerts';
|
||||
import { BrandingTemplatesAlerts } from '../BrandingTemplates/alerts/BrandingTemplatesAlerts';
|
||||
|
||||
export default [
|
||||
...AccountsAlerts,
|
||||
@@ -61,4 +62,5 @@ export default [
|
||||
...BankRulesAlerts,
|
||||
...SubscriptionAlerts,
|
||||
...BankAccountAlerts,
|
||||
...BrandingTemplatesAlerts,
|
||||
];
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
|
||||
|
||||
.table {
|
||||
:global {
|
||||
.table .tbody .tr .td{
|
||||
padding-top: 14px;
|
||||
padding-bottom: 14px;
|
||||
}
|
||||
|
||||
.table .thead .th{
|
||||
text-transform: uppercase;
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import React, { createContext, useContext } from 'react';
|
||||
import {
|
||||
GetPdfTemplateResponse,
|
||||
useGetPdfTemplate,
|
||||
} from '@/hooks/query/pdf-templates';
|
||||
import { Spinner } from '@blueprintjs/core';
|
||||
|
||||
interface PdfTemplateContextValue {
|
||||
templateId: number | string;
|
||||
pdfTemplate: GetPdfTemplateResponse | undefined;
|
||||
isPdfTemplateLoading: boolean;
|
||||
}
|
||||
|
||||
interface BrandingTemplateProps {
|
||||
templateId: number;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const PdfTemplateContext = createContext<PdfTemplateContextValue>(
|
||||
{} as PdfTemplateContextValue,
|
||||
);
|
||||
|
||||
export const BrandingTemplateBoot = ({
|
||||
templateId,
|
||||
children,
|
||||
}: BrandingTemplateProps) => {
|
||||
const { data: pdfTemplate, isLoading: isPdfTemplateLoading } =
|
||||
useGetPdfTemplate(templateId, {
|
||||
enabled: !!templateId,
|
||||
});
|
||||
|
||||
const value = {
|
||||
templateId,
|
||||
pdfTemplate,
|
||||
isPdfTemplateLoading,
|
||||
};
|
||||
|
||||
if (isPdfTemplateLoading) {
|
||||
return <Spinner size={20} />
|
||||
}
|
||||
return (
|
||||
<PdfTemplateContext.Provider value={value}>
|
||||
{children}
|
||||
</PdfTemplateContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useBrandingTemplateBoot = () => {
|
||||
return useContext<PdfTemplateContextValue>(PdfTemplateContext);
|
||||
};
|
||||
@@ -0,0 +1,87 @@
|
||||
import * as Yup from 'yup';
|
||||
import {
|
||||
ElementCustomize,
|
||||
ElementCustomizeProps,
|
||||
} from '../ElementCustomize/ElementCustomize';
|
||||
import {
|
||||
transformToEditRequest,
|
||||
transformToNewRequest,
|
||||
useBrandingTemplateFormInitialValues,
|
||||
} from './_utils';
|
||||
import { AppToaster } from '@/components';
|
||||
import { Intent } from '@blueprintjs/core';
|
||||
import {
|
||||
useCreatePdfTemplate,
|
||||
useEditPdfTemplate,
|
||||
} from '@/hooks/query/pdf-templates';
|
||||
import { FormikHelpers } from 'formik';
|
||||
import { BrandingTemplateValues } from './types';
|
||||
|
||||
interface BrandingTemplateFormProps<T> extends ElementCustomizeProps<T> {
|
||||
resource: string;
|
||||
templateId?: number;
|
||||
onSuccess?: () => void;
|
||||
onError?: () => void;
|
||||
defaultValues?: T;
|
||||
}
|
||||
|
||||
export function BrandingTemplateForm<T extends BrandingTemplateValues>({
|
||||
templateId,
|
||||
onSuccess,
|
||||
onError,
|
||||
defaultValues,
|
||||
resource,
|
||||
...props
|
||||
}: BrandingTemplateFormProps<T>) {
|
||||
const { mutateAsync: createPdfTemplate } = useCreatePdfTemplate();
|
||||
const { mutateAsync: editPdfTemplate } = useEditPdfTemplate();
|
||||
|
||||
const initialValues = useBrandingTemplateFormInitialValues<T>(defaultValues);
|
||||
|
||||
const handleFormSubmit = (values: T, { setSubmitting }: FormikHelpers<T>) => {
|
||||
const handleSuccess = (message: string) => {
|
||||
AppToaster.show({ intent: Intent.SUCCESS, message });
|
||||
setSubmitting(false);
|
||||
onSuccess && onSuccess();
|
||||
};
|
||||
const handleError = (message: string) => {
|
||||
AppToaster.show({ intent: Intent.DANGER, message });
|
||||
setSubmitting(false);
|
||||
onError && onError();
|
||||
};
|
||||
if (templateId) {
|
||||
const reqValues = transformToEditRequest(values);
|
||||
setSubmitting(true);
|
||||
|
||||
// Edit existing template
|
||||
editPdfTemplate({ templateId, values: reqValues })
|
||||
.then(() => handleSuccess('PDF template updated successfully!'))
|
||||
.catch(() =>
|
||||
handleError('An error occurred while updating the PDF template.'),
|
||||
);
|
||||
} else {
|
||||
const reqValues = transformToNewRequest(values, resource);
|
||||
setSubmitting(true);
|
||||
|
||||
// Create new template
|
||||
createPdfTemplate(reqValues)
|
||||
.then(() => handleSuccess('PDF template created successfully!'))
|
||||
.catch(() =>
|
||||
handleError('An error occurred while creating the PDF template.'),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<ElementCustomize<T>
|
||||
initialValues={initialValues}
|
||||
validationSchema={validationSchema}
|
||||
onSubmit={handleFormSubmit}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export const validationSchema = Yup.object().shape({
|
||||
templateName: Yup.string().required('Template Name is required'),
|
||||
});
|
||||
@@ -0,0 +1,45 @@
|
||||
// @ts-nocheck
|
||||
import React, { useMemo } from 'react';
|
||||
import { Button, NavbarGroup, Intent } from '@blueprintjs/core';
|
||||
import { DashboardActionsBar, Icon } from '@/components';
|
||||
import withDrawerActions from '@/containers/Drawer/withDrawerActions';
|
||||
import {
|
||||
getButtonLabelFromResource,
|
||||
getCustomizeDrawerNameFromResource,
|
||||
} from './_utils';
|
||||
import { compose } from '@/utils';
|
||||
import { useDrawerContext } from '@/components/Drawer/DrawerProvider';
|
||||
|
||||
/**
|
||||
* Account drawer action bar.
|
||||
*/
|
||||
function BrandingTemplateActionsBarRoot({ openDrawer }) {
|
||||
const {
|
||||
payload: { resource },
|
||||
} = useDrawerContext();
|
||||
|
||||
// Handle new child button click.
|
||||
const handleCreateBtnClick = () => {
|
||||
const drawerResource = getCustomizeDrawerNameFromResource(resource);
|
||||
openDrawer(drawerResource);
|
||||
};
|
||||
const label = useMemo(() => getButtonLabelFromResource(resource), [resource]);
|
||||
|
||||
return (
|
||||
<DashboardActionsBar>
|
||||
<NavbarGroup>
|
||||
<Button
|
||||
intent={Intent.PRIMARY}
|
||||
icon={<Icon icon="plus" />}
|
||||
onClick={handleCreateBtnClick}
|
||||
minimal
|
||||
>
|
||||
{label}
|
||||
</Button>
|
||||
</NavbarGroup>
|
||||
</DashboardActionsBar>
|
||||
);
|
||||
}
|
||||
export const BrandingTemplateActionsBar = compose(withDrawerActions)(
|
||||
BrandingTemplateActionsBarRoot,
|
||||
);
|
||||
@@ -0,0 +1,36 @@
|
||||
import React, { createContext } from 'react';
|
||||
import { useGetPdfTemplates } from '@/hooks/query/pdf-templates';
|
||||
import { useDrawerContext } from '@/components/Drawer/DrawerProvider';
|
||||
|
||||
interface BrandingTemplatesBootValues {
|
||||
pdfTemplates: any;
|
||||
isPdfTemplatesLoading: boolean;
|
||||
}
|
||||
|
||||
const BrandingTemplatesBootContext = createContext<BrandingTemplatesBootValues>(
|
||||
{} as BrandingTemplatesBootValues,
|
||||
);
|
||||
|
||||
interface BrandingTemplatesBootProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
function BrandingTemplatesBoot({ ...props }: BrandingTemplatesBootProps) {
|
||||
const { payload } = useDrawerContext();
|
||||
const resource = payload?.resource || null;
|
||||
|
||||
const { data: pdfTemplates, isLoading: isPdfTemplatesLoading } =
|
||||
useGetPdfTemplates({ resource });
|
||||
|
||||
const provider = {
|
||||
pdfTemplates,
|
||||
isPdfTemplatesLoading,
|
||||
} as BrandingTemplatesBootValues;
|
||||
|
||||
return <BrandingTemplatesBootContext.Provider value={provider} {...props} />;
|
||||
}
|
||||
|
||||
const useBrandingTemplatesBoot = () =>
|
||||
React.useContext<BrandingTemplatesBootValues>(BrandingTemplatesBootContext);
|
||||
|
||||
export { BrandingTemplatesBoot, useBrandingTemplatesBoot };
|
||||
@@ -0,0 +1,46 @@
|
||||
// @ts-nocheck
|
||||
import * as R from 'ramda';
|
||||
import { Button, Classes, Intent } from '@blueprintjs/core';
|
||||
import { BrandingTemplatesBoot } from './BrandingTemplatesBoot';
|
||||
import { Box, Card, DrawerHeaderContent, Group } from '@/components';
|
||||
import { DRAWERS } from '@/constants/drawers';
|
||||
import { BrandingTemplatesTable } from './BrandingTemplatesTable';
|
||||
import { BrandingTemplateActionsBar } from './BrandingTemplatesActionsBar';
|
||||
import withDrawerActions from '@/containers/Drawer/withDrawerActions';
|
||||
|
||||
export default function BrandingTemplateContent() {
|
||||
return (
|
||||
<Box>
|
||||
<DrawerHeaderContent
|
||||
name={DRAWERS.BRANDING_TEMPLATES}
|
||||
title={'Branding Templates'}
|
||||
/>
|
||||
<Box className={Classes.DRAWER_BODY}>
|
||||
<BrandingTemplatesBoot>
|
||||
<BrandingTemplateActionsBar />
|
||||
|
||||
<Card style={{ padding: 0 }}>
|
||||
<BrandingTemplatesTable />
|
||||
</Card>
|
||||
</BrandingTemplatesBoot>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
const BrandingTemplateHeader = R.compose(withDrawerActions)(
|
||||
({ openDrawer }) => {
|
||||
const handleCreateBtnClick = () => {
|
||||
openDrawer(DRAWERS.INVOICE_CUSTOMIZE);
|
||||
};
|
||||
return (
|
||||
<Group>
|
||||
<Button intent={Intent.PRIMARY} onClick={handleCreateBtnClick}>
|
||||
Create Invoice Branding
|
||||
</Button>
|
||||
</Group>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
BrandingTemplateHeader.displayName = 'BrandingTemplateHeader';
|
||||
@@ -0,0 +1,38 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import * as R from 'ramda';
|
||||
import { Drawer, DrawerSuspense } from '@/components';
|
||||
import withDrawers from '@/containers/Drawer/withDrawers';
|
||||
|
||||
const BrandingTemplatesContent = React.lazy(
|
||||
() => import('./BrandingTemplatesContent'),
|
||||
);
|
||||
|
||||
/**
|
||||
* Invoice customize drawer.
|
||||
* @returns {React.ReactNode}
|
||||
*/
|
||||
function BrandingTemplatesDrawerRoot({
|
||||
name,
|
||||
// #withDrawer
|
||||
isOpen,
|
||||
payload,
|
||||
}) {
|
||||
return (
|
||||
<Drawer
|
||||
isOpen={isOpen}
|
||||
name={name}
|
||||
payload={payload}
|
||||
size={'600px'}
|
||||
style={{ borderLeftColor: '#cbcbcb' }}
|
||||
>
|
||||
<DrawerSuspense>
|
||||
<BrandingTemplatesContent />
|
||||
</DrawerSuspense>
|
||||
</Drawer>
|
||||
);
|
||||
}
|
||||
|
||||
export const BrandingTemplatesDrawer = R.compose(withDrawers())(
|
||||
BrandingTemplatesDrawerRoot,
|
||||
);
|
||||
@@ -0,0 +1,46 @@
|
||||
import { Button } from '@blueprintjs/core';
|
||||
import styled from 'styled-components';
|
||||
import { FFormGroup } from '@/components';
|
||||
|
||||
export const BrandingThemeFormGroup = styled(FFormGroup)`
|
||||
margin-bottom: 0;
|
||||
|
||||
.bp4-label {
|
||||
color: #7a8492;
|
||||
}
|
||||
|
||||
&.bp4-inline label.bp4-label {
|
||||
margin-right: 0;
|
||||
}
|
||||
`;
|
||||
|
||||
export const BrandingThemeSelectButton = styled(Button)`
|
||||
position: relative;
|
||||
padding-right: 26px;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
display: inline-block;
|
||||
width: 0;
|
||||
height: 0;
|
||||
|
||||
border-left: 4px solid transparent;
|
||||
border-right: 4px solid transparent;
|
||||
border-top: 5px solid #98a1ae;
|
||||
|
||||
position: absolute;
|
||||
right: -2px;
|
||||
top: 50%;
|
||||
margin-top: -2px;
|
||||
margin-right: 12px;
|
||||
border-radius: 1px;
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
export const convertBrandingTemplatesToOptions = (brandingTemplates: Array<any>) => {
|
||||
return brandingTemplates?.map(
|
||||
(template) =>
|
||||
({ text: template.template_name, value: template.id } || []),
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
// @ts-nocheck
|
||||
import * as R from 'ramda';
|
||||
import { DataTable, TableSkeletonRows } from '@/components';
|
||||
import { useBrandingTemplatesBoot } from './BrandingTemplatesBoot';
|
||||
import { ActionsMenu } from './_components';
|
||||
import { DRAWERS } from '@/constants/drawers';
|
||||
import withAlertActions from '@/containers/Alert/withAlertActions';
|
||||
import withDrawerActions from '@/containers/Drawer/withDrawerActions';
|
||||
import { getCustomizeDrawerNameFromResource } from './_utils';
|
||||
import { useBrandingTemplatesColumns } from './_hooks';
|
||||
import styles from './BrandTemplates.module.scss';
|
||||
|
||||
interface BrandingTemplatesTableProps {}
|
||||
|
||||
function BrandingTemplateTableRoot({
|
||||
openAlert,
|
||||
openDrawer,
|
||||
}: BrandingTemplatesTableProps) {
|
||||
// Table columns.
|
||||
const columns = useBrandingTemplatesColumns();
|
||||
const { isPdfTemplatesLoading, pdfTemplates } = useBrandingTemplatesBoot();
|
||||
|
||||
const handleEditTemplate = (template) => {
|
||||
openDrawer(DRAWERS.INVOICE_CUSTOMIZE, {
|
||||
templateId: template.id,
|
||||
resource: template.resource,
|
||||
});
|
||||
};
|
||||
|
||||
const handleDeleteTemplate = (template) => {
|
||||
openAlert('branding-template-delete', { templateId: template.id });
|
||||
};
|
||||
|
||||
const handleCellClick = (cell, event) => {
|
||||
const templateId = cell.row.original.id;
|
||||
const resource = cell.row.original.resource;
|
||||
|
||||
// Retrieves the customize drawer name from the given resource name.
|
||||
const drawerName = getCustomizeDrawerNameFromResource(resource);
|
||||
|
||||
openDrawer(drawerName, { templateId, resource });
|
||||
};
|
||||
|
||||
// Handle mark as default button click.
|
||||
const handleMarkDefaultTemplate = (template) => {
|
||||
openAlert('branding-template-mark-default', { templateId: template.id });
|
||||
};
|
||||
|
||||
return (
|
||||
<DataTable
|
||||
columns={columns}
|
||||
data={pdfTemplates || []}
|
||||
loading={isPdfTemplatesLoading}
|
||||
progressBarLoading={isPdfTemplatesLoading}
|
||||
TableLoadingRenderer={TableSkeletonRows}
|
||||
ContextMenu={ActionsMenu}
|
||||
noInitialFetch={true}
|
||||
payload={{
|
||||
onDeleteTemplate: handleDeleteTemplate,
|
||||
onEditTemplate: handleEditTemplate,
|
||||
onMarkDefaultTemplate: handleMarkDefaultTemplate,
|
||||
}}
|
||||
rowContextMenu={ActionsMenu}
|
||||
onCellClick={handleCellClick}
|
||||
className={styles.table}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export const BrandingTemplatesTable = R.compose(
|
||||
withAlertActions,
|
||||
withDrawerActions,
|
||||
)(BrandingTemplateTableRoot);
|
||||
@@ -0,0 +1,35 @@
|
||||
// @ts-nocheck
|
||||
import { safeCallback } from '@/utils';
|
||||
import { Intent, Menu, MenuDivider, MenuItem } from '@blueprintjs/core';
|
||||
|
||||
/**
|
||||
* Templates table actions menu.
|
||||
*/
|
||||
export function ActionsMenu({
|
||||
row: { original },
|
||||
payload: { onDeleteTemplate, onEditTemplate, onMarkDefaultTemplate },
|
||||
}) {
|
||||
return (
|
||||
<Menu>
|
||||
{!original.default && (
|
||||
<>
|
||||
<MenuItem
|
||||
text={'Mark as Default'}
|
||||
onClick={safeCallback(onMarkDefaultTemplate, original)}
|
||||
/>
|
||||
<MenuDivider />
|
||||
</>
|
||||
)}
|
||||
<MenuItem
|
||||
text={'Edit Template'}
|
||||
onClick={safeCallback(onEditTemplate, original)}
|
||||
/>
|
||||
<MenuDivider />
|
||||
<MenuItem
|
||||
text={'Delete Template'}
|
||||
intent={Intent.DANGER}
|
||||
onClick={safeCallback(onDeleteTemplate, original)}
|
||||
/>
|
||||
</Menu>
|
||||
);
|
||||
}
|
||||
25
packages/webapp/src/containers/BrandingTemplates/_hooks.tsx
Normal file
25
packages/webapp/src/containers/BrandingTemplates/_hooks.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import clsx from 'classnames';
|
||||
import { Classes, Tag } from '@blueprintjs/core';
|
||||
import { Group } from '@/components';
|
||||
|
||||
export const useBrandingTemplatesColumns = () => {
|
||||
return [
|
||||
{
|
||||
Header: 'Template Name',
|
||||
accessor: (row: any) => (
|
||||
<Group spacing={10}>
|
||||
{row.template_name} {row.default && <Tag round>Default</Tag>}
|
||||
</Group>
|
||||
),
|
||||
width: 65,
|
||||
clickable: true,
|
||||
},
|
||||
{
|
||||
Header: 'Created At',
|
||||
accessor: 'created_at_formatted',
|
||||
width: 35,
|
||||
className: clsx(Classes.TEXT_MUTED),
|
||||
clickable: true,
|
||||
},
|
||||
];
|
||||
};
|
||||
70
packages/webapp/src/containers/BrandingTemplates/_utils.ts
Normal file
70
packages/webapp/src/containers/BrandingTemplates/_utils.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import { omit } from 'lodash';
|
||||
import * as R from 'ramda';
|
||||
import {
|
||||
CreatePdfTemplateValues,
|
||||
EditPdfTemplateValues,
|
||||
} from '@/hooks/query/pdf-templates';
|
||||
import { useBrandingTemplateBoot } from './BrandingTemplateBoot';
|
||||
import { transformToForm } from '@/utils';
|
||||
import { BrandingTemplateValues } from './types';
|
||||
import { useFormikContext } from 'formik';
|
||||
import { DRAWERS } from '@/constants/drawers';
|
||||
|
||||
export const transformToEditRequest = <T extends BrandingTemplateValues>(
|
||||
values: T,
|
||||
): EditPdfTemplateValues => {
|
||||
return {
|
||||
templateName: values.templateName,
|
||||
attributes: omit(values, ['templateName']),
|
||||
};
|
||||
};
|
||||
|
||||
export const transformToNewRequest = <T extends BrandingTemplateValues>(
|
||||
values: T,
|
||||
resource: string,
|
||||
): CreatePdfTemplateValues => {
|
||||
return {
|
||||
resource,
|
||||
templateName: values.templateName,
|
||||
attributes: omit(values, ['templateName']),
|
||||
};
|
||||
};
|
||||
|
||||
export const useBrandingTemplateFormInitialValues = <
|
||||
T extends BrandingTemplateValues,
|
||||
>(
|
||||
initialValues = {},
|
||||
) => {
|
||||
const { pdfTemplate } = useBrandingTemplateBoot();
|
||||
|
||||
const defaultPdfTemplate = {
|
||||
templateName: pdfTemplate?.templateName,
|
||||
...pdfTemplate?.attributes,
|
||||
};
|
||||
return {
|
||||
...initialValues,
|
||||
...(transformToForm(defaultPdfTemplate, initialValues) as T),
|
||||
};
|
||||
};
|
||||
|
||||
export const getCustomizeDrawerNameFromResource = (resource: string) => {
|
||||
const pairs = {
|
||||
SaleInvoice: DRAWERS.INVOICE_CUSTOMIZE,
|
||||
SaleEstimate: DRAWERS.ESTIMATE_CUSTOMIZE,
|
||||
SaleReceipt: DRAWERS.RECEIPT_CUSTOMIZE,
|
||||
CreditNote: DRAWERS.CREDIT_NOTE_CUSTOMIZE,
|
||||
PaymentReceive: DRAWERS.PAYMENT_RECEIVED_CUSTOMIZE,
|
||||
};
|
||||
return R.prop(resource, pairs) || DRAWERS.INVOICE_CUSTOMIZE;
|
||||
};
|
||||
|
||||
export const getButtonLabelFromResource = (resource: string) => {
|
||||
const pairs = {
|
||||
SaleInvoice: 'Create Invoice Branding',
|
||||
SaleEstimate: 'Create Estimate Branding',
|
||||
SaleReceipt: 'Create Receipt Branding',
|
||||
CreditNote: 'Create Credit Note Branding',
|
||||
PaymentReceive: 'Create Payment Branding',
|
||||
};
|
||||
return R.prop(resource, pairs) || 'Create Branding Template';
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
|
||||
const DeleteBrandingTemplateAlert = React.lazy(
|
||||
() => import('./DeleteBrandingTemplateAlert'),
|
||||
);
|
||||
|
||||
const MarkDefaultBrandingTemplateAlert = React.lazy(
|
||||
() => import('./MarkDefaultBrandingTemplateAlert'),
|
||||
);
|
||||
|
||||
export const BrandingTemplatesAlerts = [
|
||||
{ name: 'branding-template-delete', component: DeleteBrandingTemplateAlert },
|
||||
{
|
||||
name: 'branding-template-mark-default',
|
||||
component: MarkDefaultBrandingTemplateAlert,
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,85 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import intl from 'react-intl-universal';
|
||||
import { AppToaster } from '@/components';
|
||||
import { Alert, Intent } from '@blueprintjs/core';
|
||||
import { useDeletePdfTemplate } from '@/hooks/query/pdf-templates';
|
||||
|
||||
import withAlertStoreConnect from '@/containers/Alert/withAlertStoreConnect';
|
||||
import withAlertActions from '@/containers/Alert/withAlertActions';
|
||||
|
||||
import { compose } from '@/utils';
|
||||
|
||||
/**
|
||||
* Delete branding template alert.
|
||||
*/
|
||||
function DeleteBrandingTemplateAlert({
|
||||
// #ownProps
|
||||
name,
|
||||
|
||||
// #withAlertStoreConnect
|
||||
isOpen,
|
||||
payload: { templateId },
|
||||
|
||||
// #withAlertActions
|
||||
closeAlert,
|
||||
}) {
|
||||
const { mutateAsync: deleteBrandingTemplateMutate } = useDeletePdfTemplate();
|
||||
|
||||
const handleConfirmDelete = () => {
|
||||
deleteBrandingTemplateMutate({ templateId })
|
||||
.then(() => {
|
||||
AppToaster.show({
|
||||
message: 'The branding template has been deleted successfully.',
|
||||
intent: Intent.SUCCESS,
|
||||
});
|
||||
closeAlert(name);
|
||||
})
|
||||
.catch(
|
||||
({
|
||||
response: {
|
||||
data: { errors },
|
||||
},
|
||||
}) => {
|
||||
if (
|
||||
errors.find(
|
||||
(error) => error.type === 'CANNOT_DELETE_PREDEFINED_PDF_TEMPLATE',
|
||||
)
|
||||
) {
|
||||
AppToaster.show({
|
||||
message: 'Cannot delete a predefined branding template.',
|
||||
intent: Intent.DANGER,
|
||||
});
|
||||
} else {
|
||||
AppToaster.show({
|
||||
message: 'Something went wrong.',
|
||||
intent: Intent.DANGER,
|
||||
});
|
||||
}
|
||||
closeAlert(name);
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
closeAlert(name);
|
||||
};
|
||||
|
||||
return (
|
||||
<Alert
|
||||
cancelButtonText={intl.get('cancel')}
|
||||
confirmButtonText={intl.get('delete')}
|
||||
intent={Intent.DANGER}
|
||||
isOpen={isOpen}
|
||||
onCancel={handleCancel}
|
||||
onConfirm={handleConfirmDelete}
|
||||
>
|
||||
<p>Are you sure want to delete branding template?</p>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
export default compose(
|
||||
withAlertStoreConnect(),
|
||||
withAlertActions,
|
||||
)(DeleteBrandingTemplateAlert);
|
||||
@@ -0,0 +1,72 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import intl from 'react-intl-universal';
|
||||
import { AppToaster } from '@/components';
|
||||
import { Alert, Intent } from '@blueprintjs/core';
|
||||
import { useAssignPdfTemplateAsDefault } from '@/hooks/query/pdf-templates';
|
||||
|
||||
import withAlertStoreConnect from '@/containers/Alert/withAlertStoreConnect';
|
||||
import withAlertActions from '@/containers/Alert/withAlertActions';
|
||||
|
||||
import { compose } from '@/utils';
|
||||
|
||||
/**
|
||||
* Mark default branding template alert.
|
||||
*/
|
||||
function MarkDefaultBrandingTemplateAlert({
|
||||
// #ownProps
|
||||
name,
|
||||
|
||||
// #withAlertStoreConnect
|
||||
isOpen,
|
||||
payload: { templateId },
|
||||
|
||||
// #withAlertActions
|
||||
closeAlert,
|
||||
}) {
|
||||
const { mutateAsync: assignPdfTemplateAsDefault } =
|
||||
useAssignPdfTemplateAsDefault();
|
||||
|
||||
const handleConfirmDelete = () => {
|
||||
assignPdfTemplateAsDefault({ templateId })
|
||||
.then(() => {
|
||||
AppToaster.show({
|
||||
message:
|
||||
'The branding template has been marked as a default template.',
|
||||
intent: Intent.SUCCESS,
|
||||
});
|
||||
closeAlert(name);
|
||||
})
|
||||
.catch((error) => {
|
||||
AppToaster.show({
|
||||
message: 'Something went wrong.',
|
||||
intent: Intent.DANGER,
|
||||
});
|
||||
closeAlert(name);
|
||||
});
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
closeAlert(name);
|
||||
};
|
||||
|
||||
return (
|
||||
<Alert
|
||||
cancelButtonText={intl.get('cancel')}
|
||||
confirmButtonText={'Mark as Default'}
|
||||
intent={Intent.WARNING}
|
||||
isOpen={isOpen}
|
||||
onCancel={handleCancel}
|
||||
onConfirm={handleConfirmDelete}
|
||||
>
|
||||
<p>
|
||||
Are you sure want to mark the given branding template as a default template?
|
||||
</p>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
export default compose(
|
||||
withAlertStoreConnect(),
|
||||
withAlertActions,
|
||||
)(MarkDefaultBrandingTemplateAlert);
|
||||
@@ -0,0 +1,5 @@
|
||||
|
||||
|
||||
export interface BrandingTemplateValues {
|
||||
templateName: string;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import { useFormikContext } from 'formik';
|
||||
import { BrandingTemplateValues } from './types';
|
||||
|
||||
export const useIsTemplateNamedFilled = () => {
|
||||
const { values } = useFormikContext<BrandingTemplateValues>();
|
||||
|
||||
return values.templateName && values.templateName?.length >= 4;
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
.root {
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.mainFields{
|
||||
width: 400px;
|
||||
height: 100vh;
|
||||
}
|
||||
.fieldGroup {
|
||||
|
||||
:global .bp4-form-content{
|
||||
margin-left: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.footerActions{
|
||||
padding: 10px 16px;
|
||||
border-top: 1px solid #d9d9d9;
|
||||
flex-flow: row-reverse;
|
||||
}
|
||||
|
||||
.showCompanyLogoField:global(.bp4-large){
|
||||
font-size: 14px;
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import React from 'react';
|
||||
import { Box, Group } from '@/components';
|
||||
import { ElementCustomizeProvider } from './ElementCustomizeProvider';
|
||||
import {
|
||||
ElementCustomizeForm,
|
||||
ElementCustomizeFormProps,
|
||||
} from './ElementCustomizerForm';
|
||||
import { ElementCustomizeTabsControllerProvider } from './ElementCustomizeTabsController';
|
||||
import { ElementCustomizeFields } from './ElementCustomizeFields';
|
||||
import { ElementCustomizePreview } from './ElementCustomizePreview';
|
||||
import { extractChildren } from '@/utils/extract-children';
|
||||
|
||||
export interface ElementCustomizeProps<T> extends ElementCustomizeFormProps<T> {
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
export function ElementCustomize<T>({
|
||||
initialValues,
|
||||
validationSchema,
|
||||
onSubmit,
|
||||
children,
|
||||
}: ElementCustomizeProps<T>) {
|
||||
const PaperTemplate = React.useMemo(
|
||||
() => extractChildren(children, ElementCustomize.PaperTemplate),
|
||||
[children],
|
||||
);
|
||||
const CustomizeTabs = React.useMemo(
|
||||
() => extractChildren(children, ElementCustomize.FieldsTab),
|
||||
[children],
|
||||
);
|
||||
|
||||
const value = { PaperTemplate, CustomizeTabs };
|
||||
|
||||
return (
|
||||
<ElementCustomizeForm
|
||||
initialValues={initialValues}
|
||||
validationSchema={validationSchema}
|
||||
onSubmit={onSubmit}
|
||||
>
|
||||
<ElementCustomizeTabsControllerProvider>
|
||||
<ElementCustomizeProvider value={value}>
|
||||
<Group spacing={0} align="stretch">
|
||||
<ElementCustomizeFields />
|
||||
<ElementCustomizePreview />
|
||||
</Group>
|
||||
</ElementCustomizeProvider>
|
||||
</ElementCustomizeTabsControllerProvider>
|
||||
</ElementCustomizeForm>
|
||||
);
|
||||
}
|
||||
|
||||
export interface ElementCustomizePaperTemplateProps {
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
ElementCustomize.PaperTemplate = ({
|
||||
children,
|
||||
}: ElementCustomizePaperTemplateProps) => {
|
||||
return <>{children}</>;
|
||||
};
|
||||
|
||||
export interface ElementCustomizeContentProps {
|
||||
id: string;
|
||||
label: string;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
ElementCustomize.FieldsTab = ({
|
||||
id,
|
||||
label,
|
||||
children,
|
||||
}: ElementCustomizeContentProps) => {
|
||||
return <>{children}</>;
|
||||
};
|
||||
@@ -0,0 +1,77 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import * as R from 'ramda';
|
||||
import { Button, Intent } from '@blueprintjs/core';
|
||||
import { useFormikContext } from 'formik';
|
||||
import { Box, Group, Stack } from '@/components';
|
||||
import { ElementCustomizeHeader } from './ElementCustomizeHeader';
|
||||
import { ElementCustomizeTabs } from './ElementCustomizeTabs';
|
||||
import { useElementCustomizeTabsController } from './ElementCustomizeTabsController';
|
||||
import { useDrawerContext } from '@/components/Drawer/DrawerProvider';
|
||||
import { useElementCustomizeContext } from './ElementCustomizeProvider';
|
||||
import withDrawerActions from '@/containers/Drawer/withDrawerActions';
|
||||
import styles from './ElementCustomize.module.scss';
|
||||
|
||||
export function ElementCustomizeFields() {
|
||||
return (
|
||||
<Group spacing={0} align={'stretch'} className={styles.root}>
|
||||
<ElementCustomizeTabs />
|
||||
<ElementCustomizeFieldsMain />
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
|
||||
export function ElementCustomizeFieldsMain() {
|
||||
const { currentTabId } = useElementCustomizeTabsController();
|
||||
const { CustomizeTabs } = useElementCustomizeContext();
|
||||
|
||||
const CustomizeTabPanel = React.useMemo(
|
||||
() =>
|
||||
React.Children.map(CustomizeTabs, (tab) => {
|
||||
return tab.props.id === currentTabId ? tab : null;
|
||||
}).filter(Boolean),
|
||||
[CustomizeTabs, currentTabId],
|
||||
);
|
||||
|
||||
return (
|
||||
<Stack spacing={0} className={styles.mainFields}>
|
||||
<ElementCustomizeHeader label={'Customize'} />
|
||||
|
||||
<Stack spacing={0} style={{ flex: '1 1 auto', overflow: 'auto' }}>
|
||||
<Box style={{ flex: '1 1' }}>{CustomizeTabPanel}</Box>
|
||||
<ElementCustomizeFooterActions />
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
function ElementCustomizeFooterActionsRoot({ closeDrawer }) {
|
||||
const { name } = useDrawerContext();
|
||||
const { submitForm, isSubmitting } = useFormikContext();
|
||||
|
||||
const handleSubmitBtnClick = () => {
|
||||
submitForm();
|
||||
};
|
||||
const handleCancelBtnClick = () => {
|
||||
closeDrawer(name);
|
||||
};
|
||||
|
||||
return (
|
||||
<Group spacing={10} className={styles.footerActions}>
|
||||
<Button
|
||||
onClick={handleSubmitBtnClick}
|
||||
intent={Intent.PRIMARY}
|
||||
style={{ minWidth: 75 }}
|
||||
loading={isSubmitting}
|
||||
type={'submit'}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
<Button onClick={handleCancelBtnClick}>Cancel</Button>
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
|
||||
const ElementCustomizeFooterActions = R.compose(withDrawerActions)(
|
||||
ElementCustomizeFooterActionsRoot,
|
||||
);
|
||||
@@ -0,0 +1,40 @@
|
||||
// @ts-nocheck
|
||||
import { InputGroupProps, SwitchProps } from '@blueprintjs/core';
|
||||
import { FInputGroup, FSwitch, Group, Stack } from '@/components';
|
||||
import { CLASSES } from '@/constants';
|
||||
|
||||
export function ElementCustomizeFieldsGroup({
|
||||
label,
|
||||
children,
|
||||
}: {
|
||||
label: string;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<Stack spacing={20}>
|
||||
<h4 className={CLASSES.TEXT_MUTED} style={{ fontWeight: 600 }}>
|
||||
{label}
|
||||
</h4>
|
||||
|
||||
<Stack spacing={14}>{children}</Stack>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
export function ElementCustomizeContentItemFieldGroup({
|
||||
inputGroupProps,
|
||||
switchProps,
|
||||
}: {
|
||||
inputGroupProps: InputGroupProps;
|
||||
switchProps?: SwitchProps;
|
||||
}) {
|
||||
return (
|
||||
<Group spacing={14} position={'apart'}>
|
||||
<FSwitch {...inputGroupProps} fastField />
|
||||
|
||||
{switchProps?.name && (
|
||||
<FInputGroup {...switchProps} style={{ maxWidth: 150 }} fastField />
|
||||
)}
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
|
||||
.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: 55px;
|
||||
padding: 5px 5px 5px 20px;
|
||||
position: relative;
|
||||
background-color: #fff;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.title{
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
font-weight: 500;
|
||||
color: #666;
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { Button, Classes } from '@blueprintjs/core';
|
||||
import { Group, Icon } from '@/components';
|
||||
import styles from './ElementCustomizeHeader.module.scss';
|
||||
|
||||
interface ElementCustomizeHeaderProps {
|
||||
label?: string;
|
||||
children?: React.ReactNode;
|
||||
closeButton?: boolean;
|
||||
onClose?: () => void;
|
||||
}
|
||||
|
||||
export function ElementCustomizeHeader({
|
||||
label,
|
||||
closeButton,
|
||||
onClose,
|
||||
children,
|
||||
}: ElementCustomizeHeaderProps) {
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
// @ts-nocheck
|
||||
import * as R from 'ramda';
|
||||
import { Stack } from '@/components';
|
||||
import { ElementCustomizeHeader } from './ElementCustomizeHeader';
|
||||
import { ElementCustomizePreviewContent } from './ElementCustomizePreviewContent';
|
||||
import { useDrawerContext } from '@/components/Drawer/DrawerProvider';
|
||||
import withDrawerActions from '@/containers/Drawer/withDrawerActions';
|
||||
|
||||
function ElementCustomizePreviewRoot({ closeDrawer }) {
|
||||
const { name } = useDrawerContext();
|
||||
|
||||
const handleCloseBtnClick = () => {
|
||||
closeDrawer(name);
|
||||
};
|
||||
return (
|
||||
<Stack
|
||||
spacing={0}
|
||||
style={{ borderLeft: '1px solid #D9D9D9', height: '100vh', flex: '1 1' }}
|
||||
>
|
||||
<ElementCustomizeHeader
|
||||
label={'Preview'}
|
||||
closeButton
|
||||
onClose={handleCloseBtnClick}
|
||||
/>
|
||||
<ElementCustomizePreviewContent />
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
export const ElementCustomizePreview = R.compose(withDrawerActions)(
|
||||
ElementCustomizePreviewRoot,
|
||||
);
|
||||
@@ -0,0 +1,19 @@
|
||||
import { Box } from '@/components';
|
||||
import { useElementCustomizeContext } from './ElementCustomizeProvider';
|
||||
|
||||
export function ElementCustomizePreviewContent() {
|
||||
const { PaperTemplate } = useElementCustomizeContext();
|
||||
|
||||
return (
|
||||
<Box
|
||||
style={{
|
||||
padding: '28px 24px 40px',
|
||||
backgroundColor: '#F5F5F5',
|
||||
overflow: 'auto',
|
||||
flex: '1',
|
||||
}}
|
||||
>
|
||||
{PaperTemplate}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import React, { createContext, useContext } from 'react';
|
||||
|
||||
interface ElementCustomizeValue {
|
||||
PaperTemplate?: React.ReactNode;
|
||||
CustomizeTabs: React.ReactNode;
|
||||
}
|
||||
|
||||
const ElementCustomizeContext = createContext<ElementCustomizeValue>(
|
||||
{} as ElementCustomizeValue,
|
||||
);
|
||||
|
||||
export const ElementCustomizeProvider: React.FC<{
|
||||
value: ElementCustomizeValue;
|
||||
children: React.ReactNode;
|
||||
}> = ({ value, children }) => {
|
||||
return (
|
||||
<ElementCustomizeContext.Provider value={{ ...value }}>
|
||||
{children}
|
||||
</ElementCustomizeContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useElementCustomizeContext = (): ElementCustomizeValue => {
|
||||
const context = useContext<ElementCustomizeValue>(ElementCustomizeContext);
|
||||
|
||||
if (!context) {
|
||||
throw new Error(
|
||||
'useElementCustomize must be used within an ElementCustomizeProvider',
|
||||
);
|
||||
}
|
||||
return context;
|
||||
};
|
||||
@@ -0,0 +1,21 @@
|
||||
|
||||
.root {
|
||||
flex: 1;
|
||||
min-width: 165px;
|
||||
max-width: 165px;
|
||||
}
|
||||
|
||||
.content{
|
||||
padding: 5px;
|
||||
flex: 1;
|
||||
border-right: 1px solid #E1E1E1;
|
||||
}
|
||||
|
||||
.tabsList{
|
||||
width: 100%;
|
||||
flex: 1;
|
||||
|
||||
:global .bp4-tab-list{
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import React from 'react';
|
||||
import { Box, Stack } from '@/components';
|
||||
import { Tab, Tabs } from '@blueprintjs/core';
|
||||
import { ElementCustomizeHeader } from './ElementCustomizeHeader';
|
||||
import {
|
||||
ElementCustomizeTabsEnum,
|
||||
useElementCustomizeTabsController,
|
||||
} from './ElementCustomizeTabsController';
|
||||
import { useElementCustomizeContext } from './ElementCustomizeProvider';
|
||||
import styles from './ElementCustomizeTabs.module.scss';
|
||||
|
||||
export function ElementCustomizeTabs() {
|
||||
const { setCurrentTabId } = useElementCustomizeTabsController();
|
||||
|
||||
const { CustomizeTabs } = useElementCustomizeContext();
|
||||
|
||||
const tabItems = React.Children.map(CustomizeTabs, (node) => ({
|
||||
...(React.isValidElement(node) ? node.props : {}),
|
||||
}));
|
||||
const handleChange = (value: ElementCustomizeTabsEnum) => {
|
||||
setCurrentTabId(value);
|
||||
};
|
||||
return (
|
||||
<Stack spacing={0} className={styles.root}>
|
||||
<ElementCustomizeHeader label={''} />
|
||||
|
||||
<Box className={styles.content}>
|
||||
<Tabs
|
||||
vertical
|
||||
fill
|
||||
large
|
||||
onChange={handleChange}
|
||||
className={styles.tabsList}
|
||||
>
|
||||
{tabItems?.map(({ id, label }: { id: string; label: string }) => (
|
||||
<Tab id={id} key={id} title={label} />
|
||||
))}
|
||||
</Tabs>
|
||||
</Box>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import React, { createContext, useContext, useState } from 'react';
|
||||
|
||||
export enum ElementCustomizeTabsEnum {
|
||||
General = 'general',
|
||||
Items = 'items',
|
||||
Totals = 'totals'
|
||||
}
|
||||
|
||||
const DEFAULT_TAB_ID = ElementCustomizeTabsEnum.General;
|
||||
|
||||
interface ElementCustomizeTabsControllerValue {
|
||||
currentTabId: ElementCustomizeTabsEnum;
|
||||
setCurrentTabId: React.Dispatch<
|
||||
React.SetStateAction<ElementCustomizeTabsEnum>
|
||||
>;
|
||||
}
|
||||
|
||||
const ElementCustomizeTabsController = createContext(
|
||||
{} as ElementCustomizeTabsControllerValue,
|
||||
);
|
||||
|
||||
export const useElementCustomizeTabsController = () => {
|
||||
return useContext(ElementCustomizeTabsController);
|
||||
};
|
||||
|
||||
interface ElementCustomizeTabsControllerProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export const ElementCustomizeTabsControllerProvider = ({
|
||||
children,
|
||||
}: ElementCustomizeTabsControllerProps) => {
|
||||
const [currentTabId, setCurrentTabId] =
|
||||
useState<ElementCustomizeTabsEnum>(DEFAULT_TAB_ID);
|
||||
|
||||
const value = {
|
||||
currentTabId,
|
||||
setCurrentTabId,
|
||||
};
|
||||
|
||||
return (
|
||||
<ElementCustomizeTabsController.Provider value={value}>
|
||||
{children}
|
||||
</ElementCustomizeTabsController.Provider>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,27 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import { Formik, Form, FormikHelpers } from 'formik';
|
||||
|
||||
export interface ElementCustomizeFormProps<T> {
|
||||
initialValues?: T;
|
||||
validationSchema?: any;
|
||||
onSubmit?: (values: T, formikHelpers: FormikHelpers<T>) => void;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
export function ElementCustomizeForm<T>({
|
||||
initialValues,
|
||||
validationSchema,
|
||||
onSubmit,
|
||||
children,
|
||||
}: ElementCustomizeFormProps<T>) {
|
||||
return (
|
||||
<Formik<T>
|
||||
initialValues={{ ...initialValues }}
|
||||
validationSchema={validationSchema}
|
||||
onSubmit={(value, helpers) => onSubmit && onSubmit(value, helpers)}
|
||||
>
|
||||
<Form>{children}</Form>
|
||||
</Formik>
|
||||
);
|
||||
}
|
||||
@@ -8,6 +8,11 @@ import {
|
||||
NavbarGroup,
|
||||
Intent,
|
||||
Alignment,
|
||||
Menu,
|
||||
MenuItem,
|
||||
Popover,
|
||||
PopoverInteractionKind,
|
||||
Position,
|
||||
} from '@blueprintjs/core';
|
||||
import {
|
||||
Icon,
|
||||
@@ -30,9 +35,11 @@ import withSettingsActions from '@/containers/Settings/withSettingsActions';
|
||||
import withVendorsCreditNotes from './withVendorsCreditNotes';
|
||||
import withDialogActions from '@/containers/Dialog/withDialogActions';
|
||||
import withVendorActions from './withVendorActions';
|
||||
import withDrawerActions from '@/containers/Drawer/withDrawerActions';
|
||||
|
||||
import { compose } from '@/utils';
|
||||
import { DialogsName } from '@/constants/dialogs';
|
||||
import { DRAWERS } from '@/constants/drawers';
|
||||
|
||||
/**
|
||||
* Vendors Credit note table actions bar.
|
||||
@@ -54,6 +61,9 @@ function VendorsCreditNoteActionsBar({
|
||||
|
||||
// #withDialogActions
|
||||
openDialog,
|
||||
|
||||
// #withDrawerActions
|
||||
openDrawer,
|
||||
}) {
|
||||
const history = useHistory();
|
||||
|
||||
@@ -92,6 +102,10 @@ function VendorsCreditNoteActionsBar({
|
||||
const handlePrintBtnClick = () => {
|
||||
downloadExportPdf({ resource: 'VendorCredit' });
|
||||
};
|
||||
// Handle the customize button click.
|
||||
const handleCustomizeBtnClick = () => {
|
||||
openDrawer(DRAWERS.CREDIT_NOTE_DETAILS);
|
||||
};
|
||||
|
||||
return (
|
||||
<DashboardActionsBar>
|
||||
@@ -152,6 +166,25 @@ function VendorsCreditNoteActionsBar({
|
||||
<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 Credit Note'}
|
||||
/>
|
||||
</Menu>
|
||||
}
|
||||
>
|
||||
<Button icon={<Icon icon="cog-16" iconSize={16} />} minimal={true} />
|
||||
</Popover>
|
||||
<NavbarDivider />
|
||||
<Button
|
||||
className={Classes.MINIMAL}
|
||||
icon={<Icon icon="refresh-16" iconSize={14} />}
|
||||
@@ -173,4 +206,5 @@ export default compose(
|
||||
creditNoteTableSize: vendorsCreditNoteSetting?.tableSize,
|
||||
})),
|
||||
withDialogActions,
|
||||
withDrawerActions,
|
||||
)(VendorsCreditNoteActionsBar);
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import { useFormikContext } from 'formik';
|
||||
import { ElementCustomize } from '../../../ElementCustomize/ElementCustomize';
|
||||
import { CreditNoteCustomizeGeneralField } from './CreditNoteCustomizeGeneralFields';
|
||||
import { CreditNoteCustomizeContentFields } from './CreditNoteCutomizeContentFields';
|
||||
import { CreditNotePaperTemplate } from './CreditNotePaperTemplate';
|
||||
import { CreditNoteCustomizeValues } from './types';
|
||||
import { initialValues } from './constants';
|
||||
import { BrandingTemplateForm } from '@/containers/BrandingTemplates/BrandingTemplateForm';
|
||||
import { useDrawerActions } from '@/hooks/state';
|
||||
import { useDrawerContext } from '@/components/Drawer/DrawerProvider';
|
||||
|
||||
export function CreditNoteCustomizeContent() {
|
||||
const { payload, name } = useDrawerContext();
|
||||
const { closeDrawer } = useDrawerActions();
|
||||
|
||||
const templateId = payload?.templateId || null;
|
||||
|
||||
const handleSuccess = () => {
|
||||
closeDrawer(name);
|
||||
};
|
||||
|
||||
return (
|
||||
<BrandingTemplateForm<CreditNoteCustomizeValues>
|
||||
resource={'CreditNote'}
|
||||
templateId={templateId}
|
||||
defaultValues={initialValues}
|
||||
onSuccess={handleSuccess}
|
||||
>
|
||||
<ElementCustomize.PaperTemplate>
|
||||
<CreditNotePaperTemplateFormConnected />
|
||||
</ElementCustomize.PaperTemplate>
|
||||
|
||||
<ElementCustomize.FieldsTab id={'general'} label={'General'}>
|
||||
<CreditNoteCustomizeGeneralField />
|
||||
</ElementCustomize.FieldsTab>
|
||||
|
||||
<ElementCustomize.FieldsTab id={'content'} label={'Content'}>
|
||||
<CreditNoteCustomizeContentFields />
|
||||
</ElementCustomize.FieldsTab>
|
||||
</BrandingTemplateForm>
|
||||
);
|
||||
}
|
||||
|
||||
function CreditNotePaperTemplateFormConnected() {
|
||||
const { values } = useFormikContext<CreditNoteCustomizeValues>();
|
||||
|
||||
return <CreditNotePaperTemplate {...values} />;
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import * as R from 'ramda';
|
||||
import { Drawer, DrawerSuspense } from '@/components';
|
||||
import withDrawers from '@/containers/Drawer/withDrawers';
|
||||
|
||||
const CreditNoteCustomizeDrawerBody = React.lazy(
|
||||
() => import('./CreditNoteCustomizeDrawerBody'),
|
||||
);
|
||||
|
||||
/**
|
||||
* Invoice customize drawer.
|
||||
* @returns {React.ReactNode}
|
||||
*/
|
||||
function CreditNoteCustomizeDrawerRoot({
|
||||
name,
|
||||
// #withDrawer
|
||||
isOpen,
|
||||
payload,
|
||||
}) {
|
||||
return (
|
||||
<Drawer isOpen={isOpen} name={name} payload={payload} size={'100%'}>
|
||||
<DrawerSuspense>
|
||||
<CreditNoteCustomizeDrawerBody />
|
||||
</DrawerSuspense>
|
||||
</Drawer>
|
||||
);
|
||||
}
|
||||
|
||||
export const CreditNoteCustomizeDrawer = R.compose(withDrawers())(
|
||||
CreditNoteCustomizeDrawerRoot,
|
||||
);
|
||||
@@ -0,0 +1,18 @@
|
||||
import { Box } from '@/components';
|
||||
import { CreditNoteCustomizeContent } from './CreditNoteCustomizeContent';
|
||||
import { Classes } from '@blueprintjs/core';
|
||||
import { BrandingTemplateBoot } from '@/containers/BrandingTemplates/BrandingTemplateBoot';
|
||||
import { useDrawerContext } from '@/components/Drawer/DrawerProvider';
|
||||
|
||||
export default function CreditNoteCustomizeDrawerBody() {
|
||||
const { payload } = useDrawerContext();
|
||||
const templateId = payload?.templateId || null;
|
||||
|
||||
return (
|
||||
<Box className={Classes.DRAWER_BODY}>
|
||||
<BrandingTemplateBoot templateId={templateId}>
|
||||
<CreditNoteCustomizeContent />
|
||||
</BrandingTemplateBoot>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
// @ts-nocheck
|
||||
import { Classes } from '@blueprintjs/core';
|
||||
import {
|
||||
FFormGroup,
|
||||
FieldRequiredHint,
|
||||
FInputGroup,
|
||||
FSwitch,
|
||||
Stack,
|
||||
} from '@/components';
|
||||
import { FColorInput } from '@/components/Forms/FColorInput';
|
||||
import { Overlay } from '../../Invoices/InvoiceCustomize/Overlay';
|
||||
import { useIsTemplateNamedFilled } from '@/containers/BrandingTemplates/utils';
|
||||
|
||||
export function CreditNoteCustomizeGeneralField() {
|
||||
const isTemplateNameFilled = useIsTemplateNamedFilled();
|
||||
|
||||
return (
|
||||
<Stack style={{ padding: 20, flex: '1 1 auto' }}>
|
||||
<Stack spacing={0}>
|
||||
<h2 style={{ fontSize: 16, marginBottom: 10 }}>General Branding</h2>
|
||||
<p className={Classes.TEXT_MUTED}>
|
||||
Set your invoice details to be automatically applied every timeyou
|
||||
create a new invoice.
|
||||
</p>
|
||||
</Stack>
|
||||
|
||||
<FFormGroup
|
||||
name={'templateName'}
|
||||
label={'Template Name'}
|
||||
labelInfo={<FieldRequiredHint />}
|
||||
fastField
|
||||
style={{ marginBottom: 10 }}
|
||||
>
|
||||
<FInputGroup name={'templateName'} fastField />
|
||||
</FFormGroup>
|
||||
|
||||
<Overlay visible={!isTemplateNameFilled}>
|
||||
<Stack spacing={0}>
|
||||
<FFormGroup
|
||||
name={'primaryColor'}
|
||||
label={'Primary Color'}
|
||||
style={{ justifyContent: 'space-between' }}
|
||||
inline
|
||||
fastField
|
||||
>
|
||||
<FColorInput
|
||||
name={'primaryColor'}
|
||||
inputProps={{ style: { maxWidth: 120 } }}
|
||||
fastField
|
||||
/>
|
||||
</FFormGroup>
|
||||
|
||||
<FFormGroup
|
||||
name={'secondaryColor'}
|
||||
label={'Secondary Color'}
|
||||
style={{ justifyContent: 'space-between' }}
|
||||
inline
|
||||
fastField
|
||||
>
|
||||
<FColorInput
|
||||
name={'secondaryColor'}
|
||||
inputProps={{ style: { maxWidth: 120 } }}
|
||||
fastField
|
||||
/>
|
||||
</FFormGroup>
|
||||
|
||||
<FFormGroup name={'showCompanyLogo'} label={'Logo'} fastField>
|
||||
<FSwitch
|
||||
name={'showCompanyLogo'}
|
||||
label={'Display company logo in the paper'}
|
||||
style={{ fontSize: 14 }}
|
||||
large
|
||||
fastField
|
||||
/>
|
||||
</FFormGroup>
|
||||
</Stack>
|
||||
</Overlay>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
// @ts-nocheck
|
||||
import { Stack } from '@/components';
|
||||
import {
|
||||
ElementCustomizeContentItemFieldGroup,
|
||||
ElementCustomizeFieldsGroup,
|
||||
} from '@/containers/ElementCustomize/ElementCustomizeFieldsGroup';
|
||||
import { Classes } from '@blueprintjs/core';
|
||||
import { fieldsGroups } from './constants';
|
||||
|
||||
export function CreditNoteCustomizeContentFields() {
|
||||
return (
|
||||
<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>
|
||||
|
||||
<Stack>
|
||||
{fieldsGroups.map((group) => (
|
||||
<ElementCustomizeFieldsGroup label={group.label}>
|
||||
{group.fields.map((item, index) => (
|
||||
<ElementCustomizeContentItemFieldGroup
|
||||
key={index}
|
||||
inputGroupProps={{
|
||||
name: item.enableKey,
|
||||
label: item.label,
|
||||
}}
|
||||
switchProps={{
|
||||
name: item.labelKey,
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</ElementCustomizeFieldsGroup>
|
||||
))}
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
import { Group, Stack } from '@/components';
|
||||
import {
|
||||
PaperTemplate,
|
||||
PaperTemplateProps,
|
||||
} from '../../Invoices/InvoiceCustomize/PaperTemplate';
|
||||
|
||||
export interface CreditNotePaperTemplateProps extends PaperTemplateProps {
|
||||
// Address
|
||||
billedToAddress?: Array<string>;
|
||||
billedFromAddress?: Array<string>;
|
||||
showBilledToAddress?: boolean;
|
||||
showBilledFromAddress?: boolean;
|
||||
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;
|
||||
}>;
|
||||
|
||||
// Date issue.
|
||||
creditNoteDateLabel?: string;
|
||||
showCreditNoteDate?: boolean;
|
||||
creditNoteDate?: string;
|
||||
|
||||
// Credit Number.
|
||||
creditNoteNumebr?: string;
|
||||
creditNoteNumberLabel?: string;
|
||||
showCreditNoteNumber?: boolean;
|
||||
}
|
||||
|
||||
export function CreditNotePaperTemplate({
|
||||
primaryColor,
|
||||
secondaryColor,
|
||||
showCompanyLogo = true,
|
||||
companyLogo,
|
||||
companyName = 'Bigcapital Technology, Inc.',
|
||||
|
||||
// Address
|
||||
billedToAddress = [
|
||||
'Bigcapital Technology, Inc.',
|
||||
'131 Continental Dr Suite 305 Newark,',
|
||||
'Delaware 19713',
|
||||
'United States',
|
||||
'+1 762-339-5634',
|
||||
'ahmed@bigcapital.app',
|
||||
],
|
||||
billedFromAddress = [
|
||||
'131 Continental Dr Suite 305 Newark,',
|
||||
'Delaware 19713',
|
||||
'United States',
|
||||
'+1 762-339-5634',
|
||||
'ahmed@bigcapital.app',
|
||||
],
|
||||
showBilledToAddress = true,
|
||||
showBilledFromAddress = 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 = 'It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout.',
|
||||
customerNoteLabel = 'Customer Note',
|
||||
|
||||
// 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.',
|
||||
termsConditionsLabel = 'Terms & Conditions',
|
||||
|
||||
lines = [
|
||||
{
|
||||
item: 'Simply dummy text',
|
||||
description: 'Simply dummy text of the printing and typesetting',
|
||||
rate: '1',
|
||||
quantity: '1000',
|
||||
total: '$1000.00',
|
||||
},
|
||||
],
|
||||
// Credit note number.
|
||||
showCreditNoteNumber = true,
|
||||
creditNoteNumberLabel = 'Credit Note Number',
|
||||
creditNoteNumebr = '346D3D40-0001',
|
||||
|
||||
// Credit note date.
|
||||
creditNoteDate = 'September 3, 2024',
|
||||
showCreditNoteDate = true,
|
||||
creditNoteDateLabel = 'Credit Note Date',
|
||||
}: CreditNotePaperTemplateProps) {
|
||||
return (
|
||||
<PaperTemplate
|
||||
primaryColor={primaryColor}
|
||||
secondaryColor={secondaryColor}
|
||||
showCompanyLogo={showCompanyLogo}
|
||||
companyLogo={companyLogo}
|
||||
bigtitle={'Credit Note'}
|
||||
>
|
||||
<Stack spacing={24}>
|
||||
<PaperTemplate.TermsList>
|
||||
{showCreditNoteNumber && (
|
||||
<PaperTemplate.TermsItem label={creditNoteNumberLabel}>
|
||||
{creditNoteNumebr}
|
||||
</PaperTemplate.TermsItem>
|
||||
)}
|
||||
{showCreditNoteDate && (
|
||||
<PaperTemplate.TermsItem label={creditNoteDateLabel}>
|
||||
{creditNoteDate}
|
||||
</PaperTemplate.TermsItem>
|
||||
)}
|
||||
</PaperTemplate.TermsList>
|
||||
|
||||
<Group spacing={10}>
|
||||
{showBilledFromAddress && (
|
||||
<PaperTemplate.Address
|
||||
items={[<strong>{companyName}</strong>, ...billedFromAddress]}
|
||||
/>
|
||||
)}
|
||||
{showBilledToAddress && (
|
||||
<PaperTemplate.Address
|
||||
items={[<strong>{billedToLabel}</strong>, ...billedToAddress]}
|
||||
/>
|
||||
)}
|
||||
</Group>
|
||||
|
||||
<Stack spacing={0}>
|
||||
<PaperTemplate.Table
|
||||
columns={[
|
||||
{ label: 'Item', accessor: 'item' },
|
||||
{ label: 'Description', accessor: 'item' },
|
||||
{ label: 'Rate', accessor: 'rate', align: 'right' },
|
||||
{ label: 'Total', 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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
export const initialValues = {
|
||||
templateName: '',
|
||||
|
||||
// Colors
|
||||
primaryColor: '#2c3dd8',
|
||||
secondaryColor: '#2c3dd8',
|
||||
|
||||
// Company logo.
|
||||
showCompanyLogo: true,
|
||||
companyLogo:
|
||||
'https://cdn-development.mercury.com/demo-assets/avatars/mercury-demo-dark.png',
|
||||
|
||||
// Address
|
||||
showBilledToAddress: true,
|
||||
showBilledFromAddress: true,
|
||||
billedToLabel: 'Bill To',
|
||||
|
||||
// Entries
|
||||
itemNameLabel: 'Item',
|
||||
itemDescriptionLabel: 'Description',
|
||||
itemRateLabel: 'Rate',
|
||||
itemTotalLabel: 'Total',
|
||||
|
||||
// Total
|
||||
showTotal: true,
|
||||
totalLabel: 'Total',
|
||||
|
||||
// Subtotal
|
||||
showSubtotal: true,
|
||||
subtotalLabel: 'Subtotal',
|
||||
|
||||
// Customer Note.
|
||||
showCustomerNote: true,
|
||||
customerNoteLabel: 'Customer Note',
|
||||
|
||||
// Terms & Conditions
|
||||
showTermsConditions: true,
|
||||
termsConditionsLabel: 'Terms & Conditions',
|
||||
|
||||
// Date issue.
|
||||
creditNoteDateLabel: 'Issue of Date',
|
||||
showCreditNoteDate: true,
|
||||
|
||||
// Credit Number.
|
||||
creditNoteNumberLabel: 'Credit Note #',
|
||||
showCreditNoteNumber: true,
|
||||
};
|
||||
|
||||
export const fieldsGroups = [
|
||||
{
|
||||
label: 'Header',
|
||||
fields: [
|
||||
{
|
||||
labelKey: 'creditNoteDateLabel',
|
||||
enableKey: 'showCreditNoteDate',
|
||||
label: 'Issue of Date',
|
||||
},
|
||||
{
|
||||
labelKey: 'creditNoteNumberLabel',
|
||||
enableKey: 'showCreditNoteNumber',
|
||||
label: 'Credit Note #',
|
||||
},
|
||||
{
|
||||
enableKey: 'showBilledToAddress',
|
||||
labelKey: 'billedToLabel',
|
||||
label: 'Bill To',
|
||||
},
|
||||
{
|
||||
enableKey: 'showBilledFromAddress',
|
||||
label: 'Billed From',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Totals',
|
||||
fields: [
|
||||
{
|
||||
labelKey: 'subtotalLabel',
|
||||
enableKey: 'showSubtotal',
|
||||
label: 'Subtotal',
|
||||
},
|
||||
{ labelKey: 'totalLabel', enableKey: 'showTotal', label: 'Total' },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Footer',
|
||||
fields: [
|
||||
{
|
||||
labelKey: 'termsConditionsLabel',
|
||||
enableKey: 'showTermsConditions',
|
||||
label: 'Terms & Conditions',
|
||||
},
|
||||
{
|
||||
labelKey: 'customerNoteLabel',
|
||||
enableKey: 'showCustomerNote',
|
||||
label: 'Customer Note',
|
||||
labelPlaceholder: 'Customer Note',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,41 @@
|
||||
import { BrandingTemplateValues } from '@/containers/BrandingTemplates/types';
|
||||
|
||||
export interface CreditNoteCustomizeValues extends BrandingTemplateValues {
|
||||
// Colors
|
||||
primaryColor?: string;
|
||||
secondaryColor?: string;
|
||||
|
||||
// Company Logo
|
||||
showCompanyLogo?: boolean;
|
||||
companyLogo?: string;
|
||||
|
||||
// Entries
|
||||
itemNameLabel?: string;
|
||||
itemDescriptionLabel?: string;
|
||||
itemRateLabel?: string;
|
||||
itemTotalLabel?: string;
|
||||
|
||||
// Total
|
||||
showTotal?: boolean;
|
||||
totalLabel?: string;
|
||||
|
||||
// Subtotal
|
||||
showSubtotal?: boolean;
|
||||
subtotalLabel?: string;
|
||||
|
||||
// Customer Note.
|
||||
showCustomerNote?: boolean;
|
||||
customerNoteLabel?: string;
|
||||
|
||||
// Terms & Conditions
|
||||
showTermsConditions?: boolean;
|
||||
termsConditionsLabel?: string;
|
||||
|
||||
// Date issue.
|
||||
creditNoteDateLabel?: string;
|
||||
showCreditNoteDate?: boolean;
|
||||
|
||||
// Credit Number.
|
||||
creditNoteNumberLabel?: string;
|
||||
showCreditNoteNumber?: boolean;
|
||||
}
|
||||
@@ -12,10 +12,15 @@ import {
|
||||
Menu,
|
||||
MenuItem,
|
||||
} from '@blueprintjs/core';
|
||||
import { If, Icon, FormattedMessage as T, Group } from '@/components';
|
||||
import { If, Icon, FormattedMessage as T, Group, FSelect } from '@/components';
|
||||
import { CLASSES } from '@/constants/classes';
|
||||
import classNames from 'classnames';
|
||||
import { useCreditNoteFormContext } from './CreditNoteFormProvider';
|
||||
import {
|
||||
BrandingThemeFormGroup,
|
||||
BrandingThemeSelectButton,
|
||||
} from '@/containers/BrandingTemplates/BrandingTemplatesSelectFields';
|
||||
import { useCreditNoteFormBrandingTemplatesOptions } from './utils';
|
||||
|
||||
/**
|
||||
* Credit note floating actions.
|
||||
@@ -74,6 +79,8 @@ export default function CreditNoteFloatingActions() {
|
||||
resetForm();
|
||||
};
|
||||
|
||||
const brandingTemplatesOptions = useCreditNoteFormBrandingTemplatesOptions();
|
||||
|
||||
return (
|
||||
<Group
|
||||
spacing={10}
|
||||
@@ -190,6 +197,25 @@ export default function CreditNoteFloatingActions() {
|
||||
onClick={handleCancelBtnClick}
|
||||
text={<T id={'cancel'} />}
|
||||
/>
|
||||
|
||||
{/* ----------- Branding Template Select ----------- */}
|
||||
<BrandingThemeFormGroup
|
||||
name={'pdf_template_id'}
|
||||
label={'Branding'}
|
||||
inline
|
||||
fastField
|
||||
style={{ marginLeft: 20 }}
|
||||
>
|
||||
<FSelect
|
||||
name={'pdf_template_id'}
|
||||
items={brandingTemplatesOptions}
|
||||
input={({ activeItem, text, label, value }) => (
|
||||
<BrandingThemeSelectButton text={text || 'Brand Theme'} minimal />
|
||||
)}
|
||||
filterable={false}
|
||||
popoverProps={{ minimal: true }}
|
||||
/>
|
||||
</BrandingThemeFormGroup>
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
useSettingsCreditNotes,
|
||||
useInvoice,
|
||||
} from '@/hooks/query';
|
||||
import { useGetPdfTemplates } from '@/hooks/query/pdf-templates';
|
||||
|
||||
const CreditNoteFormContext = React.createContext();
|
||||
|
||||
@@ -73,6 +74,10 @@ function CreditNoteFormProvider({ creditNoteId, ...props }) {
|
||||
isSuccess: isBranchesSuccess,
|
||||
} = useBranches({}, { enabled: isBranchFeatureCan });
|
||||
|
||||
// Fetches branding templates of invoice.
|
||||
const { data: brandingTemplates, isLoading: isBrandingTemplatesLoading } =
|
||||
useGetPdfTemplates({ resource: 'PaymentReceive' });
|
||||
|
||||
// Handle fetching settings.
|
||||
useSettingsCreditNotes();
|
||||
|
||||
@@ -115,13 +120,18 @@ function CreditNoteFormProvider({ creditNoteId, ...props }) {
|
||||
createCreditNoteMutate,
|
||||
editCreditNoteMutate,
|
||||
setSubmitPayload,
|
||||
|
||||
// Branding templates.
|
||||
brandingTemplates,
|
||||
isBrandingTemplatesLoading,
|
||||
};
|
||||
|
||||
const isLoading =
|
||||
isItemsLoading ||
|
||||
isCustomersLoading ||
|
||||
isCreditNoteLoading ||
|
||||
isInvoiceLoading;
|
||||
isInvoiceLoading ||
|
||||
isBrandingTemplatesLoading;
|
||||
|
||||
return (
|
||||
<DashboardInsider loading={isLoading} name={'credit-note-form'}>
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
transformAttachmentsToForm,
|
||||
transformAttachmentsToRequest,
|
||||
} from '@/containers/Attachments/utils';
|
||||
import { convertBrandingTemplatesToOptions } from '@/containers/BrandingTemplates/BrandingTemplatesSelectFields';
|
||||
|
||||
export const MIN_LINES_NUMBER = 1;
|
||||
|
||||
@@ -54,7 +55,8 @@ export const defaultCreditNote = {
|
||||
exchange_rate: 1,
|
||||
currency_code: '',
|
||||
entries: [...repeatValue(defaultCreditNoteEntry, MIN_LINES_NUMBER)],
|
||||
attachments: []
|
||||
attachments: [],
|
||||
pdf_template_id: '',
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -214,3 +216,13 @@ export const useCreditNoteIsForeignCustomer = () => {
|
||||
);
|
||||
return isForeignCustomer;
|
||||
};
|
||||
|
||||
|
||||
export const useCreditNoteFormBrandingTemplatesOptions = () => {
|
||||
const { brandingTemplates } = useCreditNoteFormContext();
|
||||
|
||||
return React.useMemo(
|
||||
() => convertBrandingTemplatesToOptions(brandingTemplates),
|
||||
[brandingTemplates],
|
||||
);
|
||||
};
|
||||
|
||||
@@ -6,6 +6,11 @@ import {
|
||||
NavbarDivider,
|
||||
NavbarGroup,
|
||||
Alignment,
|
||||
Menu,
|
||||
MenuItem,
|
||||
Popover,
|
||||
PopoverInteractionKind,
|
||||
Position,
|
||||
} from '@blueprintjs/core';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import {
|
||||
@@ -28,9 +33,11 @@ import withCreditNotesActions from './withCreditNotesActions';
|
||||
import withSettings from '@/containers/Settings/withSettings';
|
||||
import withSettingsActions from '@/containers/Settings/withSettingsActions';
|
||||
import withDialogActions from '@/containers/Dialog/withDialogActions';
|
||||
import withDrawerActions from '@/containers/Drawer/withDrawerActions';
|
||||
|
||||
import { DialogsName } from '@/constants/dialogs';
|
||||
import { compose } from '@/utils';
|
||||
import { DRAWERS } from '@/constants/drawers';
|
||||
|
||||
/**
|
||||
* Credit note table actions bar.
|
||||
@@ -50,6 +57,9 @@ function CreditNotesActionsBar({
|
||||
|
||||
// #withDialogActions
|
||||
openDialog,
|
||||
|
||||
// #withDrawerActions
|
||||
openDrawer
|
||||
}) {
|
||||
const history = useHistory();
|
||||
|
||||
@@ -89,6 +99,10 @@ function CreditNotesActionsBar({
|
||||
const handlePrintBtnClick = () => {
|
||||
downloadExportPdf({ resource: 'CreditNote' });
|
||||
};
|
||||
// Handle the customize button click.
|
||||
const handleCustomizeBtnClick = () => {
|
||||
openDrawer(DRAWERS.BRANDING_TEMPLATES, { resource: 'CreditNote' });
|
||||
}
|
||||
|
||||
return (
|
||||
<DashboardActionsBar>
|
||||
@@ -149,6 +163,25 @@ function CreditNotesActionsBar({
|
||||
<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 Templates'}
|
||||
/>
|
||||
</Menu>
|
||||
}
|
||||
>
|
||||
<Button icon={<Icon icon="cog-16" iconSize={16} />} minimal={true} />
|
||||
</Popover>
|
||||
<NavbarDivider />
|
||||
<Button
|
||||
className={Classes.MINIMAL}
|
||||
icon={<Icon icon="refresh-16" iconSize={14} />}
|
||||
@@ -169,4 +202,5 @@ export default compose(
|
||||
creditNoteTableSize: creditNoteSettings?.tableSize,
|
||||
})),
|
||||
withDialogActions,
|
||||
withDrawerActions
|
||||
)(CreditNotesActionsBar);
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import { useFormikContext } from 'formik';
|
||||
import { ElementCustomize } from '../../../ElementCustomize/ElementCustomize';
|
||||
import { EstimateCustomizeGeneralField } from './EstimateCustomizeFieldsGeneral';
|
||||
import { EstimateCustomizeContentFields } from './EstimateCustomizeFieldsContent';
|
||||
import { EstimatePaperTemplate } from './EstimatePaperTemplate';
|
||||
import { EstimateCustomizeValues } from './types';
|
||||
import { initialValues } from './constants';
|
||||
import { useDrawerContext } from '@/components/Drawer/DrawerProvider';
|
||||
import { useDrawerActions } from '@/hooks/state';
|
||||
import { BrandingTemplateForm } from '@/containers/BrandingTemplates/BrandingTemplateForm';
|
||||
|
||||
export function EstimateCustomizeContent() {
|
||||
const { payload, name } = useDrawerContext();
|
||||
const { closeDrawer } = useDrawerActions();
|
||||
const templateId = payload?.templateId || null;
|
||||
|
||||
const handleSuccess = () => {
|
||||
closeDrawer(name);
|
||||
};
|
||||
|
||||
return (
|
||||
<BrandingTemplateForm<EstimateCustomizeValues>
|
||||
templateId={templateId}
|
||||
defaultValues={initialValues}
|
||||
onSuccess={handleSuccess}
|
||||
resource={'SaleEstimate'}
|
||||
>
|
||||
<ElementCustomize.PaperTemplate>
|
||||
<EstimatePaperTemplateFormConnected />
|
||||
</ElementCustomize.PaperTemplate>
|
||||
|
||||
<ElementCustomize.FieldsTab id={'general'} label={'General'}>
|
||||
<EstimateCustomizeGeneralField />
|
||||
</ElementCustomize.FieldsTab>
|
||||
|
||||
<ElementCustomize.FieldsTab id={'content'} label={'Content'}>
|
||||
<EstimateCustomizeContentFields />
|
||||
</ElementCustomize.FieldsTab>
|
||||
</BrandingTemplateForm>
|
||||
);
|
||||
}
|
||||
|
||||
function EstimatePaperTemplateFormConnected() {
|
||||
const { values } = useFormikContext<EstimateCustomizeValues>();
|
||||
|
||||
return <EstimatePaperTemplate {...values} />;
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import * as R from 'ramda';
|
||||
import { Drawer, DrawerSuspense } from '@/components';
|
||||
import withDrawers from '@/containers/Drawer/withDrawers';
|
||||
|
||||
const EstimateCustomizeDrawerBody = React.lazy(
|
||||
() => import('./EstimateCustomizeDrawerBody'),
|
||||
);
|
||||
|
||||
/**
|
||||
* Estimate customize drawer.
|
||||
* @returns {React.ReactNode}
|
||||
*/
|
||||
function EstimateCustomizeDrawerRoot({
|
||||
name,
|
||||
|
||||
// #withDrawer
|
||||
isOpen,
|
||||
payload,
|
||||
}) {
|
||||
return (
|
||||
<Drawer isOpen={isOpen} name={name} payload={payload} size={'100%'}>
|
||||
<DrawerSuspense>
|
||||
<EstimateCustomizeDrawerBody />
|
||||
</DrawerSuspense>
|
||||
</Drawer>
|
||||
);
|
||||
}
|
||||
|
||||
export const EstimateCustomizeDrawer = R.compose(withDrawers())(
|
||||
EstimateCustomizeDrawerRoot,
|
||||
);
|
||||
@@ -0,0 +1,18 @@
|
||||
import { Box } from '@/components';
|
||||
import { useDrawerContext } from '@/components/Drawer/DrawerProvider';
|
||||
import { BrandingTemplateBoot } from '@/containers/BrandingTemplates/BrandingTemplateBoot';
|
||||
import { Classes } from '@blueprintjs/core';
|
||||
import { EstimateCustomizeContent } from './EstimateCustomizeContent';
|
||||
|
||||
export default function EstimateCustomizeDrawerBody() {
|
||||
const { payload } = useDrawerContext();
|
||||
const templateId = payload?.templateId || null;
|
||||
|
||||
return (
|
||||
<Box className={Classes.DRAWER_BODY}>
|
||||
<BrandingTemplateBoot templateId={templateId}>
|
||||
<EstimateCustomizeContent />
|
||||
</BrandingTemplateBoot>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
// @ts-nocheck
|
||||
import { FInputGroup, FSwitch, Group, Stack } from '@/components';
|
||||
import { CLASSES } from '@/constants';
|
||||
import { Classes } from '@blueprintjs/core';
|
||||
import { fieldsGroups } from './constants';
|
||||
|
||||
export function EstimateCustomizeContentFields() {
|
||||
return (
|
||||
<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 time
you
|
||||
create a new invoice.
|
||||
</p>
|
||||
</Stack>
|
||||
|
||||
<Stack>
|
||||
{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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
// @ts-nocheck
|
||||
import { Classes, Text } from '@blueprintjs/core';
|
||||
import {
|
||||
FFormGroup,
|
||||
FInputGroup,
|
||||
FSwitch,
|
||||
FieldRequiredHint,
|
||||
Group,
|
||||
Stack,
|
||||
} from '@/components';
|
||||
import { FColorInput } from '@/components/Forms/FColorInput';
|
||||
import { useIsTemplateNamedFilled } from '@/containers/BrandingTemplates/utils';
|
||||
import { Overlay } from '../../Invoices/InvoiceCustomize/Overlay';
|
||||
|
||||
export function EstimateCustomizeGeneralField() {
|
||||
const isTemplateNameFilled = useIsTemplateNamedFilled();
|
||||
|
||||
return (
|
||||
<Stack style={{ padding: 20, flex: '1 1 auto' }}>
|
||||
<Stack spacing={0}>
|
||||
<h2 style={{ fontSize: 16, marginBottom: 10 }}>General Branding</h2>
|
||||
<p className={Classes.TEXT_MUTED}>
|
||||
Set your invoice details to be automatically applied every timeyou
|
||||
create a new invoice.
|
||||
</p>
|
||||
</Stack>
|
||||
|
||||
<FFormGroup
|
||||
name={'templateName'}
|
||||
label={'Template Name'}
|
||||
labelInfo={<FieldRequiredHint />}
|
||||
fastField
|
||||
style={{ marginBottom: 10 }}
|
||||
>
|
||||
<FInputGroup name={'templateName'} fastField />
|
||||
</FFormGroup>
|
||||
|
||||
<Overlay visible={!isTemplateNameFilled}>
|
||||
<Stack spacing={0}>
|
||||
<FFormGroup
|
||||
name={'primaryColor'}
|
||||
label={'Primary Color'}
|
||||
style={{ justifyContent: 'space-between' }}
|
||||
inline
|
||||
fastField
|
||||
>
|
||||
<FColorInput
|
||||
name={'primaryColor'}
|
||||
inputProps={{ style: { maxWidth: 120 } }}
|
||||
fastField
|
||||
/>
|
||||
</FFormGroup>
|
||||
|
||||
<FFormGroup
|
||||
name={'secondaryColor'}
|
||||
label={'Secondary Color'}
|
||||
style={{ justifyContent: 'space-between' }}
|
||||
inline
|
||||
fastField
|
||||
>
|
||||
<FColorInput
|
||||
name={'secondaryColor'}
|
||||
inputProps={{ style: { maxWidth: 120 } }}
|
||||
fastField
|
||||
/>
|
||||
</FFormGroup>
|
||||
|
||||
<FFormGroup name={'showCompanyLogo'} label={'Logo'} fastField>
|
||||
<FSwitch
|
||||
name={'showCompanyLogo'}
|
||||
label={'Display company logo in the paper'}
|
||||
style={{ fontSize: 14 }}
|
||||
large
|
||||
fastField
|
||||
/>
|
||||
</FFormGroup>
|
||||
</Stack>
|
||||
</Overlay>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,200 @@
|
||||
import { Group, Stack } from '@/components';
|
||||
import {
|
||||
PaperTemplate,
|
||||
PaperTemplateProps,
|
||||
} from '../../Invoices/InvoiceCustomize/PaperTemplate';
|
||||
|
||||
export interface EstimatePaperTemplateProps extends PaperTemplateProps {
|
||||
estimateNumebr?: string;
|
||||
estimateNumberLabel?: string;
|
||||
showEstimateNumber?: boolean;
|
||||
|
||||
expirationDate?: string;
|
||||
showExpirationDate?: boolean;
|
||||
expirationDateLabel?: string;
|
||||
|
||||
estimateDateLabel?: string;
|
||||
showEstimateDate?: boolean;
|
||||
estimateDate?: string;
|
||||
|
||||
companyName?: string;
|
||||
|
||||
// Address
|
||||
showBilledToAddress?: boolean;
|
||||
billedToAddress?: Array<string>;
|
||||
|
||||
showBilledFromAddress?: boolean;
|
||||
billedFromAddress?: Array<string>;
|
||||
billedToLabel?: string;
|
||||
|
||||
// Totals
|
||||
total?: string;
|
||||
showTotal?: boolean;
|
||||
totalLabel?: string;
|
||||
|
||||
subtotal?: string;
|
||||
showSubtotal?: boolean;
|
||||
subtotalLabel?: string;
|
||||
|
||||
// Statements
|
||||
showCustomerNote?: boolean;
|
||||
customerNote?: string;
|
||||
customerNoteLabel?: string;
|
||||
|
||||
showTermsConditions?: boolean;
|
||||
termsConditions?: string;
|
||||
termsConditionsLabel?: string;
|
||||
|
||||
lines?: Array<{
|
||||
item: string;
|
||||
description: string;
|
||||
rate: string;
|
||||
quantity: string;
|
||||
total: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
export function EstimatePaperTemplate({
|
||||
primaryColor,
|
||||
secondaryColor,
|
||||
showCompanyLogo = true,
|
||||
companyLogo,
|
||||
companyName,
|
||||
|
||||
billedToAddress = [
|
||||
'Bigcapital Technology, Inc.',
|
||||
'131 Continental Dr Suite 305 Newark,',
|
||||
'Delaware 19713',
|
||||
'United States',
|
||||
'+1 762-339-5634',
|
||||
'ahmed@bigcapital.app',
|
||||
],
|
||||
billedFromAddress = [
|
||||
'131 Continental Dr Suite 305 Newark,',
|
||||
'Delaware 19713',
|
||||
'United States',
|
||||
'+1 762-339-5634',
|
||||
'ahmed@bigcapital.app',
|
||||
],
|
||||
showBilledFromAddress = true,
|
||||
showBilledToAddress = true,
|
||||
billedToLabel = 'Billed To',
|
||||
|
||||
total = '$1000.00',
|
||||
totalLabel = 'Total',
|
||||
showTotal = true,
|
||||
|
||||
subtotal = '1000/00',
|
||||
subtotalLabel = 'Subtotal',
|
||||
showSubtotal = true,
|
||||
|
||||
showCustomerNote = true,
|
||||
customerNote = 'It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout.',
|
||||
customerNoteLabel = 'Customer Note',
|
||||
|
||||
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.',
|
||||
termsConditionsLabel = 'Terms & Conditions',
|
||||
|
||||
lines = [
|
||||
{
|
||||
item: 'Simply dummy text',
|
||||
description: 'Simply dummy text of the printing and typesetting',
|
||||
rate: '1',
|
||||
quantity: '1000',
|
||||
total: '$1000.00',
|
||||
},
|
||||
],
|
||||
showEstimateNumber = true,
|
||||
estimateNumberLabel = 'Estimate Number',
|
||||
estimateNumebr = '346D3D40-0001',
|
||||
|
||||
estimateDate = 'September 3, 2024',
|
||||
showEstimateDate = true,
|
||||
estimateDateLabel = 'Estimate Date',
|
||||
|
||||
expirationDateLabel = 'Expiration Date',
|
||||
showExpirationDate = true,
|
||||
expirationDate = 'September 3, 2024',
|
||||
}: EstimatePaperTemplateProps) {
|
||||
return (
|
||||
<PaperTemplate
|
||||
primaryColor={primaryColor}
|
||||
secondaryColor={secondaryColor}
|
||||
showCompanyLogo={showCompanyLogo}
|
||||
companyLogo={companyLogo}
|
||||
bigtitle={'Estimate'}
|
||||
>
|
||||
<Stack spacing={24}>
|
||||
<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>
|
||||
|
||||
<Group spacing={10}>
|
||||
{showBilledFromAddress && (
|
||||
<PaperTemplate.Address
|
||||
items={[<strong>{companyName}</strong>, ...billedFromAddress]}
|
||||
/>
|
||||
)}
|
||||
{showBilledToAddress && (
|
||||
<PaperTemplate.Address
|
||||
items={[<strong>{billedToLabel}</strong>, ...billedToAddress]}
|
||||
/>
|
||||
)}
|
||||
</Group>
|
||||
|
||||
<Stack spacing={0}>
|
||||
<PaperTemplate.Table
|
||||
columns={[
|
||||
{ label: 'Item', accessor: 'item' },
|
||||
{ label: 'Description', accessor: 'item' },
|
||||
{ label: 'Rate', accessor: 'rate', align: 'right' },
|
||||
{ label: 'Total', 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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
export const initialValues = {
|
||||
templateName: '',
|
||||
|
||||
// Colors
|
||||
primaryColor: '#2c3dd8',
|
||||
secondaryColor: '#2c3dd8',
|
||||
|
||||
// Company logo.
|
||||
showCompanyLogo: true,
|
||||
companyLogo:
|
||||
'https://cdn-development.mercury.com/demo-assets/avatars/mercury-demo-dark.png',
|
||||
|
||||
// Top details.
|
||||
showEstimateNumber: true,
|
||||
estimateNumberLabel: 'Estimate number',
|
||||
|
||||
estimateDateLabel: 'Date of Issue',
|
||||
showEstimateDate: true,
|
||||
|
||||
showExpirationDate: true,
|
||||
expirationDateLabel: 'Expiration Date',
|
||||
|
||||
// Company name
|
||||
companyName: 'Bigcapital Technology, Inc.',
|
||||
|
||||
// Addresses
|
||||
showBilledFromAddress: true,
|
||||
showBilledToAddress: true,
|
||||
billedToLabel: 'Billed To',
|
||||
|
||||
// Entries
|
||||
itemNameLabel: 'Item',
|
||||
itemDescriptionLabel: 'Description',
|
||||
itemRateLabel: 'Rate',
|
||||
itemTotalLabel: 'Total',
|
||||
|
||||
// Totals
|
||||
showSubtotal: true,
|
||||
subtotalLabel: 'Subtotal',
|
||||
|
||||
showTotal: true,
|
||||
totalLabel: 'Total',
|
||||
|
||||
// Statements
|
||||
showCustomerNote: true,
|
||||
customerNoteLabel: 'Customer Note',
|
||||
|
||||
showTermsConditions: true,
|
||||
termsConditionsLabel: 'Terms & Conditions',
|
||||
};
|
||||
|
||||
export const fieldsGroups = [
|
||||
{
|
||||
label: 'Header',
|
||||
fields: [
|
||||
{
|
||||
labelKey: 'estimateNumberLabel',
|
||||
enableKey: 'showEstimateNumber',
|
||||
label: 'Estimate No.',
|
||||
},
|
||||
{
|
||||
labelKey: 'estimateDateLabel',
|
||||
enableKey: 'showEstimateDate',
|
||||
label: 'Issue Date',
|
||||
},
|
||||
{
|
||||
labelKey: 'expirationDateLabel',
|
||||
enableKey: 'showExpirationDate',
|
||||
label: 'Expiration Date',
|
||||
},
|
||||
{
|
||||
enableKey: 'showBilledToAddress',
|
||||
labelKey: 'billedToLabel',
|
||||
label: 'Bill To',
|
||||
},
|
||||
{
|
||||
enableKey: 'showBilledFromAddress',
|
||||
label: 'Billed From',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Totals',
|
||||
fields: [
|
||||
{
|
||||
labelKey: 'subtotalLabel',
|
||||
enableKey: 'showSubtotal',
|
||||
label: 'Subtotal',
|
||||
},
|
||||
{ labelKey: 'totalLabel', enableKey: 'showTotal', label: 'Total' },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Footer',
|
||||
fields: [
|
||||
{
|
||||
labelKey: 'termsConditionsLabel',
|
||||
enableKey: 'showTermsConditions',
|
||||
label: 'Terms & Conditions',
|
||||
},
|
||||
{
|
||||
labelKey: 'customerNoteLabel',
|
||||
enableKey: 'showCustomerNote',
|
||||
label: 'Statement',
|
||||
labelPlaceholder: 'Statement',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,49 @@
|
||||
import { BrandingTemplateValues } from "@/containers/BrandingTemplates/types";
|
||||
|
||||
export interface EstimateCustomizeValues extends BrandingTemplateValues {
|
||||
// Colors
|
||||
primaryColor?: string;
|
||||
secondaryColor?: string;
|
||||
|
||||
// Company Logo
|
||||
showCompanyLogo?: boolean;
|
||||
companyLogo?: string;
|
||||
|
||||
// Top details.
|
||||
estimateNumberLabel?: string;
|
||||
showEstimateNumber?: boolean;
|
||||
|
||||
showExpirationDate?: boolean;
|
||||
expirationDateLabel?: string;
|
||||
|
||||
estimateDateLabel?: string;
|
||||
showEstimateDate?: boolean;
|
||||
|
||||
// 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;
|
||||
|
||||
showTotal?: boolean;
|
||||
totalLabel?: string;
|
||||
|
||||
// Statements
|
||||
showCustomerNote?: boolean;
|
||||
customerNoteLabel?: string;
|
||||
|
||||
showTermsConditions?: boolean;
|
||||
termsConditionsLabel?: string;
|
||||
}
|
||||
@@ -11,11 +11,16 @@ import {
|
||||
Menu,
|
||||
MenuItem,
|
||||
} from '@blueprintjs/core';
|
||||
import { If, Icon, FormattedMessage as T, Group } from '@/components';
|
||||
import { If, Icon, FormattedMessage as T, Group, FSelect } from '@/components';
|
||||
import { CLASSES } from '@/constants/classes';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { useFormikContext } from 'formik';
|
||||
import { useEstimateFormContext } from './EstimateFormProvider';
|
||||
import { useEstimateFormBrandingTemplatesOptions } from './utils';
|
||||
import {
|
||||
BrandingThemeFormGroup,
|
||||
BrandingThemeSelectButton,
|
||||
} from '@/containers/BrandingTemplates/BrandingTemplatesSelectFields';
|
||||
|
||||
/**
|
||||
* Estimate floating actions bar.
|
||||
@@ -73,6 +78,8 @@ export default function EstimateFloatingActions() {
|
||||
resetForm();
|
||||
};
|
||||
|
||||
const brandingTemplatesOptions = useEstimateFormBrandingTemplatesOptions();
|
||||
|
||||
return (
|
||||
<Group
|
||||
spacing={10}
|
||||
@@ -193,6 +200,25 @@ export default function EstimateFloatingActions() {
|
||||
onClick={handleCancelBtnClick}
|
||||
text={<T id={'cancel'} />}
|
||||
/>
|
||||
|
||||
{/* ----------- Branding Template Select ----------- */}
|
||||
<BrandingThemeFormGroup
|
||||
name={'pdf_template_id'}
|
||||
label={'Branding'}
|
||||
inline
|
||||
fastField
|
||||
style={{ marginLeft: 20 }}
|
||||
>
|
||||
<FSelect
|
||||
name={'pdf_template_id'}
|
||||
items={brandingTemplatesOptions}
|
||||
input={({ activeItem, text, label, value }) => (
|
||||
<BrandingThemeSelectButton text={text || 'Brand Theme'} minimal />
|
||||
)}
|
||||
filterable={false}
|
||||
popoverProps={{ minimal: true }}
|
||||
/>
|
||||
</BrandingThemeFormGroup>
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -12,8 +12,9 @@ import {
|
||||
useCreateEstimate,
|
||||
useEditEstimate,
|
||||
} from '@/hooks/query';
|
||||
import { Features } from '@/constants';
|
||||
import { useProjects } from '@/containers/Projects/hooks';
|
||||
import { useGetPdfTemplates } from '@/hooks/query/pdf-templates';
|
||||
import { Features } from '@/constants';
|
||||
import { useFeatureCan } from '@/hooks/state';
|
||||
import { ITEMS_FILTER_ROLES } from './utils';
|
||||
|
||||
@@ -71,6 +72,10 @@ function EstimateFormProvider({ query, estimateId, ...props }) {
|
||||
isLoading: isProjectsLoading,
|
||||
} = useProjects({}, { enabled: !!isProjectsFeatureCan });
|
||||
|
||||
// Fetches branding templates of invoice.
|
||||
const { data: brandingTemplates, isLoading: isBrandingTemplatesLoading } =
|
||||
useGetPdfTemplates({ resource: 'SaleEstimate' });
|
||||
|
||||
// Handle fetch settings.
|
||||
useSettingsEstimates();
|
||||
|
||||
@@ -112,13 +117,19 @@ function EstimateFormProvider({ query, estimateId, ...props }) {
|
||||
|
||||
createEstimateMutate,
|
||||
editEstimateMutate,
|
||||
|
||||
brandingTemplates,
|
||||
isBrandingTemplatesLoading,
|
||||
};
|
||||
|
||||
const isLoading =
|
||||
isCustomersLoading ||
|
||||
isItemsLoading ||
|
||||
isEstimateLoading ||
|
||||
isBrandingTemplatesLoading;
|
||||
|
||||
return (
|
||||
<DashboardInsider
|
||||
loading={isCustomersLoading || isItemsLoading || isEstimateLoading}
|
||||
name={'estimate-form'}
|
||||
>
|
||||
<DashboardInsider loading={isLoading} name={'estimate-form'}>
|
||||
<EstimateFormContext.Provider value={provider} {...props} />
|
||||
</DashboardInsider>
|
||||
);
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
transformAttachmentsToForm,
|
||||
transformAttachmentsToRequest,
|
||||
} from '@/containers/Attachments/utils';
|
||||
import { convertBrandingTemplatesToOptions } from '@/containers/BrandingTemplates/BrandingTemplatesSelectFields';
|
||||
|
||||
export const MIN_LINES_NUMBER = 1;
|
||||
|
||||
@@ -60,7 +61,8 @@ export const defaultEstimate = {
|
||||
exchange_rate: 1,
|
||||
currency_code: '',
|
||||
entries: [...repeatValue(defaultEstimateEntry, MIN_LINES_NUMBER)],
|
||||
attachments: []
|
||||
attachments: [],
|
||||
pdf_template_id: '',
|
||||
};
|
||||
|
||||
const ERRORS = {
|
||||
@@ -262,3 +264,12 @@ export const resetFormState = ({ initialValues, values, resetForm }) => {
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const useEstimateFormBrandingTemplatesOptions = () => {
|
||||
const { brandingTemplates } = useEstimateFormContext();
|
||||
|
||||
return React.useMemo(
|
||||
() => convertBrandingTemplatesToOptions(brandingTemplates),
|
||||
[brandingTemplates],
|
||||
);
|
||||
};
|
||||
|
||||
@@ -7,6 +7,11 @@ import {
|
||||
NavbarGroup,
|
||||
Intent,
|
||||
Alignment,
|
||||
Menu,
|
||||
MenuItem,
|
||||
Popover,
|
||||
PopoverInteractionKind,
|
||||
Position,
|
||||
} from '@blueprintjs/core';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
|
||||
@@ -20,6 +25,7 @@ import {
|
||||
DashboardFilterButton,
|
||||
DashboardRowsHeightButton,
|
||||
DashboardActionsBar,
|
||||
FSelect,
|
||||
} from '@/components';
|
||||
|
||||
import withEstimates from './withEstimates';
|
||||
@@ -35,6 +41,12 @@ import { useDownloadExportPdf } from '@/hooks/query/FinancialReports/use-export-
|
||||
import { SaleEstimateAction, AbilitySubject } from '@/constants/abilityOption';
|
||||
import { compose } from '@/utils';
|
||||
import { DialogsName } from '@/constants/dialogs';
|
||||
import withDrawerActions from '@/containers/Drawer/withDrawerActions';
|
||||
import { DRAWERS } from '@/constants/drawers';
|
||||
import {
|
||||
BrandingThemeFormGroup,
|
||||
BrandingThemeSelectButton,
|
||||
} from '@/containers/BrandingTemplates/BrandingTemplatesSelectFields';
|
||||
|
||||
/**
|
||||
* Estimates list actions bar.
|
||||
@@ -52,6 +64,9 @@ function EstimateActionsBar({
|
||||
// #withDialogActions
|
||||
openDialog,
|
||||
|
||||
// #withDrawerActions
|
||||
openDrawer,
|
||||
|
||||
// #withSettingsActions
|
||||
addSetting,
|
||||
}) {
|
||||
@@ -96,6 +111,10 @@ function EstimateActionsBar({
|
||||
const handlePrintBtnClick = () => {
|
||||
downloadExportPdf({ resource: 'SaleEstimate' });
|
||||
};
|
||||
// Handle customize button clicl.
|
||||
const handleCustomizeBtnClick = () => {
|
||||
openDrawer(DRAWERS.BRANDING_TEMPLATES, { resource: 'SaleEstimate' });
|
||||
};
|
||||
|
||||
return (
|
||||
<DashboardActionsBar>
|
||||
@@ -167,6 +186,25 @@ function EstimateActionsBar({
|
||||
</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 Templates'}
|
||||
/>
|
||||
</Menu>
|
||||
}
|
||||
>
|
||||
<Button icon={<Icon icon="cog-16" iconSize={16} />} minimal={true} />
|
||||
</Popover>
|
||||
<NavbarDivider />
|
||||
<Button
|
||||
className={Classes.MINIMAL}
|
||||
icon={<Icon icon="refresh-16" iconSize={14} />}
|
||||
@@ -187,4 +225,5 @@ export default compose(
|
||||
estimatesTableSize: estimatesSettings?.tableSize,
|
||||
})),
|
||||
withDialogActions,
|
||||
withDrawerActions,
|
||||
)(EstimateActionsBar);
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
// @ts-nocheck
|
||||
import { Classes } from '@blueprintjs/core';
|
||||
import { useDrawerContext } from '@/components/Drawer/DrawerProvider';
|
||||
import { InvoiceCustomizeContent } from './InvoiceCustomizeContent';
|
||||
import { BrandingTemplateBoot } from '@/containers/BrandingTemplates/BrandingTemplateBoot';
|
||||
import { Box } from '@/components';
|
||||
|
||||
export default function InvoiceCustomize() {
|
||||
const { payload } = useDrawerContext();
|
||||
const templateId = payload.templateId;
|
||||
|
||||
return (
|
||||
<Box className={Classes.DRAWER_BODY}>
|
||||
<BrandingTemplateBoot templateId={templateId}>
|
||||
<InvoiceCustomizeContent />
|
||||
</BrandingTemplateBoot>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import React from 'react';
|
||||
import * as R from 'ramda';
|
||||
import { useFormikContext } from 'formik';
|
||||
import {
|
||||
InvoicePaperTemplate,
|
||||
InvoicePaperTemplateProps,
|
||||
} from './InvoicePaperTemplate';
|
||||
import { ElementCustomize } from '../../../ElementCustomize/ElementCustomize';
|
||||
import { InvoiceCustomizeGeneralField } from './InvoiceCustomizeGeneralFields';
|
||||
import { InvoiceCustomizeContentFields } from './InvoiceCutomizeContentFields';
|
||||
import { InvoiceCustomizeValues } from './types';
|
||||
import { InvoiceCustomizeSchema } from './InvoiceCustomizeForm.schema';
|
||||
import { useDrawerContext } from '@/components/Drawer/DrawerProvider';
|
||||
import { useDrawerActions } from '@/hooks/state';
|
||||
import { BrandingTemplateForm } from '@/containers/BrandingTemplates/BrandingTemplateForm';
|
||||
import { initialValues } from './constants';
|
||||
|
||||
export function InvoiceCustomizeContent() {
|
||||
const { payload, name } = useDrawerContext();
|
||||
const { closeDrawer } = useDrawerActions();
|
||||
const templateId = payload?.templateId || null;
|
||||
|
||||
const handleSuccess = () => {
|
||||
closeDrawer(name);
|
||||
};
|
||||
|
||||
return (
|
||||
<BrandingTemplateForm<InvoiceCustomizeValues>
|
||||
templateId={templateId}
|
||||
defaultValues={initialValues}
|
||||
validationSchema={InvoiceCustomizeSchema}
|
||||
onSuccess={handleSuccess}
|
||||
resource={'SaleInvoice'}
|
||||
>
|
||||
<ElementCustomize.PaperTemplate>
|
||||
<InvoicePaperTemplateFormConnected />
|
||||
</ElementCustomize.PaperTemplate>
|
||||
|
||||
<ElementCustomize.FieldsTab id={'general'} label={'General'}>
|
||||
<InvoiceCustomizeGeneralField />
|
||||
</ElementCustomize.FieldsTab>
|
||||
|
||||
<ElementCustomize.FieldsTab id={'content'} label={'Content'}>
|
||||
<InvoiceCustomizeContentFields />
|
||||
</ElementCustomize.FieldsTab>
|
||||
</BrandingTemplateForm>
|
||||
);
|
||||
}
|
||||
|
||||
const withFormikProps = <P extends object>(
|
||||
Component: React.ComponentType<P>,
|
||||
) => {
|
||||
return (props: Omit<P, keyof InvoicePaperTemplateProps>) => {
|
||||
const { values } = useFormikContext<InvoicePaperTemplateProps>();
|
||||
|
||||
return <Component {...(props as P)} {...values} />;
|
||||
};
|
||||
};
|
||||
|
||||
export const InvoicePaperTemplateFormConnected =
|
||||
R.compose(withFormikProps)(InvoicePaperTemplate);
|
||||
@@ -0,0 +1,30 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import * as R from 'ramda';
|
||||
import { Drawer, DrawerSuspense } from '@/components';
|
||||
import withDrawers from '@/containers/Drawer/withDrawers';
|
||||
|
||||
const InvoiceCustomize = React.lazy(() => import('./InvoiceCustomize'));
|
||||
|
||||
/**
|
||||
* Invoice customize drawer.
|
||||
* @returns {React.ReactNode}
|
||||
*/
|
||||
function InvoiceCustomizeDrawerRoot({
|
||||
name,
|
||||
// #withDrawer
|
||||
isOpen,
|
||||
payload,
|
||||
}) {
|
||||
return (
|
||||
<Drawer isOpen={isOpen} name={name} size={'100%'} payload={payload}>
|
||||
<DrawerSuspense>
|
||||
<InvoiceCustomize />
|
||||
</DrawerSuspense>
|
||||
</Drawer>
|
||||
);
|
||||
}
|
||||
|
||||
export const InvoiceCustomizeDrawer = R.compose(withDrawers())(
|
||||
InvoiceCustomizeDrawerRoot,
|
||||
);
|
||||
@@ -0,0 +1,24 @@
|
||||
.root {
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.mainFields{
|
||||
width: 400px;
|
||||
height: 100vh;
|
||||
}
|
||||
.fieldGroup {
|
||||
|
||||
:global .bp4-form-content{
|
||||
margin-left: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.footerActions{
|
||||
padding: 10px 16px;
|
||||
border-top: 1px solid #d9d9d9;
|
||||
flex-flow: row-reverse;
|
||||
}
|
||||
|
||||
.showCompanyLogoField:global(.bp4-large){
|
||||
font-size: 14px;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import * as Yup from 'yup';
|
||||
|
||||
export const InvoiceCustomizeSchema = Yup.object().shape({
|
||||
templateName: Yup.string().required('Template Name is required'),
|
||||
});
|
||||
@@ -0,0 +1,3 @@
|
||||
.showCompanyLogoField{
|
||||
font-size: 14px;
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
// @ts-nocheck
|
||||
import { Classes, Text } from '@blueprintjs/core';
|
||||
import {
|
||||
FFormGroup,
|
||||
FieldRequiredHint,
|
||||
FInputGroup,
|
||||
FSwitch,
|
||||
Group,
|
||||
Stack,
|
||||
} from '@/components';
|
||||
import { FColorInput } from '@/components/Forms/FColorInput';
|
||||
import { CreditCardIcon } from '@/icons/CreditCardIcon';
|
||||
import { Overlay } from './Overlay';
|
||||
import { useIsTemplateNamedFilled } from '@/containers/BrandingTemplates/utils';
|
||||
|
||||
export function InvoiceCustomizeGeneralField() {
|
||||
const isTemplateNameFilled = useIsTemplateNamedFilled();
|
||||
|
||||
return (
|
||||
<Stack style={{ padding: 20, flex: '1 1 auto' }}>
|
||||
<Stack spacing={0}>
|
||||
<h2 style={{ fontSize: 16, marginBottom: 10 }}>General Branding</h2>
|
||||
<p className={Classes.TEXT_MUTED}>
|
||||
Set your invoice details to be automatically applied every timeyou
|
||||
create a new invoice.
|
||||
</p>
|
||||
</Stack>
|
||||
|
||||
<FFormGroup
|
||||
name={'templateName'}
|
||||
label={'Template Name'}
|
||||
labelInfo={<FieldRequiredHint />}
|
||||
fastField
|
||||
style={{ marginBottom: 10 }}
|
||||
>
|
||||
<FInputGroup name={'templateName'} fastField />
|
||||
</FFormGroup>
|
||||
|
||||
<Overlay visible={!isTemplateNameFilled}>
|
||||
<Stack spacing={0}>
|
||||
<FFormGroup
|
||||
name={'primaryColor'}
|
||||
label={'Primary Color'}
|
||||
style={{ justifyContent: 'space-between' }}
|
||||
inline
|
||||
fastField
|
||||
>
|
||||
<FColorInput
|
||||
name={'primaryColor'}
|
||||
inputProps={{ style: { maxWidth: 120 } }}
|
||||
fastField
|
||||
/>
|
||||
</FFormGroup>
|
||||
|
||||
<FFormGroup
|
||||
name={'secondaryColor'}
|
||||
label={'Secondary Color'}
|
||||
style={{ justifyContent: 'space-between' }}
|
||||
inline
|
||||
fastField
|
||||
>
|
||||
<FColorInput
|
||||
name={'secondaryColor'}
|
||||
inputProps={{ style: { maxWidth: 120 } }}
|
||||
fastField
|
||||
/>
|
||||
</FFormGroup>
|
||||
|
||||
<FFormGroup name={'showCompanyLogo'} label={'Logo'} fastField>
|
||||
<FSwitch
|
||||
name={'showCompanyLogo'}
|
||||
label={'Display company logo in the paper'}
|
||||
style={{ fontSize: 14 }}
|
||||
large
|
||||
fastField
|
||||
/>
|
||||
</FFormGroup>
|
||||
</Stack>
|
||||
|
||||
<InvoiceCustomizePaymentManage />
|
||||
</Overlay>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
function InvoiceCustomizePaymentManage() {
|
||||
return (
|
||||
<Group
|
||||
style={{
|
||||
backgroundColor: '#FBFBFB',
|
||||
border: '1px solid #E1E1E1',
|
||||
padding: 10,
|
||||
borderRadius: 5,
|
||||
}}
|
||||
position={'apart'}
|
||||
>
|
||||
<Group spacing={10}>
|
||||
<CreditCardIcon fill={'#7D8897'} height={16} width={16} />
|
||||
<Text>Accept payment methods</Text>
|
||||
</Group>
|
||||
|
||||
<a href={'#'}>Manage</a>
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
// @ts-nocheck
|
||||
import { Stack } from '@/components';
|
||||
import { Classes } from '@blueprintjs/core';
|
||||
import { fieldsGroups } from './constants';
|
||||
import {
|
||||
ElementCustomizeFieldsGroup,
|
||||
ElementCustomizeContentItemFieldGroup,
|
||||
} from '@/containers/ElementCustomize/ElementCustomizeFieldsGroup';
|
||||
|
||||
export function InvoiceCustomizeContentFields() {
|
||||
return (
|
||||
<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>
|
||||
|
||||
<Stack>
|
||||
{fieldsGroups.map((group) => (
|
||||
<ElementCustomizeFieldsGroup label={group.label}>
|
||||
{group.fields.map((item, index) => (
|
||||
<ElementCustomizeContentItemFieldGroup
|
||||
key={index}
|
||||
inputGroupProps={{
|
||||
name: item.enableKey,
|
||||
label: item.label,
|
||||
}}
|
||||
switchProps={{
|
||||
name: item.labelKey,
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</ElementCustomizeFieldsGroup>
|
||||
))}
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
|
||||
.root {
|
||||
border-radius: 5px;
|
||||
background-color: #fff;
|
||||
color: #111;
|
||||
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;
|
||||
margin: 0 auto;
|
||||
width: 794px;
|
||||
height: 1123px;
|
||||
}
|
||||
|
||||
.bigTitle{
|
||||
font-size: 60px;
|
||||
margin: 0;
|
||||
line-height: 1;
|
||||
margin-bottom: 25px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.details {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
.detail {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 12px;
|
||||
}
|
||||
.detailLabel {
|
||||
min-width: 120px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.addressRoot{
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.addressBillTo{
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.addressFrom{
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
text-align: left;
|
||||
|
||||
thead th{
|
||||
font-weight: 400;
|
||||
border-bottom: 1px solid #000;
|
||||
padding: 2px 10px;
|
||||
color: #333;
|
||||
|
||||
&.rate,
|
||||
&.total{
|
||||
text-align: right;
|
||||
}
|
||||
&:first-of-type{
|
||||
padding-left: 0;
|
||||
}
|
||||
&:last-of-type{
|
||||
padding-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
tbody{
|
||||
|
||||
tr {
|
||||
|
||||
}
|
||||
td{
|
||||
border-bottom: 1px solid #F6F6F6;
|
||||
padding: 12px 10px;
|
||||
|
||||
&:first-of-type{
|
||||
padding-left: 0;
|
||||
}
|
||||
&:last-of-type{
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
&.rate,
|
||||
&.total{
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.totals{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-left: auto;
|
||||
width: 300px;
|
||||
}
|
||||
.totalsItem{
|
||||
display: flex;
|
||||
padding: 4px 0;
|
||||
}
|
||||
.totalsItemLabel{
|
||||
min-width: 160px;
|
||||
}
|
||||
.totalsItemAmount{
|
||||
flex: 1 1 auto;
|
||||
text-align: right;
|
||||
}
|
||||
.totalBottomBordered {
|
||||
border-bottom: 1px solid #000;
|
||||
}
|
||||
.totalBottomGrayBordered {
|
||||
border-bottom: 1px solid #DADADA;
|
||||
}
|
||||
|
||||
.logoWrap{
|
||||
height: 120px;
|
||||
width: 120px;
|
||||
position: absolute;
|
||||
right: 26px;
|
||||
top: 26px;
|
||||
border-radius: 5px;
|
||||
overflow: hidden;
|
||||
|
||||
img{
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.footer{
|
||||
|
||||
}
|
||||
|
||||
.paragraph{
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.paragraphLabel{
|
||||
color: #666;
|
||||
}
|
||||
@@ -0,0 +1,298 @@
|
||||
import React from 'react';
|
||||
import { PaperTemplate, PaperTemplateTotalBorder } from './PaperTemplate';
|
||||
import { Group, Stack } from '@/components';
|
||||
|
||||
interface PapaerLine {
|
||||
item?: string;
|
||||
description?: string;
|
||||
quantity?: string;
|
||||
rate?: string;
|
||||
total?: string;
|
||||
}
|
||||
|
||||
interface PaperTax {
|
||||
label: string;
|
||||
amount: string;
|
||||
}
|
||||
|
||||
export interface InvoicePaperTemplateProps {
|
||||
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;
|
||||
|
||||
// Address
|
||||
showBillingToAddress?: boolean;
|
||||
showBilledFromAddress?: boolean;
|
||||
billedToLabel?: string;
|
||||
|
||||
// Entries
|
||||
lineItemLabel?: string;
|
||||
lineDescriptionLabel?: string;
|
||||
lineRateLabel?: string;
|
||||
lineTotalLabel?: string;
|
||||
|
||||
// Totals
|
||||
showTotal?: boolean;
|
||||
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 | React.ReactNode>;
|
||||
billedToAddress?: Array<string | React.ReactNode>;
|
||||
}
|
||||
|
||||
export function InvoicePaperTemplate({
|
||||
primaryColor,
|
||||
secondaryColor,
|
||||
|
||||
companyName = 'Bigcapital Technology, Inc.',
|
||||
|
||||
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
|
||||
lineItemLabel = 'Item',
|
||||
lineDescriptionLabel = 'Description',
|
||||
lineRateLabel = 'Rate',
|
||||
lineTotalLabel = 'Total',
|
||||
|
||||
totalLabel = 'Total',
|
||||
subtotalLabel = 'Subtotal',
|
||||
discountLabel = 'Discount',
|
||||
paymentMadeLabel = 'Payment Made',
|
||||
balanceDueLabel = 'Balance Due',
|
||||
|
||||
// Totals
|
||||
showTotal = true,
|
||||
showSubtotal = true,
|
||||
showDiscount = true,
|
||||
showTaxes = true,
|
||||
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',
|
||||
],
|
||||
}: InvoicePaperTemplateProps) {
|
||||
return (
|
||||
<PaperTemplate
|
||||
primaryColor={primaryColor}
|
||||
secondaryColor={secondaryColor}
|
||||
showCompanyLogo={showCompanyLogo}
|
||||
companyLogo={companyLogo}
|
||||
bigtitle={'Invoice'}
|
||||
>
|
||||
<Stack spacing={24}>
|
||||
<PaperTemplate.TermsList>
|
||||
{showInvoiceNumber && (
|
||||
<PaperTemplate.TermsItem label={invoiceNumberLabel}>
|
||||
{invoiceNumber}
|
||||
</PaperTemplate.TermsItem>
|
||||
)}
|
||||
{showDateIssue && (
|
||||
<PaperTemplate.TermsItem label={dateIssueLabel}>
|
||||
{dateIssue}
|
||||
</PaperTemplate.TermsItem>
|
||||
)}
|
||||
{showDueDate && (
|
||||
<PaperTemplate.TermsItem label={dueDateLabel}>
|
||||
{dueDate}
|
||||
</PaperTemplate.TermsItem>
|
||||
)}
|
||||
</PaperTemplate.TermsList>
|
||||
|
||||
<Group spacing={10}>
|
||||
{showBilledFromAddress && (
|
||||
<PaperTemplate.Address
|
||||
items={[<strong>{companyName}</strong>, ...billedFromAddres]}
|
||||
/>
|
||||
)}
|
||||
{showBillingToAddress && (
|
||||
<PaperTemplate.Address
|
||||
items={[<strong>{billedToLabel}</strong>, ...billedToAddress]}
|
||||
/>
|
||||
)}
|
||||
</Group>
|
||||
|
||||
<Stack spacing={0}>
|
||||
<PaperTemplate.Table
|
||||
columns={[
|
||||
{ label: lineItemLabel, accessor: 'item' },
|
||||
{ label: lineDescriptionLabel, accessor: 'description' },
|
||||
{ label: lineRateLabel, accessor: 'rate', align: 'right' },
|
||||
{ label: lineTotalLabel, accessor: 'total', align: 'right' },
|
||||
]}
|
||||
data={lines}
|
||||
/>
|
||||
<PaperTemplate.Totals>
|
||||
{showSubtotal && (
|
||||
<PaperTemplate.TotalLine
|
||||
label={subtotalLabel}
|
||||
amount={subtotal}
|
||||
border={PaperTemplateTotalBorder.Gray}
|
||||
/>
|
||||
)}
|
||||
{showDiscount && (
|
||||
<PaperTemplate.TotalLine
|
||||
label={discountLabel}
|
||||
amount={discount}
|
||||
/>
|
||||
)}
|
||||
{showTaxes && (
|
||||
<>
|
||||
{taxes.map((tax, index) => (
|
||||
<PaperTemplate.TotalLine
|
||||
key={index}
|
||||
label={tax.label}
|
||||
amount={tax.amount}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
{showTotal && (
|
||||
<PaperTemplate.TotalLine
|
||||
label={totalLabel}
|
||||
amount={total}
|
||||
border={PaperTemplateTotalBorder.Dark}
|
||||
style={{ fontWeight: 500 }}
|
||||
/>
|
||||
)}
|
||||
{showPaymentMade && (
|
||||
<PaperTemplate.TotalLine
|
||||
label={paymentMadeLabel}
|
||||
amount={paymentMade}
|
||||
/>
|
||||
)}
|
||||
{showBalanceDue && (
|
||||
<PaperTemplate.TotalLine
|
||||
label={balanceDueLabel}
|
||||
amount={balanceDue}
|
||||
border={PaperTemplateTotalBorder.Dark}
|
||||
style={{ fontWeight: 500 }}
|
||||
/>
|
||||
)}
|
||||
</PaperTemplate.Totals>
|
||||
</Stack>
|
||||
|
||||
<Stack spacing={0}>
|
||||
{showTermsConditions && (
|
||||
<PaperTemplate.Statement label={termsConditionsLabel}>
|
||||
{termsConditions}
|
||||
</PaperTemplate.Statement>
|
||||
)}
|
||||
{showStatement && (
|
||||
<PaperTemplate.Statement label={statementLabel}>
|
||||
{statement}
|
||||
</PaperTemplate.Statement>
|
||||
)}
|
||||
</Stack>
|
||||
</Stack>
|
||||
</PaperTemplate>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
|
||||
|
||||
.root {
|
||||
position: relative;
|
||||
|
||||
&.visible::before{
|
||||
background: rgba(255, 255, 255, 0.75);
|
||||
z-index: 2;
|
||||
}
|
||||
&::before{
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: -1000;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import clsx from 'classnames';
|
||||
import { Box } from '@/components';
|
||||
import styles from './Overlay.module.scss';
|
||||
|
||||
export interface OverlayProps {
|
||||
visible?: boolean;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
export function Overlay({ children, visible }: OverlayProps) {
|
||||
return (
|
||||
<Box
|
||||
className={clsx(styles.root, {
|
||||
[styles.visible]: visible,
|
||||
})}
|
||||
>
|
||||
{children}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
import React from 'react';
|
||||
import clsx from 'classnames';
|
||||
import { get } from 'lodash';
|
||||
import { Stack } from '@/components';
|
||||
import styles from './InvoicePaperTemplate.module.scss';
|
||||
|
||||
export interface PaperTemplateProps {
|
||||
primaryColor?: string;
|
||||
secondaryColor?: string;
|
||||
|
||||
showCompanyLogo?: boolean;
|
||||
companyLogo?: string;
|
||||
companyName?: string;
|
||||
|
||||
bigtitle?: string;
|
||||
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
export function PaperTemplate({
|
||||
primaryColor,
|
||||
secondaryColor,
|
||||
showCompanyLogo,
|
||||
companyLogo,
|
||||
bigtitle = 'Invoice',
|
||||
children,
|
||||
}: PaperTemplateProps) {
|
||||
return (
|
||||
<div className={styles.root}>
|
||||
<style>{`:root { --invoice-primary-color: ${primaryColor}; --invoice-secondary-color: ${secondaryColor}; }`}</style>
|
||||
|
||||
<div>
|
||||
<h1 className={styles.bigTitle}>{bigtitle}</h1>
|
||||
|
||||
{showCompanyLogo && (
|
||||
<div className={styles.logoWrap}>
|
||||
<img alt="" src={companyLogo} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface PaperTemplateTableProps {
|
||||
columns: Array<{
|
||||
accessor: string;
|
||||
label: string;
|
||||
align?: 'left' | 'center' | 'right';
|
||||
}>;
|
||||
data: Array<Record<string, any>>;
|
||||
}
|
||||
|
||||
PaperTemplate.Table = ({ columns, data }: PaperTemplateTableProps) => {
|
||||
return (
|
||||
<table className={styles.table}>
|
||||
<thead>
|
||||
<tr>
|
||||
{columns.map((col, index) => (
|
||||
<th key={index} align={col.align}>
|
||||
{col.label}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody className={styles.tableBody}>
|
||||
{data.map((_data: any) => (
|
||||
<tr>
|
||||
{columns.map((column, index) => (
|
||||
<td align={column.align} key={index}>
|
||||
{get(_data, column.accessor)}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
};
|
||||
|
||||
export enum PaperTemplateTotalBorder {
|
||||
Gray = 'gray',
|
||||
Dark = 'dark',
|
||||
}
|
||||
|
||||
PaperTemplate.Totals = ({ children }: { children: React.ReactNode }) => {
|
||||
return <div className={clsx(styles.totals)}>{children}</div>;
|
||||
};
|
||||
PaperTemplate.TotalLine = ({
|
||||
label,
|
||||
amount,
|
||||
border,
|
||||
style,
|
||||
}: {
|
||||
label: string;
|
||||
amount: string;
|
||||
border?: PaperTemplateTotalBorder;
|
||||
style?: any;
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
className={clsx(styles.totalsItem, {
|
||||
[styles.totalBottomBordered]: border === PaperTemplateTotalBorder.Dark,
|
||||
[styles.totalBottomGrayBordered]:
|
||||
border === PaperTemplateTotalBorder.Gray,
|
||||
})}
|
||||
style={style}
|
||||
>
|
||||
<div className={styles.totalsItemLabel}>{label}</div>
|
||||
<div className={styles.totalsItemAmount}>{amount}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
PaperTemplate.MutedText = () => {};
|
||||
|
||||
PaperTemplate.Text = () => {};
|
||||
|
||||
PaperTemplate.Address = ({
|
||||
items,
|
||||
}: {
|
||||
items: Array<string | React.ReactNode>;
|
||||
}) => {
|
||||
return (
|
||||
<Stack spacing={0}>
|
||||
{items.map((item, index) => (
|
||||
<div key={index}>{item}</div>
|
||||
))}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
PaperTemplate.Statement = ({
|
||||
label,
|
||||
children,
|
||||
}: {
|
||||
label: string;
|
||||
children: React.ReactNode;
|
||||
}) => {
|
||||
return (
|
||||
<div className={styles.paragraph}>
|
||||
{label && <div className={styles.paragraphLabel}>{label}</div>}
|
||||
<div>{children}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
PaperTemplate.TermsList = ({ children }: { children: React.ReactNode }) => {
|
||||
return <div className={styles.details}>{children}</div>;
|
||||
};
|
||||
|
||||
PaperTemplate.TermsItem = ({
|
||||
label,
|
||||
children,
|
||||
}: {
|
||||
label: string;
|
||||
children: React.ReactNode;
|
||||
}) => {
|
||||
return (
|
||||
<div className={styles.detail}>
|
||||
<div className={styles.detailLabel}>{label}</div>
|
||||
<div>{children}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,136 @@
|
||||
export const initialValues = {
|
||||
templateName: '',
|
||||
|
||||
// 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',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,60 @@
|
||||
import { BrandingTemplateValues } from "@/containers/BrandingTemplates/types";
|
||||
|
||||
export interface InvoiceCustomizeValues extends BrandingTemplateValues {
|
||||
// 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;
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import { omit } from 'lodash';
|
||||
import { useFormikContext } from 'formik';
|
||||
import { InvoiceCustomizeValues } from './types';
|
||||
import {
|
||||
CreatePdfTemplateValues,
|
||||
EditPdfTemplateValues,
|
||||
} from '@/hooks/query/pdf-templates';
|
||||
import { transformToForm } from '@/utils';
|
||||
import { initialValues } from './constants';
|
||||
import { useBrandingTemplateBoot } from '@/containers/BrandingTemplates/BrandingTemplateBoot';
|
||||
|
||||
export const transformToEditRequest = (
|
||||
values: InvoiceCustomizeValues,
|
||||
): EditPdfTemplateValues => {
|
||||
return {
|
||||
templateName: values.templateName,
|
||||
attributes: omit(values, ['templateName']),
|
||||
};
|
||||
};
|
||||
|
||||
export const transformToNewRequest = (
|
||||
values: InvoiceCustomizeValues,
|
||||
): CreatePdfTemplateValues => {
|
||||
return {
|
||||
resource: 'SaleInvoice',
|
||||
templateName: values.templateName,
|
||||
attributes: omit(values, ['templateName']),
|
||||
};
|
||||
};
|
||||
|
||||
export const useInvoiceCustomizeInitialValues = (): InvoiceCustomizeValues => {
|
||||
const { pdfTemplate } = useBrandingTemplateBoot();
|
||||
|
||||
const defaultPdfTemplate = {
|
||||
templateName: pdfTemplate?.templateName,
|
||||
...pdfTemplate?.attributes,
|
||||
};
|
||||
return {
|
||||
...initialValues,
|
||||
...(transformToForm(
|
||||
defaultPdfTemplate,
|
||||
initialValues,
|
||||
) as InvoiceCustomizeValues),
|
||||
};
|
||||
};
|
||||
@@ -1,5 +1,5 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import React, { useMemo } from 'react';
|
||||
import {
|
||||
Intent,
|
||||
Button,
|
||||
@@ -10,12 +10,17 @@ import {
|
||||
Menu,
|
||||
MenuItem,
|
||||
} from '@blueprintjs/core';
|
||||
import classNames from 'classnames';
|
||||
import { CLASSES } from '@/constants/classes';
|
||||
import { useFormikContext } from 'formik';
|
||||
import { If, Icon, FormattedMessage as T, Group } from '@/components';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import classNames from 'classnames';
|
||||
import { useFormikContext } from 'formik';
|
||||
import { CLASSES } from '@/constants/classes';
|
||||
import { If, Icon, FormattedMessage as T, Group, FSelect } from '@/components';
|
||||
import { useInvoiceFormContext } from './InvoiceFormProvider';
|
||||
import { useInvoiceFormBrandingTemplatesOptions } from './utils';
|
||||
import {
|
||||
BrandingThemeFormGroup,
|
||||
BrandingThemeSelectButton,
|
||||
} from '@/containers/BrandingTemplates/BrandingTemplatesSelectFields';
|
||||
|
||||
/**
|
||||
* Invoice floating actions bar.
|
||||
@@ -75,6 +80,8 @@ export default function InvoiceFloatingActions() {
|
||||
resetForm();
|
||||
};
|
||||
|
||||
const brandingTemplatesOptions = useInvoiceFormBrandingTemplatesOptions();
|
||||
|
||||
return (
|
||||
<Group
|
||||
spacing={10}
|
||||
@@ -192,6 +199,26 @@ export default function InvoiceFloatingActions() {
|
||||
onClick={handleCancelBtnClick}
|
||||
text={<T id={'cancel'} />}
|
||||
/>
|
||||
|
||||
{/* ----------- Branding Template Select ----------- */}
|
||||
<BrandingThemeFormGroup
|
||||
name={'pdf_template_id'}
|
||||
label={'Branding'}
|
||||
inline
|
||||
fastField
|
||||
style={{ marginLeft: 20 }}
|
||||
>
|
||||
<FSelect
|
||||
name={'pdf_template_id'}
|
||||
items={brandingTemplatesOptions}
|
||||
input={({ activeItem, text, label, value }) => (
|
||||
<BrandingThemeSelectButton text={text || 'Brand Theme'} minimal />
|
||||
)}
|
||||
filterable={false}
|
||||
popoverProps={{ minimal: true }}
|
||||
/>
|
||||
</BrandingThemeFormGroup>
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
} from '@/hooks/query';
|
||||
import { useProjects } from '@/containers/Projects/hooks';
|
||||
import { useTaxRates } from '@/hooks/query/taxRates';
|
||||
import { useGetPdfTemplates } from '@/hooks/query/pdf-templates';
|
||||
|
||||
const InvoiceFormContext = createContext();
|
||||
|
||||
@@ -55,6 +56,10 @@ function InvoiceFormProvider({ invoiceId, baseCurrency, ...props }) {
|
||||
{ enabled: !!estimateId },
|
||||
);
|
||||
|
||||
// Fetches branding templates of invoice.
|
||||
const { data: brandingTemplates, isLoading: isBrandingTemplatesLoading } =
|
||||
useGetPdfTemplates({ resource: 'SaleInvoice' });
|
||||
|
||||
const newInvoice = !isEmpty(estimate)
|
||||
? transformToEditForm({
|
||||
...pick(estimate, ['customer_id', 'currency_code', 'entries']),
|
||||
@@ -105,7 +110,7 @@ function InvoiceFormProvider({ invoiceId, baseCurrency, ...props }) {
|
||||
|
||||
// Determines whether the warehouse and branches are loading.
|
||||
const isFeatureLoading =
|
||||
isWarehouesLoading || isBranchesLoading || isProjectsLoading;
|
||||
isWarehouesLoading || isBranchesLoading || isProjectsLoading || isBrandingTemplatesLoading;
|
||||
|
||||
const provider = {
|
||||
invoice,
|
||||
@@ -119,6 +124,7 @@ function InvoiceFormProvider({ invoiceId, baseCurrency, ...props }) {
|
||||
warehouses,
|
||||
projects,
|
||||
taxRates,
|
||||
brandingTemplates,
|
||||
|
||||
isInvoiceLoading,
|
||||
isItemsLoading,
|
||||
@@ -130,6 +136,7 @@ function InvoiceFormProvider({ invoiceId, baseCurrency, ...props }) {
|
||||
isBranchesSuccess,
|
||||
isWarehousesSuccess,
|
||||
isTaxRatesLoading,
|
||||
isBrandingTemplatesLoading,
|
||||
|
||||
createInvoiceMutate,
|
||||
editInvoiceMutate,
|
||||
|
||||
@@ -10,7 +10,6 @@ import {
|
||||
compose,
|
||||
transformToForm,
|
||||
repeatValue,
|
||||
formattedAmount,
|
||||
defaultFastFieldShouldUpdate,
|
||||
} from '@/utils';
|
||||
import { ERROR } from '@/constants/errors';
|
||||
@@ -31,6 +30,7 @@ import {
|
||||
transformAttachmentsToForm,
|
||||
transformAttachmentsToRequest,
|
||||
} from '@/containers/Attachments/utils';
|
||||
import { convertBrandingTemplatesToOptions } from '@/containers/BrandingTemplates/BrandingTemplatesSelectFields';
|
||||
|
||||
export const MIN_LINES_NUMBER = 1;
|
||||
|
||||
@@ -66,6 +66,7 @@ export const defaultInvoice = {
|
||||
branch_id: '',
|
||||
warehouse_id: '',
|
||||
project_id: '',
|
||||
pdf_template_id: '',
|
||||
entries: [...repeatValue(defaultInvoiceEntry, MIN_LINES_NUMBER)],
|
||||
attachments: [],
|
||||
};
|
||||
@@ -406,3 +407,12 @@ export const useInvoiceCurrencyCode = () => {
|
||||
|
||||
return values.currency_code;
|
||||
};
|
||||
|
||||
export const useInvoiceFormBrandingTemplatesOptions = () => {
|
||||
const { brandingTemplates } = useInvoiceFormContext();
|
||||
|
||||
return React.useMemo(
|
||||
() => convertBrandingTemplatesToOptions(brandingTemplates),
|
||||
[brandingTemplates],
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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.BRANDING_TEMPLATES, { resource: 'SaleInvoice' });
|
||||
};
|
||||
|
||||
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 Templates'}
|
||||
/>
|
||||
</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);
|
||||
|
||||
@@ -12,10 +12,15 @@ import {
|
||||
MenuItem,
|
||||
} from '@blueprintjs/core';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { Group, Icon, FormattedMessage as T } from '@/components';
|
||||
import { FSelect, Group, Icon, FormattedMessage as T } from '@/components';
|
||||
import { useFormikContext } from 'formik';
|
||||
import { usePaymentReceiveFormContext } from './PaymentReceiveFormProvider';
|
||||
import { CLASSES } from '@/constants/classes';
|
||||
import {
|
||||
BrandingThemeFormGroup,
|
||||
BrandingThemeSelectButton,
|
||||
} from '@/containers/BrandingTemplates/BrandingTemplatesSelectFields';
|
||||
import { usePaymentReceivedFormBrandingTemplatesOptions } from './utils';
|
||||
|
||||
/**
|
||||
* Payment receive floating actions bar.
|
||||
@@ -53,6 +58,9 @@ export default function PaymentReceiveFormFloatingActions() {
|
||||
submitForm();
|
||||
};
|
||||
|
||||
const brandingTemplatesOpts =
|
||||
usePaymentReceivedFormBrandingTemplatesOptions();
|
||||
|
||||
return (
|
||||
<Group
|
||||
spacing={10}
|
||||
@@ -109,6 +117,25 @@ export default function PaymentReceiveFormFloatingActions() {
|
||||
onClick={handleCancelBtnClick}
|
||||
text={<T id={'cancel'} />}
|
||||
/>
|
||||
|
||||
{/* ----------- Branding Template Select ----------- */}
|
||||
<BrandingThemeFormGroup
|
||||
name={'pdf_template_id'}
|
||||
label={'Branding'}
|
||||
inline
|
||||
fastField
|
||||
style={{ marginLeft: 20 }}
|
||||
>
|
||||
<FSelect
|
||||
name={'pdf_template_id'}
|
||||
items={brandingTemplatesOpts}
|
||||
input={({ activeItem, text, label, value }) => (
|
||||
<BrandingThemeSelectButton text={text || 'Brand Theme'} minimal />
|
||||
)}
|
||||
filterable={false}
|
||||
popoverProps={{ minimal: true }}
|
||||
/>
|
||||
</BrandingThemeFormGroup>
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
useCreatePaymentReceive,
|
||||
useEditPaymentReceive,
|
||||
} from '@/hooks/query';
|
||||
import { useGetPdfTemplates } from '@/hooks/query/pdf-templates';
|
||||
|
||||
// Payment receive form context.
|
||||
const PaymentReceiveFormContext = createContext();
|
||||
@@ -65,6 +66,10 @@ function PaymentReceiveFormProvider({ query, paymentReceiveId, ...props }) {
|
||||
isLoading: isProjectsLoading,
|
||||
} = useProjects({}, { enabled: !!isProjectsFeatureCan });
|
||||
|
||||
// Fetches branding templates of payment received module.
|
||||
const { data: brandingTemplates, isLoading: isBrandingTemplatesLoading } =
|
||||
useGetPdfTemplates({ resource: 'PaymentReceive' });
|
||||
|
||||
// Detarmines whether the new mode.
|
||||
const isNewMode = !paymentReceiveId;
|
||||
|
||||
@@ -102,13 +107,20 @@ function PaymentReceiveFormProvider({ query, paymentReceiveId, ...props }) {
|
||||
|
||||
isExcessConfirmed,
|
||||
setIsExcessConfirmed,
|
||||
|
||||
// Branding templates
|
||||
brandingTemplates,
|
||||
isBrandingTemplatesLoading,
|
||||
};
|
||||
|
||||
const isLoading =
|
||||
isPaymentLoading ||
|
||||
isAccountsLoading ||
|
||||
isCustomersLoading ||
|
||||
isBrandingTemplatesLoading;
|
||||
|
||||
return (
|
||||
<DashboardInsider
|
||||
loading={isPaymentLoading || isAccountsLoading || isCustomersLoading}
|
||||
name={'payment-receive-form'}
|
||||
>
|
||||
<DashboardInsider loading={isLoading} name={'payment-receive-form'}>
|
||||
<PaymentReceiveFormContext.Provider value={provider} {...props} />
|
||||
</DashboardInsider>
|
||||
);
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
transformAttachmentsToForm,
|
||||
transformAttachmentsToRequest,
|
||||
} from '@/containers/Attachments/utils';
|
||||
import { convertBrandingTemplatesToOptions } from '@/containers/BrandingTemplates/BrandingTemplatesSelectFields';
|
||||
|
||||
// Default payment receive entry.
|
||||
export const defaultPaymentReceiveEntry = {
|
||||
@@ -44,10 +45,11 @@ export const defaultPaymentReceive = {
|
||||
statement: '',
|
||||
amount: '',
|
||||
currency_code: '',
|
||||
branch_id: '',
|
||||
exchange_rate: 1,
|
||||
entries: [],
|
||||
attachments: [],
|
||||
branch_id: '',
|
||||
pdf_template_id: '',
|
||||
};
|
||||
|
||||
export const defaultRequestPaymentEntry = {
|
||||
@@ -303,3 +305,12 @@ export const getExceededAmountFromValues = (values) => {
|
||||
|
||||
return totalAmount - totalApplied;
|
||||
};
|
||||
|
||||
export const usePaymentReceivedFormBrandingTemplatesOptions = () => {
|
||||
const { brandingTemplates } = usePaymentReceiveFormContext();
|
||||
|
||||
return React.useMemo(
|
||||
() => convertBrandingTemplatesToOptions(brandingTemplates),
|
||||
[brandingTemplates],
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import { Box } from '@/components';
|
||||
import { Classes } from '@blueprintjs/core';
|
||||
import { useDrawerContext } from '@/components/Drawer/DrawerProvider';
|
||||
import { PaymentReceivedCustomizeContent } from './PaymentReceivedCustomizeContent';
|
||||
import { BrandingTemplateBoot } from '@/containers/BrandingTemplates/BrandingTemplateBoot';
|
||||
|
||||
export default function PaymentReceivedCustomize() {
|
||||
const { payload } = useDrawerContext();
|
||||
const templateId = payload.templateId;
|
||||
|
||||
return (
|
||||
<Box className={Classes.DRAWER_BODY}>
|
||||
<BrandingTemplateBoot templateId={templateId}>
|
||||
<PaymentReceivedCustomizeContent />
|
||||
</BrandingTemplateBoot>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import { useFormikContext } from 'formik';
|
||||
import { ElementCustomize } from '../../../ElementCustomize/ElementCustomize';
|
||||
import { PaymentReceivedCustomizeGeneralField } from './PaymentReceivedCustomizeFieldsGeneral';
|
||||
import { PaymentReceivedCustomizeContentFields } from './PaymentReceivedCustomizeFieldsContent';
|
||||
import { PaymentReceivedCustomizeValues } from './types';
|
||||
import { PaymentReceivedPaperTemplate } from './PaymentReceivedPaperTemplate';
|
||||
import { initialValues } from './constants';
|
||||
import { useDrawerContext } from '@/components/Drawer/DrawerProvider';
|
||||
import { useDrawerActions } from '@/hooks/state';
|
||||
import { BrandingTemplateForm } from '@/containers/BrandingTemplates/BrandingTemplateForm';
|
||||
|
||||
export function PaymentReceivedCustomizeContent() {
|
||||
const { payload, name } = useDrawerContext();
|
||||
const { closeDrawer } = useDrawerActions();
|
||||
|
||||
const templateId = payload?.templateId || null;
|
||||
|
||||
const handleSuccess = () => {
|
||||
closeDrawer(name);
|
||||
};
|
||||
|
||||
return (
|
||||
<BrandingTemplateForm<PaymentReceivedCustomizeValues>
|
||||
templateId={templateId}
|
||||
defaultValues={initialValues}
|
||||
onSuccess={handleSuccess}
|
||||
resource={'PaymentReceive'}
|
||||
>
|
||||
<ElementCustomize.PaperTemplate>
|
||||
<PaymentReceivedPaperTemplateFormConnected />
|
||||
</ElementCustomize.PaperTemplate>
|
||||
|
||||
<ElementCustomize.FieldsTab id={'general'} label={'General'}>
|
||||
<PaymentReceivedCustomizeGeneralField />
|
||||
</ElementCustomize.FieldsTab>
|
||||
|
||||
<ElementCustomize.FieldsTab id={'content'} label={'Content'}>
|
||||
<PaymentReceivedCustomizeContentFields />
|
||||
</ElementCustomize.FieldsTab>
|
||||
</BrandingTemplateForm>
|
||||
);
|
||||
}
|
||||
|
||||
function PaymentReceivedPaperTemplateFormConnected() {
|
||||
const { values } = useFormikContext<PaymentReceivedCustomizeValues>();
|
||||
|
||||
return <PaymentReceivedPaperTemplate {...values} />;
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import * as R from 'ramda';
|
||||
import { Drawer, DrawerSuspense } from '@/components';
|
||||
import withDrawers from '@/containers/Drawer/withDrawers';
|
||||
|
||||
const PaymentReceivedCustomize = React.lazy(
|
||||
() => import('./PaymentReceivedCustomize'),
|
||||
);
|
||||
|
||||
/**
|
||||
* PaymentReceived customize drawer.
|
||||
* @returns {React.ReactNode}
|
||||
*/
|
||||
function PaymentReceivedCustomizeDrawerRoot({
|
||||
name,
|
||||
// #withDrawer
|
||||
isOpen,
|
||||
payload
|
||||
}) {
|
||||
return (
|
||||
<Drawer isOpen={isOpen} name={name} size={'100%'} payload={payload}>
|
||||
<DrawerSuspense>
|
||||
<PaymentReceivedCustomize />
|
||||
</DrawerSuspense>
|
||||
</Drawer>
|
||||
);
|
||||
}
|
||||
|
||||
export const PaymentReceivedCustomizeDrawer = R.compose(withDrawers())(
|
||||
PaymentReceivedCustomizeDrawerRoot,
|
||||
);
|
||||
@@ -0,0 +1,44 @@
|
||||
// @ts-nocheck
|
||||
import { Stack } from '@/components';
|
||||
import { Classes } from '@blueprintjs/core';
|
||||
import { fieldsGroups } from './constants';
|
||||
import {
|
||||
ElementCustomizeContentItemFieldGroup,
|
||||
ElementCustomizeFieldsGroup,
|
||||
} from '@/containers/ElementCustomize/ElementCustomizeFieldsGroup';
|
||||
|
||||
export function PaymentReceivedCustomizeContentFields() {
|
||||
return (
|
||||
<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>
|
||||
|
||||
<Stack>
|
||||
{fieldsGroups.map((group) => (
|
||||
<ElementCustomizeFieldsGroup label={group.label}>
|
||||
{group.fields.map((item, index) => (
|
||||
<ElementCustomizeContentItemFieldGroup
|
||||
key={index}
|
||||
inputGroupProps={{
|
||||
name: item.enableKey,
|
||||
label: item.label,
|
||||
}}
|
||||
switchProps={{
|
||||
name: item.labelKey,
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</ElementCustomizeFieldsGroup>
|
||||
))}
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
// @ts-nocheck
|
||||
import { Classes } from '@blueprintjs/core';
|
||||
import {
|
||||
FFormGroup,
|
||||
FieldRequiredHint,
|
||||
FInputGroup,
|
||||
FSwitch,
|
||||
Stack,
|
||||
} from '@/components';
|
||||
import { FColorInput } from '@/components/Forms/FColorInput';
|
||||
import { Overlay } from '../../Invoices/InvoiceCustomize/Overlay';
|
||||
import { useIsTemplateNamedFilled } from '@/containers/BrandingTemplates/utils';
|
||||
|
||||
export function PaymentReceivedCustomizeGeneralField() {
|
||||
const isTemplateNameFilled = useIsTemplateNamedFilled();
|
||||
|
||||
return (
|
||||
<Stack style={{ padding: 20, flex: '1 1 auto' }}>
|
||||
<Stack spacing={0}>
|
||||
<h2 style={{ fontSize: 16, marginBottom: 10 }}>General Branding</h2>
|
||||
<p className={Classes.TEXT_MUTED}>
|
||||
Set your invoice details to be automatically applied every timeyou
|
||||
create a new invoice.
|
||||
</p>
|
||||
</Stack>
|
||||
|
||||
<FFormGroup
|
||||
name={'templateName'}
|
||||
label={'Template Name'}
|
||||
labelInfo={<FieldRequiredHint />}
|
||||
style={{ marginBottom: 10 }}
|
||||
fastField
|
||||
>
|
||||
<FInputGroup name={'templateName'} fastField />
|
||||
</FFormGroup>
|
||||
|
||||
<Overlay visible={!isTemplateNameFilled}>
|
||||
<Stack spacing={0}>
|
||||
<FFormGroup
|
||||
name={'primaryColor'}
|
||||
label={'Primary Color'}
|
||||
style={{ justifyContent: 'space-between' }}
|
||||
inline
|
||||
fastField
|
||||
>
|
||||
<FColorInput
|
||||
name={'primaryColor'}
|
||||
inputProps={{ style: { maxWidth: 120 } }}
|
||||
fastField
|
||||
/>
|
||||
</FFormGroup>
|
||||
|
||||
<FFormGroup
|
||||
name={'secondaryColor'}
|
||||
label={'Secondary Color'}
|
||||
style={{ justifyContent: 'space-between' }}
|
||||
inline
|
||||
fastField
|
||||
>
|
||||
<FColorInput
|
||||
name={'secondaryColor'}
|
||||
inputProps={{ style: { maxWidth: 120 } }}
|
||||
fastField
|
||||
/>
|
||||
</FFormGroup>
|
||||
|
||||
<FFormGroup name={'showCompanyLogo'} label={'Logo'} fastField>
|
||||
<FSwitch
|
||||
name={'showCompanyLogo'}
|
||||
label={'Display company logo in the paper'}
|
||||
style={{ fontSize: 14 }}
|
||||
large
|
||||
fastField
|
||||
/>
|
||||
</FFormGroup>
|
||||
</Stack>
|
||||
</Overlay>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
import { Group, Stack } from '@/components';
|
||||
import {
|
||||
PaperTemplate,
|
||||
PaperTemplateProps,
|
||||
PaperTemplateTotalBorder,
|
||||
} from '../../Invoices/InvoiceCustomize/PaperTemplate';
|
||||
|
||||
export interface PaymentReceivedPaperTemplateProps extends PaperTemplateProps {
|
||||
billedToAddress?: Array<string>;
|
||||
showBillingToAddress?: boolean;
|
||||
|
||||
billedFromAddress?: Array<string>;
|
||||
showBilledFromAddress?: boolean;
|
||||
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({
|
||||
primaryColor,
|
||||
secondaryColor,
|
||||
showCompanyLogo = true,
|
||||
companyLogo,
|
||||
companyName = 'Bigcapital Technology, Inc.',
|
||||
|
||||
billedToAddress = [
|
||||
'Bigcapital Technology, Inc.',
|
||||
'131 Continental Dr Suite 305 Newark,',
|
||||
'Delaware 19713',
|
||||
'United States',
|
||||
'+1 762-339-5634',
|
||||
'ahmed@bigcapital.app',
|
||||
],
|
||||
billedFromAddress = [
|
||||
'131 Continental Dr Suite 305 Newark,',
|
||||
'Delaware 19713',
|
||||
'United States',
|
||||
'+1 762-339-5634',
|
||||
'ahmed@bigcapital.app',
|
||||
],
|
||||
showBilledFromAddress,
|
||||
showBillingToAddress,
|
||||
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}
|
||||
showCompanyLogo={showCompanyLogo}
|
||||
companyLogo={companyLogo}
|
||||
bigtitle={'Payment'}
|
||||
>
|
||||
<Stack spacing={24}>
|
||||
<PaperTemplate.TermsList>
|
||||
{showPaymentReceivedNumber && (
|
||||
<PaperTemplate.TermsItem label={paymentReceivedNumberLabel}>
|
||||
{paymentReceivedNumebr}
|
||||
</PaperTemplate.TermsItem>
|
||||
)}
|
||||
|
||||
{showPaymentReceivedDate && (
|
||||
<PaperTemplate.TermsItem label={paymentReceivedDateLabel}>
|
||||
{paymentReceivedDate}
|
||||
</PaperTemplate.TermsItem>
|
||||
)}
|
||||
</PaperTemplate.TermsList>
|
||||
|
||||
<Group spacing={10}>
|
||||
{showBilledFromAddress && (
|
||||
<PaperTemplate.Address
|
||||
items={[<strong>{companyName}</strong>, ...billedFromAddress]}
|
||||
/>
|
||||
)}
|
||||
{showBillingToAddress && (
|
||||
<PaperTemplate.Address
|
||||
items={[<strong>{billedToLabel}</strong>, ...billedToAddress]}
|
||||
/>
|
||||
)}
|
||||
</Group>
|
||||
|
||||
<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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
export const initialValues = {
|
||||
templateName: '',
|
||||
|
||||
// Colors
|
||||
primaryColor: '#2c3dd8',
|
||||
secondaryColor: '#2c3dd8',
|
||||
|
||||
// Company logo.
|
||||
showCompanyLogo: true,
|
||||
companyLogo:
|
||||
'https://cdn-development.mercury.com/demo-assets/avatars/mercury-demo-dark.png',
|
||||
|
||||
// Top details.
|
||||
showPaymentReceivedNumber: true,
|
||||
paymentReceivedNumberLabel: 'Payment number',
|
||||
|
||||
showPaymentReceivedDate: true,
|
||||
paymentReceivedDateLabel: 'Date of Issue',
|
||||
|
||||
// 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',
|
||||
|
||||
showTotal: true,
|
||||
totalLabel: 'Total',
|
||||
};
|
||||
|
||||
export const fieldsGroups = [
|
||||
{
|
||||
label: 'Header',
|
||||
fields: [
|
||||
{
|
||||
labelKey: 'paymentReceivedNumberLabel',
|
||||
enableKey: 'showPaymentReceivedNumber',
|
||||
label: 'Payment No.',
|
||||
},
|
||||
{
|
||||
labelKey: 'paymentReceivedDateLabel',
|
||||
enableKey: 'showPaymentReceivedDate',
|
||||
label: 'Payment Date',
|
||||
},
|
||||
{
|
||||
enableKey: 'showBillingToAddress',
|
||||
labelKey: 'billedToLabel',
|
||||
label: 'Bill To',
|
||||
},
|
||||
{
|
||||
enableKey: 'showBilledFromAddress',
|
||||
label: 'Billed From',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Totals',
|
||||
fields: [
|
||||
{
|
||||
labelKey: 'subtotalLabel',
|
||||
enableKey: 'showSubtotal',
|
||||
label: 'Subtotal',
|
||||
},
|
||||
{ labelKey: 'totalLabel', enableKey: 'showTotal', label: 'Total' },
|
||||
],
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,60 @@
|
||||
import { BrandingTemplateValues } from '@/containers/BrandingTemplates/types';
|
||||
|
||||
export interface PaymentReceivedCustomizeValues extends BrandingTemplateValues {
|
||||
// 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;
|
||||
}
|
||||
@@ -7,6 +7,11 @@ import {
|
||||
NavbarGroup,
|
||||
Intent,
|
||||
Alignment,
|
||||
Popover,
|
||||
Menu,
|
||||
MenuItem,
|
||||
PopoverInteractionKind,
|
||||
Position,
|
||||
} from '@blueprintjs/core';
|
||||
|
||||
import { useHistory } from 'react-router-dom';
|
||||
@@ -38,6 +43,8 @@ import { useDownloadExportPdf } from '@/hooks/query/FinancialReports/use-export-
|
||||
|
||||
import { compose } from '@/utils';
|
||||
import { DialogsName } from '@/constants/dialogs';
|
||||
import withDrawerActions from '@/containers/Drawer/withDrawerActions';
|
||||
import { DRAWERS } from '@/constants/drawers';
|
||||
|
||||
/**
|
||||
* Payment receives actions bar.
|
||||
@@ -57,6 +64,9 @@ function PaymentsReceivedActionsBar({
|
||||
|
||||
// #withDialogActions
|
||||
openDialog,
|
||||
|
||||
// #withDrawerActions
|
||||
openDrawer,
|
||||
}) {
|
||||
// History context.
|
||||
const history = useHistory();
|
||||
@@ -101,6 +111,10 @@ function PaymentsReceivedActionsBar({
|
||||
const handlePrintBtnClick = () => {
|
||||
downloadExportPdf({ resource: 'PaymentReceive' });
|
||||
};
|
||||
// Handle the customize button click.
|
||||
const handleCustomizeBtnClick = () => {
|
||||
openDrawer(DRAWERS.BRANDING_TEMPLATES, { resource: 'PaymentReceive' });
|
||||
};
|
||||
|
||||
return (
|
||||
<DashboardActionsBar>
|
||||
@@ -170,6 +184,25 @@ function PaymentsReceivedActionsBar({
|
||||
<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 Templates'}
|
||||
/>
|
||||
</Menu>
|
||||
}
|
||||
>
|
||||
<Button icon={<Icon icon="cog-16" iconSize={16} />} minimal={true} />
|
||||
</Popover>
|
||||
<NavbarDivider />
|
||||
<Button
|
||||
className={Classes.MINIMAL}
|
||||
icon={<Icon icon="refresh-16" iconSize={14} />}
|
||||
@@ -191,4 +224,5 @@ export default compose(
|
||||
paymentReceivesTableSize: paymentReceiveSettings?.tableSize,
|
||||
})),
|
||||
withDialogActions,
|
||||
withDrawerActions,
|
||||
)(PaymentsReceivedActionsBar);
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import { useFormikContext } from 'formik';
|
||||
import { ElementCustomize } from '../../../ElementCustomize/ElementCustomize';
|
||||
import { ReceiptCustomizeGeneralField } from './ReceiptCustomizeFieldsGeneral';
|
||||
import { ReceiptCustomizeFieldsContent } from './ReceiptCustomizeFieldsContent';
|
||||
import { ReceiptPaperTemplate } from './ReceiptPaperTemplate';
|
||||
import { ReceiptCustomizeValues } from './types';
|
||||
import { initialValues } from './constants';
|
||||
import { BrandingTemplateForm } from '@/containers/BrandingTemplates/BrandingTemplateForm';
|
||||
import { useDrawerActions } from '@/hooks/state';
|
||||
import { useDrawerContext } from '@/components/Drawer/DrawerProvider';
|
||||
|
||||
export function ReceiptCustomizeContent() {
|
||||
const { payload, name } = useDrawerContext();
|
||||
const { closeDrawer } = useDrawerActions();
|
||||
const templateId = payload?.templateId || null;
|
||||
|
||||
const handleFormSuccess = () => {
|
||||
closeDrawer(name);
|
||||
};
|
||||
|
||||
return (
|
||||
<BrandingTemplateForm<ReceiptCustomizeValues>
|
||||
templateId={templateId}
|
||||
initialValues={initialValues}
|
||||
onSuccess={handleFormSuccess}
|
||||
resource={'SaleReceipt'}
|
||||
>
|
||||
<ElementCustomize.PaperTemplate>
|
||||
<ReceiptPaperTemplateFormConnected />
|
||||
</ElementCustomize.PaperTemplate>
|
||||
|
||||
<ElementCustomize.FieldsTab id={'general'} label={'General'}>
|
||||
<ReceiptCustomizeGeneralField />
|
||||
</ElementCustomize.FieldsTab>
|
||||
|
||||
<ElementCustomize.FieldsTab id={'content'} label={'Content'}>
|
||||
<ReceiptCustomizeFieldsContent />
|
||||
</ElementCustomize.FieldsTab>
|
||||
</BrandingTemplateForm>
|
||||
);
|
||||
}
|
||||
|
||||
function ReceiptPaperTemplateFormConnected() {
|
||||
const { values } = useFormikContext<ReceiptCustomizeValues>();
|
||||
|
||||
return <ReceiptPaperTemplate {...values} />;
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import * as R from 'ramda';
|
||||
import { Drawer, DrawerSuspense } from '@/components';
|
||||
import withDrawers from '@/containers/Drawer/withDrawers';
|
||||
|
||||
const ReceiptCustomizeDrawerBody = React.lazy(
|
||||
() => import('./ReceiptCustomizeDrawerBody'),
|
||||
);
|
||||
|
||||
/**
|
||||
* Receipt customize drawer.
|
||||
* @returns {React.ReactNode}
|
||||
*/
|
||||
function ReceiptCustomizeDrawerRoot({
|
||||
name,
|
||||
// #withDrawer
|
||||
isOpen,
|
||||
payload,
|
||||
}) {
|
||||
return (
|
||||
<Drawer isOpen={isOpen} name={name} size={'100%'} payload={payload}>
|
||||
<DrawerSuspense>
|
||||
<ReceiptCustomizeDrawerBody />
|
||||
</DrawerSuspense>
|
||||
</Drawer>
|
||||
);
|
||||
}
|
||||
|
||||
export const ReceiptCustomizeDrawer = R.compose(withDrawers())(
|
||||
ReceiptCustomizeDrawerRoot,
|
||||
);
|
||||
@@ -0,0 +1,18 @@
|
||||
import { Box } from '@/components';
|
||||
import { Classes } from '@blueprintjs/core';
|
||||
import { ReceiptCustomizeContent } from './ReceiptCustomizeContent';
|
||||
import { BrandingTemplateBoot } from '@/containers/BrandingTemplates/BrandingTemplateBoot';
|
||||
import { useDrawerContext } from '@/components/Drawer/DrawerProvider';
|
||||
|
||||
export default function ReceiptCustomizeDrawerBody() {
|
||||
const { payload } = useDrawerContext();
|
||||
const templateId = payload.templateId;
|
||||
|
||||
return (
|
||||
<Box className={Classes.DRAWER_BODY}>
|
||||
<BrandingTemplateBoot templateId={templateId}>
|
||||
<ReceiptCustomizeContent />
|
||||
</BrandingTemplateBoot>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
// @ts-nocheck
|
||||
import { Stack } from '@/components';
|
||||
import { Classes } from '@blueprintjs/core';
|
||||
import {
|
||||
ElementCustomizeContentItemFieldGroup,
|
||||
ElementCustomizeFieldsGroup,
|
||||
} from '@/containers/ElementCustomize/ElementCustomizeFieldsGroup';
|
||||
import { fieldsGroups } from './constants';
|
||||
|
||||
export function ReceiptCustomizeFieldsContent() {
|
||||
return (
|
||||
<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>
|
||||
|
||||
<Stack>
|
||||
{fieldsGroups.map((group) => (
|
||||
<ElementCustomizeFieldsGroup label={group.label}>
|
||||
{group.fields.map((item, index) => (
|
||||
<ElementCustomizeContentItemFieldGroup
|
||||
key={index}
|
||||
inputGroupProps={{
|
||||
name: item.enableKey,
|
||||
label: item.label,
|
||||
}}
|
||||
switchProps={{
|
||||
name: item.labelKey,
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</ElementCustomizeFieldsGroup>
|
||||
))}
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user