feat: bulk categorize and match bank transactions

This commit is contained in:
Ahmed Bouhuolia
2024-07-18 19:41:23 +02:00
parent 449390143d
commit 6fb02f9869
11 changed files with 238 additions and 134 deletions

View File

@@ -12,6 +12,8 @@ import {
PopoverInteractionKind,
Position,
Intent,
Switch,
Tooltip,
} from '@blueprintjs/core';
import { useHistory } from 'react-router-dom';
import {
@@ -39,9 +41,9 @@ import { withBanking } from '../withBanking';
import { isEmpty } from 'lodash';
import {
useExcludeUncategorizedTransactions,
useUnexcludeUncategorizedTransaction,
useUnexcludeUncategorizedTransactions,
} from '@/hooks/query/bank-rules';
import { withBankingActions } from '../withBankingActions';
function AccountTransactionsActionsBar({
// #withDialogActions
@@ -56,6 +58,9 @@ function AccountTransactionsActionsBar({
// #withBanking
uncategorizedTransationsIdsSelected,
excludedTransactionsIdsSelected,
// #withBankingActions
enableMultipleCategorization,
}) {
const history = useHistory();
const { accountId } = useAccountTransactionsContext();
@@ -148,6 +153,10 @@ function AccountTransactionsActionsBar({
});
};
const handleMultipleCategorizingSwitch = (event) => {
enableMultipleCategorization(event.currentTarget.checked);
};
return (
<DashboardActionsBar>
<NavbarGroup>
@@ -201,7 +210,6 @@ function AccountTransactionsActionsBar({
disable={isExcludingLoading}
/>
)}
{!isEmpty(excludedTransactionsIdsSelected) && (
<Button
icon={<Icon icon="disable" iconSize={16} />}
@@ -215,6 +223,20 @@ function AccountTransactionsActionsBar({
</NavbarGroup>
<NavbarGroup align={Alignment.RIGHT}>
<Tooltip
content={
'Enables to categorize or matching multiple bank transactions into one transaction.'
}
position={Position.BOTTOM}
minimal
>
<Switch
label={'Multi Select'}
inline
onChange={handleMultipleCategorizingSwitch}
/>
</Tooltip>
<NavbarDivider />
<Popover
minimal={true}
interactionKind={PopoverInteractionKind.CLICK}
@@ -256,4 +278,5 @@ export default compose(
excludedTransactionsIdsSelected,
}),
),
withBankingActions,
)(AccountTransactionsActionsBar);

View File

@@ -0,0 +1,15 @@
.table :global .td.categorize_include,
.table :global .th.categorize_include {
display: none;
}
.table.showCategorizeColumn :global .td.categorize_include,
.table.showCategorizeColumn :global .th.categorize_include {
display: flex;
}
.categorizeCheckbox:global(.bp4-checkbox) :global .bp4-control-indicator {
border-radius: 20px;
}

View File

@@ -1,5 +1,6 @@
// @ts-nocheck
import React from 'react';
import clsx from 'classnames';
import styled from 'styled-components';
import { Intent } from '@blueprintjs/core';
import {
@@ -12,17 +13,19 @@ import {
AppToaster,
} from '@/components';
import { TABLES } from '@/constants/tables';
import { ActionsMenu } from './UncategorizedTransactions/components';
import { ActionsMenu } from './components';
import withSettings from '@/containers/Settings/withSettings';
import { withBankingActions } from '../withBankingActions';
import { withBankingActions } from '../../withBankingActions';
import { useMemorizedColumnsWidths } from '@/hooks';
import { useAccountUncategorizedTransactionsColumns } from './components';
import { useAccountUncategorizedTransactionsContext } from './AllTransactionsUncategorizedBoot';
import { useAccountUncategorizedTransactionsContext } from '../AllTransactionsUncategorizedBoot';
import { useExcludeUncategorizedTransaction } from '@/hooks/query/bank-rules';
import { useAccountUncategorizedTransactionsColumns } from './hooks';
import { compose } from '@/utils';
import { withBanking } from '../../withBanking';
import styles from './AccountTransactionsUncategorizedTable.module.scss';
/**
* Account transactions data table.
@@ -31,6 +34,9 @@ function AccountTransactionsDataTable({
// #withSettings
cashflowTansactionsTableSize,
// #withBanking
openMatchingTransactionAside,
// #withBankingActions
setUncategorizedTransactionIdForMatching,
setUncategorizedTransactionsSelected,
@@ -66,7 +72,7 @@ function AccountTransactionsDataTable({
message: 'The bank transaction has been excluded successfully.',
});
})
.catch((error) => {
.catch(() => {
AppToaster.show({
intent: Intent.DANGER,
message: 'Something went wrong.',
@@ -106,12 +112,14 @@ function AccountTransactionsDataTable({
noResults={
'There is no uncategorized transactions in the current account.'
}
className="table-constrant"
onSelectedRowsChange={handleSelectedRowsChange}
payload={{
onExclude: handleExcludeTransaction,
onCategorize: handleCategorizeBtnClick,
}}
className={clsx('table-constrant', styles.table, {
[styles.showCategorizeColumn]: openMatchingTransactionAside,
})}
/>
);
}
@@ -121,6 +129,9 @@ export default compose(
cashflowTansactionsTableSize: cashflowTransactionsSettings?.tableSize,
})),
withBankingActions,
withBanking(({ openMatchingTransactionAside }) => ({
openMatchingTransactionAside,
})),
)(AccountTransactionsDataTable);
const DashboardConstrantTable = styled(DataTable)`

View File

@@ -1,6 +1,6 @@
import * as R from 'ramda';
import { useEffect } from 'react';
import AccountTransactionsUncategorizedTable from '../AccountTransactionsUncategorizedTable';
import AccountTransactionsUncategorizedTable from './AccountTransactionsUncategorizedTable';
import { AccountUncategorizedTransactionsBoot } from '../AllTransactionsUncategorizedBoot';
import { AccountTransactionsCard } from './AccountTransactionsCard';
import {

View File

@@ -0,0 +1,143 @@
// @ts-nocheck
import React from 'react';
import intl from 'react-intl-universal';
import {
Checkbox,
Intent,
PopoverInteractionKind,
Position,
Tag,
Tooltip,
} from '@blueprintjs/core';
import {
useAddTransactionsToCategorizeSelected,
useRemoveTransactionsToCategorizeSelected,
} from '@/hooks/state/banking';
import { Box, Icon } from '@/components';
import styles from './AccountTransactionsUncategorizedTable.module.scss';
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.
*/
export function useAccountUncategorizedTransactionsColumns() {
const addTransactionsToCategorizeSelected =
useAddTransactionsToCategorizeSelected();
const removeTransactionsToCategorizeSelected =
useRemoveTransactionsToCategorizeSelected();
const handleChange = (value) => (event) => {
if (event.currentTarget.checked) {
addTransactionsToCategorizeSelected(value.id);
} else {
removeTransactionsToCategorizeSelected(value.id);
}
};
return React.useMemo(
() => [
{
id: 'date',
Header: intl.get('date'),
accessor: 'formatted_date',
width: 40,
clickable: true,
textOverview: true,
},
{
id: 'description',
Header: 'Description',
accessor: 'description',
width: 160,
textOverview: true,
clickable: true,
},
{
id: 'payee',
Header: 'Payee',
accessor: 'payee',
width: 60,
clickable: true,
textOverview: true,
},
{
id: 'reference_number',
Header: 'Ref.#',
accessor: 'reference_no',
width: 50,
clickable: true,
textOverview: true,
},
{
id: 'status',
Header: 'Status',
accessor: statusAccessor,
},
{
id: 'deposit',
Header: intl.get('cash_flow.label.deposit'),
accessor: 'formatted_deposit_amount',
width: 40,
className: 'deposit',
textOverview: true,
align: 'right',
clickable: true,
},
{
id: 'withdrawal',
Header: intl.get('cash_flow.label.withdrawal'),
accessor: 'formatted_withdrawal_amount',
className: 'withdrawal',
width: 40,
textOverview: true,
align: 'right',
clickable: true,
},
{
id: 'categorize_include',
Header: '',
accessor: (value) => (
<Checkbox
large
onChange={handleChange(value)}
className={styles.categorizeCheckbox}
/>
),
width: 10,
minWidth: 10,
maxWidth: 10,
align: 'right',
className: 'categorize_include',
},
],
[],
);
}

View File

@@ -9,15 +9,10 @@ import {
PopoverInteractionKind,
Position,
Tooltip,
Checkbox,
} from '@blueprintjs/core';
import { Box, FormatDateCell, Icon, MaterialProgressBar } from '@/components';
import { useAccountTransactionsContext } from './AccountTransactionsProvider';
import { safeCallback } from '@/utils';
import {
useAddTransactionsToCategorizeSelected,
useRemoveTransactionsToCategorizeSelected,
} from '@/hooks/state/banking';
export function ActionsMenu({
payload: { onUncategorize, onUnmatch },
@@ -155,122 +150,3 @@ export function AccountTransactionsProgressBar() {
<MaterialProgressBar />
) : 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.
*/
export function useAccountUncategorizedTransactionsColumns() {
const addTransactionsToCategorizeSelected =
useAddTransactionsToCategorizeSelected();
const removeTransactionsToCategorizeSelected =
useRemoveTransactionsToCategorizeSelected();
const handleChange = (value) => (event) => {
if (event.currentTarget.checked) {
addTransactionsToCategorizeSelected(value.id);
} else {
removeTransactionsToCategorizeSelected(value.id);
}
};
return React.useMemo(
() => [
{
id: 'date',
Header: intl.get('date'),
accessor: 'formatted_date',
width: 40,
clickable: true,
textOverview: true,
},
{
id: 'description',
Header: 'Description',
accessor: 'description',
width: 160,
textOverview: true,
clickable: true,
},
{
id: 'payee',
Header: 'Payee',
accessor: 'payee',
width: 60,
clickable: true,
textOverview: true,
},
{
id: 'reference_number',
Header: 'Ref.#',
accessor: 'reference_no',
width: 50,
clickable: true,
textOverview: true,
},
{
id: 'status',
Header: 'Status',
accessor: statusAccessor,
},
{
id: 'deposit',
Header: intl.get('cash_flow.label.deposit'),
accessor: 'formatted_deposit_amount',
width: 40,
className: 'deposit',
textOverview: true,
align: 'right',
clickable: true,
},
{
id: 'withdrawal',
Header: intl.get('cash_flow.label.withdrawal'),
accessor: 'formatted_withdrawal_amount',
className: 'withdrawal',
width: 40,
textOverview: true,
align: 'right',
clickable: true,
},
{
id: 'categorize_include',
Header: 'Include',
accessor: (value) => <Checkbox large onChange={handleChange(value)} />,
width: 10,
minWidth: 10,
maxWidth: 10,
align: 'right',
},
],
[],
);
}

View File

@@ -18,6 +18,7 @@ export const withBanking = (mapState) => {
state.plaid.uncategorizedTransactionsSelected,
excludedTransactionsIdsSelected: state.plaid.excludedTransactionsSelected,
isMultipleCategorization: state.plaid.isMultipleCategorization,
};
return mapState ? mapState(mapped, state, props) : mapped;
};

View File

@@ -10,6 +10,7 @@ import {
setExcludedTransactionsSelected,
resetTransactionsToCategorizeSelected,
setTransactionsToCategorizeSelected,
enableMultipleCategorization,
} from '@/store/banking/banking.reducer';
export interface WithBankingActionsProps {
@@ -28,6 +29,8 @@ export interface WithBankingActionsProps {
setTransactionsToCategorizeSelected: (ids: Array<string | number>) => void;
resetTransactionsToCategorizeSelected: () => void;
enableMultipleCategorization: (enable: boolean) => void;
}
const mapDipatchToProps = (dispatch: any): WithBankingActionsProps => ({
@@ -66,6 +69,9 @@ const mapDipatchToProps = (dispatch: any): WithBankingActionsProps => ({
dispatch(setTransactionsToCategorizeSelected({ ids })),
resetTransactionsToCategorizeSelected: () =>
dispatch(resetTransactionsToCategorizeSelected()),
enableMultipleCategorization: (enable) =>
dispatch(enableMultipleCategorization({ enable })),
});
export const withBankingActions = connect<