Compare commits

...

1 Commits

Author SHA1 Message Date
Ahmed Bouhuolia
882fc20ac1 feat(webapp): bank rules 2024-06-25 11:41:31 +02:00
17 changed files with 483 additions and 3 deletions

View File

@@ -3,7 +3,15 @@ import React from 'react';
import clsx from 'classnames';
import { Navbar } from '@blueprintjs/core';
export function DashboardActionsBar({ className, children, name }) {
interface DashboardActionsBarProps {
children?: React.ReactNode;
}
export function DashboardActionsBar({
className,
children,
name,
}: DashboardActionsBarProps) {
return (
<div
className={clsx(

View File

@@ -20,8 +20,9 @@ export const AbilitySubject = {
SubscriptionBilling: 'SubscriptionBilling',
CreditNote: 'CreditNote',
VendorCredit: 'VendorCredit',
Project:'Project',
Project: 'Project',
TaxRate: 'TaxRate',
BankRule: 'BankRule',
};
export const ItemAction = {
@@ -188,10 +189,16 @@ export const SubscriptionBillingAbility = {
Payment: 'payment',
};
export const TaxRateAction = {
View: 'View',
Create: 'Create',
Edit: 'Edit',
Delete: 'Delete',
};
export const BankRuleAction = {
View: 'View',
Create: 'Create',
Edit: 'Edit',
Delete: 'Delete',
};

View File

@@ -458,6 +458,11 @@ export const SidebarMenu = [
ability: CashflowAction.View,
},
},
{
text: 'Rules',
href: '/bank-rules',
type: ISidebarMenuItemType.Link,
},
],
},
{

View File

@@ -0,0 +1,32 @@
import React, { createContext } from 'react';
import { DialogContent } from '@/components';
interface RuleFormBootValues {
bankRule?: null;
bankRuleId?: null;
isBankRuleLoading: boolean;
}
const RuleFormBootContext = createContext<RuleFormBootValues>(
{} as RuleFormBootValues,
);
interface RuleFormBootProps {
bankRuleId?: number;
children: React.ReactNode;
}
function RuleFormBoot({ bankRuleId, ...props }: RuleFormBootProps) {
const provider = {} as RuleFormBootValues;
return (
<DialogContent isLoading={false}>
<RuleFormBootContext.Provider value={provider} {...props} />
</DialogContent>
);
}
const useRuleFormDialogBoot = () =>
React.useContext<RuleFormBootValues>(RuleFormBootContext);
export { RuleFormBoot, useRuleFormDialogBoot };

View File

@@ -0,0 +1,19 @@
import { RuleFormBoot } from "./RuleFormBoot";
interface RuleFormContentProps {
dialogName: string;
bankRuleId?: number;
}
export function RuleFormContent({
dialogName,
bankRuleId,
}: RuleFormContentProps) {
return (
<RuleFormBoot
bankRuleId={bankRuleId}
>
</RuleFormBoot>
);
}

View File

@@ -0,0 +1,13 @@
// @ts-nocheck
import * as Yup from 'yup';
const Schema = Yup.object().shape({
name: Yup.string().required().label('Rule name'),
applyIfAccountId: Yup.number().required().label(''),
applyIfTransactionType: Yup.string().required().label(''),
conditionsType: Yup.string().required(),
assignCategory: Yup.string().required(),
assignAccountId: Yup.string().required(),
});
export const CreateRuleFormSchema = Schema;

View File

@@ -0,0 +1,134 @@
import { Form, Formik, useFormikContext } from 'formik';
import { Button, Radio } from '@blueprintjs/core';
import { CreateRuleFormSchema } from './RuleFormContentForm.schema';
import {
Box,
FFormGroup,
FInputGroup,
FRadioGroup,
FSelect,
Group,
} from '@/components';
const initialValues = {
name: '',
order: 0,
applyIfAccountId: '',
applyIfTransactionType: '',
conditionsType: '',
conditions: [
{
field: 'description',
comparator: 'contains',
value: 'payment',
},
],
assignCategory: '',
assignAccountId: '',
};
interface RuleFormValues {
name: string;
order: number;
applyIfAccountId: string;
applyIfTransactionType: string;
conditionsType: string;
conditions: Array<{
field: string;
comparator: string;
value: string;
}>;
assignCategory: string;
assignAccountId: string;
}
export function RuleFormContentForm() {
const validationSchema = CreateRuleFormSchema;
const handleSubmit = () => {};
return (
<Formik<RuleFormValues>
initialValues={initialValues}
validationSchema={validationSchema}
onSubmit={handleSubmit}
>
<Form>
<FFormGroup name={'name'} label={'Rule Name'}>
<FInputGroup name={'name'} />
</FFormGroup>
<FFormGroup name={'conditionsType'} label={'Apply to transactions are'}>
<FSelect name={'conditionsType'} items={[]} />
</FFormGroup>
<FFormGroup name={''} label={'Categorize the transactions when'}>
<FRadioGroup name={'conditionsType'}>
<Radio value={'and'} label={'All the following criteria matches'} />
<Radio
value={'or'}
label={'Any one of the following criteria matches'}
/>
</FRadioGroup>
</FFormGroup>
<RuleFormConditions />
<h3>Then Assign</h3>
<FFormGroup name={'assignCategory'} label={'Transaction type'}>
<FSelect name={'assignCategory'} items={[]} />
</FFormGroup>
<FFormGroup name={'assignAccountId'} label={'Account category'}>
<FSelect name={'assignAccountId'} items={[]} />
</FFormGroup>
<FFormGroup name={'assignRef'} label={'Reference'}>
<FInputGroup name={'assignRef'} />
</FFormGroup>
</Form>
</Formik>
);
}
function RuleFormConditions() {
const { values } = useFormikContext<RuleFormValues>();
const handleAddConditionBtnClick = () => {
values.conditions.push({
field: '',
comparator: '',
value: '',
});
};
return (
<Box>
{values?.conditions?.map((condition, index) => (
<Group>
<FFormGroup name={`conditions[${index}].field`} label={'Field'}>
<FSelect name={`conditions[${index}].field`} items={[]} />
</FFormGroup>
<FFormGroup
name={`conditions[${index}].comparator`}
label={'Condition'}
>
<FSelect name={`conditions[${index}].comparator`} items={[]} />
</FFormGroup>
<FFormGroup
name={`conditions[${index}].condition`}
label={'Condition'}
>
<FInputGroup name={`conditions[${index}].value`} />
</FFormGroup>
</Group>
))}
<Button type={'button'} onClick={handleAddConditionBtnClick}>
Add Condition
</Button>
</Box>
);
}

View File

@@ -0,0 +1,33 @@
// @ts-nocheck
import React from 'react';
import { Dialog, DialogSuspense } from '@/components';
import withDialogRedux from '@/components/DialogReduxConnect';
import { compose } from '@/utils';
const RuleFormContent = React.lazy(() => import('./RuleFormContent'));
/**
* Payment mail dialog.
*/
function RuleFormDialog({
dialogName,
payload: { bankRuleId = null },
isOpen,
}) {
return (
<Dialog
name={dialogName}
title={'New Bank Rule'}
isOpen={isOpen}
canEscapeJeyClose={true}
autoFocus={true}
style={{ width: 600 }}
>
<DialogSuspense>
<RuleFormContent dialogName={dialogName} bankRuleId={bankRuleId} />
</DialogSuspense>
</Dialog>
);
}
export default compose(withDialogRedux())(RuleFormDialog);

View File

@@ -0,0 +1,39 @@
// @ts-nocheck
import * as R from 'ramda';
import { Button, Intent } from '@blueprintjs/core';
import { EmptyStatus, Can, FormattedMessage as T } from '@/components';
import { AbilitySubject, BankRuleAction } from '@/constants/abilityOption';
import withDialogActions from '@/containers/Dialog/withDialogActions';
function BankRulesLandingEmptyStateRoot({
// #withDialogAction
openDialog,
}) {
return (
<EmptyStatus
title={"The organization doesn't have taxes, yet!"}
description={
<p>
Setup the organization taxes to start tracking taxes on sales
transactions.
</p>
}
action={
<>
<Can I={BankRuleAction.Create} a={AbilitySubject.BankRule}>
<Button intent={Intent.PRIMARY} large={true} onClick={() => {}}>
New tax rate
</Button>
<Button intent={Intent.NONE} large={true}>
<T id={'learn_more'} />
</Button>
</Can>
</>
}
/>
);
}
export const BankRulesLandingEmptyState = R.compose(withDialogActions)(
BankRulesLandingEmptyStateRoot,
);

View File

@@ -0,0 +1,20 @@
// @ts-nocheck
import { DashboardPageContent } from '@/components';
import { RulesListBoot } from './RulesListBoot';
import { RulesListActionsBar } from './RulesListActionsBar';
import { BankRulesTable } from './RulesTable';
/**
*
*/
export function RulesList() {
return (
<RulesListBoot>
<RulesListActionsBar />
<DashboardPageContent>
<BankRulesTable />
</DashboardPageContent>
</RulesListBoot>
);
}

View File

@@ -0,0 +1,10 @@
import { DashboardActionsBar } from '@/components';
import { NavbarGroup } from '@blueprintjs/core';
export function RulesListActionsBar() {
return (
<DashboardActionsBar>
<NavbarGroup></NavbarGroup>
</DashboardActionsBar>
);
}

View File

@@ -0,0 +1,30 @@
import React, { createContext } from 'react';
import { DialogContent } from '@/components';
interface RulesListBootValues {
rules: any;
isRulesLoading: boolean;
}
const RulesListBootContext = createContext<RulesListBootValues>(
{} as RulesListBootValues,
);
interface RulesListBootProps {
children: React.ReactNode;
}
function RulesListBoot({ ...props }: RulesListBootProps) {
const provider = {} as RulesListBootValues;
return (
<DialogContent isLoading={false}>
<RulesListBootContext.Provider value={provider} {...props} />
</DialogContent>
);
}
const useRulesListBoot = () =>
React.useContext<RulesListBootValues>(RulesListBootContext);
export { RulesListBoot, useRulesListBoot };

View File

@@ -0,0 +1,81 @@
// @ts-nocheck
import * as R from 'ramda';
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 { useBankRulesTableColumns } from './hooks';
import { BankRulesTableActionsMenu } from './_components';
import { BankRulesLandingEmptyState } from './BankRulesLandingEmptyState';
/**
* Invoices datatable.
*/
function RulesTable({
// #withAlertsActions
openAlert,
// #withDrawerActions
openDrawer,
// #withDialogAction
openDialog,
}) {
// Invoices table columns.
const columns = useBankRulesTableColumns();
// Handle edit bank rule.
const handleDeleteBankRule = ({ id }) => {};
// Handle delete bank rule.
const handleEditBankRule = () => {};
// Display invoice empty status instead of the table.
if (isEmptyStatus) {
return <BankRulesLandingEmptyState />;
}
return (
<DashboardContentTable>
<DataTable
columns={columns}
data={[]}
loading={false}
headerLoading={false}
progressBarLoading={false}
manualSortBy={false}
selectionColumn={false}
noInitialFetch={true}
sticky={true}
pagination={false}
manualPagination={false}
autoResetSortBy={false}
autoResetPage={false}
TableLoadingRenderer={TableSkeletonRows}
TableHeaderSkeletonRenderer={TableSkeletonHeader}
ContextMenu={BankRulesTableActionsMenu}
onCellClick={handleCellClick}
size={'medium'}
payload={{
onDelete: handleDeleteTaxRate,
onEdit: handleEditTaxRate,
}}
/>
</DashboardContentTable>
);
}
export const BankRulesTable = R.compose(
withDashboardActions,
withAlertsActions,
withDrawerActions,
withDialogActions,
)(RulesTable);

View File

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

View File

@@ -0,0 +1,3 @@
export const useBankRulesTableColumns = () => {
return [];
};

View File

@@ -1221,6 +1221,14 @@ export const getDashboardRoutes = () => [
pageTitle: 'Tax Rates',
subscriptionActive: [SUBSCRIPTION_TYPE.MAIN],
},
// Bank Rules
{
path: '/bank-rules',
component: lazy(
() => import('@/containers/Banking/Rules/RulesList/RulesList'),
),
pageTitle: 'Bank Rules',
},
// Homepage
{
path: `/`,