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

@@ -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

@@ -0,0 +1,179 @@
// @ts-nocheck
import React from 'react';
import clsx from 'classnames';
import styled from 'styled-components';
import { Intent } from '@blueprintjs/core';
import {
DataTable,
TableFastCell,
TableSkeletonRows,
TableSkeletonHeader,
TableVirtualizedListRows,
FormattedMessage as T,
AppToaster,
} from '@/components';
import { TABLES } from '@/constants/tables';
import { ActionsMenu } from './components';
import withSettings from '@/containers/Settings/withSettings';
import { withBankingActions } from '../../withBankingActions';
import { useMemorizedColumnsWidths } from '@/hooks';
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.
*/
function AccountTransactionsDataTable({
// #withSettings
cashflowTansactionsTableSize,
// #withBanking
openMatchingTransactionAside,
// #withBankingActions
setUncategorizedTransactionIdForMatching,
setUncategorizedTransactionsSelected,
}) {
// Retrieve table columns.
const columns = useAccountUncategorizedTransactionsColumns();
// Retrieve list context.
const { uncategorizedTransactions, isUncategorizedTransactionsLoading } =
useAccountUncategorizedTransactionsContext();
const { mutateAsync: excludeTransaction } =
useExcludeUncategorizedTransaction();
// Local storage memorizing columns widths.
const [initialColumnsWidths, , handleColumnResizing] =
useMemorizedColumnsWidths(TABLES.UNCATEGORIZED_CASHFLOW_TRANSACTION);
// Handle cell click.
const handleCellClick = (cell) => {
setUncategorizedTransactionIdForMatching(cell.row.original.id);
};
// Handles categorize button click.
const handleCategorizeBtnClick = (transaction) => {
setUncategorizedTransactionIdForMatching(transaction.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(() => {
AppToaster.show({
intent: Intent.DANGER,
message: 'Something went wrong.',
});
});
};
// Handle selected rows change.
const handleSelectedRowsChange = (selected) => {
const _selectedIds = selected?.map((row) => row.original.id);
setUncategorizedTransactionsSelected(_selectedIds);
};
return (
<CashflowTransactionsTable
noInitialFetch={true}
columns={columns}
data={uncategorizedTransactions || []}
sticky={true}
selectionColumn={true}
loading={isUncategorizedTransactionsLoading}
headerLoading={isUncategorizedTransactionsLoading}
expandColumnSpace={1}
expandToggleColumn={2}
selectionColumnWidth={45}
TableCellRenderer={TableFastCell}
TableLoadingRenderer={TableSkeletonRows}
TableRowsRenderer={TableVirtualizedListRows}
TableHeaderSkeletonRenderer={TableSkeletonHeader}
ContextMenu={ActionsMenu}
onCellClick={handleCellClick}
// #TableVirtualizedListRows props.
vListrowHeight={cashflowTansactionsTableSize === 'small' ? 32 : 40}
vListOverscanRowCount={0}
initialColumnsWidths={initialColumnsWidths}
onColumnResizing={handleColumnResizing}
noResults={
'There is no uncategorized transactions in the current account.'
}
onSelectedRowsChange={handleSelectedRowsChange}
payload={{
onExclude: handleExcludeTransaction,
onCategorize: handleCategorizeBtnClick,
}}
className={clsx('table-constrant', styles.table, {
[styles.showCategorizeColumn]: openMatchingTransactionAside,
})}
/>
);
}
export default compose(
withSettings(({ cashflowTransactionsSettings }) => ({
cashflowTansactionsTableSize: cashflowTransactionsSettings?.tableSize,
})),
withBankingActions,
withBanking(({ openMatchingTransactionAside }) => ({
openMatchingTransactionAside,
})),
)(AccountTransactionsDataTable);
const DashboardConstrantTable = styled(DataTable)`
.table {
.thead {
.th {
background: #fff;
text-transform: uppercase;
letter-spacing: 1px;
font-size: 13px;i
font-weight: 500;
}
}
.tbody {
.tr:last-child .td {
border-bottom: 0;
}
}
}
`;
const CashflowTransactionsTable = styled(DashboardConstrantTable)`
.table .tbody {
.tbody-inner .tr.no-results {
.td {
padding: 2rem 0;
font-size: 14px;
color: #888;
font-weight: 400;
border-bottom: 0;
}
}
.tbody-inner {
.tr .td:not(:first-child) {
border-left: 1px solid #e6e6e6;
}
.td-description {
color: #5f6b7c;
}
}
}
`;

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',
},
],
[],
);
}