Merge branch 'develop' into stripe-integrate

This commit is contained in:
Ahmed Bouhuolia
2024-09-17 19:26:13 +02:00
185 changed files with 9316 additions and 517 deletions

View File

@@ -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,
];

View File

@@ -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;
}
}
}

View File

@@ -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);
};

View File

@@ -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'),
});

View File

@@ -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,
);

View File

@@ -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 };

View File

@@ -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';

View File

@@ -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,
);

View File

@@ -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 } || []),
)
}

View File

@@ -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);

View File

@@ -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>
);
}

View 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,
},
];
};

View 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';
}

View File

@@ -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,
},
];

View File

@@ -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);

View File

@@ -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);

View File

@@ -0,0 +1,5 @@
export interface BrandingTemplateValues {
templateName: string;
}

View File

@@ -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;
};

View File

@@ -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;
}

View File

@@ -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}</>;
};

View File

@@ -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,
);

View File

@@ -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>
);
}

View File

@@ -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;
}

View File

@@ -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>
);
}

View File

@@ -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,
);

View File

@@ -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>
);
}

View File

@@ -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;
};

View File

@@ -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;
}
}

View File

@@ -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>
);
}

View File

@@ -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>
);
};

View File

@@ -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>
);
}

View File

@@ -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);

View File

@@ -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} />;
}

View File

@@ -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,
);

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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',
},
],
},
];

View File

@@ -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;
}

View File

@@ -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>
);
}

View File

@@ -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'}>

View File

@@ -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],
);
};

View File

@@ -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);

View File

@@ -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} />;
}

View File

@@ -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,
);

View File

@@ -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>
);
}

View File

@@ -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 timeyou
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>
);
}

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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',
},
],
},
];

View File

@@ -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;
}

View File

@@ -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>
);
}

View File

@@ -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>
);

View File

@@ -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],
);
};

View File

@@ -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);

View File

@@ -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>
);
}

View File

@@ -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);

View File

@@ -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,
);

View File

@@ -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;
}

View File

@@ -0,0 +1,5 @@
import * as Yup from 'yup';
export const InvoiceCustomizeSchema = Yup.object().shape({
templateName: Yup.string().required('Template Name is required'),
});

View File

@@ -0,0 +1,3 @@
.showCompanyLogoField{
font-size: 14px;
}

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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;
}

View File

@@ -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>
);
}

View File

@@ -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;
}
}

View File

@@ -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>
);
}

View File

@@ -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>
);
};

View File

@@ -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',
},
],
},
];

View File

@@ -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;
}

View File

@@ -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),
};
};

View File

@@ -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>
);
}

View File

@@ -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,

View File

@@ -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],
);
};

View File

@@ -7,6 +7,11 @@ import {
NavbarGroup,
Intent,
Alignment,
Menu,
MenuItem,
Popover,
PopoverInteractionKind,
Position,
} from '@blueprintjs/core';
import { useHistory } from 'react-router-dom';
import {
@@ -32,6 +37,8 @@ import withSettingsActions from '@/containers/Settings/withSettingsActions';
import { compose } from '@/utils';
import withDialogActions from '@/containers/Dialog/withDialogActions';
import { DialogsName } from '@/constants/dialogs';
import withDrawerActions from '@/containers/Drawer/withDrawerActions';
import { DRAWERS } from '@/constants/drawers';
/**
* Invoices table actions bar.
@@ -51,6 +58,9 @@ function InvoiceActionsBar({
// #withDialogsActions
openDialog,
// #withDrawerActions
openDrawer,
}) {
const history = useHistory();
@@ -97,6 +107,11 @@ function InvoiceActionsBar({
downloadExportPdf({ resource: 'SaleInvoice' });
};
// Handles the invoice customize button click.
const handleCustomizeBtnClick = () => {
openDrawer(DRAWERS.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);

View File

@@ -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>
);
}

View File

@@ -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>
);

View File

@@ -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],
);
};

View File

@@ -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>
);
}

View File

@@ -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} />;
}

View File

@@ -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,
);

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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' },
],
},
];

View File

@@ -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;
}

View File

@@ -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);

View File

@@ -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} />;
}

View File

@@ -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,
);

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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 { useIsTemplateNamedFilled } from '@/containers/BrandingTemplates/utils';
import { Overlay } from '../../Invoices/InvoiceCustomize/Overlay';
export function ReceiptCustomizeGeneralField() {
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>
);
}

View File

@@ -0,0 +1,188 @@
import { Group, Stack } from '@/components';
import {
PaperTemplate,
PaperTemplateProps,
} from '../../Invoices/InvoiceCustomize/PaperTemplate';
export interface ReceiptPaperTemplateProps extends PaperTemplateProps {
// Addresses
billedToAddress?: Array<string>;
billedFromAddress?: Array<string>;
showBilledFromAddress?: boolean;
showBilledToAddress?: 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;
}>;
// Receipt Date.
receiptDateLabel?: string;
showReceiptDate?: boolean;
receiptDate?: string;
// Receipt Number
receiptNumebr?: string;
receiptNumberLabel?: string;
showReceiptNumber?: boolean;
}
export function ReceiptPaperTemplate({
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',
],
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',
},
],
showReceiptNumber = true,
receiptNumberLabel = 'Receipt Number',
receiptNumebr = '346D3D40-0001',
receiptDate = 'September 3, 2024',
showReceiptDate = true,
receiptDateLabel = 'Receipt Date',
}: ReceiptPaperTemplateProps) {
return (
<PaperTemplate
primaryColor={primaryColor}
secondaryColor={secondaryColor}
showCompanyLogo={showCompanyLogo}
companyLogo={companyLogo}
bigtitle={'Receipt'}
>
<Stack spacing={24}>
<PaperTemplate.TermsList>
{showReceiptNumber && (
<PaperTemplate.TermsItem label={receiptNumberLabel}>
{receiptNumebr}
</PaperTemplate.TermsItem>
)}
{showReceiptDate && (
<PaperTemplate.TermsItem label={receiptDateLabel}>
{receiptDate}
</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>
);
}

View File

@@ -0,0 +1,103 @@
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',
// Receipt Number
showReceiptNumber: true,
receiptNumberLabel: 'Receipt number',
// Receipt Date
showReceiptDate: true,
receiptDateLabel: 'Date of Issue',
// Company name
companyName: 'Bigcapital Technology, Inc.',
// Addresses
showBilledFromAddress: true,
showBilledToAddress: true,
billedToLabel: 'Billed To',
// Entries
itemNameLabel: 'Item',
itemDescriptionLabel: 'Description',
itemRateLabel: 'Rate',
itemTotalLabel: 'Total',
// Subtotal
showSubtotal: true,
subtotalLabel: 'Subtotal',
// Total
showTotal: true,
totalLabel: 'Total',
// Terms & Conditions
termsConditionsLabel: 'Terms & Conditions',
showTermsConditions: true,
// Customer Note
customerNoteLabel: 'Customer Note',
showCustomerNote: true,
};
export const fieldsGroups = [
{
label: 'Header',
fields: [
{
labelKey: 'receiptNumberLabel',
enableKey: 'showReceiptNumber',
label: 'Receipt Number',
},
{
labelKey: 'receiptDateLabel',
enableKey: 'showReceiptDate',
label: 'Receipt 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: 'Statements',
fields: [
{
enableKey: 'showCustomerNote',
labelKey: 'customerNoteLabel',
label: 'Customer Note',
},
{
enableKey: 'showTermsConditions',
labelKey: 'termsConditionsLabel',
label: 'Terms & Conditions',
},
],
},
];

View File

@@ -0,0 +1,49 @@
import { BrandingTemplateValues } from "@/containers/BrandingTemplates/types";
export interface ReceiptCustomizeValues extends BrandingTemplateValues {
// Colors
primaryColor?: string;
secondaryColor?: string;
// Company Logo
showCompanyLogo?: boolean;
companyLogo?: string;
// Receipt Number
showReceiptNumber?: boolean;
receiptNumberLabel?: string;
// Receipt Date.
showReceiptDate?: boolean;
receiptDateLabel?: string;
// Company name
companyName?: string;
// Addresses
showBilledFromAddress?: boolean;
showBilledToAddress?: boolean;
billedToLabel?: string;
// Entries
itemNameLabel?: string;
itemDescriptionLabel?: string;
itemRateLabel?: string;
itemTotalLabel?: string;
// Subtotal
showSubtotal?: boolean;
subtotalLabel?: string;
// Total
showTotal?: boolean;
totalLabel?: string;
// Terms & Conditions
termsConditionsLabel?: string;
showTermsConditions?: boolean;
// Statement
customerNoteLabel?: string;
showCustomerNote?: boolean;
}

View File

@@ -11,12 +11,17 @@ import {
Menu,
MenuItem,
} from '@blueprintjs/core';
import { Group, FormattedMessage as T } from '@/components';
import { FSelect, Group, FormattedMessage as T } from '@/components';
import { useFormikContext } from 'formik';
import { useHistory } from 'react-router-dom';
import { CLASSES } from '@/constants/classes';
import { If, Icon } from '@/components';
import { useReceiptFormContext } from './ReceiptFormProvider';
import { useReceiptFormBrandingTemplatesOptions } from './utils';
import {
BrandingThemeFormGroup,
BrandingThemeSelectButton,
} from '@/containers/BrandingTemplates/BrandingTemplatesSelectFields';
/**
* Receipt floating actions bar.
@@ -76,6 +81,8 @@ export default function ReceiptFormFloatingActions() {
resetForm();
};
const brandingTemplatesOptions = useReceiptFormBrandingTemplatesOptions();
return (
<Group
spacing={10}
@@ -191,6 +198,25 @@ export default function ReceiptFormFloatingActions() {
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>
);
}

View File

@@ -15,6 +15,7 @@ import {
useEditReceipt,
} from '@/hooks/query';
import { useProjects } from '@/containers/Projects/hooks';
import { useGetPdfTemplates } from '@/hooks/query/pdf-templates';
const ReceiptFormContext = createContext();
@@ -77,7 +78,7 @@ function ReceiptFormProvider({ receiptId, ...props }) {
[],
);
// Handle fetch Items data table or list
// Handle fetch Items data table or list.
const {
data: { items },
isLoading: isItemsLoading,
@@ -85,13 +86,16 @@ function ReceiptFormProvider({ receiptId, ...props }) {
page_size: 10000,
stringified_filter_roles: stringifiedFilterRoles,
});
// Fetch project list.
const {
data: { projects },
isLoading: isProjectsLoading,
} = useProjects({}, { enabled: !!isProjectsFeatureCan });
// Fetches branding templates of receipt.
const { data: brandingTemplates, isLoading: isBrandingTemplatesLoading } =
useGetPdfTemplates({ resource: 'SaleReceipt' });
// Fetch receipt settings.
const { isLoading: isSettingLoading } = useSettingsReceipts();
@@ -101,7 +105,6 @@ function ReceiptFormProvider({ receiptId, ...props }) {
const [submitPayload, setSubmitPayload] = useState({});
const isNewMode = !receiptId;
const isFeatureLoading = isWarehouesLoading || isBranchesLoading;
const provider = {
@@ -130,18 +133,21 @@ function ReceiptFormProvider({ receiptId, ...props }) {
createReceiptMutate,
editReceiptMutate,
setSubmitPayload,
// Branding templates
brandingTemplates,
isBrandingTemplatesLoading,
};
const isLoading =
isReceiptLoading ||
isAccountsLoading ||
isCustomersLoading ||
isItemsLoading ||
isSettingLoading ||
isBrandingTemplatesLoading;
return (
<DashboardInsider
loading={
isReceiptLoading ||
isAccountsLoading ||
isCustomersLoading ||
isItemsLoading ||
isSettingLoading
}
name={'receipt-form'}
>
<DashboardInsider loading={isLoading} name={'receipt-form'}>
<ReceiptFormContext.Provider value={provider} {...props} />
</DashboardInsider>
);

View File

@@ -22,6 +22,7 @@ import {
transformAttachmentsToForm,
transformAttachmentsToRequest,
} from '@/containers/Attachments/utils';
import { convertBrandingTemplatesToOptions } from '@/containers/BrandingTemplates/BrandingTemplatesSelectFields';
export const MIN_LINES_NUMBER = 1;
@@ -61,6 +62,7 @@ export const defaultReceipt = {
currency_code: '',
entries: [...repeatValue(defaultReceiptEntry, MIN_LINES_NUMBER)],
attachments: [],
pdf_template_id: '',
};
const ERRORS = {
@@ -272,3 +274,12 @@ export const resetFormState = ({ initialValues, values, resetForm }) => {
},
});
};
export const useReceiptFormBrandingTemplatesOptions = () => {
const { brandingTemplates } = useReceiptFormContext();
return React.useMemo(
() => convertBrandingTemplatesToOptions(brandingTemplates),
[brandingTemplates],
);
};

View File

@@ -7,6 +7,11 @@ import {
NavbarGroup,
Intent,
Alignment,
Popover,
PopoverInteractionKind,
Position,
Menu,
MenuItem,
} from '@blueprintjs/core';
import { useHistory } from 'react-router-dom';
@@ -38,6 +43,8 @@ import { SaleReceiptAction, AbilitySubject } from '@/constants/abilityOption';
import { DialogsName } from '@/constants/dialogs';
import { compose } from '@/utils';
import withDrawerActions from '@/containers/Drawer/withDrawerActions';
import { DRAWERS } from '@/constants/drawers';
/**
* Receipts actions bar.
@@ -55,6 +62,9 @@ function ReceiptActionsBar({
// #withDialogActions
openDialog,
// #withDrawerActions
openDrawer,
// #withSettingsActions
addSetting,
}) {
@@ -103,6 +113,10 @@ function ReceiptActionsBar({
const handlePrintButtonClick = () => {
downloadExportPdf({ resource: 'SaleReceipt' });
};
// Handle customize button click.
const handleCustomizeBtnClick = () => {
openDrawer(DRAWERS.BRANDING_TEMPLATES, { resource: 'SaleReceipt' });
};
return (
<DashboardActionsBar>
@@ -173,6 +187,25 @@ function ReceiptActionsBar({
<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 Template'}
/>
</Menu>
}
>
<Button icon={<Icon icon="cog-16" iconSize={16} />} minimal={true} />
</Popover>
<NavbarDivider />
<Button
className={Classes.MINIMAL}
icon={<Icon icon="refresh-16" iconSize={14} />}
@@ -193,4 +226,5 @@ export default compose(
receiptsTableSize: receiptSettings?.tableSize,
})),
withDialogActions,
withDrawerActions,
)(ReceiptActionsBar);