refactoring: expenses landing list.

refactoring: customers landing list.
refactoring: vendors landing list.
refactoring: manual journals landing list.
This commit is contained in:
a.bouhuolia
2021-02-10 18:35:19 +02:00
parent 6e10ed0721
commit c68b4ca9ba
170 changed files with 2835 additions and 4430 deletions

View File

@@ -0,0 +1,122 @@
import React, { useCallback, useState } from 'react';
import Icon from 'components/Icon';
import {
Button,
NavbarGroup,
Classes,
NavbarDivider,
Popover,
PopoverInteractionKind,
Position,
Intent,
} from '@blueprintjs/core';
import classNames from 'classnames';
import { useHistory } from 'react-router-dom';
import { FormattedMessage as T } from 'react-intl';
import DashboardActionsBar from 'components/Dashboard/DashboardActionsBar';
import withDialogActions from 'containers/Dialog/withDialogActions';
import { If, DashboardActionViewsList } from 'components';
import { useExpensesListContext } from './ExpensesListProvider';
import withExpensesActions from './withExpensesActions';
import { compose } from 'utils';
/**
* Expenses actions bar.
*/
function ExpensesActionsBar({
//#withExpensesActions
setExpensesTableState,
}) {
const [filterCount, setFilterCount] = useState(0);
// History context.
const history = useHistory();
// Expenses list context.
const { expensesViews } = useExpensesListContext();
// Handles the new expense buttn click.
const onClickNewExpense = () => {
history.push('/expenses/new');
};
// Handle delete button click.
const handleBulkDelete = () => {
};
// Handles the tab chaning.
const handleTabChange = (viewId) => {
setExpensesTableState({
customViewId: viewId.id || null,
});
};
return (
<DashboardActionsBar>
<NavbarGroup>
<DashboardActionViewsList
resourceName={'expenses'}
views={expensesViews}
onChange={handleTabChange}
/>
<NavbarDivider />
<Button
className={Classes.MINIMAL}
icon={<Icon icon="plus" />}
text={<T id={'new_expense'} />}
onClick={onClickNewExpense}
/>
<Popover
minimal={true}
content={''}
interactionKind={PopoverInteractionKind.CLICK}
position={Position.BOTTOM_LEFT}
>
<Button
className={classNames(Classes.MINIMAL, 'button--filter', {
'has-active-filters': filterCount > 0,
})}
text="Filter"
icon={<Icon icon="filter-16" iconSize={16} />}
/>
</Popover>
<If condition={false}>
<Button
className={Classes.MINIMAL}
icon={<Icon icon="trash-16" iconSize={16} />}
text={<T id={'delete'} />}
intent={Intent.DANGER}
onClick={handleBulkDelete}
/>
</If>
<Button
className={Classes.MINIMAL}
icon={<Icon icon="print-16" iconSize={16} />}
text={<T id={'print'} />}
/>
<Button
className={Classes.MINIMAL}
icon={<Icon icon="file-import-16" iconSize={16} />}
text={<T id={'import'} />}
/>
<Button
className={Classes.MINIMAL}
icon={<Icon icon="file-export-16" iconSize={16} />}
text={<T id={'export'} />}
/>
</NavbarGroup>
</DashboardActionsBar>
);
}
export default compose(
withDialogActions,
withExpensesActions,
)(ExpensesActionsBar);

View File

@@ -0,0 +1,120 @@
import React, { useCallback } from 'react';
import classNames from 'classnames';
import { compose } from 'utils';
import { useExpensesListContext } from './ExpensesListProvider';
import { Choose } from 'components';
import { CLASSES } from 'common/classes';
import DataTable from 'components/DataTable';
import ExpensesEmptyStatus from './ExpensesEmptyStatus';
import TableSkeletonRows from 'components/Datatable/TableSkeletonRows';
import TableSkeletonHeader from 'components/Datatable/TableHeaderSkeleton';
import withDashboardActions from 'containers/Dashboard/withDashboardActions';
import withExpensesActions from './withExpensesActions';
import withAlertsActions from 'containers/Alert/withAlertActions';
import { ActionsMenu, useExpensesTableColumns } from './components';
/**
* Expenses datatable.
*/
function ExpensesDataTable({
// #withExpensesActions
setExpensesTableState,
// #withAlertsActions
openAlert,
}) {
// Expenses list context.
const {
expenses,
pagination,
isExpensesLoading,
isExpensesFetching,
isEmptyStatus
} = useExpensesListContext();
// Expenses table columns.
const columns = useExpensesTableColumns();
// Handle fetch data of manual jouranls datatable.
const handleFetchData = useCallback(
({ pageIndex, pageSize, sortBy }) => {
setExpensesTableState({
pageIndex,
pageSize,
sortBy,
});
},
[setExpensesTableState],
);
// Handle the expense publish action.
const handlePublishExpense = (expense) => {
openAlert('expense-publish', { expenseId: expense.id });
};
const handleEditExpense = (expense) => {
};
// Handle the expense delete action.
const handleDeleteExpense = (expense) => {
openAlert('expense-delete', { expenseId: expense.id });
};
return (
<div className={classNames(CLASSES.DASHBOARD_DATATABLE)}>
<Choose>
<Choose.When condition={isEmptyStatus}>
<ExpensesEmptyStatus />
</Choose.When>
<Choose.Otherwise>
<DataTable
columns={columns}
data={expenses}
loading={isExpensesLoading}
headerLoading={isExpensesLoading}
progressBarLoading={isExpensesFetching}
selectionColumn={true}
noInitialFetch={true}
sticky={true}
onFetchData={handleFetchData}
pagination={true}
manualSortBy={true}
manualPagination={true}
pagesCount={pagination.pagesCount}
autoResetSortBy={false}
autoResetPage={false}
TableLoadingRenderer={TableSkeletonRows}
TableHeaderSkeletonRenderer={TableSkeletonHeader}
ContextMenu={ActionsMenu}
payload={{
onPublish: handlePublishExpense,
onDelete: handleDeleteExpense
}}
/>
</Choose.Otherwise>
</Choose>
</div>
);
}
export default compose(
withDashboardActions,
withAlertsActions,
withExpensesActions,
)(ExpensesDataTable);

View File

@@ -0,0 +1,59 @@
import React from 'react';
import { Alignment, Navbar, NavbarGroup } from '@blueprintjs/core';
import { pick } from 'lodash';
import { DashboardViewsTabs } from 'components';
import { useExpensesListContext } from './ExpensesListProvider';
import withExpenses from './withExpenses';
import withExpensesActions from './withExpensesActions';
import { compose } from 'utils';
/**
* Expesne views tabs.
*/
function ExpenseViewTabs({
// #withExpensesActions
setExpensesTableState,
// #withExpenses
expensesTableState
}) {
// Expenses list context.
const { expensesViews } = useExpensesListContext();
// Handle the tabs change.
const handleTabChange = (viewId) => {
setExpensesTableState({
customViewId: viewId || null,
});
};
const tabs = expensesViews.map((view) => ({
...pick(view, ['name', 'id']),
}));
// Handle click a new view tab.
const handleClickNewView = () => {};
return (
<Navbar className={'navbar--dashboard-views'}>
<NavbarGroup align={Alignment.LEFT}>
<DashboardViewsTabs
customViewId={expensesTableState.customViewId}
resourceName={'expenses'}
tabs={tabs}
onNewViewTabClick={handleClickNewView}
onChange={handleTabChange}
/>
</NavbarGroup>
</Navbar>
);
}
export default compose(
withExpensesActions,
withExpenses(({ expensesTableState }) => ({ expensesTableState }))
)(ExpenseViewTabs);

View File

@@ -0,0 +1,37 @@
import React from 'react';
import { Button, Intent } from '@blueprintjs/core';
import { useHistory } from 'react-router-dom';
import { EmptyStatus } from 'components';
export default function InvoicesEmptyStatus() {
const history = useHistory();
return (
<EmptyStatus
title={"Create and manage your organization's expenses"}
description={
<p>
It is a long established fact that a reader will be distracted by the
readable content of a page when looking at its layout.
</p>
}
action={
<>
<Button
intent={Intent.PRIMARY}
large={true}
onClick={() => {
history.push('/expenses/new');
}}
>
New expense
</Button>
<Button intent={Intent.NONE} large={true}>
Learn more
</Button>
</>
}
/>
);
}

View File

@@ -0,0 +1,53 @@
import React, { useEffect } from 'react';
import { useIntl } from 'react-intl';
import DashboardPageContent from 'components/Dashboard/DashboardPageContent';
import ExpenseActionsBar from './ExpenseActionsBar';
import ExpenseViewTabs from './ExpenseViewTabs';
import ExpenseDataTable from './ExpenseDataTable';
import ExpensesAlerts from '../ExpensesAlerts';
import withDashboardActions from 'containers/Dashboard/withDashboardActions';
import withExpenses from './withExpenses';
import { compose, transformTableStateToQuery } from 'utils';
import { ExpensesListProvider } from './ExpensesListProvider';
/**
* Expenses list.
*/
function ExpensesList({
// #withDashboardActions
changePageTitle,
// #withExpenses
expensesTableState,
}) {
const { formatMessage } = useIntl();
// Changes the page title once the page mount.
useEffect(() => {
changePageTitle(formatMessage({ id: 'expenses_list' }));
}, [changePageTitle, formatMessage]);
return (
<ExpensesListProvider
query={transformTableStateToQuery(expensesTableState)}
>
<ExpenseActionsBar />
<DashboardPageContent>
<ExpenseViewTabs />
<ExpenseDataTable />
</DashboardPageContent>
<ExpensesAlerts />
</ExpensesListProvider>
);
}
export default compose(
withDashboardActions,
withExpenses(({ expensesTableState }) => ({ expensesTableState })),
)(ExpensesList);

View File

@@ -0,0 +1,55 @@
import React, { createContext } from 'react';
import DashboardInsider from 'components/Dashboard/DashboardInsider';
import { useExpenses, useResourceViews } from 'hooks/query';
import { isTableEmptyStatus } from 'utils';
const ExpensesListContext = createContext();
/**
* Accounts chart data provider.
*/
function ExpensesListProvider({ query, ...props }) {
// Fetch accounts resource views and fields.
const { data: expensesViews, isLoading: isViewsLoading } = useResourceViews(
'expenses',
);
// Fetches the expenses with pagination meta.
const {
data: { expenses, pagination, filterMeta },
isLoading: isExpensesLoading,
isFetching: isExpensesFetching,
} = useExpenses(query, { keepPreviousData: true });
// Detarmines the datatable empty status.
const isEmptyStatus = isTableEmptyStatus({
data: expenses, pagination, filterMeta,
}) && !isExpensesFetching;
// Provider payload.
const provider = {
expensesViews,
expenses,
pagination,
isViewsLoading,
isExpensesLoading,
isExpensesFetching,
isEmptyStatus
};
return (
<DashboardInsider
loading={isViewsLoading || isExpensesLoading}
name={'expenses'}
>
<ExpensesListContext.Provider value={provider} {...props} />
</DashboardInsider>
);
}
const useExpensesListContext = () =>
React.useContext(ExpensesListContext);
export { ExpensesListProvider, useExpensesListContext };

View File

@@ -0,0 +1,185 @@
import React from 'react';
import {
Intent,
Button,
Classes,
Popover,
Tooltip,
Position,
Tag,
MenuItem,
Menu,
MenuDivider,
} from '@blueprintjs/core';
import moment from 'moment';
import { FormattedMessage as T } from 'react-intl';
import { Money, Icon, If } from 'components';
import { formatMessage } from 'services/intl';
import { safeCallback } from 'utils';
/**
* Description accessor.
*/
export function DescriptionAccessor(row) {
return (
<If condition={row.description}>
<Tooltip
className={Classes.TOOLTIP_INDICATOR}
content={row.description}
position={Position.TOP}
hoverOpenDelay={250}
>
<Icon icon={'file-alt'} iconSize={16} />
</Tooltip>
</If>
);
}
/**
* Actions menu.
*/
export function ActionsMenu({
row: { original },
payload: { onPublish, onEdit, onDelete },
}) {
return (
<Menu>
<MenuItem
icon={<Icon icon="reader-18" />}
text={formatMessage({ id: 'view_details' })}
/>
<MenuDivider />
<If condition={!original.is_published}>
<MenuItem
icon={<Icon icon={'arrow-to-top'} size={16} />}
text={formatMessage({ id: 'publish_expense' })}
onClick={safeCallback(onPublish, original)}
/>
</If>
<MenuItem
icon={<Icon icon="pen-18" />}
text={formatMessage({ id: 'edit_expense' })}
onClick={safeCallback(onEdit, original)}
/>
<MenuItem
icon={<Icon icon="trash-16" iconSize={16} />}
text={formatMessage({ id: 'delete_expense' })}
intent={Intent.DANGER}
onClick={safeCallback(onDelete, original)}
/>
</Menu>
);
}
/**
* Actions cell.
*/
export function ActionsCell(props) {
return (
<Popover
content={<ActionsMenu {...props} />}
position={Position.RIGHT_BOTTOM}
>
<Button icon={<Icon icon="more-h-16" iconSize={16} />} />
</Popover>
);
}
/**
* Total amount accessor.
*/
export function TotalAmountAccessor(row) {
return <Money amount={row.total_amount} currency={'USD'} />;
}
/**
* Publish accessor.
*/
export function PublishAccessor(row) {
return row.is_published ? (
<Tag minimal={true}>
<T id={'published'} />
</Tag>
) : (
<Tag minimal={true} intent={Intent.WARNING}>
<T id={'draft'} />
</Tag>
);
}
/**
* Retrieve the expenses table columns.
*/
export function useExpensesTableColumns() {
return React.useMemo(
() => [
{
id: 'payment_date',
Header: formatMessage({ id: 'payment_date' }),
accessor: (r) => moment(r.payment_date).format('YYYY MMM DD'),
width: 140,
className: 'payment_date',
},
{
id: 'total_amount',
Header: formatMessage({ id: 'full_amount' }),
accessor: TotalAmountAccessor,
className: 'total_amount',
width: 150,
},
{
id: 'payment_account_id',
Header: formatMessage({ id: 'payment_account' }),
accessor: 'payment_account.name',
className: 'payment_account',
width: 150,
},
{
id: 'expense_account_id',
Header: formatMessage({ id: 'expense_account' }),
accessor: ExpenseAccountAccessor,
width: 160,
className: 'expense_account',
},
{
id: 'publish',
Header: formatMessage({ id: 'publish' }),
accessor: PublishAccessor,
width: 100,
className: 'publish',
},
{
id: 'description',
Header: formatMessage({ id: 'description' }),
accessor: DescriptionAccessor,
disableSorting: true,
width: 150,
className: 'description',
},
{
id: 'actions',
Header: '',
Cell: ActionsCell,
className: 'actions',
width: 50,
disableResizing: true,
},
],
[],
);
}
export function ExpenseAccountAccessor(expense) {
if (expense.categories.length === 1) {
return expense.categories[0].expense_account.name;
} else if (expense.categories.length > 1) {
const mutliCategories = expense.categories.map((category) => (
<div>
- {category.expense_account.name} ${category.amount}
</div>
));
return (
<Tooltip content={mutliCategories}>{'- Multi Categories -'}</Tooltip>
);
}
}

View File

@@ -0,0 +1,15 @@
import { connect } from 'react-redux';
import { getExpensesTableStateFactory } from 'store/expenses/expenses.selectors';
export default (mapState) => {
const getExpensesTableState = getExpensesTableStateFactory();
const mapStateToProps = (state, props) => {
const mapped = {
expensesTableState: getExpensesTableState(state, props),
};
return mapState ? mapState(mapped, state, props) : mapped;
};
return connect(mapStateToProps);
};

View File

@@ -0,0 +1,8 @@
import { connect } from 'react-redux';
import { setExpensesTableState } from 'store/expenses/expenses.actions';
const mapDispatchToProps = (dispatch) => ({
setExpensesTableState: (state) => dispatch(setExpensesTableState(state)),
});
export default connect(null, mapDispatchToProps);