feat(webapp): wip tax rates management

This commit is contained in:
Ahmed Bouhuolia
2023-09-14 23:35:54 +02:00
parent b98b73ad98
commit 8a64198433
34 changed files with 1205 additions and 14 deletions

View File

@@ -47,6 +47,7 @@ import ProjectExpenseForm from '@/containers/Projects/containers/ProjectExpenseF
import EstimatedExpenseFormDialog from '@/containers/Projects/containers/EstimatedExpenseFormDialog';
import ProjectInvoicingFormDialog from '@/containers/Projects/containers/ProjectInvoicingFormDialog';
import ProjectBillableEntriesFormDialog from '@/containers/Projects/containers/ProjectBillableEntriesFormDialog';
import TaxRateFormDialog from '@/containers/TaxRates/dialogs/TaxRateFormDialog/TaxRateFormDialog';
import { DialogsName } from '@/constants/dialogs';
/**
@@ -134,7 +135,10 @@ export default function DialogsContainer() {
<ProjectInvoicingFormDialog
dialogName={DialogsName.ProjectInvoicingForm}
/>
<ProjectBillableEntriesFormDialog dialogName={DialogsName.ProjectBillableEntriesForm}/>
<ProjectBillableEntriesFormDialog
dialogName={DialogsName.ProjectBillableEntriesForm}
/>
<TaxRateFormDialog dialogName={DialogsName.TaxRateForm} />
</div>
);
}

View File

@@ -22,6 +22,7 @@ import VendorCreditDetailDrawer from '@/containers/Drawers/VendorCreditDetailDra
import RefundCreditNoteDetailDrawer from '@/containers/Drawers/RefundCreditNoteDetailDrawer';
import RefundVendorCreditDetailDrawer from '@/containers/Drawers/RefundVendorCreditDetailDrawer';
import WarehouseTransferDetailDrawer from '@/containers/Drawers/WarehouseTransferDetailDrawer';
import TaxRateDetailsDrawer from '@/containers/TaxRates/drawers/TaxRateDetailsDrawer/TaxRateDetailsDrawer';
import { DRAWERS } from '@/constants/drawers';
@@ -43,16 +44,25 @@ export default function DrawersContainer() {
<ItemDetailDrawer name={DRAWERS.ITEM_DETAILS} />
<CustomerDetailsDrawer name={DRAWERS.CUSTOMER_DETAILS} />
<VendorDetailsDrawer name={DRAWERS.VENDOR_DETAILS} />
<InventoryAdjustmentDetailDrawer name={DRAWERS.INVENTORY_ADJUSTMENT_DETAILS} />
<CashflowTransactionDetailDrawer name={DRAWERS.CASHFLOW_TRNASACTION_DETAILS} />
<InventoryAdjustmentDetailDrawer
name={DRAWERS.INVENTORY_ADJUSTMENT_DETAILS}
/>
<CashflowTransactionDetailDrawer
name={DRAWERS.CASHFLOW_TRNASACTION_DETAILS}
/>
<QuickCreateCustomerDrawer name={DRAWERS.QUICK_CREATE_CUSTOMER} />
<QuickCreateItemDrawer name={DRAWERS.QUICK_CREATE_ITEM} />
<QuickWriteVendorDrawer name={DRAWERS.QUICK_WRITE_VENDOR} />
<CreditNoteDetailDrawer name={DRAWERS.CREDIT_NOTE_DETAILS} />
<VendorCreditDetailDrawer name={DRAWERS.VENDOR_CREDIT_DETAILS} />
<RefundCreditNoteDetailDrawer name={DRAWERS.REFUND_CREDIT_NOTE_DETAILS} />
<RefundVendorCreditDetailDrawer name={DRAWERS.REFUND_VENDOR_CREDIT_DETAILS} />
<WarehouseTransferDetailDrawer name={DRAWERS.WAREHOUSE_TRANSFER_DETAILS} />
<RefundVendorCreditDetailDrawer
name={DRAWERS.REFUND_VENDOR_CREDIT_DETAILS}
/>
<WarehouseTransferDetailDrawer
name={DRAWERS.WAREHOUSE_TRANSFER_DETAILS}
/>
<TaxRateDetailsDrawer name={DRAWERS.TAX_RATE_DETAILS} />
</div>
);
}

View File

@@ -20,7 +20,8 @@ export const AbilitySubject = {
SubscriptionBilling: 'SubscriptionBilling',
CreditNote: 'CreditNote',
VendorCredit: 'VendorCredit',
Project:'Project'
Project:'Project',
TaxRate: 'TaxRate',
};
export const ItemAction = {
@@ -186,3 +187,11 @@ export const SubscriptionBillingAbility = {
View: 'view',
Payment: 'payment',
};
export const TaxRateAction = {
View: 'View',
Create: 'Create',
Edit: 'Edit',
Delete: 'Delete',
};

View File

@@ -46,5 +46,6 @@ export enum DialogsName {
EstimateExpenseForm = 'estimate-expense-form',
ProjectInvoicingForm = 'project-invoicing-form',
ProjectBillableEntriesForm = 'project-billable-entries',
InvoiceNumberSettings = 'InvoiceNumberSettings'
InvoiceNumberSettings = 'InvoiceNumberSettings',
TaxRateForm = 'tax-rate-form',
}

View File

@@ -22,4 +22,5 @@ export enum DRAWERS {
REFUND_CREDIT_NOTE_DETAILS = 'refund-credit-detail-drawer',
REFUND_VENDOR_CREDIT_DETAILS = 'refund-vendor-detail-drawer',
WAREHOUSE_TRANSFER_DETAILS = 'warehouse-transfer-detail-drawer',
TAX_RATE_DETAILS = 'tax-rate-detail-drawer',
}

View File

@@ -406,6 +406,11 @@ export const SidebarMenu = [
href: '/transactions-locking',
type: ISidebarMenuItemType.Link,
},
{
text: 'Tax Rates',
href: '/tax-rates',
type: ISidebarMenuItemType.Link,
},
],
},
{

View File

@@ -25,6 +25,7 @@ import WarehousesAlerts from '@/containers/Preferences/Warehouses/WarehousesAler
import WarehousesTransfersAlerts from '@/containers/WarehouseTransfers/WarehousesTransfersAlerts';
import BranchesAlerts from '@/containers/Preferences/Branches/BranchesAlerts';
import ProjectAlerts from '@/containers/Projects/containers/ProjectAlerts';
import TaxRatesAlerts from '@/containers/TaxRates/alerts';
export default [
...AccountsAlerts,
@@ -53,4 +54,5 @@ export default [
...WarehousesTransfersAlerts,
...BranchesAlerts,
...ProjectAlerts,
...TaxRatesAlerts
];

View File

@@ -25,14 +25,12 @@ import { InvoiceDetailsStatus } from './utils';
export default function InvoiceDetailHeader() {
const { invoice } = useInvoiceDetailDrawerContext();
const handleCustomerLinkClick = () => {};
return (
<CommercialDocHeader>
<CommercialDocTopHeader>
<DetailsMenu>
<AmountDetailItem label={intl.get('amount')}>
<h3 class="big-number">{invoice.formatted_amount}</h3>
<h3 class="big-number">{invoice.total_formatted}</h3>
</AmountDetailItem>
<StatusDetailItem label={''}>
@@ -75,11 +73,11 @@ export default function InvoiceDetailHeader() {
textAlign={'right'}
>
<DetailItem label={intl.get('due_amount')}>
<strong>{invoice.formatted_due_amount}</strong>
<strong>{invoice.due_amount_formatted}</strong>
</DetailItem>
<DetailItem label={intl.get('invoice.details.payment_amount')}>
<strong>{invoice.formatted_payment_amount}</strong>
<strong>{invoice.payment_amount_formatted}</strong>
</DetailItem>
<DetailItem

View File

@@ -80,6 +80,9 @@ export function transformToEditForm(invoice) {
return {
...transformToForm(invoice, defaultInvoice),
inclusive_exclusive_tax: invoice.is_inclusive_tax
? TaxType.Inclusive
: TaxType.Exclusive,
entries,
};
}

View File

@@ -225,8 +225,8 @@ export function useInvoicesTableColumns() {
},
{
id: 'amount',
Header: intl.get('balance'),
accessor: 'formatted_amount',
Header: intl.get('amount'),
accessor: 'total_formatted',
width: 120,
align: 'right',
clickable: true,

View File

@@ -0,0 +1,97 @@
// @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 { useDeleteTaxRate } from '@/hooks/query/taxRates';
import withAlertStoreConnect from '@/containers/Alert/withAlertStoreConnect';
import withAlertActions from '@/containers/Alert/withAlertActions';
import withItemsActions from '@/containers/Items/withItemsActions';
import withDrawerActions from '@/containers/Drawer/withDrawerActions';
import { compose } from '@/utils';
import { DRAWERS } from '@/constants/drawers';
/**
* Item delete alerts.
*/
function TaxRateDeleteAlert({
name,
// #withAlertStoreConnect
isOpen,
payload: { taxRateId },
// #withAlertActions
closeAlert,
// #withDrawerActions
closeDrawer,
}) {
const { mutateAsync: deleteTaxRate, isLoading } = useDeleteTaxRate();
// Handle cancel delete item alert.
const handleCancelItemDelete = () => {
closeAlert(name);
};
// Handle confirm delete item.
const handleConfirmDeleteItem = () => {
deleteTaxRate(taxRateId)
.then(() => {
AppToaster.show({
message: 'The tax rate has been deleted successfully.',
intent: Intent.SUCCESS,
});
closeDrawer(DRAWERS.TAX_RATE_DETAILS);
})
.catch(
({
response: {
data: { errors },
},
}) => {
// handleDeleteErrors(errors);
},
)
.finally(() => {
closeAlert(name);
});
};
return (
<Alert
cancelButtonText={<T id={'cancel'} />}
confirmButtonText={<T id={'delete'} />}
icon="trash"
intent={Intent.DANGER}
isOpen={isOpen}
onCancel={handleCancelItemDelete}
onConfirm={handleConfirmDeleteItem}
loading={isLoading}
>
<p>
Once you delete this tax rate, you won't be able to restore the item
later.
</p>
<p>
Are you sure you want to delete ? If you're not sure, you can inactivate
it instead.
</p>
</Alert>
);
}
export default compose(
withAlertStoreConnect(),
withAlertActions,
withItemsActions,
withDrawerActions,
)(TaxRateDeleteAlert);

View File

@@ -0,0 +1,11 @@
// @ts-nocheck
import React from 'react';
const TaxRateDeleteAlert = React.lazy(() => import('./TaxRateDeleteAlert'));
/**
* Project alerts.
*/
export default [
{ name: 'tax-rate-delete', component: TaxRateDeleteAlert },
];

View File

@@ -0,0 +1,61 @@
// @ts-nocheck
import React from 'react';
import { NavbarGroup, NavbarDivider, Button, Classes } from '@blueprintjs/core';
import {
DashboardActionsBar,
FormattedMessage as T,
Can,
Icon,
} from '@/components';
import { AbilitySubject, TaxRateAction } from '@/constants/abilityOption';
import { useTaxRatesLandingContext } from './TaxRatesLandingProvider';
import withDialogActions from '@/containers/Dialog/withDialogActions';
import { DialogsName } from '@/constants/dialogs';
import { compose } from '@/utils';
/**
* Tax rates actions bar.
*/
function TaxRatesActionsBar({
// #withDialogActions
openDialog,
}) {
// Items list context.
const {} = useTaxRatesLandingContext();
// Handle `new item` button click.
const onClickNewItem = () => {
openDialog(DialogsName.TaxRateForm);
};
return (
<DashboardActionsBar>
<NavbarGroup>
<Can I={TaxRateAction.Create} a={AbilitySubject.TaxRate}>
<Button
className={Classes.MINIMAL}
icon={<Icon icon="plus" />}
text={'New Tax Rate'}
onClick={onClickNewItem}
/>
</Can>
<NavbarDivider />
<Button
className={Classes.MINIMAL}
icon={<Icon icon="file-import-16" iconSize={16} />}
text={<T id={'import'} />}
/>
<Button
className={Classes.MINIMAL}
icon={<Icon icon="file-export-16" iconSize={16} />}
text={<T id={'export'} />}
/>
</NavbarGroup>
</DashboardActionsBar>
);
}
export default compose(withDialogActions)(TaxRatesActionsBar);

View File

@@ -0,0 +1,39 @@
// @ts-nocheck
import React from 'react';
import { Button, Intent } from '@blueprintjs/core';
import { useHistory } from 'react-router-dom';
import { EmptyStatus, Can, FormattedMessage as T } from '@/components';
import { SaleInvoiceAction, AbilitySubject } from '@/constants/abilityOption';
export function TaxRatesLandingEmptyState() {
const history = useHistory();
return (
<EmptyStatus
title={<T id={'the_organization_doesn_t_receive_money_yet'} />}
description={
<p>
<T id={'invoices_empty_status_description'} />
</p>
}
action={
<>
<Can I={SaleInvoiceAction.Create} a={AbilitySubject.Invoice}>
<Button
intent={Intent.PRIMARY}
large={true}
onClick={() => {
history.push('/invoices/new');
}}
>
<T id={'new_sale_invoice'} />
</Button>
<Button intent={Intent.NONE} large={true}>
<T id={'learn_more'} />
</Button>
</Can>
</>
}
/>
);
}

View File

@@ -0,0 +1,41 @@
// @ts-nocheck
import React from 'react';
import { isEmpty } from 'lodash';
import { DashboardInsider } from '@/components/Dashboard';
import { useTaxRates } from '@/hooks/query/taxRates';
const TaxRatesLandingContext = React.createContext();
/**
* Cash Flow data provider.
*/
function TaxRatesLandingProvider({ tableState, ...props }) {
// Fetch cash flow list .
const {
data: taxRates,
isFetching: isTaxRatesFetching,
isLoading: isTaxRatesLoading,
} = useTaxRates({}, { keepPreviousData: true });
// Detarmines whether the table should show empty state.
const isEmptyStatus = isEmpty(taxRates) && !isTaxRatesLoading;
// Provider payload.
const provider = {
taxRates,
isTaxRatesFetching,
isTaxRatesLoading,
isEmptyStatus
};
return (
<DashboardInsider name={'cashflow-accounts'}>
<TaxRatesLandingContext.Provider value={provider} {...props} />
</DashboardInsider>
);
}
const useTaxRatesLandingContext = () =>
React.useContext(TaxRatesLandingContext);
export { TaxRatesLandingProvider, useTaxRatesLandingContext };

View File

@@ -0,0 +1,110 @@
// @ts-nocheck
import React, { useCallback } from 'react';
import { useHistory } from 'react-router-dom';
import {
DataTable,
DashboardContentTable,
TableSkeletonHeader,
TableSkeletonRows,
} from '@/components';
import withAlertsActions from '@/containers/Alert/withAlertActions';
import withDrawerActions from '@/containers/Drawer/withDrawerActions';
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';
import { TaxRatesTableActionsMenu } from './_components';
import { compose } from '@/utils';
import { DRAWERS } from '@/constants/drawers';
import { DialogsName } from '@/constants/dialogs';
/**
* Invoices datatable.
*/
function TaxRatesDataTable({
// #withAlertsActions
openAlert,
// #withDrawerActions
openDrawer,
// #withDialogAction
openDialog,
}) {
// Invoices list context.
const { taxRates, isTaxRatesLoading, isEmptyStatus } =
useTaxRatesLandingContext();
// Invoices table columns.
const columns = useTaxRatesTableColumns();
// Handle delete tax rate.
const handleDeleteTaxRate = ({ id }) => {
openAlert('tax-rate-delete', { taxRateId: id });
};
// Handle edit tax rate.
const handleEditTaxRate = (taxRate) => {
openDialog(DialogsName.TaxRateForm, { id: taxRate.id });
};
// Handle view details tax rate.
const handleViewDetails = (taxRate) => {
openDrawer(DRAWERS.TAX_RATE_DETAILS, { taxRateId: taxRate.id });
};
// Handle table cell click.
const handleCellClick = (cell, event) => {
openDrawer(DRAWERS.TAX_RATE_DETAILS, { taxRateId: cell.row.original.id });
};
// Display invoice empty status instead of the table.
if (isEmptyStatus) {
return <TaxRatesLandingEmptyState />;
}
return (
<DashboardContentTable>
<DataTable
columns={columns}
data={taxRates}
loading={isTaxRatesLoading}
headerLoading={isTaxRatesLoading}
progressBarLoading={isTaxRatesLoading}
manualSortBy={false}
selectionColumn={false}
noInitialFetch={true}
sticky={true}
pagination={false}
manualPagination={false}
autoResetSortBy={false}
autoResetPage={false}
TableLoadingRenderer={TableSkeletonRows}
TableHeaderSkeletonRenderer={TableSkeletonHeader}
ContextMenu={TaxRatesTableActionsMenu}
onCellClick={handleCellClick}
size={'medium'}
payload={{
onViewDetails: handleViewDetails,
onDelete: handleDeleteTaxRate,
onEdit: handleEditTaxRate,
}}
/>
</DashboardContentTable>
);
}
export default compose(
withDashboardActions,
withAlertsActions,
withDrawerActions,
withDialogActions,
withSettings(({ invoiceSettings }) => ({
invoicesTableSize: invoiceSettings?.tableSize,
})),
)(TaxRatesDataTable);

View File

@@ -0,0 +1,42 @@
// @ts-nocheck
import React from 'react';
import { Can, Icon } from '@/components';
import { AbilitySubject, TaxRateAction } from '@/constants/abilityOption';
import { safeCallback } from '@/utils';
import { Intent, Menu, MenuDivider, MenuItem } from '@blueprintjs/core';
/**
* Tax rates table actions menu.
* @returns {JSX.Element}
*/
export function TaxRatesTableActionsMenu({
payload: { onEdit, onDelete, onViewDetails },
row: { original },
}) {
return (
<Menu>
<MenuItem
icon={<Icon icon="reader-18" />}
text={'View Details'}
onClick={safeCallback(onViewDetails, original)}
/>
<Can I={TaxRateAction.Edit} a={AbilitySubject.TaxRate}>
<MenuDivider />
<MenuItem
icon={<Icon icon="pen-18" />}
text={'Edit Tax Rate'}
onClick={safeCallback(onEdit, original)}
/>
</Can>
<Can I={TaxRateAction.Delete} a={AbilitySubject.TaxRate}>
<MenuDivider />
<MenuItem
text={'Delete Tax Rate'}
intent={Intent.DANGER}
onClick={safeCallback(onDelete, original)}
icon={<Icon icon="trash-16" iconSize={16} />}
/>
</Can>
</Menu>
);
}

View File

@@ -0,0 +1,53 @@
// @ts-nocheck
import React from 'react';
import { Button, Intent, Tag, Icon } from '@blueprintjs/core';
import { Align } from '@/constants';
import { FormatDateCell } from '@/components';
const codeAccessor = (taxRate) => {
return (
<Tag minimal={true} round={false} intent={Intent.NONE} interactive={true}>
{taxRate.code}
</Tag>
);
};
const statusAccessor = (taxRate) => {
return (
<Tag round={false} intent={Intent.SUCCESS}>
Active
</Tag>
);
};
export const useTaxRatesTableColumns = () => {
return [
{
Header: 'Name',
accessor: 'name',
width: 40,
},
{
Header: 'Code',
accessor: codeAccessor,
width: 40,
},
{
Header: 'Rate',
accessor: 'rate_formatted',
align: Align.Right,
width: 30,
},
{
Header: 'Description',
accessor: () => <span>Specital tax for certain goods and services.</span>,
width: 120,
},
{
Header: 'Status',
accessor: statusAccessor,
width: 30,
align: Align.Right,
},
];
};

View File

@@ -0,0 +1,16 @@
// @ts-nocheck
import * as Yup from 'yup';
const getSchema = () =>
Yup.object().shape({
name: Yup.string().required().label('Name'),
code: Yup.string().required().label('Code'),
active: Yup.boolean().optional().label('Active'),
describtion: Yup.string().optional().label('Description'),
rate: Yup.number().required().label('Rate'),
is_compound: Yup.boolean().optional().label('Is Compound'),
is_non_recoverable: Yup.boolean().optional().label('Is Non Recoverable'),
});
export const CreateTaxRateFormSchema = getSchema;
export const EditTaxRateFormSchema = getSchema;

View File

@@ -0,0 +1,39 @@
// @ts-nocheck
import React, { lazy } from 'react';
import styled from 'styled-components';
import { Dialog, DialogSuspense } from '@/components';
import withDialogRedux from '@/components/DialogReduxConnect';
import { compose } from '@/utils';
const TaxRateFormDialogContent = lazy(
() => import('./TaxRateFormDialogContent'),
);
const TaxRateDialog = styled(Dialog)`
max-width: 450px;
`;
/**
* Account form dialog.
*/
function TaxRateFormDialog({
dialogName,
payload = { action: '', id: null },
isOpen,
}) {
return (
<TaxRateDialog
name={dialogName}
title={payload.action === 'edit' ? 'Edit Tax Rate' : 'Create Tax Rate'}
autoFocus={true}
canEscapeKeyClose={true}
isOpen={isOpen}
>
<DialogSuspense>
<TaxRateFormDialogContent dialogName={dialogName} payload={payload} />
</DialogSuspense>
</TaxRateDialog>
);
}
export default compose(withDialogRedux())(TaxRateFormDialog);

View File

@@ -0,0 +1,37 @@
// @ts-nocheck
import React, { useState } from 'react';
import { DialogContent } from '@/components';
import { useTaxRates } from '@/hooks/query/taxRates';
const TaxRateFormDialogContext = React.createContext();
/**
* Money in dialog provider.
*/
function TaxRateFormDialogBoot({ ...props }) {
const {
data: taxRates,
isLoading: isTaxRatesLoading,
isSuccess: isTaxRatesSuccess,
} = useTaxRates({});
// Provider data.
const provider = {
taxRates,
isTaxRatesLoading,
isTaxRatesSuccess,
};
const isLoading = isTaxRatesLoading;
return (
<DialogContent isLoading={isLoading}>
<TaxRateFormDialogContext.Provider value={provider} {...props} />
</DialogContent>
);
}
const useTaxRateFormDialogContext = () =>
React.useContext(TaxRateFormDialogContext);
export { TaxRateFormDialogBoot, useTaxRateFormDialogContext };

View File

@@ -0,0 +1,15 @@
// @ts-nocheck
import React from 'react';
import TaxRateFormDialogForm from './TaxRateFormDialogForm';
import { TaxRateFormDialogBoot } from './TaxRateFormDialogBoot';
/**
* Account dialog content.
*/
export default function TaxRateFormDialogContent({ dialogName, payload }) {
return (
<TaxRateFormDialogBoot dialogName={dialogName} payload={payload}>
<TaxRateFormDialogForm />
</TaxRateFormDialogBoot>
);
}

View File

@@ -0,0 +1,124 @@
// @ts-nocheck
import React, { useCallback } from 'react';
import { Classes, Intent } from '@blueprintjs/core';
import { Form, Formik } from 'formik';
import { AppToaster } from '@/components';
import TaxRateFormDialogFormContent from './TaxRateFormDialogFormContent';
import withDialogActions from '@/containers/Dialog/withDialogActions';
import {
CreateTaxRateFormSchema,
EditTaxRateFormSchema,
} from './TaxRateForm.schema';
import { transformApiErrors, transformFormToReq } 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.
*/
function TaxRateFormDialogForm({
// #withDialogActions
closeDialog,
}) {
// Account form context.
const {
account,
payload,
isNewMode,
dialogName,
} = useTaxRateFormDialogContext();
// Form validation schema in create and edit mode.
const validationSchema = isNewMode
? CreateTaxRateFormSchema
: EditTaxRateFormSchema;
const { mutateAsync: createTaxRateMutate } = useCreateTaxRate();
const { mutateAsync: editTaxRateMutate } = useEditTaxRate();
// Callbacks handles form submit.
const handleFormSubmit = (values, { setSubmitting, setErrors }) => {
const form = transformFormToReq(values);
// Handle request success.
const handleSuccess = () => {
closeDialog(dialogName);
AppToaster.show({
message: 'The tax rate has been created successfully.',
intent: Intent.SUCCESS,
});
};
// Handle request error.
const handleError = (error) => {
const {
response: {
data: { errors },
},
} = error;
const errorsTransformed = transformApiErrors(errors);
setErrors({ ...errorsTransformed });
setSubmitting(false);
};
if (payload.accountId) {
editTaxRateMutate([payload.accountId, form])
.then(handleSuccess)
.catch(handleError);
} else {
createTaxRateMutate({ ...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);
};
return (
<Formik
validationSchema={validationSchema}
initialValues={initialValues}
onSubmit={handleFormSubmit}
>
<Form>
<div className={Classes.DIALOG_BODY}>
<TaxRateFormDialogFormContent
dialogName={dialogName}
action={payload?.action}
onClose={handleClose}
/>
</div>
<TaxRateFormDialogFormFooter />
</Form>
</Formik>
);
}
export default compose(withDialogActions)(TaxRateFormDialogForm);

View File

@@ -0,0 +1,77 @@
import {
FCheckbox,
FFormGroup,
FInputGroup,
FieldHint,
Hint,
} from '@/components';
import { Tag } from '@blueprintjs/core';
import React from 'react';
import styled from 'styled-components';
/**
*
* @returns
*/
export default function TaxRateFormDialogContent() {
return (
<div>
<FFormGroup
name={'name'}
label={'Name'}
labelInfo={<Tag minimal>Required</Tag>}
subLabel={
'The name as you would like it to appear in customers invoices.'
}
>
<FInputGroup name={'name'} />
</FFormGroup>
<FFormGroup
name={'code'}
label={'Code'}
labelInfo={<Tag minimal>Required</Tag>}
>
<FInputGroup name={'code'} />
</FFormGroup>
<FFormGroup
name={'rate'}
label={'Rate (%)'}
labelInfo={<Tag minimal>Required</Tag>}
>
<RateFormGroup
name={'rate'}
rightElement={<Tag minimal>%</Tag>}
fill={false}
/>
</FFormGroup>
<FFormGroup
name={'description'}
label={'Description'}
labelInfo={
<FieldHint content="This description is for internal use only and will not be visiable to your customers." />
}
>
<FInputGroup name={'description'} />
</FFormGroup>
<CompoundFormGroup name={'is_compound'}>
<FCheckbox label={'Is compound'} name={'is_compound'} />
</CompoundFormGroup>
<CompoundFormGroup name={'is_non_recoverable'}>
<FCheckbox label={'Is non recoverable'} name={'is_non_recoverable'} />
</CompoundFormGroup>
</div>
);
}
const RateFormGroup = styled(FInputGroup)`
max-width: 100px;
`;
const CompoundFormGroup = styled(FFormGroup)`
margin-bottom: 0;
`;

View File

@@ -0,0 +1,41 @@
import React from 'react';
import * as R from 'ramda';
import { useFormikContext } from 'formik';
import { Button, Classes, Intent } from '@blueprintjs/core';
import { DialogsName } from '@/constants/dialogs';
import withDialogActions from '@/containers/Dialog/withDialogActions';
function TaxRateFormDialogFormFooterRoot({ closeDialog }) {
const { isSubmitting } = useFormikContext();
const handleClose = () => {
closeDialog(DialogsName.TaxRateForm);
};
return (
<div className={Classes.DIALOG_FOOTER}>
<div className={Classes.DIALOG_FOOTER_ACTIONS}>
<Button
disabled={isSubmitting}
onClick={handleClose}
style={{ minWidth: '75px' }}
>
Close
</Button>
<Button
intent={Intent.PRIMARY}
loading={isSubmitting}
style={{ minWidth: '95px' }}
type="submit"
>
Submit
</Button>
</div>
</div>
);
}
export const TaxRateFormDialogFormFooter = R.compose(withDialogActions)(
TaxRateFormDialogFormFooterRoot,
);

View File

@@ -0,0 +1,7 @@
export const transformApiErrors = () => {
return {};
};
export const transformFormToReq = () => {
return {};
};

View File

@@ -0,0 +1,29 @@
// @ts-nocheck
import React from 'react';
import TaxRateDetailsContentActionsBar from './TaxRateDetailsContentActionsBar';
import { TaxRateDetailsContentBoot } from './TaxRateDetailsContentBoot';
import { DrawerBody, DrawerHeaderContent } from '@/components';
import TaxRateDetailsContentDetails from './TaxRateDetailsContentDetails';
import { DRAWERS } from '@/constants/drawers';
interface TaxRateDetailsContentProps {
taxRateid: number;
}
export default function TaxRateDetailsContent({
taxRateId,
}: TaxRateDetailsContentProps) {
return (
<TaxRateDetailsContentBoot taxRateId={taxRateId}>
<DrawerHeaderContent
name={DRAWERS.TAX_RATE_DETAILS}
title={'Tax Rate Details'}
/>
<TaxRateDetailsContentActionsBar />
<DrawerBody>
<TaxRateDetailsContentDetails />
</DrawerBody>
</TaxRateDetailsContentBoot>
);
}

View File

@@ -0,0 +1,71 @@
// @ts-nocheck
import React from 'react';
import {
Button,
Classes,
Intent,
NavbarDivider,
NavbarGroup,
} from '@blueprintjs/core';
import * as R from 'ramda';
import { Can, DashboardActionsBar, Icon } from '@/components';
import { AbilitySubject, TaxRateAction } from '@/constants/abilityOption';
import withDrawerActions from '@/containers/Drawer/withDrawerActions';
import withAlertsActions from '@/containers/Alert/withAlertActions';
import withDialogActions from '@/containers/Dialog/withDialogActions';
import { useTaxRateDetailsContext } from './TaxRateDetailsContentBoot';
import { DialogsName } from '@/constants/dialogs';
/**
* Tax rate details content actions bar.
* @returns {JSX.Element}
*/
function TaxRateDetailsContentActionsBar({
// #withDrawerActions
openDialog,
// #withAlertsActions
openAlert,
}) {
const { taxRateId } = useTaxRateDetailsContext();
// Handle edit tax rate.
const handleEditTaxRate = () => {
openDialog(DialogsName.TaxRateForm, { id: taxRateId });
};
// Handle delete tax rate.
const handleDeleteTaxRate = () => {
openAlert('tax-rate-delete', { taxRateId });
};
return (
<DashboardActionsBar>
<NavbarGroup>
<Can I={TaxRateAction.Edit} a={AbilitySubject.TaxRate}>
<Button
className={Classes.MINIMAL}
icon={<Icon icon="pen-18" />}
text={'Edit Tax Rate'}
onClick={handleEditTaxRate}
/>
</Can>
<Can I={TaxRateAction.Delete} a={AbilitySubject.Item}>
<NavbarDivider />
<Button
className={Classes.MINIMAL}
text={'Delete'}
icon={<Icon icon={'trash-16'} iconSize={16} />}
intent={Intent.DANGER}
onClick={handleDeleteTaxRate}
/>
</Can>
</NavbarGroup>
</DashboardActionsBar>
);
}
export default R.compose(
withDrawerActions,
withDialogActions,
withAlertsActions
)(TaxRateDetailsContentActionsBar);

View File

@@ -0,0 +1,40 @@
// @ts-nocheck
import React, { createContext, useContext } from 'react';
import { DrawerLoading } from '@/components';
import { useTaxRate } from '@/hooks/query/taxRates';
const TaxRateDetailsContext = createContext();
interface TaxRateDetailsContentBootProps {
taxRateId: number;
}
/**
* Tax rate details content boot.
* @returns {JSX}
*/
export function TaxRateDetailsContentBoot({
taxRateId,
...props
}: TaxRateDetailsContentBootProps) {
const {
data: taxRate,
isFetching: isTaxRateFetching,
isLoading: isTaxRateLoading,
} = useTaxRate(taxRateId, { keepPreviousData: true });
const provider = {
isTaxRateLoading,
isTaxRateFetching,
taxRate,
taxRateId,
};
return (
<DrawerLoading loading={isTaxRateLoading}>
<TaxRateDetailsContext.Provider value={provider} {...props} />
</DrawerLoading>
);
}
export const useTaxRateDetailsContext = () => useContext(TaxRateDetailsContext);

View File

@@ -0,0 +1,68 @@
// @ts-nocheck
import React from 'react';
import { Card, DetailItem, DetailsMenu } from '@/components';
import { useTaxRateDetailsContext } from './TaxRateDetailsContentBoot';
import { Intent, Tag } from '@blueprintjs/core';
import styled from 'styled-components';
export default function TaxRateDetailsContentDetails() {
const { taxRate } = useTaxRateDetailsContext();
return (
<Card>
<div>
<TaxRateHeader>
<TaxRateAmount>{taxRate.rate}%</TaxRateAmount>
<TaxRateActiveTag round={false} intent={Intent.SUCCESS} minimal>
Active
</TaxRateActiveTag>
</TaxRateHeader>
<DetailsMenu direction={'horizantal'} minLabelSize={200}>
<DetailItem label={'Tax Rate Name'} children={taxRate.name} />
<DetailItem label={'Code'} children={taxRate.code} />
<DetailItem
label={'Description'}
children={taxRate.description || '-'}
/>
<DetailItem
label={'Non Recoverable'}
children={
<Tag round={false} intent={Intent.SUCCESS} minimal>
Enabled
</Tag>
}
/>
<DetailItem
label={'Compound'}
children={
<Tag round={false} intent={Intent.SUCCESS} minimal>
Enabled
</Tag>
}
/>
</DetailsMenu>
</div>
</Card>
);
}
const TaxRateHeader = styled(`div`)`
margin-bottom: 1.25rem;
display: flex;
align-items: flex-start;
margin-top: 0.25rem;
`;
const TaxRateAmount = styled('div')`
line-height: 1;
font-size: 30px;
color: #565b71;
font-weight: 600;
display: inline-block;
`;
const TaxRateActiveTag = styled(Tag)`
margin-top: auto;
margin-bottom: auto;
margin-left: 1rem;
`;

View File

@@ -0,0 +1,35 @@
// @ts-nocheck
import React from 'react';
import * as R from 'ramda';
import { Drawer, DrawerHeaderContent, DrawerSuspense } from '@/components';
import withDrawers from '@/containers/Drawer/withDrawers';
import { DRAWERS } from '@/constants/drawers';
const TaxRateDetailsDrawerContent = React.lazy(
() => import('./TaxRateDetailsContent'),
);
/**
* Tax rate details drawer.
*/
function TaxRateDetailsDrawer({
name,
// #withDrawer
isOpen,
payload: { taxRateId },
}) {
return (
<Drawer
isOpen={isOpen}
name={name}
style={{ minWidth: '650px', maxWidth: '650px' }}
size={'65%'}
>
<DrawerSuspense>
<TaxRateDetailsDrawerContent name={name} taxRateId={taxRateId} />
</DrawerSuspense>
</Drawer>
);
}
export default R.compose(withDrawers())(TaxRateDetailsDrawer);

View File

@@ -0,0 +1,23 @@
// @ts-nocheck
import React, { useEffect } from 'react';
import { DashboardPageContent } from '@/components';
import { TaxRatesLandingProvider } from '../containers/TaxRatesLandingProvider';
import TaxRatesLandingActionsBar from '../containers/TaxRatesLandingActionsBar';
import TaxRatesDataTable from '../containers/TaxRatesLandingTable';
/**
* Tax rates landing page.
* @returns {JSX.Element}
*/
export default function TaxRatesLanding() {
return (
<TaxRatesLandingProvider>
<TaxRatesLandingActionsBar />
<DashboardPageContent>
<TaxRatesDataTable />
</DashboardPageContent>
</TaxRatesLandingProvider>
);
}

View File

@@ -1,6 +1,8 @@
// @ts-nocheck
import { useMutation, useQueryClient } from 'react-query';
import { useRequestQuery } from '../useQueryRequest';
import QUERY_TYPES from './types';
import useApiRequest from '../useRequest';
/**
* Retrieves tax rates.
@@ -20,3 +22,75 @@ export function useTaxRates(props) {
},
);
}
/**
* Retrieves tax rate.
* @param {number} taxRateId - Tax rate id.
*/
export function useTaxRate(taxRateId: string, props) {
return useRequestQuery(
[QUERY_TYPES.TAX_RATES, taxRateId],
{
method: 'get',
url: `tax-rates/${taxRateId}}`,
},
{
select: (res) => res.data.data,
...props,
},
);
}
/**
* Edit the given tax rate.
*/
export function useEditTaxRate(props) {
const queryClient = useQueryClient();
const apiRequest = useApiRequest();
return useMutation(
([id, values]) => apiRequest.post(`tax-rates/${id}`, values),
{
onSuccess: (res, id) => {
// Invalidate specific item.
queryClient.invalidateQueries([QUERY_TYPES.TAX_RATES, id]);
queryClient.invalidateQueries([QUERY_TYPES.TAX_RATES]);
},
...props,
},
);
}
/**
* Creates a new tax rate.
*/
export function useCreateTaxRate(props) {
const queryClient = useQueryClient();
const apiRequest = useApiRequest();
return useMutation(([values]) => apiRequest.post('tax-rates', values), {
onSuccess: (res, id) => {
// Invalidate specific item.
queryClient.invalidateQueries([QUERY_TYPES.TAX_RATES, id]);
queryClient.invalidateQueries([QUERY_TYPES.TAX_RATES]);
},
...props,
});
}
/**
* Deletes a new tax rate.
*/
export function useDeleteTaxRate(props) {
const queryClient = useQueryClient();
const apiRequest = useApiRequest();
return useMutation(([id]) => apiRequest.delete(`tax-rates/${id}`), {
onSuccess: (res, id) => {
// Invalidate specific item.
queryClient.invalidateQueries([QUERY_TYPES.TAX_RATES, id]);
queryClient.invalidateQueries([QUERY_TYPES.TAX_RATES]);
},
...props,
});
}

View File

@@ -1069,6 +1069,14 @@ export const getDashboardRoutes = () => [
),
pageTitle: intl.get('sidebar.projects'),
},
{
path: '/tax-rates',
component: lazy(
() =>
import('@/containers/TaxRates/pages/TaxRatesLanding'),
),
pageTitle: 'Tax Rates',
},
// Homepage
{
path: `/`,