feat: hook up the invice customize api

This commit is contained in:
Ahmed Bouhuolia
2024-09-12 14:16:07 +02:00
parent a7df23cebc
commit 632c4629de
21 changed files with 391 additions and 169 deletions

View File

@@ -1,7 +1,14 @@
// @ts-nocheck
import React, { createContext, useContext } from 'react';
const DrawerContext = createContext();
interface DrawerContextValue {
name: string;
payload: Record<string, any>;
}
const DrawerContext = createContext<DrawerContextValue>(
{} as DrawerContextValue,
);
/**
* Account form provider.

View File

@@ -47,7 +47,7 @@ export function ElementCustomizeFieldsMain() {
function ElementCustomizeFooterActionsRoot({ closeDrawer }) {
const { name } = useDrawerContext();
const { submitForm } = useFormikContext();
const { submitForm, isSubmitting } = useFormikContext();
const handleSubmitBtnClick = () => {
submitForm();
@@ -62,6 +62,7 @@ function ElementCustomizeFooterActionsRoot({ closeDrawer }) {
onClick={handleSubmitBtnClick}
intent={Intent.PRIMARY}
style={{ minWidth: 75 }}
loading={isSubmitting}
>
Save
</Button>

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

@@ -2,13 +2,7 @@
import * as R from 'ramda';
import { Button, Classes, Intent } from '@blueprintjs/core';
import { BrandingTemplatesBoot } from './BrandingTemplatesBoot';
import {
Box,
Card,
DrawerBody,
DrawerHeaderContent,
Group,
} from '@/components';
import { Box, Card, DrawerHeaderContent, Group } from '@/components';
import { DRAWERS } from '@/constants/drawers';
import { BrandingTemplatesTable } from './BrandingTemplatesTable';
import withDrawerActions from '@/containers/Drawer/withDrawerActions';

View File

@@ -18,7 +18,6 @@ function BrandingTemplateTableRoot({
}: BrandingTemplatesTableProps) {
// Table columns.
const columns = useBrandingTemplatesColumns();
const { isPdfTemplatesLoading, pdfTemplates } = useBrandingTemplatesBoot();
const handleEditTemplate = (template) => {
@@ -70,7 +69,7 @@ const useBrandingTemplatesColumns = () => {
Header: 'Template Name',
accessor: (row) => (
<Group spacing={10}>
{row.template_name} <Tag round>Default</Tag>
{row.template_name} {row.default && <Tag round>Default</Tag>}
</Group>
),
width: 65,

View File

@@ -1,98 +1,19 @@
import React from 'react';
import * as R from 'ramda';
import { AppToaster, Box } from '@/components';
import { Classes, Intent } from '@blueprintjs/core';
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 { initialValues } from './constants';
import {
useCreatePdfTemplate,
useEditPdfTemplate,
} from '@/hooks/query/pdf-templates';
import { transformToEditRequest, transformToNewRequest } from './utils';
// @ts-nocheck
import { Classes } from '@blueprintjs/core';
import { useDrawerContext } from '@/components/Drawer/DrawerProvider';
import { BrandingTemplateBoot } from '../BrandingTemplates/BrandingTemplateBoot';
import { InvoiceCustomizeContent } from './InvoiceCustomizeContent';
import { Box } from '@/components';
export default function InvoiceCustomizeContent() {
const { mutateAsync: createPdfTemplate } = useCreatePdfTemplate();
const { mutateAsync: editPdfTemplate } = useEditPdfTemplate();
const templateId: number = 1;
const handleFormSubmit = (values: InvoiceCustomizeValues) => {
const handleSuccess = (message: string) => {
AppToaster.show({
intent: Intent.SUCCESS,
message,
});
};
const handleError = (message: string) => {
AppToaster.show({
intent: Intent.DANGER,
message,
});
};
if (templateId) {
const reqValues = transformToEditRequest(values, templateId);
// 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);
// Create new template
createPdfTemplate(reqValues)
.then(() => handleSuccess('PDF template created successfully!'))
.catch(() =>
handleError('An error occurred while creating the PDF template.'),
);
}
};
export default function InvoiceCustomize() {
const { payload } = useDrawerContext();
const templateId = payload.templateId;
return (
<Box className={Classes.DRAWER_BODY}>
<ElementCustomize<InvoiceCustomizeValues>
initialValues={initialValues}
onSubmit={handleFormSubmit}
>
<ElementCustomize.PaperTemplate>
<InvoicePaperTemplateFormConnected />
</ElementCustomize.PaperTemplate>
<ElementCustomize.FieldsTab id={'general'} label={'General'}>
<InvoiceCustomizeGeneralField />
</ElementCustomize.FieldsTab>
<ElementCustomize.FieldsTab id={'content'} label={'Content'}>
<InvoiceCustomizeContentFields />
</ElementCustomize.FieldsTab>
<ElementCustomize.FieldsTab id={'totals'} label={'Totals'}>
asdfasdfdsaf #3
</ElementCustomize.FieldsTab>
</ElementCustomize>
<BrandingTemplateBoot templateId={templateId}>
<InvoiceCustomizeContent />
</BrandingTemplateBoot>
</Box>
);
}
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,109 @@
import React from 'react';
import * as R from 'ramda';
import { AppToaster } from '@/components';
import { Intent } from '@blueprintjs/core';
import { FormikHelpers, 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 {
useCreatePdfTemplate,
useEditPdfTemplate,
} from '@/hooks/query/pdf-templates';
import {
transformToEditRequest,
transformToNewRequest,
useInvoiceCustomizeInitialValues,
} from './utils';
import { InvoiceCustomizeSchema } from './InvoiceCustomizeForm.schema';
import { useDrawerContext } from '@/components/Drawer/DrawerProvider';
import { useDrawerActions } from '@/hooks/state';
export function InvoiceCustomizeContent() {
const { mutateAsync: createPdfTemplate } = useCreatePdfTemplate();
const { mutateAsync: editPdfTemplate } = useEditPdfTemplate();
const { payload, name } = useDrawerContext();
const { closeDrawer } = useDrawerActions();
const templateId = payload?.templateId || null;
const handleFormSubmit = (
values: InvoiceCustomizeValues,
{ setSubmitting }: FormikHelpers<InvoiceCustomizeValues>,
) => {
const handleSuccess = (message: string) => {
AppToaster.show({ intent: Intent.SUCCESS, message });
setSubmitting(false);
closeDrawer(name);
};
const handleError = (message: string) => {
AppToaster.show({ intent: Intent.DANGER, message });
setSubmitting(false);
};
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);
setSubmitting(true);
// Create new template
createPdfTemplate(reqValues)
.then(() => handleSuccess('PDF template created successfully!'))
.catch(() =>
handleError('An error occurred while creating the PDF template.'),
);
}
};
const initialValues = useInvoiceCustomizeInitialValues();
return (
<ElementCustomize<InvoiceCustomizeValues>
initialValues={initialValues}
validationSchema={InvoiceCustomizeSchema}
onSubmit={handleFormSubmit}
>
<ElementCustomize.PaperTemplate>
<InvoicePaperTemplateFormConnected />
</ElementCustomize.PaperTemplate>
<ElementCustomize.FieldsTab id={'general'} label={'General'}>
<InvoiceCustomizeGeneralField />
</ElementCustomize.FieldsTab>
<ElementCustomize.FieldsTab id={'content'} label={'Content'}>
<InvoiceCustomizeContentFields />
</ElementCustomize.FieldsTab>
<ElementCustomize.FieldsTab id={'totals'} label={'Totals'}>
asdfasdfdsaf #3
</ElementCustomize.FieldsTab>
</ElementCustomize>
);
}
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

@@ -4,9 +4,7 @@ import * as R from 'ramda';
import { Drawer, DrawerSuspense } from '@/components';
import withDrawers from '@/containers/Drawer/withDrawers';
const InvoiceCustomize = React.lazy(
() => import('./InvoiceCustomize'),
);
const InvoiceCustomize = React.lazy(() => import('./InvoiceCustomize'));
/**
* Invoice customize drawer.
@@ -16,10 +14,10 @@ function InvoiceCustomizeDrawerRoot({
name,
// #withDrawer
isOpen,
payload: {},
payload,
}) {
return (
<Drawer isOpen={isOpen} name={name} size={'100%'}>
<Drawer isOpen={isOpen} name={name} size={'100%'} payload={payload}>
<DrawerSuspense>
<InvoiceCustomize />
</DrawerSuspense>

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

@@ -1,61 +1,84 @@
// @ts-nocheck
import { Classes, Text } from '@blueprintjs/core';
import { FFormGroup, FSwitch, Group, Stack } from '@/components';
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 './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
Set your invoice details to be automatically applied every timeyou
create a new invoice.
</p>
</Stack>
<Stack spacing={0}>
<FFormGroup
name={'primaryColor'}
label={'Primary Color'}
style={{ justifyContent: 'space-between' }}
inline
fastField
>
<FColorInput
<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'}
inputProps={{ style: { maxWidth: 120 } }}
label={'Primary Color'}
style={{ justifyContent: 'space-between' }}
inline
fastField
/>
</FFormGroup>
>
<FColorInput
name={'primaryColor'}
inputProps={{ style: { maxWidth: 120 } }}
fastField
/>
</FFormGroup>
<FFormGroup
name={'secondaryColor'}
label={'Secondary Color'}
style={{ justifyContent: 'space-between' }}
inline
fastField
>
<FColorInput
<FFormGroup
name={'secondaryColor'}
inputProps={{ style: { maxWidth: 120 } }}
label={'Secondary Color'}
style={{ justifyContent: 'space-between' }}
inline
fastField
/>
</FFormGroup>
>
<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>
<FFormGroup name={'showCompanyLogo'} label={'Logo'} fastField>
<FSwitch
name={'showCompanyLogo'}
label={'Display company logo in the paper'}
style={{ fontSize: 14 }}
large
fastField
/>
</FFormGroup>
</Stack>
<InvoiceCustomizePaymentManage />
<InvoiceCustomizePaymentManage />
</Overlay>
</Stack>
);
}

View File

@@ -16,7 +16,7 @@ export function InvoiceCustomizeContentFields() {
<Stack spacing={10}>
<h3>General Branding</h3>
<p className={Classes.TEXT_MUTED}>
Set your invoice details to be automatically applied every timeyou
Set your invoice details to be automatically applied every timeyou
create a new invoice.
</p>
</Stack>

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

@@ -1,4 +1,6 @@
export const initialValues = {
templateName: '',
// Colors
primaryColor: '#2c3dd8',
secondaryColor: '#2c3dd8',

View File

@@ -1,4 +1,6 @@
export interface InvoiceCustomizeValues {
templateName: string;
// Colors
primaryColor?: string;
secondaryColor?: string;

View File

@@ -1,14 +1,19 @@
import { omit } from 'lodash';
import { useFormikContext } from 'formik';
import { InvoiceCustomizeValues } from './types';
import { CreatePdfTemplateValues, EditPdfTemplateValues } from '@/hooks/query/pdf-templates';
import {
CreatePdfTemplateValues,
EditPdfTemplateValues,
} from '@/hooks/query/pdf-templates';
import { useBrandingTemplateBoot } from '../BrandingTemplates/BrandingTemplateBoot';
import { transformToForm } from '@/utils';
import { initialValues } from './constants';
export const transformToEditRequest = (
values: InvoiceCustomizeValues,
templateId: number,
): EditPdfTemplateValues => {
return {
templateId,
templateName: 'Template Name',
templateName: values.templateName,
attributes: omit(values, ['templateName']),
};
};
@@ -18,7 +23,29 @@ export const transformToNewRequest = (
): CreatePdfTemplateValues => {
return {
resource: 'SaleInvoice',
templateName: 'Template Name',
templateName: values.templateName,
attributes: omit(values, ['templateName']),
};
};
export const useIsTemplateNamedFilled = () => {
const { values } = useFormikContext<InvoiceCustomizeValues>();
return values.templateName && values.templateName?.length >= 4;
};
export const useInvoiceCustomizeInitialValues = (): InvoiceCustomizeValues => {
const { pdfTemplate } = useBrandingTemplateBoot();
const defaultPdfTemplate = {
templateName: pdfTemplate?.templateName,
...pdfTemplate?.attributes,
};
return {
...initialValues,
...(transformToForm(
defaultPdfTemplate,
initialValues,
) as InvoiceCustomizeValues),
};
};

View File

@@ -190,7 +190,7 @@ function InvoiceActionsBar({
<Menu>
<MenuItem
onClick={handleCustomizeBtnClick}
text={'Customize Invoice'}
text={'Invoice Templates'}
/>
</Menu>
}

View File

@@ -6,8 +6,12 @@ import {
UseQueryOptions,
UseMutationResult,
UseQueryResult,
useQueryClient,
} from 'react-query';
import useApiRequest from '../useRequest';
import { transformToCamelCase, transfromToSnakeCase } from '@/utils';
const PdfTemplatesQueryKey = 'PdfTemplate';
export interface CreatePdfTemplateValues {
templateName: string;
@@ -18,7 +22,6 @@ export interface CreatePdfTemplateValues {
export interface CreatePdfTemplateResponse {}
export interface EditPdfTemplateValues {
templateId: string | number;
templateName: string;
attributes: Record<string, any>;
}
@@ -33,7 +36,14 @@ export interface DeletePdfTemplateResponse {}
export interface GetPdfTemplateValues {}
export interface GetPdfTemplateResponse {}
export interface GetPdfTemplateResponse {
templateName: string;
attributes: Record<string, any>;
predefined: boolean;
default: boolean;
createdAt: string;
updatedAt: string | null;
}
export interface GetPdfTemplatesValues {}
@@ -52,10 +62,18 @@ export const useCreatePdfTemplate = (
CreatePdfTemplateValues
> => {
const apiRequest = useApiRequest();
const queryClient = useQueryClient();
return useMutation<CreatePdfTemplateResponse, Error, CreatePdfTemplateValues>(
(values) =>
apiRequest.post('/pdf-templates', values).then((res) => res.data),
options,
apiRequest
.post('/pdf-templates', transfromToSnakeCase(values))
.then((res) => res.data),
{
onSuccess: () => {
queryClient.invalidateQueries([PdfTemplatesQueryKey]);
},
...options,
},
);
};
@@ -71,6 +89,7 @@ export const useEditPdfTemplate = (
Error,
{ templateId: number; values: EditPdfTemplateValues }
> => {
const queryClient = useQueryClient();
const apiRequest = useApiRequest();
return useMutation<
EditPdfTemplateResponse,
@@ -79,9 +98,14 @@ export const useEditPdfTemplate = (
>(
({ templateId, values }) =>
apiRequest
.put(`/pdf-templates/${templateId}`, values)
.put(`/pdf-templates/${templateId}`, transfromToSnakeCase(values))
.then((res) => res.data),
options,
{
onSuccess: () => {
queryClient.invalidateQueries([PdfTemplatesQueryKey]);
},
...options,
},
);
};
@@ -98,10 +122,16 @@ export const useDeletePdfTemplate = (
{ templateId: number }
> => {
const apiRequest = useApiRequest();
const queryClient = useQueryClient();
return useMutation<DeletePdfTemplateResponse, Error, { templateId: number }>(
({ templateId }) =>
apiRequest.delete(`/pdf-templates/${templateId}`).then((res) => res.data),
options,
{
onSuccess: () => {
queryClient.invalidateQueries([PdfTemplatesQueryKey]);
},
...options,
},
);
};
@@ -112,9 +142,11 @@ export const useGetPdfTemplate = (
): UseQueryResult<GetPdfTemplateResponse, Error> => {
const apiRequest = useApiRequest();
return useQuery<GetPdfTemplateResponse, Error>(
['pdfTemplate', templateId],
[PdfTemplatesQueryKey, templateId],
() =>
apiRequest.get(`/pdf-templates/${templateId}`).then((res) => res.data),
apiRequest
.get(`/pdf-templates/${templateId}`)
.then((res) => transformToCamelCase(res.data)),
options,
);
};
@@ -125,7 +157,7 @@ export const useGetPdfTemplates = (
): UseQueryResult<GetPdfTemplatesResponse, Error> => {
const apiRequest = useApiRequest();
return useQuery<GetPdfTemplatesResponse, Error>(
'pdfTemplates',
PdfTemplatesQueryKey,
() => apiRequest.get('/pdf-templates').then((res) => res.data),
options,
);

View File

@@ -10,6 +10,8 @@ import {
closeSidebarSubmenu,
openDialog,
closeDialog,
openDrawer,
closeDrawer,
} from '@/store/dashboard/dashboard.actions';
export const useDispatchAction = (action) => {
@@ -77,3 +79,10 @@ export const useDialogActions = () => {
closeDialog: useDispatchAction(closeDialog),
};
};
export const useDrawerActions = () => {
return {
openDrawer: useDispatchAction(openDrawer),
closeDrawer: useDispatchAction(closeDrawer),
};
};