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

@@ -1,285 +0,0 @@
import React, { useCallback, useMemo } from 'react';
import {
Intent,
Button,
Classes,
Popover,
Tooltip,
Menu,
MenuItem,
MenuDivider,
Position,
Tag,
} from '@blueprintjs/core';
import { FormattedMessage as T, useIntl } from 'react-intl';
import moment from 'moment';
import classNames from 'classnames';
import Icon from 'components/Icon';
import { compose, saveInvoke } from 'utils';
import { useExpensesListContext } from './ExpensesListProvider';
import { If, Money, Choose } from 'components';
import { CLASSES } from 'common/classes';
import DataTable from 'components/DataTable';
import ExpensesEmptyStatus from './ExpensesEmptyStatus';
import withDialogActions from 'containers/Dialog/withDialogActions';
import withDashboardActions from 'containers/Dashboard/withDashboardActions';
import withExpensesActions from 'containers/Expenses/withExpensesActions';
import TableSkeletonRows from 'components/Datatable/TableSkeletonRows';
/**
* Expenses datatable.
*/
function ExpensesDataTable({
// #withExpensesActions
addExpensesTableQueries,
// #ownProps
onEditExpense,
onDeleteExpense,
onPublishExpense,
onSelectedRowsChange,
}) {
const { formatMessage } = useIntl();
const { expenses, isExpensesLoading } = useExpensesListContext();
// Handle fetch data of manual jouranls datatable.
const handleFetchData = useCallback(
({ pageIndex, pageSize, sortBy }) => {
addExpensesTableQueries({
...(sortBy.length > 0
? {
column_sort_by: sortBy[0].id,
sort_order: sortBy[0].desc ? 'desc' : 'asc',
}
: {}),
page_size: pageSize,
page: pageIndex + 1,
});
},
[addExpensesTableQueries],
);
const handlePublishExpense = useCallback(
(expense) => () => {
saveInvoke(onPublishExpense, expense);
},
[onPublishExpense],
);
const handleEditExpense = useCallback(
(expense) => () => {
saveInvoke(onEditExpense, expense);
},
[onEditExpense],
);
const handleDeleteExpense = useCallback(
(expense) => () => {
saveInvoke(onDeleteExpense, expense);
},
[onDeleteExpense],
);
const actionMenuList = useCallback(
(expense) => (
<Menu>
<MenuItem
icon={<Icon icon="reader-18" />}
text={formatMessage({ id: 'view_details' })}
/>
<MenuDivider />
<If condition={!expense.is_published}>
<MenuItem
icon={<Icon icon={'arrow-to-top'} size={16} />}
text={formatMessage({ id: 'publish_expense' })}
onClick={handlePublishExpense(expense)}
/>
</If>
<MenuItem
icon={<Icon icon="pen-18" />}
text={formatMessage({ id: 'edit_expense' })}
onClick={handleEditExpense(expense)}
/>
<MenuItem
icon={<Icon icon="trash-16" iconSize={16} />}
text={formatMessage({ id: 'delete_expense' })}
intent={Intent.DANGER}
onClick={handleDeleteExpense(expense)}
/>
</Menu>
),
[
handleEditExpense,
handleDeleteExpense,
handlePublishExpense,
formatMessage,
],
);
const onRowContextMenu = useCallback(
(cell) => {
return actionMenuList(cell.row.original);
},
[actionMenuList],
);
const 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>
);
}
};
const columns = 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: (r) => (
<Money amount={r.total_amount} currency={r.currency_code} />
),
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: (r) => {
return r.is_published ? (
<Tag minimal={true}>
<T id={'published'} />
</Tag>
) : (
<Tag minimal={true} intent={Intent.WARNING}>
<T id={'draft'} />
</Tag>
);
},
width: 100,
className: 'publish',
},
{
id: 'description',
Header: formatMessage({ id: 'description' }),
accessor: (row) => (
<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>
),
disableSorting: true,
width: 150,
className: 'description',
},
{
id: 'actions',
Header: '',
Cell: ({ cell }) => (
<Popover
content={actionMenuList(cell.row.original)}
position={Position.RIGHT_BOTTOM}
>
<Button icon={<Icon icon="more-h-16" iconSize={16} />} />
</Popover>
),
className: 'actions',
width: 50,
disableResizing: true,
},
],
[actionMenuList, formatMessage],
);
const handleSelectedRowsChange = useCallback(
(selectedRows) => {
saveInvoke(
onSelectedRowsChange,
selectedRows.map((s) => s.original),
);
},
[onSelectedRowsChange],
);
return (
<div className={classNames(CLASSES.DASHBOARD_DATATABLE)}>
<Choose>
<Choose.When condition={false}>
<ExpensesEmptyStatus />
</Choose.When>
<Choose.Otherwise>
<DataTable
columns={columns}
data={expenses}
loading={isExpensesLoading}
manualSortBy={true}
selectionColumn={true}
noInitialFetch={true}
sticky={true}
onSelectedRowsChange={handleSelectedRowsChange}
onFetchData={handleFetchData}
rowContextMenu={onRowContextMenu}
TableLoadingRenderer={TableSkeletonRows}
pagination={true}
// pagesCount={expensesPagination.pagesCount}
autoResetSortBy={false}
autoResetPage={false}
// initialPageSize={expensesTableQuery.page_size}
// initialPageIndex={expensesTableQuery.page - 1}
/>
</Choose.Otherwise>
</Choose>
</div>
);
}
export default compose(
withDialogActions,
withDashboardActions,
withExpensesActions,
)(ExpensesDataTable);

View File

@@ -20,7 +20,6 @@ import withMediaActions from 'containers/Media/withMediaActions';
import withSettings from 'containers/Settings/withSettings';
import AppToaster from 'components/AppToaster';
import Dragzone from 'components/Dragzone';
import {
CreateExpenseFormSchema,
EditExpenseFormSchema,
@@ -52,10 +51,6 @@ const defaultInitialValues = {
* Expense form.
*/
function ExpenseForm({
// #withMedia
requestSubmitMedia,
requestDeleteMedia,
// #withDashboard
changePageTitle,
@@ -210,7 +205,7 @@ function ExpenseForm({
);
}
export default compose(
export default compose(
withDashboardActions,
withMediaActions,
withSettings(({ organizationSettings, expenseSettings }) => ({

View File

@@ -16,76 +16,8 @@ import {
InputGroupCell,
} from 'components/DataTableCells';
import { useExpenseFormContext } from './ExpenseFormPageProvider';
import { useExpenseFormTableColumns } from './components';
const ExpenseCategoryHeaderCell = () => {
return (
<>
<T id={'expense_category'} />
<Hint />
</>
);
};
// Actions cell renderer.
const ActionsCellRenderer = ({
row: { index },
column: { id },
cell: { value: initialValue },
data,
payload,
}) => {
if (data.length <= index + 1) {
return '';
}
const onClickRemoveRole = () => {
payload.removeRow(index);
};
return (
<Tooltip content={<T id={'remove_the_line'} />} position={Position.LEFT}>
<Button
icon={<Icon icon="times-circle" iconSize={14} />}
iconSize={14}
className="ml2"
minimal={true}
intent={Intent.DANGER}
onClick={onClickRemoveRole}
/>
</Tooltip>
);
};
// Total text cell renderer.
const TotalExpenseCellRenderer = (chainedComponent) => (props) => {
if (props.data.length <= props.row.index + 1) {
return (
<span>
<T id={'total_currency'} values={{ currency: 'USD' }} />
</span>
);
}
return chainedComponent(props);
};
const NoteCellRenderer = (chainedComponent) => (props) => {
if (props.data.length === props.row.index + 1) {
return '';
}
return chainedComponent(props);
};
const TotalAmountCellRenderer = (chainedComponent, type) => (props) => {
if (props.data.length === props.row.index + 1) {
const total = props.data.reduce((total, entry) => {
const amount = parseInt(entry[type], 10);
const computed = amount ? total + amount : total;
return computed;
}, 0);
return <span>{formattedAmount(total, 'USD')}</span>;
}
return chainedComponent(props);
};
export default function ExpenseTable({
// #ownPorps
@@ -110,55 +42,7 @@ export default function ExpenseTable({
const tableRows = useMemo(() => [...rows, { rowType: 'total' }], [rows]);
// Memorized data table columns.
const columns = useMemo(
() => [
{
Header: '#',
accessor: 'index',
Cell: ({ row: { index } }) => <span>{index + 1}</span>,
className: 'index',
width: 40,
disableResizing: true,
disableSortBy: true,
},
{
Header: ExpenseCategoryHeaderCell,
id: 'expense_account_id',
accessor: 'expense_account_id',
Cell: TotalExpenseCellRenderer(AccountsListFieldCell),
className: 'expense_account_id',
disableSortBy: true,
width: 40,
filterAccountsByRootType: ['expense'],
},
{
Header: formatMessage({ id: 'amount_currency' }, { currency: 'USD' }),
accessor: 'amount',
Cell: TotalAmountCellRenderer(MoneyFieldCell, 'amount'),
disableSortBy: true,
width: 40,
className: 'amount',
},
{
Header: formatMessage({ id: 'description' }),
accessor: 'description',
Cell: NoteCellRenderer(InputGroupCell),
disableSortBy: true,
className: 'description',
width: 100,
},
{
Header: '',
accessor: 'action',
Cell: ActionsCellRenderer,
className: 'actions',
disableSortBy: true,
disableResizing: true,
width: 45,
},
],
[formatMessage],
);
const columns = useExpenseFormTableColumns();
// Handles update datatable data.
const handleUpdateData = useCallback(

View File

@@ -36,24 +36,9 @@ function ExpenseFormPage({
};
}, [resetSidebarPreviousExpand, setSidebarShrink, setDashboardBackLink]);
const handleFormSubmit = useCallback(
(payload) => {
payload.redirect && history.push('/expenses-list');
},
[history],
);
const handleCancel = useCallback(() => {
history.goBack();
}, [history]);
return (
<ExpenseFormPageProvider expenseId={id}>
<ExpenseForm
onFormSubmit={handleFormSubmit}
expenseId={id}
onCancelForm={handleCancel}
/>
<ExpenseForm />
</ExpenseFormPageProvider>
);
}

View File

@@ -0,0 +1,132 @@
const ExpenseCategoryHeaderCell = () => {
return (
<>
<T id={'expense_category'} />
<Hint />
</>
);
};
// Actions cell renderer.
const ActionsCellRenderer = ({
row: { index },
column: { id },
cell: { value: initialValue },
data,
payload,
}) => {
if (data.length <= index + 1) {
return '';
}
const onClickRemoveRole = () => {
payload.removeRow(index);
};
return (
<Tooltip content={<T id={'remove_the_line'} />} position={Position.LEFT}>
<Button
icon={<Icon icon="times-circle" iconSize={14} />}
iconSize={14}
className="ml2"
minimal={true}
intent={Intent.DANGER}
onClick={onClickRemoveRole}
/>
</Tooltip>
);
};
// Total text cell renderer.
const TotalExpenseCellRenderer = (chainedComponent) => (props) => {
if (props.data.length <= props.row.index + 1) {
return (
<span>
<T id={'total_currency'} values={{ currency: 'USD' }} />
</span>
);
}
return chainedComponent(props);
};
/**
* Note cell renderer.
*/
const NoteCellRenderer = (chainedComponent) => (props) => {
if (props.data.length === props.row.index + 1) {
return '';
}
return chainedComponent(props);
};
/**
* Total amount cell renderer.
*/
const TotalAmountCellRenderer = (chainedComponent, type) => (props) => {
if (props.data.length === props.row.index + 1) {
const total = props.data.reduce((total, entry) => {
const amount = parseInt(entry[type], 10);
const computed = amount ? total + amount : total;
return computed;
}, 0);
return <span>{formattedAmount(total, 'USD')}</span>;
}
return chainedComponent(props);
};
export function useExpenseFormTableColumns() {
return React.useMemo(
() => [
{
Header: '#',
accessor: 'index',
Cell: ({ row: { index } }) => <span>{index + 1}</span>,
className: 'index',
width: 40,
disableResizing: true,
disableSortBy: true,
},
{
Header: ExpenseCategoryHeaderCell,
id: 'expense_account_id',
accessor: 'expense_account_id',
Cell: TotalExpenseCellRenderer(AccountsListFieldCell),
className: 'expense_account_id',
disableSortBy: true,
width: 40,
filterAccountsByRootType: ['expense'],
},
{
Header: formatMessage({ id: 'amount_currency' }, { currency: 'USD' }),
accessor: 'amount',
Cell: TotalAmountCellRenderer(MoneyFieldCell, 'amount'),
disableSortBy: true,
width: 40,
className: 'amount',
},
{
Header: formatMessage({ id: 'description' }),
accessor: 'description',
Cell: NoteCellRenderer(InputGroupCell),
disableSortBy: true,
className: 'description',
width: 100,
},
{
Header: '',
accessor: 'action',
Cell: ActionsCellRenderer,
className: 'actions',
disableSortBy: true,
disableResizing: true,
width: 45,
},
],
[formatMessage],
)
}

View File

@@ -1,17 +1,15 @@
import React from 'react';
import ExpenseDeleteAlert from 'alerts/expenses/ExpenseDeleteAlert';
import ExpensePublishAlert from 'alerts/expenses/ExpensePublishAlert';
import ExpenseDeleteAlert from 'containers/Alerts/Expenses/ExpenseDeleteAlert';
import ExpensePublishAlert from 'containers/Alerts/Expenses/ExpensePublishAlert';
/**
* Accounts alert.
*/
export default function ExpensesAlerts({
}) {
export default function ExpensesAlerts({}) {
return (
<div class="expenses-alerts">
<ExpenseDeleteAlert name={'expense-delete'} />
<ExpensePublishAlert name={'expense-publish'} />
</div>
)
}
);
}

View File

@@ -1,40 +0,0 @@
import React from 'react';
function DatatableEmptyState({
title,
description,
newButtonText,
newButtonUrl,
learnMoreButtonText,
learnMoreButtonUrl,
}) {
return (
<div class={'datatable-empty-state'}>
<h1 class={CLASSES.DATATABLE_EMPTY_STATE_TITLE}>
{ title }
</h1>
</div>
)
}
export default function ExpensesEmptyState({
}) {
return (
<DatatableEmptyState
title={''}
description={''}
newButtonText={''}
newButtonUrl={''}
learnMoreButtonText={''}
learnMoreButtonUrl={''}
/>
);
}

View File

@@ -14,14 +14,13 @@ import classNames from 'classnames';
import { useHistory } from 'react-router-dom';
import { FormattedMessage as T } from 'react-intl';
import FilterDropdown from 'components/FilterDropdown';
import DashboardActionsBar from 'components/Dashboard/DashboardActionsBar';
import withDialogActions from 'containers/Dialog/withDialogActions';
import { If, DashboardActionViewsList } from 'components';
import { useExpensesListContext } from './ExpensesListProvider';
import withExpensesActions from 'containers/Expenses/withExpensesActions';
import withExpensesActions from './withExpensesActions';
import { compose } from 'utils';
@@ -30,25 +29,30 @@ import { compose } from 'utils';
*/
function ExpensesActionsBar({
//#withExpensesActions
addExpensesTableQueries,
setExpensesTableState,
}) {
const [filterCount, setFilterCount] = useState(0);
// History context.
const history = useHistory();
// Expenses list context.
const { expensesViews } = useExpensesListContext();
const onClickNewExpense = useCallback(() => {
// Handles the new expense buttn click.
const onClickNewExpense = () => {
history.push('/expenses/new');
}, [history]);
};
// Handle delete button click.
const handleBulkDelete = () => {
};
// Handles the tab chaning.
const handleTabChange = (viewId) => {
addExpensesTableQueries({
custom_view_id: viewId.id || null,
setExpensesTableState({
customViewId: viewId.id || null,
});
};
return (

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

@@ -1,12 +1,11 @@
import React from 'react';
import { Alignment, Navbar, NavbarGroup } from '@blueprintjs/core';
import { useParams } from 'react-router-dom';
import { pick } from 'lodash';
import { DashboardViewsTabs } from 'components';
import { useExpensesListContext } from './ExpensesListProvider';
import withExpenses from './withExpenses';
import withExpensesActions from './withExpensesActions';
import { compose } from 'utils';
@@ -16,15 +15,18 @@ import { compose } from 'utils';
*/
function ExpenseViewTabs({
// #withExpensesActions
addExpensesTableQueries,
setExpensesTableState,
// #withExpenses
expensesTableState
}) {
// Expenses list context.
const { expensesViews } = useExpensesListContext();
const { custom_view_id: customViewId = null } = useParams();
// Handle the tabs change.
const handleTabChange = (viewId) => {
addExpensesTableQueries({
custom_view_id: viewId || null,
setExpensesTableState({
customViewId: viewId || null,
});
};
@@ -39,7 +41,7 @@ function ExpenseViewTabs({
<Navbar className={'navbar--dashboard-views'}>
<NavbarGroup align={Alignment.LEFT}>
<DashboardViewsTabs
initialViewId={customViewId}
customViewId={expensesTableState.customViewId}
resourceName={'expenses'}
tabs={tabs}
onNewViewTabClick={handleClickNewView}
@@ -53,4 +55,5 @@ function ExpenseViewTabs({
export default compose(
withExpensesActions,
withExpenses(({ expensesTableState }) => ({ expensesTableState }))
)(ExpenseViewTabs);

View File

@@ -3,13 +3,15 @@ import React, { useEffect } from 'react';
import { useIntl } from 'react-intl';
import DashboardPageContent from 'components/Dashboard/DashboardPageContent';
import ExpenseActionsBar from 'containers/Expenses/ExpenseActionsBar';
import ExpensesViewPage from './ExpensesViewPage';
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 'containers/Expenses/withExpenses';
import withExpenses from './withExpenses';
import { compose } from 'utils';
import { compose, transformTableStateToQuery } from 'utils';
import { ExpensesListProvider } from './ExpensesListProvider';
/**
@@ -20,26 +22,32 @@ function ExpensesList({
changePageTitle,
// #withExpenses
expensesTableQuery,
expensesTableState,
}) {
const { formatMessage } = useIntl();
// Changes the page title once the page mount.
useEffect(() => {
changePageTitle(formatMessage({ id: 'expenses_list' }));
}, [changePageTitle, formatMessage]);
return (
<ExpensesListProvider query={expensesTableQuery}>
<ExpensesListProvider
query={transformTableStateToQuery(expensesTableState)}
>
<ExpenseActionsBar />
<DashboardPageContent>
<ExpensesViewPage />
<ExpenseViewTabs />
<ExpenseDataTable />
</DashboardPageContent>
<ExpensesAlerts />
</ExpensesListProvider>
);
}
export default compose(
withDashboardActions,
withExpenses(({ expensesTableQuery }) => ({ expensesTableQuery })),
withExpenses(({ expensesTableState }) => ({ expensesTableState })),
)(ExpensesList);

View File

@@ -1,6 +1,7 @@
import React, { createContext } from 'react';
import DashboardInsider from 'components/Dashboard/DashboardInsider';
import { useExpenses, useResourceViews } from 'hooks/query';
import { isTableEmptyStatus } from 'utils';
const ExpensesListContext = createContext();
@@ -9,14 +10,21 @@ const ExpensesListContext = createContext();
*/
function ExpensesListProvider({ query, ...props }) {
// Fetch accounts resource views and fields.
const { data: expensesViews, isFetching: isViewsLoading } = useResourceViews(
const { data: expensesViews, isLoading: isViewsLoading } = useResourceViews(
'expenses',
);
// Fetches the expenses with pagination meta.
const {
data: { expenses, pagination },
isFetching: isExpensesLoading,
} = useExpenses();
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 = {
@@ -25,12 +33,15 @@ function ExpensesListProvider({ query, ...props }) {
pagination,
isViewsLoading,
isExpensesLoading
isExpensesLoading,
isExpensesFetching,
isEmptyStatus
};
return (
<DashboardInsider
loading={isViewsLoading}
loading={isViewsLoading || isExpensesLoading}
name={'expenses'}
>
<ExpensesListContext.Provider value={provider} {...props} />

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);

View File

@@ -1,37 +0,0 @@
import React from 'react';
import { Route, Switch } from 'react-router-dom';
import ExpenseViewTabs from 'containers/Expenses/ExpenseViewTabs';
import ExpenseDataTable from './ExpenseDataTable';
import withAlertsActions from 'containers/Alert/withAlertActions';
import withDialogActions from 'containers/Dialog/withDialogActions';
import { compose } from 'utils';
/**
* Expenses inner page.
*/
function ExpensesViewPage() {
return (
<Switch>
<Route
exact={true}
path={['/expenses/:custom_view_id/custom_view', '/expenses']}
>
<ExpenseViewTabs />
<ExpenseDataTable />
{/* // onDeleteExpense={handleDeleteExpense}
// onEditExpense={handleEidtExpense}
// onPublishExpense={handlePublishExpense}
// onSelectedRowsChange={handleSelectedRowsChange}
// /> */}
</Route>
</Switch>
);
}
export default compose(
withAlertsActions,
withDialogActions,
)(ExpensesViewPage);

View File

@@ -1,32 +0,0 @@
import { connect } from 'react-redux';
import { getResourceViews } from 'store/customViews/customViews.selectors';
import {
getExpensesCurrentPageFactory,
getExpenseByIdFactory,
getExpensesTableQuery,
getExpensesPaginationMetaFactory,
getExpensesCurrentViewIdFactory,
} from 'store/expenses/expenses.selectors';
export default (mapState) => {
const getExpensesItems = getExpensesCurrentPageFactory();
const getExpensesPaginationMeta = getExpensesPaginationMetaFactory();
const getExpensesCurrentViewId = getExpensesCurrentViewIdFactory();
const mapStateToProps = (state, props) => {
const query = getExpensesTableQuery(state, props);
const mapped = {
expensesCurrentPage: getExpensesItems(state, props, query),
expensesViews: getResourceViews(state, props, 'expenses'),
expensesItems: state.expenses.items,
expensesTableQuery: query,
expensesPagination: getExpensesPaginationMeta(state, props),
expensesLoading: state.expenses.loading,
expensesCurrentViewId: getExpensesCurrentViewId(state, props),
};
return mapState ? mapState(mapped, state, props) : mapped;
};
return connect(mapStateToProps);
};

View File

@@ -1,36 +0,0 @@
import { connect } from 'react-redux';
import {
submitExpense,
fetchExpense,
editExpense,
deleteExpense,
deleteBulkExpenses,
publishExpense,
fetchExpensesTable,
} from 'store/expenses/expenses.actions';
import t from 'store/types';
const mapDispatchToProps = (dispatch) => ({
requestSubmitExpense: (form) => dispatch(submitExpense({ form })),
requestFetchExpense: (id) => dispatch(fetchExpense({ id })),
requestEditExpense: (id, form) => dispatch(editExpense({ id, form })),
requestDeleteExpense: (id) => dispatch(deleteExpense({ id })),
requestFetchExpensesTable: (query = {}) =>
dispatch(fetchExpensesTable({ query: { ...query } })),
requestPublishExpense: (id) => dispatch(publishExpense({ id })),
requestDeleteBulkExpenses: (ids) => dispatch(deleteBulkExpenses({ ids })),
changeExpensesView: (id) =>
dispatch({
type: t.EXPENSES_SET_CURRENT_VIEW,
currentViewId: parseInt(id, 10),
}),
addExpensesTableQueries: (queries) =>
dispatch({
type: t.EXPENSES_TABLE_QUERIES_ADD,
payload: { queries },
}),
});
export default connect(null, mapDispatchToProps);