mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-19 06:10:31 +00:00
feat: cashflow account transactions infinity scroll loading.
This commit is contained in:
@@ -2,6 +2,8 @@ import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { Classes } from '@blueprintjs/core';
|
||||
import clsx from 'classnames';
|
||||
import useContextMenu from 'react-use-context-menu';
|
||||
import ContextMenu from '../ContextMenu';
|
||||
import Icon from '../Icon';
|
||||
|
||||
const BankAccountWrap = styled.div`
|
||||
@@ -20,21 +22,6 @@ const BankAccountWrap = styled.div`
|
||||
}
|
||||
`;
|
||||
|
||||
const BankAccountAnchor = styled.a`
|
||||
text-decoration: none;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
color: inherit;
|
||||
|
||||
&:hover,
|
||||
&:focus,
|
||||
&:active {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
`;
|
||||
|
||||
const BankAccountHeader = styled.div`
|
||||
padding: 10px 12px;
|
||||
padding-top: 16px;
|
||||
@@ -179,43 +166,65 @@ function BankAccountTypeIcon({ type }) {
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
export function BankAccount({
|
||||
title,
|
||||
code,
|
||||
type,
|
||||
balance,
|
||||
loading = false,
|
||||
to
|
||||
updatedBeforeText,
|
||||
to,
|
||||
contextMenuContent,
|
||||
}) {
|
||||
const [
|
||||
bindMenu,
|
||||
bindMenuItem,
|
||||
useContextTrigger,
|
||||
{ coords, setVisible, isVisible },
|
||||
] = useContextMenu();
|
||||
|
||||
const [bindTrigger] = useContextTrigger({
|
||||
collect: () => 'Title',
|
||||
});
|
||||
|
||||
const handleClose = React.useCallback(() => {
|
||||
setVisible(false);
|
||||
}, [setVisible]);
|
||||
|
||||
return (
|
||||
<BankAccountWrap>
|
||||
<BankAccountAnchor href="#">
|
||||
<BankAccountHeader>
|
||||
<BankAccountTitle className={clsx({ [Classes.SKELETON]: loading })}>
|
||||
{title}
|
||||
</BankAccountTitle>
|
||||
<BnakAccountCode className={clsx({ [Classes.SKELETON]: loading })}>
|
||||
{code}
|
||||
</BnakAccountCode>
|
||||
{!loading && <BankAccountTypeIcon type={type} />}
|
||||
</BankAccountHeader>
|
||||
<BankAccountWrap {...bindTrigger}>
|
||||
<BankAccountHeader>
|
||||
<BankAccountTitle className={clsx({ [Classes.SKELETON]: loading })}>
|
||||
{title}
|
||||
</BankAccountTitle>
|
||||
<BnakAccountCode className={clsx({ [Classes.SKELETON]: loading })}>
|
||||
{code}
|
||||
</BnakAccountCode>
|
||||
{!loading && <BankAccountTypeIcon type={type} />}
|
||||
</BankAccountHeader>
|
||||
|
||||
<BankAccountMeta>
|
||||
<BankAccountMetaLine
|
||||
title={'Account transactions'}
|
||||
value={2}
|
||||
className={clsx({ [Classes.SKELETON]: loading })}
|
||||
/>
|
||||
<BankAccountMetaLine
|
||||
title={'Updated 2 days ago'}
|
||||
className={clsx({ [Classes.SKELETON]: loading })}
|
||||
/>
|
||||
</BankAccountMeta>
|
||||
<BankAccountMeta>
|
||||
<BankAccountMetaLine
|
||||
title={'Account transactions'}
|
||||
value={2}
|
||||
className={clsx({ [Classes.SKELETON]: loading })}
|
||||
/>
|
||||
<BankAccountMetaLine
|
||||
title={updatedBeforeText}
|
||||
className={clsx({ [Classes.SKELETON]: loading })}
|
||||
/>
|
||||
</BankAccountMeta>
|
||||
|
||||
<BankAccountBalance amount={balance} loading={loading} />
|
||||
</BankAccountAnchor>
|
||||
<BankAccountBalance amount={balance} loading={loading} />
|
||||
|
||||
<ContextMenu
|
||||
bindMenu={bindMenu}
|
||||
isOpen={isVisible}
|
||||
coords={coords}
|
||||
onClosed={handleClose}
|
||||
>
|
||||
<contextMenuContent />
|
||||
</ContextMenu>
|
||||
</BankAccountWrap>
|
||||
);
|
||||
}
|
||||
|
||||
23
src/components/IntersectionObserver/index.js
Normal file
23
src/components/IntersectionObserver/index.js
Normal file
@@ -0,0 +1,23 @@
|
||||
import React from 'react';
|
||||
import { useIntersectionObserver } from 'hooks/utils';
|
||||
|
||||
/**
|
||||
* Intersection observer.
|
||||
*/
|
||||
export function IntersectionObserver({ onIntersect }) {
|
||||
const loadMoreButtonRef = React.useRef();
|
||||
|
||||
useIntersectionObserver({
|
||||
// enabled: !isItemsLoading && !isResourceLoading,
|
||||
target: loadMoreButtonRef,
|
||||
onIntersect: () => {
|
||||
onIntersect && onIntersect();
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<div ref={loadMoreButtonRef} style={{ opacity: 0 }}>
|
||||
Load Newer
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -82,6 +82,7 @@ export * from './MultiSelectTaggable';
|
||||
export * from './Utils/FormatNumber';
|
||||
export * from './Utils/FormatDate';
|
||||
export * from './BankAccounts';
|
||||
export * from './IntersectionObserver'
|
||||
|
||||
const Hint = FieldHint;
|
||||
|
||||
|
||||
@@ -52,14 +52,13 @@ function AccountTransactionsDataTable({
|
||||
TableRowsRenderer={TableVirtualizedListRows}
|
||||
TableHeaderSkeletonRenderer={TableSkeletonHeader}
|
||||
// #TableVirtualizedListRows props.
|
||||
vListrowHeight={cashflowTansactionsTableSize == 'small' ? 40 : 42}
|
||||
vListrowHeight={cashflowTansactionsTableSize == 'small' ? 32 : 40}
|
||||
vListOverscanRowCount={0}
|
||||
TableHeaderSkeletonRenderer={TableSkeletonHeader}
|
||||
initialColumnsWidths={initialColumnsWidths}
|
||||
onColumnResizing={handleColumnResizing}
|
||||
size={cashflowTansactionsTableSize}
|
||||
noResults={'There is deposit/withdrawal transactions on the current account.'}
|
||||
|
||||
className="table-constrant"
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -29,7 +29,7 @@ function AccountSwitchButton() {
|
||||
|
||||
function AccountSwitchItem() {
|
||||
const { push } = useHistory();
|
||||
const { cashflowAccounts } = useAccountTransactionsContext();
|
||||
const { cashflowAccounts, accountId } = useAccountTransactionsContext();
|
||||
|
||||
// Handle item click.
|
||||
const handleItemClick = curry((account, event) => {
|
||||
@@ -39,7 +39,9 @@ function AccountSwitchItem() {
|
||||
const items = cashflowAccounts.map((account) => (
|
||||
<AccountSwitchMenuItem
|
||||
name={account.name}
|
||||
balance={account.formatted_amount}
|
||||
onClick={handleItemClick(account)}
|
||||
active={account.id === accountId}
|
||||
/>
|
||||
));
|
||||
|
||||
@@ -111,7 +113,7 @@ function AccountSwitchMenuItem({
|
||||
}) {
|
||||
return (
|
||||
<MenuItem
|
||||
label={'LYD100,000'}
|
||||
label={balance}
|
||||
text={
|
||||
<React.Fragment>
|
||||
<AccountSwitchItemName>{name}</AccountSwitchItemName>
|
||||
|
||||
@@ -1,14 +1,20 @@
|
||||
import React from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { flatten, map } from 'lodash';
|
||||
import DashboardInsider from 'components/Dashboard/DashboardInsider';
|
||||
import { IntersectionObserver } from 'components';
|
||||
import {
|
||||
useCashflowTransactions,
|
||||
useAccountTransactionsInfinity,
|
||||
useCashflowAccounts,
|
||||
useAccount,
|
||||
} from 'hooks/query';
|
||||
|
||||
const AccountTransactionsContext = React.createContext();
|
||||
|
||||
function flattenInfinityPages(data) {
|
||||
return flatten(map(data.pages, (page) => page.cashflow_transactions));
|
||||
}
|
||||
|
||||
/**
|
||||
* Account transctions provider.
|
||||
*/
|
||||
@@ -18,26 +24,44 @@ function AccountTransactionsProvider({ query, ...props }) {
|
||||
|
||||
// Fetch cashflow account transactions list
|
||||
const {
|
||||
data: cashflowTransactions,
|
||||
data: cashflowTransactionsPages,
|
||||
isFetching: isCashFlowTransactionsFetching,
|
||||
isLoading: isCashFlowTransactionsLoading,
|
||||
} = useCashflowTransactions(accountId, {
|
||||
enabled: !!accountId,
|
||||
isSuccess: isCashflowTransactionsSuccess,
|
||||
fetchNextPage: fetchNextTransactionsPage,
|
||||
isFetchingNextPage,
|
||||
} = useAccountTransactionsInfinity(accountId, {
|
||||
page_size: 50,
|
||||
});
|
||||
|
||||
// Fetch cashflow accounts .
|
||||
const cashflowTransactions = React.useMemo(
|
||||
() =>
|
||||
isCashflowTransactionsSuccess
|
||||
? flattenInfinityPages(cashflowTransactionsPages)
|
||||
: [],
|
||||
[cashflowTransactionsPages, isCashflowTransactionsSuccess],
|
||||
);
|
||||
|
||||
// Fetch cashflow accounts.
|
||||
const {
|
||||
data: cashflowAccounts,
|
||||
isFetching: isCashFlowAccountsFetching,
|
||||
isLoading: isCashFlowAccountsLoading,
|
||||
} = useCashflowAccounts(query, { keepPreviousData: true });
|
||||
|
||||
// Retrieve specific account details.
|
||||
const {
|
||||
data: currentAccount,
|
||||
isFetching: isCurrentAccountFetching,
|
||||
isLoading: isCurrentAccountLoading,
|
||||
} = useAccount(accountId, { keepPreviousData: true });
|
||||
|
||||
const handleObserverInteract = React.useCallback(() => {
|
||||
if (!isFetchingNextPage) {
|
||||
fetchNextTransactionsPage();
|
||||
}
|
||||
}, [isFetchingNextPage, fetchNextTransactionsPage]);
|
||||
|
||||
// Provider payload.
|
||||
const provider = {
|
||||
accountId,
|
||||
@@ -55,6 +79,10 @@ function AccountTransactionsProvider({ query, ...props }) {
|
||||
return (
|
||||
<DashboardInsider name={'account-transactions'}>
|
||||
<AccountTransactionsContext.Provider value={provider} {...props} />
|
||||
<IntersectionObserver
|
||||
onIntersect={handleObserverInteract}
|
||||
// enabled={!isFetchingNextPage}
|
||||
/>
|
||||
</DashboardInsider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -21,32 +21,41 @@ export function useAccountTransactionsColumns() {
|
||||
{
|
||||
id: 'type',
|
||||
Header: intl.get('type'),
|
||||
accessor: 'type',
|
||||
accessor: 'reference_type_formatted',
|
||||
className: 'type',
|
||||
width: 140,
|
||||
textOverview: true,
|
||||
},
|
||||
{
|
||||
id: 'status',
|
||||
Header: intl.get('status'),
|
||||
// accessor:
|
||||
id: 'transaction_number',
|
||||
Header: intl.get('transaction_number'),
|
||||
accessor: 'transaction_number',
|
||||
width: 160,
|
||||
className: 'status',
|
||||
className: 'transaction_number',
|
||||
},
|
||||
{
|
||||
id: 'reference_number',
|
||||
Header: intl.get('reference_no'),
|
||||
accessor: 'reference_number',
|
||||
width: 160,
|
||||
className: 'reference_number',
|
||||
},
|
||||
{
|
||||
id: 'deposit',
|
||||
Header: intl.get('cash_flow.label.deposit'),
|
||||
accessor: 'formattedDeposit',
|
||||
accessor: 'formatted_deposit',
|
||||
width: 110,
|
||||
className: 'deposit',
|
||||
align: 'right',
|
||||
textOverview: true,
|
||||
align: 'right'
|
||||
},
|
||||
{
|
||||
id: 'withdrawal',
|
||||
Header: intl.get('cash_flow.label.withdrawal'),
|
||||
accessor: 'formattedWithdrawal',
|
||||
accessor: 'formatted_withdrawal',
|
||||
className: 'withdrawal',
|
||||
width: 150,
|
||||
textOverview: true,
|
||||
align: 'right',
|
||||
},
|
||||
{
|
||||
@@ -55,7 +64,8 @@ export function useAccountTransactionsColumns() {
|
||||
accessor: 'running_balance',
|
||||
className: 'running_balance',
|
||||
width: 150,
|
||||
align: 'right',
|
||||
textOverview: true,
|
||||
align: 'right'
|
||||
},
|
||||
],
|
||||
[],
|
||||
|
||||
@@ -2,7 +2,9 @@ import React from 'react';
|
||||
import { isNull } from 'lodash';
|
||||
import styled from 'styled-components';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { BankAccountsList, BankAccount } from '../../../components';
|
||||
import { Menu, MenuItem, MenuDivider, Intent } from '@blueprintjs/core';
|
||||
import intl from 'react-intl-universal';
|
||||
import { BankAccountsList, BankAccount, If, Icon } from '../../../components';
|
||||
import { useCashFlowAccountsContext } from './CashFlowAccountsProvider';
|
||||
|
||||
const CashflowAccountsGridWrap = styled.div`
|
||||
@@ -22,16 +24,22 @@ function CashflowAccountsSkeleton() {
|
||||
));
|
||||
}
|
||||
|
||||
function getUpdatedBeforeText(createdAt) {
|
||||
return 'Updated before 2 years.';
|
||||
}
|
||||
|
||||
function CashflowAccountsGridItems({ accounts }) {
|
||||
return accounts.map((account) => (
|
||||
<Link to={`/cashflow-accounts/${account.id}/transactions`}>
|
||||
<CashflowAccountAnchor to={`/cashflow-accounts/${account.id}/transactions`}>
|
||||
<BankAccount
|
||||
title={account.name}
|
||||
code={account.code}
|
||||
balance={!isNull(account.amount) ? account.formattedAmount : '-'}
|
||||
balance={!isNull(account.amount) ? account.formatted_amount : '-'}
|
||||
type={'cash'}
|
||||
contextMenuContent={CashflowAccountContextMenu}
|
||||
updatedBeforeText={getUpdatedBeforeText(account.createdAt)}
|
||||
/>
|
||||
</Link>
|
||||
</CashflowAccountAnchor>
|
||||
));
|
||||
}
|
||||
|
||||
@@ -52,3 +60,44 @@ export default function CashflowAccountsGrid() {
|
||||
</CashflowAccountsGridWrap>
|
||||
);
|
||||
}
|
||||
|
||||
function CashflowAccountContextMenu() {
|
||||
return (
|
||||
<Menu>
|
||||
<MenuItem
|
||||
icon={<Icon icon="reader-18" />}
|
||||
text={intl.get('view_details')}
|
||||
/>
|
||||
<MenuDivider />
|
||||
<MenuItem icon={<Icon icon="pen-18" />} text={intl.get('edit_account')} />
|
||||
<MenuDivider />
|
||||
<If condition={false}>
|
||||
<MenuItem
|
||||
text={intl.get('inactivate_account')}
|
||||
icon={<Icon icon="pause-16" iconSize={16} />}
|
||||
/>
|
||||
</If>
|
||||
<If condition={!false}>
|
||||
<MenuItem
|
||||
text={intl.get('activate_account')}
|
||||
icon={<Icon icon="play-16" iconSize={16} />}
|
||||
/>
|
||||
</If>
|
||||
<MenuItem
|
||||
text={intl.get('delete_account')}
|
||||
icon={<Icon icon="trash-16" iconSize={16} />}
|
||||
intent={Intent.DANGER}
|
||||
/>
|
||||
</Menu>
|
||||
);
|
||||
}
|
||||
|
||||
const CashflowAccountAnchor = styled(Link)`
|
||||
&,
|
||||
&:hover,
|
||||
&:focus,
|
||||
&:active {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { useMutation, useQueryClient } from 'react-query';
|
||||
import { useMutation, useQueryClient, useInfiniteQuery } from 'react-query';
|
||||
import { useRequestQuery } from '../useQueryRequest';
|
||||
import useApiRequest from '../useRequest';
|
||||
import t from './types';
|
||||
|
||||
const commonInvalidateQueries = (queryClient) => {
|
||||
// queryClient.invalidateQueries(t.CASH_FLOW_TRANSACTIONS);
|
||||
// Invalidate accounts.
|
||||
queryClient.invalidateQueries(t.ACCOUNTS);
|
||||
queryClient.invalidateQueries(t.ACCOUNT);
|
||||
queryClient.invalidateQueries(t.CASH_FLOW_TRANSACTION);
|
||||
};
|
||||
|
||||
@@ -22,13 +24,14 @@ export function useCashflowAccounts(query, props) {
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve account transactions list.
|
||||
*/
|
||||
export function useCashflowTransactions(id, props) {
|
||||
return useRequestQuery(
|
||||
[t.CASH_FLOW_TRANSACTIONS, id],
|
||||
{ method: 'get', url: `cashflow/account/${1000}/transactions` },
|
||||
{ method: 'get', url: `cashflow/account/${id}/transactions` },
|
||||
{
|
||||
select: (res) => res.data.cashflow_transactions,
|
||||
defaultData: [],
|
||||
@@ -73,3 +76,42 @@ export function useDeleteCashflowTransaction(props) {
|
||||
...props,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve account transactions infinity scrolling.
|
||||
* @param {number} accountId
|
||||
* @param {*} axios
|
||||
* @returns
|
||||
*/
|
||||
export function useAccountTransactionsInfinity(
|
||||
accountId,
|
||||
query,
|
||||
axios,
|
||||
infinityProps,
|
||||
) {
|
||||
const apiRequest = useApiRequest();
|
||||
|
||||
return useInfiniteQuery(
|
||||
['CASHFLOW_ACCOUNT_TRANSACTIONS_INFINITY', accountId],
|
||||
async ({ pageParam = 1 }) => {
|
||||
const response = await apiRequest.http({
|
||||
...axios,
|
||||
method: 'get',
|
||||
url: `/api/cashflow/account/${accountId}/transactions`,
|
||||
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,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,4 +6,5 @@ export * from './useUpdateEffect';
|
||||
export * from './useWatch';
|
||||
export * from './useWhen';
|
||||
export * from './useRequestPdf';
|
||||
export * from './useAsync';
|
||||
export * from './useAsync';
|
||||
export * from './useIntersectionObserver';
|
||||
36
src/hooks/utils/useIntersectionObserver.js
Normal file
36
src/hooks/utils/useIntersectionObserver.js
Normal file
@@ -0,0 +1,36 @@
|
||||
import React from 'react';
|
||||
|
||||
export function useIntersectionObserver({
|
||||
root,
|
||||
target,
|
||||
onIntersect,
|
||||
threshold = 1.0,
|
||||
rootMargin = '0px',
|
||||
enabled = true,
|
||||
}) {
|
||||
React.useEffect(() => {
|
||||
if (!enabled) {
|
||||
return;
|
||||
}
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) =>
|
||||
entries.forEach((entry) => entry.isIntersecting && onIntersect()),
|
||||
{
|
||||
root: root && root.current,
|
||||
rootMargin,
|
||||
// threshold,
|
||||
threshold: 0.25,
|
||||
},
|
||||
);
|
||||
const el = target && target.current;
|
||||
|
||||
if (!el) {
|
||||
return;
|
||||
}
|
||||
observer.observe(el);
|
||||
|
||||
return () => {
|
||||
observer.unobserve(el);
|
||||
};
|
||||
}, [target.current, enabled, onIntersect, root]);
|
||||
}
|
||||
Reference in New Issue
Block a user