refactoring: sales tables.

refacoring: purchases tables.
This commit is contained in:
a.bouhuolia
2021-02-11 20:45:06 +02:00
parent 3901c336df
commit d48532a7e6
210 changed files with 2799 additions and 5392 deletions

View File

@@ -1,296 +0,0 @@
import React, { useCallback, useMemo } from 'react';
import {
Intent,
Button,
Popover,
Menu,
MenuItem,
MenuDivider,
Position,
Tag,
} from '@blueprintjs/core';
import { withRouter } from 'react-router';
import { FormattedMessage as T, useIntl } from 'react-intl';
import moment from 'moment';
import classNames from 'classnames';
import Icon from 'components/Icon';
import { compose, saveInvoke, isBlank } from 'utils';
import { CLASSES } from 'common/classes';
import { useIsValuePassed } from 'hooks';
import { LoadingIndicator, Money, Choose, If } from 'components';
import DataTable from 'components/DataTable';
import BillsEmptyStatus from './BillsEmptyStatus';
import withDialogActions from 'containers/Dialog/withDialogActions';
import withDashboardActions from 'containers/Dashboard/withDashboardActions';
import withViewDetails from 'containers/Views/withViewDetails';
import withBills from './withBills';
import withBillActions from './withBillActions';
import withCurrentView from 'containers/Views/withCurrentView';
import withSettings from 'containers/Settings/withSettings';
// Bills transactions datatable.
function BillsDataTable({
// #withBills
billsCurrentPage,
billsLoading,
billsPageination,
billsCurrentViewId,
// #withDashboardActions
changeCurrentView,
changePageSubtitle,
setTopbarEditView,
// #withBillsActions
addBillsTableQueries,
// #withView
viewMeta,
// #withSettings
baseCurrency,
// #ownProps
loading,
onFetchData,
onEditBill,
onDeleteBill,
onOpenBill,
onSelectedRowsChange,
}) {
const { formatMessage } = useIntl();
const handleFetchData = useCallback(
({ pageIndex, pageSize, sortBy }) => {
const page = pageIndex + 1;
addBillsTableQueries({
...(sortBy.length > 0
? {
column_sort_by: sortBy[0].id,
sort_order: sortBy[0].desc ? 'desc' : 'asc',
}
: {}),
page_size: pageSize,
page,
});
},
[addBillsTableQueries],
);
const handleEditBill = useCallback(
(_bill) => () => {
saveInvoke(onEditBill, _bill);
},
[onEditBill],
);
const handleDeleteBill = useCallback(
(_bill) => () => {
saveInvoke(onDeleteBill, _bill);
},
[onDeleteBill],
);
const actionMenuList = useCallback(
(bill) => (
<Menu>
<MenuItem
icon={<Icon icon="reader-18" />}
text={formatMessage({ id: 'view_details' })}
/>
<MenuDivider />
<MenuItem
icon={<Icon icon="pen-18" />}
text={formatMessage({ id: 'edit_bill' })}
onClick={handleEditBill(bill)}
/>
<If condition={!bill.is_open}>
<MenuItem
text={formatMessage({ id: 'mark_as_opened' })}
onClick={() => onOpenBill(bill)}
/>
</If>
<MenuItem
text={formatMessage({ id: 'delete_bill' })}
intent={Intent.DANGER}
onClick={handleDeleteBill(bill)}
icon={<Icon icon="trash-16" iconSize={16} />}
/>
</Menu>
),
[handleDeleteBill, handleEditBill, formatMessage],
);
const onRowContextMenu = useCallback(
(cell) => {
return actionMenuList(cell.row.original);
},
[actionMenuList],
);
const columns = useMemo(
() => [
{
id: 'bill_date',
Header: formatMessage({ id: 'bill_date' }),
accessor: (r) => moment(r.bill_date).format('YYYY MMM DD'),
width: 140,
className: 'bill_date',
},
{
id: 'vendor_id',
Header: formatMessage({ id: 'vendor_name' }),
accessor: 'vendor.display_name',
width: 140,
className: 'vendor_id',
},
{
id: 'bill_number',
Header: formatMessage({ id: 'bill_number' }),
accessor: (row) => (row.bill_number ? `#${row.bill_number}` : null),
width: 140,
className: 'bill_number',
},
{
id: 'due_date',
Header: formatMessage({ id: 'due_date' }),
accessor: (r) => moment(r.due_date).format('YYYY MMM DD'),
width: 140,
className: 'due_date',
},
{
id: 'amount',
Header: formatMessage({ id: 'amount' }),
accessor: (row) =>
!isBlank(row.amount) ? (
<Money amount={row.amount} currency={baseCurrency} />
) : (
''
),
width: 140,
className: 'amount',
},
{
id: 'status',
Header: formatMessage({ id: 'status' }),
accessor: (row) => (
<Choose>
<Choose.When condition={row.is_open}>
<Tag minimal={true} intent={Intent.SUCCESS}>
<T id={'opened'} />
</Tag>
</Choose.When>
<Choose.Otherwise>
<Tag minimal={true}>
<T id={'draft'} />
</Tag>
</Choose.Otherwise>
</Choose>
),
width: 140,
className: 'status',
},
{
id: 'reference_no',
Header: formatMessage({ id: 'reference_no' }),
accessor: 'reference_no',
width: 140,
className: 'reference_no',
},
{
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],
);
const showEmptyStatus = [
billsCurrentViewId === -1,
billsCurrentPage.length === 0,
].every((condition) => condition === true);
return (
<div className={classNames(CLASSES.DASHBOARD_DATATABLE)}>
<Choose>
<Choose.When condition={showEmptyStatus}>
<BillsEmptyStatus />
</Choose.When>
<Choose.Otherwise>
<DataTable
columns={columns}
data={billsCurrentPage}
onFetchData={handleFetchData}
manualSortBy={true}
selectionColumn={true}
noInitialFetch={true}
sticky={true}
onSelectedRowsChange={handleSelectedRowsChange}
rowContextMenu={onRowContextMenu}
pagination={true}
pagesCount={billsPageination.pagesCount}
initialPageSize={billsPageination.pageSize}
initialPageIndex={billsPageination.page - 1}
/>
</Choose.Otherwise>
</Choose>
</div>
);
}
export default compose(
withRouter,
withCurrentView,
withDialogActions,
withDashboardActions,
withBillActions,
withBills(
({
billsCurrentPage,
billsLoading,
billsPageination,
billsTableQuery,
billsCurrentViewId,
}) => ({
billsCurrentPage,
billsLoading,
billsPageination,
billsTableQuery,
billsCurrentViewId,
}),
),
withSettings(({ organizationSettings }) => ({
baseCurrency: organizationSettings?.baseCurrency,
})),
withViewDetails(),
)(BillsDataTable);

View File

@@ -1,49 +0,0 @@
import React, { useCallback } from 'react';
import { Switch, Route, useHistory } from 'react-router-dom';
import BillViewTabs from './BillViewTabs';
import BillsDataTable from './BillsDataTable';
import withAlertsActions from 'containers/Alert/withAlertActions';
import withDialogActions from 'containers/Dialog/withDialogActions';
import { compose } from 'utils';
function BillsViewPage() {
const history = useHistory();
const handleEditBill = useCallback((bill) => {
history.push(`/bills/${bill.id}/edit`);
});
const handleDeleteBill = () => {
};
const handleOpenBill = () => {
};
const handleSelectedRowsChange = () => {
}
return (
<Switch>
<Route
exact={true}
path={['/bills/:custom_view_id/custom_view', '/bills']}
>
<BillViewTabs />
{/* <BillsDataTable
onDeleteBill={handleDeleteBill}
onEditBill={handleEditBill}
onOpenBill={handleOpenBill}
onSelectedRowsChange={handleSelectedRowsChange}
/> */}
</Route>
</Switch>
);
}
export default compose(withAlertsActions, withDialogActions)(BillsViewPage);

View File

@@ -1,40 +0,0 @@
import { connect } from 'react-redux';
import {
submitBill,
deleteBill,
editBill,
fetchBillsTable,
fetchBill,
fetchDueBills,
openBill,
} from 'store/Bills/bills.actions';
import t from 'store/types';
const mapDispatchToProps = (dispatch) => ({
requestSubmitBill: (form) => dispatch(submitBill({ form })),
requestFetchBill: (id) => dispatch(fetchBill({ id })),
requestEditBill: (id, form) => dispatch(editBill(id, form)),
requestDeleteBill: (id) => dispatch(deleteBill({ id })),
requestFetchBillsTable: (query = {}) =>
dispatch(fetchBillsTable({ query: { ...query } })),
requestFetchDueBills: (vendorId) => dispatch(fetchDueBills({ vendorId })),
requestOpenBill: (id) => dispatch(openBill({ id })),
changeBillView: (id) =>
dispatch({
type: t.BILLS_SET_CURRENT_VIEW,
currentViewId: parseInt(id, 10),
}),
addBillsTableQueries: (queries) =>
dispatch({
type: t.BILLS_TABLE_QUERIES_ADD,
payload: { queries }
}),
setBillNumberChanged: (isChanged) =>
dispatch({
type: t.BILL_NUMBER_CHANGED,
payload: { isChanged },
}),
});
export default connect(null, mapDispatchToProps);

View File

@@ -1,11 +0,0 @@
import { connect } from 'react-redux';
import { getBillByIdFactory } from 'store/Bills/bills.selectors';
export default () => {
const getBillById = getBillByIdFactory();
const mapStateToProps = (state, props) => ({
bill: getBillById(state, props),
});
return connect(mapStateToProps);
};

View File

@@ -1,40 +0,0 @@
import { connect } from 'react-redux';
import { getResourceViews } from 'store/customViews/customViews.selectors';
import {
getBillCurrentPageFactory,
getBillPaginationMetaFactory,
getBillTableQueryFactory,
getVendorPayableBillsFactory,
getVendorPayableBillsEntriesFactory,
getBillsCurrentViewIdFactory,
} from 'store/Bills/bills.selectors';
export default (mapState) => {
const getBillsItems = getBillCurrentPageFactory();
const getBillsPaginationMeta = getBillPaginationMetaFactory();
const getBillTableQuery = getBillTableQueryFactory();
const getVendorPayableBills = getVendorPayableBillsFactory();
const getVendorPayableBillsEntries = getVendorPayableBillsEntriesFactory();
const getBillsCurrentViewId = getBillsCurrentViewIdFactory();
const mapStateToProps = (state, props) => {
const tableQuery = getBillTableQuery(state, props);
const mapped = {
billsCurrentPage: getBillsItems(state, props, tableQuery),
billsCurrentViewId: getBillsCurrentViewId(state),
billsViews: getResourceViews(state, props, 'bills'),
billsItems: state.bills.items,
billsTableQuery: tableQuery,
billsPageination: getBillsPaginationMeta(state, props, tableQuery),
billsLoading: state.bills.loading,
nextBillNumberChanged: state.bills.nextBillNumberChanged,
vendorPayableBills: getVendorPayableBills(state, props),
vendorPayableBillsEntries: getVendorPayableBillsEntries(state, props),
};
return mapState ? mapState(mapped, state, props) : mapped;
};
return connect(mapStateToProps);
};

View File

@@ -18,7 +18,7 @@ import { FormattedMessage as T, useIntl } from 'react-intl';
import DashboardActionsBar from 'components/Dashboard/DashboardActionsBar';
import { If, DashboardActionViewsList } from 'components';
import withBillActions from './withBillActions';
import withBillsActions from './withBillsActions';
import { useBillsListContext } from './BillsListProvider';
import { compose } from 'utils';
@@ -27,28 +27,31 @@ import { compose } from 'utils';
* Bills actions bar.
*/
function BillActionsBar({
//#withBillActions
addBillsTableQueries,
// #withBillActions
setBillsTableState,
}) {
const history = useHistory();
// React intl.
const { formatMessage } = useIntl();
// Bills list context.
const { billsViews } = useBillsListContext();
const [filterCount] = useState(0);
// Handle click a new bill.
const handleClickNewBill = () => {
history.push('/bills/new');
};
// Handle tab change.
const handleTabChange = (viewId) => {
addBillsTableQueries({
custom_view_id: viewId.id || null,
const handleTabChange = (customView) => {
setBillsTableState({
customViewId: customView.id || null,
});
};
return (
<DashboardActionsBar>
<NavbarGroup>
@@ -111,6 +114,4 @@ function BillActionsBar({
);
}
export default compose(
withBillActions,
)(BillActionsBar);
export default compose(withBillsActions)(BillActionsBar);

View File

@@ -4,15 +4,16 @@ import { useIntl } from 'react-intl';
import DashboardPageContent from 'components/Dashboard/DashboardPageContent';
import { BillsListProvider } from './BillsListProvider';
import BillsDataTable from './BillsDataTable';
import BillsActionsBar from './BillsActionsBar';
import BillsAlerts from './BillsAlerts';
import BillsViewPage from './BillsViewPage';
import BillsViewsTabs from './BillsViewsTabs';
import BillsTable from './BillsTable';
import withDashboardActions from 'containers/Dashboard/withDashboardActions';
import withBills from './withBills';
import { compose } from 'utils';
import { transformTableStateToQuery, compose } from 'utils';
/**
* Bills list.
@@ -22,7 +23,7 @@ function BillsList({
changePageTitle,
// #withBills
billsTableQuery,
billsTableState,
}) {
const { formatMessage } = useIntl();
@@ -31,20 +32,20 @@ function BillsList({
}, [changePageTitle, formatMessage]);
return (
<BillsListProvider query={billsTableQuery}>
<BillsListProvider query={transformTableStateToQuery(billsTableState)}>
<BillsActionsBar />
<DashboardPageContent>
<BillsViewPage />
<BillsAlerts />
<BillsViewsTabs />
<BillsTable />
</DashboardPageContent>
<BillsAlerts />
</BillsListProvider>
);
}
export default compose(
withDashboardActions,
withBills(({ billsTableQuery }) => ({
billsTableQuery,
})),
withBills(({ billsTableState }) => ({ billsTableState })),
)(BillsList);

View File

@@ -1,6 +1,7 @@
import React, { createContext } from 'react';
import DashboardInsider from 'components/Dashboard/DashboardInsider';
import { useResourceViews, useResourceFields, useBills } from 'hooks/query';
import { isTableEmptyStatus } from 'utils';
const BillsListContext = createContext();
@@ -21,9 +22,15 @@ function BillsListProvider({ query, ...props }) {
// Fetch accounts list according to the given custom view id.
const {
data: { bills, pagination },
isFetching: isBillsLoading,
} = useBills(query);
data: { bills, pagination, filterMeta },
isLoading: isBillsLoading,
isFetching: isBillsFetching,
} = useBills(query, { keepPreviousData: true });
// Detarmines the datatable empty status.
const isEmptyStatus = isTableEmptyStatus({
data: bills, pagination, filterMeta,
}) && !isBillsFetching;
// Provider payload.
const provider = {
@@ -33,8 +40,10 @@ function BillsListProvider({ query, ...props }) {
billsViews,
isBillsLoading,
isBillsFetching,
isFieldsLoading,
isViewsLoading,
isEmptyStatus
};
return (

View File

@@ -0,0 +1,108 @@
import React, { useCallback } from 'react';
import classNames from 'classnames';
import { useHistory } from 'react-router-dom';
import { compose } from 'utils';
import { CLASSES } from 'common/classes';
import DataTable from 'components/DataTable';
import TableSkeletonRows from 'components/Datatable/TableSkeletonRows';
import TableSkeletonHeader from 'components/Datatable/TableHeaderSkeleton';
import BillsEmptyStatus from './BillsEmptyStatus';
import withBillActions from './withBillsActions';
import withSettings from 'containers/Settings/withSettings';
import withAlertsActions from 'containers/Alert/withAlertActions';
import { useBillsTableColumns, ActionsMenu } from './components';
import { useBillsListContext } from './BillsListProvider';
/**
* Bills transactions datatable.
*/
function BillsDataTable({
// #withBillsActions
setBillsTableState,
// #withAlerts
openAlert
}) {
// Bills list context.
const {
bills,
pagination,
isBillsLoading,
isBillsFetching,
isEmptyStatus,
} = useBillsListContext();
const history = useHistory();
// Bills table columns.
const columns = useBillsTableColumns();
const handleFetchData = useCallback(
({ pageIndex, pageSize, sortBy }) => {
setBillsTableState({
pageIndex,
pageSize,
sortBy,
});
},
[setBillsTableState],
);
// Handle bill edit action.
const handleEditBill = (bill) => {
history.push(`/bills/${bill.id}/edit`);
};
// Handle bill delete action.
const handleDeleteBill = (bill) => {
openAlert('bill-delete', { billId: bill.id });
};
// Handle bill open action.
const handleOpenBill = (bill) => {
openAlert('bill-open', { billId: bill.id });
};
if (isEmptyStatus) {
return <BillsEmptyStatus />;
}
return (
<div className={classNames(CLASSES.DASHBOARD_DATATABLE)}>
<DataTable
columns={columns}
data={bills}
loading={isBillsLoading}
headerLoading={isBillsLoading}
progressBarLoading={isBillsFetching}
onFetchData={handleFetchData}
manualSortBy={true}
selectionColumn={true}
noInitialFetch={true}
sticky={true}
pagination={true}
pagesCount={pagination.pagesCount}
TableLoadingRenderer={TableSkeletonRows}
TableHeaderSkeletonRenderer={TableSkeletonHeader}
ContextMenu={ActionsMenu}
payload={{
onDelete: handleDeleteBill,
onEdit: handleEditBill,
onOpen: handleOpenBill
}}
/>
</div>
);
}
export default compose(
withBillActions,
withAlertsActions,
withSettings(({ organizationSettings }) => ({
baseCurrency: organizationSettings?.baseCurrency,
})),
)(BillsDataTable);

View File

@@ -7,7 +7,7 @@ import { pick } from 'lodash';
import { DashboardViewsTabs } from 'components';
import { useBillsListContext } from './BillsListProvider';
import withBillActions from './withBillActions';
import withBillActions from './withBillsActions';
import { compose } from 'utils';
@@ -16,16 +16,17 @@ import { compose } from 'utils';
*/
function BillViewTabs({
//#withBillActions
addBillsTableQueries,
setBillsTableState,
}) {
const { custom_view_id: customViewId = null } = useParams();
// Bills list context.
const { billsViews } = useBillsListContext();
const handleTabsChange = (viewId) => {
addBillsTableQueries({
custom_view_id: customViewId || null,
// Handle tab chaging.
const handleTabsChange = (customView) => {
setBillsTableState({
customViewId: customView || null,
});
};

View File

@@ -0,0 +1,167 @@
import React from 'react';
import {
Intent,
Menu,
MenuItem,
MenuDivider,
Popover,
Button,
Position,
Tag,
} from '@blueprintjs/core';
import { useIntl, FormattedMessage as T } from 'react-intl';
import { Icon, If, Choose, Money } from 'components';
import { safeCallback, isBlank } from 'utils';
import moment from 'moment';
/**
* Actions menu.
*/
export function ActionsMenu({
payload: { onEdit, onOpen, onDelete },
row: { original },
}) {
const { formatMessage } = useIntl();
return (
<Menu>
<MenuItem
icon={<Icon icon="reader-18" />}
text={formatMessage({ id: 'view_details' })}
/>
<MenuDivider />
<MenuItem
icon={<Icon icon="pen-18" />}
text={formatMessage({ id: 'edit_bill' })}
onClick={safeCallback(onEdit, original)}
/>
<If condition={!original.is_open}>
<MenuItem
text={formatMessage({ id: 'mark_as_opened' })}
onClick={safeCallback(onOpen, original)}
/>
</If>
<MenuItem
text={formatMessage({ id: 'delete_bill' })}
intent={Intent.DANGER}
onClick={safeCallback(onDelete, original)}
icon={<Icon icon="trash-16" iconSize={16} />}
/>
</Menu>
);
}
/**
* Amount accessor.
*/
export function AmountAccessor(bill) {
return !isBlank(bill.amount) ? (
<Money amount={bill.amount} currency={'USD'} />
) : (
''
);
}
/**
* Status accessor.
*/
export function StatusAccessor(bill) {
return (
<Choose>
<Choose.When condition={bill.is_open}>
<Tag minimal={true} intent={Intent.SUCCESS}>
<T id={'opened'} />
</Tag>
</Choose.When>
<Choose.Otherwise>
<Tag minimal={true}>
<T id={'draft'} />
</Tag>
</Choose.Otherwise>
</Choose>
);
}
export function ActionsCell(props) {
return (
<Popover
content={<ActionsMenu {...props} />}
position={Position.RIGHT_BOTTOM}
>
<Button icon={<Icon icon="more-h-16" iconSize={16} />} />
</Popover>
);
}
/**
* Retrieve bills table columns.
*/
export function useBillsTableColumns() {
const { formatMessage } = useIntl();
return React.useMemo(
() => [
{
id: 'bill_date',
Header: formatMessage({ id: 'bill_date' }),
accessor: (r) => moment(r.bill_date).format('YYYY MMM DD'),
width: 140,
className: 'bill_date',
},
{
id: 'vendor_id',
Header: formatMessage({ id: 'vendor_name' }),
accessor: 'vendor.display_name',
width: 140,
className: 'vendor_id',
},
{
id: 'bill_number',
Header: formatMessage({ id: 'bill_number' }),
accessor: (row) => (row.bill_number ? `#${row.bill_number}` : null),
width: 140,
className: 'bill_number',
},
{
id: 'due_date',
Header: formatMessage({ id: 'due_date' }),
accessor: (r) => moment(r.due_date).format('YYYY MMM DD'),
width: 140,
className: 'due_date',
},
{
id: 'amount',
Header: formatMessage({ id: 'amount' }),
accessor: AmountAccessor,
width: 140,
className: 'amount',
},
{
id: 'status',
Header: formatMessage({ id: 'status' }),
accessor: StatusAccessor,
width: 140,
className: 'status',
},
{
id: 'reference_no',
Header: formatMessage({ id: 'reference_no' }),
accessor: 'reference_no',
width: 140,
className: 'reference_no',
},
{
id: 'actions',
Header: '',
Cell: ActionsCell,
className: 'actions',
width: 50,
disableResizing: true,
},
],
[formatMessage],
);
}

View File

@@ -0,0 +1,14 @@
import { connect } from 'react-redux';
import { getBillsTableStateFactory } from 'store/Bills/bills.selectors';
export default (mapState) => {
const getBillsTableState = getBillsTableStateFactory();
const mapStateToProps = (state, props) => {
const mapped = {
billsTableState: getBillsTableState(state, props),
};
return mapState ? mapState(mapped, state, props) : mapped;
};
return connect(mapStateToProps);
};

View File

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

View File

@@ -1,249 +0,0 @@
import React, { useCallback, useMemo } from 'react';
import {
Intent,
Button,
Popover,
Menu,
MenuItem,
MenuDivider,
Position,
} from '@blueprintjs/core';
import { withRouter } from 'react-router';
import { FormattedMessage as T, useIntl } from 'react-intl';
import moment from 'moment';
import classNames from 'classnames';
import { compose, saveInvoke } from 'utils';
import { useIsValuePassed } from 'hooks';
import { CLASSES } from 'common/classes';
import { DataTable, Money, Icon, Choose, LoadingIndicator } from 'components';
import PaymentMadesEmptyStatus from './PaymentMadesEmptyStatus';
import withPaymentMade from './withPaymentMade';
import withPaymentMadeActions from './withPaymentMadeActions';
import withCurrentView from 'containers/Views/withCurrentView';
import withSettings from 'containers/Settings/withSettings';
/**
* Payment made datatable transactions.
*/
function PaymentMadeDataTable({
// #withPaymentMades
paymentMadeCurrentPage,
paymentMadePageination,
paymentMadesLoading,
paymentMadeTableQuery,
paymentMadesCurrentViewId,
// #withPaymentMadeActions
addPaymentMadesTableQueries,
// #withSettings
baseCurrency,
// #ownProps
onEditPaymentMade,
onDeletePaymentMade,
onSelectedRowsChange,
}) {
const isLoaded = useIsValuePassed(paymentMadesLoading, false);
const { formatMessage } = useIntl();
const handleEditPaymentMade = useCallback(
(paymentMade) => () => {
saveInvoke(onEditPaymentMade, paymentMade);
},
[onEditPaymentMade],
);
const handleDeletePaymentMade = useCallback(
(paymentMade) => () => {
saveInvoke(onDeletePaymentMade, paymentMade);
},
[onDeletePaymentMade],
);
const actionMenuList = useCallback(
(paymentMade) => (
<Menu>
<MenuItem
icon={<Icon icon="reader-18" />}
text={formatMessage({ id: 'view_details' })}
/>
<MenuDivider />
<MenuItem
icon={<Icon icon="pen-18" />}
text={formatMessage({ id: 'edit_payment_made' })}
onClick={handleEditPaymentMade(paymentMade)}
/>
<MenuItem
text={formatMessage({ id: 'delete_payment_made' })}
intent={Intent.DANGER}
onClick={handleDeletePaymentMade(paymentMade)}
icon={<Icon icon="trash-16" iconSize={16} />}
/>
</Menu>
),
[handleDeletePaymentMade, handleEditPaymentMade, formatMessage],
);
const onRowContextMenu = useCallback(
(cell) => {
return actionMenuList(cell.row.original);
},
[actionMenuList],
);
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: 'vendor_id',
Header: formatMessage({ id: 'vendor_name' }),
accessor: 'vendor.display_name',
width: 140,
className: 'vendor_id',
},
{
id: 'payment_number',
Header: formatMessage({ id: 'payment_number' }),
accessor: (row) =>
row.payment_number ? `#${row.payment_number}` : null,
width: 140,
className: 'payment_number',
},
{
id: 'payment_account_id',
Header: formatMessage({ id: 'payment_account' }),
accessor: 'payment_account.name',
width: 140,
className: 'payment_account_id',
},
{
id: 'amount',
Header: formatMessage({ id: 'amount' }),
accessor: (r) => <Money amount={r.amount} currency={baseCurrency} />,
width: 140,
className: 'amount',
},
{
id: 'reference',
Header: formatMessage({ id: 'reference' }),
accessor: 'reference',
width: 140,
className: 'reference',
},
{
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 handleDataTableFetchData = useCallback(
({ pageIndex, pageSize, sortBy }) => {
addPaymentMadesTableQueries({
...(sortBy.length > 0
? {
column_sort_by: sortBy[0].id,
sort_order: sortBy[0].desc ? 'desc' : 'asc',
}
: {}),
page_size: pageSize,
page: pageIndex + 1,
});
},
[addPaymentMadesTableQueries],
);
const handleSelectedRowsChange = useCallback(
(selectedRows) => {
saveInvoke(
onSelectedRowsChange,
selectedRows.map((s) => s.original),
);
},
[onSelectedRowsChange],
);
const showEmptyStatuts = [
paymentMadeCurrentPage.length === 0,
paymentMadesCurrentViewId === -1,
].every((condition) => condition === true);
return (
<div className={classNames(CLASSES.DASHBOARD_DATATABLE)}>
<LoadingIndicator loading={paymentMadesLoading && !isLoaded}>
<Choose>
<Choose.When condition={showEmptyStatuts}>
<PaymentMadesEmptyStatus />
</Choose.When>
<Choose.Otherwise>
<DataTable
columns={columns}
data={paymentMadeCurrentPage}
onFetchData={handleDataTableFetchData}
manualSortBy={true}
selectionColumn={true}
noInitialFetch={true}
sticky={true}
onSelectedRowsChange={handleSelectedRowsChange}
rowContextMenu={onRowContextMenu}
pagination={true}
pagesCount={paymentMadePageination.pagesCount}
initialPageSize={paymentMadeTableQuery.page_size}
initialPageIndex={paymentMadeTableQuery.page - 1}
autoResetSortBy={false}
autoResetPage={false}
/>
</Choose.Otherwise>
</Choose>
</LoadingIndicator>
</div>
);
}
export default compose(
withRouter,
withCurrentView,
withPaymentMadeActions,
withPaymentMade(
({
paymentMadeCurrentPage,
paymentMadesLoading,
paymentMadePageination,
paymentMadeTableQuery,
paymentMadesCurrentViewId,
}) => ({
paymentMadeCurrentPage,
paymentMadesLoading,
paymentMadePageination,
paymentMadeTableQuery,
paymentMadesCurrentViewId,
}),
),
withSettings(({ organizationSettings }) => ({
baseCurrency: organizationSettings?.baseCurrency,
})),
)(PaymentMadeDataTable);

View File

@@ -4,7 +4,7 @@ import PaymentMadeDeleteAlert from 'containers/Alerts/PaymentMades/PaymentMadeDe
export default function PaymentMadesAlerts() {
return (
<div>
<PaymentMadeDeleteAlert dialogName={'payment-made-delete'} />
<PaymentMadeDeleteAlert name={'payment-made-delete'} />
</div>
);
}

View File

@@ -27,8 +27,8 @@ import { compose } from 'utils';
* Payment made actions bar.
*/
function PaymentMadeActionsBar({
//#withPaymentMadesActions
addPaymentMadesTableQueries,
// #withPaymentMadesActions
setPaymentMadesTableState,
}) {
const history = useHistory();
const { formatMessage } = useIntl();
@@ -41,12 +41,18 @@ function PaymentMadeActionsBar({
history.push('/payment-mades/new');
};
// Handle tab changing.
const handleTabChange = (customView) => {
setPaymentMadesTableState({ customViewId: customView.id || null });
};
return (
<DashboardActionsBar>
<NavbarGroup>
<DashboardActionViewsList
resourceName={'bill_payments'}
views={paymentMadesViews}
onChange={handleTabChange}
/>
<NavbarDivider />
<Button

View File

@@ -1,16 +1,17 @@
import React, { useEffect } from 'react';
import { FormattedMessage as T, useIntl } from 'react-intl';
import { useIntl } from 'react-intl';
import DashboardPageContent from 'components/Dashboard/DashboardPageContent';
import PaymentMadeActionsBar from './PaymentMadeActionsBar';
import PaymentMadesAlerts from './PaymentMadesAlerts';
import PaymentMadesAlerts from '../PaymentMadesAlerts';
import PaymentMadesTable from './PaymentMadesTable';
import { PaymentMadesListProvider } from './PaymentMadesListProvider';
import PaymentMadesView from './PaymentMadesView';
import PaymentMadeViewTabs from './PaymentMadeViewTabs';
import withDashboardActions from 'containers/Dashboard/withDashboardActions';
import withPaymentMades from './withPaymentMade';
import { compose } from 'utils';
import { compose, transformTableStateToQuery } from 'utils';
/**
* Payment mades list.
@@ -20,7 +21,7 @@ function PaymentMadeList({
changePageTitle,
// #withPaymentMades
paymentMadeTableQuery,
paymentMadesTableState,
}) {
const { formatMessage } = useIntl();
@@ -29,20 +30,24 @@ function PaymentMadeList({
}, [changePageTitle, formatMessage]);
return (
<PaymentMadesListProvider query={paymentMadeTableQuery}>
<PaymentMadesListProvider
query={transformTableStateToQuery(paymentMadesTableState)}
>
<PaymentMadeActionsBar />
<DashboardPageContent>
<PaymentMadesView />
<PaymentMadesAlerts />
<PaymentMadeViewTabs />
<PaymentMadesTable />
</DashboardPageContent>
<PaymentMadesAlerts />
</PaymentMadesListProvider>
);
}
export default compose(
withDashboardActions,
withPaymentMades(({ paymentMadeTableQuery }) => ({
paymentMadeTableQuery,
withPaymentMades(({ paymentMadesTableState }) => ({
paymentMadesTableState,
})),
)(PaymentMadeList);

View File

@@ -2,7 +2,6 @@ import React from 'react';
import { useHistory } from 'react-router';
import { FormattedMessage as T } from 'react-intl';
import { Alignment, Navbar, NavbarGroup } from '@blueprintjs/core';
import { useParams } from 'react-router-dom';
import { pick } from 'lodash';
import { DashboardViewsTabs } from 'components';
@@ -11,20 +10,27 @@ import { usePaymentMadesListContext } from './PaymentMadesListProvider';
import withPaymentMadeActions from './withPaymentMadeActions';
import { compose } from 'utils';
import withPaymentMade from './withPaymentMade';
/**
* Payment made views tabs.
*/
function PaymentMadeViewTabs({
//#withPaymentMadesActions
addPaymentMadesTableQueries,
// #withPaymentMadesActions
setPaymentMadesTableState,
// #withPaymentMade
paymentMadesTableState,
}) {
const history = useHistory();
const { custom_view_id: customViewId = null } = useParams();
// Payment receives list context.
const { paymentMadesViews } = usePaymentMadesListContext();
const handleTabsChange = (viewId) => {
addPaymentMadesTableQueries({
custom_view_id: viewId || null,
// Handle the active tab changning.
const handleTabsChange = (customView) => {
setPaymentMadesTableState({
customViewId: customView.id || null,
});
};
@@ -40,7 +46,7 @@ function PaymentMadeViewTabs({
<Navbar className={'navbar--dashboard-views'}>
<NavbarGroup align={Alignment.LEFT}>
<DashboardViewsTabs
initialViewId={customViewId}
customViewId={paymentMadesTableState.customViewId}
defaultTabText={<T id={'all_payments'} />}
tabs={tabs}
onNewViewTabClick={handleClickNewView}
@@ -51,4 +57,7 @@ function PaymentMadeViewTabs({
);
}
export default compose(withPaymentMadeActions)(PaymentMadeViewTabs);
export default compose(
withPaymentMadeActions,
withPaymentMade(({ paymentMadesTableState }) => ({ paymentMadesTableState }))
)(PaymentMadeViewTabs);

View File

@@ -3,8 +3,9 @@ import DashboardInsider from 'components/Dashboard/DashboardInsider';
import {
useResourceViews,
useResourceFields,
usePaymentReceives,
usePaymentMades,
} from 'hooks/query';
import { isTableEmptyStatus } from 'utils';
const PaymentMadesListContext = createContext();
@@ -26,20 +27,32 @@ function PaymentMadesListProvider({ accountsTableQuery, ...props }) {
// Fetch accounts list according to the given custom view id.
const {
data: { paymentMades, pagination },
isFetching: isInvoicesLoading,
} = usePaymentReceives(accountsTableQuery);
data: { paymentMades, pagination, filterMeta },
isLoading: isPaymentsLoading,
isFetching: isPaymentsFetching,
} = usePaymentMades(accountsTableQuery, { keepPreviousData: true });
// Detarmines the datatable empty status.
const isEmptyStatus =
isTableEmptyStatus({
data: paymentMades,
pagination,
filterMeta,
}) && !isPaymentsLoading;
// Provider payload.
const provider = {
paymentMades,
pagination,
filterMeta,
paymentMadesFields,
paymentMadesViews,
isInvoicesLoading,
isPaymentsLoading,
isPaymentsFetching,
isFieldsLoading,
isViewsLoading,
isEmptyStatus
};
return (

View File

@@ -0,0 +1,95 @@
import React, { useCallback } from 'react';
import classNames from 'classnames';
import { compose } from 'utils';
import { CLASSES } from 'common/classes';
import { DataTable } from 'components';
import PaymentMadesEmptyStatus from './PaymentMadesEmptyStatus';
import TableSkeletonRows from 'components/Datatable/TableSkeletonRows';
import TableSkeletonHeader from 'components/Datatable/TableHeaderSkeleton';
import withPaymentMadeActions from './withPaymentMadeActions';
import withSettings from 'containers/Settings/withSettings';
import withAlertsActions from 'containers/Alert/withAlertActions';
import { usePaymentMadesTableColumns, ActionsMenu } from './components';
import { usePaymentMadesListContext } from './PaymentMadesListProvider';
/**
* Payment made datatable transactions.
*/
function PaymentMadesTable({
// #withPaymentMadeActions
addPaymentMadesTableQueries,
// #withAlerts
openAlert
}) {
// Payment mades table columns.
const columns = usePaymentMadesTableColumns();
// Payment mades list context.
const {
paymentMades,
pagination,
isEmptyStatus,
isPaymentsLoading,
isPaymentsFetching,
} = usePaymentMadesListContext();
// Handles the edit payment made action.
const handleEditPaymentMade = (paymentMade) => {};
// Handles the delete payment made action.
const handleDeletePaymentMade = (paymentMade) => {
openAlert('payment-made-delete', { paymentMadeId: paymentMade.id })
};
// Handle datatable fetch data once the table state change.
const handleDataTableFetchData = useCallback(
({ pageIndex, pageSize, sortBy }) => {
addPaymentMadesTableQueries({ pageIndex, pageSize, sortBy });
},
[addPaymentMadesTableQueries],
);
if (isEmptyStatus) {
return <PaymentMadesEmptyStatus />;
}
return (
<div className={classNames(CLASSES.DASHBOARD_DATATABLE)}>
<DataTable
columns={columns}
data={paymentMades}
onFetchData={handleDataTableFetchData}
loading={isPaymentsLoading}
headerLoading={isPaymentsLoading}
progressBarLoading={isPaymentsFetching}
manualSortBy={true}
selectionColumn={true}
noInitialFetch={true}
sticky={true}
pagination={true}
pagesCount={pagination.pagesCount}
autoResetSortBy={false}
autoResetPage={false}
TableLoadingRenderer={TableSkeletonRows}
TableHeaderSkeletonRenderer={TableSkeletonHeader}
ContextMenu={ActionsMenu}
payload={{
onEdit: handleEditPaymentMade,
onDelete: handleDeletePaymentMade,
}}
/>
</div>
);
}
export default compose(
withPaymentMadeActions,
withAlertsActions,
withSettings(({ organizationSettings }) => ({
baseCurrency: organizationSettings?.baseCurrency,
})),
)(PaymentMadesTable);

View File

@@ -21,7 +21,7 @@ function PaymentMadesViewPage({
exact={true}
path={['/payment-mades/:custom_view_id/custom_view', '/payment-mades']}
>
<PaymentMadeViewTabs />
{/* <PaymentMadeDataTable
onDeletePaymentMade={handleDeletePaymentMade}
onEditPaymentMade={handleEditPaymentMade}

View File

@@ -0,0 +1,133 @@
import React from 'react';
import moment from 'moment';
import {
Intent,
Button,
Popover,
Menu,
MenuItem,
MenuDivider,
Position,
} from '@blueprintjs/core';
import { useIntl } from 'react-intl';
import { Icon, Money } from 'components';
import { safeCallback } from 'utils';
export function DateCell({ value }) {
return moment(value).format('YYYY MMM DD');
}
export function AmountAccessor(row) {
return <Money amount={row.amount} currency={'USD'} />
}
/**
* Actions menu.
*/
export function ActionsMenu({
row: { original },
payload: { onEdit, onDelete },
}) {
const { formatMessage } = useIntl();
return (
<Menu>
<MenuItem
icon={<Icon icon="reader-18" />}
text={formatMessage({ id: 'view_details' })}
/>
<MenuDivider />
<MenuItem
icon={<Icon icon="pen-18" />}
text={formatMessage({ id: 'edit_payment_made' })}
onClick={safeCallback(onEdit, original)}
/>
<MenuItem
text={formatMessage({ id: 'delete_payment_made' })}
intent={Intent.DANGER}
onClick={safeCallback(onDelete, original)}
icon={<Icon icon="trash-16" iconSize={16} />}
/>
</Menu>
);
}
/**
* Payment mades table 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>
);
}
/**
* Retrieve payment mades table columns.
*/
export function usePaymentMadesTableColumns() {
const { formatMessage } = useIntl();
return React.useMemo(
() => [
{
id: 'payment_date',
Header: formatMessage({ id: 'payment_date' }),
Cell: DateCell,
accessor: 'payment_date',
width: 140,
className: 'payment_date',
},
{
id: 'vendor_id',
Header: formatMessage({ id: 'vendor_name' }),
accessor: 'vendor.display_name',
width: 140,
className: 'vendor_id',
},
{
id: 'payment_number',
Header: formatMessage({ id: 'payment_number' }),
accessor: (row) =>
row.payment_number ? `#${row.payment_number}` : null,
width: 140,
className: 'payment_number',
},
{
id: 'payment_account_id',
Header: formatMessage({ id: 'payment_account' }),
accessor: 'payment_account.name',
width: 140,
className: 'payment_account_id',
},
{
id: 'amount',
Header: formatMessage({ id: 'amount' }),
accessor: AmountAccessor,
width: 140,
className: 'amount',
},
{
id: 'reference',
Header: formatMessage({ id: 'reference' }),
accessor: 'reference',
width: 140,
className: 'reference',
},
{
id: 'actions',
Header: '',
Cell: ActionsCell,
className: 'actions',
width: 50,
disableResizing: true,
},
],
[formatMessage],
);
}

View File

@@ -0,0 +1,16 @@
import { connect } from 'react-redux';
import {
getPaymentMadesTableStateFactory
} from 'store/PaymentMades/paymentMades.selector';
export default (mapState) => {
const getPaymentMadesTableState = getPaymentMadesTableStateFactory();
const mapStateToProps = (state, props) => {
const mapped = {
paymentMadesTableState: getPaymentMadesTableState(state, props),
};
return mapState ? mapState(mapped, state, props) : mapped;
};
return connect(mapStateToProps);
};

View File

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

View File

@@ -1,39 +0,0 @@
import { connect } from 'react-redux';
import { getResourceViews } from 'store/customViews/customViews.selectors';
import {
getPaymentMadeCurrentPageFactory,
getPaymentMadePaginationMetaFactory,
getPaymentMadeTableQuery,
getPaymentMadeEntriesFactory,
getPaymentMadesCurrentViewIdFactory
} from 'store/PaymentMades/paymentMade.selector';
export default (mapState) => {
const getPyamentMadesItems = getPaymentMadeCurrentPageFactory();
const getPyamentMadesPaginationMeta = getPaymentMadePaginationMetaFactory();
const getPaymentMadeEntries = getPaymentMadeEntriesFactory();
const getPaymentMadesCurrentViewId = getPaymentMadesCurrentViewIdFactory();
const mapStateToProps = (state, props) => {
const query = getPaymentMadeTableQuery(state, props);
const mapped = {
paymentMadeCurrentPage: getPyamentMadesItems(state, props, query),
paymentMadeViews: getResourceViews(state, props, 'bill_payments'),
paymentMadeItems: state.paymentMades.items,
paymentMadeTableQuery: query,
paymentMadePageination: getPyamentMadesPaginationMeta(
state,
props,
query,
),
paymentMadesLoading: state.paymentMades.loading,
nextPaymentNumberChanged: state.paymentMades.nextPaymentNumberChanged,
paymentMadeEntries: getPaymentMadeEntries(state, props),
paymentMadesCurrentViewId: getPaymentMadesCurrentViewId(state, props),
};
return mapState ? mapState(mapped, state, props) : mapped;
};
return connect(mapStateToProps);
};

View File

@@ -1,39 +0,0 @@
import { connect } from 'react-redux';
import t from 'store/types';
import {
submitPaymentMade,
editPaymentMade,
deletePaymentMade,
fetchPaymentMadesTable,
fetchPaymentMade,
fetchPaymentMadeBills,
} from 'store/PaymentMades/paymentMade.actions';
const mapDispatchToProps = (dispatch) => ({
requestSubmitPaymentMade: (form) => dispatch(submitPaymentMade({ form })),
requestFetchPaymentMade: (id) => dispatch(fetchPaymentMade({ id })),
requestEditPaymentMade: (id, form) => dispatch(editPaymentMade(id, form)),
requestDeletePaymentMade: (id) => dispatch(deletePaymentMade({ id })),
requestFetchPaymentMadesTable: (query = {}) =>
dispatch(fetchPaymentMadesTable({ query: { ...query } })),
requestFetchPaymentMadeBills: (paymentMadeId) => dispatch(fetchPaymentMadeBills({ paymentMadeId })),
changePaymentMadeView: (id) =>
dispatch({
type: t.PAYMENT_MADE_SET_CURRENT_VIEW,
currentViewId: parseInt(id, 10),
}),
addPaymentMadesTableQueries: (queries) =>
dispatch({
type: t.PAYMENT_MADES_TABLE_QUERIES_ADD,
payload: { queries },
}),
setPaymentNumberChange: (isChanged) =>
dispatch({
type: t.PAYMENT_MADES_NUMBER_CHANGED,
payload: { isChanged },
}),
});
export default connect(null, mapDispatchToProps);