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 && ( + - - - - - ); -} 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, + }, + ); +}