feat(webapp): wip tax rate form dialog

This commit is contained in:
Ahmed Bouhuolia
2023-09-18 01:35:53 +02:00
parent fbd74c559b
commit 2356921f27
14 changed files with 348 additions and 119 deletions

View File

@@ -1,12 +1,7 @@
// @ts-nocheck
import React from 'react';
import intl from 'react-intl-universal';
import { Intent, Alert } from '@blueprintjs/core';
import {
AppToaster,
FormattedMessage as T,
FormattedHTMLMessage,
} from '@/components';
import { AppToaster, FormattedMessage as T } from '@/components';
import { useDeleteTaxRate } from '@/hooks/query/taxRates';
@@ -40,7 +35,6 @@ function TaxRateDeleteAlert({
const handleCancelItemDelete = () => {
closeAlert(name);
};
// Handle confirm delete item.
const handleConfirmDeleteItem = () => {
deleteTaxRate(taxRateId)

View File

@@ -1,11 +1,12 @@
// @ts-nocheck
import React, { useCallback } from 'react';
import { useHistory } from 'react-router-dom';
import React from 'react';
import { Intent } from '@blueprintjs/core';
import {
DataTable,
DashboardContentTable,
TableSkeletonHeader,
TableSkeletonRows,
AppToaster,
} from '@/components';
import withAlertsActions from '@/containers/Alert/withAlertActions';
@@ -14,10 +15,6 @@ import withDialogActions from '@/containers/Dialog/withDialogActions';
import withDashboardActions from '@/containers/Dashboard/withDashboardActions';
import withSettings from '@/containers/Settings/withSettings';
// import { useMemorizedColumnsWidths } from '@/hooks';
// import { ActionsMenu } from './components';
// import { useInvoicesListContext } from './InvoicesListProvider';
import { useTaxRatesTableColumns } from './_utils';
import { useTaxRatesLandingContext } from './TaxRatesLandingProvider';
import { TaxRatesLandingEmptyState } from './TaxRatesLandingEmptyState';
@@ -26,6 +23,10 @@ import { TaxRatesTableActionsMenu } from './_components';
import { compose } from '@/utils';
import { DRAWERS } from '@/constants/drawers';
import { DialogsName } from '@/constants/dialogs';
import {
useActivateTaxRate,
useInactivateTaxRate,
} from '@/hooks/query/taxRates';
/**
* Invoices datatable.
@@ -47,6 +48,9 @@ function TaxRatesDataTable({
// Invoices table columns.
const columns = useTaxRatesTableColumns();
const { mutateAsync: activateTaxRateMutate } = useActivateTaxRate();
const { mutateAsync: inactivateTaxRateMutate } = useInactivateTaxRate();
// Handle delete tax rate.
const handleDeleteTaxRate = ({ id }) => {
openAlert('tax-rate-delete', { taxRateId: id });
@@ -63,6 +67,38 @@ function TaxRatesDataTable({
const handleCellClick = (cell, event) => {
openDrawer(DRAWERS.TAX_RATE_DETAILS, { taxRateId: cell.row.original.id });
};
// Handles activating the given tax rate.
const handleActivateTaxRate = (taxRate) => {
activateTaxRateMutate(taxRate.id)
.then(() => {
AppToaster.show({
message: 'The tax rate has been activated successfully.',
intent: Intent.SUCCESS,
});
})
.catch(() => {
AppToaster.show({
message: 'Something went wrong.',
intent: Intent.DANGER,
});
});
};
// Handles inactivating the given tax rate.
const handleInactivateTaxRate = (taxRate) => {
inactivateTaxRateMutate(taxRate.id)
.then(() => {
AppToaster.show({
message: 'The tax rate has been inactivated successfully.',
intent: Intent.SUCCESS,
});
})
.catch(() => {
AppToaster.show({
message: 'Something went wrong.',
intent: Intent.DANGER,
});
});
};
// Display invoice empty status instead of the table.
if (isEmptyStatus) {
return <TaxRatesLandingEmptyState />;
@@ -93,6 +129,8 @@ function TaxRatesDataTable({
onViewDetails: handleViewDetails,
onDelete: handleDeleteTaxRate,
onEdit: handleEditTaxRate,
onActivate: handleActivateTaxRate,
onInactivate: handleInactivateTaxRate,
}}
/>
</DashboardContentTable>

View File

@@ -10,7 +10,7 @@ import { Intent, Menu, MenuDivider, MenuItem } from '@blueprintjs/core';
* @returns {JSX.Element}
*/
export function TaxRatesTableActionsMenu({
payload: { onEdit, onDelete, onViewDetails },
payload: { onEdit, onDelete, onViewDetails, onActivate, onInactivate },
row: { original },
}) {
return (
@@ -28,6 +28,21 @@ export function TaxRatesTableActionsMenu({
onClick={safeCallback(onEdit, original)}
/>
</Can>
<MenuDivider />
{!original.active && (
<MenuItem
icon={<Icon icon="play-16" iconSize={16} />}
text={'Activate Tax Rate'}
onClick={safeCallback(onActivate, original)}
/>
)}
{original.active && (
<MenuItem
icon={<Icon icon="pause-16" iconSize={16} />}
text={'Inactivate Tax Rate'}
onClick={safeCallback(onInactivate, original)}
/>
)}
<Can I={TaxRateAction.Delete} a={AbilitySubject.TaxRate}>
<MenuDivider />
<MenuItem

View File

@@ -1,8 +1,7 @@
// @ts-nocheck
import React from 'react';
import { Button, Intent, Tag, Icon } from '@blueprintjs/core';
import { Intent, Tag } from '@blueprintjs/core';
import { Align } from '@/constants';
import { FormatDateCell } from '@/components';
const codeAccessor = (taxRate) => {
return (
@@ -13,19 +12,26 @@ const codeAccessor = (taxRate) => {
};
const statusAccessor = (taxRate) => {
return (
return taxRate.active ? (
<Tag round={false} intent={Intent.SUCCESS}>
Active
</Tag>
) : (
<Tag round={false} intent={Intent.NONE}>
Inactive
</Tag>
);
};
/**
* Retrieves the tax rates table columns.
*/
export const useTaxRatesTableColumns = () => {
return [
{
Header: 'Name',
accessor: 'name',
width: 40,
width: 50,
},
{
Header: 'Code',
@@ -40,8 +46,8 @@ export const useTaxRatesTableColumns = () => {
},
{
Header: 'Description',
accessor: () => <span>Specital tax for certain goods and services.</span>,
width: 120,
accessor: 'description',
width: 110,
},
{
Header: 'Status',

View File

@@ -7,9 +7,14 @@ const getSchema = () =>
code: Yup.string().required().label('Code'),
active: Yup.boolean().optional().label('Active'),
describtion: Yup.string().optional().label('Description'),
rate: Yup.number().required().label('Rate'),
rate: Yup.number()
.min(0.01, 'Enter a rate percentage of at least 0.01%')
.max(100, 'Enter a rate percentage of at most 100%')
.required()
.label('Rate'),
is_compound: Yup.boolean().optional().label('Is Compound'),
is_non_recoverable: Yup.boolean().optional().label('Is Non Recoverable'),
confirm_edit: Yup.boolean().optional(),
});
export const CreateTaxRateFormSchema = getSchema;

View File

@@ -24,13 +24,16 @@ function TaxRateFormDialog({
return (
<TaxRateDialog
name={dialogName}
title={payload.action === 'edit' ? 'Edit Tax Rate' : 'Create Tax Rate'}
title={payload.id ? 'Edit Tax Rate' : 'Create Tax Rate'}
autoFocus={true}
canEscapeKeyClose={true}
isOpen={isOpen}
>
<DialogSuspense>
<TaxRateFormDialogContent dialogName={dialogName} payload={payload} />
<TaxRateFormDialogContent
dialogName={dialogName}
taxRateId={payload.id}
/>
</DialogSuspense>
</TaxRateDialog>
);

View File

@@ -1,28 +1,51 @@
// @ts-nocheck
import React, { useState } from 'react';
import React from 'react';
import { DialogContent } from '@/components';
import { useTaxRates } from '@/hooks/query/taxRates';
import { useTaxRate, useTaxRates } from '@/hooks/query/taxRates';
import { DialogsName } from '@/constants/dialogs';
const TaxRateFormDialogContext = React.createContext();
interface TaxRateFormDialogBootProps {
taxRateId: number;
children?: JSX.Element;
}
interface TaxRateFormDialogBootContext {
taxRateId: number;
taxRate: any;
isTaxRateLoading: boolean;
isTaxRateSuccess: boolean;
isNewMode: boolean;
}
/**
* Money in dialog provider.
*/
function TaxRateFormDialogBoot({ ...props }) {
function TaxRateFormDialogBoot({
taxRateId,
...props
}: TaxRateFormDialogBootProps) {
const {
data: taxRates,
isLoading: isTaxRatesLoading,
isSuccess: isTaxRatesSuccess,
} = useTaxRates({});
data: taxRate,
isLoading: isTaxRateLoading,
isSuccess: isTaxRateSuccess,
} = useTaxRate(taxRateId, {
enabled: !!taxRateId,
});
const isNewMode = !taxRateId;
// Provider data.
const provider = {
taxRates,
isTaxRatesLoading,
isTaxRatesSuccess,
taxRateId,
taxRate,
isTaxRateLoading,
isTaxRateSuccess,
isNewMode,
dialogName: DialogsName.TaxRateForm,
};
const isLoading = isTaxRatesLoading;
const isLoading = isTaxRateLoading;
return (
<DialogContent isLoading={isLoading}>
@@ -32,6 +55,6 @@ function TaxRateFormDialogBoot({ ...props }) {
}
const useTaxRateFormDialogContext = () =>
React.useContext(TaxRateFormDialogContext);
React.useContext<TaxRateFormDialogBootContext>(TaxRateFormDialogContext);
export { TaxRateFormDialogBoot, useTaxRateFormDialogContext };

View File

@@ -3,12 +3,20 @@ import React from 'react';
import TaxRateFormDialogForm from './TaxRateFormDialogForm';
import { TaxRateFormDialogBoot } from './TaxRateFormDialogBoot';
interface TaxRateFormDialogContentProps {
dialogName: string;
taxRateId: number;
}
/**
* Account dialog content.
*/
export default function TaxRateFormDialogContent({ dialogName, payload }) {
export default function TaxRateFormDialogContent({
dialogName,
taxRateId,
}: TaxRateFormDialogContentProps) {
return (
<TaxRateFormDialogBoot dialogName={dialogName} payload={payload}>
<TaxRateFormDialogBoot dialogName={dialogName} taxRateId={taxRateId}>
<TaxRateFormDialogForm />
</TaxRateFormDialogBoot>
);

View File

@@ -1,5 +1,5 @@
// @ts-nocheck
import React, { useCallback } from 'react';
import React from 'react';
import { Classes, Intent } from '@blueprintjs/core';
import { Form, Formik } from 'formik';
import { AppToaster } from '@/components';
@@ -11,21 +11,12 @@ import {
CreateTaxRateFormSchema,
EditTaxRateFormSchema,
} from './TaxRateForm.schema';
import { transformApiErrors, transformFormToReq } from './utils';
import { transformApiErrors, transformFormToReq, transformTaxRateToForm } from './utils';
import { useCreateTaxRate, useEditTaxRate } from '@/hooks/query/taxRates';
import { useTaxRateFormDialogContext } from './TaxRateFormDialogBoot';
import { TaxRateFormDialogFormFooter } from './TaxRateFormDialogFormFooter';
import { compose, transformToForm } from '@/utils';
// Default initial form values.
const defaultInitialValues = {
name: '',
code: '',
rate: '',
description: '',
is_compound: false,
is_non_recoverable: false,
};
/**
* Tax rate form dialog content.
@@ -35,13 +26,8 @@ function TaxRateFormDialogForm({
closeDialog,
}) {
// Account form context.
const {
account,
payload,
isNewMode,
dialogName,
} = useTaxRateFormDialogContext();
const { taxRate, taxRateId, isNewMode, dialogName } =
useTaxRateFormDialogContext();
// Form validation schema in create and edit mode.
const validationSchema = isNewMode
@@ -76,30 +62,18 @@ function TaxRateFormDialogForm({
setErrors({ ...errorsTransformed });
setSubmitting(false);
};
if (payload.accountId) {
editTaxRateMutate([payload.accountId, form])
if (isNewMode) {
createTaxRateMutate({ ...form })
.then(handleSuccess)
.catch(handleError);
} else {
createTaxRateMutate({ ...form })
editTaxRateMutate([taxRateId, { ...form }])
.then(handleSuccess)
.catch(handleError);
}
};
// Form initial values in create and edit mode.
const initialValues = {
...defaultInitialValues,
/**
* We only care about the fields in the form. Previously unfilled optional
* values such as `notes` come back from the API as null, so remove those
* as well.
*/
...transformToForm(account, defaultInitialValues),
};
// Handles dialog close.
const handleClose = () => {
closeDialog(dialogName);
};
const initialValues = transformTaxRateToForm(taxRate);
return (
<Formik
@@ -109,11 +83,7 @@ function TaxRateFormDialogForm({
>
<Form>
<div className={Classes.DIALOG_BODY}>
<TaxRateFormDialogFormContent
dialogName={dialogName}
action={payload?.action}
onClose={handleClose}
/>
<TaxRateFormDialogFormContent />
</div>
<TaxRateFormDialogFormFooter />
</Form>

View File

@@ -1,17 +1,15 @@
import {
FCheckbox,
FFormGroup,
FInputGroup,
FieldHint,
Hint,
} from '@/components';
import { Tag } from '@blueprintjs/core';
// @ts-nocheck
import React from 'react';
import { useFormikContext } from 'formik';
import { Tag, Text } from '@blueprintjs/core';
import styled from 'styled-components';
import { FCheckbox, FFormGroup, FInputGroup, Hint } from '@/components';
import { transformTaxRateCodeValue, useIsTaxRateChanged } from './utils';
import { useTaxRateFormDialogContext } from './TaxRateFormDialogBoot';
/**
*
* @returns
* Tax rate form content.
* @returns {JSX.Element}
*/
export default function TaxRateFormDialogContent() {
return (
@@ -23,27 +21,23 @@ export default function TaxRateFormDialogContent() {
subLabel={
'The name as you would like it to appear in customers invoices.'
}
fastField={true}
>
<FInputGroup name={'name'} />
</FFormGroup>
<FFormGroup
name={'code'}
label={'Code'}
labelInfo={<Tag minimal>Required</Tag>}
>
<FInputGroup name={'code'} />
<FInputGroup name={'name'} fastField={true} />
</FFormGroup>
<TaxRateCodeField />
<FFormGroup
name={'rate'}
label={'Rate (%)'}
labelInfo={<Tag minimal>Required</Tag>}
fastField={true}
>
<RateFormGroup
name={'rate'}
rightElement={<Tag minimal>%</Tag>}
fill={false}
fastField={true}
/>
</FFormGroup>
@@ -51,23 +45,81 @@ export default function TaxRateFormDialogContent() {
name={'description'}
label={'Description'}
labelInfo={
<FieldHint content="This description is for internal use only and will not be visiable to your customers." />
<Hint content="This description is for internal use only and will not be visiable to your customers." />
}
fastField={true}
>
<FInputGroup name={'description'} />
<FInputGroup name={'description'} fastField={true} />
</FFormGroup>
<CompoundFormGroup name={'is_compound'}>
<FCheckbox label={'Is compound'} name={'is_compound'} />
<CompoundFormGroup name={'is_compound'} fastField={true}>
<FCheckbox
label={'Is compound'}
name={'is_compound'}
fastField={true}
/>
</CompoundFormGroup>
<CompoundFormGroup name={'is_non_recoverable'}>
<FCheckbox label={'Is non recoverable'} name={'is_non_recoverable'} />
<CompoundFormGroup name={'is_non_recoverable'} fastField={true}>
<FCheckbox
label={'Is non recoverable'}
name={'is_non_recoverable'}
fastField={true}
/>
</CompoundFormGroup>
<ConfirmEditingTaxRate />
</div>
);
}
/**
* Tax rate code input group
* @returns {JSX.Element}
*/
function TaxRateCodeField() {
const { setFieldValue } = useFormikContext();
// Handle the field change.
const handleChange = (event) => {
const transformedValue = transformTaxRateCodeValue(event.target.value);
setFieldValue('code', transformedValue);
};
return (
<FFormGroup
name={'code'}
label={'Code'}
labelInfo={<Tag minimal>Required</Tag>}
fastField={true}
>
<FInputGroup name={'code'} fastField={true} onChange={handleChange} />
</FFormGroup>
);
}
function ConfirmEditingTaxRate() {
const isTaxRateChanged = useIsTaxRateChanged();
const { isNewMode } = useTaxRateFormDialogContext();
// Can't continue if it is new mode or tax rate not changed.
if (!isTaxRateChanged || isNewMode) return null;
return (
<EditWarningWrap>
<Text color={'#766f58'}>Please Note:</Text>
<ConfirmEditFormGroup name={'confirm_edit'}>
<FCheckbox
name={'confirm_edit'}
label={`I understand that updating the
tax will mark the existing tax inactive, create a new tax, and update
it in the chosen transactions.`}
/>
</ConfirmEditFormGroup>
</EditWarningWrap>
);
}
const RateFormGroup = styled(FInputGroup)`
max-width: 100px;
`;
@@ -75,3 +127,18 @@ const RateFormGroup = styled(FInputGroup)`
const CompoundFormGroup = styled(FFormGroup)`
margin-bottom: 0;
`;
const EditWarningWrap = styled(`div`)`
background: #fcf8ec;
margin-left: -20px;
margin-right: -20px;
padding: 14px 20px;
font-size: 13px;
margin-top: 8px;
border-top: 1px solid #f2eddf;
border-bottom: 1px solid #f2eddf;
`;
const ConfirmEditFormGroup = styled(FFormGroup)`
margin-bottom: 0;
`;

View File

@@ -1,3 +1,4 @@
// @ts-nocheck
import React from 'react';
import * as R from 'ramda';
import { useFormikContext } from 'formik';

View File

@@ -1,7 +1,60 @@
// @ts-nocheck
import { useFormikContext } from 'formik';
import * as R from 'ramda';
import { omit } from 'lodash';
import { transformToForm } from '@/utils';
// Default initial form values.
export const defaultInitialValues = {
name: '',
code: '',
rate: '',
description: '',
is_compound: false,
is_non_recoverable: false,
confirm_edit: false,
};
export const transformApiErrors = () => {
return {};
};
export const transformFormToReq = () => {
return {};
export const transformFormToReq = (form) => {
return omit({ ...form }, ['confirm_edit']);
};
export const useIsTaxRateChanged = () => {
const { initialValues, values } = useFormikContext();
return initialValues.rate !== values.rate;
};
const convertFormAttrsToBoolean = (form) => {
return {
...form,
is_compound: !!form.is_compound,
is_non_recoverable: !!form.is_non_recoverable,
};
};
export const transformTaxRateToForm = (taxRate) => {
return R.compose(convertFormAttrsToBoolean)({
...defaultInitialValues,
/**
* We only care about the fields in the form. Previously unfilled optional
* values such as `notes` come back from the API as null, so remove those
* as well.
*/
...transformToForm(taxRate, defaultInitialValues),
});
};
export const transformTaxRateCodeValue = (input: string) => {
// Remove non-alphanumeric characters and spaces using a regular expression
const cleanedString = input.replace(/\s+/g, '');
// Convert the cleaned string to uppercase
const uppercasedString = cleanedString.toUpperCase();
return uppercasedString;
};

View File

@@ -27,17 +27,29 @@ export default function TaxRateDetailsContentDetails() {
<DetailItem
label={'Non Recoverable'}
children={
<Tag round={false} intent={Intent.SUCCESS} minimal>
Enabled
</Tag>
taxRate.is_non_recoverable ? (
<Tag round={false} intent={Intent.SUCCESS} minimal>
Enabled
</Tag>
) : (
<Tag round={false} intent={Intent.NONE} minimal>
Disabled
</Tag>
)
}
/>
<DetailItem
label={'Compound'}
children={
<Tag round={false} intent={Intent.SUCCESS} minimal>
Enabled
</Tag>
taxRate.is_compound ? (
<Tag round={false} intent={Intent.SUCCESS} minimal>
Enabled
</Tag>
) : (
<Tag round={false} intent={Intent.NONE} minimal>
Disabled
</Tag>
)
}
/>
</DetailsMenu>

View File

@@ -4,6 +4,11 @@ import { useRequestQuery } from '../useQueryRequest';
import QUERY_TYPES from './types';
import useApiRequest from '../useRequest';
// Common invalidate queries.
const commonInvalidateQueries = (queryClient) => {
queryClient.invalidateQueries(QUERY_TYPES.TAX_RATES);
};
/**
* Retrieves tax rates.
* @param {number} customerId - Customer id.
@@ -52,9 +57,8 @@ export function useEditTaxRate(props) {
([id, values]) => apiRequest.post(`tax-rates/${id}`, values),
{
onSuccess: (res, id) => {
// Invalidate specific item.
commonInvalidateQueries(queryClient);
queryClient.invalidateQueries([QUERY_TYPES.TAX_RATES, id]);
queryClient.invalidateQueries([QUERY_TYPES.TAX_RATES]);
},
...props,
},
@@ -68,28 +72,58 @@ export function useCreateTaxRate(props) {
const queryClient = useQueryClient();
const apiRequest = useApiRequest();
return useMutation(([values]) => apiRequest.post('tax-rates', values), {
return useMutation((values) => apiRequest.post('tax-rates', values), {
onSuccess: (res, id) => {
// Invalidate specific item.
commonInvalidateQueries(queryClient);
queryClient.invalidateQueries([QUERY_TYPES.TAX_RATES, id]);
queryClient.invalidateQueries([QUERY_TYPES.TAX_RATES]);
},
...props,
});
}
/**
* Deletes a new tax rate.
* Delete the given tax rate.
*/
export function useDeleteTaxRate(props) {
const queryClient = useQueryClient();
const apiRequest = useApiRequest();
return useMutation(([id]) => apiRequest.delete(`tax-rates/${id}`), {
return useMutation((id) => apiRequest.post(`tax-rates/${id}`), {
onSuccess: (res, id) => {
// Invalidate specific item.
commonInvalidateQueries(queryClient);
queryClient.invalidateQueries([QUERY_TYPES.TAX_RATES, id]);
},
...props,
});
}
/**
* Activate the given tax rate.
*/
export function useActivateTaxRate(props) {
const queryClient = useQueryClient();
const apiRequest = useApiRequest();
return useMutation((id) => apiRequest.post(`tax-rates/${id}/active`), {
onSuccess: (res, id) => {
commonInvalidateQueries(queryClient);
queryClient.invalidateQueries([QUERY_TYPES.TAX_RATES, id]);
},
...props,
});
}
/**
* Inactivate the given tax rate.
*/
export function useInactivateTaxRate(props) {
const queryClient = useQueryClient();
const apiRequest = useApiRequest();
return useMutation((id) => apiRequest.delete(`tax-rates/${id}/inactive`), {
onSuccess: (res, id) => {
commonInvalidateQueries(queryClient);
queryClient.invalidateQueries([QUERY_TYPES.TAX_RATES, id]);
queryClient.invalidateQueries([QUERY_TYPES.TAX_RATES]);
},
...props,
});