feat(webapp): categorize the cashflow uncategorized transactions

This commit is contained in:
Ahmed Bouhuolia
2024-03-04 13:41:15 +02:00
parent 0273714a07
commit 9db03350e0
12 changed files with 439 additions and 36 deletions

View File

@@ -6,7 +6,6 @@ import { useUncontrolled } from '@/hooks/useUncontrolled';
const ContentTabsRoot = styled('div')`
display: flex;
gap: 10px;
`;
interface ContentTabItemRootProps {
active?: boolean;
@@ -62,9 +61,10 @@ const ContentTabsItem = ({
title,
description,
active,
onClick,
}: ContentTabsItemProps) => {
return (
<ContentTabItemRoot active={active}>
<ContentTabItemRoot active={active} onClick={onClick}>
<ContentTabTitle>{title}</ContentTabTitle>
<ContentTabDesc>{description}</ContentTabDesc>
</ContentTabItemRoot>
@@ -84,7 +84,7 @@ export function ContentTabs({
value,
onChange,
children,
className
className,
}: ContentTabsProps) {
const [localValue, handleItemChange] = useUncontrolled<string>({
initialValue,

View File

@@ -1,13 +1,21 @@
// @ts-nocheck
import styled from 'styled-components';
import { ContentTabs } from '@/components/ContentTabs/ContentTabs';
import { useAccountTransactionsContext } from './AccountTransactionsProvider';
const AccountContentTabs = styled(ContentTabs)`
margin: 15px 15px 0 15px;
`;
export function AccountTransactionsFilterTabs() {
const { filterTab, setFilterTab } = useAccountTransactionsContext();
const handleChange = (value) => {
setFilterTab(value);
};
return (
<AccountContentTabs value={'uncategorized'}>
<AccountContentTabs value={filterTab} onChange={handleChange}>
<ContentTabs.Tab
id={'dashboard'}
title={'Dashboard'}

View File

@@ -1,18 +1,20 @@
// @ts-nocheck
import React from 'react';
import React, { Suspense } from 'react';
import styled from 'styled-components';
import { Spinner } from '@blueprintjs/core';
import '@/style/pages/CashFlow/AccountTransactions/List.scss';
import { DashboardPageContent } from '@/components';
import AccountTransactionsActionsBar from './AccountTransactionsActionsBar';
import AccountTransactionsDataTable from './AccountTransactionsDataTable';
import { AccountTransactionsProvider } from './AccountTransactionsProvider';
import {
AccountTransactionsProvider,
useAccountTransactionsContext,
} from './AccountTransactionsProvider';
import { AccountTransactionsDetailsBar } from './AccountTransactionsDetailsBar';
import { AccountTransactionsProgressBar } from './components';
import { AccountTransactionsFilterTabs } from './AccountTransactionsFilterTabs';
import { AccountTransactionsUncategorizeFilter } from './AccountTransactionsUncategorizeFilter';
/**
* Account transactions list.
@@ -27,13 +29,9 @@ function AccountTransactionsList() {
<DashboardPageContent>
<AccountTransactionsFilterTabs />
<Box>
<AccountTransactionsUncategorizeFilter />
<CashflowTransactionsTableCard>
<AccountTransactionsDataTable />
</CashflowTransactionsTableCard>
</Box>
<Suspense fallback={<Spinner size={30} />}>
<AccountTransactionsContent />
</Suspense>
</DashboardPageContent>
</AccountTransactionsProvider>
);
@@ -41,14 +39,20 @@ function AccountTransactionsList() {
export default AccountTransactionsList;
const CashflowTransactionsTableCard = styled.div`
border: 2px solid #f0f0f0;
border-radius: 10px;
padding: 30px 18px;
background: #fff;
flex: 0 1;
`;
const AccountsTransactionsAll = React.lazy(
() => import('./AccountsTransactionsAll'),
);
const Box = styled.div`
margin: 30px 15px;
`;
const AccountsTransactionsUncategorized = React.lazy(
() => import('./AllTransactionsUncategorized'),
);
function AccountTransactionsContent() {
const { filterTab } = useAccountTransactionsContext();
return filterTab === 'uncategorized' ? (
<AccountsTransactionsUncategorized />
) : (
<AccountsTransactionsAll />
);
}

View File

@@ -7,7 +7,9 @@ import {
useAccountTransactionsInfinity,
useCashflowAccounts,
useAccount,
useAccountUncategorizedTransactionsInfinity,
} from '@/hooks/query';
import { useAppQueryString } from '@/hooks';
const AccountTransactionsContext = React.createContext();
@@ -15,6 +17,10 @@ function flattenInfinityPages(data) {
return flatten(map(data.pages, (page) => page.transactions));
}
function flattenInfinityPagesData(data) {
return flatten(map(data.pages, (page) => page.data));
}
/**
* Account transctions provider.
*/
@@ -22,6 +28,13 @@ function AccountTransactionsProvider({ query, ...props }) {
const { id } = useParams();
const accountId = parseInt(id, 10);
const [locationQuery, setLocationQuery] = useAppQueryString();
const filterTab = locationQuery?.filter || 'all';
const setFilterTab = (value: stirng) => {
setLocationQuery({ filter: value });
};
// Fetch cashflow account transactions list
const {
data: cashflowTransactionsPages,
@@ -31,10 +44,32 @@ function AccountTransactionsProvider({ query, ...props }) {
fetchNextPage: fetchNextTransactionsPage,
isFetchingNextPage,
hasNextPage,
} = useAccountTransactionsInfinity(accountId, {
page_size: 50,
account_id: accountId,
});
} = useAccountTransactionsInfinity(
accountId,
{
page_size: 50,
account_id: accountId,
},
{
enabled: filterTab === 'all' || filterTab === 'dashboard',
},
);
const {
data: uncategorizedTransactionsPage,
isFetching: isUncategorizedTransactionFetching,
isLoading: isUncategorizedTransactionsLoading,
isSuccess: isUncategorizedTransactionsSuccess,
fetchNextPage: fetchNextUncategorizedTransactionsPage,
} = useAccountUncategorizedTransactionsInfinity(
accountId,
{
page_size: 50,
},
{
enabled: filterTab === 'uncategorized',
},
);
// Memorized the cashflow account transactions.
const cashflowTransactions = React.useMemo(
@@ -45,6 +80,15 @@ function AccountTransactionsProvider({ query, ...props }) {
[cashflowTransactionsPages, isCashflowTransactionsSuccess],
);
// Memorized the cashflow account transactions.
const uncategorizedTransactions = React.useMemo(
() =>
isUncategorizedTransactionsSuccess
? flattenInfinityPagesData(uncategorizedTransactionsPage)
: [],
[uncategorizedTransactionsPage, isUncategorizedTransactionsSuccess],
);
// Fetch cashflow accounts.
const {
data: cashflowAccounts,
@@ -78,6 +122,12 @@ function AccountTransactionsProvider({ query, ...props }) {
isCashFlowAccountsLoading,
isCurrentAccountFetching,
isCurrentAccountLoading,
filterTab,
setFilterTab,
uncategorizedTransactions,
isUncategorizedTransactionFetching
};
return (

View File

@@ -0,0 +1,139 @@
// @ts-nocheck
import React from 'react';
import styled from 'styled-components';
import {
DataTable,
TableFastCell,
TableSkeletonRows,
TableSkeletonHeader,
TableVirtualizedListRows,
FormattedMessage as T,
} from '@/components';
import { TABLES } from '@/constants/tables';
import withSettings from '@/containers/Settings/withSettings';
import withAlertsActions from '@/containers/Alert/withAlertActions';
import withDrawerActions from '@/containers/Drawer/withDrawerActions';
import { useMemorizedColumnsWidths } from '@/hooks';
import {
ActionsMenu,
useAccountUncategorizedTransactionsColumns,
} from './components';
import { useAccountTransactionsContext } from './AccountTransactionsProvider';
import { handleCashFlowTransactionType } from './utils';
import { compose } from '@/utils';
/**
* Account transactions data table.
*/
function AccountTransactionsDataTable({
// #withSettings
cashflowTansactionsTableSize,
// #withAlertsActions
openAlert,
// #withDrawerActions
openDrawer,
}) {
// Retrieve table columns.
const columns = useAccountUncategorizedTransactionsColumns();
// Retrieve list context.
const { uncategorizedTransactions, isCashFlowTransactionsLoading } =
useAccountTransactionsContext();
// Local storage memorizing columns widths.
const [initialColumnsWidths, , handleColumnResizing] =
useMemorizedColumnsWidths(TABLES.CASHFLOW_Transactions);
// handle delete transaction
const handleDeleteTransaction = ({ reference_id }) => {};
const handleViewDetailCashflowTransaction = (referenceType) => {};
// Handle cell click.
const handleCellClick = (cell, event) => {};
return (
<CashflowTransactionsTable
noInitialFetch={true}
columns={columns}
data={uncategorizedTransactions || []}
sticky={true}
loading={isCashFlowTransactionsLoading}
headerLoading={isCashFlowTransactionsLoading}
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={<T id={'cash_flow.account_transactions.no_results'} />}
className="table-constrant"
payload={{
onViewDetails: handleViewDetailCashflowTransaction,
onDelete: handleDeleteTransaction,
}}
/>
);
}
export default compose(
withSettings(({ cashflowTransactionsSettings }) => ({
cashflowTansactionsTableSize: cashflowTransactionsSettings?.tableSize,
})),
withAlertsActions,
withDrawerActions,
)(AccountTransactionsDataTable);
const DashboardConstrantTable = styled(DataTable)`
.table {
.thead {
.th {
background: #fff;
}
}
.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

@@ -0,0 +1,31 @@
// @ts-nocheck
import styled from 'styled-components';
import '@/style/pages/CashFlow/AccountTransactions/List.scss';
import AccountTransactionsDataTable from './AccountTransactionsDataTable';
import { AccountTransactionsUncategorizeFilter } from './AccountTransactionsUncategorizeFilter';
const Box = styled.div`
margin: 30px 15px;
`;
const CashflowTransactionsTableCard = styled.div`
border: 2px solid #f0f0f0;
border-radius: 10px;
padding: 30px 18px;
background: #fff;
flex: 0 1;
`;
export default function AccountTransactionsAll() {
return (
<Box>
<AccountTransactionsUncategorizeFilter />
<CashflowTransactionsTableCard>
<AccountTransactionsDataTable />
</CashflowTransactionsTableCard>
</Box>
);
}

View File

@@ -0,0 +1,29 @@
// @ts-nocheck
import styled from 'styled-components';
import '@/style/pages/CashFlow/AccountTransactions/List.scss';
import AccountTransactionsUncategorizedTable from './AccountTransactionsUncategorizedTable';
const Box = styled.div`
margin: 30px 15px;
`;
const CashflowTransactionsTableCard = styled.div`
border: 2px solid #f0f0f0;
border-radius: 10px;
padding: 30px 18px;
background: #fff;
flex: 0 1;
`
export default function AllTransactionsUncategorized() {
return (
<Box>
<CashflowTransactionsTableCard>
<AccountTransactionsUncategorizedTable />
</CashflowTransactionsTableCard>
</Box>
)
}

View File

@@ -131,7 +131,75 @@ export function useAccountTransactionsColumns() {
* Account transactions progress bar.
*/
export function AccountTransactionsProgressBar() {
const { isCashFlowTransactionsFetching } = useAccountTransactionsContext();
const { isCashFlowTransactionsFetching, isUncategorizedTransactionFetching } =
useAccountTransactionsContext();
return isCashFlowTransactionsFetching ? <MaterialProgressBar /> : null;
return isCashFlowTransactionsFetching ||
isUncategorizedTransactionFetching ? (
<MaterialProgressBar />
) : null;
}
/**
* Retrieve account uncategorized transctions table columns.
*/
export function useAccountUncategorizedTransactionsColumns() {
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: intl.get('reference_no'),
accessor: 'reference_number',
width: 50,
className: 'reference_number',
clickable: true,
textOverview: true,
},
{
id: 'deposit',
Header: intl.get('cash_flow.label.deposit'),
accessor: 'formattet_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,
},
],
[],
);
}

View File

@@ -110,12 +110,12 @@ function CashFlowAccountsActionsBar({
</NavbarGroup>
<NavbarGroup align={Alignment.RIGHT}>
{/* <Button
<Button
className={Classes.MINIMAL}
text={'Connect to Bank / Credit Card'}
onClick={handleConnectToBank}
/>
<NavbarDivider /> */}
<NavbarDivider />
<Button
className={Classes.MINIMAL}
icon={<Icon icon="refresh-16" iconSize={14} />}

View File

@@ -0,0 +1,33 @@
// @ts-nocheck
import React, { lazy } from 'react';
import { Drawer, DrawerSuspense } from '@/components';
import withDrawers from '@/containers/Drawer/withDrawers';
import { compose } from '@/utils';
const AccountDrawerContent = lazy(() => import('./AccountDrawerContent'));
/**
* Categorize the uncategorized transaction drawer.
*/
function CategorizeTransactionDrawer({
name,
// #withDrawer
isOpen,
payload: { uncategorizedTranasctionId },
}) {
return (
<Drawer
isOpen={isOpen}
name={name}
style={{ minWidth: '700px', maxWidth: '900px' }}
size={'65%'}
>
<DrawerSuspense>
<AccountDrawerContent name={name} accountId={accountId} />
</DrawerSuspense>
</Drawer>
);
}
export default compose(withDrawers())(AccountDrawer);

View File

@@ -104,8 +104,8 @@ export function useDeleteCashflowTransaction(props) {
export function useAccountTransactionsInfinity(
accountId,
query,
axios,
infinityProps,
axios,
) {
const apiRequest = useApiRequest();
@@ -134,6 +134,45 @@ export function useAccountTransactionsInfinity(
);
}
/**
* Retrieve account transactions infinity scrolling.
* @param {number} accountId
* @param {*} axios
* @returns
*/
export function useAccountUncategorizedTransactionsInfinity(
accountId,
query,
infinityProps,
axios,
) {
const apiRequest = useApiRequest();
return useInfiniteQuery(
[t.CASHFLOW_ACCOUNT_UNCATEGORIZED_TRANSACTIONS_INFINITY, accountId],
async ({ pageParam = 1 }) => {
const response = await apiRequest.http({
...axios,
method: 'get',
url: `/api/cashflow/transactions/${accountId}/uncategorized`,
params: { page: pageParam, ...query },
});
return response.data;
},
{
getPreviousPageParam: (firstPage) => firstPage.pagination.page - 1,
getNextPageParam: (lastPage) => {
const { pagination } = lastPage;
return pagination.total > pagination.page_size * pagination.page
? lastPage.pagination.page + 1
: undefined;
},
...infinityProps,
},
);
}
/**
* Refresh cashflow transactions infinity.
*/

View File

@@ -77,7 +77,7 @@ const SALE_RECEIPTS = {
SALE_RECEIPT: 'SALE_RECEIPT',
SALE_RECEIPT_SMS_DETAIL: 'SALE_RECEIPT_SMS_DETAIL',
NOTIFY_SALE_RECEIPT_BY_SMS: 'NOTIFY_SALE_RECEIPT_BY_SMS',
SALE_RECEIPT_MAIL_OPTIONS: 'SALE_RECEIPT_MAIL_OPTIONS'
SALE_RECEIPT_MAIL_OPTIONS: 'SALE_RECEIPT_MAIL_OPTIONS',
};
const INVENTORY_ADJUSTMENTS = {
@@ -115,7 +115,7 @@ const SALE_INVOICES = {
BAD_DEBT: 'BAD_DEBT',
CANCEL_BAD_DEBT: 'CANCEL_BAD_DEBT',
SALE_INVOICE_PAYMENT_TRANSACTIONS: 'SALE_INVOICE_PAYMENT_TRANSACTIONS',
SALE_INVOICE_DEFAULT_OPTIONS: 'SALE_INVOICE_DEFAULT_OPTIONS'
SALE_INVOICE_DEFAULT_OPTIONS: 'SALE_INVOICE_DEFAULT_OPTIONS',
};
const USERS = {
@@ -200,6 +200,8 @@ const CASH_FLOW_ACCOUNTS = {
CASH_FLOW_TRANSACTION: 'CASH_FLOW_TRANSACTION',
CASHFLOW_ACCOUNT_TRANSACTIONS_INFINITY:
'CASHFLOW_ACCOUNT_TRANSACTIONS_INFINITY',
CASHFLOW_ACCOUNT_UNCATEGORIZED_TRANSACTIONS_INFINITY:
'CASHFLOW_ACCOUNT_UNCATEGORIZED_TRANSACTIONS_INFINITY',
};
const TARNSACTIONS_LOCKING = {