mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-20 06:40:31 +00:00
feat: hook up the matching form to the server
This commit is contained in:
10
packages/webapp/src/components/Aside/Aside.module.scss
Normal file
10
packages/webapp/src/components/Aside/Aside.module.scss
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
.title{
|
||||||
|
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;
|
||||||
|
}
|
||||||
40
packages/webapp/src/components/Aside/Aside.tsx
Normal file
40
packages/webapp/src/components/Aside/Aside.tsx
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import { Button, Classes } from '@blueprintjs/core';
|
||||||
|
import { Box, Group } from '../Layout';
|
||||||
|
import { Icon } from '../Icon';
|
||||||
|
import styles from './Aside.module.scss';
|
||||||
|
|
||||||
|
interface AsideProps {
|
||||||
|
title?: string;
|
||||||
|
onClose?: () => void;
|
||||||
|
children?: React.ReactNode;
|
||||||
|
hideCloseButton?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Aside({
|
||||||
|
title,
|
||||||
|
onClose,
|
||||||
|
children,
|
||||||
|
hideCloseButton,
|
||||||
|
}: AsideProps) {
|
||||||
|
const handleClose = () => {
|
||||||
|
onClose && onClose();
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
<Group position="apart" className={styles.title}>
|
||||||
|
{title}
|
||||||
|
|
||||||
|
{hideCloseButton !== true && (
|
||||||
|
<Button
|
||||||
|
aria-label="Close"
|
||||||
|
className={Classes.DIALOG_CLOSE_BUTTON}
|
||||||
|
icon={<Icon icon={'smallCross'} color={'#000'} />}
|
||||||
|
minimal={true}
|
||||||
|
onClick={handleClose}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Group>
|
||||||
|
<Box>{children}</Box>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -33,7 +33,6 @@ function RulesTable({
|
|||||||
}) {
|
}) {
|
||||||
// Invoices table columns.
|
// Invoices table columns.
|
||||||
const columns = useBankRulesTableColumns();
|
const columns = useBankRulesTableColumns();
|
||||||
|
|
||||||
const { bankRules } = useRulesListBoot();
|
const { bankRules } = useRulesListBoot();
|
||||||
|
|
||||||
// Handle edit bank rule.
|
// Handle edit bank rule.
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
TableSkeletonHeader,
|
TableSkeletonHeader,
|
||||||
TableVirtualizedListRows,
|
TableVirtualizedListRows,
|
||||||
FormattedMessage as T,
|
FormattedMessage as T,
|
||||||
|
AppToaster,
|
||||||
} from '@/components';
|
} from '@/components';
|
||||||
import { TABLES } from '@/constants/tables';
|
import { TABLES } from '@/constants/tables';
|
||||||
|
|
||||||
@@ -24,6 +25,8 @@ import { useAccountUncategorizedTransactionsContext } from './AllTransactionsUnc
|
|||||||
|
|
||||||
import { compose } from '@/utils';
|
import { compose } from '@/utils';
|
||||||
import { DRAWERS } from '@/constants/drawers';
|
import { DRAWERS } from '@/constants/drawers';
|
||||||
|
import { useExcludeUncategorizedTransaction } from '@/hooks/query/bank-rules';
|
||||||
|
import { Intent } from '@blueprintjs/core';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Account transactions data table.
|
* Account transactions data table.
|
||||||
@@ -42,6 +45,9 @@ function AccountTransactionsDataTable({
|
|||||||
const { uncategorizedTransactions, isUncategorizedTransactionsLoading } =
|
const { uncategorizedTransactions, isUncategorizedTransactionsLoading } =
|
||||||
useAccountUncategorizedTransactionsContext();
|
useAccountUncategorizedTransactionsContext();
|
||||||
|
|
||||||
|
const { mutateAsync: excludeTransaction } =
|
||||||
|
useExcludeUncategorizedTransaction();
|
||||||
|
|
||||||
// Local storage memorizing columns widths.
|
// Local storage memorizing columns widths.
|
||||||
const [initialColumnsWidths, , handleColumnResizing] =
|
const [initialColumnsWidths, , handleColumnResizing] =
|
||||||
useMemorizedColumnsWidths(TABLES.UNCATEGORIZED_CASHFLOW_TRANSACTION);
|
useMemorizedColumnsWidths(TABLES.UNCATEGORIZED_CASHFLOW_TRANSACTION);
|
||||||
@@ -52,6 +58,22 @@ function AccountTransactionsDataTable({
|
|||||||
uncategorizedTransactionId: cell.row.original.id,
|
uncategorizedTransactionId: cell.row.original.id,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
// Handle exclude transaction.
|
||||||
|
const handleExcludeTransaction = (transaction) => {
|
||||||
|
excludeTransaction(transaction.id)
|
||||||
|
.then(() => {
|
||||||
|
AppToaster.show({
|
||||||
|
intent: Intent.SUCCESS,
|
||||||
|
message: 'The bank transaction has been excluded successfully.',
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
AppToaster.show({
|
||||||
|
intent: Intent.DANGER,
|
||||||
|
message: 'Something went wrong.',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CashflowTransactionsTable
|
<CashflowTransactionsTable
|
||||||
@@ -77,6 +99,9 @@ function AccountTransactionsDataTable({
|
|||||||
onColumnResizing={handleColumnResizing}
|
onColumnResizing={handleColumnResizing}
|
||||||
noResults={<T id={'cash_flow.account_transactions.no_results'} />}
|
noResults={<T id={'cash_flow.account_transactions.no_results'} />}
|
||||||
className="table-constrant"
|
className="table-constrant"
|
||||||
|
payload={{
|
||||||
|
onExclude: handleExcludeTransaction,
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import { AbilitySubject, CashflowAction } from '@/constants/abilityOption';
|
|||||||
import { safeCallback } from '@/utils';
|
import { safeCallback } from '@/utils';
|
||||||
|
|
||||||
export function ActionsMenu({
|
export function ActionsMenu({
|
||||||
payload: { onDelete, onViewDetails },
|
payload: { onDelete, onViewDetails, onExclude },
|
||||||
row: { original },
|
row: { original },
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
@@ -36,6 +36,12 @@ export function ActionsMenu({
|
|||||||
/>
|
/>
|
||||||
</If>
|
</If>
|
||||||
</Can>
|
</Can>
|
||||||
|
<MenuDivider />
|
||||||
|
<MenuItem
|
||||||
|
text={'Exclude'}
|
||||||
|
onClick={safeCallback(onExclude, original)}
|
||||||
|
// icon={<Icon icon="trash-16" iconSize={16} />}
|
||||||
|
/>
|
||||||
</Menu>
|
</Menu>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,47 +1,25 @@
|
|||||||
|
|
||||||
|
|
||||||
.transaction {
|
.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{
|
.matchBar{
|
||||||
padding: 16px 18px;
|
padding: 12px 18px;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
border-bottom: 1px solid #E1E2E9;
|
border-bottom: 1px solid #E1E2E9;
|
||||||
border-top: 1px solid #E1E2E9;
|
border-top: 1px solid #E1E2E9;
|
||||||
}
|
}
|
||||||
|
|
||||||
.matchBarTitle {
|
.matchBarTitle {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
background-color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
.footerActions {
|
.footerActions {
|
||||||
padding: 14px 16px;
|
padding: 14px 16px;
|
||||||
border-top: 1px solid #E1E2E9;
|
border-top: 1px solid #E1E2E9;
|
||||||
@@ -51,11 +29,3 @@
|
|||||||
padding: 8px 16px;
|
padding: 8px 16px;
|
||||||
border: 1px solid #E1E2E9;
|
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,145 +1,10 @@
|
|||||||
import { Box, Group, Icon, Stack } from '@/components';
|
import { Aside } from '@/components/Aside/Aside';
|
||||||
import {
|
import { CategorizeTransactionTabs } from './CategorizeTransactionTabs';
|
||||||
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() {
|
export function CategorizeTransactionAside() {
|
||||||
return (
|
return (
|
||||||
<Aside title={'Categorize Bank Transaction'}>
|
<Aside title={'Categorize Bank Transaction'}>
|
||||||
<Tabs large className={styles.tabs}>
|
<CategorizeTransactionTabs />
|
||||||
<Tab
|
|
||||||
id="categorize"
|
|
||||||
title="Categorize Transaction"
|
|
||||||
panel={<CategorizeBankTransactionContent />}
|
|
||||||
/>
|
|
||||||
<Tab
|
|
||||||
id="matching"
|
|
||||||
title="Matching Transaction"
|
|
||||||
panel={<MatchingBankTransactionContent />}
|
|
||||||
/>
|
|
||||||
</Tabs>
|
|
||||||
</Aside>
|
</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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
|
||||||
|
.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;
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { Tab, Tabs } from '@blueprintjs/core';
|
||||||
|
import {
|
||||||
|
CategorizeBankTransactionContent,
|
||||||
|
MatchingBankTransaction,
|
||||||
|
} from './MatchingTransaction';
|
||||||
|
import styles from './CategorizeTransactionTabs.module.scss';
|
||||||
|
|
||||||
|
export function CategorizeTransactionTabs() {
|
||||||
|
return (
|
||||||
|
<Tabs large className={styles.tabs}>
|
||||||
|
<Tab
|
||||||
|
id="categorize"
|
||||||
|
title="Categorize Transaction"
|
||||||
|
panel={<CategorizeBankTransactionContent />}
|
||||||
|
/>
|
||||||
|
<Tab
|
||||||
|
id="matching"
|
||||||
|
title="Matching Transaction"
|
||||||
|
panel={<MatchingBankTransaction />}
|
||||||
|
/>
|
||||||
|
</Tabs>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
|
||||||
|
.root{
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 5px;
|
||||||
|
border: 1px solid #D6DBE3;
|
||||||
|
padding: 12px 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&.active{
|
||||||
|
border-color: #88ABDB;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.checkbox:global(.bp4-control.bp4-checkbox){
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
.checkbox:global(.bp4-control.bp4-checkbox) :global .bp4-control-indicator{
|
||||||
|
border-color: #CBCBCB;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
// @ts-nocheck
|
||||||
|
import clsx from 'classnames';
|
||||||
|
import { Group, Stack } from '@/components';
|
||||||
|
import { Checkbox, Text } from '@blueprintjs/core';
|
||||||
|
import styles from './MatchTransaction.module.scss';
|
||||||
|
import { useUncontrolled } from '@/hooks/useUncontrolled';
|
||||||
|
|
||||||
|
export interface MatchTransactionProps {
|
||||||
|
active?: boolean;
|
||||||
|
initialActive?: boolean;
|
||||||
|
onChange?: (state: boolean) => void;
|
||||||
|
label: string;
|
||||||
|
date: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function MatchTransaction({
|
||||||
|
active,
|
||||||
|
initialActive,
|
||||||
|
onChange,
|
||||||
|
label,
|
||||||
|
date,
|
||||||
|
}: MatchTransactionProps) {
|
||||||
|
const [_active, handleChange] = useUncontrolled<boolean>({
|
||||||
|
value: active,
|
||||||
|
initialValue: initialActive,
|
||||||
|
finalValue: false,
|
||||||
|
onChange,
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleClick = () => {
|
||||||
|
handleChange(!_active);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCheckboxChange = (event) => {
|
||||||
|
handleChange(!event.target.checked);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Group
|
||||||
|
className={clsx(styles.root, {
|
||||||
|
[styles.active]: _active,
|
||||||
|
})}
|
||||||
|
position="apart"
|
||||||
|
onClick={handleClick}
|
||||||
|
>
|
||||||
|
<Stack spacing={3}>
|
||||||
|
<span>{label}</span>
|
||||||
|
<Text style={{ fontSize: 12, color: '#5C7080' }}>Date: {date}</Text>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
<Checkbox
|
||||||
|
checked={_active as boolean}
|
||||||
|
className={styles.checkbox}
|
||||||
|
onChange={handleCheckboxChange}
|
||||||
|
/>
|
||||||
|
</Group>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,204 @@
|
|||||||
|
// @ts-nocheck
|
||||||
|
import { isEmpty } from 'lodash';
|
||||||
|
import { AnchorButton, Button, Intent, Tag, Text } from '@blueprintjs/core';
|
||||||
|
import { AppToaster, Box, Group, Stack } from '@/components';
|
||||||
|
import {
|
||||||
|
MatchingTransactionBoot,
|
||||||
|
useMatchingTransactionBoot,
|
||||||
|
} from './MatchingTransactionBoot';
|
||||||
|
import { MatchTransaction, MatchTransactionProps } from './MatchTransaction';
|
||||||
|
import styles from './CategorizeTransactionAside.module.scss';
|
||||||
|
import { FastField, FastFieldProps, Form, Formik } from 'formik';
|
||||||
|
import { useMatchTransaction } from '@/hooks/query/bank-rules';
|
||||||
|
import { MatchingTransactionFormValues } from './types';
|
||||||
|
import { transformToReq } from './utils';
|
||||||
|
|
||||||
|
const initialValues = {
|
||||||
|
matched: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
export function MatchingBankTransaction() {
|
||||||
|
const uncategorizedTransactionId = 1;
|
||||||
|
const { mutateAsync: matchTransaction } = useMatchTransaction();
|
||||||
|
|
||||||
|
// Handles the form submitting.
|
||||||
|
const handleSubmit = (values: MatchingTransactionFormValues) => {
|
||||||
|
const _values = transformToReq(values);
|
||||||
|
|
||||||
|
if (_values.matchedTransactions?.length === 0) {
|
||||||
|
AppToaster.show({
|
||||||
|
message: 'You should select at least one transaction for matching.',
|
||||||
|
intent: Intent.DANGER,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
matchTransaction([uncategorizedTransactionId, _values])
|
||||||
|
.then(() => {
|
||||||
|
AppToaster.show({
|
||||||
|
intent: Intent.SUCCESS,
|
||||||
|
message: 'The bank transaction has been matched successfully.',
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
AppToaster.show({
|
||||||
|
intent: Intent.DANGER,
|
||||||
|
message: 'Something went wrong.',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MatchingTransactionBoot>
|
||||||
|
<Formik initialValues={initialValues} onSubmit={handleSubmit}>
|
||||||
|
<Form>
|
||||||
|
<MatchingBankTransactionContent />
|
||||||
|
</Form>
|
||||||
|
</Formik>
|
||||||
|
</MatchingTransactionBoot>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function MatchingBankTransactionContent() {
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
<PerfectMatchingTransactions />
|
||||||
|
<GoodMatchingTransactions />
|
||||||
|
<MatchTransactionFooter />
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders the perfect match transactions.
|
||||||
|
* @returns {React.ReactNode}
|
||||||
|
*/
|
||||||
|
function PerfectMatchingTransactions() {
|
||||||
|
const { matchingTransactions } = useMatchingTransactionBoot();
|
||||||
|
|
||||||
|
// Can't continue if the perfect matches is empty.
|
||||||
|
if (isEmpty(matchingTransactions)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<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 }}>
|
||||||
|
{matchingTransactions.map((match, index) => (
|
||||||
|
<MatchTransactionField
|
||||||
|
key={index}
|
||||||
|
label={`${match.transsactionTypeFormatted} for ${match.amountFormatted}`}
|
||||||
|
date={match.dateFormatted}
|
||||||
|
transactionId={match.transactionId}
|
||||||
|
transactionType={match.transactionType}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders the possible match transactions.
|
||||||
|
* @returns {React.ReactNode}
|
||||||
|
*/
|
||||||
|
function GoodMatchingTransactions() {
|
||||||
|
const { matchingTransactions } = useMatchingTransactionBoot();
|
||||||
|
|
||||||
|
// Can't continue if the possible matches is emoty.
|
||||||
|
if (isEmpty(matchingTransactions)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Box className={styles.matchBar}>
|
||||||
|
<Stack spacing={2}>
|
||||||
|
<h2 className={styles.matchBarTitle}>Possible Matches</h2>
|
||||||
|
<Text style={{ fontSize: 12, color: '#5C7080' }}>
|
||||||
|
Transactions up to 20 Aug 2019
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Stack spacing={9} style={{ padding: 15 }}>
|
||||||
|
{matchingTransactions.map((match, index) => (
|
||||||
|
<MatchTransaction
|
||||||
|
key={index}
|
||||||
|
label={`${match.transsactionTypeFormatted} for ${match.amountFormatted}`}
|
||||||
|
date={match.dateFormatted}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
interface MatchTransactionFieldProps
|
||||||
|
extends Omit<MatchTransactionProps, 'onChange' | 'active' | 'initialActive'> {
|
||||||
|
transactionId: number;
|
||||||
|
transactionType: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function MatchTransactionField({
|
||||||
|
transactionId,
|
||||||
|
transactionType,
|
||||||
|
...props
|
||||||
|
}: MatchTransactionFieldProps) {
|
||||||
|
const name = `matched.${transactionType}-${transactionId}`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FastField name={name}>
|
||||||
|
{({ form, field: { value } }: FastFieldProps) => (
|
||||||
|
<MatchTransaction
|
||||||
|
{...props}
|
||||||
|
active={!!value}
|
||||||
|
onChange={(state) => {
|
||||||
|
form.setFieldValue(name, state);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</FastField>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CategorizeBankTransactionContent() {
|
||||||
|
return <h1>Categorizing</h1>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders the match transactions footer.
|
||||||
|
* @returns {React.ReactNode}
|
||||||
|
*/
|
||||||
|
function MatchTransactionFooter() {
|
||||||
|
return (
|
||||||
|
<Box className={styles.footer}>
|
||||||
|
<Box className={styles.footerTotal}>
|
||||||
|
<Group position={'apart'}>
|
||||||
|
<AnchorButton small minimal intent={Intent.PRIMARY}>
|
||||||
|
Add Reconcile Transaction +
|
||||||
|
</AnchorButton>
|
||||||
|
<Text style={{ fontSize: 13 }}>Pending $10,000</Text>
|
||||||
|
</Group>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box className={styles.footerActions}>
|
||||||
|
<Group spacing={10}>
|
||||||
|
<Button
|
||||||
|
intent={Intent.PRIMARY}
|
||||||
|
style={{ minWidth: 85 }}
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
|
Match
|
||||||
|
</Button>
|
||||||
|
<Button>Cancel</Button>
|
||||||
|
</Group>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import { useMatchingTransactions } from '@/hooks/query/bank-rules';
|
||||||
|
import React, { createContext } from 'react';
|
||||||
|
|
||||||
|
interface MatchingTransactionBootValues {
|
||||||
|
isMatchingTransactionsLoading: boolean;
|
||||||
|
matchingTransactions: Array<any>;
|
||||||
|
perfectMatchesCount: number;
|
||||||
|
perfectMatches: Array<any>;
|
||||||
|
matches: Array<any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const RuleFormBootContext = createContext<MatchingTransactionBootValues>(
|
||||||
|
{} as MatchingTransactionBootValues,
|
||||||
|
);
|
||||||
|
|
||||||
|
interface RuleFormBootProps {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
function MatchingTransactionBoot({ ...props }: RuleFormBootProps) {
|
||||||
|
const {
|
||||||
|
data: matchingTransactions,
|
||||||
|
isLoading: isMatchingTransactionsLoading,
|
||||||
|
} = useMatchingTransactions();
|
||||||
|
|
||||||
|
const provider = {
|
||||||
|
isMatchingTransactionsLoading,
|
||||||
|
matchingTransactions,
|
||||||
|
perfectMatchesCount: 2,
|
||||||
|
perfectMatches: [],
|
||||||
|
matches: [],
|
||||||
|
} as MatchingTransactionBootValues;
|
||||||
|
|
||||||
|
return <RuleFormBootContext.Provider value={provider} {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
const useMatchingTransactionBoot = () =>
|
||||||
|
React.useContext<MatchingTransactionBootValues>(RuleFormBootContext);
|
||||||
|
|
||||||
|
export { MatchingTransactionBoot, useMatchingTransactionBoot };
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export interface MatchingTransactionFormValues {
|
||||||
|
matched: Record<string, boolean>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { MatchingTransactionFormValues } from './types';
|
||||||
|
|
||||||
|
export const transformToReq = (values: MatchingTransactionFormValues) => {
|
||||||
|
const matchedTransactions = Object.entries(values.matched)
|
||||||
|
.filter(([key, value]) => value)
|
||||||
|
.map(([key]) => {
|
||||||
|
const [reference_type, reference_id] = key.split('-');
|
||||||
|
|
||||||
|
return { reference_type, reference_id: parseInt(reference_id, 10) };
|
||||||
|
});
|
||||||
|
|
||||||
|
return { matchedTransactions };
|
||||||
|
};
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
// @ts-nocheck
|
// @ts-nocheck
|
||||||
import { useMutation, useQuery, useQueryClient } from 'react-query';
|
import { useMutation, useQuery, useQueryClient } from 'react-query';
|
||||||
import useApiRequest from '../useRequest';
|
import useApiRequest from '../useRequest';
|
||||||
|
import { transformToCamelCase } from '@/utils';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
@@ -75,3 +76,72 @@ export function useBankRule(bankRuleId: number, props) {
|
|||||||
props,
|
props,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export function useMatchingTransactions(props?: any) {
|
||||||
|
const apiRequest = useApiRequest();
|
||||||
|
|
||||||
|
return useQuery(
|
||||||
|
['MATCHING_TRANSACTION'],
|
||||||
|
() =>
|
||||||
|
apiRequest
|
||||||
|
.get(`/banking/matches`)
|
||||||
|
.then((res) => transformToCamelCase(res.data.data)),
|
||||||
|
props,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useExcludeUncategorizedTransaction(props) {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const apiRequest = useApiRequest();
|
||||||
|
|
||||||
|
return useMutation(
|
||||||
|
(uncategorizedTransactionId: number) =>
|
||||||
|
apiRequest.put(
|
||||||
|
`/cashflow/transactions/${uncategorizedTransactionId}/exclude`,
|
||||||
|
),
|
||||||
|
{
|
||||||
|
onSuccess: (res, id) => {
|
||||||
|
// Invalidate queries.
|
||||||
|
},
|
||||||
|
...props,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useUnexcludeUncategorizedTransaction(props) {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const apiRequest = useApiRequest();
|
||||||
|
|
||||||
|
return useMutation(
|
||||||
|
(uncategorizedTransactionId: number) =>
|
||||||
|
apiRequest.post(
|
||||||
|
`/cashflow/transactions/${uncategorizedTransactionId}/unexclude`,
|
||||||
|
),
|
||||||
|
{
|
||||||
|
onSuccess: (res, id) => {
|
||||||
|
// Invalidate queries.
|
||||||
|
},
|
||||||
|
...props,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useMatchTransaction(props?: any) {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const apiRequest = useApiRequest();
|
||||||
|
|
||||||
|
return useMutation(
|
||||||
|
([uncategorizedTransactionId, values]) =>
|
||||||
|
apiRequest.post(`/banking/matches/${uncategorizedTransactionId}`, values),
|
||||||
|
{
|
||||||
|
onSuccess: (res, id) => {
|
||||||
|
// Invalidate queries.
|
||||||
|
},
|
||||||
|
...props,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user