feat: get bank account meta summary

This commit is contained in:
Ahmed Bouhuolia
2024-07-02 19:21:26 +02:00
parent 8a09de9771
commit 91730d204e
22 changed files with 476 additions and 69 deletions

View File

@@ -4,6 +4,7 @@ import { useParams } from 'react-router-dom';
import { DashboardInsider } from '@/components';
import { useCashflowAccounts, useAccount } from '@/hooks/query';
import { useAppQueryString } from '@/hooks';
import { useGetBankAccountSummaryMeta } from '@/hooks/query/bank-rules';
const AccountTransactionsContext = React.createContext();
@@ -20,31 +21,38 @@ function AccountTransactionsProvider({ query, ...props }) {
const setFilterTab = (value: string) => {
setLocationQuery({ filter: value });
};
// Fetch cashflow accounts.
// Retrieves cashflow accounts.
const {
data: cashflowAccounts,
isFetching: isCashFlowAccountsFetching,
isLoading: isCashFlowAccountsLoading,
} = useCashflowAccounts(query, { keepPreviousData: true });
// Retrieve specific account details.
// Retrieves specific account details.
const {
data: currentAccount,
isFetching: isCurrentAccountFetching,
isLoading: isCurrentAccountLoading,
} = useAccount(accountId, { keepPreviousData: true });
// Retrieves the bank account meta summary.
const {
data: bankAccountMetaSummary,
isLoading: isBankAccountMetaSummaryLoading,
} = useGetBankAccountSummaryMeta(accountId);
// Provider payload.
const provider = {
accountId,
cashflowAccounts,
currentAccount,
bankAccountMetaSummary,
isCashFlowAccountsFetching,
isCashFlowAccountsLoading,
isCurrentAccountFetching,
isCurrentAccountLoading,
isBankAccountMetaSummaryLoading,
filterTab,
setFilterTab,

View File

@@ -4,6 +4,7 @@ import { Tag } from '@blueprintjs/core';
import { useAppQueryString } from '@/hooks';
import { useUncontrolled } from '@/hooks/useUncontrolled';
import { Group } from '@/components';
import { useAccountTransactionsContext } from './AccountTransactionsProvider';
const Root = styled.div`
display: flex;
@@ -26,8 +27,13 @@ const FilterTag = styled(Tag)`
`;
export function AccountTransactionsUncategorizeFilter() {
const { bankAccountMetaSummary } = useAccountTransactionsContext();
const [locationQuery, setLocationQuery] = useAppQueryString();
const totalUncategorized =
bankAccountMetaSummary?.totalUncategorizedTransactions;
const totalRecognized = bankAccountMetaSummary?.totalRecognizedTransactions;
const handleTabsChange = (value) => {
setLocationQuery({ uncategorizedFilter: value });
};
@@ -36,8 +42,22 @@ export function AccountTransactionsUncategorizeFilter() {
<Group position={'apart'}>
<SegmentedTabs
options={[
{ value: 'all', label: 'All' },
{ value: 'recognized', label: 'Recognized' },
{
value: 'all',
label: (
<>
All <strong>({totalUncategorized})</strong>
</>
),
},
{
value: 'recognized',
label: (
<>
Recognized <strong>({totalRecognized})</strong>
</>
),
},
]}
value={locationQuery?.uncategorizedFilter || 'all'}
onValueChange={handleTabsChange}
@@ -66,8 +86,9 @@ function SegmentedTabs({ options, initialValue, value, onValueChange }) {
});
return (
<Root>
{options.map((option) => (
{options.map((option, index) => (
<FilterTag
key={index}
round
interactive
onClick={() => handleChange(option.value)}

View File

@@ -102,7 +102,7 @@ function AccountTransactionsDataTable({
vListOverscanRowCount={0}
initialColumnsWidths={initialColumnsWidths}
onColumnResizing={handleColumnResizing}
noResults={<T id={'cash_flow.account_transactions.no_results'} />}
noResults={'There is no uncategorized transactions in the current account.'}
className="table-constrant"
payload={{
onExclude: handleExcludeTransaction,

View File

@@ -81,7 +81,7 @@ function ExcludedTransactionsTableRoot() {
vListOverscanRowCount={0}
initialColumnsWidths={initialColumnsWidths}
onColumnResizing={handleColumnResizing}
// noResults={<T id={'cash_flow.account_transactions.no_results'} />}
noResults={'There is no excluded bank transactions.'}
className="table-constrant"
payload={{
onRestore: handleRestoreClick,

View File

@@ -0,0 +1,20 @@
.emptyState{
text-align: center;
font-size: 15px;
color: #738091;
:global ul{
list-style: inside;
li{
margin-bottom: 12px;
&::marker{
color: #C5CBD3;
font-size: 12px;
}
}
}
}

View File

@@ -10,6 +10,7 @@ import {
TableVirtualizedListRows,
FormattedMessage as T,
AppToaster,
Stack,
} from '@/components';
import { TABLES } from '@/constants/tables';
@@ -24,11 +25,12 @@ import { useRecognizedTransactionsBoot } from './RecognizedTransactionsTableBoot
import { ActionsMenu } from './_components';
import { compose } from '@/utils';
import { useExcludeUncategorizedTransaction } from '@/hooks/query/bank-rules';
import { Intent } from '@blueprintjs/core';
import { Intent, Text } from '@blueprintjs/core';
import {
WithBankingActionsProps,
withBankingActions,
} from '../../withBankingActions';
import styles from './RecognizedTransactionsTable.module.scss';
interface RecognizedTransactionsTableProps extends WithBankingActionsProps {}
@@ -114,7 +116,7 @@ function RecognizedTransactionsTableRoot({
vListOverscanRowCount={0}
initialColumnsWidths={initialColumnsWidths}
onColumnResizing={handleColumnResizing}
noResults={<T id={'cash_flow.account_transactions.no_results'} />}
noResults={<RecognizedTransactionsTableNoResults />}
className="table-constrant"
payload={{
onExclude: handleExcludeClick,
@@ -168,3 +170,26 @@ const CashflowTransactionsTable = styled(DashboardConstrantTable)`
}
}
`;
function RecognizedTransactionsTableNoResults() {
return (
<Stack spacing={12} className={styles.emptyState}>
<Text>
There are no Recognized transactions due to one of the following
reasons:
</Text>
<ul>
<li>
Transaction Rules have not yet been created. Transactions are
recognized based on the rule criteria.
</li>
<li>
The transactions in your bank do not satisfy the criteria in any of
your transaction rule(s).
</li>
</ul>
</Stack>
);
}

View File

@@ -1,8 +1,24 @@
// @ts-nocheck
import React from 'react';
import intl from 'react-intl-universal';
import { Intent, Menu, MenuItem, MenuDivider, Tag } from '@blueprintjs/core';
import { Can, FormatDateCell, Icon, MaterialProgressBar } from '@/components';
import {
Intent,
Menu,
MenuItem,
MenuDivider,
Tag,
Popover,
PopoverInteractionKind,
Position,
Tooltip,
} from '@blueprintjs/core';
import {
Box,
Can,
FormatDateCell,
Icon,
MaterialProgressBar,
} from '@/components';
import { useAccountTransactionsContext } from './AccountTransactionsProvider';
import { safeCallback } from '@/utils';
@@ -128,6 +144,34 @@ export function AccountTransactionsProgressBar() {
) : null;
}
function statusAccessor(transaction) {
return transaction.is_recognized ? (
<Tooltip
compact
interactionKind={PopoverInteractionKind.HOVER}
position={Position.RIGHT}
content={
<Box>
<span>{transaction.assigned_category_formatted}</span>
<Icon
icon={'arrowRight'}
color={'#8F99A8'}
iconSize={12}
style={{ marginLeft: 8, marginRight: 8 }}
/>
<span>{transaction.assigned_account_name}</span>
</Box>
}
>
<Box>
<Tag intent={Intent.SUCCESS} interactive>
Recognized
</Tag>
</Box>
</Tooltip>
) : null;
}
/**
* Retrieve account uncategorized transctions table columns.
*/
@@ -170,16 +214,7 @@ export function useAccountUncategorizedTransactionsColumns() {
{
id: 'status',
Header: 'Status',
accessor: () =>
false ? (
<Tag intent={Intent.SUCCESS} interactive>
1 Matches
</Tag>
) : (
<Tag intent={Intent.SUCCESS} interactive>
Recognized
</Tag>
),
accessor: statusAccessor,
},
{
id: 'deposit',

View File

@@ -2,6 +2,7 @@
.root {
flex: 1 1 auto;
overflow: auto;
padding-bottom: 60px;
}
.transaction {
@@ -35,4 +36,5 @@
.footerTotal {
padding: 8px 16px;
border-top: 1px solid #d8d9d9;
line-height: 24px;
}

View File

@@ -1,11 +1,23 @@
// @ts-nocheck
import { Tab, Tabs } from '@blueprintjs/core';
import { MatchingBankTransaction } from './MatchingTransaction';
import styles from './CategorizeTransactionTabs.module.scss';
import { CategorizeTransactionContent } from '../CategorizeTransaction/drawers/CategorizeTransactionDrawer/CategorizeTransactionContent';
import { useCategorizeTransactionTabsBoot } from './CategorizeTransactionTabsBoot';
import styles from './CategorizeTransactionTabs.module.scss';
export function CategorizeTransactionTabs() {
const { uncategorizedTransaction } = useCategorizeTransactionTabsBoot();
const defaultSelectedTabId = uncategorizedTransaction?.is_recognized
? 'categorize'
: 'matching';
return (
<Tabs large renderActiveTabPanelOnly className={styles.tabs}>
<Tabs
large
renderActiveTabPanelOnly
defaultSelectedTabId={defaultSelectedTabId}
className={styles.tabs}
>
<Tab
id="categorize"
title="Categorize Transaction"

View File

@@ -2,22 +2,26 @@
import { isEmpty } from 'lodash';
import * as R from 'ramda';
import { AnchorButton, Button, Intent, Tag, Text } from '@blueprintjs/core';
import { FastField, FastFieldProps, Formik } from 'formik';
import { AppToaster, Box, FormatNumber, 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 { useMatchUncategorizedTransaction } from '@/hooks/query/bank-rules';
import { MatchingTransactionFormValues } from './types';
import { transformToReq, useGetPendingAmountMatched } from './utils';
import {
transformToReq,
useGetPendingAmountMatched,
useIsShowReconcileTransactionLink,
} from './utils';
import { useCategorizeTransactionTabsBoot } from './CategorizeTransactionTabsBoot';
import {
WithBankingActionsProps,
withBankingActions,
} from '../withBankingActions';
import styles from './CategorizeTransactionAside.module.scss';
const initialValues = {
matched: {},
@@ -189,20 +193,26 @@ interface MatchTransctionFooterProps extends WithBankingActionsProps {}
*/
const MatchTransactionFooter = R.compose(withBankingActions)(
({ closeMatchingTransactionAside }: MatchTransctionFooterProps) => {
const totalPending = useGetPendingAmountMatched();
const showReconcileLink = useIsShowReconcileTransactionLink();
const handleCancelBtnClick = () => {
closeMatchingTransactionAside();
};
const totalPending = useGetPendingAmountMatched();
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 }} tagName="span">
{showReconcileLink && (
<AnchorButton small minimal intent={Intent.PRIMARY}>
Add Reconcile Transaction +
</AnchorButton>
)}
<Text
style={{ fontSize: 14, marginLeft: 'auto', color: '#5F6B7C' }}
tagName="span"
>
Pending <FormatNumber value={totalPending} currency={'USD'} />
</Text>
</Group>

View File

@@ -1,6 +1,8 @@
import { useFormikContext } from 'formik';
import { MatchingTransactionFormValues } from './types';
import { useMatchingTransactionBoot } from './MatchingTransactionBoot';
import { useCategorizeTransactionTabsBoot } from './CategorizeTransactionTabsBoot';
import { useMemo } from 'react';
export const transformToReq = (values: MatchingTransactionFormValues) => {
const matchedTransactions = Object.entries(values.matched)
@@ -17,18 +19,38 @@ export const transformToReq = (values: MatchingTransactionFormValues) => {
export const useGetPendingAmountMatched = () => {
const { values } = useFormikContext<MatchingTransactionFormValues>();
const { perfectMatches, possibleMatches } = useMatchingTransactionBoot();
const { uncategorizedTransaction } = useCategorizeTransactionTabsBoot();
const matchedItems = [...perfectMatches, ...possibleMatches].filter(
(match) => {
const key = `${match.transactionType}-${match.transactionId}`;
return values.matched[key];
},
);
const totalMatchedAmount = matchedItems.reduce(
(total, item) => total + parseFloat(item.amount),
0,
);
const pendingAmount = 0 - totalMatchedAmount;
return useMemo(() => {
const matchedItems = [...perfectMatches, ...possibleMatches].filter(
(match) => {
const key = `${match.transactionType}-${match.transactionId}`;
return values.matched[key];
},
);
const totalMatchedAmount = matchedItems.reduce(
(total, item) => total + parseFloat(item.amount),
0,
);
const amount = uncategorizedTransaction.amount;
const pendingAmount = amount - totalMatchedAmount;
return pendingAmount;
return pendingAmount;
}, [uncategorizedTransaction, perfectMatches, possibleMatches, values]);
};
export const useAtleastOneMatchedSelected = () => {
const { values } = useFormikContext<MatchingTransactionFormValues>();
return useMemo(() => {
const matchedCount = Object.values(values.matched).filter(Boolean).length;
return matchedCount > 0;
}, [values]);
};
export const useIsShowReconcileTransactionLink = () => {
const pendingAmount = useGetPendingAmountMatched();
const atleastOneSelected = useAtleastOneMatchedSelected();
return atleastOneSelected && pendingAmount !== 0;
};