refactoring: account form.

refactoring: expense form.
refactoring: manual journal form.
refactoring: invoice form.
This commit is contained in:
a.bouhuolia
2021-02-15 12:03:47 +02:00
parent 692f3b333a
commit 760c38b54b
124 changed files with 2694 additions and 2967 deletions

View File

@@ -85,7 +85,7 @@
"react-scrollbars-custom": "^4.0.21",
"react-sortablejs": "^2.0.11",
"react-split-pane": "^0.1.91",
"react-table": "^7.0.0",
"react-table": "^7.6.3",
"react-table-sticky": "^1.1.2",
"react-transition-group": "^4.4.1",
"react-use": "^13.26.1",

View File

@@ -1,6 +1,7 @@
import { Classes } from '@blueprintjs/core';
const CLASSES = {
DASHBOARD_PAGE: 'dashboard__page',
DASHBOARD_DATATABLE: 'dashboard__datatable',
DASHBOARD_CARD: 'dashboard__card',
DASHBOARD_CARD_PAGE: 'dashboard__card--page',

View File

@@ -1,20 +1,31 @@
import React from 'react';
import { Route, Switch } from 'react-router-dom';
import routes from 'routes/dashboard'
import React, { useEffect, Suspense } from 'react';
import { Route, Switch } from 'react-router-dom';
import routes from 'routes/dashboard';
import DashboardPage from './DashboardPage';
/**
* Dashboard content route.
*/
export default function DashboardContentRoute() {
return (
<Route pathname="/">
<Switch>
{ routes.map((route, index) => (
{routes.map((route, index) => (
<Route
exact={route.exact}
key={index}
path={`${route.path}`}
component={route.component} />
>
<DashboardPage
Component={route.component}
pageTitle={route.pageTitle}
backLink={route.backLink}
sidebarShrink={route.sidebarShrink}
/>
</Route>
))}
</Switch>
</Route>
);
}
}

View File

@@ -0,0 +1,10 @@
import React from 'react';
import classNames from 'classnames';
import { CLASSES } from 'common/classes';
/**
* Dashboard content table.
*/
export default function DashboardContentTable({ children }) {
return (<div className={classNames(CLASSES.DASHBOARD_DATATABLE)}>{ children }</div>)
}

View File

@@ -0,0 +1,56 @@
import React, { useEffect, Suspense } from 'react';
import { CLASSES } from 'common/classes';
import withDashboardActions from 'containers/Dashboard/withDashboardActions';
import { compose } from 'utils';
/**
* Dashboard pages wrapper.
*/
function DashboardPage({
// #ownProps
pageTitle,
backLink,
sidebarShrink,
Component,
// #withDashboardActions
changePageTitle,
setDashboardBackLink,
setSidebarShrink,
resetSidebarPreviousExpand,
}) {
useEffect(() => {
pageTitle && changePageTitle(pageTitle);
return () => {
pageTitle && changePageTitle('');
};
});
useEffect(() => {
backLink && setDashboardBackLink(backLink);
return () => {
backLink && setDashboardBackLink(false);
};
}, [backLink, setDashboardBackLink]);
// Handle sidebar shrink in mount and reset to the pervious state
// once the page unmount.
useEffect(() => {
sidebarShrink && setSidebarShrink();
return () => {
sidebarShrink && resetSidebarPreviousExpand();
};
}, [resetSidebarPreviousExpand, sidebarShrink, setSidebarShrink]);
return (
<div className={CLASSES.DASHBOARD_PAGE}>
<Suspense fallback={''}>
<Component />
</Suspense>
</div>
);
}
export default compose(withDashboardActions)(DashboardPage);

View File

@@ -1,5 +1,8 @@
import React from 'react';
/**
* Dashboard page content.
*/
export default function DashboardPageContent({ children }) {
return (
<div class="dashboard__page-content">

View File

@@ -20,6 +20,7 @@ import TableNoResultsRow from './Datatable/TableNoResultsRow';
import TableLoadingRow from './Datatable/TableLoading';
import TableHeader from './Datatable/TableHeader';
import TablePage from './Datatable/TablePage';
import TableFooter from './Datatable/TableFooter';
import TableRow from './Datatable/TableRow';
import TableRows from './Datatable/TableRows';
import TableCell from './Datatable/TableCell';
@@ -75,6 +76,7 @@ export default function DataTable(props) {
TableWrapperRenderer,
TableTBodyRenderer,
TablePaginationRenderer,
TableFooterRenderer,
...restProps
} = props;
@@ -124,11 +126,11 @@ export default function DataTable(props) {
},
useSortBy,
useExpanded,
useRowSelect,
useResizeColumns,
useFlexLayout,
useSticky,
usePagination,
useRowSelect,
(hooks) => {
hooks.visibleColumns.push((columns) => [
// Let's make a column for selection
@@ -170,6 +172,8 @@ export default function DataTable(props) {
<TableTBodyRenderer>
<TablePageRenderer />
</TableTBodyRenderer>
<TableFooterRenderer />
</TableWrapperRenderer>
<TablePaginationRenderer />
@@ -194,6 +198,7 @@ DataTable.defaultProps = {
autoResetRowState: true,
TableHeaderRenderer: TableHeader,
TableFooterRenderer: TableFooter,
TableLoadingRenderer: TableLoadingRow,
TablePageRenderer: TablePage,
TableRowsRenderer: TableRows,

View File

@@ -1,10 +1,7 @@
import React from 'react';
import classNames from 'classnames';
import { CLASSES } from 'common/classes';
import {
DataTable,
If,
} from 'components';
import { DataTable, If } from 'components';
import 'style/components/DataTable/DataTableEditable.scss';
export default function DatatableEditable({
@@ -14,11 +11,7 @@ export default function DatatableEditable({
...tableProps
}) {
return (
<div
className={classNames(CLASSES.DATATABLE_EDITOR, {
[`${CLASSES.DATATABLE_EDITOR_HAS_TOTAL_ROW}`]: totalRow,
}, className)}
>
<div className={classNames(CLASSES.DATATABLE_EDITOR, className)}>
<DataTable {...tableProps} />
<If condition={actions}>

View File

@@ -0,0 +1,29 @@
import React, { useContext } from 'react';
import TableContext from './TableContext';
/**
* Table footer.
*/
export default function TableFooter() {
const {
table: { footerGroups },
} = useContext(TableContext);
return (
<div class="tfooter">
{footerGroups.map((group) => (
<div {...group.getFooterGroupProps({ className: 'tr' })}>
{group.headers.map((column) => (
<div
{...column.getFooterProps({
className: 'td',
})}
>
{column.render('Footer')}
</div>
))}
</div>
))}
</div>
);
}

View File

@@ -1,6 +1,6 @@
import React from 'react';
import AccountFormDialog from 'containers/Dialogs/AccountFormDialog';
import AccountDialog from 'containers/Dialogs/AccountDialog';
import InviteUserDialog from 'containers/Dialogs/InviteUserDialog';
import ItemCategoryDialog from 'containers/Dialogs/ItemCategoryDialog';
import CurrencyFormDialog from 'containers/Dialogs/CurrencyFormDialog';
@@ -19,7 +19,7 @@ import PaymentViaVoucherDialog from 'containers/Dialogs/PaymentViaVoucherDialog'
export default function DialogsContainer() {
return (
<div>
{/* <AccountFormDialog dialogName={'account-form'} /> */}
<AccountDialog dialogName={'account-form'} />
<JournalNumberDialog dialogName={'journal-number-form'} />
<PaymentReceiveNumberDialog dialogName={'payment-receive-number-form'} />
<EstimateNumberDialog dialogName={'estimate-number-form'} />

View File

@@ -47,6 +47,8 @@ import CustomersMultiSelect from './CustomersMultiSelect';
import Skeleton from './Skeleton'
import ContextMenu from './ContextMenu'
import TableFastCell from './Datatable/TableFastCell';
import DashboardContentTable from './Dashboard/DashboardContentTable';
import DashboardPageContent from './Dashboard/DashboardPageContent';
const Hint = FieldHint;
@@ -99,5 +101,7 @@ export {
CustomersMultiSelect,
TableFastCell,
Skeleton,
ContextMenu
ContextMenu,
DashboardContentTable,
DashboardPageContent
};

View File

@@ -1,7 +1,7 @@
import React from 'react';
import classNames from 'classnames';
import { DataTable, Choose } from 'components';
import { CLASSES } from 'common/classes';
import { useHistory } from 'react-router-dom';
import { DataTable } from 'components';
import ManualJournalsEmptyStatus from './ManualJournalsEmptyStatus';
import TableSkeletonRows from 'components/Datatable/TableSkeletonRows';
@@ -35,20 +35,22 @@ function ManualJournalsDataTable({
pagination,
isManualJournalsLoading,
isManualJournalsFetching,
isEmptyStatus
isEmptyStatus,
} = useManualJournalsContext();
const history = useHistory();
// Manual journals columns.
const columns = useManualJournalsColumns();
// Handles the journal publish action.
const handlePublishJournal = ({ id }) => {
openAlert('journal-publish', { manualJournalId: id })
openAlert('journal-publish', { manualJournalId: id });
};
// Handle the journal edit action.
const handleEditJournal = ({ id }) => {
history.push(`/manual-journals/${id}/edit`);
};
// Handle the journal delete action.
@@ -68,51 +70,41 @@ function ManualJournalsDataTable({
[setManualJournalsTableState],
);
// Display manual journal empty status instead of the table.
if (isEmptyStatus) {
return <ManualJournalsEmptyStatus />;
}
return (
<div className={classNames(CLASSES.DASHBOARD_DATATABLE)}>
<Choose>
<Choose.When condition={isEmptyStatus}>
<ManualJournalsEmptyStatus />
</Choose.When>
<Choose.Otherwise>
<DataTable
noInitialFetch={true}
columns={columns}
data={manualJournals}
manualSortBy={true}
selectionColumn={true}
expandable={true}
sticky={true}
loading={isManualJournalsLoading}
headerLoading={isManualJournalsLoading}
progressBarLoading={isManualJournalsFetching}
pagesCount={pagination.pagesCount}
pagination={true}
autoResetSortBy={false}
autoResetPage={false}
TableLoadingRenderer={TableSkeletonRows}
TableHeaderSkeletonRenderer={TableSkeletonHeader}
ContextMenu={ActionsMenu}
onFetchData={handleFetchData}
payload={{
onDelete: handleDeleteJournal,
onPublish: handlePublishJournal
}}
/>
</Choose.Otherwise>
</Choose>
</div>
<DataTable
noInitialFetch={true}
columns={columns}
data={manualJournals}
manualSortBy={true}
selectionColumn={true}
expandable={true}
sticky={true}
loading={isManualJournalsLoading}
headerLoading={isManualJournalsLoading}
progressBarLoading={isManualJournalsFetching}
pagesCount={pagination.pagesCount}
pagination={true}
autoResetSortBy={false}
autoResetPage={false}
TableLoadingRenderer={TableSkeletonRows}
TableHeaderSkeletonRenderer={TableSkeletonHeader}
ContextMenu={ActionsMenu}
onFetchData={handleFetchData}
payload={{
onDelete: handleDeleteJournal,
onPublish: handlePublishJournal,
onEdit: handleEditJournal,
}}
/>
);
}
export default compose(
withManualJournalsActions,
withAlertsActions
withAlertsActions,
)(ManualJournalsDataTable);

View File

@@ -1,7 +1,7 @@
import React, { useEffect } from 'react';
import { useIntl } from 'react-intl';
import DashboardPageContent from 'components/Dashboard/DashboardPageContent';
import { DashboardContentTable, DashboardPageContent } from 'components';
import { ManualJournalsListProvider } from './ManualJournalsListProvider';
import ManualJournalsAlerts from './ManualJournalsAlerts';
@@ -41,7 +41,11 @@ function ManualJournalsTable({
<DashboardPageContent>
<ManualJournalsViewTabs />
<ManualJournalsDataTable />
<DashboardContentTable>
<ManualJournalsDataTable />
</DashboardContentTable>
<ManualJournalsAlerts />
</DashboardPageContent>
</ManualJournalsListProvider>

View File

@@ -3,12 +3,12 @@ import { FastField } from 'formik';
import classNames from 'classnames';
import { CLASSES } from 'common/classes';
import MakeJournalEntriesTable from './MakeJournalEntriesTable';
import { orderingLinesIndexes, repeatValue } from 'utils';
import { defaultManualJournal, MIN_LINES_NUMBER } from './utils';
export default function MakeJournalEntriesField({
defaultRow,
linesNumber = 4,
}) {
/**
* Make journal entries field.
*/
export default function MakeJournalEntriesField() {
return (
<div className={classNames(CLASSES.PAGE_FORM_BODY)}>
<FastField name={'entries'}>
@@ -18,16 +18,9 @@ export default function MakeJournalEntriesField({
form.setFieldValue('entries', entries);
}}
entries={value}
defaultEntry={defaultManualJournal}
initialLinesNumber={MIN_LINES_NUMBER}
error={error}
onClickAddNewRow={() => {
form.setFieldValue('entries', [...value, defaultRow]);
}}
onClickClearAllLines={() => {
form.setFieldValue(
'entries',
orderingLinesIndexes([...repeatValue(defaultRow, linesNumber)])
);
}}
/>
)}
</FastField>

View File

@@ -10,79 +10,70 @@ import {
MenuItem,
} from '@blueprintjs/core';
import { FormattedMessage as T } from 'react-intl';
import { useFormikContext } from 'formik';
import { CLASSES } from 'common/classes';
import classNames from 'classnames';
import { saveInvoke } from 'utils';
import { If, Icon } from 'components';
import { useMakeJournalFormContext } from './MakeJournalProvider';
import { useHistory } from 'react-router-dom';
/**
* Make Journal floating actions bar.
*/
export default function MakeJournalEntriesFooter({
isSubmitting,
onSubmitClick,
onCancelClick,
manualJournalId,
onSubmitForm,
onResetForm,
manualJournalPublished,
}) {
export default function MakeJournalEntriesFooter() {
const history = useHistory();
// Formik context.
const { isSubmitting, submitForm } = useFormikContext();
// Make journal form context.
const {
manualJournalId,
setSubmitPayload,
manualJournalPublished = false,
} = useMakeJournalFormContext();
// Handle `submit & publish` button click.
const handleSubmitPublishBtnClick = (event) => {
saveInvoke(onSubmitClick, event, {
redirect: true,
publish: true,
});
setSubmitPayload({ redirect: true, publish: true });
submitForm();
};
// Handle `submit, publish & new` button click.
const handleSubmitPublishAndNewBtnClick = (event) => {
onSubmitForm();
saveInvoke(onSubmitClick, event, {
redirect: false,
publish: true,
resetForm: true,
});
setSubmitPayload({ redirect: false, publish: true, resetForm: true });
submitForm();
};
// Handle `submit, publish & continue editing` button click.
const handleSubmitPublishContinueEditingBtnClick = (event) => {
onSubmitForm();
saveInvoke(onSubmitClick, event, {
redirect: false,
publish: true,
});
setSubmitPayload({ redirect: false, publish: true });
submitForm();
};
// Handle `submit as draft` button click.
const handleSubmitDraftBtnClick = (event) => {
saveInvoke(onSubmitClick, event, {
redirect: true,
publish: false,
});
setSubmitPayload({ redirect: true, publish: false });
};
// Handle `submit as draft & new` button click.
const handleSubmitDraftAndNewBtnClick = (event) => {
onSubmitForm();
saveInvoke(onSubmitClick, event, {
redirect: false,
publish: false,
resetForm: true,
});
setSubmitPayload({ redirect: false, publish: false, resetForm: true });
submitForm();
};
// Handles submit as draft & continue editing button click.
const handleSubmitDraftContinueEditingBtnClick = (event) => {
onSubmitForm();
saveInvoke(onSubmitClick, event, {
redirect: false,
publish: false,
});
setSubmitPayload({ redirect: false, publish: false });
submitForm();
};
// Handle cancel button action click.
const handleCancelBtnClick = (event) => {
saveInvoke(onCancelClick, event);
history.goBack();
};
const handleClearBtnClick = (event) => {
// saveInvoke(onClearClick, event);
onResetForm();
};
const handleClearBtnClick = (event) => {};
return (
<div className={classNames(CLASSES.PAGE_FORM_FLOATING_ACTIONS)}>

View File

@@ -1,9 +1,8 @@
import React, { useMemo, useState, useEffect, useCallback } from 'react';
import React, { useMemo } from 'react';
import { Formik, Form } from 'formik';
import moment from 'moment';
import { Intent } from '@blueprintjs/core';
import { useIntl } from 'react-intl';
import { pick, defaultTo } from 'lodash';
import { defaultTo, isEmpty } from 'lodash';
import classNames from 'classnames';
import { useHistory } from 'react-router-dom';
@@ -15,56 +14,30 @@ import {
import MakeJournalEntriesHeader from './MakeJournalEntriesHeader';
import MakeJournalFormFloatingActions from './MakeJournalFormFloatingActions';
import MakeJournalEntriesField from './MakeJournalEntriesField';
import MakeJournalNumberWatcher from './MakeJournalNumberWatcher';
import MakeJournalFormFooter from './MakeJournalFormFooter';
import withDashboardActions from 'containers/Dashboard/withDashboardActions';
import withSettings from 'containers/Settings/withSettings';
import AppToaster from 'components/AppToaster';
import withMediaActions from 'containers/Media/withMediaActions';
import { compose, orderingLinesIndexes, transactionNumber } from 'utils';
import {
compose,
repeatValue,
orderingLinesIndexes,
defaultToTransform,
transactionNumber,
} from 'utils';
import { transformErrors } from './utils';
transformErrors,
transformToEditForm,
defaultManualJournal,
} from './utils';
import { useMakeJournalFormContext } from './MakeJournalProvider';
const defaultEntry = {
index: 0,
account_id: '',
credit: '',
debit: '',
contact_id: '',
note: '',
};
const defaultInitialValues = {
journal_number: '',
journal_type: 'Journal',
date: moment(new Date()).format('YYYY-MM-DD'),
description: '',
reference: '',
currency_code: '',
publish: '',
entries: [...repeatValue(defaultEntry, 4)],
};
/**
* Journal entries form.
*/
function MakeJournalEntriesForm({
// #withDashboard
changePageTitle,
changePageSubtitle,
// #withSettings
journalNextNumber,
journalNumberPrefix,
baseCurrency,
}) {
// Journal form context.
const {
createJournalMutate,
editJournalMutate,
@@ -81,58 +54,23 @@ function MakeJournalEntriesForm({
journalNumberPrefix,
journalNextNumber,
);
// Changes the page title based on the form in new and edit mode.
useEffect(() => {
const transactionNumber = manualJournal
? manualJournal.journal_number
: journalNumber;
if (isNewMode) {
changePageTitle(formatMessage({ id: 'new_journal' }));
} else {
changePageTitle(formatMessage({ id: 'edit_journal' }));
}
changePageSubtitle(
defaultToTransform(transactionNumber, `No. ${transactionNumber}`, ''),
);
}, [
changePageTitle,
changePageSubtitle,
journalNumber,
manualJournal,
formatMessage,
isNewMode,
]);
// Form initial values.
const initialValues = useMemo(
() => ({
...(manualJournal
...(!isEmpty(manualJournal)
? {
...pick(manualJournal, Object.keys(defaultInitialValues)),
entries: manualJournal.entries.map((entry) => ({
...pick(entry, Object.keys(defaultEntry)),
})),
...transformToEditForm(manualJournal),
}
: {
...defaultInitialValues,
...defaultManualJournal,
journal_number: defaultTo(journalNumber, ''),
currency_code: defaultTo(baseCurrency, ''),
entries: orderingLinesIndexes(defaultInitialValues.entries),
entries: orderingLinesIndexes(defaultManualJournal.entries),
}),
}),
[manualJournal, baseCurrency, journalNumber],
);
// Handle journal number field change.
const handleJournalNumberChanged = useCallback(
(journalNumber) => {
changePageSubtitle(
defaultToTransform(journalNumber, `No. ${journalNumber}`, ''),
);
},
[changePageSubtitle],
);
// Handle the form submiting.
const handleSubmit = (values, { setErrors, setSubmitting, resetForm }) => {
setSubmitting(true);
@@ -170,8 +108,12 @@ function MakeJournalEntriesForm({
const form = { ...values, publish: submitPayload.publish, entries };
// Handle the request error.
const handleError = (error) => {
transformErrors(error, { setErrors });
const handleError = ({
response: {
data: { errors },
},
}) => {
transformErrors(errors, { setErrors });
setSubmitting(false);
};
@@ -201,7 +143,7 @@ function MakeJournalEntriesForm({
if (isNewMode) {
createJournalMutate(form).then(handleSuccess).catch(handleError);
} else {
editJournalMutate(manualJournal.id, form)
editJournalMutate([manualJournal.id, form])
.then(handleSuccess)
.catch(handleError);
}
@@ -221,11 +163,8 @@ function MakeJournalEntriesForm({
onSubmit={handleSubmit}
>
<Form>
<MakeJournalEntriesHeader
onJournalNumberChanged={handleJournalNumberChanged}
/>
<MakeJournalNumberWatcher journalNumber={journalNumber} />
<MakeJournalEntriesField defaultRow={defaultEntry} />
<MakeJournalEntriesHeader />
<MakeJournalEntriesField />
<MakeJournalFormFooter />
<MakeJournalFormFloatingActions />
</Form>
@@ -235,7 +174,6 @@ function MakeJournalEntriesForm({
}
export default compose(
withDashboardActions,
withMediaActions,
withSettings(({ manualJournalsSettings, organizationSettings }) => ({
journalNextNumber: parseInt(manualJournalsSettings?.nextNumber, 10),

View File

@@ -1,62 +1,20 @@
import React, { useCallback, useEffect } from 'react';
import { useParams, useHistory } from 'react-router-dom';
import React from 'react';
import { useParams } from 'react-router-dom';
import MakeJournalEntriesForm from './MakeJournalEntriesForm';
import { MakeJournalProvider } from './MakeJournalProvider';
import withDashboardActions from 'containers/Dashboard/withDashboardActions';
import { compose } from 'utils';
import 'style/pages/ManualJournal/MakeJournal.scss';
/**
* Make journal entries page.
*/
function MakeJournalEntriesPage({
// #withDashboardActions
setSidebarShrink,
resetSidebarPreviousExpand,
setDashboardBackLink,
}) {
const history = useHistory();
export default function MakeJournalEntriesPage() {
const { id: journalId } = useParams();
useEffect(() => {
// Shrink the sidebar by foce.
setSidebarShrink();
// Show the back link on dashboard topbar.
setDashboardBackLink('/manual-journals');
return () => {
// Reset the sidebar to the previous status.
resetSidebarPreviousExpand();
// Hide the back link on dashboard topbar.
setDashboardBackLink(false);
};
}, [resetSidebarPreviousExpand, setDashboardBackLink, setSidebarShrink]);
const handleFormSubmit = useCallback(
(payload) => {
payload.redirect && history.push('/manual-journals');
},
[history],
);
const handleCancel = useCallback(() => {
history.goBack();
}, [history]);
return (
<MakeJournalProvider journalId={journalId}>
<MakeJournalEntriesForm
onFormSubmit={handleFormSubmit}
onCancelForm={handleCancel}
/>
<MakeJournalEntriesForm />
</MakeJournalProvider>
);
}
export default compose(
withDashboardActions,
)(MakeJournalEntriesPage);

View File

@@ -1,205 +1,111 @@
import React, { useState, useMemo, useEffect, useCallback } from 'react';
import React from 'react';
import { Button } from '@blueprintjs/core';
import { FormattedMessage as T, useIntl } from 'react-intl';
import { omit } from 'lodash';
import { saveInvoke } from 'utils';
import {
AccountsListFieldCell,
MoneyFieldCell,
InputGroupCell,
ContactsListFieldCell,
} from 'components/DataTableCells';
import {
ContactHeaderCell,
ActionsCellRenderer,
TotalAccountCellRenderer,
TotalCreditDebitCellRenderer,
NoteCellRenderer,
} from './components';
import { FormattedMessage as T } from 'react-intl';
import { saveInvoke, removeRowsByIndex } from 'utils';
import { DataTableEditable } from 'components';
import withAlertActions from 'containers/Alert/withAlertActions';
import { updateDataReducer } from './utils';
import { useMakeJournalFormContext } from './MakeJournalProvider';
import { useJournalTableEntriesColumns } from './components';
import JournalDeleteEntriesAlert from 'containers/Alerts/ManualJournals/JournalDeleteEntriesAlert';
import { compose } from 'redux';
import { repeatValue } from 'utils';
/**
* Make journal entries table component.
*/
export default function MakeJournalEntriesTable({
function MakeJournalEntriesTable({
// #withAlertsActions
openAlert,
// #ownPorps
onClickRemoveRow,
onClickAddNewRow,
onClickClearAllLines,
onChange,
entries,
defaultEntry,
error,
initialLinesNumber = 4,
minLinesNumber = 4,
}) {
const [rows, setRows] = useState([]);
const { formatMessage } = useIntl();
const { accounts, customers } = useMakeJournalFormContext();
useEffect(() => {
setRows([...entries.map((e) => ({ ...e, rowType: 'editor' }))]);
}, [entries, setRows]);
// Final table rows editor rows and total and final blank row.
const tableRows = useMemo(() => [...rows, { rowType: 'total' }], [rows]);
// Memorized data table columns.
const columns = useMemo(
() => [
{
Header: '#',
accessor: 'index',
Cell: ({ row: { index } }) => <span>{index + 1}</span>,
className: 'index',
width: 40,
disableResizing: true,
disableSortBy: true,
sticky: 'left',
},
{
Header: formatMessage({ id: 'account' }),
id: 'account_id',
accessor: 'account_id',
Cell: TotalAccountCellRenderer(AccountsListFieldCell),
className: 'account',
disableSortBy: true,
width: 140,
},
{
Header: formatMessage({ id: 'credit_currency' }, { currency: 'USD' }),
accessor: 'credit',
Cell: TotalCreditDebitCellRenderer(MoneyFieldCell, 'credit'),
className: 'credit',
disableSortBy: true,
width: 100,
},
{
Header: formatMessage({ id: 'debit_currency' }, { currency: 'USD' }),
accessor: 'debit',
Cell: TotalCreditDebitCellRenderer(MoneyFieldCell, 'debit'),
className: 'debit',
disableSortBy: true,
width: 100,
},
{
Header: ContactHeaderCell,
id: 'contact_id',
accessor: 'contact_id',
Cell: NoteCellRenderer(ContactsListFieldCell),
className: 'contact',
disableSortBy: true,
width: 120,
},
{
Header: formatMessage({ id: 'note' }),
accessor: 'note',
Cell: NoteCellRenderer(InputGroupCell),
disableSortBy: true,
className: 'note',
width: 200,
},
{
Header: '',
accessor: 'action',
Cell: ActionsCellRenderer,
className: 'actions',
disableSortBy: true,
disableResizing: true,
width: 45,
},
],
[formatMessage],
);
const columns = useJournalTableEntriesColumns();
// Handles click new line.
const onClickNewRow = () => {
saveInvoke(onClickAddNewRow);
const newRows = [...entries, defaultEntry];
saveInvoke(onChange, newRows);
};
// Handles update datatable data.
const handleUpdateData = (rowIndex, columnId, value) => {
const newRows = updateDataReducer(rows, rowIndex, columnId, value);
saveInvoke(
onChange,
newRows
.filter((row) => row.rowType === 'editor')
.map((row) => ({
...omit(row, ['rowType']),
})),
);
const newRows = updateDataReducer(entries, rowIndex, columnId, value);
saveInvoke(onChange, newRows);
};
// Handle remove datatable row.
const handleRemoveRow = (rowIndex) => {
// Can't continue if there is just one row line or less.
if (rows.length <= 2) {
return;
}
const removeIndex = parseInt(rowIndex, 10);
const newRows = rows.filter((row, index) => index !== removeIndex);
saveInvoke(
onChange,
newRows
.filter((row) => row.rowType === 'editor')
.map((row) => ({ ...omit(row, ['rowType']) })),
);
saveInvoke(onClickRemoveRow, removeIndex);
const newRows = removeRowsByIndex(entries, rowIndex);
saveInvoke(onChange, newRows);
};
// Rows class names callback.
const rowClassNames = useCallback(
(row) => ({
'row--total': rows.length === row.index + 2,
}),
[rows],
);
// Handle clear all lines action.
const handleClickClearAllLines = () => {
saveInvoke(onClickClearAllLines);
openAlert('make-journal-delete-all-entries');
};
// Handle clear all lines alaert confirm.
const handleCofirmClearEntriesAlert = () => {
const newRows = repeatValue(defaultEntry, initialLinesNumber);
saveInvoke(onChange, newRows);
};
return (
<DataTableEditable
columns={columns}
data={tableRows}
rowClassNames={rowClassNames}
sticky={true}
totalRow={true}
payload={{
accounts,
errors: error,
updateData: handleUpdateData,
removeRow: handleRemoveRow,
contacts: [
...customers.map((customer) => ({
<>
<DataTableEditable
columns={columns}
data={entries}
sticky={true}
totalRow={true}
payload={{
accounts,
errors: error,
updateData: handleUpdateData,
removeRow: handleRemoveRow,
contacts: customers.map((customer) => ({
...customer,
contact_type: 'customer',
})),
],
autoFocus: ['account_id', 0],
}}
actions={
<>
<Button
small={true}
className={'button--secondary button--new-line'}
onClick={onClickNewRow}
>
<T id={'new_lines'} />
</Button>
autoFocus: ['account_id', 0],
}}
actions={
<>
<Button
small={true}
className={'button--secondary button--new-line'}
onClick={onClickNewRow}
>
<T id={'new_lines'} />
</Button>
<Button
small={true}
className={'button--secondary button--clear-lines ml1'}
onClick={handleClickClearAllLines}
>
<T id={'clear_all_lines'} />
</Button>
</>
}
/>
<Button
small={true}
className={'button--secondary button--clear-lines ml1'}
onClick={handleClickClearAllLines}
>
<T id={'clear_all_lines'} />
</Button>
</>
}
/>
<JournalDeleteEntriesAlert
name={'make-journal-delete-all-entries'}
onConfirm={handleCofirmClearEntriesAlert}
/>
</>
);
}
export default compose(withAlertActions)(MakeJournalEntriesTable);

View File

@@ -80,8 +80,8 @@ export default function MakeJournalFloatingAction() {
<If condition={!manualJournal || !manualJournal?.is_published}>
<ButtonGroup>
<Button
disabled={isSubmitting}
loading={isSubmitting}
disabled={isSubmitting}
intent={Intent.PRIMARY}
onClick={handleSubmitPublishBtnClick}
text={<T id={'save_publish'} />}
@@ -146,6 +146,7 @@ export default function MakeJournalFloatingAction() {
<If condition={manualJournal && manualJournal?.is_published}>
<ButtonGroup>
<Button
loading={isSubmitting}
disabled={isSubmitting}
intent={Intent.PRIMARY}
onClick={handleSubmitPublishBtnClick}

View File

@@ -1,54 +0,0 @@
import { useEffect } from 'react';
import { compose } from 'redux';
import { useFormikContext } from 'formik';
import withManualJournalsActions from './withManualJournalsActions';
import withDashboardActions from 'containers/Dashboard/withDashboardActions';
import withManualJournals from './withManualJournals';
import { defaultToTransform } from 'utils';
/**
* Journal number chaلing watcher.
*/
function MakeJournalNumberChangingWatcher({
// #withDashboardActions
changePageSubtitle,
// #withManualJournals
journalNumberChanged,
// #withManualJournalsActions
setJournalNumberChanged,
// #ownProps
journalNumber,
}) {
const { setFieldValue } = useFormikContext();
// Observes journal number settings changes.
useEffect(() => {
if (journalNumberChanged) {
setFieldValue('journal_number', journalNumber);
changePageSubtitle(
defaultToTransform(journalNumber, `No. ${journalNumber}`, ''),
);
setJournalNumberChanged(false);
}
}, [
journalNumber,
journalNumberChanged,
setJournalNumberChanged,
setFieldValue,
changePageSubtitle,
]);
return null;
}
export default compose(
withManualJournals(({ journalNumberChanged }) => ({
journalNumberChanged,
})),
withManualJournalsActions,
withDashboardActions,
)(MakeJournalNumberChangingWatcher);

View File

@@ -42,6 +42,7 @@ function MakeJournalProvider({ journalId, ...props }) {
// Loading the journal settings.
const { isFetching: isSettingsLoading } = useSettings();
// Submit form payload.
const [submitPayload, setSubmitPayload] = useState({});
const provider = {

View File

@@ -1,7 +1,14 @@
import React from 'react';
import { Position } from '@blueprintjs/core';
import { FormattedMessage as T } from 'react-intl';
import { Money, Hint } from 'components';
import { Intent, Position, Button, Tooltip } from '@blueprintjs/core';
import { FormattedMessage as T, useIntl } from 'react-intl';
import { Icon, Money, Hint } from 'components';
import {
AccountsListFieldCell,
MoneyFieldCell,
InputGroupCell,
ContactsListFieldCell,
} from 'components/DataTableCells';
import { safeSumBy } from 'utils';
/**
* Contact header cell.
@@ -18,74 +25,138 @@ export function ContactHeaderCell() {
);
}
/**
* Total text cell renderer.
/**
* Account footer cell.
*/
export const TotalAccountCellRenderer = (chainedComponent) => (props) => {
if (props.data.length === props.row.index + 1) {
return <span>{'Total USD'}</span>;
}
return chainedComponent(props);
};
function AccountFooterCell() {
return <span>{'Total USD'}</span>;
}
/**
* Total credit/debit cell renderer.
* Total credit table footer cell.
*/
export const TotalCreditDebitCellRenderer = (chainedComponent, type) => (
props,
) => {
if (props.data.length === props.row.index + 1) {
const total = props.data.reduce((total, entry) => {
const amount = parseInt(entry[type], 10);
const computed = amount ? total + amount : total;
function TotalCreditFooterCell({ rows }) {
const credit = safeSumBy(rows, 'original.credit');
return computed;
}, 0);
return (
<span>
<Money amount={total} currency={'USD'} />
</span>
);
}
return chainedComponent(props);
};
export const NoteCellRenderer = (chainedComponent) => (props) => {
if (props.data.length === props.row.index + 1) {
return '';
}
return chainedComponent(props);
};
return (
<span>
<Money amount={credit} currency={'USD'} />
</span>
);
}
/**
* Total debit table footer cell.
*/
function TotalDebitFooterCell({ rows }) {
const debit = safeSumBy(rows, 'original.debit');
return (
<span>
<Money amount={debit} currency={'USD'} />
</span>
);
}
/**
* Actions cell renderer.
*/
export const ActionsCellRenderer = ({
row: { index },
column: { id },
cell: { value: initialValue },
data,
payload,
}) => {
if (data.length <= index + 1) {
return '';
}
const onClickRemoveRole = () => {
payload.removeRow(index);
};
return (
<Tooltip content={<T id={'remove_the_line'} />} position={Position.LEFT}>
<Button
icon={<Icon icon="times-circle" iconSize={14} />}
iconSize={14}
className="ml2"
minimal={true}
intent={Intent.DANGER}
onClick={onClickRemoveRole}
/>
</Tooltip>
);
row: { index },
column: { id },
cell: { value: initialValue },
data,
payload,
}) => {
const onClickRemoveRole = () => {
payload.removeRow(index);
};
return (
<Tooltip content={<T id={'remove_the_line'} />} position={Position.LEFT}>
<Button
icon={<Icon icon="times-circle" iconSize={14} />}
iconSize={14}
className="ml2"
minimal={true}
intent={Intent.DANGER}
onClick={onClickRemoveRole}
/>
</Tooltip>
);
};
/**
* Retrieve columns of make journal entries table.
*/
export const useJournalTableEntriesColumns = () => {
const { formatMessage } = useIntl();
return React.useMemo(
() => [
{
Header: '#',
accessor: 'index',
Cell: ({ row: { index } }) => <span>{index + 1}</span>,
className: 'index',
width: 40,
disableResizing: true,
disableSortBy: true,
sticky: 'left',
},
{
Header: formatMessage({ id: 'account' }),
id: 'account_id',
accessor: 'account_id',
Cell: AccountsListFieldCell,
Footer: AccountFooterCell,
className: 'account',
disableSortBy: true,
width: 160,
},
{
Header: formatMessage({ id: 'credit_currency' }, { currency: 'USD' }),
accessor: 'credit',
Cell: MoneyFieldCell,
Footer: TotalCreditFooterCell,
className: 'credit',
disableSortBy: true,
width: 100,
},
{
Header: formatMessage({ id: 'debit_currency' }, { currency: 'USD' }),
accessor: 'debit',
Cell: MoneyFieldCell,
Footer: TotalDebitFooterCell,
className: 'debit',
disableSortBy: true,
width: 100,
},
{
Header: ContactHeaderCell,
id: 'contact_id',
accessor: 'contact_id',
Cell: ContactsListFieldCell,
className: 'contact',
disableSortBy: true,
width: 120,
},
{
Header: formatMessage({ id: 'note' }),
accessor: 'note',
Cell: InputGroupCell,
disableSortBy: true,
className: 'note',
width: 200,
},
{
Header: '',
accessor: 'action',
Cell: ActionsCellRenderer,
className: 'actions',
disableSortBy: true,
disableResizing: true,
width: 45,
},
],
[formatMessage],
);
};

View File

@@ -1,8 +1,9 @@
import React from 'react';
import { Intent } from '@blueprintjs/core';
import { sumBy, setWith, toSafeInteger, get } from 'lodash';
import moment from 'moment';
import { transformUpdatedRows } from 'utils';
import { transformUpdatedRows, repeatValue, transformToForm } from 'utils';
import { AppToaster } from 'components';
import { formatMessage } from 'services/intl';
@@ -17,6 +18,44 @@ const ERROR = {
ENTRIES_SHOULD_ASSIGN_WITH_CONTACT: 'ENTRIES_SHOULD_ASSIGN_WITH_CONTACT',
};
export const MIN_LINES_NUMBER = 4;
export const defaultEntry = {
index: 0,
account_id: '',
credit: '',
debit: '',
contact_id: '',
note: '',
};
export const defaultManualJournal = {
journal_number: '',
journal_type: 'Journal',
date: moment(new Date()).format('YYYY-MM-DD'),
description: '',
reference: '',
currency_code: '',
publish: '',
entries: [...repeatValue(defaultEntry, 4)],
};
// Transform to edit form.
export function transformToEditForm(manualJournal) {
return {
...transformToForm(manualJournal, defaultManualJournal),
entries: [
...manualJournal.entries.map((entry) => ({
...transformToForm(entry, defaultManualJournal.entries[0]),
})),
...repeatValue(
defaultEntry,
Math.max(MIN_LINES_NUMBER - manualJournal.entries.length, 0),
),
],
};
}
/**
* Entries adjustment.
*/
@@ -30,6 +69,9 @@ function adjustmentEntries(entries) {
};
}
/**
*
*/
export const updateDataReducer = (rows, rowIndex, columnId, value) => {
let newRows = transformUpdatedRows(rows, rowIndex, columnId, value);
@@ -59,7 +101,9 @@ export const updateDataReducer = (rows, rowIndex, columnId, value) => {
return newRows;
};
// Transform API errors in toasts messages.
/**
* Transform API errors in toasts messages.
*/
export const transformErrors = (resErrors, { setErrors, errors }) => {
const getError = (errorType) => resErrors.find((e) => e.type === errorType);
const toastMessages = [];

View File

@@ -1,9 +1,8 @@
import React, { useEffect } from 'react';
import { useIntl } from 'react-intl';
import React from 'react';
import 'style/pages/Accounts/List.scss';
import { DashboardPageContent, DashboardContentTable } from 'components';
import DashboardPageContent from 'components/Dashboard/DashboardPageContent';
import { AccountsChartProvider } from './AccountsChartProvider';
import AccountsViewsTabs from 'containers/Accounts/AccountsViewsTabs';
@@ -11,7 +10,6 @@ import AccountsActionsBar from 'containers/Accounts/AccountsActionsBar';
import AccountsAlerts from './AccountsAlerts';
import AccountsDataTable from './AccountsDataTable';
import withDashboardActions from 'containers/Dashboard/withDashboardActions';
import withAccounts from 'containers/Accounts/withAccounts';
import { transformTableStateToQuery, compose } from 'utils';
@@ -20,18 +18,9 @@ import { transformTableStateToQuery, compose } from 'utils';
* Accounts chart list.
*/
function AccountsChart({
// #withDashboardActions
changePageTitle,
// #withAccounts
accountsTableState,
}) {
const { formatMessage } = useIntl();
useEffect(() => {
changePageTitle(formatMessage({ id: 'chart_of_accounts' }));
}, [changePageTitle, formatMessage]);
return (
<AccountsChartProvider
query={transformTableStateToQuery(accountsTableState)}
@@ -40,7 +29,10 @@ function AccountsChart({
<DashboardPageContent>
<AccountsViewsTabs />
<AccountsDataTable />
<DashboardContentTable>
<AccountsDataTable />
</DashboardContentTable>
</DashboardPageContent>
<AccountsAlerts />
@@ -49,6 +41,5 @@ function AccountsChart({
}
export default compose(
withDashboardActions,
withAccounts(({ accountsTableState }) => ({ accountsTableState })),
)(AccountsChart);

View File

@@ -1,11 +1,8 @@
import React from 'react';
import classNames from 'classnames';
import { TableFastCell, DataTable } from 'components';
import { compose } from 'utils';
import { CLASSES } from 'common/classes';
import { useAccountsTableColumns, rowClassNames } from './utils';
import { ActionsMenu } from './components';
@@ -72,48 +69,46 @@ function AccountsDataTable({
};
return (
<div className={classNames(CLASSES.DASHBOARD_DATATABLE)}>
<DataTable
noInitialFetch={true}
columns={columns}
data={accounts}
selectionColumn={true}
expandable={true}
sticky={true}
<DataTable
noInitialFetch={true}
columns={columns}
data={accounts}
selectionColumn={true}
expandable={true}
sticky={true}
loading={isAccountsLoading}
headerLoading={isAccountsLoading}
progressBarLoading={isAccountsFetching}
rowClassNames={rowClassNames}
loading={isAccountsLoading}
headerLoading={isAccountsLoading}
progressBarLoading={isAccountsFetching}
rowClassNames={rowClassNames}
autoResetExpanded={false}
autoResetSortBy={false}
autoResetSelectedRows={false}
autoResetExpanded={false}
autoResetSortBy={false}
autoResetSelectedRows={false}
expandColumnSpace={1}
expandToggleColumn={2}
selectionColumnWidth={50}
expandColumnSpace={1}
expandToggleColumn={2}
selectionColumnWidth={50}
TableCellRenderer={TableFastCell}
TableRowsRenderer={TableVirtualizedListRows}
TableLoadingRenderer={TableSkeletonRows}
TableHeaderSkeletonRenderer={TableSkeletonHeader}
ContextMenu={ActionsMenu}
TableCellRenderer={TableFastCell}
TableRowsRenderer={TableVirtualizedListRows}
TableLoadingRenderer={TableSkeletonRows}
TableHeaderSkeletonRenderer={TableSkeletonHeader}
ContextMenu={ActionsMenu}
// #TableVirtualizedListRows props.
vListrowHeight={42}
vListOverscanRowCount={10}
// #TableVirtualizedListRows props.
vListrowHeight={42}
vListOverscanRowCount={10}
payload={{
onEdit: handleEditAccount,
onDelete: handleDeleteAccount,
onActivate: handleActivateAccount,
onInactivate: handleInactivateAccount,
newChild: handleNewChildAccount
}}
/>
</div>
payload={{
onEdit: handleEditAccount,
onDelete: handleDeleteAccount,
onActivate: handleActivateAccount,
onInactivate: handleInactivateAccount,
newChild: handleNewChildAccount
}}
/>
);
}

View File

@@ -0,0 +1,55 @@
import React from 'react';
import { Intent, Alert } from '@blueprintjs/core';
import { FormattedMessage as T } from 'react-intl';
import withAlertActions from 'containers/Alert/withAlertActions';
import withAlertStoreConnect from 'containers/Alert/withAlertStoreConnect';
import { compose, saveInvoke } from 'utils';
/**
* Alert description.
*/
function ExpenseDeleteEntriesAlert({
name,
onConfirm,
// #withAlertStoreConnect
isOpen,
payload: { },
// #withAlertActions
closeAlert,
}) {
// Handle the alert cancel.
const handleCancel = () => {
closeAlert(name);
};
// Handle confirm the alert.
const handleConfirm = (event) => {
closeAlert(name);
saveInvoke(onConfirm, event)
};
return (
<Alert
cancelButtonText={<T id={'cancel'} />}
confirmButtonText={<T id={'clear_all_lines'} />}
intent={Intent.DANGER}
isOpen={isOpen}
onCancel={handleCancel}
onConfirm={handleConfirm}
loading={false}
>
<p>
Clearing the table lines will delete all expense amounts were applied, Is this okay?
</p>
</Alert>
);
}
export default compose(
withAlertStoreConnect(),
withAlertActions,
)(ExpenseDeleteEntriesAlert);

View File

@@ -0,0 +1,55 @@
import React from 'react';
import { Intent, Alert } from '@blueprintjs/core';
import { FormattedMessage as T } from 'react-intl';
import withAlertActions from 'containers/Alert/withAlertActions';
import withAlertStoreConnect from 'containers/Alert/withAlertStoreConnect';
import { compose, saveInvoke } from 'utils';
/**
* Items entries table clear all lines alert.
*/
function ItemsEntriesDeleteAlert({
name,
onConfirm,
// #withAlertStoreConnect
isOpen,
payload: { },
// #withAlertActions
closeAlert,
}) {
// Handle the alert cancel.
const handleCancel = () => {
closeAlert(name);
};
// Handle confirm the alert.
const handleConfirm = (event) => {
closeAlert(name);
saveInvoke(onConfirm, event)
};
return (
<Alert
cancelButtonText={<T id={'cancel'} />}
confirmButtonText={<T id={'clear_all_lines'} />}
intent={Intent.DANGER}
isOpen={isOpen}
onCancel={handleCancel}
onConfirm={handleConfirm}
loading={false}
>
<p>
Clearing the table lines will delete all quantities and rate were applied to the items, Is this okay?
</p>
</Alert>
);
}
export default compose(
withAlertStoreConnect(),
withAlertActions,
)(ItemsEntriesDeleteAlert);

View File

@@ -0,0 +1,56 @@
import React from 'react';
import { Intent, Alert } from '@blueprintjs/core';
import { FormattedMessage as T } from 'react-intl';
import withAlertActions from 'containers/Alert/withAlertActions';
import withAlertStoreConnect from 'containers/Alert/withAlertStoreConnect';
import { compose, saveInvoke } from 'utils';
/**
* Make journal delete entries alert.
*/
function JournalDeleteEntriesAlert({
// #ownProps
name,
onConfirm,
// #withAlertStoreConnect
isOpen,
payload: { },
// #withAlertActions
closeAlert,
}) {
// Handle the alert cancel.
const handleCancel = () => {
closeAlert(name);
};
// Handle confirm delete manual journal.
const handleConfirm = (event) => {
closeAlert(name);
saveInvoke(onConfirm, event);
};
return (
<Alert
cancelButtonText={<T id={'cancel'} />}
confirmButtonText={<T id={'clear_all_lines'} />}
intent={Intent.DANGER}
isOpen={isOpen}
onCancel={handleCancel}
onConfirm={handleConfirm}
loading={false}
>
<p>
Clearing the table lines will delete all credits and debits were applied, Is this okay?
</p>
</Alert>
);
}
export default compose(
withAlertStoreConnect(),
withAlertActions,
)(JournalDeleteEntriesAlert);

View File

@@ -1,4 +1,4 @@
import React, { useMemo, useEffect } from 'react';
import React, { useMemo } from 'react';
import { Formik, Form } from 'formik';
import moment from 'moment';
import { Intent } from '@blueprintjs/core';
@@ -14,7 +14,6 @@ import CustomerFormAfterPrimarySection from './CustomerFormAfterPrimarySection';
import CustomersTabs from './CustomersTabs';
import CustomerFloatingActions from './CustomerFloatingActions';
import withDashboardActions from 'containers/Dashboard/withDashboardActions';
import withSettings from 'containers/Settings/withSettings';
import { compose, transformToForm } from 'utils';
@@ -61,9 +60,6 @@ const defaultInitialValues = {
* Customer form.
*/
function CustomerForm({
// #withDashboardActions
changePageTitle,
// #withSettings
baseCurrency,
}) {
@@ -90,13 +86,7 @@ function CustomerForm({
}),
[customer, baseCurrency],
);
useEffect(() => {
!isNewMode
? changePageTitle(formatMessage({ id: 'edit_customer' }))
: changePageTitle(formatMessage({ id: 'new_customer' }));
}, [changePageTitle, isNewMode, formatMessage]);
//Handles the form submit.
const handleFormSubmit = (
values,
@@ -165,5 +155,4 @@ export default compose(
withSettings(({ organizationSettings }) => ({
baseCurrency: organizationSettings?.baseCurrency,
})),
withDashboardActions,
)(CustomerForm);

View File

@@ -1,49 +1,48 @@
import * as Yup from 'yup';
import { formatMessage } from 'services/intl';
const Schema = Yup.object().shape({
customer_type: Yup.string()
.required()
.trim()
.label(formatMessage({ id: 'customer_type_' })),
salutation: Yup.string().trim(),
first_name: Yup.string().trim(),
last_name: Yup.string().trim(),
company_name: Yup.string().trim(),
display_name: Yup.string()
.trim()
.required()
.label(formatMessage({ id: 'display_name_' })),
customer_type: Yup.string()
.required()
.trim()
.label(formatMessage({ id: 'customer_type_' })),
salutation: Yup.string().trim(),
first_name: Yup.string().trim(),
last_name: Yup.string().trim(),
company_name: Yup.string().trim(),
display_name: Yup.string()
.trim()
.required()
.label(formatMessage({ id: 'display_name_' })),
email: Yup.string().email().nullable(),
work_phone: Yup.number(),
personal_phone: Yup.number(),
website: Yup.string().url().nullable(),
email: Yup.string().email().nullable(),
work_phone: Yup.number(),
personal_phone: Yup.number(),
website: Yup.string().url().nullable(),
active: Yup.boolean(),
note: Yup.string().trim(),
active: Yup.boolean(),
note: Yup.string().trim(),
billing_address_country: Yup.string().trim(),
billing_address_1: Yup.string().trim(),
billing_address_2: Yup.string().trim(),
billing_address_city: Yup.string().trim(),
billing_address_state: Yup.string().trim(),
billing_address_postcode: Yup.number().nullable(),
billing_address_phone: Yup.number(),
billing_address_country: Yup.string().trim(),
billing_address_1: Yup.string().trim(),
billing_address_2: Yup.string().trim(),
billing_address_city: Yup.string().trim(),
billing_address_state: Yup.string().trim(),
billing_address_postcode: Yup.number().nullable(),
billing_address_phone: Yup.number(),
shipping_address_country: Yup.string().trim(),
shipping_address_1: Yup.string().trim(),
shipping_address_2: Yup.string().trim(),
shipping_address_city: Yup.string().trim(),
shipping_address_state: Yup.string().trim(),
shipping_address_postcode: Yup.number().nullable(),
shipping_address_phone: Yup.number(),
shipping_address_country: Yup.string().trim(),
shipping_address_1: Yup.string().trim(),
shipping_address_2: Yup.string().trim(),
shipping_address_city: Yup.string().trim(),
shipping_address_state: Yup.string().trim(),
shipping_address_postcode: Yup.number().nullable(),
shipping_address_phone: Yup.number(),
opening_balance: Yup.number().nullable(),
currency_code: Yup.string(),
opening_balance_at: Yup.date(),
});
opening_balance: Yup.number().nullable(),
currency_code: Yup.string(),
opening_balance_at: Yup.date(),
});
export const CreateCustomerForm = Schema;
export const EditCustomerForm = Schema;
export const EditCustomerForm = Schema;

View File

@@ -1,9 +1,8 @@
import React, { useEffect } from 'react';
import { useIntl } from 'react-intl';
import React from 'react';
import 'style/pages/Customers/List.scss';
import DashboardPageContent from 'components/Dashboard/DashboardPageContent';
import { DashboardContentTable, DashboardPageContent } from 'components';
import CustomersActionsBar from './CustomersActionsBar';
import CustomersViewsTabs from './CustomersViewsTabs';
@@ -12,27 +11,15 @@ import CustomersAlerts from 'containers/Customers/CustomersAlerts';
import { CustomersListProvider } from './CustomersListProvider';
import withCustomers from './withCustomers';
import withDashboardActions from 'containers/Dashboard/withDashboardActions';
import { transformTableStateToQuery, compose } from 'utils';
/**
* Customers list.
*/
function CustomersList({
// #withDashboardActions
changePageTitle,
// #withCustomers
customersTableState,
}) {
const { formatMessage } = useIntl();
// Changes the dashboard page title once the page mount.
useEffect(() => {
changePageTitle(formatMessage({ id: 'customers_list' }));
}, [changePageTitle, formatMessage]);
return (
<CustomersListProvider
query={transformTableStateToQuery(customersTableState)}
@@ -41,7 +28,10 @@ function CustomersList({
<DashboardPageContent>
<CustomersViewsTabs />
<CustomersTable />
<DashboardContentTable>
<CustomersTable />
</DashboardContentTable>
</DashboardPageContent>
<CustomersAlerts />
</CustomersListProvider>
@@ -49,6 +39,5 @@ function CustomersList({
}
export default compose(
withDashboardActions,
withCustomers(({ customersTableState }) => ({ customersTableState })),
)(CustomersList);

View File

@@ -1,13 +1,11 @@
import React from 'react';
import classNames from 'classnames';
import { useHistory } from 'react-router-dom';
import CustomersEmptyStatus from './CustomersEmptyStatus';
import TableSkeletonRows from 'components/Datatable/TableSkeletonRows';
import TableSkeletonHeader from 'components/Datatable/TableHeaderSkeleton';
import { DataTable, Choose } from 'components';
import { CLASSES } from 'common/classes';
import { DataTable } from 'components';
import withCustomersActions from './withCustomersActions';
import withAlertsActions from 'containers/Alert/withAlertActions';
@@ -23,10 +21,10 @@ import { compose } from 'utils';
*/
function CustomersTable({
// #withCustomersActions
setCustomersTableState,
setCustomersTableState,
// #withAlerts
openAlert
openAlert,
}) {
const history = useHistory();
@@ -56,60 +54,47 @@ function CustomersTable({
// Handles the customer delete action.
const handleCustomerDelete = (customer) => {
openAlert('customer-delete', { customerId: customer.id })
openAlert('customer-delete', { customerId: customer.id });
};
// Handle the customer edit action.
const handleCustomerEdit = (customer) => {
history.push(`/customers/${customer.id}/edit`);
};
if (isEmptyStatus) {
return <CustomersEmptyStatus />;
}
return (
<div className={classNames(CLASSES.DASHBOARD_DATATABLE)}>
<Choose>
<Choose.When condition={isEmptyStatus}>
<CustomersEmptyStatus />
</Choose.When>
<Choose.Otherwise>
<DataTable
noInitialFetch={true}
columns={columns}
data={customers}
loading={isCustomersLoading}
headerLoading={isCustomersLoading}
progressBarLoading={isCustomersFetching}
onFetchData={handleFetchData}
selectionColumn={true}
expandable={false}
sticky={true}
spinnerProps={{ size: 30 }}
pagination={true}
manualSortBy={true}
manualPagination={true}
pagesCount={pagination.pagesCount}
autoResetSortBy={false}
autoResetPage={false}
TableLoadingRenderer={TableSkeletonRows}
TableHeaderSkeletonRenderer={TableSkeletonHeader}
payload={{
onDelete: handleCustomerDelete,
onEdit: handleCustomerEdit,
}}
ContextMenu={ActionsMenu}
/>
</Choose.Otherwise>
</Choose>
</div>
<DataTable
noInitialFetch={true}
columns={columns}
data={customers}
loading={isCustomersLoading}
headerLoading={isCustomersLoading}
progressBarLoading={isCustomersFetching}
onFetchData={handleFetchData}
selectionColumn={true}
expandable={false}
sticky={true}
spinnerProps={{ size: 30 }}
pagination={true}
manualSortBy={true}
manualPagination={true}
pagesCount={pagination.pagesCount}
autoResetSortBy={false}
autoResetPage={false}
TableLoadingRenderer={TableSkeletonRows}
TableHeaderSkeletonRenderer={TableSkeletonHeader}
payload={{
onDelete: handleCustomerDelete,
onEdit: handleCustomerEdit,
}}
ContextMenu={ActionsMenu}
/>
);
};
}
export default compose(
withAlertsActions,

View File

@@ -0,0 +1,27 @@
import React from 'react';
import { AccountDialogProvider } from './AccountDialogProvider';
import AccountDialogForm from './AccountDialogForm';
/**
* Account dialog content.
*/
export default function AccountDialogContent({
dialogName,
accountId,
action,
parentAccountId,
accountType,
}) {
return (
<AccountDialogProvider
dialogName={dialogName}
accountId={accountId}
action={action}
parentAccountId={parentAccountId}
accountType={accountType}
>
<AccountDialogForm />
</AccountDialogProvider>
);
}

View File

@@ -3,27 +3,20 @@ import { Intent } from '@blueprintjs/core';
import { Formik } from 'formik';
import { useIntl } from 'react-intl';
import { omit } from 'lodash';
import { AppToaster, DialogContent } from 'components';
import { AppToaster } from 'components';
import AccountFormDialogFields from './AccountFormDialogFields';
import AccountDialogFormContent from './AccountDialogFormContent';
import withDialogActions from 'containers/Dialog/withDialogActions';
import {
EditAccountFormSchema,
CreateAccountFormSchema,
} from './AccountForm.schema';
import {
useAccounts,
useAccountsTypes,
useCreateAccount,
useAccount,
useEditAccount
} from 'hooks/query';
import { compose, transformToForm } from 'utils';
import { transformApiErrors, transformAccountToForm } from './utils';
import 'style/pages/Accounts/AccountFormDialog.scss';
import { useAccountDialogContext } from './AccountDialogProvider';
// Default initial form values.
const defaultInitialValues = {
@@ -41,43 +34,28 @@ const defaultInitialValues = {
function AccountFormDialogContent({
// #withDialogActions
closeDialog,
// #ownProp
dialogName,
accountId,
action,
parentAccountId,
accountType,
}) {
const { formatMessage } = useIntl();
const isNewMode = !accountId;
// Account form context.
const {
editAccountMutate,
createAccountMutate,
account,
accountId,
action,
parentAccountId,
accountType,
isNewMode,
dialogName
} = useAccountDialogContext();
// Form validation schema in create and edit mode.
const validationSchema = isNewMode
? CreateAccountFormSchema
: EditAccountFormSchema;
const { mutateAsync: createAccountMutate } = useCreateAccount();
const { mutateAsync: editAccountMutate } = useEditAccount();
// Fetches accounts list.
const {
data: accounts,
isLoading: isAccountsLoading,
} = useAccounts();
// Fetches accounts types.
const {
data: accountsTypes,
isLoading: isAccountsTypesLoading
} = useAccountsTypes();
// Fetches the specific account details.
const {
data: account,
isLoading: isAccountLoading,
} = useAccount(accountId, { enabled: !!accountId });
// Callbacks handles form submit.
const handleFormSubmit = (values, { setSubmitting, setErrors }) => {
const form = omit(values, ['subaccount']);
@@ -106,18 +84,22 @@ function AccountFormDialogContent({
};
// Handle request error.
const handleError = (error) => {
const { response: { data: { errors } } } = error;
const {
response: {
data: { errors },
},
} = error;
const errorsTransformed = transformApiErrors(errors);
setErrors({ ...errorsTransformed });
setSubmitting(false);
};
if (accountId) {
editAccountMutate(accountId, form)
editAccountMutate([accountId, form]).then(handleSuccess).catch(handleError);
} else {
createAccountMutate({ ...form })
.then(handleSuccess)
.catch(handleError);
} else {
createAccountMutate({ ...form }).then(handleSuccess).catch(handleError);
}
};
@@ -144,30 +126,19 @@ function AccountFormDialogContent({
closeDialog(dialogName);
}, [closeDialog, dialogName]);
const isFetching =
isAccountsLoading ||
isAccountsTypesLoading ||
isAccountLoading;
return (
<DialogContent isLoading={isFetching}>
<Formik
validationSchema={validationSchema}
initialValues={initialValues}
onSubmit={handleFormSubmit}
>
<AccountFormDialogFields
accounts={accounts}
accountsTypes={accountsTypes}
dialogName={dialogName}
action={action}
onClose={handleClose}
/>
</Formik>
</DialogContent>
<Formik
validationSchema={validationSchema}
initialValues={initialValues}
onSubmit={handleFormSubmit}
>
<AccountDialogFormContent
dialogName={dialogName}
action={action}
onClose={handleClose}
/>
</Formik>
);
}
export default compose(
withDialogActions,
)(AccountFormDialogContent);
export default compose(withDialogActions)(AccountFormDialogContent);

View File

@@ -23,6 +23,7 @@ import withAccounts from 'containers/Accounts/withAccounts';
import { inputIntent } from 'utils';
import { compose } from 'redux';
import { useAutofocus } from 'hooks';
import { useAccountDialogContext } from './AccountDialogProvider';
/**
* Account form dialogs fields.
@@ -31,12 +32,13 @@ function AccountFormDialogFields({
// #ownProps
onClose,
action,
accounts,
accountsTypes,
}) {
const { values, isSubmitting } = useFormikContext();
const accountNameFieldRef = useAutofocus();
// Account form context.
const { accounts, accountsTypes } = useAccountDialogContext();
return (
<Form>
<div className={Classes.DIALOG_BODY}>

View File

@@ -0,0 +1,75 @@
import React, { createContext, useContext } from 'react';
import { DialogContent } from 'components';
import {
useCreateAccount,
useAccountsTypes,
useAccount,
useAccounts,
useEditAccount,
} from 'hooks/query';
const AccountDialogContext = createContext();
/**
* Account form provider.
*/
function AccountDialogProvider({
accountId,
parentAccountId,
action,
accountType,
dialogName,
...props
}) {
// Create and edit account mutations.
const { mutateAsync: createAccountMutate } = useCreateAccount();
const { mutateAsync: editAccountMutate } = useEditAccount();
// Fetches accounts list.
const { data: accounts, isLoading: isAccountsLoading } = useAccounts();
// Fetches accounts types.
const {
data: accountsTypes,
isLoading: isAccountsTypesLoading,
} = useAccountsTypes();
// Fetches the specific account details.
const { data: account, isLoading: isAccountLoading } = useAccount(accountId, {
enabled: !!accountId,
});
const isNewMode = !accountId;
// Provider payload.
const provider = {
dialogName,
accountId,
parentAccountId,
action,
accountType,
createAccountMutate,
editAccountMutate,
accounts,
accountsTypes,
account,
isAccountsLoading,
isNewMode
};
const isLoading =
isAccountsLoading || isAccountsTypesLoading || isAccountLoading;
return (
<DialogContent isLoading={isLoading}>
<AccountDialogContext.Provider value={provider} {...props} />
</DialogContent>
);
}
const useAccountDialogContext = () => useContext(AccountDialogContext);
export { AccountDialogProvider, useAccountDialogContext };

View File

@@ -4,7 +4,7 @@ import { Dialog, DialogSuspense } from 'components';
import withDialogRedux from 'components/DialogReduxConnect';
import { compose } from 'utils';
const AccountFormDialogContent = lazy(() => import('./AccountFormDialogContent'));
const AccountDialogContent = lazy(() => import('./AccountDialogContent'));
/**
* Account form dialog.
@@ -28,7 +28,7 @@ function AccountFormDialog({
isOpen={isOpen}
>
<DialogSuspense>
<AccountFormDialogContent
<AccountDialogContent
dialogName={dialogName}
accountId={payload.id}
action={payload.action}

View File

@@ -1,7 +1,7 @@
import React, { useCallback } from 'react';
import { DialogContent } from 'components';
import { useQuery, queryCache } from 'react-query';
import { useSaveSettings } from 'hooks/query';
import { InvoiceNumberDialogProvider } from './InvoiceNumberDialogProvider';
import ReferenceNumberForm from 'containers/JournalNumber/ReferenceNumberForm';
import withDialogActions from 'containers/Dialog/withDialogActions';
@@ -19,52 +19,41 @@ function InvoiceNumberDialogContent({
// #withSettings
nextNumber,
numberPrefix,
// #withSettingsActions
requestFetchOptions,
requestSubmitOptions,
// #withDialogActions
closeDialog,
// #withInvoicesActions
// setInvoiceNumberChanged,
}) {
const fetchSettings = useQuery(['settings'], () => requestFetchOptions({}));
const { mutateAsync: saveSettings } = useSaveSettings();
const handleSubmitForm = (values, { setSubmitting }) => {
const options = optionsMapToArray(values).map((option) => {
return { key: option.key, ...option, group: 'sales_invoices' };
});
requestSubmitOptions({ options })
saveSettings({ options })
.then(() => {
setSubmitting(false);
closeDialog('invoice-number-form');
setTimeout(() => {
queryCache.invalidateQueries('settings');
// setInvoiceNumberChanged(true);
}, 250);
})
.catch(() => {
setSubmitting(false);
});
};
// Handle the dialog close.
const handleClose = useCallback(() => {
closeDialog('invoice-number-form');
}, [closeDialog]);
return (
<DialogContent isLoading={fetchSettings.isFetching}>
<InvoiceNumberDialogProvider>
<ReferenceNumberForm
initialNumber={nextNumber}
initialPrefix={numberPrefix}
onSubmit={handleSubmitForm}
onClose={handleClose}
/>
</DialogContent>
</InvoiceNumberDialogProvider>
);
}

View File

@@ -0,0 +1,28 @@
import React, { createContext, useContext } from 'react';
import { DialogContent } from 'components';
import { useSettings } from 'hooks/query';
const InvoiceNumberDialogContext = createContext();
/**
* Invoice number dialog provider.
*/
function InvoiceNumberDialogProvider({ query, ...props }) {
const { isLoading } = useSettings();
// Provider payload.
const provider = {
};
return (
<DialogContent isLoading={isLoading}>
<InvoiceNumberDialogContext.Provider value={provider} {...props} />
</DialogContent>
);
}
const useInvoiceNumberDialogContext = () =>
useContext(InvoiceNumberDialogContext);
export { InvoiceNumberDialogProvider, useInvoiceNumberDialogContext };

View File

@@ -43,10 +43,6 @@ function JournalNumberDialogContent({
setSubmitting(false);
closeDialog('journal-number-form');
setTimeout(() => {
queryCache.invalidateQueries('settings');
// setJournalNumberChanged(true);
}, 250);
}).catch(() => {
setSubmitting(false);
});

View File

@@ -1,9 +1,6 @@
import React, { useState } from 'react';
import { FastField, useFormikContext } from 'formik';
import { Alert, Intent } from '@blueprintjs/core';
import { FormattedMessage as T } from 'react-intl';
import React from 'react';
import { FastField } from 'formik';
import ItemsEntriesTable from './ItemsEntriesTable';
import { orderingLinesIndexes, repeatValue } from 'utils';
import 'style/components/DataTable/DataTableEditable.scss';
@@ -12,84 +9,11 @@ import 'style/components/DataTable/DataTableEditable.scss';
*/
export default function EditableItemsEntriesTable({
items,
defaultEntry,
minLinesNumber = 2,
linesNumber = 5,
filterSellableItems = false,
filterPurchasableItems = false,
}) {
const { setFieldValue, values } = useFormikContext();
const [clearLinesAlert, setClearLinesAlert] = useState(false);
const handleClickAddNewRow = () => {
setFieldValue(
'entries',
orderingLinesIndexes([...values.entries, defaultEntry]),
);
};
const handleClearAllLines = () => {
setClearLinesAlert(true);
};
const handleClickRemoveLine = (rowIndex) => {
if (values.entries.length <= minLinesNumber) {
return;
}
const removeIndex = parseInt(rowIndex, 10);
const newRows = values.entries.filter((row, index) => index !== removeIndex);
setFieldValue(
'entries',
orderingLinesIndexes(newRows),
);
};
const handleConfirmClearLines = () => {
setFieldValue(
'entries',
orderingLinesIndexes([...repeatValue(defaultEntry, linesNumber)]),
);
setClearLinesAlert(false);
};
const handleCancelClearLines = () => {
setClearLinesAlert(false);
};
return (
<>
<FastField name={'entries'}>
{({ form, field: { value }, meta: { error, touched } }) => (
<ItemsEntriesTable
onUpdateData={(entries) => {
form.setFieldValue('entries', entries);
}}
items={items}
entries={value}
errors={error}
filterPurchasableItems={filterPurchasableItems}
filterSellableItems={filterSellableItems}
onClickAddNewRow={handleClickAddNewRow}
onClickClearAllLines={handleClearAllLines}
onClickRemoveRow={handleClickRemoveLine}
/>
)}
</FastField>
<Alert
cancelButtonText={<T id={'cancel'} />}
confirmButtonText={<T id={'ok'} />}
intent={Intent.WARNING}
isOpen={clearLinesAlert}
onCancel={handleCancelClearLines}
onConfirm={handleConfirmClearLines}
>
<p>
Clearing the table lines will delete all entries were applied, Is this
okay?
</p>
</Alert>
</>
return (
);
}

View File

@@ -1,251 +1,147 @@
import React, { useState, useMemo, useEffect, useCallback } from 'react';
import { Button, Intent, Position, Tooltip } from '@blueprintjs/core';
import { FormattedMessage as T, useIntl } from 'react-intl';
import React, { useCallback } from 'react';
import { Button } from '@blueprintjs/core';
import { FormattedMessage as T } from 'react-intl';
import classNames from 'classnames';
import ItemsEntriesDeleteAlert from 'containers/Alerts/ItemsEntries/ItemsEntriesDeleteAlert';
import withAlertActions from 'containers/Alert/withAlertActions';
import { CLASSES } from 'common/classes';
import { Hint, Icon, DataTableEditable } from 'components';
import { DataTableEditable } from 'components';
import { useEditableItemsEntriesColumns } from './components';
import {
InputGroupCell,
MoneyFieldCell,
ItemsListCell,
PercentFieldCell,
DivFieldCell,
} from 'components/DataTableCells';
import { formattedAmount, saveInvoke } from 'utils';
saveInvoke,
updateTableRow,
repeatValue,
removeRowsByIndex,
compose,
} from 'utils';
import { updateItemsEntriesTotal } from './utils';
// Actions cell renderer component.
const ActionsCellRenderer = ({
row: { index },
column: { id },
cell: { value },
data,
payload,
}) => {
if (data.length <= index + 1) {
return '';
}
const onRemoveRole = () => {
payload.removeRow(index);
};
return (
<Tooltip content={<T id={'remove_the_line'} />} position={Position.LEFT}>
<Button
icon={<Icon icon={'times-circle'} iconSize={14} />}
iconSize={14}
className="m12"
intent={Intent.DANGER}
onClick={onRemoveRole}
/>
</Tooltip>
);
};
/**
* Items entries table.
*/
function ItemsEntriesTable({
// #withAlertActions
openAlert,
// Total cell renderer.
const TotalCellRenderer = (content, type) => (props) => {
if (props.data.length === props.row.index + 1) {
const total = props.data.reduce((total, entry) => {
const amount = parseInt(entry[type], 10);
const computed = amount ? total + amount : total;
return computed;
}, 0);
return <span>{formattedAmount(total, 'USD')}</span>;
}
return content(props);
};
const calculateDiscount = (discount, quantity, rate) =>
quantity * rate - (quantity * rate * discount) / 100;
const CellRenderer = (content, type) => (props) => {
if (props.data.length === props.row.index + 1) {
return '';
}
return content(props);
};
const ItemHeaderCell = () => (
<>
<T id={'product_and_service'} />
<Hint />
</>
);
export default function ItemsEntriesTable({
//#ownProps
// #ownProps
items,
entries,
initialEntries,
defaultEntry,
errors,
onUpdateData,
onClickRemoveRow,
onClickAddNewRow,
onClickClearAllLines,
filterPurchasableItems = false,
filterSellableItems = false,
linesNumber,
}) {
const [rows, setRows] = useState([]);
const { formatMessage } = useIntl();
const [rows, setRows] = React.useState(initialEntries);
useEffect(() => {
setRows([...entries.map((e) => ({ ...e }))]);
}, [entries]);
// Allows to observes `entries` to make table rows outside controlled.
React.useEffect(() => {
if (entries && entries !== rows) {
setRows(entries);
}
}, [entries, rows]);
const columns = useMemo(
() => [
{
Header: '#',
accessor: 'index',
Cell: ({ row: { index } }) => <span>{index + 1}</span>,
width: 40,
disableResizing: true,
disableSortBy: true,
className: 'index',
},
{
Header: ItemHeaderCell,
id: 'item_id',
accessor: 'item_id',
Cell: ItemsListCell,
disableSortBy: true,
width: 180,
filterPurchasable: filterPurchasableItems,
filterSellable: filterSellableItems,
},
{
Header: formatMessage({ id: 'description' }),
accessor: 'description',
Cell: InputGroupCell,
disableSortBy: true,
className: 'description',
width: 100,
},
{
Header: formatMessage({ id: 'quantity' }),
accessor: 'quantity',
Cell: CellRenderer(InputGroupCell, 'quantity'),
disableSortBy: true,
width: 80,
className: 'quantity',
},
{
Header: formatMessage({ id: 'rate' }),
accessor: 'rate',
Cell: TotalCellRenderer(MoneyFieldCell, 'rate'),
disableSortBy: true,
width: 80,
className: 'rate',
},
{
Header: formatMessage({ id: 'discount' }),
accessor: 'discount',
Cell: CellRenderer(PercentFieldCell, InputGroupCell),
disableSortBy: true,
width: 80,
className: 'discount',
},
{
Header: formatMessage({ id: 'total' }),
accessor: (row) =>
calculateDiscount(row.discount, row.quantity, row.rate),
Cell: TotalCellRenderer(DivFieldCell, 'total'),
disableSortBy: true,
width: 120,
className: 'total',
},
{
Header: '',
accessor: 'action',
Cell: ActionsCellRenderer,
className: 'actions',
disableSortBy: true,
disableResizing: true,
width: 45,
},
],
[formatMessage],
);
// Editiable items entries columns.
const columns = useEditableItemsEntriesColumns();
// Handles the editor data update.
const handleUpdateData = useCallback(
(rowIndex, columnId, value) => {
const newRows = rows.map((row, index) => {
if (index === rowIndex) {
const newRow = { ...rows[rowIndex], [columnId]: value };
return {
...newRow,
total: calculateDiscount(
newRow.discount,
newRow.quantity,
newRow.rate,
),
};
}
return row;
});
saveInvoke(onUpdateData, newRows);
const newRows = compose(
updateTableRow(rowIndex, columnId, value),
updateItemsEntriesTotal,
)(entries);
setRows(newRows);
onUpdateData(newRows);
},
[rows, onUpdateData],
[entries, onUpdateData],
);
const handleRemoveRow = useCallback(
(rowIndex) => {
if (rows.length <= 1) {
return;
}
const removeIndex = parseInt(rowIndex, 10);
saveInvoke(onClickRemoveRow, removeIndex);
},
[rows, onClickRemoveRow],
);
// Handle table rows removing by index.
const handleRemoveRow = (rowIndex) => {
const newRows = removeRowsByIndex(rows, rowIndex);
setRows(newRows);
saveInvoke(onUpdateData, newRows);
};
// Handle table rows adding a new row.
const onClickNewRow = (event) => {
saveInvoke(onClickAddNewRow, event);
const newRows = [...rows, defaultEntry];
setRows(newRows);
saveInvoke(onUpdateData, newRows);
};
// Handle table clearing all rows.
const handleClickClearAllLines = (event) => {
saveInvoke(onClickClearAllLines, event);
openAlert('items-entries-clear-lines');
};
const rowClassNames = useCallback(
(row) => ({
'row--total': rows.length === row.index + 1,
}),
[rows],
);
/**
* Handle alert confirm of clear all lines.
*/
const handleClearLinesAlertConfirm = () => {
const newRows = repeatValue(defaultEntry, linesNumber);
setRows(newRows);
saveInvoke(onUpdateData, newRows);
};
return (
<DataTableEditable
className={classNames(CLASSES.DATATABLE_EDITOR_ITEMS_ENTRIES)}
columns={columns}
data={rows}
rowClassNames={rowClassNames}
sticky={true}
payload={{
items,
errors: errors || [],
updateData: handleUpdateData,
removeRow: handleRemoveRow,
autoFocus: ['item_id', 0],
}}
actions={
<>
<Button
small={true}
className={'button--secondary button--new-line'}
onClick={onClickNewRow}
>
<T id={'new_lines'} />
</Button>
<>
<DataTableEditable
className={classNames(CLASSES.DATATABLE_EDITOR_ITEMS_ENTRIES)}
columns={columns}
data={entries}
sticky={true}
payload={{
items,
errors: errors || [],
updateData: handleUpdateData,
removeRow: handleRemoveRow,
autoFocus: ['item_id', 0],
}}
actions={
<>
<Button
small={true}
className={'button--secondary button--new-line'}
onClick={onClickNewRow}
>
<T id={'new_lines'} />
</Button>
<Button
small={true}
className={'button--secondary button--clear-lines ml1'}
onClick={handleClickClearAllLines}
>
<T id={'clear_all_lines'} />
</Button>
</>
}
/>
<Button
small={true}
className={'button--secondary button--clear-lines ml1'}
onClick={handleClickClearAllLines}
>
<T id={'clear_all_lines'} />
</Button>
</>
}
/>
<ItemsEntriesDeleteAlert
name={'items-entries-clear-lines'}
onConfirm={handleClearLinesAlertConfirm}
/>
</>
);
}
ItemsEntriesTable.defaultProps = {
defaultEntry: {
index: 0,
item_id: '',
description: '',
quantity: 1,
rate: '',
discount: '',
},
initialEntries: [],
linesNumber: 4,
};
export default compose(withAlertActions)(ItemsEntriesTable);

View File

@@ -0,0 +1,166 @@
import React from 'react';
import { FormattedMessage as T, useIntl } from 'react-intl';
import { Tooltip, Button, Intent, Position } from '@blueprintjs/core';
import { sumBy } from 'lodash';
import { Hint, Icon } from 'components';
import { formattedAmount } from 'utils';
import {
InputGroupCell,
MoneyFieldCell,
ItemsListCell,
PercentFieldCell,
} from 'components/DataTableCells';
/**
* Item header cell.
*/
export function ItemHeaderCell() {
return (
<>
<T id={'product_and_service'} />
<Hint />
</>
);
}
/**
* Item column footer cell.
*/
export function ItemFooterCell() {
return <span>Total</span>;
}
/**
* Actions cell renderer component.
*/
export function ActionsCellRenderer({
row: { index },
column: { id },
cell: { value },
data,
payload: { removeRow },
}) {
const onRemoveRole = () => {
removeRow(index);
};
return (
<Tooltip content={<T id={'remove_the_line'} />} position={Position.LEFT}>
<Button
icon={<Icon icon={'times-circle'} iconSize={14} />}
iconSize={14}
className="m12"
intent={Intent.DANGER}
onClick={onRemoveRole}
/>
</Tooltip>
);
}
/**
* Quantity total footer cell.
*/
export function QuantityTotalFooterCell({ rows }) {
const quantity = sumBy(rows, r => parseInt(r.original.quantity, 10));
return <span>{ formattedAmount(quantity, 'USD') }</span>;
}
/**
* Total footer cell.
*/
export function TotalFooterCell({ rows }) {
const total = sumBy(rows, 'original.total');
return <span>{ formattedAmount(total, 'USD') }</span>;
}
/**
* Total accessor.
*/
export function TotalCell({ value }) {
return <span>{ formattedAmount(value, 'USD', { noZero: true }) }</span>;
}
/**
* Retrieve editable items entries columns.
*/
export function useEditableItemsEntriesColumns() {
const { formatMessage } = useIntl();
return React.useMemo(
() => [
{
Header: '#',
accessor: 'index',
Cell: ({ row: { index } }) => <span>{index + 1}</span>,
width: 40,
disableResizing: true,
disableSortBy: true,
className: 'index',
},
{
Header: ItemHeaderCell,
id: 'item_id',
accessor: 'item_id',
Cell: ItemsListCell,
Footer: ItemFooterCell,
disableSortBy: true,
width: 180,
// filterPurchasable: filterPurchasableItems,
// filterSellable: filterSellableItems,
},
{
Header: formatMessage({ id: 'description' }),
accessor: 'description',
Cell: InputGroupCell,
disableSortBy: true,
className: 'description',
width: 100,
},
{
Header: formatMessage({ id: 'quantity' }),
accessor: 'quantity',
Cell: InputGroupCell,
Footer: QuantityTotalFooterCell,
disableSortBy: true,
width: 80,
className: 'quantity',
},
{
Header: formatMessage({ id: 'rate' }),
accessor: 'rate',
Cell: MoneyFieldCell,
disableSortBy: true,
width: 80,
className: 'rate',
},
{
Header: formatMessage({ id: 'discount' }),
accessor: 'discount',
Cell: PercentFieldCell,
disableSortBy: true,
width: 80,
className: 'discount',
},
{
Header: formatMessage({ id: 'total' }),
Footer: TotalFooterCell,
accessor: 'total',
Cell: TotalCell,
disableSortBy: true,
width: 120,
className: 'total',
},
{
Header: '',
accessor: 'action',
Cell: ActionsCellRenderer,
className: 'actions',
disableSortBy: true,
disableResizing: true,
width: 45,
},
],
[formatMessage],
);
}

View File

@@ -0,0 +1,26 @@
import { toSafeNumber } from 'utils';
/**
* Retrieve item entry total from the given rate, quantity and discount.
* @param {number} rate
* @param {number} quantity
* @param {number} discount
* @return {number}
*/
export const calcItemEntryTotal = (discount, quantity, rate) => {
const _quantity = toSafeNumber(quantity);
const _rate = toSafeNumber(rate);
const _discount = toSafeNumber(discount);
return _quantity * _rate - (_quantity * _rate * _discount) / 100;
};
/**
* Updates the items entries total.
*/
export function updateItemsEntriesTotal(rows) {
return rows.map((row) => ({
...row,
total: calcItemEntryTotal(row.discount, row.quantity, row.rate)
}));
};

View File

@@ -11,63 +11,67 @@ import {
} from '@blueprintjs/core';
import { useFormikContext } from 'formik';
import { FormattedMessage as T } from 'react-intl';
import { useHistory } from 'react-router-dom';
import { CLASSES } from 'common/classes';
import classNames from 'classnames';
import { saveInvoke } from 'utils';
import { Icon, If } from 'components';
import { useExpenseFormContext } from './ExpenseFormPageProvider';
/**
* Expense form floating actions.
*/
export default function ExpenseFloatingFooter({
isSubmitting,
onSubmitClick,
onCancelClick,
expense,
expensePublished,
}) {
const { submitForm, resetForm } = useFormikContext();
export default function ExpenseFloatingFooter() {
const history = useHistory();
// Formik context.
const { isSubmitting, submitForm, resetForm } = useFormikContext();
// Expense form context.
const { setSubmitPayload, isNewMode } = useExpenseFormContext();
// Handle submit & publish button click.
const handleSubmitPublishBtnClick = (event) => {
saveInvoke(onSubmitClick, event, { redirect: true, publish: true});
setSubmitPayload({ redirect: true, publish: true});
submitForm();
};
// Handle submit, publish & new button click.
const handleSubmitPublishAndNewBtnClick = (event) => {
setSubmitPayload({ redirect: false, publish: true, resetForm: true });
submitForm();
saveInvoke(onSubmitClick, event, {
redirect: false,
publish: true,
resetForm: true,
});
};
// Handle submit, publish & continue editing button click.
const handleSubmitPublishContinueEditingBtnClick = (event) => {
setSubmitPayload({ redirect: false, publish: true });
submitForm();
saveInvoke(onSubmitClick, event, { redirect: false, publish: true });
};
// Handle submit as draft button click.
const handleSubmitDraftBtnClick = (event) => {
saveInvoke(onSubmitClick, event, { redirect: true, publish: false });
setSubmitPayload({ redirect: true, publish: false });
submitForm();
};
// Handle submit as draft & new button click.
const handleSubmitDraftAndNewBtnClick = (event) => {
setSubmitPayload({ redirect: false, publish: false, resetForm: true });
submitForm();
saveInvoke(onSubmitClick, event, {
redirect: false,
publish: false,
resetForm: true,
});
};
// Handles submit as draft & continue editing button click.
const handleSubmitDraftContinueEditingBtnClick = (event) => {
setSubmitPayload({ redirect: false, publish: false });
submitForm();
saveInvoke(onSubmitClick, event, { redirect: false, publish: false });
};
// Handle cancel button click.
const handleCancelBtnClick = (event) => {
saveInvoke(onCancelClick, event);
history.goBack();
};
// Handles clear form button click.
const handleClearBtnClick = (event) => {
resetForm();
};
@@ -75,10 +79,11 @@ export default function ExpenseFloatingFooter({
return (
<div className={classNames(CLASSES.PAGE_FORM_FLOATING_ACTIONS)}>
{/* ----------- Save And Publish ----------- */}
<If condition={!expense || !expensePublished}>
<If condition={isNewMode}>
<ButtonGroup>
<Button
disabled={isSubmitting}
loading={isSubmitting}
intent={Intent.PRIMARY}
onClick={handleSubmitPublishBtnClick}
text={<T id={'save_publish'} />}
@@ -140,10 +145,11 @@ export default function ExpenseFloatingFooter({
</ButtonGroup>
</If>
{/* ----------- Save and New ----------- */}
<If condition={expense && expensePublished}>
<If condition={!isNewMode}>
<ButtonGroup>
<Button
disabled={isSubmitting}
loading={isSubmitting}
intent={Intent.PRIMARY}
onClick={handleSubmitPublishBtnClick}
text={<T id={'save'} />}
@@ -174,7 +180,7 @@ export default function ExpenseFloatingFooter({
className={'ml1'}
disabled={isSubmitting}
onClick={handleClearBtnClick}
text={expense ? <T id={'reset'} /> : <T id={'clear'} />}
text={!isNewMode ? <T id={'reset'} /> : <T id={'clear'} />}
/>
{/* ----------- Cancel ----------- */}
<Button

View File

@@ -1,9 +1,8 @@
import React, { useMemo, useEffect, useState, useCallback } from 'react';
import React, { useMemo } from 'react';
import { Intent } from '@blueprintjs/core';
import { useIntl } from 'react-intl';
import { defaultTo, pick, sumBy } from 'lodash';
import { defaultTo, sumBy, isEmpty } from 'lodash';
import { Formik, Form } from 'formik';
import moment from 'moment';
import classNames from 'classnames';
import { useHistory } from 'react-router-dom';
import { CLASSES } from 'common/classes';
@@ -24,80 +23,51 @@ import {
CreateExpenseFormSchema,
EditExpenseFormSchema,
} from './ExpenseForm.schema';
import { transformErrors } from './utils';
import { compose, repeatValue, orderingLinesIndexes } from 'utils';
const MIN_LINES_NUMBER = 4;
const defaultCategory = {
index: 0,
amount: '',
expense_account_id: '',
description: '',
};
const defaultInitialValues = {
payment_account_id: '',
beneficiary: '',
payment_date: moment(new Date()).format('YYYY-MM-DD'),
description: '',
reference_no: '',
currency_code: '',
publish: '',
categories: [...repeatValue(defaultCategory, MIN_LINES_NUMBER)],
};
import { transformErrors, defaultExpense, transformToEditForm } from './utils';
import { compose, orderingLinesIndexes } from 'utils';
/**
* Expense form.
*/
function ExpenseForm({
// #withDashboard
changePageTitle,
// #withSettings
baseCurrency,
preferredPaymentAccount,
}) {
// Expense form context.
const {
editExpenseMutate,
createExpenseMutate,
expense,
expenseId,
submitPayload,
} = useExpenseFormContext();
const isNewMode = !expenseId;
const [submitPayload, setSubmitPayload] = useState({});
const { formatMessage } = useIntl();
// History context.
const history = useHistory();
useEffect(() => {
if (isNewMode) {
changePageTitle(formatMessage({ id: 'new_expense' }));
} else {
changePageTitle(formatMessage({ id: 'edit_expense' }));
}
}, [changePageTitle, isNewMode, formatMessage]);
// Form initial values.
const initialValues = useMemo(
() => ({
...(expense
...(!isEmpty(expense)
? {
...pick(expense, Object.keys(defaultInitialValues)),
categories: [
...expense.categories.map((category) => ({
...pick(category, Object.keys(defaultCategory)),
})),
],
...transformToEditForm(expense, defaultExpense),
}
: {
...defaultInitialValues,
...defaultExpense,
currency_code: baseCurrency,
payment_account_id: defaultTo(preferredPaymentAccount, ''),
categories: orderingLinesIndexes(defaultInitialValues.categories),
categories: orderingLinesIndexes(defaultExpense.categories),
}),
}),
[expense, baseCurrency, preferredPaymentAccount],
[
expense,
baseCurrency,
preferredPaymentAccount,
],
);
// Handle form submit.
@@ -155,21 +125,11 @@ function ExpenseForm({
if (isNewMode) {
createExpenseMutate(form).then(handleSuccess).catch(handleError);
} else {
editExpenseMutate(expense.id, form)
editExpenseMutate([expense.id, form])
.then(handleSuccess)
.catch(handleError);
}
};
const handleCancelClick = useCallback(() => {
history.goBack();
}, [history]);
const handleSubmitClick = useCallback(
(event, payload) => {
setSubmitPayload({ ...payload });
},
[setSubmitPayload],
);
return (
<div
@@ -180,32 +140,24 @@ function ExpenseForm({
)}
>
<Formik
validationSchema={isNewMode
? CreateExpenseFormSchema
: EditExpenseFormSchema}
validationSchema={
isNewMode ? CreateExpenseFormSchema : EditExpenseFormSchema
}
initialValues={initialValues}
onSubmit={handleSubmit}
>
{({ isSubmitting, values }) => (
<Form>
<ExpenseFormHeader />
<ExpenseFormBody />
<ExpenseFormFooter />
<ExpenseFloatingFooter
isSubmitting={isSubmitting}
expense={expenseId}
expensePublished={values.publish}
onCancelClick={handleCancelClick}
onSubmitClick={handleSubmitClick}
/>
</Form>
)}
<Form>
<ExpenseFormHeader />
<ExpenseFormBody />
<ExpenseFormFooter />
<ExpenseFloatingFooter />
</Form>
</Formik>
</div>
);
}
export default compose(
export default compose(
withDashboardActions,
withMediaActions,
withSettings(({ organizationSettings, expenseSettings }) => ({

View File

@@ -3,7 +3,9 @@ import classNames from 'classnames';
import { CLASSES } from 'common/classes';
import ExpenseFormEntriesField from './ExpenseFormEntriesField';
export default function ExpenseFormBody() {
export default function ExpenseFormBody({
}) {
return (
<div className={classNames(CLASSES.PAGE_FORM_BODY)}>
<ExpenseFormEntriesField />

View File

@@ -1,143 +0,0 @@
import React, { useState, useMemo, useEffect, useCallback } from 'react';
import { Button, Intent, Position, Tooltip } from '@blueprintjs/core';
import { FormattedMessage as T, useIntl } from 'react-intl';
import { omit } from 'lodash';
import { DataTableEditable, Icon } from 'components';
import { Hint } from 'components';
import {
formattedAmount,
transformUpdatedRows,
saveInvoke,
} from 'utils';
import {
AccountsListFieldCell,
MoneyFieldCell,
InputGroupCell,
} from 'components/DataTableCells';
import { useExpenseFormContext } from './ExpenseFormPageProvider';
import { useExpenseFormTableColumns } from './components';
export default function ExpenseTable({
// #ownPorps
onClickRemoveRow,
onClickAddNewRow,
onClickClearAllLines,
entries,
error,
onChange,
}) {
const [rows, setRows] = useState([]);
const { formatMessage } = useIntl();
const { accounts } = useExpenseFormContext();
useEffect(() => {
setRows([...entries.map((e) => ({ ...e, rowType: 'editor' }))]);
}, [entries]);
// Final table rows editor rows and total and final blank row.
const tableRows = useMemo(() => [...rows, { rowType: 'total' }], [rows]);
// Memorized data table columns.
const columns = useExpenseFormTableColumns();
// Handles update datatable data.
const handleUpdateData = useCallback(
(rowIndex, columnIdOrObj, value) => {
const newRows = transformUpdatedRows(
rows,
rowIndex,
columnIdOrObj,
value,
);
saveInvoke(
onChange,
newRows
.filter((row) => row.rowType === 'editor')
.map((row) => ({
...omit(row, ['rowType']),
})),
);
},
[rows, onChange],
);
// Handles click remove datatable row.
const handleRemoveRow = useCallback(
(rowIndex) => {
// Can't continue if there is just one row line or less.
if (rows.length <= 1) {
return;
}
const removeIndex = parseInt(rowIndex, 10);
const newRows = rows.filter((row, index) => index !== removeIndex);
saveInvoke(
onChange,
newRows
.filter((row) => row.rowType === 'editor')
.map((row, index) => ({
...omit(row, ['rowType']),
index: index + 1,
})),
);
saveInvoke(onClickRemoveRow, removeIndex);
},
[rows, onChange, onClickRemoveRow],
);
// Invoke when click on add new line button.
const onClickNewRow = () => {
saveInvoke(onClickAddNewRow);
};
// Invoke when click on clear all lines button.
const handleClickClearAllLines = () => {
saveInvoke(onClickClearAllLines);
};
// Rows classnames callback.
const rowClassNames = useCallback(
(row) => ({
'row--total': rows.length === row.index + 1,
}),
[rows],
);
return (
<DataTableEditable
columns={columns}
data={tableRows}
rowClassNames={rowClassNames}
sticky={true}
payload={{
accounts: accounts,
errors: error,
updateData: handleUpdateData,
removeRow: handleRemoveRow,
autoFocus: ['expense_account_id', 0],
}}
actions={
<>
<Button
small={true}
className={'button--secondary button--new-line'}
onClick={onClickNewRow}
>
<T id={'new_lines'} />
</Button>
<Button
small={true}
className={'button--secondary button--clear-lines ml1'}
onClick={handleClickClearAllLines}
>
<T id={'clear_all_lines'} />
</Button>
</>
}
totalRow={true}
/>
);
}

View File

@@ -1,30 +1,27 @@
import { FastField } from 'formik';
import React from 'react';
import ExpenseFormEntries from './ExpenseFormEntries';
import { orderingLinesIndexes, repeatValue } from 'utils';
import ExpenseFormEntriesTable from './ExpenseFormEntriesTable';
import { useExpenseFormContext } from './ExpenseFormPageProvider';
/**
* Expense form entries field.
*/
export default function ExpenseFormEntriesField({
defaultRow,
linesNumber = 4,
}) {
const { defaultCategoryEntry } = useExpenseFormContext();
return (
<FastField name={'categories'}>
{({ form, field: { value }, meta: { error, touched } }) => (
<ExpenseFormEntries
<ExpenseFormEntriesTable
entries={value}
error={error}
onChange={(entries) => {
form.setFieldValue('categories', entries);
}}
onClickAddNewRow={() => {
form.setFieldValue('categories', [...value, defaultRow]);
}}
onClickClearAllLines={() => {
form.setFieldValue(
'categories',
orderingLinesIndexes([...repeatValue(defaultRow, linesNumber)])
);
}}
defaultEntry={defaultCategoryEntry}
linesNumber={linesNumber}
/>
)}
</FastField>

View File

@@ -0,0 +1,121 @@
import React, { useCallback } from 'react';
import { Button } from '@blueprintjs/core';
import { FormattedMessage as T } from 'react-intl';
import { DataTableEditable } from 'components';
import ExpenseDeleteEntriesAlert from 'containers/Alerts/Expenses/ExpenseDeleteEntriesAlert';
import { useExpenseFormContext } from './ExpenseFormPageProvider';
import { useExpenseFormTableColumns } from './components';
import withAlertActions from 'containers/Alert/withAlertActions';
import { transformUpdatedRows, compose, saveInvoke, repeatValue } from 'utils';
/**
* Expenses form entries.
*/
function ExpenseFormEntriesTable({
// #withAlertActions
openAlert,
// #ownPorps
entries,
defaultEntry,
error,
onChange,
}) {
// Expense form context.
const { accounts } = useExpenseFormContext();
// Memorized data table columns.
const columns = useExpenseFormTableColumns();
// Handles update datatable data.
const handleUpdateData = useCallback(
(rowIndex, columnIdOrObj, value) => {
const newRows = transformUpdatedRows(
entries,
rowIndex,
columnIdOrObj,
value,
);
saveInvoke(onChange, newRows);
},
[entries, onChange],
);
// Handles click remove datatable row.
const handleRemoveRow = useCallback(
(rowIndex) => {
// Can't continue if there is just one row line or less.
if (entries.length <= 1) {
return;
}
const newRows = entries.filter((row, index) => index !== rowIndex);
saveInvoke(onChange, newRows);
},
[entries, onChange],
);
// Invoke when click on add new line button.
const onClickNewRow = () => {
const newRows = [...entries, defaultEntry];
saveInvoke(onChange, newRows);
};
// Invoke when click on clear all lines button.
const handleClickClearAllLines = () => {
openAlert('expense-delete-entries');
};
// handle confirm clear all entries alert.
const handleConfirmClearEntriesAlert = () => {
const newRows = repeatValue(defaultEntry, 3);
saveInvoke(onChange, newRows);
};
return (
<>
<DataTableEditable
columns={columns}
data={entries}
sticky={true}
payload={{
accounts: accounts,
errors: error,
updateData: handleUpdateData,
removeRow: handleRemoveRow,
autoFocus: ['expense_account_id', 0],
}}
actions={
<>
<Button
small={true}
className={'button--secondary button--new-line'}
onClick={onClickNewRow}
>
<T id={'new_lines'} />
</Button>
<Button
small={true}
className={'button--secondary button--clear-lines ml1'}
onClick={handleClickClearAllLines}
>
<T id={'clear_all_lines'} />
</Button>
</>
}
totalRow={true}
/>
<ExpenseDeleteEntriesAlert
name={'expense-delete-entries'}
onConfirm={handleConfirmClearEntriesAlert}
/>
</>
);
}
export default compose(
withAlertActions
)(ExpenseFormEntriesTable);

View File

@@ -7,7 +7,7 @@ import { inputIntent } from 'utils';
import { Row, Dragzone, Col } from 'components';
import { CLASSES } from 'common/classes';
export default function ExpenseFormFooter({}) {
export default function ExpenseFormFooter() {
return (
<div className={classNames(CLASSES.PAGE_FORM_FOOTER)}>
<Row>

View File

@@ -6,7 +6,6 @@ import { FormattedMessage as T } from 'react-intl';
import { CLASSES } from 'common/classes';
import {
momentFormatter,
compose,
tansformDateValue,
inputIntent,
handleDateChange,
@@ -27,7 +26,7 @@ import { useExpenseFormContext } from './ExpenseFormPageProvider';
/**
* Expense form header.
*/
export default function ExpenseFormHeader({}) {
export default function ExpenseFormHeader() {
const { currencies, accounts, customers } = useExpenseFormContext();
return (

View File

@@ -1,48 +1,21 @@
import React, { useCallback, useEffect } from 'react';
import { useParams, useHistory } from 'react-router-dom';
import React from 'react';
import { useParams } from 'react-router-dom';
import ExpenseForm from './ExpenseForm';
import { ExpenseFormPageProvider } from './ExpenseFormPageProvider';
import withDashboardActions from 'containers/Dashboard/withDashboardActions';
import { compose } from 'utils';
import 'style/pages/Expense/PageForm.scss';
/**
* Expense page form.
*/
function ExpenseFormPage({
// #withDashboardActions
setSidebarShrink,
resetSidebarPreviousExpand,
setDashboardBackLink,
}) {
const history = useHistory();
export default function ExpenseFormPage() {
const { id } = useParams();
useEffect(() => {
// Shrink the sidebar by foce.
setSidebarShrink();
// Show the back link on dashboard topbar.
setDashboardBackLink(true);
return () => {
// Reset the sidebar to the previous status.
resetSidebarPreviousExpand();
// Hide the back link on dashboard topbar.
setDashboardBackLink(false);
};
}, [resetSidebarPreviousExpand, setSidebarShrink, setDashboardBackLink]);
return (
<ExpenseFormPageProvider expenseId={id}>
<ExpenseForm />
</ExpenseFormPageProvider>
);
}
export default compose(
withDashboardActions,
)(ExpenseFormPage);

View File

@@ -24,7 +24,12 @@ function ExpenseFormPageProvider({ expenseId, ...props }) {
} = useCustomers();
// Fetch the expense details.
const { data: expense, isFetching: isExpenseLoading } = useExpense(expenseId);
const { data: expense, isFetching: isExpenseLoading } = useExpense(
expenseId,
{
enabled: !!expenseId,
},
);
// Fetch accounts list.
const { data: accounts, isFetching: isAccountsLoading } = useAccounts();
@@ -33,9 +38,17 @@ function ExpenseFormPageProvider({ expenseId, ...props }) {
const { mutateAsync: createExpenseMutate } = useCreateExpense();
const { mutateAsync: editExpenseMutate } = useEditExpense();
// Submit form payload.
const [submitPayload, setSubmitPayload] = React.useState({});
//
const isNewMode = !expenseId;
// Provider payload.
const provider = {
isNewMode,
expenseId,
submitPayload,
currencies,
customers,
@@ -49,6 +62,7 @@ function ExpenseFormPageProvider({ expenseId, ...props }) {
createExpenseMutate,
editExpenseMutate,
setSubmitPayload,
};
return (

View File

@@ -1,6 +1,17 @@
import React from 'react';
import { Button, Tooltip, Intent, Position } from '@blueprintjs/core';
import { FormattedMessage as T, useIntl } from 'react-intl';
import { Icon, Hint } from 'components';
import {
InputGroupCell,
MoneyFieldCell,
AccountsListFieldCell,
} from 'components/DataTableCells';
import { formattedAmount, safeSumBy } from 'utils';
/**
* Expense category header cell.
*/
const ExpenseCategoryHeaderCell = () => {
return (
<>
@@ -10,7 +21,9 @@ const ExpenseCategoryHeaderCell = () => {
);
};
// Actions cell renderer.
/**
* Actions cell renderer.
*/
const ActionsCellRenderer = ({
row: { index },
column: { id },
@@ -18,9 +31,6 @@ const ActionsCellRenderer = ({
data,
payload,
}) => {
if (data.length <= index + 1) {
return '';
}
const onClickRemoveRole = () => {
payload.removeRow(index);
};
@@ -38,95 +48,76 @@ const ActionsCellRenderer = ({
);
};
// Total text cell renderer.
const TotalExpenseCellRenderer = (chainedComponent) => (props) => {
if (props.data.length <= props.row.index + 1) {
return (
<span>
<T id={'total_currency'} values={{ currency: 'USD' }} />
</span>
);
}
return chainedComponent(props);
};
/**
* Amount footer cell.
*/
function AmountFooterCell({ rows }) {
const total = safeSumBy(rows, 'original.amount');
return <span>{formattedAmount(total, 'USD')}</span>;
}
/**
* Note cell renderer.
* Expense account footer cell.
*/
const NoteCellRenderer = (chainedComponent) => (props) => {
if (props.data.length === props.row.index + 1) {
return '';
}
return chainedComponent(props);
};
function ExpenseAccountFooterCell() {
return 'Total';
}
/**
* Total amount cell renderer.
* Retrieve expense form table entries columns.
*/
const TotalAmountCellRenderer = (chainedComponent, type) => (props) => {
if (props.data.length === props.row.index + 1) {
const total = props.data.reduce((total, entry) => {
const amount = parseInt(entry[type], 10);
const computed = amount ? total + amount : total;
return computed;
}, 0);
return <span>{formattedAmount(total, 'USD')}</span>;
}
return chainedComponent(props);
};
export function useExpenseFormTableColumns() {
const { formatMessage } = useIntl();
export function useExpenseFormTableColumns() {
return React.useMemo(
() => [
{
Header: '#',
accessor: 'index',
Cell: ({ row: { index } }) => <span>{index + 1}</span>,
className: 'index',
width: 40,
disableResizing: true,
disableSortBy: true,
},
{
Header: ExpenseCategoryHeaderCell,
id: 'expense_account_id',
accessor: 'expense_account_id',
Cell: TotalExpenseCellRenderer(AccountsListFieldCell),
className: 'expense_account_id',
disableSortBy: true,
width: 40,
filterAccountsByRootType: ['expense'],
},
{
Header: formatMessage({ id: 'amount_currency' }, { currency: 'USD' }),
accessor: 'amount',
Cell: TotalAmountCellRenderer(MoneyFieldCell, 'amount'),
disableSortBy: true,
width: 40,
className: 'amount',
},
{
Header: formatMessage({ id: 'description' }),
accessor: 'description',
Cell: NoteCellRenderer(InputGroupCell),
disableSortBy: true,
className: 'description',
width: 100,
},
{
Header: '',
accessor: 'action',
Cell: ActionsCellRenderer,
className: 'actions',
disableSortBy: true,
disableResizing: true,
width: 45,
},
],
[formatMessage],
)
}
return React.useMemo(
() => [
{
Header: '#',
accessor: 'index',
Cell: ({ row: { index } }) => <span>{index + 1}</span>,
className: 'index',
width: 40,
disableResizing: true,
disableSortBy: true,
},
{
Header: ExpenseCategoryHeaderCell,
id: 'expense_account_id',
accessor: 'expense_account_id',
Cell: AccountsListFieldCell,
Footer: ExpenseAccountFooterCell,
className: 'expense_account_id',
disableSortBy: true,
width: 40,
filterAccountsByRootType: ['expense'],
},
{
Header: formatMessage({ id: 'amount_currency' }, { currency: 'USD' }),
accessor: 'amount',
Cell: MoneyFieldCell,
Footer: AmountFooterCell,
disableSortBy: true,
width: 40,
className: 'amount',
},
{
Header: formatMessage({ id: 'description' }),
accessor: 'description',
Cell: InputGroupCell,
disableSortBy: true,
className: 'description',
width: 100,
},
{
Header: '',
accessor: 'action',
Cell: ActionsCellRenderer,
className: 'actions',
disableSortBy: true,
disableResizing: true,
width: 45,
},
],
[formatMessage],
);
}

View File

@@ -1,5 +1,7 @@
import { AppToaster } from 'components';
import moment from 'moment';
import { formatMessage } from 'services/intl';
import { transformToForm, repeatValue } from 'utils';
const ERROR = {
EXPENSE_ALREADY_PUBLISHED: 'EXPENSE.ALREADY.PUBLISHED',
@@ -18,4 +20,46 @@ export const transformErrors = (errors, { setErrors }) => {
}),
);
}
};
};
export const MIN_LINES_NUMBER = 4;
export const defaultExpenseEntry = {
index: 0,
amount: '',
expense_account_id: '',
description: '',
};
export const defaultExpense = {
payment_account_id: '',
beneficiary: '',
payment_date: moment(new Date()).format('YYYY-MM-DD'),
description: '',
reference_no: '',
currency_code: '',
publish: '',
categories: [...repeatValue(defaultExpenseEntry, MIN_LINES_NUMBER)],
};
/**
* Transformes the expense to form initial values in edit mode.
*/
export const transformToEditForm = (
expense,
defaultExpense,
linesNumber = 4,
) => {
return {
...transformToForm(expense, defaultExpense),
categories: [
...expense.categories.map((category) => ({
...transformToForm(category, defaultExpense.categories[0]),
})),
...repeatValue(
expense,
Math.max(linesNumber - expense.categories.length, 0),
),
],
};
};

View File

@@ -1,8 +1,8 @@
import React, { useCallback } from 'react';
import classNames from 'classnames';
import { useHistory } from 'react-router-dom';
import { compose } from 'utils';
import { useExpensesListContext } from './ExpensesListProvider';
import { Choose } from 'components';
@@ -39,6 +39,8 @@ function ExpensesDataTable({
isEmptyStatus
} = useExpensesListContext();
const history = useHistory();
// Expenses table columns.
const columns = useExpensesTableColumns();
@@ -59,7 +61,9 @@ function ExpensesDataTable({
openAlert('expense-publish', { expenseId: expense.id });
};
const handleEditExpense = (expense) => {
// Handle the expense edit action.
const handleEditExpense = ({ id }) => {
history.push(`/expenses/${id}/edit`);
};
// Handle the expense delete action.
@@ -67,49 +71,45 @@ function ExpensesDataTable({
openAlert('expense-delete', { expenseId: expense.id });
};
// Display empty status instead of the table.
if (isEmptyStatus) {
return <ExpensesEmptyStatus />;
}
return (
<div className={classNames(CLASSES.DASHBOARD_DATATABLE)}>
<Choose>
<Choose.When condition={isEmptyStatus}>
<ExpensesEmptyStatus />
</Choose.When>
<DataTable
columns={columns}
data={expenses}
loading={isExpensesLoading}
headerLoading={isExpensesLoading}
progressBarLoading={isExpensesFetching}
<Choose.Otherwise>
<DataTable
columns={columns}
data={expenses}
selectionColumn={true}
noInitialFetch={true}
sticky={true}
loading={isExpensesLoading}
headerLoading={isExpensesLoading}
progressBarLoading={isExpensesFetching}
onFetchData={handleFetchData}
pagination={true}
manualSortBy={true}
manualPagination={true}
pagesCount={pagination.pagesCount}
selectionColumn={true}
noInitialFetch={true}
sticky={true}
autoResetSortBy={false}
autoResetPage={false}
onFetchData={handleFetchData}
pagination={true}
manualSortBy={true}
manualPagination={true}
pagesCount={pagination.pagesCount}
TableLoadingRenderer={TableSkeletonRows}
TableHeaderSkeletonRenderer={TableSkeletonHeader}
autoResetSortBy={false}
autoResetPage={false}
ContextMenu={ActionsMenu}
TableLoadingRenderer={TableSkeletonRows}
TableHeaderSkeletonRenderer={TableSkeletonHeader}
ContextMenu={ActionsMenu}
payload={{
onPublish: handlePublishExpense,
onDelete: handleDeleteExpense
}}
/>
</Choose.Otherwise>
</Choose>
</div>
payload={{
onPublish: handlePublishExpense,
onDelete: handleDeleteExpense,
onEdit: handleEditExpense
}}
/>
);
}

View File

@@ -1,16 +1,14 @@
import React, { useEffect } from 'react';
import { useIntl } from 'react-intl';
import React from 'react';
import 'style/pages/Expense/List.scss';
import DashboardPageContent from 'components/Dashboard/DashboardPageContent';
import { DashboardContentTable, DashboardPageContent } from 'components';
import ExpenseActionsBar from './ExpenseActionsBar';
import ExpenseViewTabs from './ExpenseViewTabs';
import ExpenseDataTable from './ExpenseDataTable';
import ExpensesAlerts from '../ExpensesAlerts';
import withDashboardActions from 'containers/Dashboard/withDashboardActions';
import withExpenses from './withExpenses';
import { compose, transformTableStateToQuery } from 'utils';
@@ -20,19 +18,9 @@ import { ExpensesListProvider } from './ExpensesListProvider';
* Expenses list.
*/
function ExpensesList({
// #withDashboardActions
changePageTitle,
// #withExpenses
expensesTableState,
}) {
const { formatMessage } = useIntl();
// Changes the page title once the page mount.
useEffect(() => {
changePageTitle(formatMessage({ id: 'expenses_list' }));
}, [changePageTitle, formatMessage]);
return (
<ExpensesListProvider
query={transformTableStateToQuery(expensesTableState)}
@@ -41,7 +29,10 @@ function ExpensesList({
<DashboardPageContent>
<ExpenseViewTabs />
<ExpenseDataTable />
<DashboardContentTable>
<ExpenseDataTable />
</DashboardContentTable>
</DashboardPageContent>
<ExpensesAlerts />
@@ -50,6 +41,5 @@ function ExpensesList({
}
export default compose(
withDashboardActions,
withExpenses(({ expensesTableState }) => ({ expensesTableState })),
)(ExpensesList);

View File

@@ -107,6 +107,24 @@ export function PublishAccessor(row) {
);
}
/**
* Expense account accessor.
*/
export function ExpenseAccountAccessor(expense) {
if (expense.categories.length === 1) {
return expense.categories[0].expense_account.name;
} else if (expense.categories.length > 1) {
const mutliCategories = expense.categories.map((category) => (
<div>
- {category.expense_account.name} ${category.amount}
</div>
));
return (
<Tooltip content={mutliCategories}>{'- Multi Categories -'}</Tooltip>
);
}
}
/**
* Retrieve the expenses table columns.
*/
@@ -168,18 +186,3 @@ export function useExpensesTableColumns() {
[],
);
}
export function ExpenseAccountAccessor(expense) {
if (expense.categories.length === 1) {
return expense.categories[0].expense_account.name;
} else if (expense.categories.length > 1) {
const mutliCategories = expense.categories.map((category) => (
<div>
- {category.expense_account.name} ${category.amount}
</div>
));
return (
<Tooltip content={mutliCategories}>{'- Multi Categories -'}</Tooltip>
);
}
}

View File

@@ -1,9 +1,5 @@
import React, { useEffect, useState } from 'react';
import { compose } from 'utils';
import React, { useState } from 'react';
import moment from 'moment';
import { useIntl } from 'react-intl';
import 'style/pages/FinancialStatements/BalanceSheet.scss';
@@ -14,25 +10,18 @@ import BalanceSheetActionsBar from './BalanceSheetActionsBar';
import { FinancialStatement } from 'components';
import withDashboardActions from 'containers/Dashboard/withDashboardActions';
import withSettings from 'containers/Settings/withSettings';
import { BalanceSheetProvider } from './BalanceSheetProvider';
import { compose } from 'utils';
/**
* Balance sheet.
*/
function BalanceSheet({
// #withDashboardActions
changePageTitle,
setDashboardBackLink,
setSidebarShrink,
// #withPreferences
organizationName,
}) {
const { formatMessage } = useIntl();
const [filter, setFilter] = useState({
fromDate: moment().startOf('year').format('YYYY-MM-DD'),
toDate: moment().endOf('year').format('YYYY-MM-DD'),
@@ -41,21 +30,6 @@ function BalanceSheet({
accountsFilter: 'all-accounts',
});
useEffect(() => {
setSidebarShrink();
changePageTitle(formatMessage({ id: 'balance_sheet' }));
}, [changePageTitle, formatMessage, setSidebarShrink]);
useEffect(() => {
// Show the back link on dashboard topbar.
setDashboardBackLink(true);
return () => {
// Hide the back link on dashboard topbar.
setDashboardBackLink(false);
};
}, [setDashboardBackLink]);
// Handle re-fetch balance sheet after filter change.
const handleFilterSubmit = (filter) => {
const _filter = {
@@ -95,7 +69,6 @@ function BalanceSheet({
}
export default compose(
withDashboardActions,
withSettings(({ organizationSettings }) => ({
organizationName: organizationSettings.name,
})),

View File

@@ -34,16 +34,7 @@ function FinancialReportsSection({ sectionTitle, reports }) {
);
}
function FinancialReports({
// #withDashboardActions
changePageTitle,
}) {
const { formatMessage } = useIntl();
useEffect(() => {
changePageTitle(formatMessage({ id: 'all_financial_reports' }));
}, [changePageTitle, formatMessage]);
export default function FinancialReports() {
return (
<DashboardInsider name={'financial-reports'}>
<div class="financial-reports">
@@ -53,4 +44,3 @@ function FinancialReports({
);
}
export default compose(withDashboardActions)(FinancialReports);

View File

@@ -1,15 +1,12 @@
import React, { useEffect } from 'react';
import { useIntl } from 'react-intl';
import DashboardPageContent from 'components/Dashboard/DashboardPageContent';
import React from 'react';
import { DashboardContentTable, DashboardPageContent } from 'components';
import InventoryAdjustmentsAlerts from './InventoryAdjustmentsAlerts';
import { InventoryAdjustmentsProvider } from './InventoryAdjustmentsProvider';
import InventoryAdjustmentTable from './InventoryAdjustmentTable';
import withInventoryAdjustments from './withInventoryAdjustments';
import withDashboardActions from 'containers/Dashboard/withDashboardActions';
import { compose, transformTableStateToQuery } from 'utils';
@@ -17,25 +14,18 @@ import { compose, transformTableStateToQuery } from 'utils';
* Inventory Adjustment List.
*/
function InventoryAdjustmentList({
// #withDashboardActions
changePageTitle,
// #withInventoryAdjustments
inventoryAdjustmentTableState,
}) {
const { formatMessage } = useIntl();
// Changes the dashboard title once the page mount.
useEffect(() => {
changePageTitle(formatMessage({ id: 'inventory_adjustment_list' }));
}, [changePageTitle, formatMessage]);
return (
<InventoryAdjustmentsProvider
query={transformTableStateToQuery(inventoryAdjustmentTableState)}
>
<DashboardPageContent>
<InventoryAdjustmentTable />
<DashboardContentTable>
<InventoryAdjustmentTable />
</DashboardContentTable>
<InventoryAdjustmentsAlerts />
</DashboardPageContent>
</InventoryAdjustmentsProvider>
@@ -43,7 +33,6 @@ function InventoryAdjustmentList({
}
export default compose(
withDashboardActions,
withInventoryAdjustments(({ inventoryAdjustmentTableState }) => ({
inventoryAdjustmentTableState,
})),

View File

@@ -1,9 +1,5 @@
import React, { useCallback } from 'react';
import classNames from 'classnames';
import { DataTable } from 'components';
import { CLASSES } from 'common/classes';
import { useInventoryAdjustmentsColumns, ActionsMenu } from './components';
import withAlertsActions from 'containers/Alert/withAlertActions';
@@ -57,38 +53,36 @@ function InventoryAdjustmentDataTable({
[setInventoryAdjustmentTableState],
);
return (
<div className={classNames(CLASSES.DASHBOARD_DATATABLE)}>
<DataTable
columns={columns}
data={inventoryAdjustments}
return (
<DataTable
columns={columns}
data={inventoryAdjustments}
loading={isAdjustmentsLoading}
headerLoading={isAdjustmentsLoading}
progressBarLoading={isAdjustmentsFetching}
initialState={inventoryAdjustmentTableState}
noInitialFetch={true}
loading={isAdjustmentsLoading}
headerLoading={isAdjustmentsLoading}
progressBarLoading={isAdjustmentsFetching}
initialState={inventoryAdjustmentTableState}
noInitialFetch={true}
onFetchData={handleDataTableFetchData}
manualSortBy={true}
selectionColumn={true}
onFetchData={handleDataTableFetchData}
manualSortBy={true}
selectionColumn={true}
pagination={true}
pagesCount={pagination.pagesCount}
pagination={true}
pagesCount={pagination.pagesCount}
autoResetSortBy={false}
autoResetPage={false}
autoResetSortBy={false}
autoResetPage={false}
payload={{
onDelete: handleDeleteAdjustment,
}}
ContextMenu={ActionsMenu}
noResults={'There is no inventory adjustments transactions yet.'}
{...tableProps}
/>
</div>
payload={{
onDelete: handleDeleteAdjustment,
}}
ContextMenu={ActionsMenu}
noResults={'There is no inventory adjustments transactions yet.'}
{...tableProps}
/>
);
}

View File

@@ -1,32 +1,15 @@
import React, { useEffect } from 'react';
import React from 'react';
import { useParams } from 'react-router-dom';
import { ItemFormProvider } from './ItemFormProvider';
import DashboardCard from 'components/Dashboard/DashboardCard';
import ItemForm from 'containers/Items/ItemForm';
import withDashboardActions from 'containers/Dashboard/withDashboardActions';
import { compose } from 'utils';
/**
* Item form page.
*/
function ItemFormPage({
// #withDashboardActions
setDashboardBackLink
}) {
export default function ItemFormPage() {
const { id } = useParams();
useEffect(() => {
// Show the back link on dashboard topbar.
setDashboardBackLink(true);
return () => {
// Hide the back link on dashboard topbar.
setDashboardBackLink(false);
};
}, [setDashboardBackLink]);
return (
<ItemFormProvider itemId={id}>
@@ -35,8 +18,4 @@ function ItemFormPage({
</DashboardCard>
</ItemFormProvider>
);
}
export default compose(
withDashboardActions,
)(ItemFormPage);
}

View File

@@ -1,5 +1,4 @@
import React from 'react';
import classNames from 'classnames';
import { useHistory } from 'react-router-dom';
import { DataTable } from 'components';
@@ -8,7 +7,6 @@ import ItemsEmptyStatus from './ItemsEmptyStatus';
import TableSkeletonRows from 'components/Datatable/TableSkeletonRows';
import TableSkeletonHeader from 'components/Datatable/TableHeaderSkeleton';
import { CLASSES } from 'common/classes';
import withItems from 'containers/Items/withItems';
import withItemsActions from 'containers/Items/withItemsActions';
@@ -95,53 +93,51 @@ function ItemsDataTable({
openDialog('inventory-adjustment', { itemId: id });
};
// Cannot continue in case the items has empty status.
// Display empty status instead of the table.
if (isEmptyStatus) {
return <ItemsEmptyStatus />;
}
return (
<div className={classNames(CLASSES.DASHBOARD_DATATABLE)}>
<DataTable
columns={columns}
data={items}
initialState={itemsTableState}
loading={isItemsLoading}
headerLoading={isItemsLoading}
progressBarLoading={isItemsFetching}
<DataTable
columns={columns}
data={items}
initialState={itemsTableState}
loading={isItemsLoading}
headerLoading={isItemsLoading}
progressBarLoading={isItemsFetching}
noInitialFetch={true}
selectionColumn={true}
spinnerProps={{ size: 30 }}
expandable={false}
sticky={true}
rowClassNames={rowClassNames}
noInitialFetch={true}
selectionColumn={true}
spinnerProps={{ size: 30 }}
expandable={false}
sticky={true}
rowClassNames={rowClassNames}
pagination={true}
manualSortBy={true}
manualPagination={true}
pagesCount={pagination.pagesCount}
pagination={true}
manualSortBy={true}
manualPagination={true}
pagesCount={pagination.pagesCount}
autoResetSortBy={false}
autoResetPage={true}
autoResetSortBy={false}
autoResetPage={true}
TableLoadingRenderer={TableSkeletonRows}
TableHeaderSkeletonRenderer={TableSkeletonHeader}
TableLoadingRenderer={TableSkeletonRows}
TableHeaderSkeletonRenderer={TableSkeletonHeader}
ContextMenu={ItemsActionMenuList}
onFetchData={handleFetchData}
payload={{
onDeleteItem: handleDeleteItem,
onEditItem: handleEditItem,
onInactivateItem: handleInactiveItem,
onActivateItem: handleActivateItem,
onMakeAdjustment: handleMakeAdjustment,
}}
noResults={'There is no items in the table yet.'}
{...tableProps}
/>
</div>
ContextMenu={ItemsActionMenuList}
onFetchData={handleFetchData}
payload={{
onDeleteItem: handleDeleteItem,
onEditItem: handleEditItem,
onInactivateItem: handleInactiveItem,
onActivateItem: handleActivateItem,
onMakeAdjustment: handleMakeAdjustment,
}}
noResults={'There is no items in the table yet.'}
{...tableProps}
/>
);
}

View File

@@ -3,7 +3,7 @@ import { compose } from 'utils';
import 'style/pages/Items/List.scss';
import DashboardPageContent from 'components/Dashboard/DashboardPageContent';
import { DashboardContentTable, DashboardPageContent } from 'components';
import ItemsActionsBar from './ItemsActionsBar';
import ItemsAlerts from './ItemsAlerts';
@@ -27,7 +27,10 @@ function ItemsList({
<DashboardPageContent>
<ItemsViewsTabs />
<ItemsDataTable />
<DashboardContentTable>
<ItemsDataTable />
</DashboardContentTable>
</DashboardPageContent>
<ItemsAlerts />

View File

@@ -1,46 +1,25 @@
import React, { useEffect } from 'react';
import { useParams } from 'react-router-dom';
import { useIntl } from 'react-intl';
import DashboardPageContent from 'components/Dashboard/DashboardPageContent';
import React from 'react';
import { DashboardContentTable, DashboardPageContent } from 'components';
import ItemsCategoriesAlerts from './ItemsCategoriesAlerts';
import ItemsCategoryActionsBar from './ItemsCategoryActionsBar';
import { ItemsCategoriesProvider } from './ItemsCategoriesProvider';
import ItemCategoriesTable from './ItemCategoriesTable';
import withDashboardActions from 'containers/Dashboard/withDashboardActions';
import { compose } from 'utils';
/**
* Item categories list.
*/
const ItemCategoryList = ({
// #withDashboardActions
changePageTitle,
}) => {
const { id } = useParams();
const { formatMessage } = useIntl();
// Changes the dashboard page title once the page mount.
useEffect(() => {
id
? changePageTitle(formatMessage({ id: 'edit_category_details' }))
: changePageTitle(formatMessage({ id: 'category_list' }));
}, [id, changePageTitle, formatMessage]);
export default function ItemCategoryList() {
return (
<ItemsCategoriesProvider query={{}}>
<ItemsCategoryActionsBar />
<DashboardPageContent>
<ItemCategoriesTable />
<DashboardContentTable>
<ItemCategoriesTable />
</DashboardContentTable>
</DashboardPageContent>
<ItemsCategoriesAlerts />
</ItemsCategoriesProvider>
);
};
export default compose(
withDashboardActions,
)(ItemCategoryList);
}

View File

@@ -24,7 +24,7 @@ function ItemsCategoryTable({
openDialog,
// #withAlertActions
openAlert
openAlert,
}) {
// Items categories context.
const {
@@ -49,34 +49,27 @@ function ItemsCategoryTable({
};
return (
<div className={classNames(CLASSES.DASHBOARD_DATATABLE)}>
<DataTable
noInitialFetch={true}
columns={columns}
data={itemsCategories}
loading={isCategoriesLoading}
headerLoading={isCategoriesLoading}
progressBarLoading={isCategoriesFetching}
manualSortBy={true}
expandable={true}
sticky={true}
selectionColumn={true}
TableLoadingRenderer={TableSkeletonRows}
noResults={'There is no items categories in table yet.'}
payload={{
onDeleteCategory: handleDeleteCategory,
onEditCategory: handleEditCategory
}}
ContextMenu={ActionMenuList}
{...tableProps}
/>
</div>
<DataTable
noInitialFetch={true}
columns={columns}
data={itemsCategories}
loading={isCategoriesLoading}
headerLoading={isCategoriesLoading}
progressBarLoading={isCategoriesFetching}
manualSortBy={true}
expandable={true}
sticky={true}
selectionColumn={true}
TableLoadingRenderer={TableSkeletonRows}
noResults={'There is no items categories in table yet.'}
payload={{
onDeleteCategory: handleDeleteCategory,
onEditCategory: handleEditCategory,
}}
ContextMenu={ActionMenuList}
{...tableProps}
/>
);
}
export default compose(
withDialogActions,
withAlertActions,
)(ItemsCategoryTable);
export default compose(withDialogActions, withAlertActions)(ItemsCategoryTable);

View File

@@ -11,11 +11,10 @@ import {
InputGroup,
Intent,
} from '@blueprintjs/core';
import withSettingsActions from 'containers/Settings/withSettingsActions';
import { compose } from 'utils';
/**
* Reference number form.
*/
export default function ReferenceNumberForm({
onSubmit,
onClose,
@@ -62,6 +61,7 @@ export default function ReferenceNumberForm({
<Row>
{/* prefix */}
<Col>
<FormGroup
label={<T id={'prefix'} />}
className={'form-group--'}

View File

@@ -1,4 +1,4 @@
import React, { useMemo, useCallback, useEffect } from 'react';
import React, { useMemo } from 'react';
import { Formik, Form } from 'formik';
import moment from 'moment';
import { Intent } from '@blueprintjs/core';
@@ -13,12 +13,10 @@ import BillFormHeader from './BillFormHeader';
import BillFloatingActions from './BillFloatingActions';
import BillFormFooter from './BillFormFooter';
import withDashboardActions from 'containers/Dashboard/withDashboardActions';
import { AppToaster } from 'components';
import { ERROR } from 'common/errors';
import { compose, repeatValue, orderingLinesIndexes } from 'utils';
import { repeatValue, orderingLinesIndexes } from 'utils';
import BillFormBody from './BillFormBody';
import { useBillFormContext } from './BillFormProvider';
@@ -47,10 +45,8 @@ const defaultInitialValues = {
/**
* Bill form.
*/
function BillForm({
//#withDashboard
changePageTitle,
changePageSubtitle,
export default function BillForm({
}) {
const { formatMessage } = useIntl();
const history = useHistory();
@@ -65,14 +61,6 @@ function BillForm({
const isNewMode = !billId;
useEffect(() => {
if (!isNewMode) {
changePageTitle(formatMessage({ id: 'edit_bill' }));
} else {
changePageTitle(formatMessage({ id: 'new_bill' }));
}
}, [changePageTitle, isNewMode, formatMessage]);
// Initial values in create and edit mode.
const initialValues = useMemo(
() => ({
@@ -146,8 +134,6 @@ function BillForm({
});
setSubmitting(false);
changePageSubtitle('');
if (submitPayload.redirect) {
history.push('/bills');
}
@@ -167,14 +153,6 @@ function BillForm({
}
};
// Handle bill number changed once the field blur.
const handleBillNumberChanged = useCallback(
(billNumber) => {
changePageSubtitle(billNumber);
},
[changePageSubtitle],
);
return (
<div
className={classNames(
@@ -189,7 +167,7 @@ function BillForm({
onSubmit={handleFormSubmit}
>
<Form>
<BillFormHeader onBillNumberChanged={handleBillNumberChanged} />
<BillFormHeader />
<BillFormBody defaultBill={defaultBill} />
<BillFormFooter />
<BillFloatingActions />
@@ -198,5 +176,3 @@ function BillForm({
</div>
);
}
export default compose(withDashboardActions)(BillForm);

View File

@@ -1,7 +1,6 @@
import React from 'react';
import classNames from 'classnames';
import { CLASSES } from 'common/classes';
import EditableItemsEntriesTable from 'containers/Entries/EditableItemsEntriesTable';
import { useBillFormContext } from './BillFormProvider';
export default function BillFormBody({ defaultBill }) {
@@ -9,11 +8,7 @@ export default function BillFormBody({ defaultBill }) {
return (
<div className={classNames(CLASSES.PAGE_FORM_BODY)}>
<EditableItemsEntriesTable
items={items}
defaultEntry={defaultBill}
filterPurchasableItems={true}
/>
</div>
);
}

View File

@@ -14,8 +14,6 @@ import { compose } from 'redux';
* Fill form header.
*/
function BillFormHeader({
onBillNumberChanged,
// #withSettings
baseCurrency,
}) {
@@ -28,7 +26,7 @@ function BillFormHeader({
return (
<div className={classNames(CLASSES.PAGE_FORM_HEADER)}>
<BillFormHeaderFields onBillNumberChanged={onBillNumberChanged} />
<BillFormHeaderFields />
<PageFormBigNumber
label={'Due Amount'}
amount={totalDueAmount}

View File

@@ -1,45 +1,17 @@
import React, { useCallback, useEffect } from 'react';
import { useParams, useHistory } from 'react-router-dom';
import React from 'react';
import { useParams } from 'react-router-dom';
import BillForm from './BillForm';
import { BillFormProvider } from './BillFormProvider';
import withDashboardActions from 'containers/Dashboard/withDashboardActions';
import { compose } from 'utils';
import 'style/pages/Bills/PageForm.scss';
function BillFormPage({
// #withDashboardActions
setSidebarShrink,
resetSidebarPreviousExpand,
setDashboardBackLink
}) {
export default function BillFormPage() {
const { id } = useParams();
useEffect(() => {
// Shrink the sidebar by foce.
setSidebarShrink();
// Show the back link on dashboard topbar.
setDashboardBackLink(true);
return () => {
// Reset the sidebar to the previous status.
resetSidebarPreviousExpand();
// Hide the back link on dashboard topbar.
setDashboardBackLink(false);
};
}, [resetSidebarPreviousExpand, setSidebarShrink, setDashboardBackLink]);
return (
<BillFormProvider billId={id}>
<BillForm />
</BillFormProvider>
);
}
export default compose(
withDashboardActions
)(BillFormPage);
}

View File

@@ -1,7 +1,5 @@
import React, { useEffect } from 'react';
import { useIntl } from 'react-intl';
import DashboardPageContent from 'components/Dashboard/DashboardPageContent';
import React from 'react';
import { DashboardContentTable, DashboardPageContent } from 'components';
import { BillsListProvider } from './BillsListProvider';
@@ -10,7 +8,6 @@ import BillsAlerts from './BillsAlerts';
import BillsViewsTabs from './BillsViewsTabs';
import BillsTable from './BillsTable';
import withDashboardActions from 'containers/Dashboard/withDashboardActions';
import withBills from './withBills';
import { transformTableStateToQuery, compose } from 'utils';
@@ -19,25 +16,19 @@ import { transformTableStateToQuery, compose } from 'utils';
* Bills list.
*/
function BillsList({
// #withDashboardActions
changePageTitle,
// #withBills
billsTableState,
}) {
const { formatMessage } = useIntl();
useEffect(() => {
changePageTitle(formatMessage({ id: 'bills_list' }));
}, [changePageTitle, formatMessage]);
return (
<BillsListProvider query={transformTableStateToQuery(billsTableState)}>
<BillsActionsBar />
<DashboardPageContent>
<BillsViewsTabs />
<BillsTable />
<DashboardContentTable>
<BillsTable />
</DashboardContentTable>
</DashboardPageContent>
<BillsAlerts />
@@ -46,6 +37,5 @@ function BillsList({
}
export default compose(
withDashboardActions,
withBills(({ billsTableState }) => ({ billsTableState })),
)(BillsList);

View File

@@ -1,9 +1,7 @@
import React, { useCallback } from 'react';
import classNames from 'classnames';
import { useHistory } from 'react-router-dom';
import { compose } from 'utils';
import { CLASSES } from 'common/classes';
import DataTable from 'components/DataTable';
import TableSkeletonRows from 'components/Datatable/TableSkeletonRows';
@@ -25,7 +23,7 @@ function BillsDataTable({
setBillsTableState,
// #withAlerts
openAlert
openAlert,
}) {
// Bills list context.
const {
@@ -72,30 +70,28 @@ function BillsDataTable({
}
return (
<div className={classNames(CLASSES.DASHBOARD_DATATABLE)}>
<DataTable
columns={columns}
data={bills}
loading={isBillsLoading}
headerLoading={isBillsLoading}
progressBarLoading={isBillsFetching}
onFetchData={handleFetchData}
manualSortBy={true}
selectionColumn={true}
noInitialFetch={true}
sticky={true}
pagination={true}
pagesCount={pagination.pagesCount}
TableLoadingRenderer={TableSkeletonRows}
TableHeaderSkeletonRenderer={TableSkeletonHeader}
ContextMenu={ActionsMenu}
payload={{
onDelete: handleDeleteBill,
onEdit: handleEditBill,
onOpen: handleOpenBill
}}
/>
</div>
<DataTable
columns={columns}
data={bills}
loading={isBillsLoading}
headerLoading={isBillsLoading}
progressBarLoading={isBillsFetching}
onFetchData={handleFetchData}
manualSortBy={true}
selectionColumn={true}
noInitialFetch={true}
sticky={true}
pagination={true}
pagesCount={pagination.pagesCount}
TableLoadingRenderer={TableSkeletonRows}
TableHeaderSkeletonRenderer={TableSkeletonHeader}
ContextMenu={ActionsMenu}
payload={{
onDelete: handleDeleteBill,
onEdit: handleEditBill,
onOpen: handleOpenBill,
}}
/>
);
}

View File

@@ -1,14 +1,11 @@
import React, { useEffect } from 'react';
import { useIntl } from 'react-intl';
import DashboardPageContent from 'components/Dashboard/DashboardPageContent';
import React from 'react';
import { DashboardContentTable, DashboardPageContent } from 'components';
import PaymentMadeActionsBar from './PaymentMadeActionsBar';
import PaymentMadesAlerts from '../PaymentMadesAlerts';
import PaymentMadesTable from './PaymentMadesTable';
import { PaymentMadesListProvider } from './PaymentMadesListProvider';
import PaymentMadeViewTabs from './PaymentMadeViewTabs';
import withDashboardActions from 'containers/Dashboard/withDashboardActions';
import withPaymentMades from './withPaymentMade';
import { compose, transformTableStateToQuery } from 'utils';
@@ -17,18 +14,9 @@ import { compose, transformTableStateToQuery } from 'utils';
* Payment mades list.
*/
function PaymentMadeList({
// #withDashboardActions
changePageTitle,
// #withPaymentMades
paymentMadesTableState,
}) {
const { formatMessage } = useIntl();
useEffect(() => {
changePageTitle(formatMessage({ id: 'payment_made_list' }));
}, [changePageTitle, formatMessage]);
return (
<PaymentMadesListProvider
query={transformTableStateToQuery(paymentMadesTableState)}
@@ -37,7 +25,10 @@ function PaymentMadeList({
<DashboardPageContent>
<PaymentMadeViewTabs />
<PaymentMadesTable />
<DashboardContentTable>
<PaymentMadesTable />
</DashboardContentTable>
</DashboardPageContent>
<PaymentMadesAlerts />
@@ -46,7 +37,6 @@ function PaymentMadeList({
}
export default compose(
withDashboardActions,
withPaymentMades(({ paymentMadesTableState }) => ({
paymentMadesTableState,
})),

View File

@@ -23,7 +23,7 @@ function PaymentMadesTable({
addPaymentMadesTableQueries,
// #withAlerts
openAlert
openAlert,
}) {
// Payment mades table columns.
const columns = usePaymentMadesTableColumns();
@@ -42,7 +42,7 @@ function PaymentMadesTable({
// Handles the delete payment made action.
const handleDeletePaymentMade = (paymentMade) => {
openAlert('payment-made-delete', { paymentMadeId: paymentMade.id })
openAlert('payment-made-delete', { paymentMadeId: paymentMade.id });
};
// Handle datatable fetch data once the table state change.
@@ -52,37 +52,36 @@ function PaymentMadesTable({
},
[addPaymentMadesTableQueries],
);
// Display empty status instead of the table.
if (isEmptyStatus) {
return <PaymentMadesEmptyStatus />;
}
return (
<div className={classNames(CLASSES.DASHBOARD_DATATABLE)}>
<DataTable
columns={columns}
data={paymentMades}
onFetchData={handleDataTableFetchData}
loading={isPaymentsLoading}
headerLoading={isPaymentsLoading}
progressBarLoading={isPaymentsFetching}
manualSortBy={true}
selectionColumn={true}
noInitialFetch={true}
sticky={true}
pagination={true}
pagesCount={pagination.pagesCount}
autoResetSortBy={false}
autoResetPage={false}
TableLoadingRenderer={TableSkeletonRows}
TableHeaderSkeletonRenderer={TableSkeletonHeader}
ContextMenu={ActionsMenu}
payload={{
onEdit: handleEditPaymentMade,
onDelete: handleDeletePaymentMade,
}}
/>
</div>
<DataTable
columns={columns}
data={paymentMades}
onFetchData={handleDataTableFetchData}
loading={isPaymentsLoading}
headerLoading={isPaymentsLoading}
progressBarLoading={isPaymentsFetching}
manualSortBy={true}
selectionColumn={true}
noInitialFetch={true}
sticky={true}
pagination={true}
pagesCount={pagination.pagesCount}
autoResetSortBy={false}
autoResetPage={false}
TableLoadingRenderer={TableSkeletonRows}
TableHeaderSkeletonRenderer={TableSkeletonHeader}
ContextMenu={ActionsMenu}
payload={{
onEdit: handleEditPaymentMade,
onDelete: handleDeletePaymentMade,
}}
/>
);
}

View File

@@ -13,70 +13,55 @@ import { FormattedMessage as T } from 'react-intl';
import { CLASSES } from 'common/classes';
import classNames from 'classnames';
import { useFormikContext } from 'formik';
import { saveInvoke } from 'utils';
import { If, Icon } from 'components';
import { useEstimateFormContext } from './EstimateFormProvider';
/**
* Estimate floating actions bar.
*/
export default function EstimateFloatingActions({
isSubmitting,
onSubmitClick,
onCancelClick,
estimate,
}) {
const { resetForm, submitForm } = useFormikContext();
export default function EstimateFloatingActions() {
const { resetForm, submitForm, isSubmitting } = useFormikContext();
// Estimate form context.
const { estimate, setSubmitPayload } = useEstimateFormContext();
// Handle submit & deliver button click.
const handleSubmitDeliverBtnClick = (event) => {
saveInvoke(onSubmitClick, event, {
redirect: true,
deliver: true,
});
setSubmitPayload({ redirect: true, deliver: true, });
};
// Handle submit, deliver & new button click.
const handleSubmitDeliverAndNewBtnClick = (event) => {
setSubmitPayload({ redirect: false, deliver: true, resetForm: true });
submitForm();
saveInvoke(onSubmitClick, event, {
redirect: false,
deliver: true,
resetForm: true,
});
};
// Handle submit, deliver & continue editing button click.
const handleSubmitDeliverContinueEditingBtnClick = (event) => {
setSubmitPayload({ redirect: false, deliver: true });
submitForm();
saveInvoke(onSubmitClick, event, {
redirect: false,
deliver: true,
});
};
// Handle submit as draft button click.
const handleSubmitDraftBtnClick = (event) => {
saveInvoke(onSubmitClick, event, {
redirect: true,
deliver: false,
});
setSubmitPayload({ redirect: true, deliver: false });
submitForm();
};
// Handle submit as draft & new button click.
const handleSubmitDraftAndNewBtnClick = (event) => {
setSubmitPayload({ redirect: false, deliver: false, resetForm: true });
submitForm();
saveInvoke(onSubmitClick, event, {
redirect: false,
deliver: false,
resetForm: true,
});
};
// Handle submit as draft & continue editing button click.
const handleSubmitDraftContinueEditingBtnClick = (event) => {
setSubmitPayload({ redirect: false, deliver: false });
submitForm();
saveInvoke(onSubmitClick, event, {
redirect: false,
deliver: false,
});
};
const handleCancelBtnClick = (event) => {
saveInvoke(onCancelClick, event);
};
const handleClearBtnClick = (event) => {
@@ -90,6 +75,7 @@ export default function EstimateFloatingActions({
<ButtonGroup>
<Button
disabled={isSubmitting}
loading={isSubmitting}
intent={Intent.PRIMARY}
type="submit"
onClick={handleSubmitDeliverBtnClick}

View File

@@ -1,4 +1,4 @@
import React, { useMemo, useCallback, useEffect, useState } from 'react';
import React, { useMemo } from 'react';
import { Formik, Form } from 'formik';
import moment from 'moment';
import { Intent } from '@blueprintjs/core';
@@ -17,10 +17,6 @@ import EstimateFormHeader from './EstimateFormHeader';
import EstimateFormBody from './EstimateFormBody';
import EstimateFloatingActions from './EstimateFloatingActions';
import EstimateFormFooter from './EstimateFormFooter';
import EstimateNumberWatcher from './EstimateNumberWatcher';
import withEstimateActions from './withEstimateActions';
import withEstimateDetail from './withEstimateDetail';
import withDashboardActions from 'containers/Dashboard/withDashboardActions';
import withMediaActions from 'containers/Media/withMediaActions';
@@ -28,13 +24,12 @@ import withSettings from 'containers/Settings/withSettings';
import { AppToaster } from 'components';
import { ERROR } from 'common/errors';
import {
compose,
repeatValue,
defaultToTransform,
orderingLinesIndexes,
} from 'utils';
import { useEstimateFormContext } from './EstimateFormProvider';
const MIN_LINES_NUMBER = 4;
@@ -63,64 +58,25 @@ const defaultInitialValues = {
* Estimate form.
*/
const EstimateForm = ({
// #WithMedia
requestSubmitMedia,
requestDeleteMedia,
// #WithEstimateActions
requestSubmitEstimate,
requestEditEstimate,
setEstimateNumberChanged,
//#withDashboard
changePageTitle,
changePageSubtitle,
// #withSettings
estimateNextNumber,
estimateNumberPrefix,
//#withEstimateDetail
estimate,
// #withEstimates
estimateNumberChanged,
//#own Props
estimateId,
onFormSubmit,
onCancelForm,
}) => {
const { formatMessage } = useIntl();
const history = useHistory();
const [submitPayload, setSubmitPayload] = useState({});
const isNewMode = !estimateId;
const {
estimate,
isNewMode,
submitPayload,
createEstimateMutate,
editEstimateMutate,
} = useEstimateFormContext();
const estimateNumber = estimateNumberPrefix
? `${estimateNumberPrefix}-${estimateNextNumber}`
: estimateNextNumber;
useEffect(() => {
const transNumber = !isNewMode ? estimate.estimate_number : estimateNumber;
if (!isNewMode) {
changePageTitle(formatMessage({ id: 'edit_estimate' }));
} else {
changePageTitle(formatMessage({ id: 'new_estimate' }));
}
changePageSubtitle(
defaultToTransform(estimateNumber, `No. ${transNumber}`, ''),
);
}, [
estimate,
estimateNumber,
isNewMode,
formatMessage,
changePageTitle,
changePageSubtitle,
]);
// Initial values in create and edit mode.
const initialValues = useMemo(
() => ({
@@ -167,7 +123,6 @@ const EstimateForm = ({
const entries = values.entries.filter(
(item) => item.item_id && item.quantity,
);
const totalQuantity = sumBy(entries, (entry) => parseInt(entry.quantity));
if (totalQuantity === 0) {
@@ -218,35 +173,12 @@ const EstimateForm = ({
};
if (estimate && estimate.id) {
requestEditEstimate(estimate.id, form).then(onSuccess).catch(onError);
editEstimateMutate([estimate.id, form]).then(onSuccess).catch(onError);
} else {
requestSubmitEstimate(form).then(onSuccess).catch(onError);
createEstimateMutate(form).then(onSuccess).catch(onError);
}
};
const handleEstimateNumberChange = useCallback(
(estimateNumber) => {
changePageSubtitle(
defaultToTransform(estimateNumber, `No. ${estimateNumber}`, ''),
);
},
[changePageSubtitle],
);
const handleSubmitClick = useCallback(
(event, payload) => {
setSubmitPayload({ ...payload });
},
[setSubmitPayload],
);
const handleCancelClick = useCallback(
(event) => {
history.goBack();
},
[history],
);
return (
<div
className={classNames(
@@ -262,30 +194,18 @@ const EstimateForm = ({
initialValues={initialValues}
onSubmit={handleFormSubmit}
>
{({ isSubmitting}) => (
<Form>
<EstimateFormHeader
onEstimateNumberChanged={handleEstimateNumberChange}
/>
<EstimateNumberWatcher estimateNumber={estimateNumber} />
<EstimateFormBody defaultEstimate={defaultEstimate} />
<EstimateFormFooter />
<EstimateFloatingActions
isSubmitting={isSubmitting}
estimate={estimate}
onSubmitClick={handleSubmitClick}
onCancelClick={handleCancelClick}
/>
</Form>
)}
<Form>
<EstimateFormHeader />
<EstimateFormBody defaultEstimate={defaultEstimate} />
<EstimateFormFooter />
<EstimateFloatingActions />
</Form>
</Formik>
</div>
);
};
export default compose(
withEstimateActions,
withEstimateDetail(),
withDashboardActions,
withMediaActions,
withSettings(({ estimatesSettings }) => ({

View File

@@ -1,15 +1,15 @@
import React from 'react';
import classNames from 'classnames';
import { CLASSES } from 'common/classes';
import EditableItemsEntriesTable from 'containers/Entries/EditableItemsEntriesTable';
// import EditableItemsEntriesTable from 'containers/Entries/EditableItemsEntriesTable';
export default function EstimateFormBody({ defaultEstimate }) {
return (
<div className={classNames(CLASSES.PAGE_FORM_BODY)}>
<EditableItemsEntriesTable
{/* <EditableItemsEntriesTable
defaultEntry={defaultEstimate}
filterSellableItems={true}
/>
/> */}
</div>
);
}

View File

@@ -12,9 +12,6 @@ import { compose } from 'utils';
// Estimate form top header.
function EstimateFormHeader({
// #ownProps
onEstimateNumberChanged,
// #withSettings
baseCurrency,
}) {
@@ -27,9 +24,7 @@ function EstimateFormHeader({
return (
<div className={classNames(CLASSES.PAGE_FORM_HEADER)}>
<EstimateFormHeaderFields
onEstimateNumberChanged={onEstimateNumberChanged}
/>
<EstimateFormHeaderFields />
<PageFormBigNumber
label={'Amount'}
amount={totalDueAmount}

View File

@@ -1,4 +1,4 @@
import React, { useCallback } from 'react';
import React from 'react';
import {
FormGroup,
InputGroup,
@@ -8,7 +8,7 @@ import {
import { DateInput } from '@blueprintjs/datetime';
import { FormattedMessage as T } from 'react-intl';
import { FastField, ErrorMessage } from 'formik';
import { momentFormatter, compose, tansformDateValue, saveInvoke } from 'utils';
import { momentFormatter, compose, tansformDateValue } from 'utils';
import classNames from 'classnames';
import { CLASSES } from 'common/classes';
import {
@@ -16,30 +16,25 @@ import {
FieldRequiredHint,
Icon,
InputPrependButton,
Row,
Col,
} from 'components';
import withCustomers from 'containers/Customers/withCustomers';
import withDialogActions from 'containers/Dialog/withDialogActions';
import { inputIntent, handleDateChange } from 'utils';
import { formatMessage } from 'services/intl';
import { useEstimateFormContext } from './EstimateFormProvider';
/**
* Estimate form header.
*/
function EstimateFormHeader({
//#withCustomers
customers,
// #withDialogActions
openDialog,
// #ownProps
onEstimateNumberChanged,
}) {
const handleEstimateNumberChange = useCallback(() => {
openDialog('estimate-number-form', {});
}, [openDialog]);
const { customers } = useEstimateFormContext();
const handleEstimateNumberChanged = (event) => {
saveInvoke(onEstimateNumberChanged, event.currentTarget.value);
const handleEstimateNumberChange = () => {
openDialog('estimate-number-form', {});
};
return (
@@ -138,7 +133,6 @@ function EstimateFormHeader({
<InputGroup
minimal={true}
{...field}
onBlur={handleEstimateNumberChanged}
/>
<InputPrependButton
buttonProps={{

View File

@@ -1,39 +1,24 @@
import React, { useCallback, useEffect } from 'react';
import React, { useEffect } from 'react';
import { useParams, useHistory } from 'react-router-dom';
import { useQuery } from 'react-query';
import EstimateForm from './EstimateForm';
import DashboardInsider from 'components/Dashboard/DashboardInsider';
import withCustomersActions from 'containers/Customers/withCustomersActions';
import withItemsActions from 'containers/Items/withItemsActions';
import withEstimateActions from './withEstimateActions';
import withSettingsActions from 'containers/Settings/withSettingsActions';
import withDashboardActions from 'containers/Dashboard/withDashboardActions';
import { compose } from 'utils';
import 'style/pages/SaleEstimate/PageForm.scss';
import EstimateForm from './EstimateForm';
import { EstimateFormProvider } from './EstimateFormProvider';
import withDashboardActions from 'containers/Dashboard/withDashboardActions';
import { compose } from 'utils';
/**
* Estimate form page.
*/
function EstimateFormPage({
// #withCustomersActions
requestFetchCustomers,
// #withItemsActions
requestFetchItems,
// #withEstimateActions
requestFetchEstimate,
// #withSettingsActions
requestFetchOptions,
// #withDashboardActions
setSidebarShrink,
resetSidebarPreviousExpand,
setDashboardBackLink,
}) {
const history = useHistory();
const { id } = useParams();
useEffect(() => {
@@ -50,55 +35,15 @@ function EstimateFormPage({
};
}, [resetSidebarPreviousExpand, setSidebarShrink, setDashboardBackLink]);
const fetchEstimate = useQuery(
['estimate', id],
(key, _id) => requestFetchEstimate(_id),
{ enabled: !!id },
);
// Handle fetch Items data table or list
const fetchItems = useQuery('items-list', () => requestFetchItems({}));
// Handle fetch customers data table or list
const fetchCustomers = useQuery('customers-table', () =>
requestFetchCustomers({}),
);
//
const handleFormSubmit = useCallback(
(payload) => {
payload.redirect && history.push('/estimates');
},
[history],
);
const handleCancel = useCallback(() => {
history.goBack();
}, [history]);
const fetchSettings = useQuery(['settings'], () => requestFetchOptions({}));
return (
<DashboardInsider
loading={
fetchCustomers.isFetching ||
fetchItems.isFetching ||
fetchEstimate.isFetching
}
name={'estimate-form'}
>
<EstimateForm
estimateId={id}
onFormSubmit={handleFormSubmit}
onCancelForm={handleCancel}
/>
</DashboardInsider>
<EstimateFormProvider estimateId={id}>
<EstimateForm />
</EstimateFormProvider>
);
}
export default compose(
withEstimateActions,
withCustomersActions,
withItemsActions,
withSettingsActions,
withDashboardActions,
)(EstimateFormPage);

View File

@@ -0,0 +1,76 @@
import React, { createContext, useContext } from 'react';
import DashboardInsider from 'components/Dashboard/DashboardInsider';
import {
useEstimate,
useCustomers,
useItems,
useSettings,
useCreateEstimate,
useEditEstimate
} from 'query/hooks';
const EstimateFormContext = createContext();
/**
* Estimate form provider.
*/
function EstimateFormProvider({ estimateId, ...props }) {
const { data: estimate, isFetching: isEstimateFetching } = useEstimate(
estimateId,
);
// Handle fetch Items data table or list
const {
data: { items },
isFetching: isItemsFetching,
} = useItems();
// Handle fetch customers data table or list
const {
data: { customers },
isFetch: isCustomersFetching,
} = useCustomers();
// Handle fetch settings.
const {
data: { settings },
} = useSettings();
const [submitPayload, setSubmitPayload] = React.useState({});
const isNewMode = !estimateId;
const { mutateAsync: createEstimateMutate } = useCreateEstimate();
const { mutateAsync: editEstimateMutate } = useEditEstimate();
// Provider payload.
const provider = {
estimateId,
estimate,
items,
customers,
isNewMode,
isItemsFetching,
isEstimateFetching,
submitPayload,
setSubmitPayload,
createEstimateMutate,
editEstimateMutate
};
return (
<DashboardInsider
loading={isCustomersFetching || isItemsFetching || isEstimateFetching}
name={'estimate-form'}
>
<EstimateFormContext.Provider value={provider} {...props} />
</DashboardInsider>
);
}
const useEstimateFormContext = () => useContext(EstimateFormContext);
export { EstimateFormProvider, useEstimateFormContext };

View File

@@ -17,7 +17,6 @@ import withAlertsActions from 'containers/Alert/withAlertActions';
import { useEstimatesListContext } from './EstimatesListProvider';
import { ActionsMenu, useEstiamtesTableColumns } from './components';
/**
* Estimates datatable.
*/
@@ -78,43 +77,37 @@ function EstimatesDataTable({
[setEstimatesTableState],
);
// Display empty status instead of the table.
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>
<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,
}}
/>
);
}

View File

@@ -1,13 +1,11 @@
import React, { useEffect } from 'react';
import { FormattedMessage as T, useIntl } from 'react-intl';
import DashboardPageContent from 'components/Dashboard/DashboardPageContent';
import React from 'react';
import { DashboardContentTable, DashboardPageContent } from 'components';
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';
@@ -17,18 +15,9 @@ 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)}
@@ -37,7 +26,10 @@ function EstimatesList({
<DashboardPageContent>
<EstimatesViewTabs />
<EstimatesDataTable />
<DashboardContentTable>
<EstimatesDataTable />
</DashboardContentTable>
</DashboardPageContent>
<EstimatesAlerts />
@@ -46,6 +38,5 @@ function EstimatesList({
}
export default compose(
withDashboardActions,
withEstimates(({ estimatesTableState }) => ({ estimatesTableState })),
)(EstimatesList);

View File

@@ -151,6 +151,7 @@ export default function InvoiceFloatingActions() {
<ButtonGroup>
<Button
disabled={isSubmitting}
loading={isSubmitting}
intent={Intent.PRIMARY}
onClick={handleSubmitDeliverBtnClick}
text={<T id={'save'} />}

View File

@@ -1,10 +1,10 @@
import React, { useMemo, useCallback, useEffect } from 'react';
import React, { useMemo } from 'react';
import { Formik, Form } from 'formik';
import moment from 'moment';
import { Intent } from '@blueprintjs/core';
import { useIntl } from 'react-intl';
import { pick, sumBy, omit, isEmpty } from 'lodash';
import { sumBy, omit, isEmpty } from 'lodash';
import classNames from 'classnames';
import { useHistory } from 'react-router-dom';
import { CLASSES } from 'common/classes';
import {
CreateInvoiceFormSchema,
@@ -12,12 +12,10 @@ import {
} from './InvoiceForm.schema';
import InvoiceFormHeader from './InvoiceFormHeader';
import EditableItemsEntriesTable from 'containers/Entries/EditableItemsEntriesTable';
import InvoiceItemsEntriesEditorField from './InvoiceItemsEntriesEditorField';
import InvoiceFloatingActions from './InvoiceFloatingActions';
import InvoiceFormFooter from './InvoiceFormFooter';
import InvoiceNumberChangeWatcher from './InvoiceNumberChangeWatcher';
import withInvoiceActions from './withInvoiceActions';
import withDashboardActions from 'containers/Dashboard/withDashboardActions';
import withMediaActions from 'containers/Media/withMediaActions';
import withSettings from 'containers/Settings/withSettings';
@@ -26,101 +24,47 @@ import { AppToaster } from 'components';
import { ERROR } from 'common/errors';
import {
compose,
repeatValue,
defaultToTransform,
orderingLinesIndexes,
transactionNumber,
} from 'utils';
import { useHistory } from 'react-router-dom';
import { useInvoiceFormContext } from './InvoiceFormProvider';
const MIN_LINES_NUMBER = 4;
const defaultInvoice = {
index: 0,
item_id: '',
rate: '',
discount: 0,
quantity: 1,
description: '',
};
const defaultInitialValues = {
customer_id: '',
invoice_date: moment(new Date()).format('YYYY-MM-DD'),
due_date: moment(new Date()).format('YYYY-MM-DD'),
delivered: '',
invoice_no: '',
reference_no: '',
invoice_message: '',
terms_conditions: '',
entries: [...repeatValue(defaultInvoice, MIN_LINES_NUMBER)],
};
import { transformToEditForm } from './utils';
import {
MIN_LINES_NUMBER,
defaultInitialValues
} from './constants';
/**
* Invoice form.
*/
function InvoiceForm({
// #withDashboard
changePageTitle,
changePageSubtitle,
// #withSettings
invoiceNextNumber,
invoiceNumberPrefix,
}) {
const { formatMessage } = useIntl();
const history = useHistory();
// Invoice form context.
const {
items,
invoiceId,
isNewMode,
invoice,
createInvoiceMutate,
editInvoiceMutate,
submitPayload,
} = useInvoiceFormContext();
const isNewMode = !invoiceId;
// Invoice number.
const invoiceNumber = transactionNumber(
invoiceNumberPrefix,
invoiceNextNumber,
);
useEffect(() => {
const transactionNumber = invoice ? invoice.invoice_no : invoiceNumber;
if (invoice && invoice.id) {
changePageTitle(formatMessage({ id: 'edit_invoice' }));
} else {
changePageTitle(formatMessage({ id: 'new_invoice' }));
}
changePageSubtitle(
defaultToTransform(transactionNumber, `No. ${transactionNumber}`, ''),
);
}, [
changePageTitle,
changePageSubtitle,
invoice,
invoiceNumber,
formatMessage,
]);
// Form initial values.
const initialValues = useMemo(
() => ({
...(!isEmpty(invoice)
? {
...pick(invoice, Object.keys(defaultInitialValues)),
entries: [
...invoice.entries.map((invoice) => ({
...pick(invoice, Object.keys(defaultInvoice)),
})),
...repeatValue(
defaultInvoice,
Math.max(MIN_LINES_NUMBER - invoice.entries.length, 0),
),
],
}
? transformToEditForm(invoice, defaultInitialValues, MIN_LINES_NUMBER)
: {
...defaultInitialValues,
invoice_no: invoiceNumber,
@@ -193,22 +137,13 @@ function InvoiceForm({
setSubmitting(false);
};
if (invoice && invoice.id) {
editInvoiceMutate(invoice.id, form).then(onSuccess).catch(onError);
if (!isEmpty(invoice)) {
editInvoiceMutate([invoice.id, form]).then(onSuccess).catch(onError);
} else {
createInvoiceMutate(form).then(onSuccess).catch(onError);
}
};
const handleInvoiceNumberChanged = useCallback(
(invoiceNumber) => {
changePageSubtitle(
defaultToTransform(invoiceNumber, `No. ${invoiceNumber}`, ''),
);
},
[changePageSubtitle],
);
return (
<div
className={classNames(
@@ -225,17 +160,10 @@ function InvoiceForm({
onSubmit={handleSubmit}
>
<Form>
<InvoiceFormHeader
onInvoiceNumberChanged={handleInvoiceNumberChanged}
/>
<InvoiceNumberChangeWatcher invoiceNumber={invoiceNumber} />
<InvoiceFormHeader />
<div className={classNames(CLASSES.PAGE_FORM_BODY)}>
<EditableItemsEntriesTable
items={items}
defaultEntry={defaultInvoice}
filterSellableItems={true}
/>
<InvoiceItemsEntriesEditorField />
</div>
<InvoiceFormFooter />
<InvoiceFloatingActions />
@@ -246,7 +174,6 @@ function InvoiceForm({
}
export default compose(
withInvoiceActions,
withDashboardActions,
withMediaActions,
withSettings(({ invoiceSettings }) => ({

View File

@@ -15,8 +15,6 @@ import { compose } from 'redux';
* Invoice form header section.
*/
function InvoiceFormHeader({
// #ownProps
onInvoiceNumberChanged,
// #withSettings
baseCurrency,
}) {
@@ -29,9 +27,7 @@ function InvoiceFormHeader({
return (
<div className={classNames(CLASSES.PAGE_FORM_HEADER)}>
<InvoiceFormHeaderFields
onInvoiceNumberChanged={onInvoiceNumberChanged}
/>
<InvoiceFormHeaderFields />
<PageFormBigNumber
label={'Due Amount'}
amount={totalDueAmount}

View File

@@ -1,4 +1,4 @@
import React, { useCallback } from 'react';
import React from 'react';
import {
FormGroup,
InputGroup,
@@ -8,7 +8,7 @@ import {
import { DateInput } from '@blueprintjs/datetime';
import { FastField, ErrorMessage } from 'formik';
import { FormattedMessage as T } from 'react-intl';
import { momentFormatter, compose, tansformDateValue, saveInvoke } from 'utils';
import { momentFormatter, compose, tansformDateValue } from 'utils';
import classNames from 'classnames';
import { CLASSES } from 'common/classes';
import {
@@ -19,9 +19,7 @@ import {
} from 'components';
import { useInvoiceFormContext } from './InvoiceFormProvider';
import withDialogActions from 'containers/Dialog/withDialogActions';
import { inputIntent, handleDateChange } from 'utils';
/**
@@ -30,19 +28,13 @@ import { inputIntent, handleDateChange } from 'utils';
function InvoiceFormHeaderFields({
// #withDialogActions
openDialog,
// #ownProps
onInvoiceNumberChanged,
}) {
// Invoice form context.
const { customers } = useInvoiceFormContext();
const handleInvoiceNumberChange = useCallback(() => {
// Handle invoice number changing.
const handleInvoiceNumberChange = () => {
openDialog('invoice-number-form', {});
}, [openDialog]);
const handleInvoiceNumberChanged = (event) => {
saveInvoke(onInvoiceNumberChanged, event.currentTarget.value);
};
return (
@@ -136,7 +128,6 @@ function InvoiceFormHeaderFields({
<InputGroup
minimal={true}
{...field}
onBlur={handleInvoiceNumberChanged}
/>
<InputPrependButton
buttonProps={{

View File

@@ -1,62 +1,20 @@
import React, { useCallback, useEffect } from 'react';
import { useParams, useHistory } from 'react-router-dom';
import React from 'react';
import { useParams } from 'react-router-dom';
import InvoiceForm from './InvoiceForm';
import withDashboardActions from 'containers/Dashboard/withDashboardActions';
import { compose } from 'utils';
import 'style/pages/SaleInvoice/PageForm.scss';
import { InvoiceFormProvider } from './InvoiceFormProvider';
/**
* Invoice form page.
*/
function InvoiceFormPage({
// #withDashboardActions
setSidebarShrink,
resetSidebarPreviousExpand,
setDashboardBackLink,
}) {
const history = useHistory();
export default function InvoiceFormPage() {
const { id } = useParams();
useEffect(() => {
// Shrink the sidebar by foce.
setSidebarShrink();
// Show the back link on dashboard topbar.
setDashboardBackLink(true);
return () => {
// Reset the sidebar to the previous status.
resetSidebarPreviousExpand();
// Hide the back link on dashboard topbar.
setDashboardBackLink(false);
};
}, [resetSidebarPreviousExpand, setSidebarShrink, setDashboardBackLink]);
const handleFormSubmit = useCallback(
(payload) => {
payload.redirect && history.push('/invoices');
},
[history],
);
const handleCancel = useCallback(() => {
history.goBack();
}, [history]);
return (
<InvoiceFormProvider invoiceId={id}>
<InvoiceForm
onFormSubmit={handleFormSubmit}
onCancelForm={handleCancel}
/>
<InvoiceForm />
</InvoiceFormProvider>
);
}
export default compose(
withDashboardActions,
)(InvoiceFormPage);

View File

@@ -40,6 +40,9 @@ function InvoiceFormProvider({ invoiceId, ...props }) {
// Form submit payload.
const [submitPayload, setSubmitPayload] = useState({});
// Detarmines whether the form in new mode.
const isNewMode = !invoiceId;
// Provider payload.
const provider = {
invoice,
@@ -54,6 +57,7 @@ function InvoiceFormProvider({ invoiceId, ...props }) {
createInvoiceMutate,
editInvoiceMutate,
setSubmitPayload,
isNewMode
};
return (

View File

@@ -0,0 +1,27 @@
import React from 'react';
import { FastField } from 'formik';
import ItemsEntriesTable from 'containers/Entries/ItemsEntriesTable';
import { useInvoiceFormContext } from './InvoiceFormProvider';
/**
* Invoice items entries editor field.
*/
export default function InvoiceItemsEntriesEditorField() {
const { items } = useInvoiceFormContext();
return (
<FastField name={'entries'}>
{({ form, field: { value }, meta: { error, touched } }) => (
<ItemsEntriesTable
entries={value}
onUpdateData={(entries) => {
form.setFieldValue('entries', entries);
}}
items={items}
errors={error}
linesNumber={4}
/>
)}
</FastField>
);
}

View File

@@ -1,44 +0,0 @@
import { useEffect } from 'react';
import { useFormikContext } from 'formik';
import withInvoices from './withInvoices';
import withInvoiceActions from './withInvoiceActions';
import withDashboardActions from 'containers/Dashboard/withDashboardActions';
import { compose } from 'utils';
function InvoiceNumberChangeWatcher({
invoiceNumber,
// #WithInvoiceActions
setInvoiceNumberChanged,
// #withInvoices
invoiceNumberChanged,
// #withDashboardActions
changePageSubtitle,
}) {
const { setFieldValue } = useFormikContext();
useEffect(() => {
if (invoiceNumberChanged) {
setFieldValue('invoice_no', invoiceNumber);
changePageSubtitle(`No. ${invoiceNumber}`);
setInvoiceNumberChanged(false);
}
}, [
invoiceNumber,
invoiceNumberChanged,
setFieldValue,
changePageSubtitle,
setInvoiceNumberChanged,
]);
return null;
}
export default compose(
withInvoices(({ invoiceNumberChanged }) => ({ invoiceNumberChanged })),
withInvoiceActions,
withDashboardActions,
)(InvoiceNumberChangeWatcher);

View File

@@ -0,0 +1,26 @@
import { moment } from 'moment';
import { repeatValue } from 'utils';
export const MIN_LINES_NUMBER = 4;
export const defaultInvoice = {
index: 0,
item_id: '',
rate: '',
discount: 0,
quantity: 1,
description: '',
total: 0,
};
export const defaultInitialValues = {
customer_id: '',
invoice_date: moment(new Date()).format('YYYY-MM-DD'),
due_date: moment(new Date()).format('YYYY-MM-DD'),
delivered: '',
invoice_no: '',
reference_no: '',
invoice_message: '',
terms_conditions: '',
entries: [...repeatValue(defaultInvoice, MIN_LINES_NUMBER)],
};

View File

@@ -0,0 +1,17 @@
import { transformToForm, repeatValue } from 'utils';
export function transformToEditForm(invoice, defaultInvoice, linesNumber) {
return {
...transformToForm(invoice, defaultInvoice),
entries: [
...invoice.entries.map((invoice) => ({
...transformToForm(invoice, defaultInvoice.entries[0]),
})),
...repeatValue(
defaultInvoice,
Math.max(linesNumber - invoice.entries.length, 0),
),
],
};
}

View File

@@ -72,40 +72,38 @@ function InvoicesDataTable({
[setInvoicesTableState],
);
// Display invoice empty status.
// Display invoice empty status instead of the table.
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>
<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,
}}
/>
);
}

View File

@@ -1,9 +1,8 @@
import React, { useEffect } from 'react';
import { useIntl } from 'react-intl';
import React from 'react';
import 'style/pages/SaleInvoice/List.scss';
import DashboardPageContent from 'components/Dashboard/DashboardPageContent';
import { DashboardContentTable, DashboardPageContent } from 'components';
import InvoiceActionsBar from './InvoiceActionsBar';
import { InvoicesListProvider } from './InvoicesListProvider';
@@ -11,7 +10,6 @@ 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';
@@ -21,18 +19,9 @@ 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)}
@@ -41,7 +30,10 @@ function InvoicesList({
<DashboardPageContent>
<InvoiceViewTabs />
<InvoicesDataTable />
<DashboardContentTable>
<InvoicesDataTable />
</DashboardContentTable>
</DashboardPageContent>
<InvoicesAlerts />
@@ -50,7 +42,6 @@ function InvoicesList({
}
export default compose(
withDashboardActions,
withInvoices(({ invoicesTableState }) => ({ invoicesTableState })),
withAlertsActions,
)(InvoicesList);

Some files were not shown because too many files have changed in this diff Show More