diff --git a/packages/webapp/src/components/Aside/Aside.module.scss b/packages/webapp/src/components/Aside/Aside.module.scss
new file mode 100644
index 000000000..a4a7c3a75
--- /dev/null
+++ b/packages/webapp/src/components/Aside/Aside.module.scss
@@ -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;
+}
diff --git a/packages/webapp/src/components/Aside/Aside.tsx b/packages/webapp/src/components/Aside/Aside.tsx
new file mode 100644
index 000000000..ade9d8c58
--- /dev/null
+++ b/packages/webapp/src/components/Aside/Aside.tsx
@@ -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 (
+
+
+ {title}
+
+ {hideCloseButton !== true && (
+ }
+ minimal={true}
+ onClick={handleClose}
+ />
+ )}
+
+ {children}
+
+ );
+}
diff --git a/packages/webapp/src/containers/Banking/Rules/RulesList/RulesTable.tsx b/packages/webapp/src/containers/Banking/Rules/RulesList/RulesTable.tsx
index 8a20eb06f..d541e88cf 100644
--- a/packages/webapp/src/containers/Banking/Rules/RulesList/RulesTable.tsx
+++ b/packages/webapp/src/containers/Banking/Rules/RulesList/RulesTable.tsx
@@ -33,7 +33,6 @@ function RulesTable({
}) {
// Invoices table columns.
const columns = useBankRulesTableColumns();
-
const { bankRules } = useRulesListBoot();
// Handle edit bank rule.
diff --git a/packages/webapp/src/containers/CashFlow/AccountTransactions/AccountTransactionsUncategorizedTable.tsx b/packages/webapp/src/containers/CashFlow/AccountTransactions/AccountTransactionsUncategorizedTable.tsx
index 781f9b9b1..8bd895198 100644
--- a/packages/webapp/src/containers/CashFlow/AccountTransactions/AccountTransactionsUncategorizedTable.tsx
+++ b/packages/webapp/src/containers/CashFlow/AccountTransactions/AccountTransactionsUncategorizedTable.tsx
@@ -9,6 +9,7 @@ import {
TableSkeletonHeader,
TableVirtualizedListRows,
FormattedMessage as T,
+ AppToaster,
} from '@/components';
import { TABLES } from '@/constants/tables';
@@ -24,6 +25,8 @@ import { useAccountUncategorizedTransactionsContext } from './AllTransactionsUnc
import { compose } from '@/utils';
import { DRAWERS } from '@/constants/drawers';
+import { useExcludeUncategorizedTransaction } from '@/hooks/query/bank-rules';
+import { Intent } from '@blueprintjs/core';
/**
* Account transactions data table.
@@ -42,6 +45,9 @@ function AccountTransactionsDataTable({
const { uncategorizedTransactions, isUncategorizedTransactionsLoading } =
useAccountUncategorizedTransactionsContext();
+ const { mutateAsync: excludeTransaction } =
+ useExcludeUncategorizedTransaction();
+
// Local storage memorizing columns widths.
const [initialColumnsWidths, , handleColumnResizing] =
useMemorizedColumnsWidths(TABLES.UNCATEGORIZED_CASHFLOW_TRANSACTION);
@@ -52,6 +58,22 @@ function AccountTransactionsDataTable({
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 (
}
className="table-constrant"
+ payload={{
+ onExclude: handleExcludeTransaction,
+ }}
/>
);
}
diff --git a/packages/webapp/src/containers/CashFlow/AccountTransactions/components.tsx b/packages/webapp/src/containers/CashFlow/AccountTransactions/components.tsx
index 6dc334aa6..1da35e9fd 100644
--- a/packages/webapp/src/containers/CashFlow/AccountTransactions/components.tsx
+++ b/packages/webapp/src/containers/CashFlow/AccountTransactions/components.tsx
@@ -15,7 +15,7 @@ import { AbilitySubject, CashflowAction } from '@/constants/abilityOption';
import { safeCallback } from '@/utils';
export function ActionsMenu({
- payload: { onDelete, onViewDetails },
+ payload: { onDelete, onViewDetails, onExclude },
row: { original },
}) {
return (
@@ -36,6 +36,12 @@ export function ActionsMenu({
/>
+
+ }
+ />
);
}
diff --git a/packages/webapp/src/containers/CashFlow/CategorizeTransactionAside/CategorizeTransactionAside.module.scss b/packages/webapp/src/containers/CashFlow/CategorizeTransactionAside/CategorizeTransactionAside.module.scss
index a52cd188c..257dab3b0 100644
--- a/packages/webapp/src/containers/CashFlow/CategorizeTransactionAside/CategorizeTransactionAside.module.scss
+++ b/packages/webapp/src/containers/CashFlow/CategorizeTransactionAside/CategorizeTransactionAside.module.scss
@@ -1,47 +1,25 @@
.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;
+ padding: 12px 18px;
background: #fff;
border-bottom: 1px solid #E1E2E9;
border-top: 1px solid #E1E2E9;
}
+
.matchBarTitle {
font-size: 14px;
font-weight: 500;
}
+.footer {
+ background-color: #fff;
+}
+
.footerActions {
padding: 14px 16px;
border-top: 1px solid #E1E2E9;
@@ -51,11 +29,3 @@
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;
-}
-
diff --git a/packages/webapp/src/containers/CashFlow/CategorizeTransactionAside/CategorizeTransactionAside.tsx b/packages/webapp/src/containers/CashFlow/CategorizeTransactionAside/CategorizeTransactionAside.tsx
index c4ef09a04..b96337152 100644
--- a/packages/webapp/src/containers/CashFlow/CategorizeTransactionAside/CategorizeTransactionAside.tsx
+++ b/packages/webapp/src/containers/CashFlow/CategorizeTransactionAside/CategorizeTransactionAside.tsx
@@ -1,145 +1,10 @@
-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 (
-
-
- {title}
- }
- minimal={true}
- onClick={handleClose}
- />
-
- {children}
-
- );
-}
+import { Aside } from '@/components/Aside/Aside';
+import { CategorizeTransactionTabs } from './CategorizeTransactionTabs';
export function CategorizeTransactionAside() {
return (
);
}
-
-export function MatchingBankTransactionContent() {
- return (
-
-
-
- Perfect Matchines
-
- 2
-
-
-
-
-
-
-
-
-
-
-
-
-
- Perfect Matches
-
- Transactions up to 20 Aug 2019
-
-
-
-
-
-
-
-
-
-
-
-
-
- );
-}
-
-export function CategorizeBankTransactionContent() {
- return
Categorizing
;
-}
-
-interface MatchTransactionProps {
- label: string;
- date: string;
-}
-
-function MatchTransaction({ label, date }: MatchTransactionProps) {
- return (
-
-
- Expense for $10,000
-
- Date: 02/02/2020{' '}
-
-
-
-
-
- );
-}
-
-function MatchTransactionFooter() {
- return (
-
-
-
-
- Add Reconcile Transaction +
-
- Pending $10,000
-
-
-
-
-
-
-
-
-
-
- );
-}
diff --git a/packages/webapp/src/containers/CashFlow/CategorizeTransactionAside/CategorizeTransactionTabs.module.scss b/packages/webapp/src/containers/CashFlow/CategorizeTransactionAside/CategorizeTransactionTabs.module.scss
new file mode 100644
index 000000000..b901d9160
--- /dev/null
+++ b/packages/webapp/src/containers/CashFlow/CategorizeTransactionAside/CategorizeTransactionTabs.module.scss
@@ -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;
+}
diff --git a/packages/webapp/src/containers/CashFlow/CategorizeTransactionAside/CategorizeTransactionTabs.tsx b/packages/webapp/src/containers/CashFlow/CategorizeTransactionAside/CategorizeTransactionTabs.tsx
new file mode 100644
index 000000000..8dd806111
--- /dev/null
+++ b/packages/webapp/src/containers/CashFlow/CategorizeTransactionAside/CategorizeTransactionTabs.tsx
@@ -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 (
+
+ }
+ />
+ }
+ />
+
+ );
+}
diff --git a/packages/webapp/src/containers/CashFlow/CategorizeTransactionAside/MatchTransaction.module.scss b/packages/webapp/src/containers/CashFlow/CategorizeTransactionAside/MatchTransaction.module.scss
new file mode 100644
index 000000000..f85e5d8fa
--- /dev/null
+++ b/packages/webapp/src/containers/CashFlow/CategorizeTransactionAside/MatchTransaction.module.scss
@@ -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;
+}
+
diff --git a/packages/webapp/src/containers/CashFlow/CategorizeTransactionAside/MatchTransaction.tsx b/packages/webapp/src/containers/CashFlow/CategorizeTransactionAside/MatchTransaction.tsx
new file mode 100644
index 000000000..4458a26a5
--- /dev/null
+++ b/packages/webapp/src/containers/CashFlow/CategorizeTransactionAside/MatchTransaction.tsx
@@ -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({
+ value: active,
+ initialValue: initialActive,
+ finalValue: false,
+ onChange,
+ });
+
+ const handleClick = () => {
+ handleChange(!_active);
+ };
+
+ const handleCheckboxChange = (event) => {
+ handleChange(!event.target.checked);
+ };
+
+ return (
+
+
+ {label}
+ Date: {date}
+
+
+
+
+ );
+}
diff --git a/packages/webapp/src/containers/CashFlow/CategorizeTransactionAside/MatchingTransaction.tsx b/packages/webapp/src/containers/CashFlow/CategorizeTransactionAside/MatchingTransaction.tsx
new file mode 100644
index 000000000..0f19bbcfb
--- /dev/null
+++ b/packages/webapp/src/containers/CashFlow/CategorizeTransactionAside/MatchingTransaction.tsx
@@ -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 (
+
+
+
+
+
+ );
+}
+
+function MatchingBankTransactionContent() {
+ return (
+
+
+
+
+
+ );
+}
+
+/**
+ * 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 (
+ <>
+
+
+ Perfect Matchines
+
+ 2
+
+
+
+
+
+ {matchingTransactions.map((match, index) => (
+
+ ))}
+
+ >
+ );
+}
+
+/**
+ * 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 (
+ <>
+
+
+ Possible Matches
+
+ Transactions up to 20 Aug 2019
+
+
+
+
+
+ {matchingTransactions.map((match, index) => (
+
+ ))}
+
+ >
+ );
+}
+interface MatchTransactionFieldProps
+ extends Omit {
+ transactionId: number;
+ transactionType: string;
+}
+
+function MatchTransactionField({
+ transactionId,
+ transactionType,
+ ...props
+}: MatchTransactionFieldProps) {
+ const name = `matched.${transactionType}-${transactionId}`;
+
+ return (
+
+ {({ form, field: { value } }: FastFieldProps) => (
+ {
+ form.setFieldValue(name, state);
+ }}
+ />
+ )}
+
+ );
+}
+
+export function CategorizeBankTransactionContent() {
+ return Categorizing
;
+}
+
+/**
+ * Renders the match transactions footer.
+ * @returns {React.ReactNode}
+ */
+function MatchTransactionFooter() {
+ return (
+
+
+
+
+ Add Reconcile Transaction +
+
+ Pending $10,000
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/packages/webapp/src/containers/CashFlow/CategorizeTransactionAside/MatchingTransactionBoot.tsx b/packages/webapp/src/containers/CashFlow/CategorizeTransactionAside/MatchingTransactionBoot.tsx
new file mode 100644
index 000000000..2c6b67ac6
--- /dev/null
+++ b/packages/webapp/src/containers/CashFlow/CategorizeTransactionAside/MatchingTransactionBoot.tsx
@@ -0,0 +1,40 @@
+import { useMatchingTransactions } from '@/hooks/query/bank-rules';
+import React, { createContext } from 'react';
+
+interface MatchingTransactionBootValues {
+ isMatchingTransactionsLoading: boolean;
+ matchingTransactions: Array;
+ perfectMatchesCount: number;
+ perfectMatches: Array;
+ matches: Array;
+}
+
+const RuleFormBootContext = createContext(
+ {} 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 ;
+}
+
+const useMatchingTransactionBoot = () =>
+ React.useContext(RuleFormBootContext);
+
+export { MatchingTransactionBoot, useMatchingTransactionBoot };
diff --git a/packages/webapp/src/containers/CashFlow/CategorizeTransactionAside/types.ts b/packages/webapp/src/containers/CashFlow/CategorizeTransactionAside/types.ts
new file mode 100644
index 000000000..3a88e7edc
--- /dev/null
+++ b/packages/webapp/src/containers/CashFlow/CategorizeTransactionAside/types.ts
@@ -0,0 +1,3 @@
+export interface MatchingTransactionFormValues {
+ matched: Record;
+}
diff --git a/packages/webapp/src/containers/CashFlow/CategorizeTransactionAside/utils.ts b/packages/webapp/src/containers/CashFlow/CategorizeTransactionAside/utils.ts
new file mode 100644
index 000000000..e3d30bbe0
--- /dev/null
+++ b/packages/webapp/src/containers/CashFlow/CategorizeTransactionAside/utils.ts
@@ -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 };
+};
diff --git a/packages/webapp/src/hooks/query/bank-rules.ts b/packages/webapp/src/hooks/query/bank-rules.ts
index 02e19a8b1..15f47e87e 100644
--- a/packages/webapp/src/hooks/query/bank-rules.ts
+++ b/packages/webapp/src/hooks/query/bank-rules.ts
@@ -1,6 +1,7 @@
// @ts-nocheck
import { useMutation, useQuery, useQueryClient } from 'react-query';
import useApiRequest from '../useRequest';
+import { transformToCamelCase } from '@/utils';
/**
*
@@ -75,3 +76,72 @@ export function useBankRule(bankRuleId: number, 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,
+ },
+ );
+}