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,116 @@
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 DashboardActionsBar from 'components/Dashboard/DashboardActionsBar';
import { If, DashboardActionViewsList } from 'components';
import { useInvoicesListContext } from './InvoicesListProvider';
import withInvoiceActions from './withInvoiceActions';
import { compose } from 'utils';
/**
* Invoices table actions bar.
*/
function InvoiceActionsBar({
// #withInvoiceActions
setInvoicesTableState,
}) {
const history = useHistory();
const { formatMessage } = useIntl();
const [filterCount, setFilterCount] = useState(0);
// Sale invoices list context.
const { invoicesViews } = useInvoicesListContext();
// Handle new invoice button click.
const handleClickNewInvoice = () => {
history.push('/invoices/new');
};
// Handle views tab change.
const handleTabChange = (customView) => {
setInvoicesTableState({
customViewId: customView.id || null,
});
};
return (
<DashboardActionsBar>
<NavbarGroup>
<DashboardActionViewsList
resourceName={'invoices'}
views={invoicesViews}
onChange={handleTabChange}
/>
<NavbarDivider />
<Button
className={Classes.MINIMAL}
icon={<Icon icon={'plus'} />}
text={<T id={'new_invoice'} />}
onClick={handleClickNewInvoice}
/>
<Popover
minimal={true}
// content={filterDropdown}
interactionKind={PopoverInteractionKind.CLICK}
position={Position.BOTTOM_LEFT}
>
<Button
className={classNames(Classes.MINIMAL)}
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(withInvoiceActions)(InvoiceActionsBar);

View File

@@ -0,0 +1,63 @@
import React from 'react';
import { useHistory } from 'react-router';
import { Alignment, Navbar, NavbarGroup } from '@blueprintjs/core';
import { pick } from 'lodash';
import { DashboardViewsTabs } from 'components';
import withInvoices from './withInvoices';
import withInvoiceActions from './withInvoiceActions';
import { compose } from 'utils';
import { useInvoicesListContext } from './InvoicesListProvider';
/**
* Invoices views tabs.
*/
function InvoiceViewTabs({
// #withInvoiceActions
setInvoicesTableState,
// #withInvoices
invoicesTableState
}) {
const history = useHistory();
// Invoices list context.
const { invoicesViews } = useInvoicesListContext();
const tabs = invoicesViews.map((view) => ({
...pick(view, ['name', 'id']),
}));
// Handle tab change.
const handleTabsChange = (customView) => {
setInvoicesTableState({
customViewId: customView.id || null,
});
};
// Handle click a new view tab.
const handleClickNewView = () => {
history.push('/custom_views/invoices/new');
};
return (
<Navbar className={'navbar--dashboard-views'}>
<NavbarGroup align={Alignment.LEFT}>
<DashboardViewsTabs
customViewId={invoicesTableState.customViewId}
resourceName={'invoices'}
tabs={tabs}
onNewViewTabClick={handleClickNewView}
onChange={handleTabsChange}
/>
</NavbarGroup>
</Navbar>
);
}
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

@@ -0,0 +1,37 @@
import React from 'react';
import { Button, Intent } from '@blueprintjs/core';
import { useHistory } from 'react-router-dom';
import { EmptyStatus } from 'components';
export default function EstimatesEmptyStatus() {
const history = useHistory();
return (
<EmptyStatus
title={'The organization does not have invoices, yet!'}
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('/invoices/new');
}}
>
New sale invoice
</Button>
<Button intent={Intent.NONE} large={true}>
Learn more
</Button>
</>
}
/>
);
}

View File

@@ -0,0 +1,56 @@
import React, { useEffect } from 'react';
import { useIntl } from 'react-intl';
import 'style/pages/SaleInvoice/List.scss';
import DashboardPageContent from 'components/Dashboard/DashboardPageContent';
import InvoiceActionsBar from './InvoiceActionsBar';
import { InvoicesListProvider } from './InvoicesListProvider';
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 { transformTableStateToQuery, compose } from 'utils';
/**
* Sale invoices list.
*/
function InvoicesList({
// #withDashboardActions
changePageTitle,
// #withInvoice
invoicesTableState,
}) {
const { formatMessage } = useIntl();
useEffect(() => {
changePageTitle(formatMessage({ id: 'invoices_list' }));
}, [changePageTitle, formatMessage]);
return (
<InvoicesListProvider
query={transformTableStateToQuery(invoicesTableState)}
>
<InvoiceActionsBar />
<DashboardPageContent>
<InvoiceViewTabs />
<InvoicesDataTable />
</DashboardPageContent>
<InvoicesAlerts />
</InvoicesListProvider>
);
}
export default compose(
withDashboardActions,
withInvoices(({ invoicesTableState }) => ({ invoicesTableState })),
withAlertsActions,
)(InvoicesList);

View File

@@ -0,0 +1,65 @@
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();
/**
* Accounts chart data provider.
*/
function InvoicesListProvider({ accountsTableQuery, ...props }) {
// Fetch accounts resource views and fields.
const { data: invoicesViews, isFetching: isViewsLoading } = useResourceViews(
'sale_invoices',
);
// Fetch the accounts resource fields.
const {
data: invoicesFields,
isFetching: isFieldsLoading,
} = useResourceFields('sale_invoices');
// Fetch accounts list according to the given custom view id.
const {
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 = {
invoices,
pagination,
invoicesFields,
invoicesViews,
isInvoicesLoading,
isInvoicesFetching,
isFieldsLoading,
isViewsLoading,
isEmptyStatus
};
return (
<DashboardInsider
loading={isViewsLoading || isFieldsLoading}
name={'sales-invoices-list'}
>
<InvoicesListContext.Provider value={provider} {...props} />
</DashboardInsider>
);
}
const useInvoicesListContext = () => React.useContext(InvoicesListContext);
export { InvoicesListProvider, useInvoicesListContext };

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