diff --git a/src/components/BankAccounts/index.js b/src/components/BankAccounts/index.js index 52233c84b..edd882a1a 100644 --- a/src/components/BankAccounts/index.js +++ b/src/components/BankAccounts/index.js @@ -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 ( - - - - - {title} - - - {code} - - {!loading && } - + + + + {title} + + + {code} + + {!loading && } + - - - - + + + + - - + + + + + ); } diff --git a/src/components/IntersectionObserver/index.js b/src/components/IntersectionObserver/index.js new file mode 100644 index 000000000..df91bc9a6 --- /dev/null +++ b/src/components/IntersectionObserver/index.js @@ -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 ( +
+ Load Newer +
+ ); +} diff --git a/src/components/index.js b/src/components/index.js index 54cdead52..6d78330e3 100644 --- a/src/components/index.js +++ b/src/components/index.js @@ -82,6 +82,7 @@ export * from './MultiSelectTaggable'; export * from './Utils/FormatNumber'; export * from './Utils/FormatDate'; export * from './BankAccounts'; +export * from './IntersectionObserver' const Hint = FieldHint; diff --git a/src/containers/CashFlow/AccountTransactions/AccountTransactionsDataTable.js b/src/containers/CashFlow/AccountTransactions/AccountTransactionsDataTable.js index e7e5944ed..5b0f8e9aa 100644 --- a/src/containers/CashFlow/AccountTransactions/AccountTransactionsDataTable.js +++ b/src/containers/CashFlow/AccountTransactions/AccountTransactionsDataTable.js @@ -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" /> ); diff --git a/src/containers/CashFlow/AccountTransactions/AccountTransactionsDetailsBar.js b/src/containers/CashFlow/AccountTransactions/AccountTransactionsDetailsBar.js index f35454566..e72994af2 100644 --- a/src/containers/CashFlow/AccountTransactions/AccountTransactionsDetailsBar.js +++ b/src/containers/CashFlow/AccountTransactions/AccountTransactionsDetailsBar.js @@ -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) => ( )); @@ -111,7 +113,7 @@ function AccountSwitchMenuItem({ }) { return ( {name} diff --git a/src/containers/CashFlow/AccountTransactions/AccountTransactionsProvider.js b/src/containers/CashFlow/AccountTransactions/AccountTransactionsProvider.js index b3e792afe..a163ced01 100644 --- a/src/containers/CashFlow/AccountTransactions/AccountTransactionsProvider.js +++ b/src/containers/CashFlow/AccountTransactions/AccountTransactionsProvider.js @@ -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 ( + ); } diff --git a/src/containers/CashFlow/AccountTransactions/components.js b/src/containers/CashFlow/AccountTransactions/components.js index 478900057..a08aa905c 100644 --- a/src/containers/CashFlow/AccountTransactions/components.js +++ b/src/containers/CashFlow/AccountTransactions/components.js @@ -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' }, ], [], diff --git a/src/containers/CashFlow/CashFlowAccounts/CashflowAccountsGrid.js b/src/containers/CashFlow/CashFlowAccounts/CashflowAccountsGrid.js index 34b3cd022..f378f6331 100644 --- a/src/containers/CashFlow/CashFlowAccounts/CashflowAccountsGrid.js +++ b/src/containers/CashFlow/CashFlowAccounts/CashflowAccountsGrid.js @@ -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) => ( - + - + )); } @@ -52,3 +60,44 @@ export default function CashflowAccountsGrid() { ); } + +function CashflowAccountContextMenu() { + return ( + + } + text={intl.get('view_details')} + /> + + } text={intl.get('edit_account')} /> + + + } + /> + + + } + /> + + } + intent={Intent.DANGER} + /> + + ); +} + +const CashflowAccountAnchor = styled(Link)` + &, + &:hover, + &:focus, + &:active { + color: inherit; + text-decoration: none; + } +`; diff --git a/src/hooks/query/cashflowAccounts.js b/src/hooks/query/cashflowAccounts.js index 3bf25b0fd..ec87baa21 100644 --- a/src/hooks/query/cashflowAccounts.js +++ b/src/hooks/query/cashflowAccounts.js @@ -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, + }, + ); +} diff --git a/src/hooks/utils/index.js b/src/hooks/utils/index.js index dd0d41c94..d4d6152a6 100644 --- a/src/hooks/utils/index.js +++ b/src/hooks/utils/index.js @@ -6,4 +6,5 @@ export * from './useUpdateEffect'; export * from './useWatch'; export * from './useWhen'; export * from './useRequestPdf'; -export * from './useAsync'; \ No newline at end of file +export * from './useAsync'; +export * from './useIntersectionObserver'; \ No newline at end of file diff --git a/src/hooks/utils/useIntersectionObserver.js b/src/hooks/utils/useIntersectionObserver.js new file mode 100644 index 000000000..a3958131e --- /dev/null +++ b/src/hooks/utils/useIntersectionObserver.js @@ -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]); +}