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,84 +0,0 @@
import React, { useCallback } from 'react';
import { Switch, Route, useHistory } from 'react-router-dom';
import EstimateViewTabs from './EstimateViewTabs';
import EstimatesDataTable from './EstimatesDataTable';
import withAlertsActions from 'containers/Alert/withAlertActions';
import withDialogActions from 'containers/Dialog/withDialogActions';
import { compose } from 'utils';
/**
* Estimates list view page.
*/
function EstimatesViewPage({
// #withAlertActions
openAlert,
}) {
const history = useHistory();
// handle delete estimate click
const handleDeleteEstimate = useCallback(
({ id }) => {
openAlert('estimate-delete', { estimateId: id });
},
[openAlert],
);
// Handle cancel/confirm estimate deliver.
const handleDeliverEstimate = useCallback(
({ id }) => {
openAlert('estimate-deliver', { estimateId: id });
},
[openAlert],
);
// Handle cancel/confirm estimate approve.
const handleApproveEstimate = useCallback(
({ id }) => {
openAlert('estimate-Approve', { estimateId: id });
},
[openAlert],
);
// Handle cancel/confirm estimate reject.
const handleRejectEstimate = useCallback(
({ id }) => {
openAlert('estimate-reject', { estimateId: id });
},
[openAlert],
);
const handleEditEstimate = useCallback(
(estimate) => {
history.push(`/estimates/${estimate.id}/edit`);
},
[history],
);
return (
<Switch>
<Route
exact={true}
path={['/estimates/:custom_view_id/custom_view', '/estimates']}
>
<EstimateViewTabs />
{/* <EstimatesDataTable
onDeleteEstimate={handleDeleteEstimate}
onEditEstimate={handleEditEstimate}
onDeliverEstimate={handleDeliverEstimate}
onApproveEstimate={handleApproveEstimate}
onRejectEstimate={handleRejectEstimate}
onSelectedRowsChange={handleSelectedRowsChange}
/> */}
</Route>
</Switch>
);
}
export default compose(
withAlertsActions,
withDialogActions,
)(EstimatesViewPage)

View File

@@ -1,292 +0,0 @@
import React, { useCallback, useMemo } from 'react';
import {
Intent,
Button,
Popover,
Menu,
MenuItem,
MenuDivider,
Position,
Tag,
} from '@blueprintjs/core';
import classNames from 'classnames';
import { FormattedMessage as T, useIntl } from 'react-intl';
import moment from 'moment';
import { CLASSES } from 'common/classes';
import { compose, saveInvoke } from 'utils';
import { useIsValuePassed } from 'hooks';
import LoadingIndicator from 'components/LoadingIndicator';
import { DataTable, Money, Choose, Icon, If } from 'components';
import EstimatesEmptyStatus from './EstimatesEmptyStatus';
import { statusAccessor } from './components';
import withEstimates from './withEstimates';
import withEstimateActions from './withEstimateActions';
import withSettings from 'containers/Settings/withSettings';
// Estimates transactions datatable.
function EstimatesDataTable({
// #withEstimates
estimatesCurrentPage,
estimatesLoading,
estimatesPageination,
estimatesTableQuery,
estimatesCurrentViewId,
// #withEstimatesActions
addEstimatesTableQueries,
// #withSettings
baseCurrency,
// #ownProps
onEditEstimate,
onDeleteEstimate,
onDeliverEstimate,
onApproveEstimate,
onRejectEstimate,
onSelectedRowsChange,
}) {
const { formatMessage } = useIntl();
const isLoaded = useIsValuePassed(estimatesLoading, false);
const handleEditEstimate = useCallback(
(estimate) => () => {
saveInvoke(onEditEstimate, estimate);
},
[onEditEstimate],
);
const handleDeleteEstimate = useCallback(
(estimate) => () => {
saveInvoke(onDeleteEstimate, estimate);
},
[onDeleteEstimate],
);
const actionMenuList = useCallback(
(estimate) => (
<Menu>
<MenuItem
icon={<Icon icon="reader-18" />}
text={formatMessage({ id: 'view_details' })}
/>
<MenuDivider />
<MenuItem
icon={<Icon icon="pen-18" />}
text={formatMessage({ id: 'edit_estimate' })}
onClick={handleEditEstimate(estimate)}
/>
<If condition={!estimate.is_delivered}>
<MenuItem
text={formatMessage({ id: 'mark_as_delivered' })}
onClick={() => onDeliverEstimate(estimate)}
/>
</If>
<Choose>
<Choose.When
condition={estimate.is_delivered && estimate.is_approved}
>
<MenuItem
text={formatMessage({ id: 'mark_as_rejected' })}
onClick={() => onRejectEstimate(estimate)}
/>
</Choose.When>
<Choose.When
condition={estimate.is_delivered && estimate.is_rejected}
>
<MenuItem
text={formatMessage({ id: 'mark_as_approved' })}
onClick={() => onApproveEstimate(estimate)}
/>
</Choose.When>
<Choose.When condition={estimate.is_delivered}>
<MenuItem
text={formatMessage({ id: 'mark_as_approved' })}
onClick={() => onApproveEstimate(estimate)}
/>
<MenuItem
text={formatMessage({ id: 'mark_as_rejected' })}
onClick={() => onRejectEstimate(estimate)}
/>
</Choose.When>
</Choose>
<MenuItem
text={formatMessage({ id: 'delete_estimate' })}
intent={Intent.DANGER}
onClick={handleDeleteEstimate(estimate)}
icon={<Icon icon="trash-16" iconSize={16} />}
/>
</Menu>
),
[handleDeleteEstimate, handleEditEstimate, formatMessage],
);
const onRowContextMenu = useCallback(
(cell) => {
return actionMenuList(cell.row.original);
},
[actionMenuList],
);
const columns = useMemo(
() => [
{
id: 'estimate_date',
Header: formatMessage({ id: 'estimate_date' }),
accessor: (r) => moment(r.estimate_date).format('YYYY MMM DD'),
width: 140,
className: 'estimate_date',
},
{
id: 'customer_id',
Header: formatMessage({ id: 'customer_name' }),
accessor: 'customer.display_name',
width: 140,
className: 'customer_id',
},
{
id: 'expiration_date',
Header: formatMessage({ id: 'expiration_date' }),
accessor: (r) => moment(r.expiration_date).format('YYYY MMM DD'),
width: 140,
className: 'expiration_date',
},
{
id: 'estimate_number',
Header: formatMessage({ id: 'estimate_number' }),
accessor: (row) =>
row.estimate_number ? `#${row.estimate_number}` : null,
width: 140,
className: 'estimate_number',
},
{
id: 'amount',
Header: formatMessage({ id: 'amount' }),
accessor: (r) => <Money amount={r.amount} currency={baseCurrency} />,
width: 140,
className: 'amount',
},
{
id: 'status',
Header: formatMessage({ id: 'status' }),
accessor: (row) => statusAccessor(row),
width: 140,
className: 'status',
},
{
id: 'reference',
Header: formatMessage({ id: 'reference_no' }),
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 handleFetchData = useCallback(
({ pageIndex, pageSize, sortBy }) => {
const page = pageIndex + 1;
addEstimatesTableQueries({
...(sortBy.length > 0
? {
column_sort_by: sortBy[0].id,
sort_order: sortBy[0].desc ? 'desc' : 'asc',
}
: {}),
page_size: pageSize,
page,
});
},
[addEstimatesTableQueries],
);
const handleSelectedRowsChange = useCallback(
(selectedRows) => {
saveInvoke(
onSelectedRowsChange,
selectedRows.map((s) => s.original),
);
},
[onSelectedRowsChange],
);
const showEmptyStatus = [
estimatesCurrentPage.length === 0,
estimatesCurrentViewId === -1,
].every((d) => d === true);
return (
<div className={classNames(CLASSES.DASHBOARD_DATATABLE)}>
<LoadingIndicator loading={estimatesLoading && !isLoaded} mount={false}>
<Choose>
<Choose.When condition={showEmptyStatus}>
<EstimatesEmptyStatus />
</Choose.When>
<Choose.Otherwise>
<DataTable
columns={columns}
data={estimatesCurrentPage}
onFetchData={handleFetchData}
manualSortBy={true}
selectionColumn={true}
noInitialFetch={true}
sticky={true}
onSelectedRowsChange={handleSelectedRowsChange}
rowContextMenu={onRowContextMenu}
pagination={true}
pagesCount={estimatesPageination.pagesCount}
initialPageSize={estimatesTableQuery.page_size}
initialPageIndex={estimatesTableQuery.page - 1}
/>
</Choose.Otherwise>
</Choose>
</LoadingIndicator>
</div>
);
}
export default compose(
withEstimateActions,
withEstimates(
({
estimatesCurrentPage,
estimatesLoading,
estimatesPageination,
estimatesTableQuery,
estimatesCurrentViewId,
}) => ({
estimatesCurrentPage,
estimatesLoading,
estimatesPageination,
estimatesTableQuery,
estimatesCurrentViewId,
}),
),
withSettings(({ organizationSettings }) => ({
baseCurrency: organizationSettings?.baseCurrency,
})),
)(EstimatesDataTable);

View File

@@ -1,31 +0,0 @@
import React from 'react';
import { Intent, Tag } from '@blueprintjs/core';
import { Choose, If } from 'components';
import { FormattedMessage as T, useIntl } from 'react-intl';
export const statusAccessor = (row) => (
<Choose>
<Choose.When condition={row.is_delivered && row.is_approved}>
<Tag minimal={true} intent={Intent.SUCCESS}>
<T id={'approved'} />
</Tag>
</Choose.When>
<Choose.When condition={row.is_delivered && row.is_rejected}>
<Tag minimal={true} intent={Intent.DANGER}>
<T id={'rejected'} />
</Tag>
</Choose.When>
<Choose.When
condition={row.is_delivered && !row.is_rejected && !row.is_approved}
>
<Tag minimal={true} intent={Intent.SUCCESS}>
<T id={'delivered'} />
</Tag>
</Choose.When>
<Choose.Otherwise>
<Tag minimal={true}>
<T id={'draft'} />
</Tag>
</Choose.Otherwise>
</Choose>
);

View File

@@ -1,48 +0,0 @@
import { connect } from 'react-redux';
import {
submitEstimate,
editEstimate,
deleteEstimate,
fetchEstimate,
fetchEstimatesTable,
deliverEstimate,
approveEstimate,
rejectEstimate,
} from 'store/Estimate/estimates.actions';
import t from 'store/types';
const mapDispatchToProps = (dispatch) => ({
requestSubmitEstimate: (form) => dispatch(submitEstimate({ form })),
requestFetchEstimate: (id) => dispatch(fetchEstimate({ id })),
requestEditEstimate: (id, form) => dispatch(editEstimate(id, form)),
requestFetchEstimatesTable: (query = {}) =>
dispatch(fetchEstimatesTable({ query: { ...query } })),
requestDeleteEstimate: (id) => dispatch(deleteEstimate({ id })),
requestDeliveredEstimate: (id) => dispatch(deliverEstimate({ id })),
requestApproveEstimate: (id) => dispatch(approveEstimate({ id })),
requestRejectEstimate: (id) => dispatch(rejectEstimate({ id })),
changeEstimateView: (id) =>
dispatch({
type: t.ESTIMATES_SET_CURRENT_VIEW,
currentViewId: parseInt(id, 10),
}),
addEstimatesTableQueries: (queries) =>
dispatch({
type: t.ESTIMATES_TABLE_QUERIES_ADD,
payload: { queries },
}),
setEstimateNumberChanged: (isChanged) =>
dispatch({
type: t.ESTIMATE_NUMBER_CHANGED,
payload: { isChanged },
}),
setSelectedRowsEstimates: (selectedRows) =>
dispatch({
type: t.ESTIMATES_SELECTED_ROWS_SET,
payload: { selectedRows },
}),
});
export default connect(null, mapDispatchToProps);

View File

@@ -1,36 +0,0 @@
import { connect } from 'react-redux';
import { getResourceViews } from 'store/customViews/customViews.selectors';
import {
getEstimateCurrentPageFactory,
getEstimatesTableQueryFactory,
getEstimatesPaginationMetaFactory,
getEstimatesCurrentViewIdFactory,
} from 'store/Estimate/estimates.selectors';
export default (mapState) => {
const getEstimatesItems = getEstimateCurrentPageFactory();
const getEstimatesPaginationMeta = getEstimatesPaginationMetaFactory();
const getEstimatesTableQuery = getEstimatesTableQueryFactory();
const getEstimatesCurrentViewId = getEstimatesCurrentViewIdFactory();
const mapStateToProps = (state, props) => {
const query = getEstimatesTableQuery(state, props);
const mapped = {
estimatesCurrentPage: getEstimatesItems(state, props, query),
estimatesCurrentViewId: getEstimatesCurrentViewId(state, props),
estimateViews: getResourceViews(state, props, 'sale_estimate'),
estimateItems: state.salesEstimates.items,
estimateSelectedRows: state.salesEstimates.selectedRows,
estimatesTableQuery: query,
estimatesPageination: getEstimatesPaginationMeta(state, props, query),
estimatesLoading: state.salesEstimates.loading,
estimateNumberChanged: state.salesEstimates.journalNumberChanged,
};
return mapState ? mapState(mapped, state, props) : mapped;
};
return connect(mapStateToProps);
};

View File

@@ -17,7 +17,7 @@ import { FormattedMessage as T, useIntl } from 'react-intl';
import { If, DashboardActionViewsList } from 'components';
import DashboardActionsBar from 'components/Dashboard/DashboardActionsBar';
import withEstimateActions from './withEstimateActions';
import withEstimatesActions from './withEstimatesActions';
import { useEstimatesListContext } from './EstimatesListProvider';
import { compose } from 'utils';
@@ -27,7 +27,7 @@ import { compose } from 'utils';
*/
function EstimateActionsBar({
// #withEstimateActions
addEstimatesTableQueries,
setEstimatesTableState,
}) {
const history = useHistory();
const { formatMessage } = useIntl();
@@ -42,9 +42,9 @@ function EstimateActionsBar({
history.push('/estimates/new');
};
const handleTabChange = (viewId) => {
addEstimatesTableQueries({
custom_view_id: viewId.id || null,
const handleTabChange = (customView) => {
setEstimatesTableState({
customViewId: customView.id || null,
});
};
@@ -111,5 +111,5 @@ function EstimateActionsBar({
}
export default compose(
withEstimateActions,
withEstimatesActions,
)(EstimateActionsBar);

View File

@@ -0,0 +1,127 @@
import React, { useCallback } from 'react';
import classNames from 'classnames';
import { useHistory } from 'react-router-dom';
import { CLASSES } from 'common/classes';
import { compose } from 'utils';
import { DataTable } from 'components';
import EstimatesEmptyStatus from './EstimatesEmptyStatus';
import TableSkeletonRows from 'components/Datatable/TableSkeletonRows';
import TableSkeletonHeader from 'components/Datatable/TableHeaderSkeleton';
import withEstimatesActions from './withEstimatesActions';
import withSettings from 'containers/Settings/withSettings';
import withAlertsActions from 'containers/Alert/withAlertActions';
import { useEstimatesListContext } from './EstimatesListProvider';
import { ActionsMenu, useEstiamtesTableColumns } from './components';
/**
* Estimates datatable.
*/
function EstimatesDataTable({
// #withEstimatesActions
setEstimatesTableState,
// #withAlertsActions
openAlert,
}) {
const history = useHistory();
// Estimates list context.
const {
estimates,
pagination,
isEmptyStatus,
isEstimatesLoading,
isEstimatesFetching,
} = useEstimatesListContext();
// Estimates table columns.
const columns = useEstiamtesTableColumns();
// Handle estimate edit action.
const handleEditEstimate = (estimate) => {
history.push(`/estimates/${estimate.id}/edit`);
};
// Handle estimate delete action.
const handleDeleteEstimate = ({ id }) => {
openAlert('estimate-delete', { estimateId: id });
};
// Handle cancel/confirm estimate deliver.
const handleDeliverEstimate = ({ id }) => {
openAlert('estimate-deliver', { estimateId: id });
};
// Handle cancel/confirm estimate approve.
const handleApproveEstimate = ({ id }) => {
openAlert('estimate-Approve', { estimateId: id });
};
// Handle cancel/confirm estimate reject.
const handleRejectEstimate = ({ id }) => {
openAlert('estimate-reject', { estimateId: id });
};
// Handles fetch data.
const handleFetchData = useCallback(
({ pageIndex, pageSize, sortBy }) => {
setEstimatesTableState({
pageIndex,
pageSize,
sortBy,
});
},
[setEstimatesTableState],
);
if (isEmptyStatus) {
return <EstimatesEmptyStatus />;
}
return (
<div className={classNames(CLASSES.DASHBOARD_DATATABLE)}>
<DataTable
columns={columns}
data={estimates}
loading={isEstimatesLoading}
headerLoading={isEstimatesLoading}
progressBarLoading={isEstimatesFetching}
onFetchData={handleFetchData}
noInitialFetch={true}
manualSortBy={true}
selectionColumn={true}
sticky={true}
pagination={true}
manualPagination={true}
pagesCount={pagination.pagesCount}
TableLoadingRenderer={TableSkeletonRows}
TableHeaderSkeletonRenderer={TableSkeletonHeader}
ContextMenu={ActionsMenu}
payload={{
onApprove: handleApproveEstimate,
onEdit: handleEditEstimate,
onReject: handleRejectEstimate,
onDeliver: handleDeliverEstimate,
onDelete: handleDeleteEstimate,
}}
/>
</div>
);
}
export default compose(
withEstimatesActions,
withAlertsActions,
withSettings(({ organizationSettings }) => ({
baseCurrency: organizationSettings?.baseCurrency,
})),
)(EstimatesDataTable);

View File

@@ -2,15 +2,16 @@ import React, { useEffect } from 'react';
import { FormattedMessage as T, useIntl } from 'react-intl';
import DashboardPageContent from 'components/Dashboard/DashboardPageContent';
import EstimateActionsBar from './EstimateActionsBar';
import EstimatesAlerts from './EstimatesAlerts';
import EstiamtesViewPage from './EstiamtesViewPage';
import EstimatesActionsBar from './EstimatesActionsBar';
import EstimatesAlerts from '../EstimatesAlerts';
import EstimatesViewTabs from './EstimatesViewTabs';
import EstimatesDataTable from './EstimatesDataTable';
import withDashboardActions from 'containers/Dashboard/withDashboardActions';
import withEstimates from './withEstimates';
import { EstimatesListProvider } from './EstimatesListProvider';
import { compose } from 'utils';
import { compose, transformTableStateToQuery } from 'utils';
/**
* Sale estimates list page.
@@ -20,7 +21,7 @@ function EstimatesList({
changePageTitle,
// #withEstimate
estimatesTableQuery,
estimatesTableState,
}) {
const { formatMessage } = useIntl();
@@ -29,20 +30,22 @@ function EstimatesList({
}, [changePageTitle, formatMessage]);
return (
<EstimatesListProvider query={estimatesTableQuery}>
<EstimateActionsBar />
<EstimatesListProvider
query={transformTableStateToQuery(estimatesTableState)}
>
<EstimatesActionsBar />
<DashboardPageContent>
<EstiamtesViewPage />
<EstimatesAlerts />
<EstimatesViewTabs />
<EstimatesDataTable />
</DashboardPageContent>
<EstimatesAlerts />
</EstimatesListProvider>
);
}
export default compose(
withDashboardActions,
withEstimates(({ estimatesTableQuery }) => ({
estimatesTableQuery,
})),
withEstimates(({ estimatesTableState }) => ({ estimatesTableState })),
)(EstimatesList);

View File

@@ -1,6 +1,7 @@
import React, { createContext } from 'react';
import DashboardInsider from 'components/Dashboard/DashboardInsider';
import { useResourceViews, useResourceFields, useEstimates } from 'hooks/query';
import { isTableEmptyStatus } from 'utils';
const EstimatesListContext = createContext();
@@ -8,12 +9,12 @@ const EstimatesListContext = createContext();
* Sale estimates data provider.
*/
function EstimatesListProvider({ query, ...props }) {
// Fetch estimates resource views and fields.
// Fetches estimates resource views and fields.
const { data: estimatesViews, isFetching: isViewsLoading } = useResourceViews(
'sale_estimates',
);
// Fetch the estimates resource fields.
// Fetches the estimates resource fields.
const {
data: estimatesFields,
isFetching: isFieldsLoading,
@@ -21,9 +22,18 @@ function EstimatesListProvider({ query, ...props }) {
// Fetch estimates list according to the given custom view id.
const {
data: { estimates, pagination },
isFetching: isEstimatesLoading,
} = useEstimates(query);
data: { estimates, pagination, filterMeta },
isLoading: isEstimatesLoading,
isFetching: isEstimatesFetching,
} = useEstimates(query, { keepPreviousData: true });
// Detarmines the datatable empty status.
const isEmptyStatus =
isTableEmptyStatus({
data: estimates,
pagination,
filterMeta,
}) && !isEstimatesFetching;
// Provider payload.
const provider = {
@@ -33,8 +43,11 @@ function EstimatesListProvider({ query, ...props }) {
estimatesViews,
isEstimatesLoading,
isEstimatesFetching,
isFieldsLoading,
isViewsLoading,
isEmptyStatus,
};
return (

View File

@@ -1,13 +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 withEstimateActions from './withEstimateActions';
import withEstimatesActions from './withEstimatesActions';
import withEstimates from './withEstimates';
import { useEstimatesListContext } from './EstimatesListProvider';
import { compose } from 'utils';
@@ -16,11 +14,12 @@ import { compose } from 'utils';
* Estimates views tabs.
*/
function EstimateViewTabs({
//#withEstimatesActions
addEstimatesTableQueries,
}) {
const { custom_view_id: customViewId = null } = useParams();
// #withEstimatesActions
setEstimatesTableState,
// #withEstimates
estimatesTableState
}) {
// Estimates list context.
const { estimatesViews } = useEstimatesListContext();
@@ -29,8 +28,8 @@ function EstimateViewTabs({
}));
const handleTabsChange = (viewId) => {
addEstimatesTableQueries({
custom_view_id: viewId || null,
setEstimatesTableState({
customViewId: viewId || null,
});
};
@@ -38,7 +37,7 @@ function EstimateViewTabs({
<Navbar className={'navbar--dashboard-views'}>
<NavbarGroup align={Alignment.LEFT}>
<DashboardViewsTabs
initialViewId={customViewId}
customViewId={estimatesTableState.customViewId}
resourceName={'estimates'}
tabs={tabs}
onChange={handleTabsChange}
@@ -49,5 +48,6 @@ function EstimateViewTabs({
}
export default compose(
withEstimateActions,
withEstimatesActions,
withEstimates(({ estimatesTableState }) => ({ estimatesTableState })),
)(EstimateViewTabs);

View File

@@ -0,0 +1,195 @@
import React from 'react';
import {
Intent,
Tag,
Button,
Popover,
Menu,
MenuItem,
MenuDivider,
Position,
} from '@blueprintjs/core';
import { Money, Choose, Icon, If } from 'components';
import { FormattedMessage as T, useIntl } from 'react-intl';
import { safeCallback } from 'utils';
import moment from 'moment';
/**
* Status accessor.
*/
export const statusAccessor = (row) => (
<Choose>
<Choose.When condition={row.is_delivered && row.is_approved}>
<Tag minimal={true} intent={Intent.SUCCESS}>
<T id={'approved'} />
</Tag>
</Choose.When>
<Choose.When condition={row.is_delivered && row.is_rejected}>
<Tag minimal={true} intent={Intent.DANGER}>
<T id={'rejected'} />
</Tag>
</Choose.When>
<Choose.When
condition={row.is_delivered && !row.is_rejected && !row.is_approved}
>
<Tag minimal={true} intent={Intent.SUCCESS}>
<T id={'delivered'} />
</Tag>
</Choose.When>
<Choose.Otherwise>
<Tag minimal={true}>
<T id={'draft'} />
</Tag>
</Choose.Otherwise>
</Choose>
);
/**
* Actions menu.
*/
export function ActionsMenu({
row: { original },
payload: { onEdit, onDeliver, onReject, onApprove, 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_estimate' })}
onClick={safeCallback(onEdit, original)}
/>
<If condition={!original.is_delivered}>
<MenuItem
text={formatMessage({ id: 'mark_as_delivered' })}
onClick={safeCallback(onDeliver, original)}
/>
</If>
<Choose>
<Choose.When condition={original.is_delivered && original.is_approved}>
<MenuItem
text={formatMessage({ id: 'mark_as_rejected' })}
onClick={safeCallback(onReject, original)}
/>
</Choose.When>
<Choose.When condition={original.is_delivered && original.is_rejected}>
<MenuItem
text={formatMessage({ id: 'mark_as_approved' })}
onClick={safeCallback(onApprove, original)}
/>
</Choose.When>
<Choose.When condition={original.is_delivered}>
<MenuItem
text={formatMessage({ id: 'mark_as_approved' })}
onClick={safeCallback(onApprove, original)}
/>
<MenuItem
text={formatMessage({ id: 'mark_as_rejected' })}
onClick={safeCallback(onReject, original)}
/>
</Choose.When>
</Choose>
<MenuItem
text={formatMessage({ id: 'delete_estimate' })}
intent={Intent.DANGER}
onClick={safeCallback(onDelete, original)}
icon={<Icon icon="trash-16" iconSize={16} />}
/>
</Menu>
);
}
function DateCell({ value }) {
return moment(value).format('YYYY MMM DD');
}
function AmountAccessor(row) {
return <Money amount={row.amount} currency={'USD'} />;
}
function ActionsCell(props) {
return (
<Popover
content={<ActionsMenu {...props} />}
position={Position.RIGHT_BOTTOM}
>
<Button icon={<Icon icon="more-h-16" iconSize={16} />} />
</Popover>
);
}
export function useEstiamtesTableColumns() {
const { formatMessage } = useIntl();
return React.useMemo(
() => [
{
id: 'estimate_date',
Header: formatMessage({ id: 'estimate_date' }),
accessor: 'estimate_date',
Cell: DateCell,
width: 140,
className: 'estimate_date',
},
{
id: 'customer_id',
Header: formatMessage({ id: 'customer_name' }),
accessor: 'customer.display_name',
width: 140,
className: 'customer_id',
},
{
id: 'expiration_date',
Header: formatMessage({ id: 'expiration_date' }),
accessor: 'expiration_date',
Cell: DateCell,
width: 140,
className: 'expiration_date',
},
{
id: 'estimate_number',
Header: formatMessage({ id: 'estimate_number' }),
accessor: (row) =>
row.estimate_number ? `#${row.estimate_number}` : null,
width: 140,
className: 'estimate_number',
},
{
id: 'amount',
Header: formatMessage({ id: 'amount' }),
accessor: AmountAccessor,
width: 140,
className: 'amount',
},
{
id: 'status',
Header: formatMessage({ id: 'status' }),
accessor: (row) => statusAccessor(row),
width: 140,
className: 'status',
},
{
id: 'reference',
Header: formatMessage({ id: 'reference_no' }),
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 {
getEstimatesTableStateFactory,
} from 'store/Estimate/estimates.selectors';
export default (mapState) => {
const getEstimatesTableState = getEstimatesTableStateFactory();
const mapStateToProps = (state, props) => {
const mapped = {
estimatesTableState: getEstimatesTableState(state, props),
};
return mapState ? mapState(mapped, state, props) : mapped;
};
return connect(mapStateToProps);
};

View File

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

View File

@@ -1,273 +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 { CLASSES } from 'common/classes';
import { compose, saveInvoke } from 'utils';
import { useIsValuePassed } from 'hooks';
import {
LoadingIndicator,
Choose,
If,
DataTable,
Money,
Icon,
} from 'components';
import InvoicesEmptyStatus from './InvoicesEmptyStatus';
import { statusAccessor } from './components';
import withDialogActions from 'containers/Dialog/withDialogActions';
import withDashboardActions from 'containers/Dashboard/withDashboardActions';
import withViewDetails from 'containers/Views/withViewDetails';
import withInvoices from './withInvoices';
import withInvoiceActions from './withInvoiceActions';
import withCurrentView from 'containers/Views/withCurrentView';
import withSettings from 'containers/Settings/withSettings';
// Invoices datatable.
function InvoicesDataTable({
// #withInvoices
invoicesCurrentPage,
invoicesLoading,
invoicesPageination,
invoicesCurrentViewId,
// #withInvoicesActions
addInvoiceTableQueries,
// #withSettings
baseCurrency,
// #OwnProps
onEditInvoice,
onDeleteInvoice,
onDeliverInvoice,
onSelectedRowsChange,
}) {
const { formatMessage } = useIntl();
const isLoadedBefore = useIsValuePassed(invoicesLoading, false);
const handleEditInvoice = useCallback(
(_invoice) => () => {
saveInvoke(onEditInvoice, _invoice);
},
[onEditInvoice],
);
const handleDeleteInvoice = useCallback(
(_invoice) => () => {
saveInvoke(onDeleteInvoice, _invoice);
},
[onDeleteInvoice],
);
const actionMenuList = useCallback(
(invoice) => (
<Menu>
<MenuItem
icon={<Icon icon="reader-18" />}
text={formatMessage({ id: 'view_details' })}
/>
<MenuDivider />
<MenuItem
icon={<Icon icon="pen-18" />}
text={formatMessage({ id: 'edit_invoice' })}
onClick={handleEditInvoice(invoice)}
/>
<If condition={!invoice.is_delivered}>
<MenuItem
text={formatMessage({ id: 'mark_as_delivered' })}
onClick={() => onDeliverInvoice(invoice)}
/>
</If>
<MenuItem
text={formatMessage({ id: 'delete_invoice' })}
intent={Intent.DANGER}
onClick={handleDeleteInvoice(invoice)}
icon={<Icon icon="trash-16" iconSize={16} />}
/>
</Menu>
),
[handleDeleteInvoice, handleEditInvoice, formatMessage],
);
const onRowContextMenu = useCallback(
(cell) => {
return actionMenuList(cell.row.original);
},
[actionMenuList],
);
const columns = useMemo(
() => [
{
id: 'invoice_date',
Header: formatMessage({ id: 'invoice_date' }),
accessor: (r) => moment(r.invoice_date).format('YYYY MMM DD'),
width: 110,
className: 'invoice_date',
},
{
id: 'customer_id',
Header: formatMessage({ id: 'customer_name' }),
accessor: 'customer.display_name',
width: 180,
className: 'customer_id',
},
{
id: 'invoice_no',
Header: formatMessage({ id: 'invoice_no__' }),
accessor: (row) => (row.invoice_no ? `#${row.invoice_no}` : null),
width: 100,
className: 'invoice_no',
},
{
id: 'balance',
Header: formatMessage({ id: 'balance' }),
accessor: (r) => <Money amount={r.balance} currency={baseCurrency} />,
width: 110,
className: 'balance',
},
{
id: 'status',
Header: formatMessage({ id: 'status' }),
accessor: (row) => statusAccessor(row),
width: 160,
className: 'status',
},
{
id: 'due_date',
Header: formatMessage({ id: 'due_date' }),
accessor: (r) => moment(r.due_date).format('YYYY MMM DD'),
width: 110,
className: 'due_date',
},
{
id: 'reference_no',
Header: formatMessage({ id: 'reference_no' }),
accessor: 'reference_no',
width: 90,
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 handleDataTableFetchData = useCallback(
({ pageSize, pageIndex, sortBy }) => {
addInvoiceTableQueries({
...(sortBy.length > 0
? {
column_sort_by: sortBy[0].id,
sort_order: sortBy[0].desc ? 'desc' : 'asc',
}
: {}),
page_size: pageSize,
page: pageIndex + 1,
});
},
[addInvoiceTableQueries],
);
const handleSelectedRowsChange = useCallback(
(selectedRows) => {
saveInvoke(
onSelectedRowsChange,
selectedRows.map((s) => s.original),
);
},
[onSelectedRowsChange],
);
const showEmptyStatus = [
invoicesCurrentPage.length === 0,
invoicesCurrentViewId === -1,
].every((d) => d === true);
return (
<div className={classNames(CLASSES.DASHBOARD_DATATABLE)}>
<LoadingIndicator loading={invoicesLoading && !isLoadedBefore}>
<Choose>
<Choose.When condition={showEmptyStatus}>
<InvoicesEmptyStatus />
</Choose.When>
<Choose.Otherwise>
<DataTable
columns={columns}
data={invoicesCurrentPage}
onFetchData={handleDataTableFetchData}
manualSortBy={true}
selectionColumn={true}
noInitialFetch={true}
sticky={true}
onSelectedRowsChange={handleSelectedRowsChange}
rowContextMenu={onRowContextMenu}
pagination={true}
autoResetSortBy={false}
autoResetPage={false}
pagesCount={invoicesPageination.pagesCount}
initialPageSize={invoicesPageination.pageSize}
initialPageIndex={invoicesPageination.page - 1}
/>
</Choose.Otherwise>
</Choose>
</LoadingIndicator>
</div>
);
}
export default compose(
withRouter,
withCurrentView,
withDialogActions,
withDashboardActions,
withInvoiceActions,
withInvoices(
({
invoicesCurrentPage,
invoicesLoading,
invoicesPageination,
invoicesTableQuery,
invoicesCurrentViewId,
}) => ({
invoicesCurrentPage,
invoicesLoading,
invoicesPageination,
invoicesTableQuery,
invoicesCurrentViewId,
}),
),
withSettings(({ organizationSettings }) => ({
baseCurrency: organizationSettings?.baseCurrency,
})),
withViewDetails(),
)(InvoicesDataTable);

View File

@@ -1,73 +0,0 @@
import React, { useCallback } from 'react';
import { Switch, Route, useHistory } from 'react-router-dom';
import InvoicesDataTable from './InvoicesDataTable';
import InvoiceViewTabs from './InvoiceViewTabs';
import withAlertsActions from 'containers/Alert/withAlertActions';
import withDialogActions from 'containers/Dialog/withDialogActions';
import { compose } from 'utils';
/**
* Invoices list view page.
*/
function InvoicesViewPage({
// #withAlertActions
openAlert,
}) {
const history = useHistory();
// Handle delete sale invoice.
const handleDeleteInvoice = useCallback(
({ id }) => {
openAlert('invoice-delete', { invoiceId: id });
},
[openAlert],
);
// Handle cancel/confirm invoice deliver.
const handleDeliverInvoice = useCallback(
({ id }) => {
openAlert('invoice-deliver', { invoiceId: id });
},
[openAlert],
);
// Handle edit sale invoice.
const handleEditInvoice = useCallback(
(invoice) => {
history.push(`/invoices/${invoice.id}/edit`);
},
[history],
);
// Handle selected rows change.
const handleSelectedRowsChange = useCallback(
(invoices) => {
},
[],
);
return (
<Switch>
<Route
exact={true}
path={['/invoices/:custom_view_id/custom_view', '/invoices']}
>
<InvoiceViewTabs />
{/* <InvoicesDataTable
onDeleteInvoice={handleDeleteInvoice}
onEditInvoice={handleEditInvoice}
onDeliverInvoice={handleDeliverInvoice}
onSelectedRowsChange={handleSelectedRowsChange}
/> */}
</Route>
</Switch>
);
}
export default compose(
withAlertsActions,
withDialogActions,
)(InvoicesViewPage)

View File

@@ -1,79 +0,0 @@
import React from 'react';
import { Intent, Tag, ProgressBar } from '@blueprintjs/core';
import { Choose, If, Icon } from 'components';
import { FormattedMessage as T, useIntl } from 'react-intl';
import { AppToaster } from 'components';
import { formatMessage } from 'services/intl';
const calculateStatus = (paymentAmount, balanceAmount) =>
paymentAmount / balanceAmount;
export const statusAccessor = (row) => {
return (
<div className={'status-accessor'}>
<Choose>
<Choose.When condition={row.is_fully_paid && row.is_delivered}>
<span className={'fully-paid-icon'}>
<Icon icon="small-tick" iconSize={18} />
</span>
<span class="fully-paid-text">
<T id={'paid'} />
</span>
</Choose.When>
<Choose.When condition={row.is_delivered}>
<Choose>
<Choose.When condition={row.is_overdue}>
<span className={'overdue-status'}>
<T id={'overdue_by'} values={{ overdue: row.overdue_days }} />
</span>
</Choose.When>
<Choose.Otherwise>
<span className={'due-status'}>
<T id={'due_in'} values={{ due: row.remaining_days }} />
</span>
</Choose.Otherwise>
</Choose>
<If condition={row.is_partially_paid}>
<span class="partial-paid">
<T
id={'day_partially_paid'}
values={{
due: row.due_amount,
currencySign: '$',
}}
/>
</span>
<ProgressBar
animate={false}
stripes={false}
intent={Intent.PRIMARY}
value={calculateStatus(row.payment_amount, row.balance)}
/>
</If>
</Choose.When>
<Choose.Otherwise>
<Tag minimal={true}>
<T id={'draft'} />
</Tag>
</Choose.Otherwise>
</Choose>
</div>
);
};
export const handleDeleteErrors = (errors) => {
if (
errors.find(
(error) => error.type === 'INVOICE_HAS_ASSOCIATED_PAYMENT_ENTRIES',
)
) {
AppToaster.show({
message: formatMessage({
id: 'the_invoice_cannot_be_deleted',
}),
intent: Intent.DANGER,
});
}
};

View File

@@ -1,40 +0,0 @@
import { connect } from 'react-redux';
import {
submitInvoice,
editInvoice,
deleteInvoice,
fetchInvoice,
fetchInvoicesTable,
fetchDueInvoices,
deliverInvoice,
} from 'store/Invoice/invoices.actions';
import t from 'store/types';
const mapDipatchToProps = (dispatch) => ({
requestSubmitInvoice: (form) => dispatch(submitInvoice({ form })),
requsetFetchInvoice: (id) => dispatch(fetchInvoice({ id })),
requestEditInvoice: (id, form) => dispatch(editInvoice(id, form)),
requestFetchInvoiceTable: (query = {}) =>
dispatch(fetchInvoicesTable({ query: { ...query } })),
requestDeleteInvoice: (id) => dispatch(deleteInvoice({ id })),
requestFetchDueInvoices: (customerId) =>
dispatch(fetchDueInvoices({ customerId })),
requestDeliverInvoice: (id) => dispatch(deliverInvoice({ id })),
changeInvoiceView: (id) =>
dispatch({
type: t.INVOICES_SET_CURRENT_VIEW,
currentViewId: parseInt(id, 10),
}),
addInvoiceTableQueries: (queries) =>
dispatch({
type: t.INVOICES_TABLE_QUERIES_ADD,
payload: { queries },
}),
setInvoiceNumberChanged: (isChanged) =>
dispatch({
type: t.INVOICE_NUMBER_CHANGED,
payload: { isChanged },
}),
});
export default connect(null, mapDipatchToProps);

View File

@@ -1,11 +0,0 @@
import { connect } from 'react-redux';
import { getInvoiecsByIdFactory } from 'store/Invoice/invoices.selector';
export default () => {
const getInvoiceById = getInvoiecsByIdFactory();
const mapStateToProps = (state, props) => ({
invoice: getInvoiceById(state, props),
});
return connect(mapStateToProps);
};

View File

@@ -1,42 +0,0 @@
import { connect } from 'react-redux';
import { getResourceViews } from 'store/customViews/customViews.selectors';
import {
getInvoiceCurrentPageFactory,
getInvoicePaginationMetaFactory,
getInvoiceTableQueryFactory,
getCustomerReceivableInvoicesEntriesFactory,
getInvoicesCurrentViewIdFactory,
} from 'store/Invoice/invoices.selector';
export default (mapState) => {
const getInvoicesItems = getInvoiceCurrentPageFactory();
const getInvoicesPaginationMeta = getInvoicePaginationMetaFactory();
const getInvoiceTableQuery = getInvoiceTableQueryFactory();
const getCustomerReceivableInvoicesEntries = getCustomerReceivableInvoicesEntriesFactory();
const getInvoicesCurrentViewId = getInvoicesCurrentViewIdFactory();
const mapStateToProps = (state, props) => {
const query = getInvoiceTableQuery(state, props);
const mapped = {
invoicesCurrentPage: getInvoicesItems(state, props, query),
invoicesCurrentViewId: getInvoicesCurrentViewId(state, props),
invoicesViews: getResourceViews(state, props, 'sale_invoice'),
invoicesItems: state.salesInvoices.items,
invoicesTableQuery: query,
invoicesPageination: getInvoicesPaginationMeta(state, props, query),
invoicesLoading: state.salesInvoices.loading,
customerInvoiceEntries: getCustomerReceivableInvoicesEntries(
state,
props,
),
invoiceNumberChanged: state.salesInvoices.journalNumberChanged,
};
return mapState ? mapState(mapped, state, props) : mapped;
};
return connect(mapStateToProps);
};

View File

@@ -29,7 +29,7 @@ import { compose } from 'utils';
*/
function InvoiceActionsBar({
// #withInvoiceActions
addInvoiceTableQueries,
setInvoicesTableState,
}) {
const history = useHistory();
const { formatMessage } = useIntl();
@@ -45,9 +45,9 @@ function InvoiceActionsBar({
};
// Handle views tab change.
const handleTabChange = (viewId) => {
addInvoiceTableQueries({
custom_view_id: viewId.id || null,
const handleTabChange = (customView) => {
setInvoicesTableState({
customViewId: customView.id || null,
});
};

View File

@@ -1,11 +1,11 @@
import React from 'react';
import { useHistory } from 'react-router';
import { Alignment, Navbar, NavbarGroup } from '@blueprintjs/core';
import { useParams } from 'react-router-dom';
import { pick } from 'lodash';
import { DashboardViewsTabs } from 'components';
import withInvoices from './withInvoices';
import withInvoiceActions from './withInvoiceActions';
import { compose } from 'utils';
@@ -16,10 +16,12 @@ import { useInvoicesListContext } from './InvoicesListProvider';
*/
function InvoiceViewTabs({
// #withInvoiceActions
addInvoiceTableQueries,
setInvoicesTableState,
// #withInvoices
invoicesTableState
}) {
const history = useHistory();
const { custom_view_id: customViewId = null } = useParams();
// Invoices list context.
const { invoicesViews } = useInvoicesListContext();
@@ -29,9 +31,9 @@ function InvoiceViewTabs({
}));
// Handle tab change.
const handleTabsChange = (viewId) => {
addInvoiceTableQueries({
custom_view_id: customViewId || null,
const handleTabsChange = (customView) => {
setInvoicesTableState({
customViewId: customView.id || null,
});
};
@@ -44,7 +46,7 @@ function InvoiceViewTabs({
<Navbar className={'navbar--dashboard-views'}>
<NavbarGroup align={Alignment.LEFT}>
<DashboardViewsTabs
initialViewId={customViewId}
customViewId={invoicesTableState.customViewId}
resourceName={'invoices'}
tabs={tabs}
onNewViewTabClick={handleClickNewView}
@@ -57,4 +59,5 @@ function InvoiceViewTabs({
export default compose(
withInvoiceActions,
withInvoices(({ invoicesTableState }) => ({ invoicesTableState })),
)(InvoiceViewTabs);

View File

@@ -0,0 +1,119 @@
import React, { useCallback } from 'react';
import { useHistory } from 'react-router-dom';
import classNames from 'classnames';
import InvoicesEmptyStatus from './InvoicesEmptyStatus';
import { CLASSES } from 'common/classes';
import { compose } from 'utils';
import { DataTable } from 'components';
import TableSkeletonRows from 'components/Datatable/TableSkeletonRows';
import TableSkeletonHeader from 'components/Datatable/TableHeaderSkeleton';
import withDashboardActions from 'containers/Dashboard/withDashboardActions';
import withInvoiceActions from './withInvoiceActions';
import withSettings from 'containers/Settings/withSettings';
import withAlertsActions from 'containers/Alert/withAlertActions';
import { useInvoicesTableColumns, ActionsMenu } from './components';
import { useInvoicesListContext } from './InvoicesListProvider';
/**
* Invoices datatable.
*/
function InvoicesDataTable({
// #withInvoicesActions
setInvoicesTableState,
// #withSettings
baseCurrency,
// #withAlertsActions
openAlert,
}) {
const history = useHistory();
// Invoices list context.
const {
invoices,
pagination,
isEmptyStatus,
isInvoicesLoading,
isInvoicesFetching,
} = useInvoicesListContext();
// Invoices table columns.
const columns = useInvoicesTableColumns();
// Handle delete sale invoice.
const handleDeleteInvoice = ({ id }) => {
openAlert('invoice-delete', { invoiceId: id });
};
// Handle cancel/confirm invoice deliver.
const handleDeliverInvoice = ({ id }) => {
openAlert('invoice-deliver', { invoiceId: id });
};
// Handle edit sale invoice.
const handleEditInvoice = (invoice) => {
history.push(`/invoices/${invoice.id}/edit`);
};
// Handles fetch data once the table state change.
const handleDataTableFetchData = useCallback(
({ pageSize, pageIndex, sortBy }) => {
setInvoicesTableState({
pageSize,
pageIndex,
sortBy,
});
},
[setInvoicesTableState],
);
// Display invoice empty status.
if (isEmptyStatus) {
return <InvoicesEmptyStatus />;
}
return (
<div className={classNames(CLASSES.DASHBOARD_DATATABLE)}>
<DataTable
columns={columns}
data={invoices}
loading={isInvoicesLoading}
headerLoading={isInvoicesLoading}
progressBarLoading={isInvoicesFetching}
onFetchData={handleDataTableFetchData}
manualSortBy={true}
selectionColumn={true}
noInitialFetch={true}
sticky={true}
pagination={true}
manualPagination={true}
pagesCount={pagination.pagesCount}
autoResetSortBy={false}
autoResetPage={false}
TableLoadingRenderer={TableSkeletonRows}
TableHeaderSkeletonRenderer={TableSkeletonHeader}
ContextMenu={ActionsMenu}
payload={{
onDelete: handleDeleteInvoice,
onDeliver: handleDeliverInvoice,
onEdit: handleEditInvoice,
baseCurrency
}}
/>
</div>
);
}
export default compose(
withDashboardActions,
withInvoiceActions,
withAlertsActions,
withSettings(({ organizationSettings }) => ({
baseCurrency: organizationSettings?.baseCurrency,
})),
)(InvoicesDataTable);

View File

@@ -7,14 +7,15 @@ import DashboardPageContent from 'components/Dashboard/DashboardPageContent';
import InvoiceActionsBar from './InvoiceActionsBar';
import { InvoicesListProvider } from './InvoicesListProvider';
import InvoicesViewPage from './InvoicesViewPage';
import InvoicesAlerts from './InvoicesAlerts';
import InvoiceViewTabs from './InvoiceViewTabs';
import InvoicesDataTable from './InvoicesDataTable';
import InvoicesAlerts from '../InvoicesAlerts';
import withDashboardActions from 'containers/Dashboard/withDashboardActions';
import withInvoices from './withInvoices';
import withAlertsActions from 'containers/Alert/withAlertActions';
import { compose } from 'utils';
import { transformTableStateToQuery, compose } from 'utils';
/**
* Sale invoices list.
@@ -24,7 +25,7 @@ function InvoicesList({
changePageTitle,
// #withInvoice
invoicesTableQuery,
invoicesTableState,
}) {
const { formatMessage } = useIntl();
@@ -33,21 +34,23 @@ function InvoicesList({
}, [changePageTitle, formatMessage]);
return (
<InvoicesListProvider query={invoicesTableQuery}>
<InvoicesListProvider
query={transformTableStateToQuery(invoicesTableState)}
>
<InvoiceActionsBar />
<DashboardPageContent>
<InvoicesViewPage />
<InvoicesAlerts />
<InvoiceViewTabs />
<InvoicesDataTable />
</DashboardPageContent>
<InvoicesAlerts />
</InvoicesListProvider>
);
}
export default compose(
withDashboardActions,
withInvoices(({ invoicesTableQuery }) => ({
invoicesTableQuery,
})),
withInvoices(({ invoicesTableState }) => ({ invoicesTableState })),
withAlertsActions,
)(InvoicesList);

View File

@@ -1,6 +1,7 @@
import React, { createContext } from 'react';
import DashboardInsider from 'components/Dashboard/DashboardInsider';
import { useResourceViews, useResourceFields, useInvoices } from 'hooks/query';
import { isTableEmptyStatus } from 'utils';
const InvoicesListContext = createContext();
@@ -21,9 +22,18 @@ function InvoicesListProvider({ accountsTableQuery, ...props }) {
// Fetch accounts list according to the given custom view id.
const {
data: { invoices, pagination },
isFetching: isInvoicesLoading,
} = useInvoices(accountsTableQuery);
data: { invoices, pagination, filterMeta },
isFetching: isInvoicesFetching,
isLoading: isInvoicesLoading,
} = useInvoices(accountsTableQuery, { keepPreviousData: true });
// Detarmines the datatable empty status.
const isEmptyStatus =
isTableEmptyStatus({
data: invoices,
pagination,
filterMeta,
}) && !isInvoicesLoading;
// Provider payload.
const provider = {
@@ -33,8 +43,11 @@ function InvoicesListProvider({ accountsTableQuery, ...props }) {
invoicesViews,
isInvoicesLoading,
isInvoicesFetching,
isFieldsLoading,
isViewsLoading,
isEmptyStatus
};
return (

View File

@@ -0,0 +1,206 @@
import React from 'react';
import {
Intent,
Tag,
Menu,
MenuItem,
MenuDivider,
ProgressBar,
Popover,
Position,
Button
} from '@blueprintjs/core';
import { Choose, If, Icon } from 'components';
import { FormattedMessage as T, useIntl } from 'react-intl';
import moment from 'moment';
import { Money, AppToaster } from 'components';
import { formatMessage } from 'services/intl';
import { safeCallback } from 'utils';
const calculateStatus = (paymentAmount, balanceAmount) =>
paymentAmount / balanceAmount;
export const statusAccessor = (row) => {
return (
<div className={'status-accessor'}>
<Choose>
<Choose.When condition={row.is_fully_paid && row.is_delivered}>
<span className={'fully-paid-icon'}>
<Icon icon="small-tick" iconSize={18} />
</span>
<span class="fully-paid-text">
<T id={'paid'} />
</span>
</Choose.When>
<Choose.When condition={row.is_delivered}>
<Choose>
<Choose.When condition={row.is_overdue}>
<span className={'overdue-status'}>
<T id={'overdue_by'} values={{ overdue: row.overdue_days }} />
</span>
</Choose.When>
<Choose.Otherwise>
<span className={'due-status'}>
<T id={'due_in'} values={{ due: row.remaining_days }} />
</span>
</Choose.Otherwise>
</Choose>
<If condition={row.is_partially_paid}>
<span class="partial-paid">
<T
id={'day_partially_paid'}
values={{
due: row.due_amount,
currencySign: '$',
}}
/>
</span>
<ProgressBar
animate={false}
stripes={false}
intent={Intent.PRIMARY}
value={calculateStatus(row.payment_amount, row.balance)}
/>
</If>
</Choose.When>
<Choose.Otherwise>
<Tag minimal={true}>
<T id={'draft'} />
</Tag>
</Choose.Otherwise>
</Choose>
</div>
);
};
export const handleDeleteErrors = (errors) => {
if (
errors.find(
(error) => error.type === 'INVOICE_HAS_ASSOCIATED_PAYMENT_ENTRIES',
)
) {
AppToaster.show({
message: formatMessage({
id: 'the_invoice_cannot_be_deleted',
}),
intent: Intent.DANGER,
});
}
};
export function ActionsMenu({
payload: { onEdit, onDeliver, 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_invoice' })}
onClick={safeCallback(onEdit, original)}
/>
<If condition={!original.is_delivered}>
<MenuItem
text={formatMessage({ id: 'mark_as_delivered' })}
onClick={safeCallback(onDeliver, original)}
/>
</If>
<MenuItem
text={formatMessage({ id: 'delete_invoice' })}
intent={Intent.DANGER}
onClick={safeCallback(onDelete, original)}
icon={<Icon icon="trash-16" iconSize={16} />}
/>
</Menu>
);
}
function ActionsCell(props) {
return (
<Popover
content={<ActionsMenu {...props} />}
position={Position.RIGHT_BOTTOM}
>
<Button icon={<Icon icon="more-h-16" iconSize={16} />} />
</Popover>
);
}
/**
* Retrieve invoices table columns.
*/
export function useInvoicesTableColumns() {
const { formatMessage } = useIntl();
return React.useMemo(
() => [
{
id: 'invoice_date',
Header: formatMessage({ id: 'invoice_date' }),
accessor: (r) => moment(r.invoice_date).format('YYYY MMM DD'),
width: 110,
className: 'invoice_date',
},
{
id: 'customer_id',
Header: formatMessage({ id: 'customer_name' }),
accessor: 'customer.display_name',
width: 180,
className: 'customer_id',
},
{
id: 'invoice_no',
Header: formatMessage({ id: 'invoice_no__' }),
accessor: (row) => (row.invoice_no ? `#${row.invoice_no}` : null),
width: 100,
className: 'invoice_no',
},
{
id: 'balance',
Header: formatMessage({ id: 'balance' }),
accessor: (r) => <Money amount={r.balance} currency={'USD'} />,
width: 110,
className: 'balance',
},
{
id: 'status',
Header: formatMessage({ id: 'status' }),
accessor: (row) => statusAccessor(row),
width: 160,
className: 'status',
},
{
id: 'due_date',
Header: formatMessage({ id: 'due_date' }),
accessor: (r) => moment(r.due_date).format('YYYY MMM DD'),
width: 110,
className: 'due_date',
},
{
id: 'reference_no',
Header: formatMessage({ id: 'reference_no' }),
accessor: 'reference_no',
width: 90,
className: 'reference_no',
},
{
id: 'actions',
Header: '',
Cell: ActionsCell,
className: 'actions',
width: 50,
disableResizing: true,
},
],
[formatMessage],
);
}

View File

@@ -0,0 +1,10 @@
import { connect } from 'react-redux';
import {
setInvoicesTableState
} from 'store/Invoice/invoices.actions';
const mapDipatchToProps = (dispatch) => ({
setInvoicesTableState: (query) => setInvoicesTableState(query),
});
export default connect(null, mapDipatchToProps);

View File

@@ -0,0 +1,16 @@
import { connect } from 'react-redux';
import {
getInvoicesTableStateFactory,
} from 'store/Invoice/invoices.selector';
export default (mapState) => {
const getInvoicesTableState = getInvoicesTableStateFactory();
const mapStateToProps = (state, props) => {
const mapped = {
invoicesTableState: getInvoicesTableState(state, props),
};
return mapState ? mapState(mapped, state, props) : mapped;
};
return connect(mapStateToProps);
};

View File

@@ -1,256 +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 PaymentReceivesEmptyStatus from './PaymentReceivesEmptyStatus';
import { LoadingIndicator, DataTable, Choose, Money, Icon } from 'components';
import withDialogActions from 'containers/Dialog/withDialogActions';
import withDashboardActions from 'containers/Dashboard/withDashboardActions';
import withViewDetails from 'containers/Views/withViewDetails';
import withPaymentReceives from './withPaymentReceives';
import withPaymentReceivesActions from './withPaymentReceivesActions';
import withCurrentView from 'containers/Views/withCurrentView';
import withSettings from 'containers/Settings/withSettings';
function PaymentReceivesDataTable({
// #withPaymentReceives
PaymentReceivesCurrentPage,
paymentReceivesPageination,
paymentReceivesLoading,
paymentReceivesTableQuery,
paymentReceivesCurrentViewId,
// #withPaymentReceivesActions
addPaymentReceivesTableQueries,
// #withSettings
baseCurrency,
// #OwnProps
onEditPaymentReceive,
onDeletePaymentReceive,
onSelectedRowsChange,
}) {
const isLoaded = useIsValuePassed(paymentReceivesLoading, false);
const { formatMessage } = useIntl();
const handleEditPaymentReceive = useCallback(
(paymentReceive) => () => {
saveInvoke(onEditPaymentReceive, paymentReceive);
},
[onEditPaymentReceive],
);
const handleDeletePaymentReceive = useCallback(
(paymentReceive) => () => {
onDeletePaymentReceive && onDeletePaymentReceive(paymentReceive);
},
[onDeletePaymentReceive],
);
const handleDataTableFetchData = useCallback(
({ pageIndex, pageSize, sortBy }) => {
addPaymentReceivesTableQueries({
...(sortBy.length > 0
? {
column_sort_by: sortBy[0].id,
sort_order: sortBy[0].desc ? 'desc' : 'asc',
}
: {}),
page_size: pageSize,
page: pageIndex + 1,
});
},
[addPaymentReceivesTableQueries],
);
const handleSelectedRowsChange = useCallback(
(selectedRows) => {
saveInvoke(
onSelectedRowsChange,
selectedRows.map((s) => s.original),
);
},
[onSelectedRowsChange],
);
const actionMenuList = useCallback(
(paymentReceive) => (
<Menu>
<MenuItem
icon={<Icon icon="reader-18" />}
text={formatMessage({ id: 'view_details' })}
/>
<MenuDivider />
<MenuItem
icon={<Icon icon="pen-18" />}
text={formatMessage({ id: 'edit_payment_receive' })}
onClick={handleEditPaymentReceive(paymentReceive)}
/>
<MenuItem
text={formatMessage({ id: 'delete_payment_receive' })}
intent={Intent.DANGER}
onClick={handleDeletePaymentReceive(paymentReceive)}
icon={<Icon icon="trash-16" iconSize={16} />}
/>
</Menu>
),
[handleDeletePaymentReceive, handleEditPaymentReceive, 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: 'customer_id',
Header: formatMessage({ id: 'customer_name' }),
accessor: 'customer.display_name',
width: 140,
className: 'customer_id',
},
{
id: 'payment_receive_no',
Header: formatMessage({ id: 'payment_receive_no' }),
accessor: (row) =>
row.payment_receive_no ? `#${row.payment_receive_no}` : null,
width: 140,
className: 'payment_receive_no',
},
{
id: 'amount',
Header: formatMessage({ id: 'amount' }),
accessor: (r) => <Money amount={r.amount} currency={baseCurrency} />,
width: 140,
className: 'amount',
},
{
id: 'reference_no',
Header: formatMessage({ id: 'reference_no' }),
accessor: 'reference_no',
width: 140,
className: 'reference_no',
},
{
id: 'deposit_account_id',
Header: formatMessage({ id: 'deposit_account' }),
accessor: 'deposit_account.name',
width: 140,
className: 'deposit_account_id',
},
{
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 showEmptyStatus = [
paymentReceivesCurrentViewId === -1,
PaymentReceivesCurrentPage.length === 0,
].every((condition) => condition === true);
return (
<div className={classNames(CLASSES.DASHBOARD_DATATABLE)}>
<LoadingIndicator
loading={paymentReceivesLoading && !isLoaded}
mount={false}
>
<Choose>
<Choose.When condition={showEmptyStatus}>
<PaymentReceivesEmptyStatus />
</Choose.When>
<Choose.Otherwise>
<DataTable
columns={columns}
data={PaymentReceivesCurrentPage}
onFetchData={handleDataTableFetchData}
manualSortBy={true}
selectionColumn={true}
noInitialFetch={true}
sticky={true}
onSelectedRowsChange={handleSelectedRowsChange}
rowContextMenu={onRowContextMenu}
pagination={true}
autoResetSortBy={false}
autoResetPage={false}
pagesCount={paymentReceivesPageination.pagesCount}
initialPageSize={paymentReceivesTableQuery.page_size}
initialPageIndex={paymentReceivesTableQuery.page - 1}
/>
</Choose.Otherwise>
</Choose>
</LoadingIndicator>
</div>
);
}
export default compose(
withRouter,
withCurrentView,
withDialogActions,
withDashboardActions,
withPaymentReceivesActions,
withPaymentReceives(
({
PaymentReceivesCurrentPage,
paymentReceivesLoading,
paymentReceivesPageination,
paymentReceivesTableQuery,
paymentReceivesCurrentViewId,
}) => ({
PaymentReceivesCurrentPage,
paymentReceivesLoading,
paymentReceivesPageination,
paymentReceivesTableQuery,
paymentReceivesCurrentViewId,
}),
),
withSettings(({ organizationSettings }) => ({
baseCurrency: organizationSettings?.baseCurrency,
})),
withViewDetails(),
)(PaymentReceivesDataTable);

View File

@@ -1,55 +0,0 @@
import React, { useCallback } from 'react';
import { Switch, Route, useHistory } from 'react-router-dom';
import PaymentReceivesDataTable from './PaymentReceivesDataTable';
import PaymentReceiveViewTabs from './PaymentReceiveViewTabs';
import withAlertsActions from 'containers/Alert/withAlertActions';
import withDialogActions from 'containers/Dialog/withDialogActions';
import { compose } from 'utils';
/**
* Payment receives view page.
*/
function PaymentReceivesViewPage({
// #withAlertActions
openAlert,
}) {
const history = useHistory();
// Handle delete Payment Receive
const handleDeletePaymentReceive = ({ id }) => {
openAlert('payment-receive-delete', { paymentReceiveId: id });
};
// Handle edit payment receive.
const handleEditPaymentReceive = (payment) => {
history.push(`/payment-receives/${payment.id}/edit`);
};
return (
<Switch>
<Route
exact={true}
path={[
'/payment-receives/:custom_view_id/custom_view',
'/payment-receives',
]}
>
<PaymentReceiveViewTabs />
{/* <PaymentReceivesDataTable
onDeletePaymentReceive={handleDeletePaymentReceive}
onEditPaymentReceive={handleEditPaymentReceive}
onSelectedRowsChange={handleSelectedRowsChange}
/> */}
</Route>
</Switch>
);
}
export default compose(
withAlertsActions,
withDialogActions,
)(PaymentReceivesViewPage)

View File

@@ -1,6 +1,11 @@
import React, { createContext } from 'react';
import React, { createContext, useContext } from 'react';
import DashboardInsider from 'components/Dashboard/DashboardInsider';
import { useResourceViews, useResourceFields, usePaymentReceives } from 'hooks/query';
import {
useResourceViews,
useResourceFields,
usePaymentReceives,
} from 'hooks/query';
import { isTableEmptyStatus } from 'utils';
const PaymentReceivesListContext = createContext();
@@ -9,9 +14,10 @@ const PaymentReceivesListContext = createContext();
*/
function PaymentReceivesListProvider({ query, ...props }) {
// Fetch payment receives resource views and fields.
const { data: paymentReceivesViews, isFetching: isViewsLoading } = useResourceViews(
'payment_receives',
);
const {
data: paymentReceivesViews,
isFetching: isViewsLoading,
} = useResourceViews('payment_receives');
// Fetch the payment receives resource fields.
const {
@@ -21,20 +27,31 @@ function PaymentReceivesListProvider({ query, ...props }) {
// Fetch payment receives list according to the given custom view id.
const {
data: { paymentReceives, pagination },
isFetching: isPaymentReceivesLoading,
data: { paymentReceives, pagination, filterMeta },
isLoading: isPaymentReceivesLoading,
isFetching: isPaymentReceivesFetching,
} = usePaymentReceives(query);
// Detarmines the datatable empty status.
const isEmptyStatus =
isTableEmptyStatus({
data: paymentReceives,
pagination,
filterMeta,
}) && !isPaymentReceivesLoading;
// Provider payload.
const provider = {
const state = {
paymentReceives,
pagination,
paymentReceivesFields,
paymentReceivesViews,
isPaymentReceivesLoading,
isPaymentReceivesFetching,
isFieldsLoading,
isViewsLoading,
isEmptyStatus,
};
return (
@@ -42,11 +59,12 @@ function PaymentReceivesListProvider({ query, ...props }) {
loading={isViewsLoading || isFieldsLoading}
name={'payment-receives'}
>
<PaymentReceivesListContext.Provider value={provider} {...props} />
<PaymentReceivesListContext.Provider value={state} {...props} />
</DashboardInsider>
);
}
const usePaymentReceivesListContext = () => React.useContext(PaymentReceivesListContext);
const usePaymentReceivesListContext = () =>
useContext(PaymentReceivesListContext);
export { PaymentReceivesListProvider, usePaymentReceivesListContext };

View File

@@ -1,10 +1,8 @@
import React, { useCallback, useState, useMemo } from 'react';
import React from 'react';
import Icon from 'components/Icon';
import {
Button,
Classes,
Menu,
MenuItem,
Popover,
NavbarDivider,
NavbarGroup,
@@ -14,62 +12,41 @@ import {
} from '@blueprintjs/core';
import classNames from 'classnames';
import { useRouteMatch, useHistory } from 'react-router-dom';
import { FormattedMessage as T, useIntl } from 'react-intl';
import { useHistory } from 'react-router-dom';
import { FormattedMessage as T } from 'react-intl';
import { connect } from 'react-redux';
import FilterDropdown from 'components/FilterDropdown';
import DashboardActionsBar from 'components/Dashboard/DashboardActionsBar';
import { If, DashboardActionViewsList } from 'components';
import withResourceDetail from 'containers/Resources/withResourceDetails';
import withPaymentReceivesActions from './withPaymentReceivesActions';
import withPaymentReceives from './withPaymentReceives';
import { compose } from 'utils';
import { usePaymentReceivesListContext } from './PaymentReceiptsListProvider';
/**
* Payment receives actions bar.
*/
function PaymentReceiveActionsBar({
// #withResourceDetail
resourceFields,
//#withPaymentReceives
paymentReceivesViews,
//#withPaymentReceivesActions
addPaymentReceivesTableQueries,
// #own Porps
onFilterChanged,
selectedRows = [],
// #withPaymentReceivesActions
setPaymentReceivesTableState,
}) {
// History context.
const history = useHistory();
const { path } = useRouteMatch();
const [filterCount, setFilterCount] = useState(0);
const { formatMessage } = useIntl();
const handleClickNewPaymentReceive = useCallback(() => {
// Payment receives list context.
const { paymentReceivesViews } = usePaymentReceivesListContext();
// Handle new payment button click.
const handleClickNewPaymentReceive = () => {
history.push('/payment-receives/new');
}, [history]);
};
// const filterDropdown = FilterDropdown({
// initialCondition: {
// fieldKey: '',
// compatator: 'contains',
// value: '',
// },
// fields: resourceFields,
// onFilterChange: (filterConditions) => {
// addPaymentReceivesTableQueries({
// filter_roles: filterConditions || '',
// });
// onFilterChanged && onFilterChanged(filterConditions);
// },
// });
const hasSelectedRows = useMemo(() => selectedRows.length > 0, [
selectedRows,
]);
// Handle tab changing.
const handleTabChange = (viewId) => {
setPaymentReceivesTableState({ customViewId: viewId.id || null });
};
return (
<DashboardActionsBar>
@@ -77,6 +54,7 @@ function PaymentReceiveActionsBar({
<DashboardActionViewsList
resourceName={'payment_receives'}
views={paymentReceivesViews}
onChange={handleTabChange}
/>
<NavbarDivider />
<Button
@@ -93,17 +71,11 @@ function PaymentReceiveActionsBar({
>
<Button
className={classNames(Classes.MINIMAL)}
text={
filterCount <= 0 ? (
<T id={'filter'} />
) : (
`${filterCount} ${formatMessage({ id: 'filters_applied' })}`
)
}
text={<T id={'filter'} />}
icon={<Icon icon={'filter-16'} iconSize={16} />}
/>
</Popover>
<If condition={hasSelectedRows}>
<If condition={false}>
<Button
className={Classes.MINIMAL}
icon={<Icon icon={'trash-16'} iconSize={16} />}
@@ -132,15 +104,9 @@ function PaymentReceiveActionsBar({
);
}
const mapStateToProps = (state, props) => ({
resourceName: 'payment_receives',
});
const withPaymentReceiveActionsBar = connect(mapStateToProps);
export default compose(
withPaymentReceiveActionsBar,
withResourceDetail(({ resourceFields }) => ({
resourceFields,
withPaymentReceivesActions,
withPaymentReceives(({ paymentReceivesTableState }) => ({
paymentReceivesTableState,
})),
withPaymentReceives(({ paymentReceivesViews }) => ({ paymentReceivesViews })),
)(PaymentReceiveActionsBar);

View File

@@ -1,12 +1,12 @@
import React from 'react';
import { useHistory } from 'react-router';
import { Alignment, Navbar, NavbarGroup } from '@blueprintjs/core';
import { useParams } from 'react-router-dom';
import { FormattedMessage as T } from 'react-intl';
import { pick } from 'lodash';
import { DashboardViewsTabs } from 'components';
import withPaymentReceives from './withPaymentReceives';
import withPaymentReceivesActions from './withPaymentReceivesActions';
import { usePaymentReceivesListContext } from './PaymentReceiptsListProvider';
@@ -16,36 +16,38 @@ import { compose } from 'utils';
* Payment receive view tabs.
*/
function PaymentReceiveViewTabs({
//#withPaymentReceivesActions
// #withPaymentReceivesActions
addPaymentReceivesTableQueries,
// #withPaymentReceives
paymentReceivesTableState,
}) {
const history = useHistory();
const { paymentReceivesViews } = usePaymentReceivesListContext();
const { paymentReceivesViews, ...res } = usePaymentReceivesListContext();
const { custom_view_id: customViewId = null } = useParams();
const tabs = paymentReceivesViews.map((view) => ({
...pick(view, ['name', 'id']),
}));
// Handle click a new view tab.
// Handles click a new view tab.
const handleClickNewView = () => {
history.push('/custom_views/payment-receives/new');
};
const handleTabsChange = (viewId) => {
// Handles the active tab chaing.
const handleTabsChange = (customView) => {
addPaymentReceivesTableQueries({
custom_view_id: viewId || null,
customViewId: customView || null,
});
}
};
return (
<Navbar className={'navbar--dashboard-views'}>
<NavbarGroup align={Alignment.LEFT}>
<DashboardViewsTabs
initialViewId={customViewId}
customViewId={paymentReceivesTableState.customViewId}
tabs={tabs}
defaultTabText={<T id={'all_payments'}/>}
defaultTabText={<T id={'all_payments'} />}
onNewViewTabClick={handleClickNewView}
onChange={handleTabsChange}
/>
@@ -56,4 +58,7 @@ function PaymentReceiveViewTabs({
export default compose(
withPaymentReceivesActions,
withPaymentReceives(({ paymentReceivesTableState }) => ({
paymentReceivesTableState,
})),
)(PaymentReceiveViewTabs);

View File

@@ -3,14 +3,15 @@ import { useIntl } from 'react-intl';
import DashboardPageContent from 'components/Dashboard/DashboardPageContent';
import PaymentReceiveActionsBar from './PaymentReceiveActionsBar';
import PaymentReceiveAlerts from './PaymentReceiveAlerts';
import PaymentReceiveAlerts from '../PaymentReceiveAlerts';
import { PaymentReceivesListProvider } from './PaymentReceiptsListProvider';
import PaymentReceivesViewPage from './PaymentReceivesViewPage';
import PaymentReceiveViewTabs from './PaymentReceiveViewTabs';
import PaymentReceivesTable from './PaymentReceivesTable';
import withDashboardActions from 'containers/Dashboard/withDashboardActions';
import withPaymentReceives from './withPaymentReceives';
import { compose } from 'utils';
import { compose, transformTableStateToQuery } from 'utils';
/**
* Payment receives list.
@@ -20,29 +21,34 @@ function PaymentReceiveList({
changePageTitle,
// #withPaymentReceives
paymentReceivesTableQuery,
paymentReceivesTableState,
}) {
const { formatMessage } = useIntl();
// Changes the dashboard page title once the page mount.
useEffect(() => {
changePageTitle(formatMessage({ id: 'payment_Receives_list' }));
}, [changePageTitle, formatMessage]);
return (
<PaymentReceivesListProvider query={paymentReceivesTableQuery}>
<PaymentReceivesListProvider
query={transformTableStateToQuery(paymentReceivesTableState)}
>
<PaymentReceiveActionsBar />
<DashboardPageContent>
<PaymentReceivesViewPage />
<PaymentReceiveAlerts />
<PaymentReceiveViewTabs />
<PaymentReceivesTable />
</DashboardPageContent>
<PaymentReceiveAlerts />
</PaymentReceivesListProvider>
);
}
export default compose(
withDashboardActions,
withPaymentReceives(({ paymentReceivesTableQuery }) => ({
paymentReceivesTableQuery,
withPaymentReceives(({ paymentReceivesTableState }) => ({
paymentReceivesTableState,
})),
)(PaymentReceiveList);

View File

@@ -26,8 +26,9 @@ function PaymentReceivesListProvider({ query, ...props }) {
// Fetch accounts list according to the given custom view id.
const {
data: { paymentReceives, pagination },
isFetching: isPaymentReceivesLoading,
data: { paymentReceives, pagination, filterMeta },
isLoading: isPaymentReceivesLoading,
isFetching: isPaymentReceivesFetching,
} = usePaymentReceives(query);
// Provider payload.
@@ -40,6 +41,7 @@ function PaymentReceivesListProvider({ query, ...props }) {
isViewsLoading,
isFieldsLoading,
isPaymentReceivesLoading,
isPaymentReceivesFetching
};
return (

View File

@@ -0,0 +1,105 @@
import React, { useCallback } from 'react';
import classNames from 'classnames';
import { compose } from 'utils';
import { useHistory } from 'react-router-dom';
import { CLASSES } from 'common/classes';
import PaymentReceivesEmptyStatus from './PaymentReceivesEmptyStatus';
import { DataTable } from 'components';
import TableSkeletonRows from 'components/Datatable/TableSkeletonRows';
import TableSkeletonHeader from 'components/Datatable/TableHeaderSkeleton';
import withAlertsActions from 'containers/Alert/withAlertActions';
import withPaymentReceivesActions from './withPaymentReceivesActions';
import withSettings from 'containers/Settings/withSettings';
import { usePaymentReceivesColumns, ActionsMenu } from './components';
import { usePaymentReceivesListContext } from './PaymentReceiptsListProvider';
/**
* Payment receives datatable.
*/
function PaymentReceivesDataTable({
// #withPaymentReceivesActions
setPaymentReceivesTableState,
// #withAlertsActions
openAlert,
}) {
const history = useHistory();
// Payment receives list context.
const {
paymentReceives,
pagination,
isPaymentReceivesLoading,
isPaymentReceivesFetching,
isEmptyStatus,
} = usePaymentReceivesListContext();
// Payment receives columns.
const columns = usePaymentReceivesColumns();
// Handles edit payment receive.
const handleEditPaymentReceive = ({ id }) => {
history.push(`/payment-receives/${id}/edit`);
};
// Handles delete payment receive.
const handleDeletePaymentReceive = ({ id }) => {
openAlert('payment-receive-delete', { paymentReceiveId: id });
};
// Handle datatable fetch once the table's state changing.
const handleDataTableFetchData = useCallback(
({ pageIndex, pageSize, sortBy }) => {
setPaymentReceivesTableState({
pageIndex,
pageSize,
sortBy,
});
},
[setPaymentReceivesTableState],
);
if (isEmptyStatus) {
return <PaymentReceivesEmptyStatus />;
}
return (
<div className={classNames(CLASSES.DASHBOARD_DATATABLE)}>
<DataTable
columns={columns}
data={paymentReceives}
loading={isPaymentReceivesLoading}
headerLoading={isPaymentReceivesLoading}
progressBarLoading={isPaymentReceivesFetching}
onFetchData={handleDataTableFetchData}
manualSortBy={true}
selectionColumn={true}
noInitialFetch={true}
sticky={true}
autoResetSortBy={false}
autoResetPage={false}
pagination={true}
pagesCount={pagination.pagesCount}
TableLoadingRenderer={TableSkeletonRows}
TableHeaderSkeletonRenderer={TableSkeletonHeader}
ContextMenu={ActionsMenu}
payload={{
onDelete: handleDeletePaymentReceive,
onEdit: handleEditPaymentReceive,
}}
/>
</div>
);
}
export default compose(
withPaymentReceivesActions,
withAlertsActions,
withSettings(({ organizationSettings }) => ({
baseCurrency: organizationSettings?.baseCurrency,
})),
)(PaymentReceivesDataTable);

View File

@@ -0,0 +1,137 @@
import React from 'react';
import {
Intent,
Button,
Popover,
Menu,
MenuItem,
MenuDivider,
Position,
} from '@blueprintjs/core';
import { FormattedMessage as T, useIntl } from 'react-intl';
import moment from 'moment';
import { Money, Icon } from 'components';
import { safeCallback } from 'utils';
/**
* Table actions menu.
*/
export function ActionsMenu({
row: { original: paymentReceive },
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_receive' })}
onClick={safeCallback(onEdit, paymentReceive)}
/>
<MenuItem
text={formatMessage({ id: 'delete_payment_receive' })}
intent={Intent.DANGER}
onClick={safeCallback(onDelete, paymentReceive)}
icon={<Icon icon="trash-16" iconSize={16} />}
/>
</Menu>
);
}
/**
* Amount accessor.
*/
export function AmountAccessor(row) {
return <Money amount={row.amount} currency={'USD'} />;
}
/**
* Payment date accessor.
*/
export function PaymentDateAccessor(row) {
return moment(row.payment_date).format('YYYY MMM DD');
}
/**
* 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 receives columns.
*/
export function usePaymentReceivesColumns() {
const { formatMessage } = useIntl();
return React.useMemo(
() => [
{
id: 'payment_date',
Header: formatMessage({ id: 'payment_date' }),
accessor: PaymentDateAccessor,
width: 140,
className: 'payment_date',
},
{
id: 'customer_id',
Header: formatMessage({ id: 'customer_name' }),
accessor: 'customer.display_name',
width: 140,
className: 'customer_id',
},
{
id: 'payment_receive_no',
Header: formatMessage({ id: 'payment_receive_no' }),
accessor: (row) =>
row.payment_receive_no ? `#${row.payment_receive_no}` : null,
width: 140,
className: 'payment_receive_no',
},
{
id: 'amount',
Header: formatMessage({ id: 'amount' }),
accessor: AmountAccessor,
width: 140,
className: 'amount',
},
{
id: 'reference_no',
Header: formatMessage({ id: 'reference_no' }),
accessor: 'reference_no',
width: 140,
className: 'reference_no',
},
{
id: 'deposit_account_id',
Header: formatMessage({ id: 'deposit_account' }),
accessor: 'deposit_account.name',
width: 140,
className: 'deposit_account_id',
},
{
id: 'actions',
Header: '',
Cell: ActionsCell,
className: 'actions',
width: 50,
disableResizing: true,
},
],
[formatMessage],
);
}

View File

@@ -0,0 +1,16 @@
import { connect } from 'react-redux';
import {
getPaymentReceiveTableStateFactory
} from 'store/PaymentReceives/paymentReceives.selector';
export default (mapState) => {
const getPaymentReceiveTableState = getPaymentReceiveTableStateFactory();
const mapStateToProps = (state, props) => {
const mapped = {
paymentReceivesTableState: getPaymentReceiveTableState(state, props),
};
return mapState ? mapState(mapped, state, props) : mapped;
};
return connect(mapStateToProps);
};

View File

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

View File

@@ -1,34 +0,0 @@
import { connect } from 'react-redux';
import { getResourceViews } from 'store/customViews/customViews.selectors';
import {
getPaymentReceiveCurrentPageFactory,
getPaymentReceivePaginationMetaFactory,
getPaymentReceiveTableQuery,
getPaymentReceivesCurrentViewIdFactory,
} from 'store/PaymentReceive/paymentReceive.selector';
export default (mapState) => {
const getPyamentReceivesItems = getPaymentReceiveCurrentPageFactory();
const getPyamentReceivesPaginationMeta = getPaymentReceivePaginationMetaFactory();
const getPaymentReceivesCurrentViewId = getPaymentReceivesCurrentViewIdFactory();
const mapStateToProps = (state, props) => {
const query = getPaymentReceiveTableQuery(state, props);
const mapped = {
PaymentReceivesCurrentPage: getPyamentReceivesItems(state, props, query),
paymentReceivesViews: getResourceViews(state, props, 'payment_receives'),
paymentReceivesItems: state.paymentReceives.items,
paymentReceivesTableQuery: query,
paymentReceivesPageination: getPyamentReceivesPaginationMeta(
state,
props,
query,
),
paymentReceivesLoading: state.paymentReceives.loading,
paymentReceiveNumberChanged: state.paymentReceives.journalNumberChanged,
paymentReceivesCurrentViewId: getPaymentReceivesCurrentViewId(state, props),
};
return mapState ? mapState(mapped, state, props) : mapped;
};
return connect(mapStateToProps);
};

View File

@@ -1,39 +0,0 @@
import { connect } from 'react-redux';
import {
submitPaymentReceive,
editPaymentReceive,
deletePaymentReceive,
fetchPaymentReceive,
fetchPaymentReceivesTable,
} from 'store/PaymentReceive/paymentReceive.actions';
import t from 'store/types';
const mapDispatchToProps = (dispatch) => ({
requestSubmitPaymentReceive: (form) =>
dispatch(submitPaymentReceive({ form })),
requestFetchPaymentReceive: (id) => dispatch(fetchPaymentReceive({ id })),
requestEditPaymentReceive: (id, form) =>
dispatch(editPaymentReceive(id, form)),
requestDeletePaymentReceive: (id) => dispatch(deletePaymentReceive({ id })),
requestFetchPaymentReceiveTable: (query = {}) =>
dispatch(fetchPaymentReceivesTable({ query: { ...query } })),
changePaymentReceiveView: (id) =>
dispatch({
type: t.PAYMENT_RECEIVES_SET_CURRENT_VIEW,
currentViewId: parseInt(id, 10),
}),
addPaymentReceivesTableQueries: (queries) =>
dispatch({
type: t.PAYMENT_RECEIVES_TABLE_QUERIES_ADD,
payload: { queries }
}),
setPaymentReceiveNumberChanged: (isChanged) =>
dispatch({
type: t.PAYMENT_RECEIVE_NUMBER_CHANGED,
payload: { isChanged },
}),
});
export default connect(null, mapDispatchToProps);

View File

@@ -1,290 +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 { compose, saveInvoke } from 'utils';
import { useIsValuePassed } from 'hooks';
import { CLASSES } from 'common/classes';
import {
Choose,
LoadingIndicator,
DataTable,
Money,
Icon,
If,
} from 'components';
import ReceiptsEmptyStatus from './ReceiptsEmptyStatus';
import withDialogActions from 'containers/Dialog/withDialogActions';
import withDashboardActions from 'containers/Dashboard/withDashboardActions';
import withReceipts from './withReceipts';
import withReceiptActions from './withReceiptActions';
import withSettings from 'containers/Settings/withSettings';
function ReceiptsDataTable({
// #withReceipts
receiptsCurrentPage,
receiptsLoading,
receiptsPagination,
receiptTableQuery,
receiptsCurrentViewId,
// #withReceiptsActions
addReceiptsTableQueries,
// #withSettings
baseCurrency,
// #ownProps
loading,
onEditReceipt,
onDeleteReceipt,
onCloseReceipt,
onSelectedRowsChange,
}) {
const { formatMessage } = useIntl();
const isLoadedBefore = useIsValuePassed(receiptsLoading, false);
const handleEditReceipt = useCallback(
(receipt) => () => {
saveInvoke(onEditReceipt, receipt);
},
[onEditReceipt],
);
const handleDeleteReceipt = useCallback(
(receipt) => () => {
saveInvoke(onDeleteReceipt, receipt);
},
[onDeleteReceipt],
);
const actionMenuList = useCallback(
(receipt) => (
<Menu>
<MenuItem
icon={<Icon icon="reader-18" />}
text={formatMessage({ id: 'view_details' })}
/>
<MenuDivider />
<MenuItem
icon={<Icon icon="pen-18" />}
text={formatMessage({ id: 'edit_receipt' })}
onClick={handleEditReceipt(receipt)}
/>
<If condition={!receipt.is_closed}>
<MenuItem
text={formatMessage({ id: 'mark_as_closed' })}
onClick={() => onCloseReceipt(receipt)}
/>
</If>
<MenuItem
text={formatMessage({ id: 'delete_receipt' })}
intent={Intent.DANGER}
onClick={handleDeleteReceipt(receipt)}
icon={<Icon icon="trash-16" iconSize={16} />}
/>
</Menu>
),
[handleDeleteReceipt, handleEditReceipt, formatMessage],
);
const onRowContextMenu = useCallback(
(cell) => {
return actionMenuList(cell.row.original);
},
[actionMenuList],
);
const columns = useMemo(
() => [
{
id: 'receipt_date',
Header: formatMessage({ id: 'receipt_date' }),
accessor: (r) => moment(r.receipt_date).format('YYYY MMM DD'),
width: 140,
className: 'receipt_date',
},
{
id: 'customer_id',
Header: formatMessage({ id: 'customer_name' }),
accessor: 'customer.display_name',
width: 140,
className: 'customer_id',
},
{
id: 'receipt_number',
Header: formatMessage({ id: 'receipt_number' }),
accessor: (row) =>
row.receipt_number ? `#${row.receipt_number}` : null,
width: 140,
className: 'receipt_number',
},
{
id: 'deposit_account_id',
Header: formatMessage({ id: 'deposit_account' }),
accessor: 'deposit_account.name',
width: 140,
className: 'deposit_account',
},
{
id: 'amount',
Header: formatMessage({ id: 'amount' }),
accessor: (r) => <Money amount={r.amount} currency={baseCurrency} />,
width: 140,
className: 'amount',
},
{
id: 'status',
Header: formatMessage({ id: 'status' }),
accessor: (row) => (
<Choose>
<Choose.When condition={row.is_closed}>
<Tag minimal={true} intent={Intent.SUCCESS}>
<T id={'closed'} />
</Tag>
</Choose.When>
<Choose.Otherwise>
<Tag minimal={true} intent={Intent.WARNING}>
<T id={'draft'} />
</Tag>
</Choose.Otherwise>
</Choose>
),
width: 140,
className: 'amount',
},
{
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 handleDataTableFetchData = useCallback(
({ sortBy, pageIndex, pageSize }) => {
const page = pageIndex + 1;
addReceiptsTableQueries({
...(sortBy.length > 0
? {
column_sort_by: sortBy[0].id,
sort_order: sortBy[0].desc ? 'desc' : 'asc',
}
: {}),
page_size: pageSize,
page,
});
},
[addReceiptsTableQueries],
);
const handleSelectedRowsChange = useCallback(
(selectedRows) => {
saveInvoke(
onSelectedRowsChange,
selectedRows.map((s) => s.original),
);
},
[onSelectedRowsChange],
);
const showEmptyStatus = [
receiptsCurrentViewId === -1,
receiptsCurrentPage.length === 0,
].every((condition) => condition === true);
return (
<div className={classNames(CLASSES.DASHBOARD_DATATABLE)}>
<LoadingIndicator loading={receiptsLoading && !isLoadedBefore}>
<Choose>
<Choose.When condition={showEmptyStatus}>
<ReceiptsEmptyStatus />
</Choose.When>
<Choose.Otherwise>
<DataTable
columns={columns}
data={receiptsCurrentPage}
onFetchData={handleDataTableFetchData}
manualSortBy={true}
selectionColumn={true}
noInitialFetch={true}
sticky={true}
onSelectedRowsChange={handleSelectedRowsChange}
rowContextMenu={onRowContextMenu}
pagination={true}
pagesCount={receiptsPagination.pagesCount}
autoResetSortBy={false}
autoResetPage={false}
initialPageSize={receiptTableQuery.page_size}
initialPageIndex={receiptTableQuery.page - 1}
/>
</Choose.Otherwise>
</Choose>
</LoadingIndicator>
</div>
);
}
export default compose(
withRouter,
withDialogActions,
withDashboardActions,
withReceiptActions,
withReceipts(
({
receiptsCurrentPage,
receiptsLoading,
receiptsPagination,
receiptTableQuery,
receiptsCurrentViewId,
}) => ({
receiptsCurrentPage,
receiptsLoading,
receiptsPagination,
receiptTableQuery,
receiptsCurrentViewId,
}),
),
withSettings(({ organizationSettings }) => ({
baseCurrency: organizationSettings?.baseCurrency,
})),
)(ReceiptsDataTable);

View File

@@ -1,66 +0,0 @@
import React, { useCallback } from 'react';
import { Switch, Route, useHistory } from 'react-router-dom';
import ReceiptViewTabs from './ReceiptViewTabs';
import withAlertsActions from 'containers/Alert/withAlertActions';
import withDialogActions from 'containers/Dialog/withDialogActions';
import { compose } from 'utils';
/**
* Sale receipts view page.
*/
function ReceiptsViewPage({
// #withAlertActions
openAlert,
// #withDialog.
openDialog,
}) {
const history = useHistory();
// handle delete receipt click
const handleDeleteReceipt = useCallback(
({ id }) => {
openAlert('receipt-delete', { receiptId: id });
},
[openAlert],
);
const handleSelectedRowsChange = useCallback((estimate) => {}, []);
const handleEditReceipt = useCallback(
(receipt) => {
history.push(`/receipts/${receipt.id}/edit`);
},
[history],
);
// Handle cancel/confirm receipt deliver.
const handleCloseReceipt = useCallback(
({ id }) => {
openAlert('receipt-close', { receiptId: id });
},
[openAlert],
);
return (
<Switch>
<Route
exact={true}
path={['/receipts/:custom_view_id/custom_view', '/receipts']}
>
<ReceiptViewTabs />
{/* <ReceiptsDataTable
onDeleteReceipt={handleDeleteReceipt}
onEditReceipt={handleEditReceipt}
onCloseReceipt={handleCloseReceipt}
onSelectedRowsChange={handleSelectedRowsChange}
/> */}
</Route>
</Switch>
);
}
export default compose(withAlertsActions, withDialogActions)(ReceiptsViewPage);

View File

@@ -1,40 +0,0 @@
import { connect } from 'react-redux';
import {
submitReceipt,
deleteReceipt,
fetchReceipt,
fetchReceiptsTable,
editReceipt,
closeReceipt,
} from 'store/receipt/receipt.actions';
import t from 'store/types';
const mapDispatchToProps = (dispatch) => ({
requestSubmitReceipt: (form) => dispatch(submitReceipt({ form })),
requestFetchReceipt: (id) => dispatch(fetchReceipt({ id })),
requestEditReceipt: (id, form) => dispatch(editReceipt(id, form)),
requestDeleteReceipt: (id) => dispatch(deleteReceipt({ id })),
requestFetchReceiptsTable: (query = {}) =>
dispatch(fetchReceiptsTable({ query: { ...query } })),
requestCloseReceipt: (id) => dispatch(closeReceipt({ id })),
// requestDeleteBulkReceipt: (ids) => dispatch(deleteBulkReceipt({ ids })),
changeReceiptView: (id) =>
dispatch({
type: t.RECEIPTS_SET_CURRENT_VIEW,
currentViewId: parseInt(id, 10),
}),
addReceiptsTableQueries: (queries) =>
dispatch({
type: t.RECEIPTS_TABLE_QUERIES_ADD,
payload: { queries },
}),
setReceiptNumberChanged: (isChanged) =>
dispatch({
type: t.RECEIPT_NUMBER_CHANGED,
payload: { isChanged },
}),
});
export default connect(null, mapDispatchToProps);

View File

@@ -1,11 +0,0 @@
import { connect } from 'react-redux';
import { getReceiptByIdFactory } from 'store/receipt/receipt.selector';
export default () => {
const getReceiptById = getReceiptByIdFactory();
const mapStateToProps = (state, props) => ({
receipt: getReceiptById(state, props),
});
return connect(mapStateToProps);
};

View File

@@ -1,35 +0,0 @@
import { connect } from 'react-redux';
import { getResourceViews } from 'store/customViews/customViews.selectors';
import {
getReceiptCurrentPageFactory,
getReceiptsTableQueryFactory,
getReceiptsPaginationMetaFactory,
getReceiptsCurrentViewIdFactory
} from 'store/receipt/receipt.selector';
export default (mapState) => {
const getReceiptsItems = getReceiptCurrentPageFactory();
const getReceiptPaginationMeta = getReceiptsPaginationMetaFactory();
const getReceiptsTableQuery = getReceiptsTableQueryFactory();
const getReceiptsCurrentViewId = getReceiptsCurrentViewIdFactory();
const mapStateToProps = (state, props) => {
const tableQuery = getReceiptsTableQuery(state, props);
const mapped = {
receiptsCurrentPage: getReceiptsItems(state, props, tableQuery),
receiptview: getResourceViews(state, props, 'sale_receipt'),
receiptItems: state.salesReceipts.items,
receiptTableQuery: tableQuery,
receiptsPagination: getReceiptPaginationMeta(state, props, tableQuery),
receiptsLoading: state.salesReceipts.loading,
receiptNumberChanged: state.salesReceipts.journalNumberChanged,
receiptsCurrentViewId: getReceiptsCurrentViewId(state, props),
};
return mapState ? mapState(mapped, state, props) : mapped;
};
return connect(mapStateToProps);
};

View File

@@ -1,4 +1,4 @@
import React, { useState, useMemo } from 'react';
import React, { useState } from 'react';
import Icon from 'components/Icon';
import {
Button,
@@ -16,11 +16,9 @@ import { useHistory } from 'react-router-dom';
import { FormattedMessage as T, useIntl } from 'react-intl';
import { If, DashboardActionViewsList } from 'components';
import FilterDropdown from 'components/FilterDropdown';
import DashboardActionsBar from 'components/Dashboard/DashboardActionsBar';
import withReceiptActions from './withReceiptActions';
import withReceiptsActions from './withReceiptsActions';
import { useReceiptsListContext } from './ReceiptsListProvider';
import { compose } from 'utils';
@@ -29,27 +27,29 @@ import { compose } from 'utils';
* Receipts actions bar.
*/
function ReceiptActionsBar({
//#withReceiptActions
addReceiptsTableQueries,
// #withReceiptsActions
setReceiptsTableState,
}) {
const history = useHistory();
const { formatMessage } = useIntl();
const [filterCount, setFilterCount] = useState(0);
// Sale receipts list context.
const { receiptsViews } = useReceiptsListContext();
// Handle new receipt button click.
const onClickNewReceipt = () => {
history.push('/receipts/new');
};
// Handle the active tab change.
const handleTabChange = (viewId) => {
addReceiptsTableQueries({
custom_view_id: viewId.id || null,
setReceiptsTableState({
csutomViewId: viewId.id || null,
});
};
return (
<DashboardActionsBar>
<NavbarGroup>
@@ -113,6 +113,4 @@ function ReceiptActionsBar({
);
}
export default compose(
withReceiptActions,
)(ReceiptActionsBar);
export default compose(withReceiptsActions)(ReceiptActionsBar);

View File

@@ -1,31 +1,35 @@
import React from 'react';
import { useHistory } from 'react-router';
import { Alignment, Navbar, NavbarGroup } from '@blueprintjs/core';
import { useParams } from 'react-router-dom';
import { pick } from 'lodash';
import { DashboardViewsTabs } from 'components';
import withReceiptActions from './withReceiptActions';
import withReceiptActions from './withReceiptsActions';
import withReceipts from './withReceipts';
import { compose } from 'utils';
import { useReceiptsListContext } from './ReceiptsListProvider';
/**
* Receipt views tabs.
* Receipts views tabs.
*/
function ReceiptViewTabs({ addReceiptsTableQueries }) {
const { custom_view_id: customViewId = null } = useParams();
function ReceiptViewTabs({
// #withReceiptActions
setReceiptsTableState,
// #withReceipts
receiptTableState
}) {
// Receipts list context.
const { receiptsViews } = useReceiptsListContext();
const tabs = receiptsViews.map((view) => ({
...pick(view, ['name', 'id']),
}));
const handleTabsChange = (viewId) => {
addReceiptsTableQueries({
custom_view_id: viewId || null,
// Handles the active tab chaning.
const handleTabsChange = (customView) => {
setReceiptsTableState({
customViewId: customView.id || null,
});
};
@@ -33,7 +37,7 @@ function ReceiptViewTabs({ addReceiptsTableQueries }) {
<Navbar className={'navbar--dashboard-views'}>
<NavbarGroup align={Alignment.LEFT}>
<DashboardViewsTabs
initialViewId={customViewId}
initialViewId={receiptTableState.customViewId}
tabs={tabs}
resourceName={'receipts'}
onChange={handleTabsChange}
@@ -43,4 +47,7 @@ function ReceiptViewTabs({ addReceiptsTableQueries }) {
);
}
export default compose(withReceiptActions)(ReceiptViewTabs);
export default compose(
withReceiptActions,
withReceipts(({ receiptTableState }) => ({ receiptTableState })),
)(ReceiptViewTabs);

View File

@@ -4,15 +4,16 @@ import { useIntl } from 'react-intl';
import DashboardPageContent from 'components/Dashboard/DashboardPageContent';
import ReceiptActionsBar from './ReceiptActionsBar';
import ReceiptsViewPage from './ReceiptsViewPage';
import ReceiptsAlerts from './ReceiptsAlerts';
import ReceiptViewTabs from './ReceiptViewTabs';
import ReceiptsAlerts from '../ReceiptsAlerts';
import ReceiptsTable from './ReceiptsTable';
import withDashboardActions from 'containers/Dashboard/withDashboardActions';
import withReceipts from './withReceipts';
import { ReceiptsListProvider } from './ReceiptsListProvider';
import { compose } from 'utils';
import { transformTableStateToQuery, compose } from 'utils';
/**
* Receipts list page.
@@ -22,20 +23,25 @@ function ReceiptsList({
changePageTitle,
// #withReceipts
receiptTableQuery,
receiptTableState,
}) {
const { formatMessage } = useIntl();
// Changes the dashboard page title once the page mount.
useEffect(() => {
changePageTitle(formatMessage({ id: 'receipts_list' }));
}, [changePageTitle, formatMessage]);
return (
<ReceiptsListProvider query={receiptTableQuery}>
<ReceiptsListProvider query={transformTableStateToQuery(receiptTableState)}>
<DashboardPageContent>
<ReceiptActionsBar />
<ReceiptsViewPage />
<DashboardPageContent>
<ReceiptViewTabs />
<ReceiptsTable />
</DashboardPageContent>
<ReceiptsAlerts />
</DashboardPageContent>
</ReceiptsListProvider>
@@ -44,7 +50,7 @@ function ReceiptsList({
export default compose(
withDashboardActions,
withReceipts(({ receiptTableQuery }) => ({
receiptTableQuery,
withReceipts(({ receiptTableState }) => ({
receiptTableState,
})),
)(ReceiptsList);

View File

@@ -1,7 +1,7 @@
import React, { createContext } from 'react';
import DashboardInsider from 'components/Dashboard/DashboardInsider';
import { useResourceViews, useResourceFields, useReceipts } from 'hooks/query';
import { isTableEmptyStatus } from 'utils';
const ReceiptsListContext = createContext();
@@ -19,9 +19,18 @@ function ReceiptsListProvider({ query, ...props }) {
// } = useResourceFields('sale_receipt');
const {
data: { receipts, pagination },
isFetching: isReceiptsLoading,
} = useReceipts(query);
data: { receipts, pagination, filterMeta },
isLoading: isReceiptsLoading,
isFetching: isReceiptsFetching,
} = useReceipts(query, { keepPreviousData: true });
// Detarmines the datatable empty status.
const isEmptyStatus =
isTableEmptyStatus({
data: receipts,
pagination,
filterMeta,
}) && !isReceiptsLoading;
const provider = {
receipts,
@@ -31,6 +40,8 @@ function ReceiptsListProvider({ query, ...props }) {
isViewsLoading,
// isFieldsLoading,
isReceiptsLoading,
isReceiptsFetching,
isEmptyStatus
};
return (

View File

@@ -0,0 +1,126 @@
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';
import ReceiptsEmptyStatus from './ReceiptsEmptyStatus';
import TableSkeletonRows from 'components/Datatable/TableSkeletonRows';
import TableSkeletonHeader from 'components/Datatable/TableHeaderSkeleton';
import withAlertsActions from 'containers/Alert/withAlertActions';
import withReceipts from './withReceipts';
import withReceiptsActions from './withReceiptsActions';
import withSettings from 'containers/Settings/withSettings';
import { useReceiptsListContext } from './ReceiptsListProvider';
import { useReceiptsTableColumns, ActionsMenu } from './components';
/**
* Sale receipts datatable.
*/
function ReceiptsDataTable({
// #withReceiptsActions
setReceiptsTableState,
// #withSettings
baseCurrency,
// #withAlertsActions
openAlert
}) {
const history = useHistory();
// Receipts list context.
const {
receipts,
pagination,
isReceiptsFetching,
isReceiptsLoading,
isEmptyStatus
} = useReceiptsListContext();
// Receipts table columns.
const columns = useReceiptsTableColumns();
// Handle receipt edit action.
const handleEditReceipt = ({ id }) => {
history.push(`/receipts/${id}/edit`);
};
// Handles receipt delete action.
const handleDeleteReceipt = (receipt) => {
openAlert('receipt-delete', { receiptId: receipt.id });
};
// Handles receipt close action.
const handleCloseReceipt = (receipt) => {
openAlert('receipt-close', { receiptId: receipt.id });
}
// Handles the datable fetch data once the state changing.
const handleDataTableFetchData = useCallback(
({ sortBy, pageIndex, pageSize }) => {
setReceiptsTableState({
pageIndex,
pageSize,
sortBy,
});
},
[setReceiptsTableState],
);
if (isEmptyStatus) {
return <ReceiptsEmptyStatus />
}
return (
<div className={classNames(CLASSES.DASHBOARD_DATATABLE)}>
<DataTable
columns={columns}
data={receipts}
loading={isReceiptsLoading}
headerLoading={isReceiptsLoading}
progressBarLoading={isReceiptsFetching}
onFetchData={handleDataTableFetchData}
manualSortBy={true}
selectionColumn={true}
noInitialFetch={true}
sticky={true}
pagination={true}
pagesCount={pagination.pagesCount}
manualPagination={true}
autoResetSortBy={false}
autoResetPage={false}
TableLoadingRenderer={TableSkeletonRows}
TableHeaderSkeletonRenderer={TableSkeletonHeader}
ContextMenu={ActionsMenu}
payload={{
onEdit: handleEditReceipt,
onDelete: handleDeleteReceipt,
onClose: handleCloseReceipt,
baseCurrency
}}
/>
</div>
);
}
export default compose(
withAlertsActions,
withReceiptsActions,
withReceipts(({ receiptTableState }) => ({
receiptTableState,
})),
withSettings(({ organizationSettings }) => ({
baseCurrency: organizationSettings?.baseCurrency,
})),
)(ReceiptsDataTable);

View File

@@ -0,0 +1,155 @@
import React from 'react';
import { FormattedMessage as T, useIntl } from 'react-intl';
import {
Position,
Menu,
MenuItem,
MenuDivider,
Intent,
Popover,
Tag,
Button,
} from '@blueprintjs/core';
import { safeCallback } from 'utils';
import { Choose, Money, Icon, If } from 'components';
import moment from 'moment';
export function ActionsMenu({
payload: { onEdit, onDelete, onClose },
row: { original: receipt },
}) {
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_receipt' })}
onClick={safeCallback(onEdit, receipt)}
/>
<If condition={!receipt.is_closed}>
<MenuItem
text={formatMessage({ id: 'mark_as_closed' })}
onClick={safeCallback(onClose, receipt)}
/>
</If>
<MenuItem
text={formatMessage({ id: 'delete_receipt' })}
intent={Intent.DANGER}
onClick={safeCallback(onDelete, receipt)}
icon={<Icon icon="trash-16" iconSize={16} />}
/>
</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>
);
}
/**
* Status accessor.
*/
export function StatusAccessor(receipt) {
return (
<Choose>
<Choose.When condition={receipt.is_closed}>
<Tag minimal={true} intent={Intent.SUCCESS}>
<T id={'closed'} />
</Tag>
</Choose.When>
<Choose.Otherwise>
<Tag minimal={true} intent={Intent.WARNING}>
<T id={'draft'} />
</Tag>
</Choose.Otherwise>
</Choose>
);
}
/**
* Retrieve receipts table columns.
*/
export function useReceiptsTableColumns() {
const { formatMessage } = useIntl();
return React.useMemo(
() => [
{
id: 'receipt_date',
Header: formatMessage({ id: 'receipt_date' }),
accessor: (r) => moment(r.receipt_date).format('YYYY MMM DD'),
width: 140,
className: 'receipt_date',
},
{
id: 'customer_id',
Header: formatMessage({ id: 'customer_name' }),
accessor: 'customer.display_name',
width: 140,
className: 'customer_id',
},
{
id: 'receipt_number',
Header: formatMessage({ id: 'receipt_number' }),
accessor: (row) =>
row.receipt_number ? `#${row.receipt_number}` : null,
width: 140,
className: 'receipt_number',
},
{
id: 'deposit_account_id',
Header: formatMessage({ id: 'deposit_account' }),
accessor: 'deposit_account.name',
width: 140,
className: 'deposit_account',
},
{
id: 'amount',
Header: formatMessage({ id: 'amount' }),
accessor: (r) => <Money amount={r.amount} currency={'USD'} />,
width: 140,
className: 'amount',
},
{
id: 'status',
Header: formatMessage({ id: 'status' }),
accessor: StatusAccessor,
width: 140,
className: 'amount',
},
{
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,16 @@
import { connect } from 'react-redux';
import {
getReceiptsTableStateFactory,
} from 'store/receipts/receipts.selector';
export default (mapState) => {
const getReceiptsTableState = getReceiptsTableStateFactory();
const mapStateToProps = (state, props) => {
const mapped = {
receiptTableState: getReceiptsTableState(state, props),
};
return mapState ? mapState(mapped, state, props) : mapped;
};
return connect(mapStateToProps);
};

View File

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