WIP Version 0.0.1

This commit is contained in:
Ahmed Bouhuolia
2020-05-08 04:36:04 +02:00
parent bd7eb0eb76
commit 71cc561bb2
151 changed files with 1742 additions and 1081 deletions

View File

@@ -0,0 +1,157 @@
import React, { useMemo, useState, useCallback } from 'react';
import Icon from 'components/Icon';
import {
Button,
NavbarGroup,
Classes,
NavbarDivider,
MenuItem,
Menu,
Popover,
PopoverInteractionKind,
Position,
Intent,
} from '@blueprintjs/core';
import classNames from 'classnames';
import { useHistory } from 'react-router-dom';
import { connect } from 'react-redux';
import { If } from 'components';
import DashboardActionsBar from 'components/Dashboard/DashboardActionsBar';
import DialogConnect from 'connectors/Dialog.connector';
import FilterDropdown from 'components/FilterDropdown';
import withResourceDetail from 'containers/Resources/withResourceDetails';
import withAccountsTableActions from 'containers/Accounts/withAccountsTableActions';
import withAccounts from 'containers/Accounts/withAccounts';
import {compose} from 'utils';
function AccountsActionsBar({
openDialog,
accountsViews,
resourceFields,
addAccountsTableQueries,
selectedRows = [],
onFilterChanged,
onBulkDelete,
onBulkArchive,
}) {
const history = useHistory();
const [filterCount, setFilterCount] = useState(0);
const onClickNewAccount = () => { openDialog('account-form', {}); };
const onClickViewItem = (view) => {
history.push(view
? `/dashboard/accounts/${view.id}/custom_view` : '/dashboard/accounts');
};
const viewsMenuItems = accountsViews.map((view) => {
return (<MenuItem onClick={() => onClickViewItem(view)} text={view.name} />);
});
const hasSelectedRows = useMemo(() => selectedRows.length > 0, [selectedRows]);
const filterDropdown = FilterDropdown({
fields: resourceFields,
onFilterChange: (filterConditions) => {
setFilterCount(filterConditions.length || 0);
addAccountsTableQueries({
filter_roles: filterConditions || '',
});
onFilterChanged && onFilterChanged(filterConditions);
},
});
const handleBulkArchive = useCallback(() => {
onBulkArchive && onBulkArchive(selectedRows.map(r => r.id));
}, [onBulkArchive, selectedRows]);
const handleBulkDelete = useCallback(() => {
onBulkDelete && onBulkDelete(selectedRows.map(r => r.id));
}, [onBulkDelete, selectedRows]);
return (
<DashboardActionsBar>
<NavbarGroup>
<Popover
content={<Menu>{viewsMenuItems}</Menu>}
minimal={true}
interactionKind={PopoverInteractionKind.HOVER}
position={Position.BOTTOM_LEFT}
>
<Button
className={classNames(Classes.MINIMAL, 'button--table-views')}
icon={<Icon icon='table' />}
text='Table Views'
rightIcon={'caret-down'}
/>
</Popover>
<NavbarDivider />
<Button
className={Classes.MINIMAL}
icon={<Icon icon='plus' />}
text='New Account'
onClick={onClickNewAccount}
/>
<Popover
minimal={true}
content={filterDropdown}
interactionKind={PopoverInteractionKind.CLICK}
position={Position.BOTTOM_LEFT}>
<Button
className={classNames(Classes.MINIMAL, 'button--filter')}
text={filterCount <= 0 ? 'Filter' : `${filterCount} filters applied`}
icon={ <Icon icon="filter" /> }/>
</Popover>
<If condition={hasSelectedRows}>
<Button
className={Classes.MINIMAL}
icon={<Icon icon='archive' iconSize={15} />}
text='Archive'
onClick={handleBulkArchive}
/>
<Button
className={Classes.MINIMAL}
icon={<Icon icon='trash' iconSize={15} />}
text='Delete'
intent={Intent.DANGER}
onClick={handleBulkDelete}
/>
</If>
<Button
className={Classes.MINIMAL}
icon={<Icon icon='file-import' />}
text='Import'
/>
<Button
className={Classes.MINIMAL}
icon={<Icon icon='file-export' />}
text='Export'
/>
</NavbarGroup>
</DashboardActionsBar>
);
}
const mapStateToProps = (state, props) => ({
resourceName: 'accounts',
});
const withAccountsActionsBar = connect(mapStateToProps);
export default compose(
withAccountsActionsBar,
DialogConnect,
withAccounts,
withResourceDetail,
withAccountsTableActions,
)(AccountsActionsBar);

View File

@@ -0,0 +1,267 @@
import React, { useEffect, useState, useCallback } from 'react';
import {
Route,
Switch,
} from 'react-router-dom';
import { Alert, Intent } from '@blueprintjs/core';
import { useQuery } from 'react-query'
import AppToaster from 'components/AppToaster';
import DashboardPageContent from 'components/Dashboard/DashboardPageContent';
import DashboardInsider from 'components/Dashboard/DashboardInsider';
import AccountsViewsTabs from 'containers/Accounts/AccountsViewsTabs';
import AccountsDataTable from 'containers/Accounts/AccountsDataTable';
import DashboardActionsBar from 'containers/Accounts/AccountsActionsBar';
import withDashboardActions from 'containers/Dashboard/withDashboard';
import withResourceActions from 'containers/Resources/withResourcesActions';
import withAccountsActions from 'containers/Accounts/withAccountsActions';
import withAccountsTableActions from 'containers/Accounts/withAccountsTableActions';
import withViewsActions from 'containers/Views/withViewsActions';
import withAccounts from 'containers/Accounts/withAccounts';
import { compose } from 'utils';
function AccountsChart({
// #withDashboard
changePageTitle,
// #withAccountsActions
requestDeleteAccount,
requestInactiveAccount,
// #withViewsActions
requestFetchResourceViews,
// #withResourceActions
requestFetchResourceFields,
// #withAccountsTableActions
requestFetchAccountsTable,
requestDeleteBulkAccounts,
addAccountsTableQueries,
// #withAccounts
accountsTableQuery,
}) {
const [deleteAccount, setDeleteAccount] = useState(false);
const [inactiveAccount, setInactiveAccount] = useState(false);
const [bulkDelete, setBulkDelete] = useState(false);
const [selectedRows, setSelectedRows] = useState([]);
const [tableLoading, setTableLoading] = useState(false);
// Fetch accounts resource views and fields.
const fetchHook = useQuery('resource-accounts', () => {
return Promise.all([
requestFetchResourceViews('accounts'),
requestFetchResourceFields('accounts'),
]);
});
// Fetch accounts list according to the given custom view id.
const fetchAccountsHook = useQuery(['accounts-table', accountsTableQuery],
() => requestFetchAccountsTable());
useEffect(() => {
changePageTitle('Chart of Accounts');
}, [changePageTitle]);
// Handle click and cancel/confirm account delete
const handleDeleteAccount = (account) => { setDeleteAccount(account); };
// handle cancel delete account alert.
const handleCancelAccountDelete = useCallback(() => { setDeleteAccount(false); }, []);
// Handle confirm account delete
const handleConfirmAccountDelete = useCallback(() => {
requestDeleteAccount(deleteAccount.id).then(() => {
setDeleteAccount(false);
AppToaster.show({ message: 'the_account_has_been_deleted' });
}).catch(errors => {
setDeleteAccount(false);
if (errors.find((e) => e.type === 'ACCOUNT.PREDEFINED')) {
AppToaster.show({
message: 'cannot_delete_predefined_account',
intent: Intent.DANGER,
});
}
if (errors.find((e) => e.type === 'ACCOUNT.HAS.ASSOCIATED.TRANSACTIONS')) {
AppToaster.show({
message: 'cannot_delete_account_has_associated_transactions'
});
}
});
}, [deleteAccount, requestDeleteAccount]);
// Handle cancel/confirm account inactive.
const handleInactiveAccount = useCallback((account) => {
setInactiveAccount(account);
}, []);
// Handle cancel inactive account alert.
const handleCancelInactiveAccount = useCallback(() => {
setInactiveAccount(false);
}, []);
// Handle confirm account activation.
const handleConfirmAccountActive = useCallback(() => {
requestInactiveAccount(inactiveAccount.id).then(() => {
setInactiveAccount(false);
requestFetchAccountsTable();
AppToaster.show({ message: 'the_account_has_been_inactivated' });
});
}, [inactiveAccount, requestFetchAccountsTable, requestInactiveAccount]);
const handleEditAccount = (account) => {
};
const handleRestoreAccount = (account) => {
};
const handleBulkDelete = useCallback((accountsIds) => {
setBulkDelete(accountsIds);
}, [setBulkDelete]);
const handleConfirmBulkDelete = useCallback(() => {
requestDeleteBulkAccounts(bulkDelete).then(() => {
setBulkDelete(false);
AppToaster.show({ message: 'the_accounts_have_been_deleted' });
}).catch((error) => {
setBulkDelete(false);
});
}, [requestDeleteBulkAccounts, bulkDelete]);
const handleCancelBulkDelete = useCallback(() => {
setBulkDelete(false);
}, []);
const handleBulkArchive = useCallback((accounts) => {
}, []);
// Handle selected rows change.
const handleSelectedRowsChange = useCallback((accounts) => {
setSelectedRows(accounts);
}, [setSelectedRows]);
// Refetches accounts data table when current custom view changed.
const handleFilterChanged = useCallback(() => {
fetchAccountsHook.refetch();
}, [fetchAccountsHook]);
// Refetch accounts data table when current custom view changed.
const handleViewChanged = useCallback(async () => {
setTableLoading(true);
}, [fetchAccountsHook]);
useEffect(() => {
if (tableLoading && !fetchAccountsHook.isFetching) {
setTableLoading(false);
}
}, [tableLoading, fetchAccountsHook.isFetching]);
// Handle fetch data of accounts datatable.
const handleFetchData = useCallback(({ pageIndex, pageSize, sortBy }) => {
addAccountsTableQueries({
...(sortBy.length > 0) ? {
column_sort_by: sortBy[0].id,
sort_order: sortBy[0].desc ? 'desc' : 'asc',
} : {},
});
fetchAccountsHook.refetch();
}, [fetchAccountsHook, addAccountsTableQueries]);
return (
<DashboardInsider
loading={fetchHook.isFetching}
name={'accounts-chart'}>
<DashboardActionsBar
selectedRows={selectedRows}
onFilterChanged={handleFilterChanged}
onBulkDelete={handleBulkDelete}
onBulkArchive={handleBulkArchive} />
<DashboardPageContent>
<Switch>
<Route
exact={true}
path={[
'/dashboard/accounts/:custom_view_id/custom_view',
'/dashboard/accounts',
]}>
<AccountsViewsTabs
onViewChanged={handleViewChanged} />
<AccountsDataTable
onDeleteAccount={handleDeleteAccount}
onInactiveAccount={handleInactiveAccount}
onRestoreAccount={handleRestoreAccount}
onEditAccount={handleEditAccount}
onFetchData={handleFetchData}
onSelectedRowsChange={handleSelectedRowsChange}
loading={tableLoading} />
</Route>
</Switch>
<Alert
cancelButtonText="Cancel"
confirmButtonText="Move to Trash"
icon="trash"
intent={Intent.DANGER}
isOpen={deleteAccount}
onCancel={handleCancelAccountDelete}
onConfirm={handleConfirmAccountDelete}>
<p>
Are you sure you want to move <b>filename</b> to Trash? You will be able to restore it later,
but it will become private to you.
</p>
</Alert>
<Alert
cancelButtonText="Cancel"
confirmButtonText="Inactivate"
icon="trash"
intent={Intent.WARNING}
isOpen={inactiveAccount}
onCancel={handleCancelInactiveAccount}
onConfirm={handleConfirmAccountActive}>
<p>
Are you sure you want to move <b>filename</b> to Trash? You will be able to restore it later,
but it will become private to you.
</p>
</Alert>
<Alert
cancelButtonText="Cancel"
confirmButtonText="Delete"
icon="trash"
intent={Intent.DANGER}
isOpen={bulkDelete}
onCancel={handleCancelBulkDelete}
onConfirm={handleConfirmBulkDelete}>
<p>
Are you sure you want to move <b>filename</b> to Trash? You will be able to restore it later,
but it will become private to you.
</p>
</Alert>
</DashboardPageContent>
</DashboardInsider>
);
}
export default compose(
withAccountsActions,
withAccountsTableActions,
withViewsActions,
withResourceActions,
withDashboardActions,
withAccounts,
)(AccountsChart);

View File

@@ -0,0 +1,191 @@
import React, { useEffect, useCallback, useState, useMemo } from 'react';
import {
Button,
Popover,
Menu,
MenuItem,
MenuDivider,
Position,
Classes,
Tooltip,
} from '@blueprintjs/core';
import Icon from 'components/Icon';
import { compose } from 'utils';
import DialogConnect from 'connectors/Dialog.connector';
import LoadingIndicator from 'components/LoadingIndicator';
import DataTable from 'components/DataTable';
import Money from 'components/Money';
import { useUpdateEffect } from 'hooks';
import withDashboardActions from 'containers/Dashboard/withDashboard';
import withAccountsActions from 'containers/Accounts/withAccountsActions';
import withAccounts from 'containers/Accounts/withAccounts';
import {If} from 'components';
function AccountsDataTable({
// # withAccounts
accounts,
accountsLoading,
// # withDialog.
openDialog,
// own properties
loading,
onFetchData,
onSelectedRowsChange,
onDeleteAccount,
onInactiveAccount,
}) {
const [initialMount, setInitialMount] = useState(false);
useUpdateEffect(() => {
if (!accountsLoading) {
setInitialMount(true);
}
}, [accountsLoading, setInitialMount]);
const handleEditAccount = useCallback((account) => () => {
openDialog('account-form', { action: 'edit', id: account.id });
}, [openDialog]);
const handleNewParentAccount = useCallback((account) => {
openDialog('account-form', { action: 'new_child', id: account.id });
}, [openDialog]);
const actionMenuList = useCallback((account) => (
<Menu>
<MenuItem text='View Details' />
<MenuDivider />
<MenuItem
text='Edit Account'
onClick={handleEditAccount(account)} />
<MenuItem
text='New Account'
onClick={() => handleNewParentAccount(account)} />
<MenuDivider />
<MenuItem
text='Inactivate Account'
onClick={() => onInactiveAccount(account)} />
<MenuItem
text='Delete Account'
onClick={() => onDeleteAccount(account)} />
</Menu>
), [handleEditAccount, onDeleteAccount, onInactiveAccount]);
const columns = useMemo(() => [
{
id: 'name',
Header: 'Account Name',
accessor: row => {
return (row.description) ?
(<Tooltip
className={Classes.TOOLTIP_INDICATOR}
content={row.description}
position={Position.RIGHT_TOP}
hoverOpenDelay={500}>
{ row.name }
</Tooltip>) : row.name;
},
className: 'account_name',
width: 300,
},
{
id: 'code',
Header: 'Code',
accessor: 'code',
className: 'code',
width: 100,
},
{
id: 'type',
Header: 'Type',
accessor: 'type.name',
className: 'type',
width: 120,
},
{
id: 'normal',
Header: 'Normal',
Cell: ({ cell }) => {
const account = cell.row.original;
const type = account.type ? account.type.normal : '';
const arrowDirection = type === 'credit' ? 'down' : 'up';
return (<Icon icon={`arrow-${arrowDirection}`} />);
},
className: 'normal',
width: 75,
},
{
id: 'balance',
Header: 'Balance',
Cell: ({ cell }) => {
const account = cell.row.original;
const {balance = null} = account;
return (balance) ?
(<span>
<Money amount={balance.amount} currency={balance.currency_code} />
</span>) :
(<span class="placeholder">--</span>);
},
width: 150,
},
{
id: 'actions',
Header: '',
Cell: ({ cell }) => (
<Popover
content={actionMenuList(cell.row.original)}
position={Position.RIGHT_TOP}>
<Button icon={<Icon icon='ellipsis-h' />} />
</Popover>
),
className: 'actions',
width: 50,
}
], [actionMenuList]);
const selectionColumn = useMemo(() => ({
minWidth: 50,
width: 50,
maxWidth: 50,
}), [])
const handleDatatableFetchData = useCallback((...params) => {
onFetchData && onFetchData(...params);
}, []);
const handleSelectedRowsChange = useCallback((selectedRows) => {
onSelectedRowsChange && onSelectedRowsChange(selectedRows.map(s => s.original));
}, [onSelectedRowsChange]);
return (
<LoadingIndicator loading={loading} mount={false}>
<If condition={loading}>
asdasdsadsa
</If>
<DataTable
noInitialFetch={true}
columns={columns}
data={accounts}
onFetchData={handleDatatableFetchData}
manualSortBy={true}
selectionColumn={selectionColumn}
expandable={true}
treeGraph={true}
onSelectedRowsChange={handleSelectedRowsChange}
loading={accountsLoading && !initialMount}
spinnerProps={{size: 30}} />
</LoadingIndicator>
);
}
export default compose(
DialogConnect,
withDashboardActions,
withAccountsActions,
withAccounts,
)(AccountsDataTable);

View File

@@ -0,0 +1,132 @@
import React, {useEffect, useCallback} from 'react';
import { useHistory } from 'react-router';
import { connect } from 'react-redux';
import {
Alignment,
Navbar,
NavbarGroup,
Tabs,
Tab,
Button
} from '@blueprintjs/core';
import { useParams, withRouter } from 'react-router-dom';
import Icon from 'components/Icon';
import { Link } from 'react-router-dom';
import { compose } from 'utils';
import {useUpdateEffect} from 'hooks';
import withDashboard from 'containers/Dashboard/withDashboard';
import withAccounts from 'containers/Accounts/withAccounts';
import withAccountsTableActions from 'containers/Accounts/withAccountsTableActions';
import withViewDetail from 'containers/Views/withViewDetails';
function AccountsViewsTabs({
// #withViewDetail
viewId,
viewItem,
// #withAccounts
accountsViews,
// #withAccountsTableActions
addAccountsTableQueries,
changeAccountsCurrentView,
// #withDashboard
setTopbarEditView,
changePageSubtitle,
// props
customViewChanged,
onViewChanged,
}) {
const history = useHistory();
const { custom_view_id: customViewId = null } = useParams();
useEffect(() => {
changeAccountsCurrentView(customViewId || -1);
setTopbarEditView(customViewId);
changePageSubtitle((customViewId && viewItem) ? viewItem.name : '');
addAccountsTableQueries({
custom_view_id: customViewId,
});
return () => {
setTopbarEditView(null);
changePageSubtitle('');
changeAccountsCurrentView(null)
};
}, [customViewId]);
useUpdateEffect(() => {
onViewChanged && onViewChanged(customViewId);
}, [customViewId]);
// Handle click a new view tab.
const handleClickNewView = () => {
setTopbarEditView(null);
history.push('/dashboard/custom_views/accounts/new');
};
// Handle view tab link click.
const handleViewLinkClick = () => {
setTopbarEditView(customViewId);
};
const tabs = accountsViews.map((view) => {
const baseUrl = '/dashboard/accounts';
const link = (
<Link
to={`${baseUrl}/${view.id}/custom_view`}
onClick={handleViewLinkClick}
>{ view.name }</Link>
);
return <Tab id={`custom_view_${view.id}`} title={link} />;
});
return (
<Navbar className='navbar--dashboard-views'>
<NavbarGroup align={Alignment.LEFT}>
<Tabs
id='navbar'
large={true}
selectedTabId={customViewId ? `custom_view_${customViewId}` : 'all'}
className='tabs--dashboard-views'
>
<Tab
id={'all'}
title={<Link to={`/dashboard/accounts`}>All</Link>}
onClick={handleViewLinkClick}
/>
{ tabs }
<Button
className='button--new-view'
icon={<Icon icon='plus' />}
onClick={handleClickNewView}
minimal={true}
/>
</Tabs>
</NavbarGroup>
</Navbar>
);
}
const mapStateToProps = (state, ownProps) => ({
// Mapping view id from matched route params.
viewId: ownProps.match.params.custom_view_id,
});
const withAccountsViewsTabs = connect(mapStateToProps);
export default compose(
withRouter,
withAccountsViewsTabs,
withDashboard,
withAccounts,
withAccountsTableActions,
withViewDetail
)(AccountsViewsTabs);

View File

@@ -0,0 +1,10 @@
import { connect } from 'react-redux';
import {
getItemById
} from 'store/selectors';
const mapStateToProps = (state, props) => ({
account: getItemById(state.accounts.items, props.accountId),
});
export default connect(mapStateToProps);

View File

@@ -0,0 +1,19 @@
import { connect } from 'react-redux';
import {
getAccountsItems,
} from 'store/accounts/accounts.selectors';
import {
getResourceViews,
} from 'store/customViews/customViews.selectors';
const mapStateToProps = (state, props) => ({
accountsViews: getResourceViews(state, 'accounts'),
accounts: getAccountsItems(state, state.accounts.currentViewId),
accountsTypes: state.accounts.accountsTypes,
accountsTableQuery: state.accounts.tableQuery,
accountsLoading: state.accounts.loading,
accountErrors: state.accounts.errors,
});
export default connect(mapStateToProps);

View File

@@ -0,0 +1,22 @@
import { connect } from 'react-redux';
import {
fetchAccountTypes,
fetchAccountsList,
deleteAccount,
inactiveAccount,
submitAccount,
fetchAccount,
deleteBulkAccounts,
} from 'store/accounts/accounts.actions';
const mapActionsToProps = (dispatch) => ({
requestFetchAccounts: (query) => dispatch(fetchAccountsList({ query })),
requestFetchAccountTypes: () => dispatch(fetchAccountTypes()),
requestSubmitAccount: ({ form }) => dispatch(submitAccount({ form })),
requestDeleteAccount: (id) => dispatch(deleteAccount({ id })),
requestInactiveAccount: (id) => dispatch(inactiveAccount({ id })),
requestFetchAccount: (id) => dispatch(fetchAccount({ id })),
requestDeleteBulkAccounts: (ids) => dispatch(deleteBulkAccounts({ ids })),
});
export default connect(null, mapActionsToProps);

View File

@@ -0,0 +1,24 @@
import { connect } from 'react-redux';
import t from 'store/types';
import {
fetchAccountsTable,
} from 'store/accounts/accounts.actions';
const mapActionsToProps = (dispatch) => ({
requestFetchAccountsTable: (query = {}) => dispatch(fetchAccountsTable({ query: { ...query } })),
changeAccountsCurrentView: (id) => dispatch({
type: t.ACCOUNTS_SET_CURRENT_VIEW,
currentViewId: parseInt(id, 10),
}),
setAccountsTableQuery: (key, value) => dispatch({
type: 'ACCOUNTS_TABLE_QUERY_SET', key, value,
}),
addAccountsTableQueries: (queries) => dispatch({
type: 'ACCOUNTS_TABLE_QUERIES_ADD', queries,
}),
setSelectedRowsAccounts: (ids) => dispatch({
type: t.ACCOUNTS_SELECTED_ROWS_SET, payload: { ids },
}),
});
export default connect(null, mapActionsToProps);