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

@@ -0,0 +1,115 @@
import React, { useState } from 'react';
import Icon from 'components/Icon';
import {
Button,
Classes,
Popover,
NavbarDivider,
NavbarGroup,
PopoverInteractionKind,
Position,
Intent,
} from '@blueprintjs/core';
import classNames from 'classnames';
import { useHistory } from 'react-router-dom';
import { FormattedMessage as T, useIntl } from 'react-intl';
import { If, DashboardActionViewsList } from 'components';
import DashboardActionsBar from 'components/Dashboard/DashboardActionsBar';
import withEstimatesActions from './withEstimatesActions';
import { useEstimatesListContext } from './EstimatesListProvider';
import { compose } from 'utils';
/**
* Estimates list actions bar.
*/
function EstimateActionsBar({
// #withEstimateActions
setEstimatesTableState,
}) {
const history = useHistory();
const { formatMessage } = useIntl();
const [filterCount, setFilterCount] = useState(0);
// Estimates list context.
const { estimatesViews } = useEstimatesListContext();
// Handle click a new sale estimate.
const onClickNewEstimate = () => {
history.push('/estimates/new');
};
const handleTabChange = (customView) => {
setEstimatesTableState({
customViewId: customView.id || null,
});
};
return (
<DashboardActionsBar>
<NavbarGroup>
<DashboardActionViewsList
resourceName={'estimates'}
views={estimatesViews}
onChange={handleTabChange}
/>
<NavbarDivider />
<Button
className={Classes.MINIMAL}
icon={<Icon icon={'plus'} />}
text={<T id={'new_estimate'} />}
onClick={onClickNewEstimate}
/>
<Popover
minimal={true}
interactionKind={PopoverInteractionKind.CLICK}
position={Position.BOTTOM_LEFT}
>
<Button
className={classNames(Classes.MINIMAL, 'button--filter')}
text={
filterCount <= 0 ? (
<T id={'filter'} />
) : (
`${filterCount} ${formatMessage({ id: 'filters_applied' })}`
)
}
icon={<Icon icon={'filter-16'} iconSize={16} />}
/>
</Popover>
<If condition={false}>
<Button
className={Classes.MINIMAL}
icon={<Icon icon={'trash-16'} iconSize={16} />}
text={<T id={'delete'} />}
intent={Intent.DANGER}
// onClick={handleBulkDelete}
/>
</If>
<Button
className={Classes.MINIMAL}
icon={<Icon icon={'print-16'} iconSize={'16'} />}
text={<T id={'print'} />}
/>
<Button
className={Classes.MINIMAL}
icon={<Icon icon={'file-import-16'} />}
text={<T id={'import'} />}
/>
<Button
className={Classes.MINIMAL}
icon={<Icon icon={'file-export-16'} iconSize={'16'} />}
text={<T id={'export'} />}
/>
</NavbarGroup>
</DashboardActionsBar>
);
}
export default compose(
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

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

View File

@@ -0,0 +1,51 @@
import React, { useEffect } from 'react';
import { FormattedMessage as T, useIntl } from 'react-intl';
import DashboardPageContent from 'components/Dashboard/DashboardPageContent';
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, transformTableStateToQuery } from 'utils';
/**
* Sale estimates list page.
*/
function EstimatesList({
// #withDashboardActions
changePageTitle,
// #withEstimate
estimatesTableState,
}) {
const { formatMessage } = useIntl();
useEffect(() => {
changePageTitle(formatMessage({ id: 'estimates_list' }));
}, [changePageTitle, formatMessage]);
return (
<EstimatesListProvider
query={transformTableStateToQuery(estimatesTableState)}
>
<EstimatesActionsBar />
<DashboardPageContent>
<EstimatesViewTabs />
<EstimatesDataTable />
</DashboardPageContent>
<EstimatesAlerts />
</EstimatesListProvider>
);
}
export default compose(
withDashboardActions,
withEstimates(({ estimatesTableState }) => ({ estimatesTableState })),
)(EstimatesList);

View File

@@ -0,0 +1,65 @@
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();
/**
* Sale estimates data provider.
*/
function EstimatesListProvider({ query, ...props }) {
// Fetches estimates resource views and fields.
const { data: estimatesViews, isFetching: isViewsLoading } = useResourceViews(
'sale_estimates',
);
// Fetches the estimates resource fields.
const {
data: estimatesFields,
isFetching: isFieldsLoading,
} = useResourceFields('sale_estimates');
// Fetch estimates list according to the given custom view id.
const {
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 = {
estimates,
pagination,
estimatesFields,
estimatesViews,
isEstimatesLoading,
isEstimatesFetching,
isFieldsLoading,
isViewsLoading,
isEmptyStatus,
};
return (
<DashboardInsider
loading={isViewsLoading || isFieldsLoading}
name={'sale_estimate'}
>
<EstimatesListContext.Provider value={provider} {...props} />
</DashboardInsider>
);
}
const useEstimatesListContext = () => React.useContext(EstimatesListContext);
export { EstimatesListProvider, useEstimatesListContext };

View File

@@ -0,0 +1,53 @@
import React from 'react';
import { Alignment, Navbar, NavbarGroup } from '@blueprintjs/core';
import { pick } from 'lodash';
import { DashboardViewsTabs } from 'components';
import withEstimatesActions from './withEstimatesActions';
import withEstimates from './withEstimates';
import { useEstimatesListContext } from './EstimatesListProvider';
import { compose } from 'utils';
/**
* Estimates views tabs.
*/
function EstimateViewTabs({
// #withEstimatesActions
setEstimatesTableState,
// #withEstimates
estimatesTableState
}) {
// Estimates list context.
const { estimatesViews } = useEstimatesListContext();
const tabs = estimatesViews.map((view) => ({
...pick(view, ['name', 'id']),
}));
const handleTabsChange = (viewId) => {
setEstimatesTableState({
customViewId: viewId || null,
});
};
return (
<Navbar className={'navbar--dashboard-views'}>
<NavbarGroup align={Alignment.LEFT}>
<DashboardViewsTabs
customViewId={estimatesTableState.customViewId}
resourceName={'estimates'}
tabs={tabs}
onChange={handleTabsChange}
/>
</NavbarGroup>
</Navbar>
);
}
export default compose(
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);