mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-16 04:40:32 +00:00
feat(webapp): bank rule
This commit is contained in:
14
packages/webapp/src/components/AppShell/AppShell.module.scss
Normal file
14
packages/webapp/src/components/AppShell/AppShell.module.scss
Normal 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;
|
||||
}
|
||||
39
packages/webapp/src/components/AppShell/AppShell.tsx
Normal file
39
packages/webapp/src/components/AppShell/AppShell.tsx
Normal 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} />;
|
||||
}
|
||||
25
packages/webapp/src/components/AppShell/AppShellProvider.tsx
Normal file
25
packages/webapp/src/components/AppShell/AppShellProvider.tsx
Normal 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);
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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',
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user