diff --git a/client/package.json b/client/package.json index 87266a85b..59cd4cede 100644 --- a/client/package.json +++ b/client/package.json @@ -77,7 +77,7 @@ "react-hotkeys-hook": "^3.0.3", "react-intl": "^3.12.0", "react-loadable": "^5.5.0", - "react-query": "^2.4.6", + "react-query": "^3.6.0", "react-redux": "^7.1.3", "react-router-breadcrumbs-hoc": "^3.2.10", "react-router-dom": "^5.2.0", diff --git a/client/src/common/accountTypes.js b/client/src/common/accountTypes.js new file mode 100644 index 000000000..3e84a1e7d --- /dev/null +++ b/client/src/common/accountTypes.js @@ -0,0 +1,223 @@ +export const ACCOUNT_TYPE = { + CASH: 'cash', + BANK: 'bank', + ACCOUNTS_RECEIVABLE: 'accounts-receivable', + INVENTORY: 'inventory', + OTHER_CURRENT_ASSET: 'other-ACCOUNT_PARENT_TYPE.CURRENT_ASSET', + FIXED_ASSET: 'fixed-asset', + NON_CURRENT_ASSET: 'non-ACCOUNT_PARENT_TYPE.CURRENT_ASSET', + + ACCOUNTS_PAYABLE: 'accounts-payable', + CREDIT_CARD: 'credit-card', + TAX_PAYABLE: 'tax-payable', + OTHER_CURRENT_LIABILITY: 'other-current-liability', + LOGN_TERM_LIABILITY: 'long-term-liability', + NON_CURRENT_LIABILITY: 'non-current-liability', + + EQUITY: 'equity', + INCOME: 'income', + OTHER_INCOME: 'other-income', + COST_OF_GOODS_SOLD: 'cost-of-goods-sold', + EXPENSE: 'expense', + OTHER_EXPENSE: 'other-expense', +}; + +export const ACCOUNT_PARENT_TYPE = { + CURRENT_ASSET: 'current-asset', + FIXED_ASSET: 'fixed-asset', + NON_CURRENT_ASSET: 'non-ACCOUNT_PARENT_TYPE.CURRENT_ASSET', + + CURRENT_LIABILITY: 'current-liability', + LOGN_TERM_LIABILITY: 'long-term-liability', + NON_CURRENT_LIABILITY: 'non-current-liability', + + EQUITY: 'equity', + EXPENSE: 'expense', + INCOME: 'income', +}; + +export const ACCOUNT_ROOT_TYPE = { + ASSET: 'asset', + LIABILITY: 'liability', + EQUITY: 'equity', + EXPENSE: 'expene', + INCOME: 'income', +}; + +export const ACCOUNT_NORMAL = { + CREDIT: 'credit', + DEBIT: 'debit', +}; +export const ACCOUNT_TYPES = [ + { + label: 'Cash', + key: ACCOUNT_TYPE.CASH, + normal: ACCOUNT_NORMAL.DEBIT, + parentType: ACCOUNT_PARENT_TYPE.CURRENT_ASSET, + rootType: ACCOUNT_ROOT_TYPE.ASSET, + balanceSheet: true, + incomeSheet: false, + }, + { + label: 'Bank', + key: ACCOUNT_TYPE.BANK, + normal: ACCOUNT_NORMAL.DEBIT, + parentType: ACCOUNT_PARENT_TYPE.CURRENT_ASSET, + rootType: ACCOUNT_ROOT_TYPE.ASSET, + balanceSheet: true, + incomeSheet: false, + }, + { + label: 'Accounts Receivable', + key: ACCOUNT_TYPE.ACCOUNTS_RECEIVABLE, + normal: ACCOUNT_NORMAL.DEBIT, + rootType: ACCOUNT_ROOT_TYPE.ASSET, + parentType: ACCOUNT_PARENT_TYPE.CURRENT_ASSET, + balanceSheet: true, + incomeSheet: false, + }, + { + label: 'Inventory', + key: ACCOUNT_TYPE.INVENTORY, + normal: ACCOUNT_NORMAL.DEBIT, + rootType: ACCOUNT_ROOT_TYPE.ASSET, + parentType: ACCOUNT_PARENT_TYPE.CURRENT_ASSET, + balanceSheet: true, + incomeSheet: false, + }, + { + label: 'Other Current Asset', + key: ACCOUNT_TYPE.OTHER_CURRENT_ASSET, + normal: ACCOUNT_NORMAL.DEBIT, + rootType: ACCOUNT_ROOT_TYPE.ASSET, + parentType: ACCOUNT_PARENT_TYPE.CURRENT_ASSET, + balanceSheet: true, + incomeSheet: false, + }, + { + label: 'Fixed Asset', + key: ACCOUNT_TYPE.FIXED_ASSET, + normal: ACCOUNT_NORMAL.DEBIT, + rootType: ACCOUNT_ROOT_TYPE.ASSET, + parentType: ACCOUNT_PARENT_TYPE.FIXED_ASSET, + balanceSheet: true, + incomeSheet: false, + }, + { + label: 'Non-Current Asset', + key: ACCOUNT_TYPE.NON_CURRENT_ASSET, + normal: ACCOUNT_NORMAL.DEBIT, + rootType: ACCOUNT_ROOT_TYPE.ASSET, + parentType: ACCOUNT_PARENT_TYPE.FIXED_ASSET, + balanceSheet: true, + incomeSheet: false, + }, + { + label: 'Accounts Payable', + key: ACCOUNT_TYPE.ACCOUNTS_PAYABLE, + normal: ACCOUNT_NORMAL.CREDIT, + rootType: ACCOUNT_ROOT_TYPE.LIABILITY, + parentType: ACCOUNT_PARENT_TYPE.CURRENT_LIABILITY, + balanceSheet: true, + incomeSheet: false, + }, + { + label: 'Credit Card', + key: ACCOUNT_TYPE.CREDIT_CARD, + normal: ACCOUNT_NORMAL.CREDIT, + rootType: ACCOUNT_ROOT_TYPE.LIABILITY, + parentType: ACCOUNT_PARENT_TYPE.CURRENT_LIABILITY, + balanceSheet: true, + incomeSheet: false, + }, + { + label: 'Tax Payable', + key: ACCOUNT_TYPE.TAX_PAYABLE, + normal: ACCOUNT_NORMAL.CREDIT, + rootType: ACCOUNT_ROOT_TYPE.LIABILITY, + parentType: ACCOUNT_PARENT_TYPE.CURRENT_LIABILITY, + balanceSheet: true, + incomeSheet: false, + }, + { + label: 'Other Current Liability', + key: ACCOUNT_TYPE.OTHER_CURRENT_LIABILITY, + normal: ACCOUNT_NORMAL.CREDIT, + rootType: ACCOUNT_ROOT_TYPE.LIABILITY, + parentType: ACCOUNT_PARENT_TYPE.CURRENT_LIABILITY, + balanceSheet: false, + incomeSheet: true, + }, + { + label: 'Long Term Liability', + key: ACCOUNT_TYPE.LOGN_TERM_LIABILITY, + normal: ACCOUNT_NORMAL.CREDIT, + rootType: ACCOUNT_ROOT_TYPE.LIABILITY, + parentType: ACCOUNT_PARENT_TYPE.LOGN_TERM_LIABILITY, + balanceSheet: false, + incomeSheet: true, + }, + { + label: 'Non-Current Liability', + key: ACCOUNT_TYPE.NON_CURRENT_LIABILITY, + normal: ACCOUNT_NORMAL.CREDIT, + rootType: ACCOUNT_ROOT_TYPE.LIABILITY, + parentType: ACCOUNT_PARENT_TYPE.NON_CURRENT_LIABILITY, + balanceSheet: false, + incomeSheet: true, + }, + { + label: 'Equity', + key: ACCOUNT_TYPE.EQUITY, + normal: ACCOUNT_NORMAL.CREDIT, + rootType: ACCOUNT_ROOT_TYPE.EQUITY, + parentType: ACCOUNT_PARENT_TYPE.EQUITY, + balanceSheet: true, + incomeSheet: false, + }, + { + label: 'Income', + key: ACCOUNT_TYPE.INCOME, + normal: ACCOUNT_NORMAL.CREDIT, + rootType: ACCOUNT_ROOT_TYPE.INCOME, + parentType: ACCOUNT_PARENT_TYPE.INCOME, + balanceSheet: false, + incomeSheet: true, + }, + { + label: 'Other Income', + key: ACCOUNT_TYPE.OTHER_INCOME, + normal: ACCOUNT_NORMAL.CREDIT, + rootType: ACCOUNT_ROOT_TYPE.INCOME, + parentType: ACCOUNT_PARENT_TYPE.INCOME, + balanceSheet: false, + incomeSheet: true, + }, + { + label: 'Cost of Goods Sold', + key: ACCOUNT_TYPE.COST_OF_GOODS_SOLD, + normal: ACCOUNT_NORMAL.DEBIT, + rootType: ACCOUNT_ROOT_TYPE.EXPENSE, + parentType: ACCOUNT_PARENT_TYPE.EXPENSE, + balanceSheet: false, + incomeSheet: true, + }, + { + label: 'Expense', + key: ACCOUNT_TYPE.EXPENSE, + normal: ACCOUNT_NORMAL.DEBIT, + rootType: ACCOUNT_ROOT_TYPE.EXPENSE, + parentType: ACCOUNT_PARENT_TYPE.EXPENSE, + balanceSheet: false, + incomeSheet: true, + }, + { + label: 'Other Expense', + key: ACCOUNT_TYPE.OTHER_EXPENSE, + normal: ACCOUNT_NORMAL.DEBIT, + rootType: ACCOUNT_ROOT_TYPE.EXPENSE, + parentType: ACCOUNT_PARENT_TYPE.EXPENSE, + balanceSheet: false, + incomeSheet: true, + }, +]; diff --git a/client/src/components/App.js b/client/src/components/App.js index 00df2b8aa..435d11a9b 100644 --- a/client/src/components/App.js +++ b/client/src/components/App.js @@ -2,9 +2,7 @@ import React from 'react'; import { RawIntlProvider } from 'react-intl'; import { Router, Switch, Route } from 'react-router'; import { createBrowserHistory } from 'history'; -import { ReactQueryConfigProvider } from 'react-query'; -import { ReactQueryDevtools } from 'react-query-devtools'; - +import { QueryClientProvider, QueryClient } from 'react-query'; import 'style/App.scss'; import PrivateRoute from 'components/Guards/PrivateRoute'; @@ -17,14 +15,18 @@ function App({ locale }) { const history = createBrowserHistory(); const queryConfig = { - queries: { - refetchOnWindowFocus: false, + defaultOptions: { + queries: { + refetchOnWindowFocus: false, + }, }, }; + const queryClient = new QueryClient(queryConfig); + return ( -
- + +
@@ -38,9 +40,8 @@ function App({ locale }) { - - -
+
+
); } diff --git a/client/src/components/CategoriesSelectList.js b/client/src/components/CategoriesSelectList.js index 52bf07a14..1345d1f6c 100644 --- a/client/src/components/CategoriesSelectList.js +++ b/client/src/components/CategoriesSelectList.js @@ -7,7 +7,7 @@ import classNames from 'classnames'; import { CLASSES } from 'common/classes'; export default function CategoriesSelectList({ - categoriesList, + categories, selecetedCategoryId, defaultSelectText = , onCategorySelected, @@ -41,7 +41,7 @@ export default function CategoriesSelectList({ return ( void; +} + +const POPPER_MODIFIERS = { + preventOverflow: { boundariesElement: "viewport" }, +}; +const TRANSITION_DURATION = 100; + +// type IContextMenuProps = IOverlayLifecycleProps; + +/* istanbul ignore next */ + +export default class ContextMenu extends React.PureComponent { + public state: IContextMenuState = { + isDarkTheme: false, + isOpen: false, + }; + + public render() { + // prevent right-clicking in a context menu + const content =
{this.state.menu}
; + const popoverClassName = {}; + + // HACKHACK: workaround until we have access to Popper#scheduleUpdate(). + // https://github.com/palantir/blueprint/issues/692 + // Generate key based on offset so a new Popover instance is created + // when offset changes, to force recomputing position. + const key = this.state.offset === undefined ? "" : `${this.state.offset.left}x${this.state.offset.top}`; + + // wrap the popover in a positioned div to make sure it is properly + // offset on the screen. + return ( +
+ } + transitionDuration={TRANSITION_DURATION} + /> +
+ ); + + } + + public show(menu: JSX.Element, offset: IOffset, onClose?: () => void, isDarkTheme = false) { + this.setState({ isOpen: true, menu, offset, onClose, isDarkTheme }); + } + + public hide() { + this.state.onClose?.(); + this.setState({ isOpen: false, onClose: undefined }); + } + + private cancelContextMenu = (e: React.SyntheticEvent) => e.preventDefault(); + + private handleBackdropContextMenu = (e: React.MouseEvent) => { + // React function to remove from the event pool, useful when using a event within a callback + e.persist(); + e.preventDefault(); + // wait for backdrop to disappear so we can find the "real" element at event coordinates. + // timeout duration is equivalent to transition duration so we know it's animated out. + setTimeout(() => { + // retrigger context menu event at the element beneath the backdrop. + // if it has a `contextmenu` event handler then it'll be invoked. + // if it doesn't, no native menu will show (at least on OSX) :( + const newTarget = document.elementFromPoint(e.clientX, e.clientY); + const { view, ...newEventInit } = e; + newTarget?.dispatchEvent(new MouseEvent("contextmenu", newEventInit)); + }, TRANSITION_DURATION); + }; + + private handlePopoverInteraction = (nextOpenState: boolean) => { + if (!nextOpenState) { + // delay the actual hiding till the event queue clears + // to avoid flicker of opening twice + this.hide(); + } + }; +} \ No newline at end of file diff --git a/client/src/components/Dashboard/DashboardViewsTabs.js b/client/src/components/Dashboard/DashboardViewsTabs.js index e07ebcf01..92fbfcb85 100644 --- a/client/src/components/Dashboard/DashboardViewsTabs.js +++ b/client/src/components/Dashboard/DashboardViewsTabs.js @@ -5,9 +5,11 @@ import { Button, Tabs, Tab, Tooltip, Position } from '@blueprintjs/core'; import { debounce } from 'lodash'; import { useHistory } from 'react-router'; import { If, Icon } from 'components'; +import { saveInvoke } from 'utils'; export default function DashboardViewsTabs({ initialViewId = 0, + viewId, tabs, defaultTabText = , allTab = true, @@ -26,16 +28,16 @@ export default function DashboardViewsTabs({ }; const handleTabClick = (viewId) => { - onTabClick && onTabClick(viewId); + saveInvoke(onTabClick, viewId); }; const mappedTabs = useMemo( () => tabs.map((tab) => ({ ...tab, onTabClick: handleTabClick })), - [tabs], + [tabs, handleTabClick], ); const handleViewLinkClick = () => { - onNewViewTabClick && onNewViewTabClick(); + saveInvoke(onNewViewTabClick); }; const debounceChangeHistory = useRef( @@ -49,7 +51,7 @@ export default function DashboardViewsTabs({ debounceChangeHistory.current(`/${resourceName}/${toPath}`); setCurrentView(viewId); - onChange && onChange(viewId); + saveInvoke(onChange, viewId); }; return ( diff --git a/client/src/components/Dashboard/PrivatePages.js b/client/src/components/Dashboard/PrivatePages.js index 55c981191..1534d4078 100644 --- a/client/src/components/Dashboard/PrivatePages.js +++ b/client/src/components/Dashboard/PrivatePages.js @@ -26,15 +26,14 @@ function DashboardPrivatePages({ // #withSubscriptionsActions requestFetchSubscriptions, }) { - // Fetch all user's organizatins. + // Fetches all user's organizatins. const fetchOrganizations = useQuery( ['organizations'], () => requestAllOrganizations(), ); - // Fetchs organization subscriptions. + // Fetches organization subscriptions. const fetchSuscriptions = useQuery( ['susbcriptions'], () => requestFetchSubscriptions(), - { enabled: fetchOrganizations.data }, ) return ( diff --git a/client/src/components/DataTable.js b/client/src/components/DataTable.js index 2abbe1047..b6d07e4a1 100644 --- a/client/src/components/DataTable.js +++ b/client/src/components/DataTable.js @@ -83,6 +83,7 @@ export default function DataTable(props) { minWidth: selectionColumnWidth, width: selectionColumnWidth, maxWidth: selectionColumnWidth, + skeletonWidthMin: 100, // The header can use the table's getToggleAllRowsSelectedProps method // to render a checkbox Header: TableIndeterminateCheckboxHeader, @@ -198,4 +199,7 @@ DataTable.defaultProps = { TableTBodyRenderer: TableTBody, TablePaginationRenderer: TablePagination, TableNoResultsRowRenderer: TableNoResultsRow, + + noResults: 'There is no results in the table.', + payload: {}, }; \ No newline at end of file diff --git a/client/src/components/Datatable/TableHeader.js b/client/src/components/Datatable/TableHeader.js index 91995b3c5..145296386 100644 --- a/client/src/components/Datatable/TableHeader.js +++ b/client/src/components/Datatable/TableHeader.js @@ -27,11 +27,13 @@ function TableHeaderCell({ column, index }) { -
+
{column.render('Header')} @@ -74,9 +76,13 @@ function TableHeaderGroup({ headerGroup }) { */ export default function TableHeader() { const { - table: { headerGroups }, + table: { headerGroups, page }, + props: { TableHeaderSkeletonRenderer, headerLoading }, } = useContext(TableContext); + if (headerLoading && TableHeaderSkeletonRenderer) { + return ; + } return (
diff --git a/client/src/components/Datatable/TableHeaderSkeleton.js b/client/src/components/Datatable/TableHeaderSkeleton.js new file mode 100644 index 000000000..e5f2baba7 --- /dev/null +++ b/client/src/components/Datatable/TableHeaderSkeleton.js @@ -0,0 +1,42 @@ +import React, { useContext } from 'react'; +import TableContext from './TableContext'; +import { Skeleton } from 'components'; + +function TableHeaderCell({ column }) { + const { skeletonWidthMax = 100, skeletonWidthMin = 40 } = column; + + return ( +
+ +
+ ); +} + +/** + * Table skeleton rows. + */ +export default function TableSkeletonHeader({}) { + const { + table: { headerGroups }, + } = useContext(TableContext); + + return ( +
+ {headerGroups.map((headerGroup) => ( +
+ {headerGroup.headers.map((column) => ( + + ))} +
+ ))} +
+ ); +} diff --git a/client/src/components/Datatable/TableRows.js b/client/src/components/Datatable/TableRows.js index 95c073f60..cd27400f0 100644 --- a/client/src/components/Datatable/TableRows.js +++ b/client/src/components/Datatable/TableRows.js @@ -9,7 +9,7 @@ export default function TableRows() { table: { prepareRow, page }, props: { TableRowRenderer, TableCellRenderer }, } = useContext(TableContext); - + return page.map((row) => { prepareRow(row); return ; diff --git a/client/src/components/Datatable/TableSkeletonRows.js b/client/src/components/Datatable/TableSkeletonRows.js new file mode 100644 index 000000000..cf1d74e89 --- /dev/null +++ b/client/src/components/Datatable/TableSkeletonRows.js @@ -0,0 +1,44 @@ +import React, { useContext } from 'react'; +import TableContext from './TableContext'; +import { Skeleton } from 'components'; + +/** + * Table header cell. + */ +function TableHeaderCell({ column }) { + const { skeletonWidthMax = 100, skeletonWidthMin = 40 } = column; + + return ( +
+ +
+ ); +} + +/** + * Table skeleton rows. + */ +export default function TableSkeletonRows({}) { + const { + table: { headerGroups }, + } = useContext(TableContext); + const skeletonRows = 10; + + return Array.from({ length: skeletonRows }).map(() => { + return headerGroups.map((headerGroup) => ( +
+ {headerGroup.headers.map((column) => ( + + ))} +
+ )); + }); +} diff --git a/client/src/components/DialogsContainer.js b/client/src/components/DialogsContainer.js index b13dbaabf..a84df5e5f 100644 --- a/client/src/components/DialogsContainer.js +++ b/client/src/components/DialogsContainer.js @@ -11,9 +11,11 @@ import EstimateNumberDialog from 'containers/Dialogs/EstimateNumberDialog'; import ReceiptNumberDialog from 'containers/Dialogs/ReceiptNumberDialog'; import InvoiceNumberDialog from 'containers/Dialogs/InvoiceNumberDialog'; import InventoryAdjustmentDialog from 'containers/Dialogs/InventoryAdjustmentFormDialog'; - import PaymentViaVoucherDialog from 'containers/Dialogs/PaymentViaVoucherDialog'; +/** + * Dialogs container. + */ export default function DialogsContainer() { return (
@@ -27,7 +29,7 @@ export default function DialogsContainer() { - +
); diff --git a/client/src/components/Skeleton.js b/client/src/components/Skeleton.js new file mode 100644 index 000000000..df6df2c25 --- /dev/null +++ b/client/src/components/Skeleton.js @@ -0,0 +1,19 @@ +import React, { useMemo } from 'react'; +import 'style/components/Skeleton.scss'; + +import { randomNumber } from 'utils'; + +/** + * Skeleton component. + */ +export default function Skeleton({ + Tag = 'span', + minWidth = 40, + maxWidth = 100, +}) { + const randomWidth = useMemo(() => randomNumber(minWidth, maxWidth), [ + minWidth, + maxWidth, + ]); + return ; +} diff --git a/client/src/components/index.js b/client/src/components/index.js index 67b239915..3084f8c9b 100644 --- a/client/src/components/index.js +++ b/client/src/components/index.js @@ -44,7 +44,7 @@ import InputPrependText from './Forms/InputPrependText'; import PageFormBigNumber from './PageFormBigNumber'; import AccountsMultiSelect from './AccountsMultiSelect'; import CustomersMultiSelect from './CustomersMultiSelect'; - +import Skeleton from './Skeleton' import TableFastCell from './Datatable/TableFastCell'; @@ -97,6 +97,6 @@ export { AccountsMultiSelect, DataTableEditable, CustomersMultiSelect, - TableFastCell, + Skeleton, }; diff --git a/client/src/config/sidebarMenu.js b/client/src/config/sidebarMenu.js index d5dea66c3..4f4d90032 100644 --- a/client/src/config/sidebarMenu.js +++ b/client/src/config/sidebarMenu.js @@ -171,7 +171,8 @@ export default [ ], }, { - divider: true, + text: , + label: true, }, { text: , diff --git a/client/src/containers/Accounting/MakeJournalEntriesField.js b/client/src/containers/Accounting/MakeJournalEntriesField.js index 86f5e1c72..3286b422a 100644 --- a/client/src/containers/Accounting/MakeJournalEntriesField.js +++ b/client/src/containers/Accounting/MakeJournalEntriesField.js @@ -7,7 +7,6 @@ import { orderingLinesIndexes, repeatValue } from 'utils'; export default function MakeJournalEntriesField({ defaultRow, - linesNumber = 4, }) { return ( diff --git a/client/src/containers/Accounting/MakeJournalEntriesForm.js b/client/src/containers/Accounting/MakeJournalEntriesForm.js index 1cf3e2dcf..673105ef1 100644 --- a/client/src/containers/Accounting/MakeJournalEntriesForm.js +++ b/client/src/containers/Accounting/MakeJournalEntriesForm.js @@ -18,23 +18,20 @@ import MakeJournalEntriesField from './MakeJournalEntriesField'; import MakeJournalNumberWatcher from './MakeJournalNumberWatcher'; import MakeJournalFormFooter from './MakeJournalFormFooter'; -import withJournalsActions from 'containers/Accounting/withJournalsActions'; -import withManualJournalDetail from 'containers/Accounting/withManualJournalDetail'; -import withAccountsActions from 'containers/Accounts/withAccountsActions'; import withDashboardActions from 'containers/Dashboard/withDashboardActions'; import withSettings from 'containers/Settings/withSettings'; import AppToaster from 'components/AppToaster'; -import Dragzone from 'components/Dragzone'; import withMediaActions from 'containers/Media/withMediaActions'; import { compose, repeatValue, orderingLinesIndexes, defaultToTransform, + transactionNumber, } from 'utils'; import { transformErrors } from './utils'; -import withManualJournalsActions from './withManualJournalsActions'; +import { useMakeJournalFormContext } from './MakeJournalProvider'; const defaultEntry = { index: 0, @@ -59,18 +56,6 @@ const defaultInitialValues = { * Journal entries form. */ function MakeJournalEntriesForm({ - // #withMedia - requestSubmitMedia, - requestDeleteMedia, - - // #withJournalsActions - requestMakeJournalEntries, - requestEditManualJournal, - setJournalNumberChanged, - - // #withManualJournals - journalNumberChanged, - // #withDashboard changePageTitle, changePageSubtitle, @@ -79,30 +64,33 @@ function MakeJournalEntriesForm({ journalNextNumber, journalNumberPrefix, baseCurrency, - // #ownProps - manualJournalId, - manualJournal, - onFormSubmit, - onCancelForm, }) { - const isNewMode = !manualJournalId; - const [submitPayload, setSubmitPayload] = useState({}); + const { + createJournalMutate, + editJournalMutate, + isNewMode, + manualJournal, + submitPayload, + } = useMakeJournalFormContext(); + const { formatMessage } = useIntl(); const history = useHistory(); - const journalNumber = isNewMode - ? `${journalNumberPrefix}-${journalNextNumber}` - : journalNextNumber; - + // New journal number. + const journalNumber = transactionNumber( + journalNumberPrefix, + journalNextNumber, + ); + // Changes the page title based on the form in new and edit mode. useEffect(() => { const transactionNumber = manualJournal ? manualJournal.journal_number : journalNumber; - if (manualJournal && manualJournal.id) { - changePageTitle(formatMessage({ id: 'edit_journal' })); - } else { + if (isNewMode) { changePageTitle(formatMessage({ id: 'new_journal' })); + } else { + changePageTitle(formatMessage({ id: 'edit_journal' })); } changePageSubtitle( defaultToTransform(transactionNumber, `No. ${transactionNumber}`, ''), @@ -113,6 +101,7 @@ function MakeJournalEntriesForm({ journalNumber, manualJournal, formatMessage, + isNewMode, ]); const initialValues = useMemo( @@ -131,7 +120,7 @@ function MakeJournalEntriesForm({ entries: orderingLinesIndexes(defaultInitialValues.entries), }), }), - [manualJournal, journalNumber], + [manualJournal, baseCurrency, journalNumber], ); // Handle journal number field change. @@ -144,6 +133,7 @@ function MakeJournalEntriesForm({ [changePageSubtitle], ); + // Handle the form submiting. const handleSubmit = (values, { setErrors, setSubmitting, resetForm }) => { setSubmitting(true); const entries = values.entries.filter( @@ -179,11 +169,13 @@ function MakeJournalEntriesForm({ } const form = { ...values, publish: submitPayload.publish, entries }; + // Handle the request error. const handleError = (error) => { transformErrors(error, { setErrors }); setSubmitting(false); }; + // Handle the request success. const handleSuccess = (errors) => { AppToaster.show({ message: formatMessage( @@ -196,7 +188,6 @@ function MakeJournalEntriesForm({ ), intent: Intent.SUCCESS, }); - setSubmitting(false); if (submitPayload.redirect) { @@ -208,25 +199,14 @@ function MakeJournalEntriesForm({ }; if (isNewMode) { - requestMakeJournalEntries(form).then(handleSuccess).catch(handleError); + createJournalMutate(form).then(handleSuccess).catch(handleError); } else { - requestEditManualJournal(manualJournal.id, form) + editJournalMutate(manualJournal.id, form) .then(handleSuccess) .catch(handleError); } }; - const handleCancelClick = useCallback(() => { - history.goBack(); - }, [history]); - - const handleSubmitClick = useCallback( - (event, payload) => { - setSubmitPayload({ ...payload }); - }, - [setSubmitPayload], - ); - return (
- {({ isSubmitting }) => ( -
- - - - - - - )} +
+ + + + + + - {/* */}
); } export default compose( - withJournalsActions, - withManualJournalDetail(), - withAccountsActions, withDashboardActions, withMediaActions, withSettings(({ manualJournalsSettings, organizationSettings }) => ({ @@ -280,5 +242,4 @@ export default compose( journalNumberPrefix: manualJournalsSettings?.numberPrefix, baseCurrency: organizationSettings?.baseCurrency, })), - withManualJournalsActions, )(MakeJournalEntriesForm); diff --git a/client/src/containers/Accounting/MakeJournalEntriesHeaderFields.js b/client/src/containers/Accounting/MakeJournalEntriesHeaderFields.js index ca2ae619b..3600deaa3 100644 --- a/client/src/containers/Accounting/MakeJournalEntriesHeaderFields.js +++ b/client/src/containers/Accounting/MakeJournalEntriesHeaderFields.js @@ -21,25 +21,24 @@ import { CurrencySelectList, } from 'components'; +import { useMakeJournalFormContext } from './MakeJournalProvider'; import withDialogActions from 'containers/Dialog/withDialogActions'; -import withCurrencies from 'containers/Currencies/withCurrencies'; import { compose, inputIntent, handleDateChange } from 'utils'; function MakeJournalEntriesHeader({ // #ownProps - manualJournal, onJournalNumberChanged, - // #withCurrencies - currenciesList, - // #withDialog openDialog, }) { - const handleJournalNumberChange = useCallback(() => { + const { currencies } = useMakeJournalFormContext(); + + // Handle journal number change. + const handleJournalNumberChange = () => { openDialog('journal-number-form', {}); - }, [openDialog]); + }; // Handle journal number field blur event. const handleJournalNumberChanged = (event) => { @@ -165,7 +164,7 @@ function MakeJournalEntriesHeader({ inline={true} > { form.setFieldValue('currency_code', currencyItem.currency_code); @@ -181,7 +180,4 @@ function MakeJournalEntriesHeader({ export default compose( withDialogActions, - withCurrencies(({ currenciesList }) => ({ - currenciesList, - })), )(MakeJournalEntriesHeader); diff --git a/client/src/containers/Accounting/MakeJournalEntriesPage.js b/client/src/containers/Accounting/MakeJournalEntriesPage.js index d129d3423..dfc405bf3 100644 --- a/client/src/containers/Accounting/MakeJournalEntriesPage.js +++ b/client/src/containers/Accounting/MakeJournalEntriesPage.js @@ -1,44 +1,26 @@ import React, { useCallback, useEffect } from 'react'; import { useParams, useHistory } from 'react-router-dom'; -import { useQuery } from 'react-query'; import MakeJournalEntriesForm from './MakeJournalEntriesForm'; -import DashboardInsider from 'components/Dashboard/DashboardInsider'; +import { MakeJournalProvider } from './MakeJournalProvider'; -import withCustomersActions from 'containers/Customers/withCustomersActions'; -import withAccountsActions from 'containers/Accounts/withAccountsActions'; -import withManualJournalsActions from 'containers/Accounting/withManualJournalsActions'; -import withCurrenciesActions from 'containers/Currencies/withCurrenciesActions'; -import withSettingsActions from 'containers/Settings/withSettingsActions'; 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. + */ function MakeJournalEntriesPage({ - // #withCustomersActions - requestFetchCustomers, - - // #withAccountsActions - requestFetchAccounts, - - // #withManualJournalActions - requestFetchManualJournal, - - // #wihtCurrenciesActions - requestFetchCurrencies, - - // #withSettingsActions - requestFetchOptions, - // #withDashboardActions setSidebarShrink, resetSidebarPreviousExpand, - setDashboardBackLink + setDashboardBackLink, }) { const history = useHistory(); - const { id } = useParams(); + const { id: journalId } = useParams(); useEffect(() => { // Shrink the sidebar by foce. @@ -52,27 +34,7 @@ function MakeJournalEntriesPage({ // Hide the back link on dashboard topbar. setDashboardBackLink(false); }; - }, [resetSidebarPreviousExpand, setSidebarShrink]); - - const fetchAccounts = useQuery('accounts-list', (key) => - requestFetchAccounts(), - ); - - const fetchCustomers = useQuery('customers-list', (key) => - requestFetchCustomers(), - ); - - const fetchCurrencies = useQuery('currencies', () => - requestFetchCurrencies(), - ); - - const fetchSettings = useQuery(['settings'], () => requestFetchOptions({})); - - const fetchJournal = useQuery( - ['manual-journal', id], - (key, journalId) => requestFetchManualJournal(journalId), - { enabled: id && id }, - ); + }, [resetSidebarPreviousExpand, setDashboardBackLink, setSidebarShrink]); const handleFormSubmit = useCallback( (payload) => { @@ -86,29 +48,15 @@ function MakeJournalEntriesPage({ }, [history]); return ( - + - + ); } export default compose( - withAccountsActions, - withCustomersActions, - withManualJournalsActions, - withCurrenciesActions, - withSettingsActions, withDashboardActions, )(MakeJournalEntriesPage); diff --git a/client/src/containers/Accounting/MakeJournalEntriesTable.js b/client/src/containers/Accounting/MakeJournalEntriesTable.js index d9ecba07c..dbc089489 100644 --- a/client/src/containers/Accounting/MakeJournalEntriesTable.js +++ b/client/src/containers/Accounting/MakeJournalEntriesTable.js @@ -2,7 +2,7 @@ import React, { useState, useMemo, useEffect, useCallback } from 'react'; import { Button } from '@blueprintjs/core'; import { FormattedMessage as T, useIntl } from 'react-intl'; import { omit } from 'lodash'; -import { compose, saveInvoke } from 'utils'; +import { saveInvoke } from 'utils'; import { AccountsListFieldCell, MoneyFieldCell, @@ -18,21 +18,13 @@ import { } from './components'; import { DataTableEditable } from 'components'; -import withAccounts from 'containers/Accounts/withAccounts'; -import withCustomers from 'containers/Customers/withCustomers'; - import { updateDataReducer } from './utils'; +import { useMakeJournalFormContext } from './MakeJournalProvider'; /** * Make journal entries table component. */ -function MakeJournalEntriesTable({ - // #withCustomers - customers, - - // #withAccounts - accountsList, - +export default function MakeJournalEntriesTable({ // #ownPorps onClickRemoveRow, onClickAddNewRow, @@ -44,6 +36,8 @@ function MakeJournalEntriesTable({ const [rows, setRows] = useState([]); const { formatMessage } = useIntl(); + const { accounts, customers } = useMakeJournalFormContext(); + useEffect(() => { setRows([...entries.map((e) => ({ ...e, rowType: 'editor' }))]); }, [entries, setRows]); @@ -175,7 +169,7 @@ function MakeJournalEntriesTable({ sticky={true} totalRow={true} payload={{ - accounts: accountsList, + accounts, errors: error, updateData: handleUpdateData, removeRow: handleRemoveRow, @@ -209,12 +203,3 @@ function MakeJournalEntriesTable({ /> ); } - -export default compose( - withAccounts(({ accountsList }) => ({ - accountsList, - })), - withCustomers(({ customers }) => ({ - customers, - })), -)(MakeJournalEntriesTable); diff --git a/client/src/containers/Accounting/MakeJournalFormFloatingActions.js b/client/src/containers/Accounting/MakeJournalFormFloatingActions.js index 4df73ae9e..1d367b614 100644 --- a/client/src/containers/Accounting/MakeJournalFormFloatingActions.js +++ b/client/src/containers/Accounting/MakeJournalFormFloatingActions.js @@ -12,73 +12,64 @@ import { import { useFormikContext } from 'formik'; import classNames from 'classnames'; import { FormattedMessage as T } from 'react-intl'; -import { saveInvoke } from 'utils'; import { CLASSES } from 'common/classes'; import { Icon, If } from 'components'; +import { useHistory } from 'react-router-dom'; +import { useMakeJournalFormContext } from './MakeJournalProvider'; /** * Make Journal floating actions bar. */ -export default function MakeJournalFloatingAction({ - isSubmitting, - onSubmitClick, - onCancelClick, - manualJournal, -}) { - const { submitForm, resetForm } = useFormikContext(); +export default function MakeJournalFloatingAction() { + const history = useHistory(); + // Formik context. + const { submitForm, resetForm, isSubmitting } = useFormikContext(); + + // Make journal form context. + const { setSubmitPayload, manualJournal } = useMakeJournalFormContext(); + + // Handle submit & publish button click. const handleSubmitPublishBtnClick = (event) => { - saveInvoke(onSubmitClick, event, { - redirect: true, - publish: true, - }); + submitForm(); + setSubmitPayload({ redirect: true, publish: true }); }; + // Handle submit, publish & new button click. const handleSubmitPublishAndNewBtnClick = (event) => { submitForm(); - saveInvoke(onSubmitClick, event, { - redirect: false, - publish: true, - resetForm: true, - }); + setSubmitPayload({ redirect: false, publish: true, resetForm: true }); }; + // Handle submit, publish & edit button click. const handleSubmitPublishContinueEditingBtnClick = (event) => { submitForm(); - saveInvoke(onSubmitClick, event, { - redirect: false, - publish: true, - }); + setSubmitPayload({ redirect: false, publish: true }); }; + // Handle submit as draft button click. const handleSubmitDraftBtnClick = (event) => { - saveInvoke(onSubmitClick, event, { - redirect: true, - publish: false, - }); + setSubmitPayload({ redirect: true, publish: false }); }; + // Handle submit as draft & new button click. const handleSubmitDraftAndNewBtnClick = (event) => { submitForm(); - saveInvoke(onSubmitClick, event, { - redirect: false, - publish: false, - resetForm: true, - }); + setSubmitPayload({ redirect: false, publish: false, resetForm: true }); }; + // Handle submit as draft & continue editing button click. const handleSubmitDraftContinueEditingBtnClick = (event) => { submitForm(); - saveInvoke(onSubmitClick, event, { - redirect: false, - publish: false, - }); + setSubmitPayload({ redirect: false, publish: false }); }; + // Handle cancel button click. const handleCancelBtnClick = (event) => { - saveInvoke(onCancelClick, event); + history.goBack(); }; + // Handle clear button click. const handleClearBtnClick = (event) => { resetForm(); }; @@ -90,6 +81,7 @@ export default function MakeJournalFloatingAction({ @@ -49,3 +65,7 @@ export default function InventoryAdjustmentFloatingActions({
); } + +export default compose( + withDialogActions +)(InventoryAdjustmentFloatingActions); \ No newline at end of file diff --git a/client/src/containers/Dialogs/InventoryAdjustmentFormDialog/InventoryAdjustmentForm.js b/client/src/containers/Dialogs/InventoryAdjustmentFormDialog/InventoryAdjustmentForm.js new file mode 100644 index 000000000..0b7ed4713 --- /dev/null +++ b/client/src/containers/Dialogs/InventoryAdjustmentFormDialog/InventoryAdjustmentForm.js @@ -0,0 +1,90 @@ +import React from 'react'; +import moment from 'moment'; +import { Intent } from '@blueprintjs/core'; +import { Formik } from 'formik'; +import { omit, get } from 'lodash'; +import { useIntl } from 'react-intl'; + +import 'style/pages/Items/ItemAdjustmentDialog.scss'; + +import { AppToaster } from 'components'; +import { CreateInventoryAdjustmentFormSchema } from './InventoryAdjustmentForm.schema'; + +import InventoryAdjustmentFormContent from './InventoryAdjustmentFormContent'; +import { useInventoryAdjContext } from './InventoryAdjustmentFormProvider'; + +import withDialogActions from 'containers/Dialog/withDialogActions'; +import { compose } from 'utils'; + +const defaultInitialValues = { + date: moment(new Date()).format('YYYY-MM-DD'), + type: 'decrement', + adjustment_account_id: '', + item_id: '', + reason: '', + cost: '', + quantity: '', + reference_no: '', + quantity_on_hand: '', + publish: '', +}; + +/** + * Inventory adjustment form. + */ +function InventoryAdjustmentForm({ + // #withDialogActions + closeDialog, +}) { + const { + dialogName, + item, + itemId, + submitPayload, + createInventoryAdjMutate, + } = useInventoryAdjContext(); + + const { formatMessage } = useIntl(); + + // Initial form values. + const initialValues = { + ...defaultInitialValues, + item_id: itemId, + quantity_on_hand: get(item, 'quantity_on_hand', 0), + }; + + // Handles the form submit. + const handleFormSubmit = (values, { setSubmitting, setErrors }) => { + const form = { + ...omit(values, ['quantity_on_hand', 'new_quantity', 'action']), + publish: submitPayload.publish, + }; + setSubmitting(true); + createInventoryAdjMutate(form) + .then(() => { + closeDialog(dialogName); + + AppToaster.show({ + message: formatMessage({ + id: 'the_make_adjustment_has_been_created_successfully', + }), + intent: Intent.SUCCESS, + }); + }) + .finally(() => { + setSubmitting(true); + }); + }; + + return ( + + + + ); +} + +export default compose(withDialogActions)(InventoryAdjustmentForm); diff --git a/client/src/containers/Dialogs/InventoryAdjustmentFormDialog/InventoryAdjustmentFormContent.js b/client/src/containers/Dialogs/InventoryAdjustmentFormDialog/InventoryAdjustmentFormContent.js new file mode 100644 index 000000000..8d4e39c51 --- /dev/null +++ b/client/src/containers/Dialogs/InventoryAdjustmentFormDialog/InventoryAdjustmentFormContent.js @@ -0,0 +1,16 @@ +import React from 'react'; +import { Form } from 'formik'; +import InventoryAdjustmentFormDialogFields from './InventoryAdjustmentFormDialogFields'; +import InventoryAdjustmentFloatingActions from './InventoryAdjustmentFloatingActions'; + +/** + * Inventory adjustment form content. + */ +export default function InventoryAdjustmentFormContent() { + return ( +
+ + + + ); +} diff --git a/client/src/containers/Dialogs/InventoryAdjustmentFormDialog/InventoryAdjustmentFormDialogContent.js b/client/src/containers/Dialogs/InventoryAdjustmentFormDialog/InventoryAdjustmentFormDialogContent.js index 65040ec5a..7ad032d07 100644 --- a/client/src/containers/Dialogs/InventoryAdjustmentFormDialog/InventoryAdjustmentFormDialogContent.js +++ b/client/src/containers/Dialogs/InventoryAdjustmentFormDialog/InventoryAdjustmentFormDialogContent.js @@ -1,142 +1,21 @@ -import React, { useState, useCallback } from 'react'; -import { Intent } from '@blueprintjs/core'; -import { Formik, Form } from 'formik'; -import { useIntl } from 'react-intl'; -import { useQuery, queryCache } from 'react-query'; -import moment from 'moment'; -import { omit, get } from 'lodash'; - +import React from 'react'; + import 'style/pages/Items/ItemAdjustmentDialog.scss'; - -import { AppToaster, DialogContent } from 'components'; - -import { CreateInventoryAdjustmentFormSchema } from './InventoryAdjustmentForm.schema'; - -import InventoryAdjustmentFormDialogFields from './InventoryAdjustmentFormDialogFields'; -import InventoryAdjustmentFloatingActions from './InventoryAdjustmentFloatingActions'; - -import withDialogActions from 'containers/Dialog/withDialogActions'; -import withInventoryAdjustmentActions from 'containers/Items/withInventoryAdjustmentActions'; -import withAccountsActions from 'containers/Accounts/withAccountsActions'; -import withItem from 'containers/Items/withItem'; -import withItemsActions from 'containers/Items/withItemsActions'; - -import { compose } from 'utils'; - -const defaultInitialValues = { - date: moment(new Date()).format('YYYY-MM-DD'), - type: 'decrement', - adjustment_account_id: '', - item_id: '', - reason: '', - cost: '', - quantity: '', - reference_no: '', - quantity_on_hand: '', - publish: '', -}; + +import { InventoryAdjustmentFormProvider } from './InventoryAdjustmentFormProvider'; +import InventoryAdjustmentForm from './InventoryAdjustmentForm'; /** * Inventory adjustment form dialog content. */ -function InventoryAdjustmentFormDialogContent({ - // #withDialogActions - closeDialog, - - // #withAccountsActions - requestFetchAccounts, - - // #withInventoryAdjustmentActions - requestSubmitInventoryAdjustment, - - // #withItemsActions - requestFetchItem, - - // #withItem - item, - - // #ownProp - itemId, +export default function InventoryAdjustmentFormDialogContent({ + // #ownProps dialogName, + itemId }) { - const { formatMessage } = useIntl(); - const [submitPayload, setSubmitPayload] = useState({}); - - // Fetches accounts list. - const fetchAccount = useQuery('accounts-list', () => requestFetchAccounts()); - - // Fetches the item details. - const fetchItem = useQuery(['item', itemId], - (key, id) => requestFetchItem(id)); - - // Initial form values. - const initialValues = { - ...defaultInitialValues, - item_id: itemId, - quantity_on_hand: get(item, 'quantity_on_hand', 0), - }; - - // Handles the form submit. - const handleFormSubmit = (values, { setSubmitting, setErrors }) => { - const form = { - ...omit(values, ['quantity_on_hand', 'new_quantity', 'action']), - publish: submitPayload.publish, - }; - const onSuccess = ({ response }) => { - closeDialog(dialogName); - queryCache.invalidateQueries('accounts-list'); - queryCache.invalidateQueries('items-table'); - - AppToaster.show({ - message: formatMessage({ - id: 'the_make_adjustment_has_been_created_successfully', - }), - intent: Intent.SUCCESS, - }); - }; - const onError = (error) => { - setSubmitting(false); - }; - requestSubmitInventoryAdjustment({ form }).then(onSuccess).catch(onError); - }; - - // Handles dialog close. - const handleCloseClick = useCallback(() => { - closeDialog(dialogName); - }, [closeDialog, dialogName]); - - const handleSubmitClick = useCallback( - (event, payload) => { - setSubmitPayload({ ...payload }); - }, - [setSubmitPayload], - ); - return ( - - -
- - - -
-
+ + + ); } - -export default compose( - withInventoryAdjustmentActions, - withDialogActions, - withAccountsActions, - withItem(({ item }) => ({ - item: item - })), - withItemsActions, -)(InventoryAdjustmentFormDialogContent); diff --git a/client/src/containers/Dialogs/InventoryAdjustmentFormDialog/InventoryAdjustmentFormDialogFields.js b/client/src/containers/Dialogs/InventoryAdjustmentFormDialog/InventoryAdjustmentFormDialogFields.js index 9623384bb..0505a901e 100644 --- a/client/src/containers/Dialogs/InventoryAdjustmentFormDialog/InventoryAdjustmentFormDialogFields.js +++ b/client/src/containers/Dialogs/InventoryAdjustmentFormDialog/InventoryAdjustmentFormDialogFields.js @@ -1,5 +1,5 @@ import React from 'react'; -import { FastField, ErrorMessage, Field, useFormikContext } from 'formik'; +import { FastField, ErrorMessage, Field } from 'formik'; import { Classes, FormGroup, @@ -10,7 +10,7 @@ import { import classNames from 'classnames'; import { FormattedMessage as T, useIntl } from 'react-intl'; import { DateInput } from '@blueprintjs/datetime'; -import { compose } from 'redux'; +import { useAutofocus } from 'hooks'; import { ListSelect, FieldRequiredHint, Col, Row } from 'components'; import { inputIntent, @@ -23,18 +23,20 @@ import { CLASSES } from 'common/classes'; import adjustmentType from 'common/adjustmentType'; import AccountsSuggestField from 'components/AccountsSuggestField'; -import withAccounts from 'containers/Accounts/withAccounts'; +import { useInventoryAdjContext } from './InventoryAdjustmentFormProvider' import { diffQuantity } from './utils'; import InventoryAdjustmentQuantityFields from './InventoryAdjustmentQuantityFields'; /** * Inventory adjustment form dialogs fields. */ -function InventoryAdjustmentFormDialogFields({ - //# withAccount - accountsList, -}) { - const { values } = useFormikContext(); +export default function InventoryAdjustmentFormDialogFields() { + const dateFieldRef = useAutofocus(); + + // Inventory adjustment dialog context. + const { accounts } = useInventoryAdjContext(); + + // Intl context. const { formatMessage } = useIntl(); return ( @@ -62,6 +64,7 @@ function InventoryAdjustmentFormDialogFields({ position: Position.BOTTOM, minimal: true, }} + inputRef={(ref) => (dateFieldRef.current = ref)} /> )} @@ -115,7 +118,7 @@ function InventoryAdjustmentFormDialogFields({ className={'form-group--adjustment-account'} > form.setFieldValue('adjustment_account_id', item.id) } @@ -159,9 +162,3 @@ function InventoryAdjustmentFormDialogFields({
); } - -export default compose( - withAccounts(({ accountsList }) => ({ - accountsList, - })), -)(InventoryAdjustmentFormDialogFields); diff --git a/client/src/containers/Dialogs/InventoryAdjustmentFormDialog/InventoryAdjustmentFormProvider.js b/client/src/containers/Dialogs/InventoryAdjustmentFormDialog/InventoryAdjustmentFormProvider.js new file mode 100644 index 000000000..5e8c1ab1f --- /dev/null +++ b/client/src/containers/Dialogs/InventoryAdjustmentFormDialog/InventoryAdjustmentFormProvider.js @@ -0,0 +1,51 @@ +import React, { useState, createContext } from 'react'; +import { DialogContent } from 'components'; +import { + useItem, + useAccounts, + useCreateInventoryAdjustment, +} from 'hooks/query'; + +const InventoryAdjustmentContext = createContext(); + +/** + * Inventory adjustment dialog provider. + */ +function InventoryAdjustmentFormProvider({ itemId, dialogName, ...props }) { + // Fetches accounts list. + const { isFetching: isAccountsLoading, data: accounts } = useAccounts(); + + // Fetches the item details. + const { isFetching: isItemLoading, data: item } = useItem(itemId); + + const { + mutateAsync: createInventoryAdjMutate, + } = useCreateInventoryAdjustment(); + + // Submit payload. + const [submitPayload, setSubmitPayload] = useState({}); + + // State provider. + const provider = { + itemId, + isAccountsLoading, + accounts, + isItemLoading, + item, + submitPayload, + dialogName, + + createInventoryAdjMutate, + setSubmitPayload, + }; + + return ( + + + + ); +} + +const useInventoryAdjContext = () => React.useContext(InventoryAdjustmentContext); + +export { InventoryAdjustmentFormProvider, useInventoryAdjContext }; diff --git a/client/src/containers/Dialogs/ItemCategoryDialog/ItemCategoryForm.js b/client/src/containers/Dialogs/ItemCategoryDialog/ItemCategoryForm.js index 774a27e30..bcb8981be 100644 --- a/client/src/containers/Dialogs/ItemCategoryDialog/ItemCategoryForm.js +++ b/client/src/containers/Dialogs/ItemCategoryDialog/ItemCategoryForm.js @@ -1,165 +1,111 @@ -import React, { useCallback } from 'react'; -import { - Button, - Classes, - FormGroup, - InputGroup, - Intent, - TextArea, - MenuItem, -} from '@blueprintjs/core'; -import { FormattedMessage as T } from 'react-intl'; -import classNames from 'classnames'; -import { ErrorMessage, Form, FastField } from 'formik'; -import { - ListSelect, - AccountsSelectList, - FieldRequiredHint, - Hint, -} from 'components'; -import { inputIntent } from 'utils'; +import React, { useMemo } from 'react'; +import { useIntl } from 'react-intl'; +import { Intent } from '@blueprintjs/core'; +import { Formik } from 'formik'; -import { useAutofocus } from 'hooks'; +import { AppToaster } from 'components'; +import { useItemCategoryContext } from './ItemCategoryProvider'; +import { transformToForm } from 'utils'; +import { + CreateItemCategoryFormSchema, + EditItemCategoryFormSchema, +} from './ItemCategoryForm.schema'; -export default function ItemCategoryForm({ - itemCategoryId, - accountsList, - categoriesList, - isSubmitting, - onClose, +import withDialogActions from 'containers/Dialog/withDialogActions'; +import ItemCategoryFormContent from './ItemCategoryFormContent' +import { compose } from 'utils'; + +const defaultInitialValues = { + name: '', + description: '', + cost_account_id: '', + sell_account_id: '', + inventory_account_id: '', +}; + +/** + * Item category form. + */ +function ItemCategoryForm({ + // #withDialogActions + closeDialog, }) { - const categoryNameFieldRef = useAutofocus(); + const { formatMessage } = useIntl(); + const { + isNewMode, + itemCategory, + itemCategoryId, + dialogName, + createItemCategoryMutate, + editItemCategoryMutate, + } = useItemCategoryContext(); + + // Initial values. + const initialValues = useMemo( + () => ({ + ...defaultInitialValues, + ...transformToForm(itemCategory, defaultInitialValues), + }), + [itemCategory], + ); + + // Transformes response errors. + const transformErrors = (errors, { setErrors }) => { + if (errors.find((error) => error.type === 'CATEGORY_NAME_EXISTS')) { + setErrors({ + name: formatMessage({ id: 'category_name_exists' }), + }); + } + }; + + // Handles the form submit. + const handleFormSubmit = (values, { setSubmitting, setErrors }) => { + setSubmitting(true); + const form = { ...values }; + + // Handle close the dialog after success response. + const afterSubmit = () => { + closeDialog(dialogName); + }; + // Handle the response success/ + const onSuccess = ({ response }) => { + AppToaster.show({ + message: formatMessage({ + id: isNewMode + ? 'the_item_category_has_been_created_successfully' + : 'the_item_category_has_been_edited_successfully', + }), + intent: Intent.SUCCESS, + }); + afterSubmit(response); + }; + // Handle the response error. + const onError = (errors) => { + transformErrors(errors, { setErrors }); + setSubmitting(false); + }; + if (isNewMode) { + createItemCategoryMutate(form).then(onSuccess).catch(onError); + } else { + editItemCategoryMutate([itemCategoryId, form]) + .then(onSuccess) + .catch(onError); + } + }; return ( -
-
- {/* ----------- Category name ----------- */} - - {({ field, field: { value }, meta: { error, touched } }) => ( - } - labelInfo={} - className={'form-group--category-name'} - intent={inputIntent({ error, touched })} - helperText={} - inline={true} - > - (categoryNameFieldRef.current = ref)} - {...field} - /> - - )} - - - - {/* ----------- Description ----------- */} - - {({ field, field: { value }, meta: { error, touched } }) => ( - } - className={'form-group--description'} - intent={inputIntent({ error, touched })} - helperText={} - inline={true} - > -