feat(webapp): bank rule

This commit is contained in:
Ahmed Bouhuolia
2024-06-25 22:20:36 +02:00
parent f1f52ce972
commit 47879d04b2
11 changed files with 417 additions and 53 deletions

View File

@@ -0,0 +1,14 @@
.main{
flex: 1 0;
}
.aside{
width: 500px;
height: 100dvh;
border-left: 1px solid rgba(17, 20, 24, 0.15);
}
.root {
display: flex;
}

View File

@@ -0,0 +1,39 @@
import React from 'react';
import { AppShellProvider } from './AppShellProvider';
import { Box } from '../Layout';
import styles from './AppShell.module.scss';
interface AppShellProps {
topbarOffset?: number;
mainProps: any;
asideProps: any;
children: React.ReactNode;
}
export function AppShell({
asideProps,
mainProps,
topbarOffset = 0,
...restProps
}: AppShellProps) {
return (
<AppShellProvider mainProps={mainProps} asideProps={asideProps} topbarOffset={topbarOffset}>
<Box {...restProps} className={styles.root} />
</AppShellProvider>
);
}
AppShell.Main = AppShellMain;
AppShell.Aside = AppShellAside;
function AppShellMain({ ...props }) {
return <Box {...props} className={styles.main} />;
}
interface AppShellAsideProps {
children: React.ReactNode;
}
function AppShellAside({ ...props }: AppShellAsideProps) {
return <Box {...props} className={styles.aside} />;
}

View File

@@ -0,0 +1,25 @@
import React, { createContext } from 'react';
interface AppShellContextValue {
topbarOffset: number
}
const AppShellContext = createContext<AppShellContextValue>(
{} as AppShellContextValue,
);
interface AppShellProviderProps {
children: React.ReactNode;
mainProps: any;
asideProps: any;
topbarOffset: number;
}
export function AppShellProvider({ topbarOffset, ...props }: AppShellProviderProps) {
const provider = { topbarOffset } as AppShellContextValue;
return <AppShellContext.Provider value={provider} {...props} />;
}
export const useAppShellContext = () =>
React.useContext<AppShellContextValue>(AppShellContext);

View File

@@ -1,3 +1,4 @@
import { Classes } from '@blueprintjs/core';
import { RuleFormBoot } from './RuleFormBoot';
import { RuleFormContentForm } from './RuleFormContentForm';
@@ -12,7 +13,9 @@ export default function RuleFormContent({
}: RuleFormContentProps) {
return (
<RuleFormBoot bankRuleId={bankRuleId}>
<RuleFormContentForm />
<div className={Classes.DIALOG_BODY}>
<RuleFormContentForm />
</div>
</RuleFormBoot>
);
}

View File

@@ -3,11 +3,20 @@ 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(),
applyIfAccountId: Yup.number().required().label('Apply to account'),
applyIfTransactionType: Yup.string()
.required()
.label('Apply to transaction type'),
conditionsType: Yup.string().required().label('Condition type'),
assignCategory: Yup.string().required().label('Assign to category'),
assignAccountId: Yup.string().required().label('Assign to account'),
conditions: Yup.array().of(
Yup.object().shape({
value: Yup.string().required().label('Value'),
comparator: Yup.string().required().label('Comparator'),
field: Yup.string().required().label('Field'),
}),
),
});
export const CreateRuleFormSchema = Schema;

View File

@@ -12,6 +12,7 @@ import {
FRadioGroup,
FSelect,
Group,
Stack,
} from '@/components';
import { useCreateBankRule } from '@/hooks/query/bank-rules';
import {
@@ -72,13 +73,14 @@ function RuleFormContentFormRoot({
onSubmit={handleSubmit}
>
<Form>
<FFormGroup name={'name'} label={'Rule Name'}>
<FFormGroup name={'name'} label={'Rule Name'} style={{ maxWidth: 300 }}>
<FInputGroup name={'name'} />
</FFormGroup>
<FFormGroup
name={'applyIfAccountId'}
label={'Apply the rule to account'}
style={{ maxWidth: 350 }}
>
<AccountsSelect name={'applyIfAccountId'} items={accounts} />
</FFormGroup>
@@ -86,10 +88,12 @@ function RuleFormContentFormRoot({
<FFormGroup
name={'applyIfTransactionType'}
label={'Apply to transactions are'}
style={{ maxWidth: 350 }}
>
<FSelect
name={'applyIfTransactionType'}
items={TransactionTypeOptions}
popoverProps={{ minimal: true, inline: false }}
/>
</FFormGroup>
@@ -107,20 +111,35 @@ function RuleFormContentFormRoot({
</FFormGroup>
<RuleFormConditions />
<h3>Then Assign</h3>
<h3 style={{ fontSize: 14, fontWeight: 600, marginBottom: '0.8rem' }}>
Then Assign
</h3>
<FFormGroup name={'assignCategory'} label={'Transaction type'}>
<FFormGroup
name={'assignCategory'}
label={'Transaction type'}
style={{ maxWidth: 300 }}
>
<FSelect
name={'assignCategory'}
items={AssignTransactionTypeOptions}
popoverProps={{ minimal: true, inline: false }}
/>
</FFormGroup>
<FFormGroup name={'assignAccountId'} label={'Account category'}>
<FFormGroup
name={'assignAccountId'}
label={'Account category'}
style={{ maxWidth: 300 }}
>
<AccountsSelect name={'assignAccountId'} items={accounts} />
</FFormGroup>
<FFormGroup name={'assignRef'} label={'Reference'}>
<FFormGroup
name={'assignRef'}
label={'Reference'}
style={{ maxWidth: 300 }}
>
<FInputGroup name={'assignRef'} />
</FFormGroup>
@@ -146,54 +165,87 @@ function RuleFormConditions() {
};
return (
<Box>
{values?.conditions?.map((condition, index) => (
<Group>
<FFormGroup name={`conditions[${index}].field`} label={'Field'}>
<FSelect name={`conditions[${index}].field`} items={Fields} />
</FFormGroup>
<Box style={{ marginBottom: 15 }}>
<Stack spacing={15}>
{values?.conditions?.map((condition, index) => (
<Group key={index} style={{ width: 500 }}>
<FFormGroup
name={`conditions[${index}].field`}
label={'Field'}
style={{ marginBottom: 0, flex: '1 0' }}
>
<FSelect
name={`conditions[${index}].field`}
items={Fields}
popoverProps={{ minimal: true, inline: false }}
/>
</FFormGroup>
<FFormGroup
name={`conditions[${index}].comparator`}
label={'Condition'}
>
<FSelect
<FFormGroup
name={`conditions[${index}].comparator`}
items={FieldCondition}
/>
</FFormGroup>
label={'Condition'}
style={{ marginBottom: 0, flex: '1 0' }}
>
<FSelect
name={`conditions[${index}].comparator`}
items={FieldCondition}
popoverProps={{ minimal: true, inline: false }}
/>
</FFormGroup>
<FFormGroup name={`conditions[${index}].value`} label={'Condition'}>
<FInputGroup name={`conditions[${index}].value`} />
</FFormGroup>
</Group>
))}
<FFormGroup
name={`conditions[${index}].value`}
label={'Value'}
style={{ marginBottom: 0, flex: '1 0 ', width: '40%' }}
>
<FInputGroup name={`conditions[${index}].value`} />
</FFormGroup>
</Group>
))}
</Stack>
<Button type={'button'} onClick={handleAddConditionBtnClick}>
<Button
minimal
small
intent={Intent.PRIMARY}
type={'button'}
onClick={handleAddConditionBtnClick}
style={{ marginTop: 8 }}
>
Add Condition
</Button>
</Box>
);
}
function RuleFormActions() {
function RuleFormActionsRoot({
// #withDialogActions
closeDialog,
}) {
const { isSubmitting, submitForm } = useFormikContext<RuleFormValues>();
const handleSaveBtnClick = () => {
submitForm();
};
const handleCancelBtnClick = () => {};
const handleCancelBtnClick = () => {
closeDialog(DialogsName.BankRuleForm);
};
return (
<Box className={Classes.DIALOG_FOOTER}>
<Button
intent={Intent.PRIMARY}
loading={isSubmitting}
onClick={handleSaveBtnClick}
>
Save
</Button>
<Button onClick={handleCancelBtnClick}>Cancel</Button>
<Box className={Classes.DIALOG_FOOTER_ACTIONS}>
<Button onClick={handleCancelBtnClick}>Cancel</Button>
<Button
intent={Intent.PRIMARY}
loading={isSubmitting}
onClick={handleSaveBtnClick}
style={{ minWidth: 100 }}
>
Save
</Button>
</Box>
</Box>
);
}
const RuleFormActions = R.compose(withDialogActions)(RuleFormActionsRoot);

View File

@@ -3,12 +3,12 @@ export const initialValues = {
order: 0,
applyIfAccountId: '',
applyIfTransactionType: '',
conditionsType: '',
conditionsType: 'and',
conditions: [
{
field: 'description',
comparator: 'contains',
value: 'payment',
value: '',
},
],
assignCategory: '',
@@ -41,7 +41,7 @@ export const Fields = [
];
export const FieldCondition = [
{ value: 'contains', text: 'Contains' },
{ value: 'equals', text: 'equals' },
{ value: 'equals', text: 'Equals' },
{ value: 'not_contains', text: 'Not Contains' },
];
export const AssignTransactionTypeOptions = [

View File

@@ -14,6 +14,8 @@ import {
import { AccountTransactionsDetailsBar } from './AccountTransactionsDetailsBar';
import { AccountTransactionsProgressBar } from './components';
import { AccountTransactionsFilterTabs } from './AccountTransactionsFilterTabs';
import { AppShell } from '@/components/AppShell/AppShell';
import { CategorizeTransactionAside } from '../CategorizeTransactionAside/CategorizeTransactionAside';
/**
* Account transactions list.
@@ -21,17 +23,25 @@ import { AccountTransactionsFilterTabs } from './AccountTransactionsFilterTabs';
function AccountTransactionsList() {
return (
<AccountTransactionsProvider>
<AccountTransactionsActionsBar />
<AccountTransactionsDetailsBar />
<AccountTransactionsProgressBar />
<AppShell>
<AppShell.Main>
<AccountTransactionsActionsBar />
<AccountTransactionsDetailsBar />
<AccountTransactionsProgressBar />
<DashboardPageContent>
<AccountTransactionsFilterTabs />
<DashboardPageContent>
<AccountTransactionsFilterTabs />
<Suspense fallback={<Spinner size={30} />}>
<AccountTransactionsContent />
</Suspense>
</DashboardPageContent>
<Suspense fallback={<Spinner size={30} />}>
<AccountTransactionsContent />
</Suspense>
</DashboardPageContent>
</AppShell.Main>
<AppShell.Aside>
<CategorizeTransactionAside />
</AppShell.Aside>
</AppShell>
</AccountTransactionsProvider>
);
}

View File

@@ -0,0 +1,61 @@
.transaction {
background: #fff;
border-radius: 5px;
border: 1px solid #D6DBE3;
padding: 12px 16px;
}
.asideHeader {
align-items: center;
background: #fff;
border-bottom: 1px solid #E1E2E9;
display: flex;
flex: 0 0 auto;
min-height: 40px;
padding: 5px 5px 5px 15px;
z-index: 0;
}
.tabs :global .bp4-tab-panel{
margin-top: 0;
}
.tabs :global .bp4-tab-list{
background: #fff;
border-bottom: 1px solid #c7d5db;
padding: 0 22px;
}
.tabs :global .bp4-large > .bp4-tab{
font-size: 14px;
}
.matchBar{
padding: 16px 18px;
background: #fff;
border-bottom: 1px solid #E1E2E9;
border-top: 1px solid #E1E2E9;
}
.matchBarTitle {
font-size: 14px;
font-weight: 500;
}
.footerActions {
padding: 14px 16px;
border-top: 1px solid #E1E2E9;
}
.footerTotal {
padding: 8px 16px;
border: 1px solid #E1E2E9;
}
.checkbox:global(.bp4-control.bp4-checkbox){
margin: 0;
}
.checkbox:global(.bp4-control.bp4-checkbox) :global .bp4-control-indicator{
border-color: #CBCBCB;
}

View File

@@ -0,0 +1,145 @@
import { Box, Group, Icon, Stack } from '@/components';
import {
AnchorButton,
Button,
Checkbox,
Classes,
Intent,
Tab,
Tabs,
Tag,
Text,
} from '@blueprintjs/core';
import styles from './CategorizeTransactionAside.module.scss';
interface AsideProps {
title?: string;
onClose?: () => void;
children?: React.ReactNode;
}
function Aside({ title, onClose, children }: AsideProps) {
const handleClose = () => {
onClose && onClose();
};
return (
<Box>
<Group position="apart" className={styles.asideHeader}>
{title}
<Button
aria-label="Close"
className={Classes.DIALOG_CLOSE_BUTTON}
icon={<Icon icon={'smallCross'} color={'#000'} />}
minimal={true}
onClick={handleClose}
/>
</Group>
<Box>{children}</Box>
</Box>
);
}
export function CategorizeTransactionAside() {
return (
<Aside title={'Categorize Bank Transaction'}>
<Tabs large className={styles.tabs}>
<Tab
id="categorize"
title="Categorize Transaction"
panel={<CategorizeBankTransactionContent />}
/>
<Tab
id="matching"
title="Matching Transaction"
panel={<MatchingBankTransactionContent />}
/>
</Tabs>
</Aside>
);
}
export function MatchingBankTransactionContent() {
return (
<Box>
<Box className={styles.matchBar}>
<Group spacing={6}>
<h2 className={styles.matchBarTitle}>Perfect Matchines</h2>
<Tag minimal intent={Intent.SUCCESS}>
2
</Tag>
</Group>
</Box>
<Stack spacing={9} style={{ padding: 15 }}>
<MatchTransaction label={''} date={''} />
<MatchTransaction label={''} date={''} />
<MatchTransaction label={''} date={''} />
<MatchTransaction label={''} date={''} />
</Stack>
<Box className={styles.matchBar}>
<Stack spacing={2}>
<h2 className={styles.matchBarTitle}>Perfect Matches</h2>
<Text style={{ fontSize: 12, color: '#5C7080' }}>
Transactions up to 20 Aug 2019
</Text>
</Stack>
</Box>
<Stack spacing={9} style={{ padding: 15 }}>
<MatchTransaction label={''} date={''} />
<MatchTransaction label={''} date={''} />
<MatchTransaction label={''} date={''} />
<MatchTransaction label={''} date={''} />
</Stack>
<MatchTransactionFooter />
</Box>
);
}
export function CategorizeBankTransactionContent() {
return <h1>Categorizing</h1>;
}
interface MatchTransactionProps {
label: string;
date: string;
}
function MatchTransaction({ label, date }: MatchTransactionProps) {
return (
<Group className={styles.transaction} position="apart">
<Stack spacing={3}>
<span>Expense for $10,000</span>
<Text style={{ fontSize: 12, color: '#5C7080' }}>
Date: 02/02/2020{' '}
</Text>
</Stack>
<Checkbox className={styles.checkbox} />
</Group>
);
}
function MatchTransactionFooter() {
return (
<Box>
<Box className={styles.footerTotal}>
<Group>
<AnchorButton small minimal intent={Intent.PRIMARY}>
Add Reconcile Transaction +
</AnchorButton>
<Text>Pending $10,000</Text>
</Group>
</Box>
<Box className={styles.footerActions}>
<Group>
<Button intent={Intent.PRIMARY}>Match</Button>
<Button>Cancel</Button>
</Group>
</Box>
</Box>
);
}

View File

@@ -605,4 +605,10 @@ export default {
],
viewBox: '0 0 16 16',
},
smallCross: {
path: [
'M9.41,8l3.29-3.29C12.89,4.53,13,4.28,13,4c0-0.55-0.45-1-1-1c-0.28,0-0.53,0.11-0.71,0.29L8,6.59L4.71,3.29C4.53,3.11,4.28,3,4,3C3.45,3,3,3.45,3,4c0,0.28,0.11,0.53,0.29,0.71L6.59,8l-3.29,3.29C3.11,11.47,3,11.72,3,12c0,0.55,0.45,1,1,1c0.28,0,0.53-0.11,0.71-0.29L8,9.41l3.29,3.29C11.47,12.89,11.72,13,12,13c0.55,0,1-0.45,1-1c0-0.28-0.11-0.53-0.29-0.71L9.41,8z',
],
viewBox: '0 0 16 16',
},
};