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-scrollbars-custom": "^4.0.21",
"react-sortablejs": "^2.0.11", "react-sortablejs": "^2.0.11",
"react-split-pane": "^0.1.91", "react-split-pane": "^0.1.91",
"react-table": "^7.0.0", "react-table": "^7.6.3",
"react-table-sticky": "^1.1.2", "react-table-sticky": "^1.1.2",
"react-transition-group": "^4.4.1", "react-transition-group": "^4.4.1",
"react-use": "^13.26.1", "react-use": "^13.26.1",

View File

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

View File

@@ -1,18 +1,29 @@
import React from 'react'; import React, { useEffect, Suspense } from 'react';
import { Route, Switch } from 'react-router-dom'; import { Route, Switch } from 'react-router-dom';
import routes from 'routes/dashboard' import routes from 'routes/dashboard';
import DashboardPage from './DashboardPage';
/**
* Dashboard content route.
*/
export default function DashboardContentRoute() { export default function DashboardContentRoute() {
return ( return (
<Route pathname="/"> <Route pathname="/">
<Switch> <Switch>
{ routes.map((route, index) => ( {routes.map((route, index) => (
<Route <Route
exact={route.exact} exact={route.exact}
key={index} key={index}
path={`${route.path}`} path={`${route.path}`}
component={route.component} /> >
<DashboardPage
Component={route.component}
pageTitle={route.pageTitle}
backLink={route.backLink}
sidebarShrink={route.sidebarShrink}
/>
</Route>
))} ))}
</Switch> </Switch>
</Route> </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'; import React from 'react';
/**
* Dashboard page content.
*/
export default function DashboardPageContent({ children }) { export default function DashboardPageContent({ children }) {
return ( return (
<div class="dashboard__page-content"> <div class="dashboard__page-content">

View File

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

View File

@@ -1,10 +1,7 @@
import React from 'react'; import React from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
import { CLASSES } from 'common/classes'; import { CLASSES } from 'common/classes';
import { import { DataTable, If } from 'components';
DataTable,
If,
} from 'components';
import 'style/components/DataTable/DataTableEditable.scss'; import 'style/components/DataTable/DataTableEditable.scss';
export default function DatatableEditable({ export default function DatatableEditable({
@@ -14,11 +11,7 @@ export default function DatatableEditable({
...tableProps ...tableProps
}) { }) {
return ( return (
<div <div className={classNames(CLASSES.DATATABLE_EDITOR, className)}>
className={classNames(CLASSES.DATATABLE_EDITOR, {
[`${CLASSES.DATATABLE_EDITOR_HAS_TOTAL_ROW}`]: totalRow,
}, className)}
>
<DataTable {...tableProps} /> <DataTable {...tableProps} />
<If condition={actions}> <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 React from 'react';
import AccountFormDialog from 'containers/Dialogs/AccountFormDialog'; import AccountDialog from 'containers/Dialogs/AccountDialog';
import InviteUserDialog from 'containers/Dialogs/InviteUserDialog'; import InviteUserDialog from 'containers/Dialogs/InviteUserDialog';
import ItemCategoryDialog from 'containers/Dialogs/ItemCategoryDialog'; import ItemCategoryDialog from 'containers/Dialogs/ItemCategoryDialog';
import CurrencyFormDialog from 'containers/Dialogs/CurrencyFormDialog'; import CurrencyFormDialog from 'containers/Dialogs/CurrencyFormDialog';
@@ -19,7 +19,7 @@ import PaymentViaVoucherDialog from 'containers/Dialogs/PaymentViaVoucherDialog'
export default function DialogsContainer() { export default function DialogsContainer() {
return ( return (
<div> <div>
{/* <AccountFormDialog dialogName={'account-form'} /> */} <AccountDialog dialogName={'account-form'} />
<JournalNumberDialog dialogName={'journal-number-form'} /> <JournalNumberDialog dialogName={'journal-number-form'} />
<PaymentReceiveNumberDialog dialogName={'payment-receive-number-form'} /> <PaymentReceiveNumberDialog dialogName={'payment-receive-number-form'} />
<EstimateNumberDialog dialogName={'estimate-number-form'} /> <EstimateNumberDialog dialogName={'estimate-number-form'} />

View File

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

View File

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

View File

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

View File

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

View File

@@ -10,79 +10,70 @@ import {
MenuItem, MenuItem,
} from '@blueprintjs/core'; } from '@blueprintjs/core';
import { FormattedMessage as T } from 'react-intl'; import { FormattedMessage as T } from 'react-intl';
import { useFormikContext } from 'formik';
import { CLASSES } from 'common/classes'; import { CLASSES } from 'common/classes';
import classNames from 'classnames'; import classNames from 'classnames';
import { saveInvoke } from 'utils';
import { If, Icon } from 'components'; import { If, Icon } from 'components';
import { useMakeJournalFormContext } from './MakeJournalProvider';
import { useHistory } from 'react-router-dom';
/** /**
* Make Journal floating actions bar. * Make Journal floating actions bar.
*/ */
export default function MakeJournalEntriesFooter({ export default function MakeJournalEntriesFooter() {
isSubmitting, const history = useHistory();
onSubmitClick,
onCancelClick, // Formik context.
manualJournalId, const { isSubmitting, submitForm } = useFormikContext();
onSubmitForm,
onResetForm, // Make journal form context.
manualJournalPublished, const {
}) { manualJournalId,
setSubmitPayload,
manualJournalPublished = false,
} = useMakeJournalFormContext();
// Handle `submit & publish` button click.
const handleSubmitPublishBtnClick = (event) => { const handleSubmitPublishBtnClick = (event) => {
saveInvoke(onSubmitClick, event, { setSubmitPayload({ redirect: true, publish: true });
redirect: true, submitForm();
publish: true,
});
}; };
// Handle `submit, publish & new` button click.
const handleSubmitPublishAndNewBtnClick = (event) => { const handleSubmitPublishAndNewBtnClick = (event) => {
onSubmitForm(); setSubmitPayload({ redirect: false, publish: true, resetForm: true });
saveInvoke(onSubmitClick, event, { submitForm();
redirect: false,
publish: true,
resetForm: true,
});
}; };
// Handle `submit, publish & continue editing` button click.
const handleSubmitPublishContinueEditingBtnClick = (event) => { const handleSubmitPublishContinueEditingBtnClick = (event) => {
onSubmitForm(); setSubmitPayload({ redirect: false, publish: true });
saveInvoke(onSubmitClick, event, { submitForm();
redirect: false,
publish: true,
});
}; };
// Handle `submit as draft` button click.
const handleSubmitDraftBtnClick = (event) => { const handleSubmitDraftBtnClick = (event) => {
saveInvoke(onSubmitClick, event, { setSubmitPayload({ redirect: true, publish: false });
redirect: true,
publish: false,
});
}; };
// Handle `submit as draft & new` button click.
const handleSubmitDraftAndNewBtnClick = (event) => { const handleSubmitDraftAndNewBtnClick = (event) => {
onSubmitForm(); setSubmitPayload({ redirect: false, publish: false, resetForm: true });
saveInvoke(onSubmitClick, event, { submitForm();
redirect: false,
publish: false,
resetForm: true,
});
}; };
// Handles submit as draft & continue editing button click.
const handleSubmitDraftContinueEditingBtnClick = (event) => { const handleSubmitDraftContinueEditingBtnClick = (event) => {
onSubmitForm(); setSubmitPayload({ redirect: false, publish: false });
saveInvoke(onSubmitClick, event, { submitForm();
redirect: false,
publish: false,
});
}; };
// Handle cancel button action click.
const handleCancelBtnClick = (event) => { const handleCancelBtnClick = (event) => {
saveInvoke(onCancelClick, event); history.goBack();
}; };
const handleClearBtnClick = (event) => { const handleClearBtnClick = (event) => {};
// saveInvoke(onClearClick, event);
onResetForm();
};
return ( return (
<div className={classNames(CLASSES.PAGE_FORM_FLOATING_ACTIONS)}> <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 { Formik, Form } from 'formik';
import moment from 'moment';
import { Intent } from '@blueprintjs/core'; import { Intent } from '@blueprintjs/core';
import { useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
import { pick, defaultTo } from 'lodash'; import { defaultTo, isEmpty } from 'lodash';
import classNames from 'classnames'; import classNames from 'classnames';
import { useHistory } from 'react-router-dom'; import { useHistory } from 'react-router-dom';
@@ -15,56 +14,30 @@ import {
import MakeJournalEntriesHeader from './MakeJournalEntriesHeader'; import MakeJournalEntriesHeader from './MakeJournalEntriesHeader';
import MakeJournalFormFloatingActions from './MakeJournalFormFloatingActions'; import MakeJournalFormFloatingActions from './MakeJournalFormFloatingActions';
import MakeJournalEntriesField from './MakeJournalEntriesField'; import MakeJournalEntriesField from './MakeJournalEntriesField';
import MakeJournalNumberWatcher from './MakeJournalNumberWatcher';
import MakeJournalFormFooter from './MakeJournalFormFooter'; import MakeJournalFormFooter from './MakeJournalFormFooter';
import withDashboardActions from 'containers/Dashboard/withDashboardActions';
import withSettings from 'containers/Settings/withSettings'; import withSettings from 'containers/Settings/withSettings';
import AppToaster from 'components/AppToaster'; import AppToaster from 'components/AppToaster';
import withMediaActions from 'containers/Media/withMediaActions'; import withMediaActions from 'containers/Media/withMediaActions';
import { compose, orderingLinesIndexes, transactionNumber } from 'utils';
import { import {
compose, transformErrors,
repeatValue, transformToEditForm,
orderingLinesIndexes, defaultManualJournal,
defaultToTransform, } from './utils';
transactionNumber,
} from 'utils';
import { transformErrors } from './utils';
import { useMakeJournalFormContext } from './MakeJournalProvider'; 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. * Journal entries form.
*/ */
function MakeJournalEntriesForm({ function MakeJournalEntriesForm({
// #withDashboard
changePageTitle,
changePageSubtitle,
// #withSettings // #withSettings
journalNextNumber, journalNextNumber,
journalNumberPrefix, journalNumberPrefix,
baseCurrency, baseCurrency,
}) { }) {
// Journal form context.
const { const {
createJournalMutate, createJournalMutate,
editJournalMutate, editJournalMutate,
@@ -81,58 +54,23 @@ function MakeJournalEntriesForm({
journalNumberPrefix, journalNumberPrefix,
journalNextNumber, journalNextNumber,
); );
// Changes the page title based on the form in new and edit mode. // Form initial values.
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,
]);
const initialValues = useMemo( const initialValues = useMemo(
() => ({ () => ({
...(manualJournal ...(!isEmpty(manualJournal)
? { ? {
...pick(manualJournal, Object.keys(defaultInitialValues)), ...transformToEditForm(manualJournal),
entries: manualJournal.entries.map((entry) => ({
...pick(entry, Object.keys(defaultEntry)),
})),
} }
: { : {
...defaultInitialValues, ...defaultManualJournal,
journal_number: defaultTo(journalNumber, ''), journal_number: defaultTo(journalNumber, ''),
currency_code: defaultTo(baseCurrency, ''), currency_code: defaultTo(baseCurrency, ''),
entries: orderingLinesIndexes(defaultInitialValues.entries), entries: orderingLinesIndexes(defaultManualJournal.entries),
}), }),
}), }),
[manualJournal, baseCurrency, journalNumber], [manualJournal, baseCurrency, journalNumber],
); );
// Handle journal number field change.
const handleJournalNumberChanged = useCallback(
(journalNumber) => {
changePageSubtitle(
defaultToTransform(journalNumber, `No. ${journalNumber}`, ''),
);
},
[changePageSubtitle],
);
// Handle the form submiting. // Handle the form submiting.
const handleSubmit = (values, { setErrors, setSubmitting, resetForm }) => { const handleSubmit = (values, { setErrors, setSubmitting, resetForm }) => {
setSubmitting(true); setSubmitting(true);
@@ -170,8 +108,12 @@ function MakeJournalEntriesForm({
const form = { ...values, publish: submitPayload.publish, entries }; const form = { ...values, publish: submitPayload.publish, entries };
// Handle the request error. // Handle the request error.
const handleError = (error) => { const handleError = ({
transformErrors(error, { setErrors }); response: {
data: { errors },
},
}) => {
transformErrors(errors, { setErrors });
setSubmitting(false); setSubmitting(false);
}; };
@@ -201,7 +143,7 @@ function MakeJournalEntriesForm({
if (isNewMode) { if (isNewMode) {
createJournalMutate(form).then(handleSuccess).catch(handleError); createJournalMutate(form).then(handleSuccess).catch(handleError);
} else { } else {
editJournalMutate(manualJournal.id, form) editJournalMutate([manualJournal.id, form])
.then(handleSuccess) .then(handleSuccess)
.catch(handleError); .catch(handleError);
} }
@@ -221,11 +163,8 @@ function MakeJournalEntriesForm({
onSubmit={handleSubmit} onSubmit={handleSubmit}
> >
<Form> <Form>
<MakeJournalEntriesHeader <MakeJournalEntriesHeader />
onJournalNumberChanged={handleJournalNumberChanged} <MakeJournalEntriesField />
/>
<MakeJournalNumberWatcher journalNumber={journalNumber} />
<MakeJournalEntriesField defaultRow={defaultEntry} />
<MakeJournalFormFooter /> <MakeJournalFormFooter />
<MakeJournalFormFloatingActions /> <MakeJournalFormFloatingActions />
</Form> </Form>
@@ -235,7 +174,6 @@ function MakeJournalEntriesForm({
} }
export default compose( export default compose(
withDashboardActions,
withMediaActions, withMediaActions,
withSettings(({ manualJournalsSettings, organizationSettings }) => ({ withSettings(({ manualJournalsSettings, organizationSettings }) => ({
journalNextNumber: parseInt(manualJournalsSettings?.nextNumber, 10), journalNextNumber: parseInt(manualJournalsSettings?.nextNumber, 10),

View File

@@ -1,62 +1,20 @@
import React, { useCallback, useEffect } from 'react'; import React from 'react';
import { useParams, useHistory } from 'react-router-dom'; import { useParams } from 'react-router-dom';
import MakeJournalEntriesForm from './MakeJournalEntriesForm'; import MakeJournalEntriesForm from './MakeJournalEntriesForm';
import { MakeJournalProvider } from './MakeJournalProvider'; import { MakeJournalProvider } from './MakeJournalProvider';
import withDashboardActions from 'containers/Dashboard/withDashboardActions';
import { compose } from 'utils';
import 'style/pages/ManualJournal/MakeJournal.scss'; import 'style/pages/ManualJournal/MakeJournal.scss';
/** /**
* Make journal entries page. * Make journal entries page.
*/ */
function MakeJournalEntriesPage({ export default function MakeJournalEntriesPage() {
// #withDashboardActions
setSidebarShrink,
resetSidebarPreviousExpand,
setDashboardBackLink,
}) {
const history = useHistory();
const { id: journalId } = useParams(); 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 ( return (
<MakeJournalProvider journalId={journalId}> <MakeJournalProvider journalId={journalId}>
<MakeJournalEntriesForm <MakeJournalEntriesForm />
onFormSubmit={handleFormSubmit}
onCancelForm={handleCancel}
/>
</MakeJournalProvider> </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 { Button } from '@blueprintjs/core';
import { FormattedMessage as T, useIntl } from 'react-intl'; import { FormattedMessage as T } from 'react-intl';
import { omit } from 'lodash'; import { saveInvoke, removeRowsByIndex } from 'utils';
import { saveInvoke } from 'utils';
import {
AccountsListFieldCell,
MoneyFieldCell,
InputGroupCell,
ContactsListFieldCell,
} from 'components/DataTableCells';
import {
ContactHeaderCell,
ActionsCellRenderer,
TotalAccountCellRenderer,
TotalCreditDebitCellRenderer,
NoteCellRenderer,
} from './components';
import { DataTableEditable } from 'components'; import { DataTableEditable } from 'components';
import withAlertActions from 'containers/Alert/withAlertActions';
import { updateDataReducer } from './utils'; import { updateDataReducer } from './utils';
import { useMakeJournalFormContext } from './MakeJournalProvider'; 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. * Make journal entries table component.
*/ */
export default function MakeJournalEntriesTable({ function MakeJournalEntriesTable({
// #withAlertsActions
openAlert,
// #ownPorps // #ownPorps
onClickRemoveRow,
onClickAddNewRow,
onClickClearAllLines,
onChange, onChange,
entries, entries,
defaultEntry,
error, error,
initialLinesNumber = 4,
minLinesNumber = 4,
}) { }) {
const [rows, setRows] = useState([]);
const { formatMessage } = useIntl();
const { accounts, customers } = useMakeJournalFormContext(); 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. // Memorized data table columns.
const columns = useMemo( const columns = useJournalTableEntriesColumns();
() => [
{
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],
);
// Handles click new line. // Handles click new line.
const onClickNewRow = () => { const onClickNewRow = () => {
saveInvoke(onClickAddNewRow); const newRows = [...entries, defaultEntry];
saveInvoke(onChange, newRows);
}; };
// Handles update datatable data. // Handles update datatable data.
const handleUpdateData = (rowIndex, columnId, value) => { const handleUpdateData = (rowIndex, columnId, value) => {
const newRows = updateDataReducer(rows, rowIndex, columnId, value); const newRows = updateDataReducer(entries, rowIndex, columnId, value);
saveInvoke(onChange, newRows);
saveInvoke(
onChange,
newRows
.filter((row) => row.rowType === 'editor')
.map((row) => ({
...omit(row, ['rowType']),
})),
);
}; };
// Handle remove datatable row. // Handle remove datatable row.
const handleRemoveRow = (rowIndex) => { const handleRemoveRow = (rowIndex) => {
// Can't continue if there is just one row line or less. const newRows = removeRowsByIndex(entries, rowIndex);
if (rows.length <= 2) { saveInvoke(onChange, newRows);
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);
}; };
// Rows class names callback. // Handle clear all lines action.
const rowClassNames = useCallback(
(row) => ({
'row--total': rows.length === row.index + 2,
}),
[rows],
);
const handleClickClearAllLines = () => { 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 ( return (
<DataTableEditable <>
columns={columns} <DataTableEditable
data={tableRows} columns={columns}
rowClassNames={rowClassNames} data={entries}
sticky={true} sticky={true}
totalRow={true} totalRow={true}
payload={{ payload={{
accounts, accounts,
errors: error, errors: error,
updateData: handleUpdateData, updateData: handleUpdateData,
removeRow: handleRemoveRow, removeRow: handleRemoveRow,
contacts: [ contacts: customers.map((customer) => ({
...customers.map((customer) => ({
...customer, ...customer,
contact_type: 'customer', contact_type: 'customer',
})), })),
], autoFocus: ['account_id', 0],
autoFocus: ['account_id', 0], }}
}} actions={
actions={ <>
<> <Button
<Button small={true}
small={true} className={'button--secondary button--new-line'}
className={'button--secondary button--new-line'} onClick={onClickNewRow}
onClick={onClickNewRow} >
> <T id={'new_lines'} />
<T id={'new_lines'} /> </Button>
</Button>
<Button <Button
small={true} small={true}
className={'button--secondary button--clear-lines ml1'} className={'button--secondary button--clear-lines ml1'}
onClick={handleClickClearAllLines} onClick={handleClickClearAllLines}
> >
<T id={'clear_all_lines'} /> <T id={'clear_all_lines'} />
</Button> </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}> <If condition={!manualJournal || !manualJournal?.is_published}>
<ButtonGroup> <ButtonGroup>
<Button <Button
disabled={isSubmitting}
loading={isSubmitting} loading={isSubmitting}
disabled={isSubmitting}
intent={Intent.PRIMARY} intent={Intent.PRIMARY}
onClick={handleSubmitPublishBtnClick} onClick={handleSubmitPublishBtnClick}
text={<T id={'save_publish'} />} text={<T id={'save_publish'} />}
@@ -146,6 +146,7 @@ export default function MakeJournalFloatingAction() {
<If condition={manualJournal && manualJournal?.is_published}> <If condition={manualJournal && manualJournal?.is_published}>
<ButtonGroup> <ButtonGroup>
<Button <Button
loading={isSubmitting}
disabled={isSubmitting} disabled={isSubmitting}
intent={Intent.PRIMARY} intent={Intent.PRIMARY}
onClick={handleSubmitPublishBtnClick} 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. // Loading the journal settings.
const { isFetching: isSettingsLoading } = useSettings(); const { isFetching: isSettingsLoading } = useSettings();
// Submit form payload.
const [submitPayload, setSubmitPayload] = useState({}); const [submitPayload, setSubmitPayload] = useState({});
const provider = { const provider = {

View File

@@ -1,7 +1,14 @@
import React from 'react'; import React from 'react';
import { Position } from '@blueprintjs/core'; import { Intent, Position, Button, Tooltip } from '@blueprintjs/core';
import { FormattedMessage as T } from 'react-intl'; import { FormattedMessage as T, useIntl } from 'react-intl';
import { Money, Hint } from 'components'; import { Icon, Money, Hint } from 'components';
import {
AccountsListFieldCell,
MoneyFieldCell,
InputGroupCell,
ContactsListFieldCell,
} from 'components/DataTableCells';
import { safeSumBy } from 'utils';
/** /**
* Contact header cell. * Contact header cell.
@@ -19,73 +26,137 @@ export function ContactHeaderCell() {
} }
/** /**
* Total text cell renderer. * Account footer cell.
*/ */
export const TotalAccountCellRenderer = (chainedComponent) => (props) => { function AccountFooterCell() {
if (props.data.length === props.row.index + 1) { return <span>{'Total USD'}</span>;
return <span>{'Total USD'}</span>; }
}
return chainedComponent(props);
};
/** /**
* Total credit/debit cell renderer. * Total credit table footer cell.
*/ */
export const TotalCreditDebitCellRenderer = (chainedComponent, type) => ( function TotalCreditFooterCell({ rows }) {
props, const credit = safeSumBy(rows, 'original.credit');
) => {
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; return (
}, 0); <span>
<Money amount={credit} currency={'USD'} />
return ( </span>
<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);
};
/**
* 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. * Actions cell renderer.
*/ */
export const ActionsCellRenderer = ({ export const ActionsCellRenderer = ({
row: { index }, row: { index },
column: { id }, column: { id },
cell: { value: initialValue }, cell: { value: initialValue },
data, data,
payload, payload,
}) => { }) => {
if (data.length <= index + 1) { const onClickRemoveRole = () => {
return ''; payload.removeRow(index);
}
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>
);
}; };
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 React from 'react';
import { Intent } from '@blueprintjs/core'; import { Intent } from '@blueprintjs/core';
import { sumBy, setWith, toSafeInteger, get } from 'lodash'; 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 { AppToaster } from 'components';
import { formatMessage } from 'services/intl'; import { formatMessage } from 'services/intl';
@@ -17,6 +18,44 @@ const ERROR = {
ENTRIES_SHOULD_ASSIGN_WITH_CONTACT: 'ENTRIES_SHOULD_ASSIGN_WITH_CONTACT', 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. * Entries adjustment.
*/ */
@@ -30,6 +69,9 @@ function adjustmentEntries(entries) {
}; };
} }
/**
*
*/
export const updateDataReducer = (rows, rowIndex, columnId, value) => { export const updateDataReducer = (rows, rowIndex, columnId, value) => {
let newRows = transformUpdatedRows(rows, rowIndex, columnId, value); let newRows = transformUpdatedRows(rows, rowIndex, columnId, value);
@@ -59,7 +101,9 @@ export const updateDataReducer = (rows, rowIndex, columnId, value) => {
return newRows; return newRows;
}; };
// Transform API errors in toasts messages. /**
* Transform API errors in toasts messages.
*/
export const transformErrors = (resErrors, { setErrors, errors }) => { export const transformErrors = (resErrors, { setErrors, errors }) => {
const getError = (errorType) => resErrors.find((e) => e.type === errorType); const getError = (errorType) => resErrors.find((e) => e.type === errorType);
const toastMessages = []; const toastMessages = [];

View File

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

View File

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

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

View File

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

View File

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

View File

@@ -1,13 +1,11 @@
import React from 'react'; import React from 'react';
import classNames from 'classnames';
import { useHistory } from 'react-router-dom'; import { useHistory } from 'react-router-dom';
import CustomersEmptyStatus from './CustomersEmptyStatus'; import CustomersEmptyStatus from './CustomersEmptyStatus';
import TableSkeletonRows from 'components/Datatable/TableSkeletonRows'; import TableSkeletonRows from 'components/Datatable/TableSkeletonRows';
import TableSkeletonHeader from 'components/Datatable/TableHeaderSkeleton'; import TableSkeletonHeader from 'components/Datatable/TableHeaderSkeleton';
import { DataTable, Choose } from 'components'; import { DataTable } from 'components';
import { CLASSES } from 'common/classes';
import withCustomersActions from './withCustomersActions'; import withCustomersActions from './withCustomersActions';
import withAlertsActions from 'containers/Alert/withAlertActions'; import withAlertsActions from 'containers/Alert/withAlertActions';
@@ -26,7 +24,7 @@ function CustomersTable({
setCustomersTableState, setCustomersTableState,
// #withAlerts // #withAlerts
openAlert openAlert,
}) { }) {
const history = useHistory(); const history = useHistory();
@@ -56,7 +54,7 @@ function CustomersTable({
// Handles the customer delete action. // Handles the customer delete action.
const handleCustomerDelete = (customer) => { const handleCustomerDelete = (customer) => {
openAlert('customer-delete', { customerId: customer.id }) openAlert('customer-delete', { customerId: customer.id });
}; };
// Handle the customer edit action. // Handle the customer edit action.
@@ -64,52 +62,39 @@ function CustomersTable({
history.push(`/customers/${customer.id}/edit`); history.push(`/customers/${customer.id}/edit`);
}; };
if (isEmptyStatus) {
return <CustomersEmptyStatus />;
}
return ( return (
<div className={classNames(CLASSES.DASHBOARD_DATATABLE)}> <DataTable
<Choose> noInitialFetch={true}
<Choose.When condition={isEmptyStatus}> columns={columns}
<CustomersEmptyStatus /> data={customers}
</Choose.When> loading={isCustomersLoading}
headerLoading={isCustomersLoading}
<Choose.Otherwise> progressBarLoading={isCustomersFetching}
<DataTable onFetchData={handleFetchData}
noInitialFetch={true} selectionColumn={true}
columns={columns} expandable={false}
data={customers} sticky={true}
spinnerProps={{ size: 30 }}
loading={isCustomersLoading} pagination={true}
headerLoading={isCustomersLoading} manualSortBy={true}
progressBarLoading={isCustomersFetching} manualPagination={true}
pagesCount={pagination.pagesCount}
onFetchData={handleFetchData} autoResetSortBy={false}
selectionColumn={true} autoResetPage={false}
expandable={false} TableLoadingRenderer={TableSkeletonRows}
sticky={true} TableHeaderSkeletonRenderer={TableSkeletonHeader}
payload={{
spinnerProps={{ size: 30 }} onDelete: handleCustomerDelete,
onEdit: handleCustomerEdit,
pagination={true} }}
manualSortBy={true} ContextMenu={ActionsMenu}
manualPagination={true} />
pagesCount={pagination.pagesCount}
autoResetSortBy={false}
autoResetPage={false}
TableLoadingRenderer={TableSkeletonRows}
TableHeaderSkeletonRenderer={TableSkeletonHeader}
payload={{
onDelete: handleCustomerDelete,
onEdit: handleCustomerEdit,
}}
ContextMenu={ActionsMenu}
/>
</Choose.Otherwise>
</Choose>
</div>
); );
}; }
export default compose( export default compose(
withAlertsActions, 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 { Formik } from 'formik';
import { useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
import { omit } from 'lodash'; 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 withDialogActions from 'containers/Dialog/withDialogActions';
import { import {
EditAccountFormSchema, EditAccountFormSchema,
CreateAccountFormSchema, CreateAccountFormSchema,
} from './AccountForm.schema'; } from './AccountForm.schema';
import {
useAccounts,
useAccountsTypes,
useCreateAccount,
useAccount,
useEditAccount
} from 'hooks/query';
import { compose, transformToForm } from 'utils'; import { compose, transformToForm } from 'utils';
import { transformApiErrors, transformAccountToForm } from './utils'; import { transformApiErrors, transformAccountToForm } from './utils';
import 'style/pages/Accounts/AccountFormDialog.scss'; import 'style/pages/Accounts/AccountFormDialog.scss';
import { useAccountDialogContext } from './AccountDialogProvider';
// Default initial form values. // Default initial form values.
const defaultInitialValues = { const defaultInitialValues = {
@@ -41,43 +34,28 @@ const defaultInitialValues = {
function AccountFormDialogContent({ function AccountFormDialogContent({
// #withDialogActions // #withDialogActions
closeDialog, closeDialog,
// #ownProp
dialogName,
accountId,
action,
parentAccountId,
accountType,
}) { }) {
const { formatMessage } = useIntl(); 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. // Form validation schema in create and edit mode.
const validationSchema = isNewMode const validationSchema = isNewMode
? CreateAccountFormSchema ? CreateAccountFormSchema
: EditAccountFormSchema; : 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. // Callbacks handles form submit.
const handleFormSubmit = (values, { setSubmitting, setErrors }) => { const handleFormSubmit = (values, { setSubmitting, setErrors }) => {
const form = omit(values, ['subaccount']); const form = omit(values, ['subaccount']);
@@ -106,18 +84,22 @@ function AccountFormDialogContent({
}; };
// Handle request error. // Handle request error.
const handleError = (error) => { const handleError = (error) => {
const { response: { data: { errors } } } = error; const {
response: {
data: { errors },
},
} = error;
const errorsTransformed = transformApiErrors(errors); const errorsTransformed = transformApiErrors(errors);
setErrors({ ...errorsTransformed }); setErrors({ ...errorsTransformed });
setSubmitting(false); setSubmitting(false);
}; };
if (accountId) { if (accountId) {
editAccountMutate(accountId, form) editAccountMutate([accountId, form]).then(handleSuccess).catch(handleError);
} else {
createAccountMutate({ ...form })
.then(handleSuccess) .then(handleSuccess)
.catch(handleError); .catch(handleError);
} else {
createAccountMutate({ ...form }).then(handleSuccess).catch(handleError);
} }
}; };
@@ -144,30 +126,19 @@ function AccountFormDialogContent({
closeDialog(dialogName); closeDialog(dialogName);
}, [closeDialog, dialogName]); }, [closeDialog, dialogName]);
const isFetching =
isAccountsLoading ||
isAccountsTypesLoading ||
isAccountLoading;
return ( return (
<DialogContent isLoading={isFetching}> <Formik
<Formik validationSchema={validationSchema}
validationSchema={validationSchema} initialValues={initialValues}
initialValues={initialValues} onSubmit={handleFormSubmit}
onSubmit={handleFormSubmit} >
> <AccountDialogFormContent
<AccountFormDialogFields dialogName={dialogName}
accounts={accounts} action={action}
accountsTypes={accountsTypes} onClose={handleClose}
dialogName={dialogName} />
action={action} </Formik>
onClose={handleClose}
/>
</Formik>
</DialogContent>
); );
} }
export default compose( export default compose(withDialogActions)(AccountFormDialogContent);
withDialogActions,
)(AccountFormDialogContent);

View File

@@ -23,6 +23,7 @@ import withAccounts from 'containers/Accounts/withAccounts';
import { inputIntent } from 'utils'; import { inputIntent } from 'utils';
import { compose } from 'redux'; import { compose } from 'redux';
import { useAutofocus } from 'hooks'; import { useAutofocus } from 'hooks';
import { useAccountDialogContext } from './AccountDialogProvider';
/** /**
* Account form dialogs fields. * Account form dialogs fields.
@@ -31,12 +32,13 @@ function AccountFormDialogFields({
// #ownProps // #ownProps
onClose, onClose,
action, action,
accounts,
accountsTypes,
}) { }) {
const { values, isSubmitting } = useFormikContext(); const { values, isSubmitting } = useFormikContext();
const accountNameFieldRef = useAutofocus(); const accountNameFieldRef = useAutofocus();
// Account form context.
const { accounts, accountsTypes } = useAccountDialogContext();
return ( return (
<Form> <Form>
<div className={Classes.DIALOG_BODY}> <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 withDialogRedux from 'components/DialogReduxConnect';
import { compose } from 'utils'; import { compose } from 'utils';
const AccountFormDialogContent = lazy(() => import('./AccountFormDialogContent')); const AccountDialogContent = lazy(() => import('./AccountDialogContent'));
/** /**
* Account form dialog. * Account form dialog.
@@ -28,7 +28,7 @@ function AccountFormDialog({
isOpen={isOpen} isOpen={isOpen}
> >
<DialogSuspense> <DialogSuspense>
<AccountFormDialogContent <AccountDialogContent
dialogName={dialogName} dialogName={dialogName}
accountId={payload.id} accountId={payload.id}
action={payload.action} action={payload.action}

View File

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

View File

@@ -1,9 +1,6 @@
import React, { useState } from 'react'; import React from 'react';
import { FastField, useFormikContext } from 'formik'; import { FastField } from 'formik';
import { Alert, Intent } from '@blueprintjs/core';
import { FormattedMessage as T } from 'react-intl';
import ItemsEntriesTable from './ItemsEntriesTable'; import ItemsEntriesTable from './ItemsEntriesTable';
import { orderingLinesIndexes, repeatValue } from 'utils';
import 'style/components/DataTable/DataTableEditable.scss'; import 'style/components/DataTable/DataTableEditable.scss';
@@ -12,84 +9,11 @@ import 'style/components/DataTable/DataTableEditable.scss';
*/ */
export default function EditableItemsEntriesTable({ export default function EditableItemsEntriesTable({
items, items,
defaultEntry,
minLinesNumber = 2,
linesNumber = 5,
filterSellableItems = false, filterSellableItems = false,
filterPurchasableItems = 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 ( 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>
</>
); );
} }

View File

@@ -1,251 +1,147 @@
import React, { useState, useMemo, useEffect, useCallback } from 'react'; import React, { useCallback } from 'react';
import { Button, Intent, Position, Tooltip } from '@blueprintjs/core'; import { Button } from '@blueprintjs/core';
import { FormattedMessage as T, useIntl } from 'react-intl'; import { FormattedMessage as T } from 'react-intl';
import classNames from 'classnames'; import classNames from 'classnames';
import ItemsEntriesDeleteAlert from 'containers/Alerts/ItemsEntries/ItemsEntriesDeleteAlert';
import withAlertActions from 'containers/Alert/withAlertActions';
import { CLASSES } from 'common/classes'; import { CLASSES } from 'common/classes';
import { Hint, Icon, DataTableEditable } from 'components'; import { DataTableEditable } from 'components';
import { useEditableItemsEntriesColumns } from './components';
import { import {
InputGroupCell, saveInvoke,
MoneyFieldCell, updateTableRow,
ItemsListCell, repeatValue,
PercentFieldCell, removeRowsByIndex,
DivFieldCell, compose,
} from 'components/DataTableCells'; } from 'utils';
import { formattedAmount, saveInvoke } from 'utils'; import { updateItemsEntriesTotal } from './utils';
// Actions cell renderer component. /**
const ActionsCellRenderer = ({ * Items entries table.
row: { index }, */
column: { id }, function ItemsEntriesTable({
cell: { value }, // #withAlertActions
data, openAlert,
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>
);
};
// Total cell renderer. // #ownProps
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
items, items,
entries, entries,
initialEntries,
defaultEntry,
errors, errors,
onUpdateData, onUpdateData,
onClickRemoveRow, linesNumber,
onClickAddNewRow,
onClickClearAllLines,
filterPurchasableItems = false,
filterSellableItems = false,
}) { }) {
const [rows, setRows] = useState([]); const [rows, setRows] = React.useState(initialEntries);
const { formatMessage } = useIntl();
useEffect(() => { // Allows to observes `entries` to make table rows outside controlled.
setRows([...entries.map((e) => ({ ...e }))]); React.useEffect(() => {
}, [entries]); if (entries && entries !== rows) {
setRows(entries);
}
}, [entries, rows]);
const columns = useMemo( // Editiable items entries columns.
() => [ const columns = useEditableItemsEntriesColumns();
{
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],
);
// Handles the editor data update.
const handleUpdateData = useCallback( const handleUpdateData = useCallback(
(rowIndex, columnId, value) => { (rowIndex, columnId, value) => {
const newRows = rows.map((row, index) => { const newRows = compose(
if (index === rowIndex) { updateTableRow(rowIndex, columnId, value),
const newRow = { ...rows[rowIndex], [columnId]: value }; updateItemsEntriesTotal,
return { )(entries);
...newRow,
total: calculateDiscount( setRows(newRows);
newRow.discount, onUpdateData(newRows);
newRow.quantity,
newRow.rate,
),
};
}
return row;
});
saveInvoke(onUpdateData, newRows);
}, },
[rows, onUpdateData], [entries, onUpdateData],
); );
const handleRemoveRow = useCallback( // Handle table rows removing by index.
(rowIndex) => { const handleRemoveRow = (rowIndex) => {
if (rows.length <= 1) { const newRows = removeRowsByIndex(rows, rowIndex);
return; setRows(newRows);
} saveInvoke(onUpdateData, newRows);
const removeIndex = parseInt(rowIndex, 10); };
saveInvoke(onClickRemoveRow, removeIndex);
},
[rows, onClickRemoveRow],
);
// Handle table rows adding a new row.
const onClickNewRow = (event) => { const onClickNewRow = (event) => {
saveInvoke(onClickAddNewRow, event); const newRows = [...rows, defaultEntry];
setRows(newRows);
saveInvoke(onUpdateData, newRows);
}; };
// Handle table clearing all rows.
const handleClickClearAllLines = (event) => { const handleClickClearAllLines = (event) => {
saveInvoke(onClickClearAllLines, event); openAlert('items-entries-clear-lines');
}; };
const rowClassNames = useCallback( /**
(row) => ({ * Handle alert confirm of clear all lines.
'row--total': rows.length === row.index + 1, */
}), const handleClearLinesAlertConfirm = () => {
[rows], const newRows = repeatValue(defaultEntry, linesNumber);
); setRows(newRows);
saveInvoke(onUpdateData, newRows);
};
return ( return (
<DataTableEditable <>
className={classNames(CLASSES.DATATABLE_EDITOR_ITEMS_ENTRIES)} <DataTableEditable
columns={columns} className={classNames(CLASSES.DATATABLE_EDITOR_ITEMS_ENTRIES)}
data={rows} columns={columns}
rowClassNames={rowClassNames} data={entries}
sticky={true} sticky={true}
payload={{ payload={{
items, items,
errors: errors || [], errors: errors || [],
updateData: handleUpdateData, updateData: handleUpdateData,
removeRow: handleRemoveRow, removeRow: handleRemoveRow,
autoFocus: ['item_id', 0], autoFocus: ['item_id', 0],
}} }}
actions={ actions={
<> <>
<Button <Button
small={true} small={true}
className={'button--secondary button--new-line'} className={'button--secondary button--new-line'}
onClick={onClickNewRow} onClick={onClickNewRow}
> >
<T id={'new_lines'} /> <T id={'new_lines'} />
</Button> </Button>
<Button <Button
small={true} small={true}
className={'button--secondary button--clear-lines ml1'} className={'button--secondary button--clear-lines ml1'}
onClick={handleClickClearAllLines} onClick={handleClickClearAllLines}
> >
<T id={'clear_all_lines'} /> <T id={'clear_all_lines'} />
</Button> </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'; } from '@blueprintjs/core';
import { useFormikContext } from 'formik'; import { useFormikContext } from 'formik';
import { FormattedMessage as T } from 'react-intl'; import { FormattedMessage as T } from 'react-intl';
import { useHistory } from 'react-router-dom';
import { CLASSES } from 'common/classes'; import { CLASSES } from 'common/classes';
import classNames from 'classnames'; import classNames from 'classnames';
import { saveInvoke } from 'utils';
import { Icon, If } from 'components'; import { Icon, If } from 'components';
import { useExpenseFormContext } from './ExpenseFormPageProvider';
/** /**
* Expense form floating actions. * Expense form floating actions.
*/ */
export default function ExpenseFloatingFooter({ export default function ExpenseFloatingFooter() {
isSubmitting, const history = useHistory();
onSubmitClick,
onCancelClick,
expense,
expensePublished,
}) {
const { submitForm, resetForm } = useFormikContext();
// Formik context.
const { isSubmitting, submitForm, resetForm } = useFormikContext();
// Expense form context.
const { setSubmitPayload, isNewMode } = useExpenseFormContext();
// Handle submit & publish button click.
const handleSubmitPublishBtnClick = (event) => { 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) => { const handleSubmitPublishAndNewBtnClick = (event) => {
setSubmitPayload({ redirect: false, publish: true, resetForm: true });
submitForm(); submitForm();
saveInvoke(onSubmitClick, event, {
redirect: false,
publish: true,
resetForm: true,
});
}; };
// Handle submit, publish & continue editing button click.
const handleSubmitPublishContinueEditingBtnClick = (event) => { const handleSubmitPublishContinueEditingBtnClick = (event) => {
setSubmitPayload({ redirect: false, publish: true });
submitForm(); submitForm();
saveInvoke(onSubmitClick, event, { redirect: false, publish: true });
}; };
// Handle submit as draft button click.
const handleSubmitDraftBtnClick = (event) => { 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) => { const handleSubmitDraftAndNewBtnClick = (event) => {
setSubmitPayload({ redirect: false, publish: false, resetForm: true });
submitForm(); submitForm();
saveInvoke(onSubmitClick, event, {
redirect: false,
publish: false,
resetForm: true,
});
}; };
// Handles submit as draft & continue editing button click.
const handleSubmitDraftContinueEditingBtnClick = (event) => { const handleSubmitDraftContinueEditingBtnClick = (event) => {
setSubmitPayload({ redirect: false, publish: false });
submitForm(); submitForm();
saveInvoke(onSubmitClick, event, { redirect: false, publish: false });
}; };
// Handle cancel button click.
const handleCancelBtnClick = (event) => { const handleCancelBtnClick = (event) => {
saveInvoke(onCancelClick, event); history.goBack();
}; };
// Handles clear form button click.
const handleClearBtnClick = (event) => { const handleClearBtnClick = (event) => {
resetForm(); resetForm();
}; };
@@ -75,10 +79,11 @@ export default function ExpenseFloatingFooter({
return ( return (
<div className={classNames(CLASSES.PAGE_FORM_FLOATING_ACTIONS)}> <div className={classNames(CLASSES.PAGE_FORM_FLOATING_ACTIONS)}>
{/* ----------- Save And Publish ----------- */} {/* ----------- Save And Publish ----------- */}
<If condition={!expense || !expensePublished}> <If condition={isNewMode}>
<ButtonGroup> <ButtonGroup>
<Button <Button
disabled={isSubmitting} disabled={isSubmitting}
loading={isSubmitting}
intent={Intent.PRIMARY} intent={Intent.PRIMARY}
onClick={handleSubmitPublishBtnClick} onClick={handleSubmitPublishBtnClick}
text={<T id={'save_publish'} />} text={<T id={'save_publish'} />}
@@ -140,10 +145,11 @@ export default function ExpenseFloatingFooter({
</ButtonGroup> </ButtonGroup>
</If> </If>
{/* ----------- Save and New ----------- */} {/* ----------- Save and New ----------- */}
<If condition={expense && expensePublished}> <If condition={!isNewMode}>
<ButtonGroup> <ButtonGroup>
<Button <Button
disabled={isSubmitting} disabled={isSubmitting}
loading={isSubmitting}
intent={Intent.PRIMARY} intent={Intent.PRIMARY}
onClick={handleSubmitPublishBtnClick} onClick={handleSubmitPublishBtnClick}
text={<T id={'save'} />} text={<T id={'save'} />}
@@ -174,7 +180,7 @@ export default function ExpenseFloatingFooter({
className={'ml1'} className={'ml1'}
disabled={isSubmitting} disabled={isSubmitting}
onClick={handleClearBtnClick} onClick={handleClearBtnClick}
text={expense ? <T id={'reset'} /> : <T id={'clear'} />} text={!isNewMode ? <T id={'reset'} /> : <T id={'clear'} />}
/> />
{/* ----------- Cancel ----------- */} {/* ----------- Cancel ----------- */}
<Button <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 { Intent } from '@blueprintjs/core';
import { useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
import { defaultTo, pick, sumBy } from 'lodash'; import { defaultTo, sumBy, isEmpty } from 'lodash';
import { Formik, Form } from 'formik'; import { Formik, Form } from 'formik';
import moment from 'moment';
import classNames from 'classnames'; import classNames from 'classnames';
import { useHistory } from 'react-router-dom'; import { useHistory } from 'react-router-dom';
import { CLASSES } from 'common/classes'; import { CLASSES } from 'common/classes';
@@ -24,80 +23,51 @@ import {
CreateExpenseFormSchema, CreateExpenseFormSchema,
EditExpenseFormSchema, EditExpenseFormSchema,
} from './ExpenseForm.schema'; } from './ExpenseForm.schema';
import { transformErrors } from './utils'; import { transformErrors, defaultExpense, transformToEditForm } from './utils';
import { compose, repeatValue, orderingLinesIndexes } from 'utils'; import { compose, 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)],
};
/** /**
* Expense form. * Expense form.
*/ */
function ExpenseForm({ function ExpenseForm({
// #withDashboard
changePageTitle,
// #withSettings // #withSettings
baseCurrency, baseCurrency,
preferredPaymentAccount, preferredPaymentAccount,
}) { }) {
// Expense form context.
const { const {
editExpenseMutate, editExpenseMutate,
createExpenseMutate, createExpenseMutate,
expense, expense,
expenseId, expenseId,
submitPayload,
} = useExpenseFormContext(); } = useExpenseFormContext();
const isNewMode = !expenseId; const isNewMode = !expenseId;
const [submitPayload, setSubmitPayload] = useState({});
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();
// History context.
const history = useHistory(); const history = useHistory();
useEffect(() => { // Form initial values.
if (isNewMode) {
changePageTitle(formatMessage({ id: 'new_expense' }));
} else {
changePageTitle(formatMessage({ id: 'edit_expense' }));
}
}, [changePageTitle, isNewMode, formatMessage]);
const initialValues = useMemo( const initialValues = useMemo(
() => ({ () => ({
...(expense ...(!isEmpty(expense)
? { ? {
...pick(expense, Object.keys(defaultInitialValues)), ...transformToEditForm(expense, defaultExpense),
categories: [
...expense.categories.map((category) => ({
...pick(category, Object.keys(defaultCategory)),
})),
],
} }
: { : {
...defaultInitialValues, ...defaultExpense,
currency_code: baseCurrency, currency_code: baseCurrency,
payment_account_id: defaultTo(preferredPaymentAccount, ''), payment_account_id: defaultTo(preferredPaymentAccount, ''),
categories: orderingLinesIndexes(defaultInitialValues.categories), categories: orderingLinesIndexes(defaultExpense.categories),
}), }),
}), }),
[expense, baseCurrency, preferredPaymentAccount], [
expense,
baseCurrency,
preferredPaymentAccount,
],
); );
// Handle form submit. // Handle form submit.
@@ -155,21 +125,11 @@ function ExpenseForm({
if (isNewMode) { if (isNewMode) {
createExpenseMutate(form).then(handleSuccess).catch(handleError); createExpenseMutate(form).then(handleSuccess).catch(handleError);
} else { } else {
editExpenseMutate(expense.id, form) editExpenseMutate([expense.id, form])
.then(handleSuccess) .then(handleSuccess)
.catch(handleError); .catch(handleError);
} }
}; };
const handleCancelClick = useCallback(() => {
history.goBack();
}, [history]);
const handleSubmitClick = useCallback(
(event, payload) => {
setSubmitPayload({ ...payload });
},
[setSubmitPayload],
);
return ( return (
<div <div
@@ -180,32 +140,24 @@ function ExpenseForm({
)} )}
> >
<Formik <Formik
validationSchema={isNewMode validationSchema={
? CreateExpenseFormSchema isNewMode ? CreateExpenseFormSchema : EditExpenseFormSchema
: EditExpenseFormSchema} }
initialValues={initialValues} initialValues={initialValues}
onSubmit={handleSubmit} onSubmit={handleSubmit}
> >
{({ isSubmitting, values }) => ( <Form>
<Form> <ExpenseFormHeader />
<ExpenseFormHeader /> <ExpenseFormBody />
<ExpenseFormBody /> <ExpenseFormFooter />
<ExpenseFormFooter /> <ExpenseFloatingFooter />
<ExpenseFloatingFooter </Form>
isSubmitting={isSubmitting}
expense={expenseId}
expensePublished={values.publish}
onCancelClick={handleCancelClick}
onSubmitClick={handleSubmitClick}
/>
</Form>
)}
</Formik> </Formik>
</div> </div>
); );
} }
export default compose( export default compose(
withDashboardActions, withDashboardActions,
withMediaActions, withMediaActions,
withSettings(({ organizationSettings, expenseSettings }) => ({ withSettings(({ organizationSettings, expenseSettings }) => ({

View File

@@ -3,7 +3,9 @@ import classNames from 'classnames';
import { CLASSES } from 'common/classes'; import { CLASSES } from 'common/classes';
import ExpenseFormEntriesField from './ExpenseFormEntriesField'; import ExpenseFormEntriesField from './ExpenseFormEntriesField';
export default function ExpenseFormBody() { export default function ExpenseFormBody({
}) {
return ( return (
<div className={classNames(CLASSES.PAGE_FORM_BODY)}> <div className={classNames(CLASSES.PAGE_FORM_BODY)}>
<ExpenseFormEntriesField /> <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 { FastField } from 'formik';
import React from 'react'; import React from 'react';
import ExpenseFormEntries from './ExpenseFormEntries'; import ExpenseFormEntriesTable from './ExpenseFormEntriesTable';
import { orderingLinesIndexes, repeatValue } from 'utils'; import { useExpenseFormContext } from './ExpenseFormPageProvider';
/**
* Expense form entries field.
*/
export default function ExpenseFormEntriesField({ export default function ExpenseFormEntriesField({
defaultRow,
linesNumber = 4, linesNumber = 4,
}) { }) {
const { defaultCategoryEntry } = useExpenseFormContext();
return ( return (
<FastField name={'categories'}> <FastField name={'categories'}>
{({ form, field: { value }, meta: { error, touched } }) => ( {({ form, field: { value }, meta: { error, touched } }) => (
<ExpenseFormEntries <ExpenseFormEntriesTable
entries={value} entries={value}
error={error} error={error}
onChange={(entries) => { onChange={(entries) => {
form.setFieldValue('categories', entries); form.setFieldValue('categories', entries);
}} }}
onClickAddNewRow={() => { defaultEntry={defaultCategoryEntry}
form.setFieldValue('categories', [...value, defaultRow]); linesNumber={linesNumber}
}}
onClickClearAllLines={() => {
form.setFieldValue(
'categories',
orderingLinesIndexes([...repeatValue(defaultRow, linesNumber)])
);
}}
/> />
)} )}
</FastField> </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 { Row, Dragzone, Col } from 'components';
import { CLASSES } from 'common/classes'; import { CLASSES } from 'common/classes';
export default function ExpenseFormFooter({}) { export default function ExpenseFormFooter() {
return ( return (
<div className={classNames(CLASSES.PAGE_FORM_FOOTER)}> <div className={classNames(CLASSES.PAGE_FORM_FOOTER)}>
<Row> <Row>

View File

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

View File

@@ -1,41 +1,17 @@
import React, { useCallback, useEffect } from 'react'; import React from 'react';
import { useParams, useHistory } from 'react-router-dom'; import { useParams } from 'react-router-dom';
import ExpenseForm from './ExpenseForm'; import ExpenseForm from './ExpenseForm';
import { ExpenseFormPageProvider } from './ExpenseFormPageProvider'; import { ExpenseFormPageProvider } from './ExpenseFormPageProvider';
import withDashboardActions from 'containers/Dashboard/withDashboardActions';
import { compose } from 'utils';
import 'style/pages/Expense/PageForm.scss'; import 'style/pages/Expense/PageForm.scss';
/** /**
* Expense page form. * Expense page form.
*/ */
function ExpenseFormPage({ export default function ExpenseFormPage() {
// #withDashboardActions
setSidebarShrink,
resetSidebarPreviousExpand,
setDashboardBackLink,
}) {
const history = useHistory();
const { id } = useParams(); 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 ( return (
<ExpenseFormPageProvider expenseId={id}> <ExpenseFormPageProvider expenseId={id}>
<ExpenseForm /> <ExpenseForm />
@@ -43,6 +19,3 @@ function ExpenseFormPage({
); );
} }
export default compose(
withDashboardActions,
)(ExpenseFormPage);

View File

@@ -24,7 +24,12 @@ function ExpenseFormPageProvider({ expenseId, ...props }) {
} = useCustomers(); } = useCustomers();
// Fetch the expense details. // Fetch the expense details.
const { data: expense, isFetching: isExpenseLoading } = useExpense(expenseId); const { data: expense, isFetching: isExpenseLoading } = useExpense(
expenseId,
{
enabled: !!expenseId,
},
);
// Fetch accounts list. // Fetch accounts list.
const { data: accounts, isFetching: isAccountsLoading } = useAccounts(); const { data: accounts, isFetching: isAccountsLoading } = useAccounts();
@@ -33,9 +38,17 @@ function ExpenseFormPageProvider({ expenseId, ...props }) {
const { mutateAsync: createExpenseMutate } = useCreateExpense(); const { mutateAsync: createExpenseMutate } = useCreateExpense();
const { mutateAsync: editExpenseMutate } = useEditExpense(); const { mutateAsync: editExpenseMutate } = useEditExpense();
// Submit form payload.
const [submitPayload, setSubmitPayload] = React.useState({});
//
const isNewMode = !expenseId;
// Provider payload. // Provider payload.
const provider = { const provider = {
isNewMode,
expenseId, expenseId,
submitPayload,
currencies, currencies,
customers, customers,
@@ -49,6 +62,7 @@ function ExpenseFormPageProvider({ expenseId, ...props }) {
createExpenseMutate, createExpenseMutate,
editExpenseMutate, editExpenseMutate,
setSubmitPayload,
}; };
return ( 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 = () => { const ExpenseCategoryHeaderCell = () => {
return ( return (
<> <>
@@ -10,7 +21,9 @@ const ExpenseCategoryHeaderCell = () => {
); );
}; };
// Actions cell renderer. /**
* Actions cell renderer.
*/
const ActionsCellRenderer = ({ const ActionsCellRenderer = ({
row: { index }, row: { index },
column: { id }, column: { id },
@@ -18,9 +31,6 @@ const ActionsCellRenderer = ({
data, data,
payload, payload,
}) => { }) => {
if (data.length <= index + 1) {
return '';
}
const onClickRemoveRole = () => { const onClickRemoveRole = () => {
payload.removeRow(index); payload.removeRow(index);
}; };
@@ -38,95 +48,76 @@ const ActionsCellRenderer = ({
); );
}; };
// Total text cell renderer. /**
const TotalExpenseCellRenderer = (chainedComponent) => (props) => { * Amount footer cell.
if (props.data.length <= props.row.index + 1) { */
return ( function AmountFooterCell({ rows }) {
<span> const total = safeSumBy(rows, 'original.amount');
<T id={'total_currency'} values={{ currency: 'USD' }} /> return <span>{formattedAmount(total, 'USD')}</span>;
</span> }
);
}
return chainedComponent(props);
};
/** /**
* Note cell renderer. * Expense account footer cell.
*/ */
const NoteCellRenderer = (chainedComponent) => (props) => { function ExpenseAccountFooterCell() {
if (props.data.length === props.row.index + 1) { return 'Total';
return ''; }
}
return chainedComponent(props);
};
/** /**
* Total amount cell renderer. * Retrieve expense form table entries columns.
*/ */
const TotalAmountCellRenderer = (chainedComponent, type) => (props) => { export function useExpenseFormTableColumns() {
if (props.data.length === props.row.index + 1) { const { formatMessage } = useIntl();
const total = props.data.reduce((total, entry) => {
const amount = parseInt(entry[type], 10);
const computed = amount ? total + amount : total;
return computed; return React.useMemo(
}, 0); () => [
{
return <span>{formattedAmount(total, 'USD')}</span>; Header: '#',
} accessor: 'index',
return chainedComponent(props); Cell: ({ row: { index } }) => <span>{index + 1}</span>,
}; className: 'index',
width: 40,
disableResizing: true,
disableSortBy: true,
export function useExpenseFormTableColumns() { },
return React.useMemo( {
() => [ Header: ExpenseCategoryHeaderCell,
{ id: 'expense_account_id',
Header: '#', accessor: 'expense_account_id',
accessor: 'index', Cell: AccountsListFieldCell,
Cell: ({ row: { index } }) => <span>{index + 1}</span>, Footer: ExpenseAccountFooterCell,
className: 'index', className: 'expense_account_id',
width: 40, disableSortBy: true,
disableResizing: true, width: 40,
disableSortBy: true, filterAccountsByRootType: ['expense'],
}, },
{ {
Header: ExpenseCategoryHeaderCell, Header: formatMessage({ id: 'amount_currency' }, { currency: 'USD' }),
id: 'expense_account_id', accessor: 'amount',
accessor: 'expense_account_id', Cell: MoneyFieldCell,
Cell: TotalExpenseCellRenderer(AccountsListFieldCell), Footer: AmountFooterCell,
className: 'expense_account_id', disableSortBy: true,
disableSortBy: true, width: 40,
width: 40, className: 'amount',
filterAccountsByRootType: ['expense'], },
}, {
{ Header: formatMessage({ id: 'description' }),
Header: formatMessage({ id: 'amount_currency' }, { currency: 'USD' }), accessor: 'description',
accessor: 'amount', Cell: InputGroupCell,
Cell: TotalAmountCellRenderer(MoneyFieldCell, 'amount'), disableSortBy: true,
disableSortBy: true, className: 'description',
width: 40, width: 100,
className: 'amount', },
}, {
{ Header: '',
Header: formatMessage({ id: 'description' }), accessor: 'action',
accessor: 'description', Cell: ActionsCellRenderer,
Cell: NoteCellRenderer(InputGroupCell), className: 'actions',
disableSortBy: true, disableSortBy: true,
className: 'description', disableResizing: true,
width: 100, width: 45,
}, },
{ ],
Header: '', [formatMessage],
accessor: 'action', );
Cell: ActionsCellRenderer, }
className: 'actions',
disableSortBy: true,
disableResizing: true,
width: 45,
},
],
[formatMessage],
)
}

View File

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

View File

@@ -1,16 +1,14 @@
import React, { useEffect } from 'react'; import React from 'react';
import { useIntl } from 'react-intl';
import 'style/pages/Expense/List.scss'; import 'style/pages/Expense/List.scss';
import DashboardPageContent from 'components/Dashboard/DashboardPageContent'; import { DashboardContentTable, DashboardPageContent } from 'components';
import ExpenseActionsBar from './ExpenseActionsBar'; import ExpenseActionsBar from './ExpenseActionsBar';
import ExpenseViewTabs from './ExpenseViewTabs'; import ExpenseViewTabs from './ExpenseViewTabs';
import ExpenseDataTable from './ExpenseDataTable'; import ExpenseDataTable from './ExpenseDataTable';
import ExpensesAlerts from '../ExpensesAlerts'; import ExpensesAlerts from '../ExpensesAlerts';
import withDashboardActions from 'containers/Dashboard/withDashboardActions';
import withExpenses from './withExpenses'; import withExpenses from './withExpenses';
import { compose, transformTableStateToQuery } from 'utils'; import { compose, transformTableStateToQuery } from 'utils';
@@ -20,19 +18,9 @@ import { ExpensesListProvider } from './ExpensesListProvider';
* Expenses list. * Expenses list.
*/ */
function ExpensesList({ function ExpensesList({
// #withDashboardActions
changePageTitle,
// #withExpenses // #withExpenses
expensesTableState, expensesTableState,
}) { }) {
const { formatMessage } = useIntl();
// Changes the page title once the page mount.
useEffect(() => {
changePageTitle(formatMessage({ id: 'expenses_list' }));
}, [changePageTitle, formatMessage]);
return ( return (
<ExpensesListProvider <ExpensesListProvider
query={transformTableStateToQuery(expensesTableState)} query={transformTableStateToQuery(expensesTableState)}
@@ -41,7 +29,10 @@ function ExpensesList({
<DashboardPageContent> <DashboardPageContent>
<ExpenseViewTabs /> <ExpenseViewTabs />
<ExpenseDataTable />
<DashboardContentTable>
<ExpenseDataTable />
</DashboardContentTable>
</DashboardPageContent> </DashboardPageContent>
<ExpensesAlerts /> <ExpensesAlerts />
@@ -50,6 +41,5 @@ function ExpensesList({
} }
export default compose( export default compose(
withDashboardActions,
withExpenses(({ expensesTableState }) => ({ expensesTableState })), withExpenses(({ expensesTableState }) => ({ expensesTableState })),
)(ExpensesList); )(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. * 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 React, { useState } from 'react';
import { compose } from 'utils';
import moment from 'moment'; import moment from 'moment';
import { useIntl } from 'react-intl';
import 'style/pages/FinancialStatements/BalanceSheet.scss'; import 'style/pages/FinancialStatements/BalanceSheet.scss';
@@ -14,25 +10,18 @@ import BalanceSheetActionsBar from './BalanceSheetActionsBar';
import { FinancialStatement } from 'components'; import { FinancialStatement } from 'components';
import withDashboardActions from 'containers/Dashboard/withDashboardActions';
import withSettings from 'containers/Settings/withSettings'; import withSettings from 'containers/Settings/withSettings';
import { BalanceSheetProvider } from './BalanceSheetProvider'; import { BalanceSheetProvider } from './BalanceSheetProvider';
import { compose } from 'utils';
/** /**
* Balance sheet. * Balance sheet.
*/ */
function BalanceSheet({ function BalanceSheet({
// #withDashboardActions
changePageTitle,
setDashboardBackLink,
setSidebarShrink,
// #withPreferences // #withPreferences
organizationName, organizationName,
}) { }) {
const { formatMessage } = useIntl();
const [filter, setFilter] = useState({ const [filter, setFilter] = useState({
fromDate: moment().startOf('year').format('YYYY-MM-DD'), fromDate: moment().startOf('year').format('YYYY-MM-DD'),
toDate: moment().endOf('year').format('YYYY-MM-DD'), toDate: moment().endOf('year').format('YYYY-MM-DD'),
@@ -41,21 +30,6 @@ function BalanceSheet({
accountsFilter: 'all-accounts', 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. // Handle re-fetch balance sheet after filter change.
const handleFilterSubmit = (filter) => { const handleFilterSubmit = (filter) => {
const _filter = { const _filter = {
@@ -95,7 +69,6 @@ function BalanceSheet({
} }
export default compose( export default compose(
withDashboardActions,
withSettings(({ organizationSettings }) => ({ withSettings(({ organizationSettings }) => ({
organizationName: organizationSettings.name, organizationName: organizationSettings.name,
})), })),

View File

@@ -34,16 +34,7 @@ function FinancialReportsSection({ sectionTitle, reports }) {
); );
} }
function FinancialReports({ export default function FinancialReports() {
// #withDashboardActions
changePageTitle,
}) {
const { formatMessage } = useIntl();
useEffect(() => {
changePageTitle(formatMessage({ id: 'all_financial_reports' }));
}, [changePageTitle, formatMessage]);
return ( return (
<DashboardInsider name={'financial-reports'}> <DashboardInsider name={'financial-reports'}>
<div class="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 React from 'react';
import { useIntl } from 'react-intl';
import DashboardPageContent from 'components/Dashboard/DashboardPageContent';
import { DashboardContentTable, DashboardPageContent } from 'components';
import InventoryAdjustmentsAlerts from './InventoryAdjustmentsAlerts'; import InventoryAdjustmentsAlerts from './InventoryAdjustmentsAlerts';
import { InventoryAdjustmentsProvider } from './InventoryAdjustmentsProvider'; import { InventoryAdjustmentsProvider } from './InventoryAdjustmentsProvider';
import InventoryAdjustmentTable from './InventoryAdjustmentTable'; import InventoryAdjustmentTable from './InventoryAdjustmentTable';
import withInventoryAdjustments from './withInventoryAdjustments'; import withInventoryAdjustments from './withInventoryAdjustments';
import withDashboardActions from 'containers/Dashboard/withDashboardActions';
import { compose, transformTableStateToQuery } from 'utils'; import { compose, transformTableStateToQuery } from 'utils';
@@ -17,25 +14,18 @@ import { compose, transformTableStateToQuery } from 'utils';
* Inventory Adjustment List. * Inventory Adjustment List.
*/ */
function InventoryAdjustmentList({ function InventoryAdjustmentList({
// #withDashboardActions
changePageTitle,
// #withInventoryAdjustments // #withInventoryAdjustments
inventoryAdjustmentTableState, inventoryAdjustmentTableState,
}) { }) {
const { formatMessage } = useIntl();
// Changes the dashboard title once the page mount.
useEffect(() => {
changePageTitle(formatMessage({ id: 'inventory_adjustment_list' }));
}, [changePageTitle, formatMessage]);
return ( return (
<InventoryAdjustmentsProvider <InventoryAdjustmentsProvider
query={transformTableStateToQuery(inventoryAdjustmentTableState)} query={transformTableStateToQuery(inventoryAdjustmentTableState)}
> >
<DashboardPageContent> <DashboardPageContent>
<InventoryAdjustmentTable /> <DashboardContentTable>
<InventoryAdjustmentTable />
</DashboardContentTable>
<InventoryAdjustmentsAlerts /> <InventoryAdjustmentsAlerts />
</DashboardPageContent> </DashboardPageContent>
</InventoryAdjustmentsProvider> </InventoryAdjustmentsProvider>
@@ -43,7 +33,6 @@ function InventoryAdjustmentList({
} }
export default compose( export default compose(
withDashboardActions,
withInventoryAdjustments(({ inventoryAdjustmentTableState }) => ({ withInventoryAdjustments(({ inventoryAdjustmentTableState }) => ({
inventoryAdjustmentTableState, inventoryAdjustmentTableState,
})), })),

View File

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

View File

@@ -1,33 +1,16 @@
import React, { useEffect } from 'react'; import React from 'react';
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
import { ItemFormProvider } from './ItemFormProvider'; import { ItemFormProvider } from './ItemFormProvider';
import DashboardCard from 'components/Dashboard/DashboardCard'; import DashboardCard from 'components/Dashboard/DashboardCard';
import ItemForm from 'containers/Items/ItemForm'; import ItemForm from 'containers/Items/ItemForm';
import withDashboardActions from 'containers/Dashboard/withDashboardActions';
import { compose } from 'utils';
/** /**
* Item form page. * Item form page.
*/ */
function ItemFormPage({ export default function ItemFormPage() {
// #withDashboardActions
setDashboardBackLink
}) {
const { id } = useParams(); 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 ( return (
<ItemFormProvider itemId={id}> <ItemFormProvider itemId={id}>
<DashboardCard page> <DashboardCard page>
@@ -36,7 +19,3 @@ function ItemFormPage({
</ItemFormProvider> </ItemFormProvider>
); );
} }
export default compose(
withDashboardActions,
)(ItemFormPage);

View File

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

View File

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

View File

@@ -1,46 +1,25 @@
import React, { useEffect } from 'react'; import React from 'react';
import { useParams } from 'react-router-dom'; import { DashboardContentTable, DashboardPageContent } from 'components';
import { useIntl } from 'react-intl';
import DashboardPageContent from 'components/Dashboard/DashboardPageContent';
import ItemsCategoriesAlerts from './ItemsCategoriesAlerts'; import ItemsCategoriesAlerts from './ItemsCategoriesAlerts';
import ItemsCategoryActionsBar from './ItemsCategoryActionsBar'; import ItemsCategoryActionsBar from './ItemsCategoryActionsBar';
import { ItemsCategoriesProvider } from './ItemsCategoriesProvider'; import { ItemsCategoriesProvider } from './ItemsCategoriesProvider';
import ItemCategoriesTable from './ItemCategoriesTable'; import ItemCategoriesTable from './ItemCategoriesTable';
import withDashboardActions from 'containers/Dashboard/withDashboardActions';
import { compose } from 'utils';
/** /**
* Item categories list. * Item categories list.
*/ */
const ItemCategoryList = ({ export default function 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]);
return ( return (
<ItemsCategoriesProvider query={{}}> <ItemsCategoriesProvider query={{}}>
<ItemsCategoryActionsBar /> <ItemsCategoryActionsBar />
<DashboardPageContent> <DashboardPageContent>
<ItemCategoriesTable /> <DashboardContentTable>
<ItemCategoriesTable />
</DashboardContentTable>
</DashboardPageContent> </DashboardPageContent>
<ItemsCategoriesAlerts /> <ItemsCategoriesAlerts />
</ItemsCategoriesProvider> </ItemsCategoriesProvider>
); );
}; }
export default compose(
withDashboardActions,
)(ItemCategoryList);

View File

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

View File

@@ -11,11 +11,10 @@ import {
InputGroup, InputGroup,
Intent, Intent,
} from '@blueprintjs/core'; } from '@blueprintjs/core';
import withSettingsActions from 'containers/Settings/withSettingsActions';
import { compose } from 'utils';
/**
* Reference number form.
*/
export default function ReferenceNumberForm({ export default function ReferenceNumberForm({
onSubmit, onSubmit,
onClose, onClose,
@@ -62,6 +61,7 @@ export default function ReferenceNumberForm({
<Row> <Row>
{/* prefix */} {/* prefix */}
<Col> <Col>
<FormGroup <FormGroup
label={<T id={'prefix'} />} label={<T id={'prefix'} />}
className={'form-group--'} 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 { Formik, Form } from 'formik';
import moment from 'moment'; import moment from 'moment';
import { Intent } from '@blueprintjs/core'; import { Intent } from '@blueprintjs/core';
@@ -13,12 +13,10 @@ import BillFormHeader from './BillFormHeader';
import BillFloatingActions from './BillFloatingActions'; import BillFloatingActions from './BillFloatingActions';
import BillFormFooter from './BillFormFooter'; import BillFormFooter from './BillFormFooter';
import withDashboardActions from 'containers/Dashboard/withDashboardActions';
import { AppToaster } from 'components'; import { AppToaster } from 'components';
import { ERROR } from 'common/errors'; import { ERROR } from 'common/errors';
import { compose, repeatValue, orderingLinesIndexes } from 'utils'; import { repeatValue, orderingLinesIndexes } from 'utils';
import BillFormBody from './BillFormBody'; import BillFormBody from './BillFormBody';
import { useBillFormContext } from './BillFormProvider'; import { useBillFormContext } from './BillFormProvider';
@@ -47,10 +45,8 @@ const defaultInitialValues = {
/** /**
* Bill form. * Bill form.
*/ */
function BillForm({ export default function BillForm({
//#withDashboard
changePageTitle,
changePageSubtitle,
}) { }) {
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();
const history = useHistory(); const history = useHistory();
@@ -65,14 +61,6 @@ function BillForm({
const isNewMode = !billId; 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. // Initial values in create and edit mode.
const initialValues = useMemo( const initialValues = useMemo(
() => ({ () => ({
@@ -146,8 +134,6 @@ function BillForm({
}); });
setSubmitting(false); setSubmitting(false);
changePageSubtitle('');
if (submitPayload.redirect) { if (submitPayload.redirect) {
history.push('/bills'); 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 ( return (
<div <div
className={classNames( className={classNames(
@@ -189,7 +167,7 @@ function BillForm({
onSubmit={handleFormSubmit} onSubmit={handleFormSubmit}
> >
<Form> <Form>
<BillFormHeader onBillNumberChanged={handleBillNumberChanged} /> <BillFormHeader />
<BillFormBody defaultBill={defaultBill} /> <BillFormBody defaultBill={defaultBill} />
<BillFormFooter /> <BillFormFooter />
<BillFloatingActions /> <BillFloatingActions />
@@ -198,5 +176,3 @@ function BillForm({
</div> </div>
); );
} }
export default compose(withDashboardActions)(BillForm);

View File

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

View File

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

View File

@@ -1,45 +1,17 @@
import React, { useCallback, useEffect } from 'react'; import React from 'react';
import { useParams, useHistory } from 'react-router-dom'; import { useParams } from 'react-router-dom';
import BillForm from './BillForm'; import BillForm from './BillForm';
import { BillFormProvider } from './BillFormProvider'; import { BillFormProvider } from './BillFormProvider';
import withDashboardActions from 'containers/Dashboard/withDashboardActions';
import { compose } from 'utils';
import 'style/pages/Bills/PageForm.scss'; import 'style/pages/Bills/PageForm.scss';
function BillFormPage({ export default function BillFormPage() {
// #withDashboardActions
setSidebarShrink,
resetSidebarPreviousExpand,
setDashboardBackLink
}) {
const { id } = useParams(); 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 ( return (
<BillFormProvider billId={id}> <BillFormProvider billId={id}>
<BillForm /> <BillForm />
</BillFormProvider> </BillFormProvider>
); );
} }
export default compose(
withDashboardActions
)(BillFormPage);

View File

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

View File

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

View File

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

View File

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

View File

@@ -13,70 +13,55 @@ import { FormattedMessage as T } from 'react-intl';
import { CLASSES } from 'common/classes'; import { CLASSES } from 'common/classes';
import classNames from 'classnames'; import classNames from 'classnames';
import { useFormikContext } from 'formik'; import { useFormikContext } from 'formik';
import { saveInvoke } from 'utils';
import { If, Icon } from 'components'; import { If, Icon } from 'components';
import { useEstimateFormContext } from './EstimateFormProvider';
/** /**
* Estimate floating actions bar. * Estimate floating actions bar.
*/ */
export default function EstimateFloatingActions({ export default function EstimateFloatingActions() {
isSubmitting, const { resetForm, submitForm, isSubmitting } = useFormikContext();
onSubmitClick,
onCancelClick,
estimate,
}) {
const { resetForm, submitForm } = useFormikContext();
// Estimate form context.
const { estimate, setSubmitPayload } = useEstimateFormContext();
// Handle submit & deliver button click.
const handleSubmitDeliverBtnClick = (event) => { const handleSubmitDeliverBtnClick = (event) => {
saveInvoke(onSubmitClick, event, { setSubmitPayload({ redirect: true, deliver: true, });
redirect: true,
deliver: true,
});
}; };
// Handle submit, deliver & new button click.
const handleSubmitDeliverAndNewBtnClick = (event) => { const handleSubmitDeliverAndNewBtnClick = (event) => {
setSubmitPayload({ redirect: false, deliver: true, resetForm: true });
submitForm(); submitForm();
saveInvoke(onSubmitClick, event, {
redirect: false,
deliver: true,
resetForm: true,
});
}; };
// Handle submit, deliver & continue editing button click.
const handleSubmitDeliverContinueEditingBtnClick = (event) => { const handleSubmitDeliverContinueEditingBtnClick = (event) => {
setSubmitPayload({ redirect: false, deliver: true });
submitForm(); submitForm();
saveInvoke(onSubmitClick, event, {
redirect: false,
deliver: true,
});
}; };
// Handle submit as draft button click.
const handleSubmitDraftBtnClick = (event) => { const handleSubmitDraftBtnClick = (event) => {
saveInvoke(onSubmitClick, event, { setSubmitPayload({ redirect: true, deliver: false });
redirect: true, submitForm();
deliver: false,
});
}; };
// Handle submit as draft & new button click.
const handleSubmitDraftAndNewBtnClick = (event) => { const handleSubmitDraftAndNewBtnClick = (event) => {
setSubmitPayload({ redirect: false, deliver: false, resetForm: true });
submitForm(); submitForm();
saveInvoke(onSubmitClick, event, {
redirect: false,
deliver: false,
resetForm: true,
});
}; };
// Handle submit as draft & continue editing button click.
const handleSubmitDraftContinueEditingBtnClick = (event) => { const handleSubmitDraftContinueEditingBtnClick = (event) => {
setSubmitPayload({ redirect: false, deliver: false });
submitForm(); submitForm();
saveInvoke(onSubmitClick, event, {
redirect: false,
deliver: false,
});
}; };
const handleCancelBtnClick = (event) => { const handleCancelBtnClick = (event) => {
saveInvoke(onCancelClick, event);
}; };
const handleClearBtnClick = (event) => { const handleClearBtnClick = (event) => {
@@ -90,6 +75,7 @@ export default function EstimateFloatingActions({
<ButtonGroup> <ButtonGroup>
<Button <Button
disabled={isSubmitting} disabled={isSubmitting}
loading={isSubmitting}
intent={Intent.PRIMARY} intent={Intent.PRIMARY}
type="submit" type="submit"
onClick={handleSubmitDeliverBtnClick} 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 { Formik, Form } from 'formik';
import moment from 'moment'; import moment from 'moment';
import { Intent } from '@blueprintjs/core'; import { Intent } from '@blueprintjs/core';
@@ -17,10 +17,6 @@ import EstimateFormHeader from './EstimateFormHeader';
import EstimateFormBody from './EstimateFormBody'; import EstimateFormBody from './EstimateFormBody';
import EstimateFloatingActions from './EstimateFloatingActions'; import EstimateFloatingActions from './EstimateFloatingActions';
import EstimateFormFooter from './EstimateFormFooter'; import EstimateFormFooter from './EstimateFormFooter';
import EstimateNumberWatcher from './EstimateNumberWatcher';
import withEstimateActions from './withEstimateActions';
import withEstimateDetail from './withEstimateDetail';
import withDashboardActions from 'containers/Dashboard/withDashboardActions'; import withDashboardActions from 'containers/Dashboard/withDashboardActions';
import withMediaActions from 'containers/Media/withMediaActions'; import withMediaActions from 'containers/Media/withMediaActions';
@@ -28,13 +24,12 @@ import withSettings from 'containers/Settings/withSettings';
import { AppToaster } from 'components'; import { AppToaster } from 'components';
import { ERROR } from 'common/errors'; import { ERROR } from 'common/errors';
import { import {
compose, compose,
repeatValue, repeatValue,
defaultToTransform,
orderingLinesIndexes, orderingLinesIndexes,
} from 'utils'; } from 'utils';
import { useEstimateFormContext } from './EstimateFormProvider';
const MIN_LINES_NUMBER = 4; const MIN_LINES_NUMBER = 4;
@@ -63,64 +58,25 @@ const defaultInitialValues = {
* Estimate form. * Estimate form.
*/ */
const EstimateForm = ({ const EstimateForm = ({
// #WithMedia
requestSubmitMedia,
requestDeleteMedia,
// #WithEstimateActions
requestSubmitEstimate,
requestEditEstimate,
setEstimateNumberChanged,
//#withDashboard
changePageTitle,
changePageSubtitle,
// #withSettings // #withSettings
estimateNextNumber, estimateNextNumber,
estimateNumberPrefix, estimateNumberPrefix,
//#withEstimateDetail
estimate,
// #withEstimates
estimateNumberChanged,
//#own Props
estimateId,
onFormSubmit,
onCancelForm,
}) => { }) => {
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();
const history = useHistory(); const history = useHistory();
const [submitPayload, setSubmitPayload] = useState({});
const isNewMode = !estimateId; const {
estimate,
isNewMode,
submitPayload,
createEstimateMutate,
editEstimateMutate,
} = useEstimateFormContext();
const estimateNumber = estimateNumberPrefix const estimateNumber = estimateNumberPrefix
? `${estimateNumberPrefix}-${estimateNextNumber}` ? `${estimateNumberPrefix}-${estimateNextNumber}`
: 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. // Initial values in create and edit mode.
const initialValues = useMemo( const initialValues = useMemo(
() => ({ () => ({
@@ -167,7 +123,6 @@ const EstimateForm = ({
const entries = values.entries.filter( const entries = values.entries.filter(
(item) => item.item_id && item.quantity, (item) => item.item_id && item.quantity,
); );
const totalQuantity = sumBy(entries, (entry) => parseInt(entry.quantity)); const totalQuantity = sumBy(entries, (entry) => parseInt(entry.quantity));
if (totalQuantity === 0) { if (totalQuantity === 0) {
@@ -218,35 +173,12 @@ const EstimateForm = ({
}; };
if (estimate && estimate.id) { if (estimate && estimate.id) {
requestEditEstimate(estimate.id, form).then(onSuccess).catch(onError); editEstimateMutate([estimate.id, form]).then(onSuccess).catch(onError);
} else { } 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 ( return (
<div <div
className={classNames( className={classNames(
@@ -262,30 +194,18 @@ const EstimateForm = ({
initialValues={initialValues} initialValues={initialValues}
onSubmit={handleFormSubmit} onSubmit={handleFormSubmit}
> >
{({ isSubmitting}) => ( <Form>
<Form> <EstimateFormHeader />
<EstimateFormHeader <EstimateFormBody defaultEstimate={defaultEstimate} />
onEstimateNumberChanged={handleEstimateNumberChange} <EstimateFormFooter />
/> <EstimateFloatingActions />
<EstimateNumberWatcher estimateNumber={estimateNumber} /> </Form>
<EstimateFormBody defaultEstimate={defaultEstimate} />
<EstimateFormFooter />
<EstimateFloatingActions
isSubmitting={isSubmitting}
estimate={estimate}
onSubmitClick={handleSubmitClick}
onCancelClick={handleCancelClick}
/>
</Form>
)}
</Formik> </Formik>
</div> </div>
); );
}; };
export default compose( export default compose(
withEstimateActions,
withEstimateDetail(),
withDashboardActions, withDashboardActions,
withMediaActions, withMediaActions,
withSettings(({ estimatesSettings }) => ({ withSettings(({ estimatesSettings }) => ({

View File

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

View File

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

View File

@@ -1,4 +1,4 @@
import React, { useCallback } from 'react'; import React from 'react';
import { import {
FormGroup, FormGroup,
InputGroup, InputGroup,
@@ -8,7 +8,7 @@ import {
import { DateInput } from '@blueprintjs/datetime'; import { DateInput } from '@blueprintjs/datetime';
import { FormattedMessage as T } from 'react-intl'; import { FormattedMessage as T } from 'react-intl';
import { FastField, ErrorMessage } from 'formik'; import { FastField, ErrorMessage } from 'formik';
import { momentFormatter, compose, tansformDateValue, saveInvoke } from 'utils'; import { momentFormatter, compose, tansformDateValue } from 'utils';
import classNames from 'classnames'; import classNames from 'classnames';
import { CLASSES } from 'common/classes'; import { CLASSES } from 'common/classes';
import { import {
@@ -16,30 +16,25 @@ import {
FieldRequiredHint, FieldRequiredHint,
Icon, Icon,
InputPrependButton, InputPrependButton,
Row,
Col,
} from 'components'; } from 'components';
import withCustomers from 'containers/Customers/withCustomers'; import withCustomers from 'containers/Customers/withCustomers';
import withDialogActions from 'containers/Dialog/withDialogActions'; import withDialogActions from 'containers/Dialog/withDialogActions';
import { inputIntent, handleDateChange } from 'utils'; import { inputIntent, handleDateChange } from 'utils';
import { formatMessage } from 'services/intl'; import { useEstimateFormContext } from './EstimateFormProvider';
/**
* Estimate form header.
*/
function EstimateFormHeader({ function EstimateFormHeader({
//#withCustomers
customers,
// #withDialogActions // #withDialogActions
openDialog, openDialog,
// #ownProps
onEstimateNumberChanged,
}) { }) {
const handleEstimateNumberChange = useCallback(() => { const { customers } = useEstimateFormContext();
openDialog('estimate-number-form', {});
}, [openDialog]);
const handleEstimateNumberChanged = (event) => { const handleEstimateNumberChange = () => {
saveInvoke(onEstimateNumberChanged, event.currentTarget.value); openDialog('estimate-number-form', {});
}; };
return ( return (
@@ -138,7 +133,6 @@ function EstimateFormHeader({
<InputGroup <InputGroup
minimal={true} minimal={true}
{...field} {...field}
onBlur={handleEstimateNumberChanged}
/> />
<InputPrependButton <InputPrependButton
buttonProps={{ 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 { 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 '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({ function EstimateFormPage({
// #withCustomersActions
requestFetchCustomers,
// #withItemsActions
requestFetchItems,
// #withEstimateActions
requestFetchEstimate,
// #withSettingsActions
requestFetchOptions,
// #withDashboardActions // #withDashboardActions
setSidebarShrink, setSidebarShrink,
resetSidebarPreviousExpand, resetSidebarPreviousExpand,
setDashboardBackLink, setDashboardBackLink,
}) { }) {
const history = useHistory();
const { id } = useParams(); const { id } = useParams();
useEffect(() => { useEffect(() => {
@@ -50,55 +35,15 @@ function EstimateFormPage({
}; };
}, [resetSidebarPreviousExpand, setSidebarShrink, setDashboardBackLink]); }, [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 ( return (
<DashboardInsider <EstimateFormProvider estimateId={id}>
loading={ <EstimateForm />
fetchCustomers.isFetching || </EstimateFormProvider>
fetchItems.isFetching ||
fetchEstimate.isFetching
}
name={'estimate-form'}
>
<EstimateForm
estimateId={id}
onFormSubmit={handleFormSubmit}
onCancelForm={handleCancel}
/>
</DashboardInsider>
); );
} }
export default compose( export default compose(
withEstimateActions,
withCustomersActions,
withItemsActions,
withSettingsActions,
withDashboardActions, withDashboardActions,
)(EstimateFormPage); )(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 { useEstimatesListContext } from './EstimatesListProvider';
import { ActionsMenu, useEstiamtesTableColumns } from './components'; import { ActionsMenu, useEstiamtesTableColumns } from './components';
/** /**
* Estimates datatable. * Estimates datatable.
*/ */
@@ -78,43 +77,37 @@ function EstimatesDataTable({
[setEstimatesTableState], [setEstimatesTableState],
); );
// Display empty status instead of the table.
if (isEmptyStatus) { if (isEmptyStatus) {
return <EstimatesEmptyStatus />; return <EstimatesEmptyStatus />;
} }
return ( return (
<div className={classNames(CLASSES.DASHBOARD_DATATABLE)}> <DataTable
<DataTable columns={columns}
columns={columns} data={estimates}
data={estimates} loading={isEstimatesLoading}
headerLoading={isEstimatesLoading}
loading={isEstimatesLoading} progressBarLoading={isEstimatesFetching}
headerLoading={isEstimatesLoading} onFetchData={handleFetchData}
progressBarLoading={isEstimatesFetching} noInitialFetch={true}
manualSortBy={true}
onFetchData={handleFetchData} selectionColumn={true}
noInitialFetch={true} sticky={true}
manualSortBy={true} pagination={true}
selectionColumn={true} manualPagination={true}
sticky={true} pagesCount={pagination.pagesCount}
pagination={true} TableLoadingRenderer={TableSkeletonRows}
TableHeaderSkeletonRenderer={TableSkeletonHeader}
manualPagination={true} ContextMenu={ActionsMenu}
pagesCount={pagination.pagesCount} payload={{
onApprove: handleApproveEstimate,
TableLoadingRenderer={TableSkeletonRows} onEdit: handleEditEstimate,
TableHeaderSkeletonRenderer={TableSkeletonHeader} onReject: handleRejectEstimate,
onDeliver: handleDeliverEstimate,
ContextMenu={ActionsMenu} onDelete: handleDeleteEstimate,
payload={{ }}
onApprove: handleApproveEstimate, />
onEdit: handleEditEstimate,
onReject: handleRejectEstimate,
onDeliver: handleDeliverEstimate,
onDelete: handleDeleteEstimate,
}}
/>
</div>
); );
} }

View File

@@ -1,13 +1,11 @@
import React, { useEffect } from 'react'; import React from 'react';
import { FormattedMessage as T, useIntl } from 'react-intl'; import { DashboardContentTable, DashboardPageContent } from 'components';
import DashboardPageContent from 'components/Dashboard/DashboardPageContent';
import EstimatesActionsBar from './EstimatesActionsBar'; import EstimatesActionsBar from './EstimatesActionsBar';
import EstimatesAlerts from '../EstimatesAlerts'; import EstimatesAlerts from '../EstimatesAlerts';
import EstimatesViewTabs from './EstimatesViewTabs'; import EstimatesViewTabs from './EstimatesViewTabs';
import EstimatesDataTable from './EstimatesDataTable'; import EstimatesDataTable from './EstimatesDataTable';
import withDashboardActions from 'containers/Dashboard/withDashboardActions';
import withEstimates from './withEstimates'; import withEstimates from './withEstimates';
import { EstimatesListProvider } from './EstimatesListProvider'; import { EstimatesListProvider } from './EstimatesListProvider';
@@ -17,18 +15,9 @@ import { compose, transformTableStateToQuery } from 'utils';
* Sale estimates list page. * Sale estimates list page.
*/ */
function EstimatesList({ function EstimatesList({
// #withDashboardActions
changePageTitle,
// #withEstimate // #withEstimate
estimatesTableState, estimatesTableState,
}) { }) {
const { formatMessage } = useIntl();
useEffect(() => {
changePageTitle(formatMessage({ id: 'estimates_list' }));
}, [changePageTitle, formatMessage]);
return ( return (
<EstimatesListProvider <EstimatesListProvider
query={transformTableStateToQuery(estimatesTableState)} query={transformTableStateToQuery(estimatesTableState)}
@@ -37,7 +26,10 @@ function EstimatesList({
<DashboardPageContent> <DashboardPageContent>
<EstimatesViewTabs /> <EstimatesViewTabs />
<EstimatesDataTable />
<DashboardContentTable>
<EstimatesDataTable />
</DashboardContentTable>
</DashboardPageContent> </DashboardPageContent>
<EstimatesAlerts /> <EstimatesAlerts />
@@ -46,6 +38,5 @@ function EstimatesList({
} }
export default compose( export default compose(
withDashboardActions,
withEstimates(({ estimatesTableState }) => ({ estimatesTableState })), withEstimates(({ estimatesTableState }) => ({ estimatesTableState })),
)(EstimatesList); )(EstimatesList);

View File

@@ -151,6 +151,7 @@ export default function InvoiceFloatingActions() {
<ButtonGroup> <ButtonGroup>
<Button <Button
disabled={isSubmitting} disabled={isSubmitting}
loading={isSubmitting}
intent={Intent.PRIMARY} intent={Intent.PRIMARY}
onClick={handleSubmitDeliverBtnClick} onClick={handleSubmitDeliverBtnClick}
text={<T id={'save'} />} 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 { Formik, Form } from 'formik';
import moment from 'moment';
import { Intent } from '@blueprintjs/core'; import { Intent } from '@blueprintjs/core';
import { useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
import { pick, sumBy, omit, isEmpty } from 'lodash'; import { sumBy, omit, isEmpty } from 'lodash';
import classNames from 'classnames'; import classNames from 'classnames';
import { useHistory } from 'react-router-dom';
import { CLASSES } from 'common/classes'; import { CLASSES } from 'common/classes';
import { import {
CreateInvoiceFormSchema, CreateInvoiceFormSchema,
@@ -12,12 +12,10 @@ import {
} from './InvoiceForm.schema'; } from './InvoiceForm.schema';
import InvoiceFormHeader from './InvoiceFormHeader'; import InvoiceFormHeader from './InvoiceFormHeader';
import EditableItemsEntriesTable from 'containers/Entries/EditableItemsEntriesTable'; import InvoiceItemsEntriesEditorField from './InvoiceItemsEntriesEditorField';
import InvoiceFloatingActions from './InvoiceFloatingActions'; import InvoiceFloatingActions from './InvoiceFloatingActions';
import InvoiceFormFooter from './InvoiceFormFooter'; import InvoiceFormFooter from './InvoiceFormFooter';
import InvoiceNumberChangeWatcher from './InvoiceNumberChangeWatcher';
import withInvoiceActions from './withInvoiceActions';
import withDashboardActions from 'containers/Dashboard/withDashboardActions'; import withDashboardActions from 'containers/Dashboard/withDashboardActions';
import withMediaActions from 'containers/Media/withMediaActions'; import withMediaActions from 'containers/Media/withMediaActions';
import withSettings from 'containers/Settings/withSettings'; import withSettings from 'containers/Settings/withSettings';
@@ -26,101 +24,47 @@ import { AppToaster } from 'components';
import { ERROR } from 'common/errors'; import { ERROR } from 'common/errors';
import { import {
compose, compose,
repeatValue,
defaultToTransform,
orderingLinesIndexes, orderingLinesIndexes,
transactionNumber, transactionNumber,
} from 'utils'; } from 'utils';
import { useHistory } from 'react-router-dom';
import { useInvoiceFormContext } from './InvoiceFormProvider'; import { useInvoiceFormContext } from './InvoiceFormProvider';
import { transformToEditForm } from './utils';
const MIN_LINES_NUMBER = 4; import {
MIN_LINES_NUMBER,
const defaultInvoice = { defaultInitialValues
index: 0, } from './constants';
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)],
};
/** /**
* Invoice form. * Invoice form.
*/ */
function InvoiceForm({ function InvoiceForm({
// #withDashboard
changePageTitle,
changePageSubtitle,
// #withSettings // #withSettings
invoiceNextNumber, invoiceNextNumber,
invoiceNumberPrefix, invoiceNumberPrefix,
}) { }) {
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();
const history = useHistory(); const history = useHistory();
// Invoice form context.
const { const {
items, isNewMode,
invoiceId,
invoice, invoice,
createInvoiceMutate, createInvoiceMutate,
editInvoiceMutate, editInvoiceMutate,
submitPayload, submitPayload,
} = useInvoiceFormContext(); } = useInvoiceFormContext();
const isNewMode = !invoiceId;
// Invoice number. // Invoice number.
const invoiceNumber = transactionNumber( const invoiceNumber = transactionNumber(
invoiceNumberPrefix, invoiceNumberPrefix,
invoiceNextNumber, 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( const initialValues = useMemo(
() => ({ () => ({
...(!isEmpty(invoice) ...(!isEmpty(invoice)
? { ? transformToEditForm(invoice, defaultInitialValues, MIN_LINES_NUMBER)
...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),
),
],
}
: { : {
...defaultInitialValues, ...defaultInitialValues,
invoice_no: invoiceNumber, invoice_no: invoiceNumber,
@@ -193,22 +137,13 @@ function InvoiceForm({
setSubmitting(false); setSubmitting(false);
}; };
if (invoice && invoice.id) { if (!isEmpty(invoice)) {
editInvoiceMutate(invoice.id, form).then(onSuccess).catch(onError); editInvoiceMutate([invoice.id, form]).then(onSuccess).catch(onError);
} else { } else {
createInvoiceMutate(form).then(onSuccess).catch(onError); createInvoiceMutate(form).then(onSuccess).catch(onError);
} }
}; };
const handleInvoiceNumberChanged = useCallback(
(invoiceNumber) => {
changePageSubtitle(
defaultToTransform(invoiceNumber, `No. ${invoiceNumber}`, ''),
);
},
[changePageSubtitle],
);
return ( return (
<div <div
className={classNames( className={classNames(
@@ -225,17 +160,10 @@ function InvoiceForm({
onSubmit={handleSubmit} onSubmit={handleSubmit}
> >
<Form> <Form>
<InvoiceFormHeader <InvoiceFormHeader />
onInvoiceNumberChanged={handleInvoiceNumberChanged}
/>
<InvoiceNumberChangeWatcher invoiceNumber={invoiceNumber} />
<div className={classNames(CLASSES.PAGE_FORM_BODY)}> <div className={classNames(CLASSES.PAGE_FORM_BODY)}>
<EditableItemsEntriesTable <InvoiceItemsEntriesEditorField />
items={items}
defaultEntry={defaultInvoice}
filterSellableItems={true}
/>
</div> </div>
<InvoiceFormFooter /> <InvoiceFormFooter />
<InvoiceFloatingActions /> <InvoiceFloatingActions />
@@ -246,7 +174,6 @@ function InvoiceForm({
} }
export default compose( export default compose(
withInvoiceActions,
withDashboardActions, withDashboardActions,
withMediaActions, withMediaActions,
withSettings(({ invoiceSettings }) => ({ withSettings(({ invoiceSettings }) => ({

View File

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

View File

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

View File

@@ -1,62 +1,20 @@
import React, { useCallback, useEffect } from 'react'; import React from 'react';
import { useParams, useHistory } from 'react-router-dom'; import { useParams } from 'react-router-dom';
import InvoiceForm from './InvoiceForm'; import InvoiceForm from './InvoiceForm';
import withDashboardActions from 'containers/Dashboard/withDashboardActions';
import { compose } from 'utils';
import 'style/pages/SaleInvoice/PageForm.scss'; import 'style/pages/SaleInvoice/PageForm.scss';
import { InvoiceFormProvider } from './InvoiceFormProvider'; import { InvoiceFormProvider } from './InvoiceFormProvider';
/** /**
* Invoice form page. * Invoice form page.
*/ */
function InvoiceFormPage({ export default function InvoiceFormPage() {
// #withDashboardActions
setSidebarShrink,
resetSidebarPreviousExpand,
setDashboardBackLink,
}) {
const history = useHistory();
const { id } = useParams(); 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 ( return (
<InvoiceFormProvider invoiceId={id}> <InvoiceFormProvider invoiceId={id}>
<InvoiceForm <InvoiceForm />
onFormSubmit={handleFormSubmit}
onCancelForm={handleCancel}
/>
</InvoiceFormProvider> </InvoiceFormProvider>
); );
} }
export default compose(
withDashboardActions,
)(InvoiceFormPage);

View File

@@ -40,6 +40,9 @@ function InvoiceFormProvider({ invoiceId, ...props }) {
// Form submit payload. // Form submit payload.
const [submitPayload, setSubmitPayload] = useState({}); const [submitPayload, setSubmitPayload] = useState({});
// Detarmines whether the form in new mode.
const isNewMode = !invoiceId;
// Provider payload. // Provider payload.
const provider = { const provider = {
invoice, invoice,
@@ -54,6 +57,7 @@ function InvoiceFormProvider({ invoiceId, ...props }) {
createInvoiceMutate, createInvoiceMutate,
editInvoiceMutate, editInvoiceMutate,
setSubmitPayload, setSubmitPayload,
isNewMode
}; };
return ( 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], [setInvoicesTableState],
); );
// Display invoice empty status. // Display invoice empty status instead of the table.
if (isEmptyStatus) { if (isEmptyStatus) {
return <InvoicesEmptyStatus />; return <InvoicesEmptyStatus />;
} }
return ( return (
<div className={classNames(CLASSES.DASHBOARD_DATATABLE)}> <DataTable
<DataTable columns={columns}
columns={columns} data={invoices}
data={invoices} loading={isInvoicesLoading}
loading={isInvoicesLoading} headerLoading={isInvoicesLoading}
headerLoading={isInvoicesLoading} progressBarLoading={isInvoicesFetching}
progressBarLoading={isInvoicesFetching} onFetchData={handleDataTableFetchData}
onFetchData={handleDataTableFetchData} manualSortBy={true}
manualSortBy={true} selectionColumn={true}
selectionColumn={true} noInitialFetch={true}
noInitialFetch={true} sticky={true}
sticky={true} pagination={true}
pagination={true} manualPagination={true}
manualPagination={true} pagesCount={pagination.pagesCount}
pagesCount={pagination.pagesCount} autoResetSortBy={false}
autoResetSortBy={false} autoResetPage={false}
autoResetPage={false} TableLoadingRenderer={TableSkeletonRows}
TableLoadingRenderer={TableSkeletonRows} TableHeaderSkeletonRenderer={TableSkeletonHeader}
TableHeaderSkeletonRenderer={TableSkeletonHeader} ContextMenu={ActionsMenu}
ContextMenu={ActionsMenu} payload={{
payload={{ onDelete: handleDeleteInvoice,
onDelete: handleDeleteInvoice, onDeliver: handleDeliverInvoice,
onDeliver: handleDeliverInvoice, onEdit: handleEditInvoice,
onEdit: handleEditInvoice, baseCurrency,
baseCurrency }}
}} />
/>
</div>
); );
} }

View File

@@ -1,9 +1,8 @@
import React, { useEffect } from 'react'; import React from 'react';
import { useIntl } from 'react-intl';
import 'style/pages/SaleInvoice/List.scss'; import 'style/pages/SaleInvoice/List.scss';
import DashboardPageContent from 'components/Dashboard/DashboardPageContent'; import { DashboardContentTable, DashboardPageContent } from 'components';
import InvoiceActionsBar from './InvoiceActionsBar'; import InvoiceActionsBar from './InvoiceActionsBar';
import { InvoicesListProvider } from './InvoicesListProvider'; import { InvoicesListProvider } from './InvoicesListProvider';
@@ -11,7 +10,6 @@ import InvoiceViewTabs from './InvoiceViewTabs';
import InvoicesDataTable from './InvoicesDataTable'; import InvoicesDataTable from './InvoicesDataTable';
import InvoicesAlerts from '../InvoicesAlerts'; import InvoicesAlerts from '../InvoicesAlerts';
import withDashboardActions from 'containers/Dashboard/withDashboardActions';
import withInvoices from './withInvoices'; import withInvoices from './withInvoices';
import withAlertsActions from 'containers/Alert/withAlertActions'; import withAlertsActions from 'containers/Alert/withAlertActions';
@@ -21,18 +19,9 @@ import { transformTableStateToQuery, compose } from 'utils';
* Sale invoices list. * Sale invoices list.
*/ */
function InvoicesList({ function InvoicesList({
// #withDashboardActions
changePageTitle,
// #withInvoice // #withInvoice
invoicesTableState, invoicesTableState,
}) { }) {
const { formatMessage } = useIntl();
useEffect(() => {
changePageTitle(formatMessage({ id: 'invoices_list' }));
}, [changePageTitle, formatMessage]);
return ( return (
<InvoicesListProvider <InvoicesListProvider
query={transformTableStateToQuery(invoicesTableState)} query={transformTableStateToQuery(invoicesTableState)}
@@ -41,7 +30,10 @@ function InvoicesList({
<DashboardPageContent> <DashboardPageContent>
<InvoiceViewTabs /> <InvoiceViewTabs />
<InvoicesDataTable />
<DashboardContentTable>
<InvoicesDataTable />
</DashboardContentTable>
</DashboardPageContent> </DashboardPageContent>
<InvoicesAlerts /> <InvoicesAlerts />
@@ -50,7 +42,6 @@ function InvoicesList({
} }
export default compose( export default compose(
withDashboardActions,
withInvoices(({ invoicesTableState }) => ({ invoicesTableState })), withInvoices(({ invoicesTableState }) => ({ invoicesTableState })),
withAlertsActions, withAlertsActions,
)(InvoicesList); )(InvoicesList);

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