refactoring: migrating to react-query to manage service-side state.

This commit is contained in:
a.bouhuolia
2021-02-07 08:10:21 +02:00
parent e093be0663
commit adac2386bb
284 changed files with 8255 additions and 6610 deletions

View File

@@ -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,
},
];

View File

@@ -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 (
<RawIntlProvider value={intl}>
<div className="App">
<ReactQueryConfigProvider config={queryConfig}>
<QueryClientProvider client={queryClient}>
<div className="App">
<Router history={history}>
<Switch>
<Route path={'/auth'}>
@@ -38,9 +40,8 @@ function App({ locale }) {
</Router>
<GlobalErrors />
<ReactQueryDevtools />
</ReactQueryConfigProvider>
</div>
</div>
</QueryClientProvider>
</RawIntlProvider>
);
}

View File

@@ -7,7 +7,7 @@ import classNames from 'classnames';
import { CLASSES } from 'common/classes';
export default function CategoriesSelectList({
categoriesList,
categories,
selecetedCategoryId,
defaultSelectText = <T id={'select_category'} />,
onCategorySelected,
@@ -41,7 +41,7 @@ export default function CategoriesSelectList({
return (
<ListSelect
items={categoriesList}
items={categories}
selectedItemProp={'id'}
selectedItem={selecetedCategoryId}
textProp={'name'}

View File

@@ -0,0 +1,131 @@
/*
* Copyright 2016 Palantir Technologies, Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// import classNames from 'classnames';
import * as React from "react";
import * as ReactDOM from "react-dom";
// import { polyfill } from "react-lifecycles-compat";
import {
Popover,
Classes,
Position,
} from '@blueprintjs/core';
// import { IOverlayLifecycleProps } from "../overlay/overlay";
// import { Popover } from "../popover/popover";
// import { PopperModifiers } from "../popover/popoverSharedProps";
export interface IOffset {
left: number;
top: number;
}
interface IContextMenuState {
isOpen: boolean;
isDarkTheme: boolean;
menu?: JSX.Element;
offset?: IOffset;
onClose?: () => 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 = <div onContextMenu={this.cancelContextMenu}>{this.state.menu}</div>;
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 (
<div className={Classes.CONTEXT_MENU_POPOVER_TARGET} style={this.state.offset}>
<Popover
{...this.props}
backdropProps={{ onContextMenu: this.handleBackdropContextMenu }}
content={content}
enforceFocus={false}
key={key}
hasBackdrop={true}
isOpen={this.state.isOpen}
minimal={true}
// modifiers={POPPER_MODIFIERS}
onInteraction={this.handlePopoverInteraction}
position={Position.RIGHT_TOP}
// popoverClassName={popoverClassName}
target={<div />}
transitionDuration={TRANSITION_DURATION}
/>
</div>
);
}
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<HTMLDivElement>) => e.preventDefault();
private handleBackdropContextMenu = (e: React.MouseEvent<HTMLDivElement>) => {
// 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();
}
};
}

View File

@@ -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 = <T id={'all'} />,
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 (

View File

@@ -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 (

View File

@@ -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: {},
};

View File

@@ -27,11 +27,13 @@ function TableHeaderCell({ column, index }) {
</span>
</If>
<div {...column.getSortByToggleProps({
className: classNames('cell-inner', {
'text-overview': column.textOverview,
})
})}>
<div
{...column.getSortByToggleProps({
className: classNames('cell-inner', {
'text-overview': column.textOverview,
}),
})}
>
{column.render('Header')}
<If condition={column.isSorted}>
@@ -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 <TableHeaderSkeletonRenderer />;
}
return (
<ScrollSyncPane>
<div className="thead">

View File

@@ -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 (
<div
{...column.getHeaderProps({
className: 'th',
})}
>
<Skeleton minWidth={skeletonWidthMin} maxWidth={skeletonWidthMax} />
</div>
);
}
/**
* Table skeleton rows.
*/
export default function TableSkeletonHeader({}) {
const {
table: { headerGroups },
} = useContext(TableContext);
return (
<div class="thead">
{headerGroups.map((headerGroup) => (
<div
{...headerGroup.getHeaderGroupProps({
className: 'tr',
})}
>
{headerGroup.headers.map((column) => (
<TableHeaderCell column={column} />
))}
</div>
))}
</div>
);
}

View File

@@ -9,7 +9,7 @@ export default function TableRows() {
table: { prepareRow, page },
props: { TableRowRenderer, TableCellRenderer },
} = useContext(TableContext);
return page.map((row) => {
prepareRow(row);
return <TableRowRenderer row={row} TableCellRenderer={TableCellRenderer} />;

View File

@@ -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 (
<div
{...column.getHeaderProps({
className: 'td',
})}
>
<Skeleton minWidth={skeletonWidthMin} maxWidth={skeletonWidthMax} />
</div>
);
}
/**
* 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) => (
<div
{...headerGroup.getHeaderGroupProps({
className: 'tr',
})}
>
{headerGroup.headers.map((column) => (
<TableHeaderCell column={column} />
))}
</div>
));
});
}

View File

@@ -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 (
<div>
@@ -27,7 +29,7 @@ export default function DialogsContainer() {
<InviteUserDialog dialogName={'invite-user'} />
<ExchangeRateFormDialog dialogName={'exchangeRate-form'} />
<ItemCategoryDialog dialogName={'item-category-form'} />
<InventoryAdjustmentDialog dialogName={'inventory-adjustment-form'} />
<InventoryAdjustmentDialog dialogName={'inventory-adjustment'} />
<PaymentViaVoucherDialog dialogName={'payment-via-voucher'} />
</div>
);

View File

@@ -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 <Tag className={'skeleton'} style={{ width: `${randomWidth}%` }} />;
}

View File

@@ -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,
};

View File

@@ -171,7 +171,8 @@ export default [
],
},
{
divider: true,
text: <T id={'system'} />,
label: true,
},
{
text: <T id={'preferences'} />,

View File

@@ -7,7 +7,6 @@ import { orderingLinesIndexes, repeatValue } from 'utils';
export default function MakeJournalEntriesField({
defaultRow,
linesNumber = 4,
}) {
return (

View File

@@ -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 (
<div
className={classNames(
@@ -240,39 +220,21 @@ function MakeJournalEntriesForm({
validationSchema={isNewMode ? CreateJournalSchema : EditJournalSchema}
onSubmit={handleSubmit}
>
{({ isSubmitting }) => (
<Form>
<MakeJournalEntriesHeader
manualJournal={manualJournalId}
onJournalNumberChanged={handleJournalNumberChanged}
/>
<MakeJournalNumberWatcher journalNumber={journalNumber} />
<MakeJournalEntriesField defaultRow={defaultEntry} />
<MakeJournalFormFooter />
<MakeJournalFormFloatingActions
isSubmitting={isSubmitting}
manualJournal={manualJournal}
// manualJournalPublished={values.status}
onCancelClick={handleCancelClick}
onSubmitClick={handleSubmitClick}
/>
</Form>
)}
<Form>
<MakeJournalEntriesHeader
onJournalNumberChanged={handleJournalNumberChanged}
/>
<MakeJournalNumberWatcher journalNumber={journalNumber} />
<MakeJournalEntriesField defaultRow={defaultEntry} />
<MakeJournalFormFooter />
<MakeJournalFormFloatingActions />
</Form>
</Formik>
{/* <Dragzone
initialFiles={initialAttachmentFiles}
onDrop={handleDropFiles}
onDeleteFile={handleDeleteFile}
hint={'Attachments: Maxiumum size: 20MB'}
/> */}
</div>
);
}
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);

View File

@@ -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}
>
<CurrencySelectList
currenciesList={currenciesList}
currenciesList={currencies}
selectedCurrencyCode={value}
onCurrencySelected={(currencyItem) => {
form.setFieldValue('currency_code', currencyItem.currency_code);
@@ -181,7 +180,4 @@ function MakeJournalEntriesHeader({
export default compose(
withDialogActions,
withCurrencies(({ currenciesList }) => ({
currenciesList,
})),
)(MakeJournalEntriesHeader);

View File

@@ -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 (
<DashboardInsider
loading={
fetchJournal.isFetching ||
fetchAccounts.isFetching ||
fetchCurrencies.isFetching ||
fetchCustomers.isFetching
}
name={'make-journal-page'}
>
<MakeJournalProvider journalId={journalId}>
<MakeJournalEntriesForm
onFormSubmit={handleFormSubmit}
manualJournalId={id}
onCancelForm={handleCancel}
/>
</DashboardInsider>
</MakeJournalProvider>
);
}
export default compose(
withAccountsActions,
withCustomersActions,
withManualJournalsActions,
withCurrenciesActions,
withSettingsActions,
withDashboardActions,
)(MakeJournalEntriesPage);

View File

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

View File

@@ -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({
<ButtonGroup>
<Button
disabled={isSubmitting}
loading={isSubmitting}
intent={Intent.PRIMARY}
onClick={handleSubmitPublishBtnClick}
text={<T id={'save_publish'} />}
@@ -114,6 +106,7 @@ export default function MakeJournalFloatingAction({
<Button
intent={Intent.PRIMARY}
rightIcon={<Icon icon="arrow-drop-up-16" iconSize={20} />}
disabled={isSubmitting}
/>
</Popover>
</ButtonGroup>
@@ -144,6 +137,7 @@ export default function MakeJournalFloatingAction({
>
<Button
rightIcon={<Icon icon="arrow-drop-up-16" iconSize={20} />}
disabled={isSubmitting}
/>
</Popover>
</ButtonGroup>
@@ -173,6 +167,7 @@ export default function MakeJournalFloatingAction({
<Button
intent={Intent.PRIMARY}
rightIcon={<Icon icon="arrow-drop-up-16" iconSize={20} />}
disabled={isSubmitting}
/>
</Popover>
</ButtonGroup>

View File

@@ -9,14 +9,20 @@ import withManualJournals from './withManualJournals';
import { defaultToTransform } from 'utils';
/**
*
* Journal number chaلing watcher.
*/
function MakeJournalNumberChangingWatcher({
journalNumber,
// #withDashboardActions
changePageSubtitle,
// #withManualJournals
journalNumberChanged,
// #withManualJournalsActions
setJournalNumberChanged,
changePageSubtitle
// #ownProps
journalNumber,
}) {
const { setFieldValue } = useFormikContext();

View File

@@ -0,0 +1,87 @@
import React, { createContext, useState } from 'react';
import DashboardInsider from 'components/Dashboard/DashboardInsider';
import {
useAccounts,
useCustomers,
useCurrencies,
useJournal,
useCreateJournal,
useEditJournal,
useSettings
} from 'hooks/query';
const MakeJournalFormContext = createContext();
/**
* Make journal form provider.
*/
function MakeJournalProvider({ journalId, ...props }) {
// Load the accounts list.
const { data: accounts, isFetching: isAccountsLoading } = useAccounts();
// Load the customers list.
const {
data: { customers },
isFetching: isCustomersLoading,
} = useCustomers();
// Load the currencies list.
const { data: currencies, isFetching: isCurrenciesLoading } = useCurrencies();
// Load the details of the given manual journal.
const { data: manualJournal, isFetching: isJournalLoading } = useJournal(
journalId,
{
enabled: !!journalId,
},
);
// Create and edit journal mutations.
const { mutateAsync: createJournalMutate } = useCreateJournal();
const { mutateAsync: editJournalMutate } = useEditJournal();
// Loading the journal settings.
const { isFetching: isSettingsLoading } = useSettings();
const [submitPayload, setSubmitPayload] = useState({});
const provider = {
accounts,
customers,
currencies,
manualJournal,
createJournalMutate,
editJournalMutate,
isAccountsLoading,
isCustomersLoading,
isCurrenciesLoading,
isJournalLoading,
isSettingsLoading,
isNewMode: !journalId,
submitPayload,
setSubmitPayload
};
return (
<DashboardInsider
loading={
isJournalLoading ||
isAccountsLoading ||
isCurrenciesLoading ||
isCustomersLoading ||
isSettingsLoading
}
name={'make-journal-page'}
>
<MakeJournalFormContext.Provider value={provider} {...props} />
</DashboardInsider>
);
}
const useMakeJournalFormContext = () =>
React.useContext(MakeJournalFormContext);
export { MakeJournalProvider, useMakeJournalFormContext };

View File

@@ -1,23 +1,21 @@
import React, { useMemo, useState, useCallback } from 'react';
import React from 'react';
import Icon from 'components/Icon';
import {
Button,
NavbarGroup,
Classes,
NavbarDivider,
MenuItem,
Menu,
Popover,
PopoverInteractionKind,
Position,
Intent,
} from '@blueprintjs/core';
import classNames from 'classnames';
import { useRouteMatch, useHistory } from 'react-router-dom';
import { useHistory } from 'react-router-dom';
import { FormattedMessage as T } from 'react-intl';
import { connect } from 'react-redux';
import FilterDropdown from 'components/FilterDropdown';
import { useManualJournalsContext } from 'containers/Accounting/ManualJournalsListProvider';
import DashboardActionsBar from 'components/Dashboard/DashboardActionsBar';
import withDialogActions from 'containers/Dialog/withDialogActions';
@@ -33,53 +31,24 @@ import { compose } from 'utils';
* Manual journal actions bar.
*/
function ManualJournalActionsBar({
// #withResourceDetail
resourceFields,
// #withManualJournals
manualJournalsViews,
// #withManualJournalsActions
addManualJournalsTableQueries,
changeManualJournalCurrentView,
onFilterChanged,
selectedRows = [],
onBulkDelete,
}) {
const [filterCount, setFilterCount] = useState(0);
const history = useHistory();
const { journalsViews } = useManualJournalsContext();
const onClickNewManualJournal = useCallback(() => {
// Handle click a new manual journal.
const onClickNewManualJournal = () => {
history.push('/make-journal-entry');
}, [history]);
// const filterDropdown = FilterDropdown({
// fields: resourceFields,
// initialCondition: {
// fieldKey: 'journal_number',
// compatator: 'contains',
// value: '',
// },
// onFilterChange: (filterConditions) => {
// setFilterCount(filterConditions.length || 0);
// addManualJournalsTableQueries({
// filter_roles: filterConditions || '',
// });
// onFilterChanged && onFilterChanged(filterConditions);
// },
// });
const hasSelectedRows = useMemo(() => selectedRows.length > 0, [
selectedRows,
]);
};
// Handle delete button click.
const handleBulkDelete = useCallback(() => {
onBulkDelete && onBulkDelete(selectedRows.map((r) => r.id));
}, [onBulkDelete, selectedRows]);
const handleBulkDelete = () => {
};
// Handle tab change.
const handleTabChange = (viewId) => {
changeManualJournalCurrentView(viewId.id || -1);
addManualJournalsTableQueries({
custom_view_id: viewId.id || null,
});
@@ -90,7 +59,7 @@ function ManualJournalActionsBar({
<NavbarGroup>
<DashboardActionViewsList
resourceName={'manual-journals'}
views={manualJournalsViews}
views={journalsViews}
onChange={handleTabChange}
/>
<NavbarDivider />
@@ -109,14 +78,14 @@ function ManualJournalActionsBar({
>
<Button
className={classNames(Classes.MINIMAL, 'button--filter', {
'has-active-filters': filterCount > 0,
'has-active-filters': false,
})}
text="Filter"
icon={<Icon icon="filter-16" iconSize={16} />}
/>
</Popover>
<If condition={hasSelectedRows}>
<If condition={false}>
<Button
className={Classes.MINIMAL}
icon={<Icon icon="trash-16" iconSize={16} />}

View File

@@ -0,0 +1,15 @@
import React from 'react';
import JournalDeleteAlert from 'containers/Alerts/ManualJournals/JournalDeleteAlert';
import JournalPublishAlert from 'containers/Alerts/ManualJournals/JournalPublishAlert';
/**
* Manual journals alerts.
*/
export default function ManualJournalsAlerts() {
return (
<div>
<JournalDeleteAlert name={'journal-delete'} />
<JournalPublishAlert name={'journal-publish'} />
</div>
)
}

View File

@@ -3,13 +3,11 @@ import {
Intent,
Button,
Popover,
Tooltip,
Menu,
MenuItem,
MenuDivider,
Position,
} from '@blueprintjs/core';
import { withRouter, useParams } from 'react-router-dom';
import { useIntl } from 'react-intl';
import moment from 'moment';
import classNames from 'classnames';
@@ -17,68 +15,59 @@ import classNames from 'classnames';
import {
DataTable,
If,
Money,
Choose,
Icon,
LoadingIndicator,
} from 'components';
import { CLASSES } from 'common/classes';
import { useIsValuePassed } from 'hooks';
import ManualJournalsEmptyStatus from './ManualJournalsEmptyStatus';
import TableSkeletonRows from 'components/Datatable/TableSkeletonRows';
import TableSkeletonHeader from 'components/Datatable/TableHeaderSkeleton';
import {
AmountPopoverContent,
NoteAccessor,
StatusAccessor,
DateAccessor,
AmountAccessor
} from './components';
import withDialogActions from 'containers/Dialog/withDialogActions';
import withManualJournals from 'containers/Accounting/withManualJournals';
import withManualJournalsActions from 'containers/Accounting/withManualJournalsActions';
import { compose, saveInvoke } from 'utils';
import { useManualJournalsContext } from './ManualJournalsListProvider';
/**
* Manual journals data-table.
*/
function ManualJournalsDataTable({
// #withManualJournals
manualJournalsCurrentPage,
manualJournalsLoading,
manualJournalsPagination,
manualJournalsTableQuery,
manualJournalsCurrentViewId,
// #withManualJournalsActions
addManualJournalsTableQueries,
// #ownProps
onEditJournal,
onDeleteJournal,
onPublishJournal,
onSelectedRowsChange,
manualJournalViewLoading,
}) {
const { custom_view_id: customViewId } = useParams();
const { formatMessage } = useIntl();
const isLoadedBefore = useIsValuePassed(manualJournalsLoading, false);
const { manualJournals, isManualJournalsLoading } = useManualJournalsContext();
const handlePublishJournal = useCallback(
(journal) => () => {
saveInvoke(onPublishJournal, journal);
},
[onPublishJournal],
[],
);
const handleEditJournal = useCallback(
(journal) => () => {
saveInvoke(onEditJournal, journal);
},
[onEditJournal],
[],
);
const handleDeleteJournal = useCallback(
(journal) => () => {
saveInvoke(onDeleteJournal, journal);
},
[onDeleteJournal],
[],
);
const actionMenuList = useCallback(
@@ -127,22 +116,14 @@ function ManualJournalsDataTable({
{
id: 'date',
Header: formatMessage({ id: 'date' }),
accessor: (r) => moment(r.date).format('YYYY MMM DD'),
accessor: DateAccessor,
width: 115,
className: 'date',
},
{
id: 'amount',
Header: formatMessage({ id: 'amount' }),
accessor: (r) => (
<Tooltip
content={<AmountPopoverContent journalEntries={r.entries} />}
position={Position.RIGHT_TOP}
boundary={'viewport'}
>
<Money amount={r.amount} currency={'USD'} />
</Tooltip>
),
accessor: AmountAccessor,
className: 'amount',
width: 115,
},
@@ -227,66 +208,42 @@ function ManualJournalsDataTable({
[onSelectedRowsChange],
);
const showEmptyStatus = [
manualJournalsCurrentViewId === -1,
manualJournalsCurrentPage.length === 0,
].every((condition) => condition === true);
return (
<div className={classNames(CLASSES.DASHBOARD_DATATABLE)}>
<LoadingIndicator
loading={
(manualJournalsLoading && !isLoadedBefore) || manualJournalViewLoading
}
>
<Choose>
<Choose.When condition={showEmptyStatus}>
<ManualJournalsEmptyStatus />
</Choose.When>
<Choose>
<Choose.When condition={false}>
<ManualJournalsEmptyStatus />
</Choose.When>
<Choose.Otherwise>
<DataTable
noInitialFetch={true}
columns={columns}
data={manualJournalsCurrentPage}
onFetchData={handleDataTableFetchData}
manualSortBy={true}
selectionColumn={true}
expandable={true}
sticky={true}
onSelectedRowsChange={handleSelectedRowsChange}
rowContextMenu={onRowContextMenu}
pagesCount={manualJournalsPagination.pagesCount}
pagination={true}
initialPageSize={manualJournalsTableQuery.page_size}
initialPageIndex={manualJournalsTableQuery.page - 1}
autoResetSortBy={false}
autoResetPage={false}
/>
</Choose.Otherwise>
</Choose>
</LoadingIndicator>
<Choose.Otherwise>
<DataTable
noInitialFetch={true}
columns={columns}
data={manualJournals}
onFetchData={handleDataTableFetchData}
manualSortBy={true}
selectionColumn={true}
expandable={true}
sticky={true}
loading={isManualJournalsLoading}
onSelectedRowsChange={handleSelectedRowsChange}
rowContextMenu={onRowContextMenu}
// pagesCount={manualJournalsPagination.pagesCount}
pagination={true}
// initialPageSize={manualJournalsTableQuery.page_size}
// initialPageIndex={manualJournalsTableQuery.page - 1}
autoResetSortBy={false}
autoResetPage={false}
TableLoadingRenderer={TableSkeletonRows}
TableHeaderSkeletonRenderer={TableSkeletonHeader}
/>
</Choose.Otherwise>
</Choose>
</div>
);
}
export default compose(
withRouter,
withDialogActions,
withManualJournalsActions,
withManualJournals(
({
manualJournalsCurrentPage,
manualJournalsLoading,
manualJournalsPagination,
manualJournalsTableQuery,
manualJournalsCurrentViewId,
}) => ({
manualJournalsCurrentPage,
manualJournalsLoading,
manualJournalsPagination,
manualJournalsTableQuery,
manualJournalsCurrentViewId,
}),
),
)(ManualJournalsDataTable);

View File

@@ -1,31 +1,26 @@
import React, { useEffect, useState, useMemo, useCallback } from 'react';
import { Route, Switch, useHistory, withRouter } from 'react-router-dom';
import { queryCache, useQuery } from 'react-query';
import { Alert, Intent } from '@blueprintjs/core';
import AppToaster from 'components/AppToaster';
import React, { useEffect, useCallback } from 'react';
import { Route, Switch, useHistory } from 'react-router-dom';
import {
FormattedMessage as T,
useIntl,
} from 'react-intl';
import DashboardPageContent from 'components/Dashboard/DashboardPageContent';
import DashboardInsider from 'components/Dashboard/DashboardInsider';
import ManualJournalsViewTabs from 'containers/Accounting/ManualJournalsViewTabs';
import ManualJournalsDataTable from 'containers/Accounting/ManualJournalsDataTable';
import ManualJournalsActionsBar from 'containers/Accounting/ManualJournalActionsBar';
import { ManualJournalsListProvider } from './ManualJournalsListProvider';
import ManualJournalsAlerts from './ManualJournalsAlerts';
import ManualJournalsViewTabs from './ManualJournalsViewTabs';
import ManualJournalsDataTable from './ManualJournalsDataTable';
import ManualJournalsActionsBar from './ManualJournalActionsBar';
import withDashboardActions from 'containers/Dashboard/withDashboardActions';
import withManualJournals from 'containers/Accounting/withManualJournals';
import withManualJournalsActions from 'containers/Accounting/withManualJournalsActions';
import withViewsActions from 'containers/Views/withViewsActions';
import withRouteActions from 'containers/Router/withRouteActions';
import withResourceActions from 'containers/Resources/withResourcesActions';
import { compose } from 'utils';
import 'style/pages/ManualJournal/List.scss';
/**
* Manual journals table.
*/
@@ -33,117 +28,17 @@ function ManualJournalsTable({
// #withDashboardActions
changePageTitle,
// #withViewsActions
requestFetchResourceViews,
// #withResourceActions
requestFetchResourceFields,
// #withManualJournals
manualJournalsTableQuery,
// #withManualJournalsActions
requestFetchManualJournalsTable,
requestDeleteManualJournal,
requestPublishManualJournal,
requestDeleteBulkManualJournals,
addManualJournalsTableQueries,
}) {
const history = useHistory();
const [deleteManualJournal, setDeleteManualJournal] = useState(false);
const [selectedRows, setSelectedRows] = useState([]);
const [bulkDelete, setBulkDelete] = useState(false);
const [publishManualJournal, setPublishManualJournal] = useState(false);
const { formatMessage } = useIntl();
const fetchResourceViews = useQuery(
['resource-views', 'manual-journals'],
() => requestFetchResourceViews('manual_journals'),
);
// const fetchResourceFields = useQuery(
// ['resource-fields', 'manual-journals'],
// () => requestFetchResourceFields('manual_journals'),
// );
const fetchManualJournals = useQuery(
['manual-journals-table', manualJournalsTableQuery],
(key, q) => requestFetchManualJournalsTable(),
);
// Handle update the page title.
useEffect(() => {
changePageTitle(formatMessage({ id: 'manual_journals' }));
}, [changePageTitle, formatMessage]);
// Handle delete manual journal click.
const handleDeleteJournal = useCallback(
(journal) => {
setDeleteManualJournal(journal);
},
[setDeleteManualJournal],
);
// Handle cancel delete manual journal.
const handleCancelManualJournalDelete = useCallback(() => {
setDeleteManualJournal(false);
}, [setDeleteManualJournal]);
// Handle confirm delete manual journal.
const handleConfirmManualJournalDelete = useCallback(() => {
requestDeleteManualJournal(deleteManualJournal.id).then(() => {
AppToaster.show({
message: formatMessage(
{ id: 'the_journal_has_been_deleted_successfully' },
{ number: deleteManualJournal.journal_number },
),
intent: Intent.SUCCESS,
});
setDeleteManualJournal(false);
});
}, [deleteManualJournal, requestDeleteManualJournal, formatMessage]);
// Calculates the selected rows count.
const selectedRowsCount = useMemo(() => Object.values(selectedRows).length, [
selectedRows,
]);
const handleBulkDelete = useCallback(
(accountsIds) => {
setBulkDelete(accountsIds);
},
[setBulkDelete],
);
// Handle confirm journals bulk delete.
const handleConfirmBulkDelete = useCallback(() => {
requestDeleteBulkManualJournals(bulkDelete)
.then(() => {
setBulkDelete(false);
AppToaster.show({
message: formatMessage(
{ id: 'the_journals_has_been_deleted_successfully' },
{ count: selectedRowsCount },
),
intent: Intent.SUCCESS,
});
})
.catch((error) => {
setBulkDelete(false);
});
}, [
requestDeleteBulkManualJournals,
bulkDelete,
formatMessage,
selectedRowsCount,
]);
// Handle cancel bulk delete alert.
const handleCancelBulkDelete = useCallback(() => {
setBulkDelete(false);
}, []);
const handleEditJournal = useCallback(
(journal) => {
history.push(`/manual-journals/${journal.id}/edit`);
@@ -154,79 +49,9 @@ function ManualJournalsTable({
// Handle filter change to re-fetch data-table.
const handleFilterChanged = useCallback(() => {}, []);
// Handle view change to re-fetch data table.
// const handleViewChanged = useCallback(() => {
// }, [fetchManualJournals]);
// Handle fetch data of manual jouranls datatable.
const handleFetchData = useCallback(
({ pageIndex, pageSize, sortBy }) => {
const page = pageIndex + 1;
addManualJournalsTableQueries({
...(sortBy.length > 0
? {
column_sort_by: sortBy[0].id,
sort_order: sortBy[0].desc ? 'desc' : 'asc',
}
: {}),
page_size: pageSize,
page,
});
},
[addManualJournalsTableQueries],
);
// Handle publish manual Journal click.
const handlePublishMaunalJournal = useCallback(
(jouranl) => {
setPublishManualJournal(jouranl);
},
[setPublishManualJournal],
);
// Handle cancel manual journal alert.
const handleCancelPublishMaunalJournal = useCallback(() => {
setPublishManualJournal(false);
}, [setPublishManualJournal]);
// Handle publish manual journal confirm.
const handleConfirmPublishManualJournal = useCallback(() => {
requestPublishManualJournal(publishManualJournal.id)
.then(() => {
setPublishManualJournal(false);
AppToaster.show({
message: formatMessage({
id: 'the_manual_journal_has_been_published',
}),
intent: Intent.SUCCESS,
});
queryCache.invalidateQueries('manual-journals-table');
})
.catch((error) => {
setPublishManualJournal(false);
});
}, [publishManualJournal, requestPublishManualJournal, formatMessage]);
// Handle selected rows change.
const handleSelectedRowsChange = useCallback(
(accounts) => {
setSelectedRows(accounts);
},
[setSelectedRows],
);
return (
<DashboardInsider
loading={fetchResourceViews.isFetching}
name={'manual-journals'}
>
<ManualJournalsActionsBar
onBulkDelete={handleBulkDelete}
selectedRows={selectedRows}
onFilterChanged={handleFilterChanged}
/>
<ManualJournalsListProvider query={manualJournalsTableQuery}>
<ManualJournalsActionsBar />
<DashboardPageContent>
<Switch>
@@ -238,72 +63,17 @@ function ManualJournalsTable({
]}
>
<ManualJournalsViewTabs />
<ManualJournalsDataTable
onDeleteJournal={handleDeleteJournal}
onEditJournal={handleEditJournal}
onPublishJournal={handlePublishMaunalJournal}
onSelectedRowsChange={handleSelectedRowsChange}
manualJournalViewLoading={fetchManualJournals.isFetching}
/>
<ManualJournalsDataTable />
<ManualJournalsAlerts />
</Route>
</Switch>
<Alert
cancelButtonText={<T id={'cancel'} />}
confirmButtonText={<T id={'delete'} />}
icon="trash"
intent={Intent.DANGER}
isOpen={deleteManualJournal}
onCancel={handleCancelManualJournalDelete}
onConfirm={handleConfirmManualJournalDelete}
>
<p>
<T id={'once_delete_this_journal_you_will_able_to_restore_it'} />
</p>
</Alert>
<Alert
cancelButtonText={<T id={'cancel'} />}
confirmButtonText={
<T id={'delete_count'} values={{ count: selectedRowsCount }} />
}
icon="trash"
intent={Intent.DANGER}
isOpen={bulkDelete}
onCancel={handleCancelBulkDelete}
onConfirm={handleConfirmBulkDelete}
>
<p>
<T
id={'once_delete_these_journals_you_will_not_able_restore_them'}
/>
</p>
</Alert>
<Alert
cancelButtonText={<T id={'cancel'} />}
confirmButtonText={<T id={'publish'} />}
intent={Intent.WARNING}
isOpen={publishManualJournal}
onCancel={handleCancelPublishMaunalJournal}
onConfirm={handleConfirmPublishManualJournal}
>
<p>
<T id={'are_sure_to_publish_this_manual_journal'} />
</p>
</Alert>
</DashboardPageContent>
</DashboardInsider>
</ManualJournalsListProvider>
);
}
export default compose(
withRouter,
withRouteActions,
withDashboardActions,
withManualJournalsActions,
withViewsActions,
withResourceActions,
withManualJournals(({ manualJournalsTableQuery }) => ({
manualJournalsTableQuery,
})),

View File

@@ -0,0 +1,37 @@
import React, { createContext } from 'react';
import DashboardInsider from 'components/Dashboard/DashboardInsider';
import { useResourceViews, useJournals } from 'hooks/query';
const ManualJournalsContext = createContext();
function ManualJournalsListProvider({ query, ...props }) {
// Fetches accounts resource views and fields.
const { data: journalsViews, isFetching: isViewsLoading } = useResourceViews(
'manual_journals',
);
// Fetches the manual journals transactions with pagination meta.
const {
data: { manualJournals },
isFetching: isManualJournalsLoading,
} = useJournals(query);
// Global state.
const state = {
manualJournals,
journalsViews,
isManualJournalsLoading,
isViewsLoading,
};
return (
<DashboardInsider loading={isViewsLoading} name={'manual-journals'}>
<ManualJournalsContext.Provider value={state} {...props} />
</DashboardInsider>
);
}
const useManualJournalsContext = () => React.useContext(ManualJournalsContext);
export { ManualJournalsListProvider, useManualJournalsContext };

View File

@@ -1,61 +1,38 @@
import React, { useEffect } from 'react';
import { useHistory } from 'react-router';
import React from 'react';
import { Alignment, Navbar, NavbarGroup } from '@blueprintjs/core';
import { useParams, withRouter } from 'react-router-dom';
import { connect } from 'react-redux';
import { useParams } from 'react-router-dom';
import { pick } from 'lodash';
import { DashboardViewsTabs, Icon } from 'components';
import { DashboardViewsTabs } from 'components';
import withManualJournals from './withManualJournals';
import { useManualJournalsContext } from './ManualJournalsListProvider';
import withManualJournalsActions from './withManualJournalsActions';
import withDashboardActions from 'containers/Dashboard/withDashboardActions';
import withViewDetail from 'containers/Views/withViewDetails';
import { compose } from 'utils';
import { compose, saveInvoke } from 'utils';
/**
* Manual journal views tabs.
*/
function ManualJournalsViewTabs({
// #withViewDetail
viewId,
viewItem,
// #withManualJournals
manualJournalsViews,
// #withManualJournalsActions
addManualJournalsTableQueries,
changeManualJournalCurrentView,
// #withDashboardActions
setTopbarEditView,
changePageSubtitle,
// #ownProps
onViewChanged,
}) {
const { custom_view_id: customViewId } = useParams();
const { journalsViews } = useManualJournalsContext();
useEffect(() => {
setTopbarEditView(customViewId);
changePageSubtitle(customViewId && viewItem ? viewItem.name : '');
}, [customViewId]);
const tabs = manualJournalsViews.map((view) => ({
const tabs = journalsViews.map((view) => ({
...pick(view, ['name', 'id']),
}));
const handleClickNewView = () => {};
const handleTabChange = (viewId) => {
changeManualJournalCurrentView(viewId || -1);
addManualJournalsTableQueries({
custom_view_id: viewId || null,
});
};
return (
<Navbar className="navbar--dashboard-views">
<NavbarGroup align={Alignment.LEFT}>
@@ -71,19 +48,7 @@ function ManualJournalsViewTabs({
);
}
const mapStateToProps = (state, ownProps) => ({
viewId: parseInt(ownProps.match.params.custom_view_id, 10),
});
const withManualJournalsViewTabs = connect(mapStateToProps);
export default compose(
withRouter,
withManualJournalsViewTabs,
withManualJournals(({ manualJournalsViews }) => ({
manualJournalsViews,
})),
withManualJournalsActions,
withDashboardActions,
withViewDetail(),
)(ManualJournalsViewTabs);

View File

@@ -1,10 +1,31 @@
import React from 'react';
import { Intent, Classes, Tooltip, Position, Tag, Button } from '@blueprintjs/core';
import {
Intent,
Classes,
Tooltip,
Position,
Tag,
Button,
} from '@blueprintjs/core';
import { FormattedMessage as T } from 'react-intl';
import moment from 'moment';
import { Choose, Money, If, Icon, Hint } from 'components';
import withAccountDetails from 'containers/Accounts/withAccountDetail';
import { compose } from 'utils';
// Amount accessor.
export const AmountAccessor = (r) => (
<Tooltip
content={<AmountPopoverContent journalEntries={r.entries} />}
position={Position.RIGHT_TOP}
boundary={'viewport'}
>
<Money amount={r.amount} currency={'USD'} />
</Tooltip>
);
const AmountPopoverContentLineRender = ({
journalEntry,
accountId,
@@ -77,6 +98,13 @@ export const StatusAccessor = (row) => {
);
};
/**
* Date accessor.
*/
export const DateAccessor = (row) => {
return moment(row.date).format('YYYY MMM DD');
};
/**
* Note column accessor.
*/
@@ -156,7 +184,11 @@ export const TotalCreditDebitCellRenderer = (chainedComponent, type) => (
return computed;
}, 0);
return <span><Money amount={total} currency={'USD'} /></span>;
return (
<span>
<Money amount={total} currency={'USD'} />
</span>
);
}
return chainedComponent(props);
};

View File

@@ -0,0 +1,30 @@
import React from 'react';
import { Menu, MenuItem, MenuDivider, Intent } from '@blueprintjs/core';
import { Icon, If } from 'components';
import { useIntl } from 'react-intl';
import withDialogs from 'containers/Dialog/withDialogActions';
import withAlertsActions from 'containers/Alert/withAlertActions';
import { compose } from 'utils';
import { formatMessage } from 'services/intl';
/**
* Account actions menu list.
*/
export default function AccountActionsMenuList({
account,
// #withAlert
openAlert,
openDialog,
}) {
const { formatMessage } = useIntl();
return (
);
}
// export default compose(withDialogs, withAlertsActions)(AccountActionsMenuList);

View File

@@ -1,4 +1,4 @@
import React, { memo, useState } from 'react';
import React from 'react';
import Icon from 'components/Icon';
import {
Button,
@@ -11,16 +11,14 @@ import {
Intent,
} from '@blueprintjs/core';
import classNames from 'classnames';
import { connect } from 'react-redux';
import { FormattedMessage as T } from 'react-intl';
import { If, DashboardActionViewsList } from 'components';
import DashboardActionsBar from 'components/Dashboard/DashboardActionsBar';
import FilterDropdown from 'components/FilterDropdown';
import { useAccountsChartContext } from 'containers/Accounts/AccountsChartProvider';
import withDialogActions from 'containers/Dialog/withDialogActions';
import withResourceDetail from 'containers/Resources/withResourceDetails';
import withAccountsTableActions from 'containers/Accounts/withAccountsTableActions';
import withAccounts from 'containers/Accounts/withAccounts';
import withAlertActions from 'containers/Alert/withAlertActions';
@@ -30,61 +28,39 @@ import { compose } from 'utils';
* Accounts actions bar.
*/
function AccountsActionsBar({
// #withDialogActions
openDialog,
accountsViews,
// #withResourceDetail
resourceFields,
// #withAccountsTableActions
addAccountsTableQueries,
setAccountsBulkAction,
// #withAccounts
accountsTableQuery,
accountsSelectedRows,
// #withAlertActions
openAlert,
// #ownProps
onFilterChanged,
}) {
const [filterCount, setFilterCount] = useState(
accountsTableQuery?.filter_roles?.length || 0,
);
const { resourceViews } = useAccountsChartContext();
const onClickNewAccount = () => {
openDialog('account-form', {});
};
// Filter dropdown.
const filterDropdown = FilterDropdown({
fields: resourceFields,
initialConditions: accountsTableQuery.filter_roles,
initialCondition: {
fieldKey: 'name',
comparator: 'contains',
value: '',
},
onFilterChange: (filterConditions) => {
setFilterCount(filterConditions.length || 0);
addAccountsTableQueries({
filter_roles: filterConditions || '',
});
onFilterChanged && onFilterChanged(filterConditions);
},
});
// handle bulk accounts delete.
const handleBulkDelete = () => {
openAlert('accounts-bulk-delete', { accountsIds: accountsSelectedRows });
};
// Handle bulk accounts activate.
const handelBulkActivate = () => {
openAlert('accounts-bulk-activate', { accountsIds: accountsSelectedRows });
};
// Handle bulk accounts inactivate.
const handelBulkInactive = () => {
openAlert('accounts-bulk-inactivate', { accountsIds: accountsSelectedRows });
openAlert('accounts-bulk-inactivate', {
accountsIds: accountsSelectedRows,
});
};
return (
@@ -92,7 +68,7 @@ function AccountsActionsBar({
<NavbarGroup>
<DashboardActionViewsList
resourceName={'accounts'}
views={accountsViews}
views={resourceViews}
/>
<NavbarDivider />
@@ -104,23 +80,20 @@ function AccountsActionsBar({
/>
<Popover
minimal={true}
content={filterDropdown}
content={''}
interactionKind={PopoverInteractionKind.CLICK}
position={Position.BOTTOM_LEFT}
canOutsideClickClose={true}
>
<Button
className={classNames(Classes.MINIMAL, 'button--filter', {
'has-active-filters': filterCount > 0,
'has-active-filters': false,
})}
text={
(filterCount <= 0) ? (
true ? (
<T id={'filter'} />
) : (
<T
id={'count_filters_applied'}
values={{ count: filterCount }}
/>
<T id={'count_filters_applied'} values={{ count: 0 }} />
)
}
icon={<Icon icon="filter-16" iconSize={16} />}
@@ -169,30 +142,10 @@ function AccountsActionsBar({
);
}
// Momerize the component.
const AccountsActionsBarMemo = memo(AccountsActionsBar);
const mapStateToProps = (state, props) => ({
resourceName: 'accounts',
});
const withAccountsActionsBar = connect(mapStateToProps);
const comp = compose(
withAccountsActionsBar,
export default compose(
withDialogActions,
withAccounts(
({ accountsSelectedRows, accountsViews, accountsTableQuery }) => ({
accountsViews,
accountsTableQuery,
accountsSelectedRows,
}),
),
withResourceDetail(({ resourceFields }) => ({
resourceFields,
withAccounts(({ accountsSelectedRows }) => ({
accountsSelectedRows,
})),
withAccountsTableActions,
withAlertActions
)(AccountsActionsBarMemo);
export default comp;
withAlertActions,
)(AccountsActionsBar);

View File

@@ -1,23 +1,15 @@
import React, { useEffect, useState, useCallback } from 'react';
import { Route, Switch } from 'react-router-dom';
import { useQuery } from 'react-query';
import {
useIntl,
} from 'react-intl';
import React, { useEffect } from 'react';
import { useIntl } from 'react-intl';
import 'style/pages/Accounts/List.scss';
import DashboardPageContent from 'components/Dashboard/DashboardPageContent';
import DashboardInsider from 'components/Dashboard/DashboardInsider';
import { AccountsChartProvider } from 'containers/Accounts/AccountsChartProvider';
import AccountsViewPage from 'containers/Accounts/AccountsViewPage';
import AccountsActionsBar from 'containers/Accounts/AccountsActionsBar';
import AccountsAlerts from './AccountsAlerts';
import withDashboardActions from 'containers/Dashboard/withDashboardActions';
import withResourceActions from 'containers/Resources/withResourcesActions';
import withAccountsActions from 'containers/Accounts/withAccountsActions';
import withAccountsTableActions from 'containers/Accounts/withAccountsTableActions';
import withViewsActions from 'containers/Views/withViewsActions';
import withAccounts from 'containers/Accounts/withAccounts';
import { compose } from 'utils';
@@ -29,83 +21,29 @@ function AccountsChart({
// #withDashboardActions
changePageTitle,
// #withViewsActions
requestFetchResourceViews,
// #withResourceActions
requestFetchResourceFields,
// #withAccountsTableActions
requestFetchAccountsTable,
addAccountsTableQueries,
// #withAccounts
accountsTableQuery,
accountsTableQuery
}) {
const { formatMessage } = useIntl();
// Fetch accounts resource views and fields.
const fetchResourceViews = useQuery(
['resource-views', 'accounts'],
(key, resourceName) => requestFetchResourceViews(resourceName),
);
// Fetch the accounts resource fields.
const fetchResourceFields = useQuery(
['resource-fields', 'accounts'],
(key, resourceName) => requestFetchResourceFields(resourceName),
);
// Fetch accounts list according to the given custom view id.
const fetchAccountsHook = useQuery(
['accounts-table', accountsTableQuery],
(key, q) => requestFetchAccountsTable(),
);
useEffect(() => {
changePageTitle(formatMessage({ id: 'chart_of_accounts' }));
}, [changePageTitle, formatMessage]);
// Refetches accounts data table when current custom view changed.
const handleFilterChanged = useCallback(() => {
}, []);
// Handle fetch data of accounts datatable.
const handleFetchData = useCallback(
({ pageIndex, pageSize, sortBy }) => {
addAccountsTableQueries({
...(sortBy.length > 0
? {
column_sort_by: sortBy[0].id,
sort_order: sortBy[0].desc ? 'desc' : 'asc',
}
: {}),
});
},
[addAccountsTableQueries],
);
return (
<DashboardInsider
loading={fetchResourceFields.isFetching || fetchResourceViews.isFetching}
name={'accounts-chart'}
>
<AccountsActionsBar
onFilterChanged={handleFilterChanged}
/>
<AccountsChartProvider query={accountsTableQuery}>
<AccountsActionsBar />
<DashboardPageContent>
<AccountsViewPage />
</DashboardPageContent>
<AccountsAlerts />
</DashboardInsider>
</AccountsChartProvider>
);
}
export default compose(
withAccountsActions,
withAccountsTableActions,
withViewsActions,
withResourceActions,
withDashboardActions,
withAccounts(({ accountsTableQuery }) => ({
accountsTableQuery,

View File

@@ -0,0 +1,51 @@
import React, { createContext } from 'react';
import DashboardInsider from 'components/Dashboard/DashboardInsider';
import { useResourceViews, useResourceFields, useAccounts } from 'hooks/query';
const AccountsChartContext = createContext();
/**
* Accounts chart data provider.
*/
function AccountsChartProvider({ query, ...props }) {
// Fetch accounts resource views and fields.
const { data: resourceViews, isFetching: isViewsLoading } = useResourceViews(
'accounts',
);
// Fetch the accounts resource fields.
const {
data: resourceFields,
isFetching: isFieldsLoading,
} = useResourceFields('accounts');
// Fetch accounts list according to the given custom view id.
const { data: accounts, isFetching: isAccountsLoading } = useAccounts(
query,
{ keepPreviousData: true }
);
// Provider payload.
const provider = {
accounts,
resourceFields,
resourceViews,
isAccountsLoading,
isFieldsLoading,
isViewsLoading,
};
return (
<DashboardInsider
loading={isViewsLoading || isFieldsLoading}
name={'accounts-chart'}
>
<AccountsChartContext.Provider value={provider} {...props} />
</DashboardInsider>
);
}
const useAccountsChartContext = () => React.useContext(AccountsChartContext);
export { AccountsChartProvider, useAccountsChartContext };

View File

@@ -1,98 +1,53 @@
import React, { useCallback, useState, useMemo, useEffect } from 'react';
import {
Button,
Popover,
Position,
} from '@blueprintjs/core';
import { withRouter } from 'react-router';
import { FormattedMessage as T, useIntl } from 'react-intl';
import React, { useCallback, useMemo } from 'react';
import { Button, Popover, Position } from '@blueprintjs/core';
import { useIntl } from 'react-intl';
import classNames from 'classnames';
import { Icon, DataTable, If } from 'components';
import { saveInvoke, compose } from 'utils';
import { useUpdateEffect } from 'hooks';
import { Icon, DataTable } from 'components';
import { saveInvoke } from 'utils';
import { CLASSES } from 'common/classes';
import { NormalCell, BalanceCell, AccountActionsMenuList } from './components';
import { NormalCell, BalanceCell, AccountActionsMenu } from './components';
import { TableFastCell } from 'components';
import TableVirtualizedListRows from 'components/Datatable/TableVirtualizedRows';
import withDashboardActions from 'containers/Dashboard/withDashboardActions';
import withAccountsActions from 'containers/Accounts/withAccountsActions';
import withAccounts from 'containers/Accounts/withAccounts';
import withCurrentView from 'containers/Views/withCurrentView';
import TableVirtualizedListRows from 'components/Datatable/TableVirtualizedRows';
import TableSkeletonRows from 'components/Datatable/TableSkeletonRows';
import TableSkeletonHeader from 'components/Datatable/TableHeaderSkeleton';
/**
* Accounts data-table.
*/
function AccountsDataTable({
// #withDashboardActions
accountsTable,
accountsLoading,
// #
currentViewId,
export default function AccountsDataTable({
// #ownProps
accounts,
loading,
onFetchData,
onSelectedRowsChange,
onDeleteAccount,
onInactivateAccount,
onActivateAccount,
onEditAccount,
onNewChildAccount
// onDeleteAccount,
// onInactivateAccount,
// onActivateAccount,
// onEditAccount,
// onNewChildAccount,
}) {
const [isMounted, setIsMounted] = useState(false);
const { formatMessage } = useIntl();
useEffect(() => {
setIsMounted(false);
}, [currentViewId]);
useUpdateEffect(() => {
if (!accountsLoading) {
setIsMounted(true);
}
}, [accountsLoading, setIsMounted]);
const ActionsCell = useMemo(() =>
({ row }) => (
const ActionsCell = useMemo(
() => ({ row }) => (
<Popover
content={<AccountActionsMenuList
account={row.original}
onDeleteAccount={onDeleteAccount}
onInactivateAccount={onInactivateAccount}
onActivateAccount={onActivateAccount}
onEditAccount={onEditAccount}
/>}
content={<AccountActionsMenu row={row} />}
position={Position.RIGHT_TOP}
>
<Button icon={<Icon icon="more-h-16" iconSize={16} />} />
</Popover>
), [
onDeleteAccount,
onInactivateAccount,
onActivateAccount,
onEditAccount
]);
),
[],
);
const RowContextMenu = useMemo(() => ({ row }) => (
<AccountActionsMenuList
account={row.original}
onDeleteAccount={onDeleteAccount}
onInactivateAccount={onInactivateAccount}
onActivateAccount={onActivateAccount}
onEditAccount={onEditAccount}
onNewChildAccount={onNewChildAccount}
/>
), [
onDeleteAccount,
onInactivateAccount,
onActivateAccount,
onEditAccount,
onNewChildAccount
]);
const RowContextMenu = useMemo(
() => ({ row }) => <AccountActionsMenu row={row} />,
[],
);
const columns = useMemo(
() => [
@@ -144,6 +99,7 @@ function AccountsDataTable({
Cell: ActionsCell,
className: 'actions',
width: 50,
skeletonWidthMin: 100,
},
],
[ActionsCell, formatMessage],
@@ -172,13 +128,14 @@ function AccountsDataTable({
<DataTable
noInitialFetch={true}
columns={columns}
data={accountsTable}
data={accounts}
onFetchData={handleDatatableFetchData}
selectionColumn={true}
expandable={true}
sticky={true}
onSelectedRowsChange={handleSelectedRowsChange}
loading={accountsLoading && !isMounted}
loading={loading}
headerLoading={loading}
rowContextMenu={RowContextMenu}
rowClassNames={rowClassNames}
autoResetExpanded={false}
@@ -189,6 +146,8 @@ function AccountsDataTable({
selectionColumnWidth={50}
TableCellRenderer={TableFastCell}
TableRowsRenderer={TableVirtualizedListRows}
TableLoadingRenderer={TableSkeletonRows}
TableHeaderSkeletonRenderer={TableSkeletonHeader}
// #TableVirtualizedListRows props.
vListrowHeight={42}
vListOverscanRowCount={10}
@@ -196,14 +155,3 @@ function AccountsDataTable({
</div>
);
}
export default compose(
withRouter,
withCurrentView,
withDashboardActions,
withAccountsActions,
withAccounts(({ accountsLoading, accountsTable }) => ({
accountsLoading,
accountsTable,
})),
)(AccountsDataTable);

View File

@@ -1,7 +1,8 @@
import React, { memo } from 'react';
import React from 'react';
import { Switch, Route } from 'react-router-dom';
import AccountsViewsTabs from 'containers/Accounts/AccountsViewsTabs';
import AccountsDataTable from 'containers/Accounts/AccountsDataTable';
import { useAccountsChartContext } from 'containers/Accounts/AccountsChartProvider';
import withAccountsTableActions from 'containers/Accounts/withAccountsTableActions';
import withAlertsActions from 'containers/Alert/withAlertActions';
@@ -13,45 +14,50 @@ import { compose } from 'utils';
* Accounts view page.
*/
function AccountsViewPage({
// #withAlertActions
openAlert,
// #withDialog.
openDialog,
// #withAccountsTableActions
setSelectedRowsAccounts
setSelectedRowsAccounts,
}) {
const { isAccountsLoading, accounts } = useAccountsChartContext();
// Handle delete action account.
const handleDeleteAccount = (account) => {
openAlert('account-delete', { accountId: account.id })
// openAlert('account-delete', { accountId: account.id });
};
// Handle activate action account.
const handleActivateAccount = (account) => {
openAlert('account-activate', { accountId: account.id });
// openAlert('account-activate', { accountId: account.id });
};
// Handle inactivate action account.
const handleInactivateAccount = (account) => {
openAlert('account-inactivate', { accountId: account.id });
// openAlert('account-inactivate', { accountId: account.id });
};
// Handle select accounts datatable rows.
const handleSelectedRowsChange = (selectedRows) => {
const selectedRowsIds = selectedRows.map(r => r.id);
setSelectedRowsAccounts(selectedRowsIds);
// const handleSelectedRowsChange = (selectedRows) => {
// const selectedRowsIds = selectedRows.map((r) => r.id);
// setSelectedRowsAccounts(selectedRowsIds);
// };
// Handle edit account action.
const handleEditAccount = (account) => {
// openDialog('account-form', { action: 'edit', id: account.id });
};
const handleEditAccount = (account) => {
openDialog('account-form', { action: 'edit', id: account.id });
}
// Handle new child button click.
const handleNewChildAccount = (account) => {
openDialog('account-form', {
action: 'new_child',
parentAccountId: account.id,
accountType: account.account_type,
});
// openDialog('account-form', {
// action: 'new_child',
// parentAccountId: account.id,
// accountType: account.account_type,
// });
};
return (
@@ -63,22 +69,22 @@ function AccountsViewPage({
<AccountsViewsTabs />
<AccountsDataTable
onDeleteAccount={handleDeleteAccount}
onInactivateAccount={handleInactivateAccount}
onActivateAccount={handleActivateAccount}
onSelectedRowsChange={handleSelectedRowsChange}
onEditAccount={handleEditAccount}
onNewChildAccount={handleNewChildAccount}
loading={isAccountsLoading}
accounts={accounts}
// onDeleteAccount={handleDeleteAccount}
// onInactivateAccount={handleInactivateAccount}
// onActivateAccount={handleActivateAccount}
// onSelectedRowsChange={handleSelectedRowsChange}
// onEditAccount={handleEditAccount}
// onNewChildAccount={handleNewChildAccount}
/>
</Route>
</Switch>
);
}
const AccountsViewPageMemo = memo(AccountsViewPage);
export default compose(
withAlertsActions,
withAccountsTableActions,
withDialogActions
)(AccountsViewPageMemo);
withDialogActions,
)(AccountsViewPage);

View File

@@ -1,15 +1,12 @@
import React, { useEffect, useMemo, memo, useCallback } from 'react';
import { useHistory } from 'react-router';
import { connect } from 'react-redux';
import React, { useMemo, useCallback } from 'react';
import { Alignment, Navbar, NavbarGroup } from '@blueprintjs/core';
import { useParams, withRouter } from 'react-router-dom';
import { useParams } from 'react-router-dom';
import { pick } from 'lodash';
import { DashboardViewsTabs } from 'components';
import withDashboardActions from 'containers/Dashboard/withDashboardActions';
import withAccounts from 'containers/Accounts/withAccounts';
import { useAccountsChartContext } from 'containers/Accounts/AccountsChartProvider';
import withAccountsTableActions from 'containers/Accounts/withAccountsTableActions';
import withViewDetail from 'containers/Views/withViewDetails';
import { compose } from 'utils';
@@ -17,41 +14,28 @@ import { compose } from 'utils';
* Accounts views tabs.
*/
function AccountsViewsTabs({
// #withViewDetail
viewId,
viewItem,
// #withAccounts
accountsViews,
// #withAccountsTableActions
changeAccountsCurrentView,
// #withDashboardActions
setTopbarEditView,
changePageSubtitle,
addAccountsTableQuery,
}) {
const history = useHistory();
const { resourceViews } = useAccountsChartContext();
const { custom_view_id: customViewId = null } = useParams();
useEffect(() => {
setTopbarEditView(customViewId);
changePageSubtitle(customViewId && viewItem ? viewItem.name : '');
}, [customViewId, viewItem, changePageSubtitle, setTopbarEditView]);
const handleTabChange = useCallback(
(viewId) => {
addAccountsTableQuery({
custom_view_id: viewId || null,
});
},
[addAccountsTableQuery],
);
// Handle click a new view tab.
const handleClickNewView = useCallback(() => {
setTopbarEditView(null);
history.push('/custom_views/accounts/new');
}, [setTopbarEditView]);
const handleTabChange = useCallback((viewId) => {
changeAccountsCurrentView(viewId || -1);
}, [changeAccountsCurrentView]);
const tabs = useMemo(() => accountsViews.map((view) => ({
...pick(view, ['name', 'id']),
})), [accountsViews]);;
const tabs = useMemo(
() =>
resourceViews.map((view) => ({
...pick(view, ['name', 'id']),
})),
[resourceViews],
);
return (
<Navbar className="navbar--dashboard-views">
@@ -68,21 +52,6 @@ function AccountsViewsTabs({
);
}
const AccountsViewsTabsMemo = memo(AccountsViewsTabs);
const mapStateToProps = (state, ownProps) => ({
viewId: -1,
});
const withAccountsViewsTabs = connect(mapStateToProps);
export default compose(
withRouter,
withAccountsViewsTabs,
withDashboardActions,
withAccounts(({ accountsViews }) => ({
accountsViews,
})),
withAccountsTableActions,
withViewDetail(),
)(AccountsViewsTabsMemo);
)(AccountsViewsTabs);

View File

@@ -13,17 +13,8 @@ import classNames from 'classnames';
import { Icon, Money, If } from 'components';
import { saveInvoke } from 'utils';
import { formatMessage } from 'services/intl';
import { POPOVER_CONTENT_SIZING } from '@blueprintjs/core/lib/esm/common/classes';
export function AccountActionsMenuList({
account,
onNewChildAccount,
onEditAccount,
onActivateAccount,
onInactivateAccount,
onDeleteAccount,
}) {
export function AccountActionsMenu({ row: { original } }) {
return (
<Menu>
<MenuItem
@@ -34,38 +25,39 @@ export function AccountActionsMenuList({
<MenuItem
icon={<Icon icon="pen-18" />}
text={formatMessage({ id: 'edit_account' })}
onClick={() => saveInvoke(onEditAccount, account)}
// onClick={handleEditAccount}
/>
<MenuItem
icon={<Icon icon="plus" />}
text={formatMessage({ id: 'new_child_account' })}
onClick={() => saveInvoke(onNewChildAccount, account)}
// onClick={handleNewChildAccount}
/>
<MenuDivider />
<If condition={account.active}>
<If condition={original.active}>
<MenuItem
text={formatMessage({ id: 'inactivate_account' })}
icon={<Icon icon="pause-16" iconSize={16} />}
onClick={() => saveInvoke(onInactivateAccount, account)}
// onClick={handleInactivateAccount}
/>
</If>
<If condition={!account.active}>
<If condition={!original.active}>
<MenuItem
text={formatMessage({ id: 'activate_account' })}
icon={<Icon icon="play-16" iconSize={16} />}
onClick={() => saveInvoke(onActivateAccount, account)}
// onClick={handleActivateAccount}
/>
</If>
<MenuItem
text={formatMessage({ id: 'delete_account' })}
icon={<Icon icon="trash-16" iconSize={16} />}
intent={Intent.DANGER}
onClick={() => saveInvoke(onDeleteAccount, account)}
// onClick={handleDeleteA ccount}
/>
</Menu>
);
}
export function NormalCell({ cell: { value } }) {
const { formatMessage } = useIntl();
const arrowDirection = value === 'credit' ? 'down' : 'up';

View File

@@ -16,10 +16,10 @@ const mapActionsToProps = (dispatch) => ({
key,
value,
}),
addAccountsTableQueries: (queries) =>
addAccountsTableQuery: (queries) =>
dispatch({
type: t.ACCOUNTS_TABLE_QUERIES_ADD,
queries,
payload: { queries },
}),
setSelectedRowsAccounts: (selectedRows) =>
dispatch({

View File

@@ -1,15 +1,13 @@
import React, { useState } from 'react';
import React from 'react';
import { FormattedMessage as T, useIntl } from 'react-intl';
import { Intent, Alert } from '@blueprintjs/core';
import { queryCache } from 'react-query';
import { AppToaster } from 'components';
import withAlertStoreConnect from 'containers/Alert/withAlertStoreConnect';
import withAlertActions from 'containers/Alert/withAlertActions';
import withAccountsActions from 'containers/Accounts/withAccountsActions';
import { useActivateAccount } from 'hooks/query';
import { compose } from 'utils';
/**
* Account activate alert.
*/
@@ -20,11 +18,12 @@ function AccountActivateAlert({
// #withAlertActions
closeAlert,
requestActivateAccount,
}) {
const { formatMessage } = useIntl();
const [isLoading, setLoading] = useState(false);
const {
mutateAsync: activateAccount,
isLoading
} = useActivateAccount();
// Handle alert cancel.
const handleCancel = () => {
@@ -33,22 +32,17 @@ function AccountActivateAlert({
// Handle activate account confirm.
const handleConfirmAccountActivate = () => {
setLoading(true);
requestActivateAccount(accountId)
.then(() => {
AppToaster.show({
message: formatMessage({
id: 'the_account_has_been_successfully_activated',
}),
intent: Intent.SUCCESS,
});
queryCache.invalidateQueries('accounts-table');
})
.catch((error) => {})
.finally(() => {
closeAlert('account-activate');
setLoading(false);
activateAccount(accountId).then(() => {
AppToaster.show({
message: formatMessage({
id: 'the_account_has_been_successfully_activated',
}),
intent: Intent.SUCCESS,
});
closeAlert('account-activate');
}).finally(() => {
closeAlert('account-activate');
});
};
return (
@@ -71,5 +65,4 @@ function AccountActivateAlert({
export default compose(
withAlertStoreConnect(),
withAlertActions,
withAccountsActions,
)(AccountActivateAlert);

View File

@@ -1,19 +1,19 @@
import React, { useState } from 'react';
import React from 'react';
import {
FormattedMessage as T,
FormattedHTMLMessage,
useIntl,
} from 'react-intl';
import { Intent, Alert } from '@blueprintjs/core';
import { queryCache } from 'react-query';
import { AppToaster } from 'components';
import { handleDeleteErrors } from 'containers/Accounts/utils';
import withAccountsActions from 'containers/Accounts/withAccountsActions';
import withAlertStoreConnect from 'containers/Alert/withAlertStoreConnect';
import withAlertActions from 'containers/Alert/withAlertActions';
import { useDeleteAccount } from 'hooks/query';
import { compose } from 'utils';
/**
@@ -26,40 +26,32 @@ function AccountDeleteAlert({
isOpen,
payload: { accountId },
// #withAccountsActions
requestDeleteAccount,
// #withAlertActions
closeAlert,
}) {
const { formatMessage } = useIntl();
const [isLoading, setLoading] = useState(false);
const {
isLoading,
mutateAsync: deleteAccount,
} = useDeleteAccount();
// handle cancel delete account alert.
const handleCancelAccountDelete = () => {
closeAlert(name);
};
// Handle confirm account delete.
const handleConfirmAccountDelete = () => {
setLoading(true);
requestDeleteAccount(accountId)
.then(() => {
AppToaster.show({
message: formatMessage({
id: 'the_account_has_been_successfully_deleted',
}),
intent: Intent.SUCCESS,
});
queryCache.invalidateQueries('accounts-table');
})
.catch((errors) => {
handleDeleteErrors(errors);
})
.finally(() => {
setLoading(false);
closeAlert(name);
deleteAccount(accountId).then(() => {
AppToaster.show({
message: formatMessage({
id: 'the_account_has_been_successfully_deleted',
}),
intent: Intent.SUCCESS,
});
closeAlert(name);
}).catch(errors => {
handleDeleteErrors(errors);
});
};
return (
@@ -85,5 +77,4 @@ function AccountDeleteAlert({
export default compose(
withAlertStoreConnect(),
withAlertActions,
withAccountsActions,
)(AccountDeleteAlert);

View File

@@ -1,50 +1,50 @@
import React, { useState } from 'react';
import React from 'react';
import { FormattedMessage as T, useIntl } from 'react-intl';
import { Intent, Alert } from '@blueprintjs/core';
import { queryCache } from 'react-query';
import { AppToaster } from 'components';
import withAlertStoreConnect from 'containers/Alert/withAlertStoreConnect';
import withAlertActions from 'containers/Alert/withAlertActions';
import withAccountsActions from 'containers/Accounts/withAccountsActions';
import { compose } from 'utils';
import { useInactivateAccount } from 'hooks/query';
/**
* Account inactivate alert.
*/
function AccountInactivateAlert({
name,
// #withAlertStoreConnect
isOpen,
payload: { accountId },
// #withAlertActions
closeAlert,
// #withAccountsActions
requestInactiveAccount,
}) {
const { formatMessage } = useIntl();
const [isLoading, setLoading] = useState(false);
const {
mutateAsync: inactivateAccount,
isLoading
} = useInactivateAccount();
const handleCancelInactiveAccount = () => {
closeAlert('account-inactivate');
};
const handleConfirmAccountActive = () => {
setLoading(true);
requestInactiveAccount(accountId)
.then(() => {
AppToaster.show({
message: formatMessage({
id: 'the_account_has_been_successfully_inactivated',
}),
intent: Intent.SUCCESS,
});
queryCache.invalidateQueries('accounts-table');
})
.catch((error) => {})
.finally(() => {
setLoading(false);
closeAlert('account-inactivate');
inactivateAccount(accountId).then(() => {
AppToaster.show({
message: formatMessage({
id: 'the_account_has_been_successfully_inactivated',
}),
intent: Intent.SUCCESS,
});
}).catch(() => {
}).finally(() => {
closeAlert('account-inactivate');
});
};
return (
@@ -67,5 +67,4 @@ function AccountInactivateAlert({
export default compose(
withAlertStoreConnect(),
withAlertActions,
withAccountsActions,
)(AccountInactivateAlert);

View File

@@ -0,0 +1,70 @@
import React from 'react';
import {
FormattedMessage as T,
useIntl,
} from 'react-intl';
import { Intent, Alert } from '@blueprintjs/core';
import { AppToaster } from 'components';
import withAlertStoreConnect from 'containers/Alert/withAlertStoreConnect';
import withAlertActions from 'containers/Alert/withAlertActions';
import { useDeleteBill } from 'hooks/query';
import { compose } from 'utils';
/**
* Bill delete alert.
*/
function BillDeleteAlert({
name,
// #withAlertStoreConnect
isOpen,
payload: { billId },
// #withAlertActions
closeAlert,
}) {
const { formatMessage } = useIntl();
const { isLoading, mutateAsync: deleteBillMutate } = useDeleteBill();
// handle cancel Bill
const handleCancel = () => {
closeAlert(name);
};
// handleConfirm delete invoice
const handleConfirmBillDelete = () => {
deleteBillMutate(billId).then(() => {
AppToaster.show({
message: formatMessage({
id: 'the_bill_has_been_deleted_successfully',
}),
intent: Intent.SUCCESS,
});
});
};
return (
<Alert
cancelButtonText={<T id={'cancel'} />}
confirmButtonText={<T id={'delete'} />}
icon={'trash'}
intent={Intent.DANGER}
isOpen={isOpen}
onCancel={handleCancel}
onConfirm={handleConfirmBillDelete}
loading={isLoading}
>
<p>
<T id={'once_delete_this_bill_you_will_able_to_restore_it'} />
</p>
</Alert>
);
}
export default compose(
withAlertStoreConnect(),
withAlertActions,
)(BillDeleteAlert);

View File

@@ -0,0 +1,69 @@
import React from 'react';
import { FormattedMessage as T, useIntl } from 'react-intl';
import { Intent, Alert } from '@blueprintjs/core';
import { AppToaster } from 'components';
import withAlertStoreConnect from 'containers/Alert/withAlertStoreConnect';
import withAlertActions from 'containers/Alert/withAlertActions';
import { useOpenBill } from 'hooks/query';
import { compose } from 'utils';
/**
* Bill open alert.
*/
function BillOpenAlert({
name,
// #withAlertStoreConnect
isOpen,
payload: { billId },
// #withAlertActions
closeAlert,
}) {
const { formatMessage } = useIntl();
const { isLoading, mutateAsync: openBillMutate } = useOpenBill();
// Handle cancel open bill alert.
const handleCancelOpenBill = () => {
closeAlert(name);
};
// Handle confirm bill open.
const handleConfirmBillOpen = () => {
openBillMutate(billId)
.then(() => {
AppToaster.show({
message: formatMessage({
id: 'the_bill_has_been_opened_successfully',
}),
intent: Intent.SUCCESS,
});
})
.finally((error) => {
closeAlert(name);
});
};
return (
<Alert
cancelButtonText={<T id={'cancel'} />}
confirmButtonText={<T id={'open'} />}
intent={Intent.WARNING}
isOpen={isOpen}
onCancel={handleCancelOpenBill}
onConfirm={handleConfirmBillOpen}
loading={isLoading}
>
<p>
<T id={'are_sure_to_open_this_bill'} />
</p>
</Alert>
);
}
export default compose(
withAlertStoreConnect(),
withAlertActions,
)(BillOpenAlert);

View File

@@ -13,8 +13,10 @@ import withAlertStoreConnect from 'containers/Alert/withAlertStoreConnect';
import withAlertActions from 'containers/Alert/withAlertActions';
import withCustomersActions from 'containers/Customers/withCustomersActions';
import { useDeleteCustomer } from 'hooks/query';
import { compose } from 'utils';
/**
* Customer delete alert.
*/
@@ -31,7 +33,10 @@ function CustomerDeleteAlert({
closeAlert,
}) {
const { formatMessage } = useIntl();
const [isLoading, setLoading] = useState(false);
const {
mutateAsync: deleteCustomerMutate,
isLoading
} = useDeleteCustomer();
// handle cancel delete alert.
const handleCancelDeleteAlert = () => {
@@ -40,8 +45,7 @@ function CustomerDeleteAlert({
// handle confirm delete customer.
const handleConfirmDeleteCustomer = useCallback(() => {
setLoading(true);
requestDeleteCustomer(customerId)
deleteCustomerMutate(customerId)
.then(() => {
AppToaster.show({
message: formatMessage({
@@ -55,10 +59,9 @@ function CustomerDeleteAlert({
transformErrors(errors);
})
.finally(() => {
setLoading(false);
closeAlert(name);
});
}, [requestDeleteCustomer, customerId, formatMessage]);
}, [deleteCustomerMutate, customerId, closeAlert, name, formatMessage]);
return (
<Alert

View File

@@ -1,17 +1,18 @@
import React, { useCallback, useState } from 'react';
import React, { useCallback } from 'react';
import { FormattedMessage as T, useIntl } from 'react-intl';
import { Intent, Alert } from '@blueprintjs/core';
import { queryCache } from 'react-query';
import { useApproveEstimate } from 'hooks/query';
import { AppToaster } from 'components';
import withAlertStoreConnect from 'containers/Alert/withAlertStoreConnect';
import withAlertActions from 'containers/Alert/withAlertActions';
import withEstimateActions from 'containers/Sales/Estimate/withEstimateActions';
import { compose } from 'utils';
/**
* Estimate Approve alert.
* Estimate approve alert.
*/
function EstimateApproveAlert({
name,
@@ -27,7 +28,10 @@ function EstimateApproveAlert({
closeAlert,
}) {
const { formatMessage } = useIntl();
const [isLoading, setLoading] = useState(false);
const {
mutateAsync: deliverEstimateMutate,
isLoading,
} = useApproveEstimate();
// handle cancel approve alert.
const handleCancelApproveEstimate = () => {
@@ -35,8 +39,7 @@ function EstimateApproveAlert({
};
// Handle confirm estimate approve.
const handleConfirmEstimateApprove = useCallback(() => {
setLoading(true);
requestApproveEstimate(estimateId)
deliverEstimateMutate(estimateId)
.then(() => {
AppToaster.show({
message: formatMessage({
@@ -48,10 +51,9 @@ function EstimateApproveAlert({
})
.catch((error) => {})
.finally(() => {
setLoading(false);
closeAlert(name);
});
}, [estimateId, requestApproveEstimate, formatMessage]);
}, [estimateId, deliverEstimateMutate, closeAlert, name, formatMessage]);
return (
<Alert
@@ -74,5 +76,4 @@ function EstimateApproveAlert({
export default compose(
withAlertStoreConnect(),
withAlertActions,
withEstimateActions,
)(EstimateApproveAlert);

View File

@@ -5,12 +5,12 @@ import {
useIntl,
} from 'react-intl';
import { Intent, Alert } from '@blueprintjs/core';
import { queryCache } from 'react-query';
import { useDeleteEstimate } from 'hooks/query';
import { AppToaster } from 'components';
import withAlertStoreConnect from 'containers/Alert/withAlertStoreConnect';
import withAlertActions from 'containers/Alert/withAlertActions';
import withEstimateActions from 'containers/Sales/Estimate/withEstimateActions';
import { compose } from 'utils';
@@ -24,14 +24,11 @@ function EstimateDeleteAlert({
isOpen,
payload: { estimateId },
// #withEstimateActions
requestDeleteEstimate,
// #withAlertActions
closeAlert,
}) {
const { formatMessage } = useIntl();
const [isLoading, setLoading] = useState(false);
const { mutateAsync: deleteEstimateMutate, isLoading } = useDeleteEstimate();
// handle cancel delete alert.
const handleCancelEstimateDelete = () => {
@@ -40,8 +37,7 @@ function EstimateDeleteAlert({
// handle confirm delete estimate
const handleConfirmEstimateDelete = useCallback(() => {
setLoading(true);
requestDeleteEstimate(estimateId)
deleteEstimateMutate(estimateId)
.then(() => {
AppToaster.show({
message: formatMessage({
@@ -49,14 +45,12 @@ function EstimateDeleteAlert({
}),
intent: Intent.SUCCESS,
});
queryCache.invalidateQueries('estimates-table');
})
.catch(({ errors }) => {})
.finally(() => {
setLoading(false);
closeAlert(name);
});
}, [requestDeleteEstimate, formatMessage, estimateId]);
}, [deleteEstimateMutate, name, closeAlert, formatMessage, estimateId]);
return (
<Alert
@@ -81,5 +75,4 @@ function EstimateDeleteAlert({
export default compose(
withAlertStoreConnect(),
withAlertActions,
withEstimateActions,
)(EstimateDeleteAlert);

View File

@@ -2,11 +2,12 @@ import React, { useCallback, useState } from 'react';
import { FormattedMessage as T, useIntl } from 'react-intl';
import { Intent, Alert } from '@blueprintjs/core';
import { queryCache } from 'react-query';
import { useDeliverEstimate } from 'hooks/query';
import { AppToaster } from 'components';
import withAlertStoreConnect from 'containers/Alert/withAlertStoreConnect';
import withAlertActions from 'containers/Alert/withAlertActions';
import withEstimateActions from 'containers/Sales/Estimate/withEstimateActions';
import { compose } from 'utils';
@@ -20,14 +21,11 @@ function EstimateDeliveredAlert({
isOpen,
payload: { estimateId },
// #withEstimateActions
requestDeliveredEstimate,
// #withAlertActions
closeAlert,
}) {
const { formatMessage } = useIntl();
const [isLoading, setLoading] = useState(false);
const { mutateAsync: deliverEstimateMutate, isLoading } = useDeliverEstimate();
// Handle cancel delivered estimate alert.
const handleCancelDeliveredEstimate = () => {
@@ -36,8 +34,7 @@ function EstimateDeliveredAlert({
// Handle confirm estimate delivered.
const handleConfirmEstimateDelivered = useCallback(() => {
setLoading(true);
requestDeliveredEstimate(estimateId)
deliverEstimateMutate(estimateId)
.then(() => {
AppToaster.show({
message: formatMessage({
@@ -50,9 +47,8 @@ function EstimateDeliveredAlert({
.catch((error) => {})
.finally(() => {
closeAlert(name);
setLoading(false);
});
}, [estimateId, requestDeliveredEstimate, formatMessage]);
}, [estimateId, deliverEstimateMutate, formatMessage]);
return (
<Alert
@@ -74,5 +70,4 @@ function EstimateDeliveredAlert({
export default compose(
withAlertStoreConnect(),
withAlertActions,
withEstimateActions,
)(EstimateDeliveredAlert);

View File

@@ -1,8 +1,10 @@
import React, { useCallback, useState } from 'react';
import React, { useCallback } from 'react';
import { FormattedMessage as T, useIntl } from 'react-intl';
import { Intent, Alert } from '@blueprintjs/core';
import { queryCache } from 'react-query';
import { AppToaster } from 'components';
import { useRejectEstimate } from 'hooks/query';
import withAlertStoreConnect from 'containers/Alert/withAlertStoreConnect';
import withAlertActions from 'containers/Alert/withAlertActions';
@@ -27,7 +29,11 @@ function EstimateRejectAlert({
closeAlert,
}) {
const { formatMessage } = useIntl();
const [isLoading, setLoading] = useState(false);
const {
mutateAsync: rejectEstimateMutate,
isLoading
} = useRejectEstimate();
// Handle cancel reject estimate alert.
const handleCancelRejectEstimate = () => {
closeAlert(name);
@@ -35,7 +41,6 @@ function EstimateRejectAlert({
// Handle confirm estimate reject.
const handleConfirmEstimateReject = useCallback(() => {
setLoading(true);
requestRejectEstimate(estimateId)
.then(() => {
AppToaster.show({
@@ -48,10 +53,9 @@ function EstimateRejectAlert({
})
.catch((error) => {})
.finally(() => {
setLoading(false);
closeAlert(name);
});
}, [estimateId, requestRejectEstimate, formatMessage]);
}, [estimateId, rejectEstimateMutate, formatMessage]);
return (
<Alert

View File

@@ -0,0 +1,66 @@
import React from 'react';
import { FormattedMessage as T, useIntl } from 'react-intl';
import { Intent, Alert } from '@blueprintjs/core';
import { AppToaster } from 'components';
import withAlertStoreConnect from 'containers/Alert/withAlertStoreConnect';
import withAlertActions from 'containers/Alert/withAlertActions';
import { usePublishExpense } from 'hooks/query';
import { compose } from 'utils';
/**
* Expense bulk delete alert.
*/
function ExpenseBulkDeleteAlert({
closeAlert,
// #withAlertStoreConnect
name,
payload: { expenseId, selectedCount },
isOpen,
}) {
// Handle confirm journals bulk delete.
const handleConfirmBulkDelete = () => {
// requestDeleteBulkExpenses(bulkDelete)
// .then(() => {
// AppToaster.show({
// message: formatMessage(
// { id: 'the_expenses_have_been_deleted_successfully' },
// { count: selectedRowsCount },
// ),
// intent: Intent.SUCCESS,
// });
// })
// .catch((error) => {
// });
};
// Handle cancel bulk delete alert.
const handleCancelBulkDelete = () => {
closeAlert(name);
};
return (
<Alert
cancelButtonText={<T id={'cancel'} />}
confirmButtonText={
<T id={'delete_count'} values={{ count: selectedCount }} />
}
icon="trash"
intent={Intent.DANGER}
isOpen={isOpen}
onCancel={handleCancelBulkDelete}
onConfirm={handleConfirmBulkDelete}
>
<p>
<T id={'once_delete_these_expenses_you_will_not_able_restore_them'} />
</p>
</Alert>
);
}
export default compose(
withAlertStoreConnect(),
withAlertActions,
)(ExpenseBulkDeleteAlert);

View File

@@ -0,0 +1,71 @@
import React from 'react';
import { FormattedMessage as T, useIntl } from 'react-intl';
import { Intent, Alert } from '@blueprintjs/core';
import { AppToaster } from 'components';
import withAlertStoreConnect from 'containers/Alert/withAlertStoreConnect';
import withAlertActions from 'containers/Alert/withAlertActions';
import { useDeleteExpense } from 'hooks/query';
import { compose } from 'utils';
/**
* Expense delete alert.
*/
function ExpenseDeleteAlert({
// #withAlertActions
closeAlert,
// #withAlertStoreConnect
isOpen,
payload: { expenseId },
}) {
const { formatMessage } = useIntl();
const {
mutateAsync: deleteExpenseMutate,
isLoading,
deleteExpense
} = useDeleteExpense();
// Handle cancel expense journal.
const handleCancelExpenseDelete = () => {
closeAlert('expense-delete');
};
// Handle confirm delete expense.
const handleConfirmExpenseDelete = () => {
deleteExpenseMutate(expenseId).then(() => {
AppToaster.show({
message: formatMessage(
{ id: 'the_expense_has_been_deleted_successfully' },
{ number: expenseId },
),
intent: Intent.SUCCESS,
});
}).finally(() => {
closeAlert('expense-delete');
});
};
return (
<Alert
cancelButtonText={<T id={'cancel'} />}
confirmButtonText={<T id={'delete'} />}
icon="trash"
intent={Intent.DANGER}
isOpen={isOpen}
onCancel={handleCancelExpenseDelete}
onConfirm={handleConfirmExpenseDelete}
loading={isLoading}
>
<p>
<T id={'once_delete_this_expense_you_will_able_to_restore_it'} />
</p>
</Alert>
);
}
export default compose(
withAlertStoreConnect(),
withAlertActions,
)(ExpenseDeleteAlert);

View File

@@ -0,0 +1,64 @@
import React from 'react';
import { FormattedMessage as T, useIntl } from 'react-intl';
import { Intent, Alert } from '@blueprintjs/core';
import { AppToaster } from 'components';
import withAlertStoreConnect from 'containers/Alert/withAlertStoreConnect';
import withAlertActions from 'containers/Alert/withAlertActions';
import { usePublishExpense } from 'hooks/query';
import { compose } from 'utils';
/**
* Expense publish alert.
*/
function ExpensePublishAlert({
closeAlert,
// #withAlertStoreConnect
name,
payload: { expenseId },
isOpen,
}) {
const { formatMessage } = useIntl();
const { mutateAsync: publishExpenseMutate, isLoading } = usePublishExpense();
const handleCancelPublishExpense = () => {
closeAlert('expense-publish');
};
// Handle publish expense confirm.
const handleConfirmPublishExpense = () => {
publishExpenseMutate(expenseId)
.then(() => {
AppToaster.show({
message: formatMessage({
id: 'the_expense_has_been_published',
}),
intent: Intent.SUCCESS,
});
})
.catch((error) => {});
};
return (
<Alert
cancelButtonText={<T id={'cancel'} />}
confirmButtonText={<T id={'publish'} />}
intent={Intent.WARNING}
isOpen={isOpen}
onCancel={handleCancelPublishExpense}
onConfirm={handleConfirmPublishExpense}
loading={isLoading}
>
<p>
<T id={'are_sure_to_publish_this_expense'} />
</p>
</Alert>
);
}
export default compose(
withAlertStoreConnect(),
withAlertActions,
)(ExpensePublishAlert);

View File

@@ -1,4 +1,4 @@
import React, { useCallback, useState } from 'react';
import React, { useCallback } from 'react';
import {
FormattedMessage as T,
FormattedHTMLMessage,
@@ -6,7 +6,9 @@ import {
} from 'react-intl';
import { Intent, Alert } from '@blueprintjs/core';
import { queryCache } from 'react-query';
import { AppToaster } from 'components';
import { useDeleteInvoice } from 'hooks/query';
import { handleDeleteErrors } from 'containers/Sales/Invoice/components';
@@ -26,14 +28,14 @@ function InvoiceDeleteAlert({
isOpen,
payload: { invoiceId },
// #withInvoiceActions
requestDeleteInvoice,
// #withAlertActions
closeAlert,
}) {
const { formatMessage } = useIntl();
const [isLoading, setLoading] = useState(false);
const {
mutateAsync: deleteInvoiceMutate,
isLoading
} = useDeleteInvoice();
// handle cancel delete invoice alert.
const handleCancelDeleteAlert = () => {
@@ -41,9 +43,8 @@ function InvoiceDeleteAlert({
};
// handleConfirm delete invoice
const handleConfirmInvoiceDelete = useCallback(() => {
setLoading(true);
requestDeleteInvoice(invoiceId)
const handleConfirmInvoiceDelete = () => {
deleteInvoiceMutate(invoiceId)
.then(() => {
AppToaster.show({
message: formatMessage({
@@ -51,16 +52,14 @@ function InvoiceDeleteAlert({
}),
intent: Intent.SUCCESS,
});
queryCache.invalidateQueries('invoices-table');
})
.catch((errors) => {
handleDeleteErrors(errors);
})
.finally(() => {
closeAlert(name);
setLoading(false);
});
}, [invoiceId, requestDeleteInvoice, formatMessage]);
};
return (
<Alert

View File

@@ -1,7 +1,8 @@
import React, { useCallback, useState } from 'react';
import React from 'react';
import { FormattedMessage as T, useIntl } from 'react-intl';
import { Intent, Alert } from '@blueprintjs/core';
import { queryCache } from 'react-query';
import { useDeliverInvoice } from 'hooks/query';
import { AppToaster } from 'components';
import withAlertStoreConnect from 'containers/Alert/withAlertStoreConnect';
@@ -11,7 +12,7 @@ import withInvoiceActions from 'containers/Sales/Invoice/withInvoiceActions';
import { compose } from 'utils';
/**
* Invoice alert.
* Sale invoice alert.
*/
function InvoiceDeliverAlert({
name,
@@ -20,14 +21,14 @@ function InvoiceDeliverAlert({
isOpen,
payload: { invoiceId },
// #withInvoiceActions
requestDeliverInvoice,
// #withAlertActions
closeAlert,
}) {
const { formatMessage } = useIntl();
const [isLoading, setLoading] = useState(false);
const {
mutateAsync: deliverInvoiceMutate,
isLoading
} = useDeliverInvoice();
// handle cancel delete deliver alert.
const handleCancelDeleteAlert = () => {
@@ -35,9 +36,8 @@ function InvoiceDeliverAlert({
};
// Handle confirm invoice deliver.
const handleConfirmInvoiceDeliver = useCallback(() => {
setLoading(true);
requestDeliverInvoice(invoiceId)
const handleConfirmInvoiceDeliver = () => {
deliverInvoiceMutate(invoiceId)
.then(() => {
AppToaster.show({
message: formatMessage({
@@ -45,14 +45,12 @@ function InvoiceDeliverAlert({
}),
intent: Intent.SUCCESS,
});
queryCache.invalidateQueries('invoices-table');
})
.catch((error) => {})
.finally(() => {
closeAlert(name);
setLoading(false);
});
}, [invoiceId, requestDeliverInvoice, formatMessage]);
};
return (
<Alert

View File

@@ -1,16 +1,17 @@
import React, { useState } from 'react';
import React from 'react';
import {
FormattedMessage as T,
FormattedHTMLMessage,
useIntl,
} from 'react-intl';
import { Intent, Alert } from '@blueprintjs/core';
import { queryCache } from 'react-query';
import { AppToaster } from 'components';
import withAlertStoreConnect from 'containers/Alert/withAlertStoreConnect';
import withAlertActions from 'containers/Alert/withAlertActions';
import withInventoryAdjustmentActions from 'containers/Items/withInventoryAdjustmentActions';
import {
useDeleteInventoryAdjustment
} from 'hooks/query';
import { compose } from 'utils';
@@ -23,23 +24,24 @@ function InventoryAdjustmentDeleteAlert({
// #withAlertStoreConnect
isOpen,
payload: { inventoryId },
// #withInventoryAdjustmentActions
requestDeleteInventoryAdjustment,
// #withAlertActions
closeAlert,
}) {
const { formatMessage } = useIntl();
const [isLoading, setLoading] = useState(false);
const {
mutateAsync: deleteInventoryAdjMutate,
isLoading
} = useDeleteInventoryAdjustment();
// handle cancel delete alert.
// handle cancel delete alert.
const handleCancelInventoryAdjustmentDelete = () => {
closeAlert(name);
};
// Handle the confirm delete of the inventory adjustment transaction.
const handleConfirmInventoryAdjustmentDelete = () => {
setLoading(true);
requestDeleteInventoryAdjustment(inventoryId)
deleteInventoryAdjMutate(inventoryId)
.then(() => {
AppToaster.show({
message: formatMessage({
@@ -47,11 +49,9 @@ function InventoryAdjustmentDeleteAlert({
}),
intent: Intent.SUCCESS,
});
queryCache.invalidateQueries('inventory-adjustment-list');
})
.catch((errors) => {})
.finally(() => {
setLoading(false);
closeAlert(name);
});
};
@@ -81,5 +81,4 @@ function InventoryAdjustmentDeleteAlert({
export default compose(
withAlertStoreConnect(),
withAlertActions,
withInventoryAdjustmentActions,
)(InventoryAdjustmentDeleteAlert);

View File

@@ -1,10 +1,12 @@
import React, { useState } from 'react';
import React from 'react';
import { FormattedMessage as T, useIntl } from 'react-intl';
import { Intent, Alert } from '@blueprintjs/core';
import { queryCache } from 'react-query';
import { AppToaster } from 'components';
import withItemsActions from 'containers/Items/withItemsActions';
import {
useActivateItem,
} from 'hooks/query';
import withAlertStoreConnect from 'containers/Alert/withAlertStoreConnect';
import withAlertActions from 'containers/Alert/withAlertActions';
@@ -20,14 +22,11 @@ function ItemActivateAlert({
isOpen,
payload: { itemId },
// #withItemsActions
requestActivateItem,
// #withAlertActions
closeAlert,
}) {
const { formatMessage } = useIntl();
const [isLoading, setLoading] = useState(false);
const { mutateAsync: activateItem, isLoading } = useActivateItem();
// Handle activate item alert cancel.
const handleCancelActivateItem = () => {
@@ -36,8 +35,7 @@ function ItemActivateAlert({
// Handle confirm item activated.
const handleConfirmItemActivate = () => {
setLoading(true);
requestActivateItem(itemId)
activateItem(itemId)
.then(() => {
AppToaster.show({
message: formatMessage({
@@ -45,12 +43,10 @@ function ItemActivateAlert({
}),
intent: Intent.SUCCESS,
});
queryCache.invalidateQueries('items-table');
})
.catch((error) => {})
.finally(() => {
closeAlert(name);
setLoading(false);
});
};
@@ -61,8 +57,8 @@ function ItemActivateAlert({
intent={Intent.WARNING}
isOpen={isOpen}
onCancel={handleCancelActivateItem}
onConfirm={handleConfirmItemActivate}
loading={isLoading}
onConfirm={handleConfirmItemActivate}
>
<p>
<T id={'are_sure_to_activate_this_item'} />
@@ -74,5 +70,4 @@ function ItemActivateAlert({
export default compose(
withAlertStoreConnect(),
withAlertActions,
withItemsActions,
)(ItemActivateAlert);

View File

@@ -1,6 +1,7 @@
import React, { useState } from 'react';
import { FormattedMessage as T, useIntl } from 'react-intl';
import { Intent, Alert } from '@blueprintjs/core';
import { size } from 'lodash';
import { AppToaster } from 'components';
import withItemsActions from 'containers/Items/withItemsActions';
@@ -54,7 +55,7 @@ function ItemBulkDeleteAlert({
<Alert
cancelButtonText={<T id={'cancel'} />}
confirmButtonText={
<T id={'delete_count'} values={{ count: itemsIds.length }} />
<T id={'delete_count'} values={{ count: size(itemsIds) }} />
}
icon="trash"
intent={Intent.DANGER}

View File

@@ -5,6 +5,7 @@ import {
useIntl,
} from 'react-intl';
import { Intent, Alert } from '@blueprintjs/core';
import { size } from 'lodash';
import { AppToaster } from 'components';
import withItemCategoriesActions from 'containers/Items/withItemCategoriesActions';
@@ -59,7 +60,7 @@ function ItemCategoryBulkDeleteAlert({
<Alert
cancelButtonText={<T id={'cancel'} />}
confirmButtonText={
<T id={'delete_count'} values={{ count: itemCategoriesIds.length }} />
<T id={'delete_count'} values={{ count: size(itemCategoriesIds) }} />
}
icon="trash"
intent={Intent.DANGER}

View File

@@ -1,14 +1,14 @@
import React, { useState } from 'react';
import React from 'react';
import {
FormattedMessage as T,
FormattedHTMLMessage,
useIntl,
} from 'react-intl';
import { Intent, Alert } from '@blueprintjs/core';
import { AppToaster } from 'components';
import { queryCache } from 'react-query';
import withItemCategoriesActions from 'containers/Items/withItemCategoriesActions';
import { useDeleteItemCategory } from 'hooks/query';
import { AppToaster } from 'components';
import withAlertStoreConnect from 'containers/Alert/withAlertStoreConnect';
import withAlertActions from 'containers/Alert/withAlertActions';
@@ -24,14 +24,14 @@ function ItemCategoryDeleteAlert({
isOpen,
payload: { itemCategoryId },
// #withItemCategoriesActions
requestDeleteItemCategory,
// #withAlertActions
closeAlert,
}) {
const { formatMessage } = useIntl();
const [isLoading, setLoading] = useState(false);
const {
mutateAsync: deleteItemCategory,
isLoading,
} = useDeleteItemCategory();
// handle cancel delete item category alert.
const handleCancelItemCategoryDelete = () => {
@@ -40,8 +40,7 @@ function ItemCategoryDeleteAlert({
// Handle alert confirm delete item category.
const handleConfirmItemDelete = () => {
setLoading(true);
requestDeleteItemCategory(itemCategoryId)
deleteItemCategory(itemCategoryId)
.then(() => {
AppToaster.show({
message: formatMessage({
@@ -49,11 +48,9 @@ function ItemCategoryDeleteAlert({
}),
intent: Intent.SUCCESS,
});
queryCache.invalidateQueries('items-categories-list');
})
.catch(() => {})
.finally(() => {
setLoading(false);
closeAlert(name);
});
};
@@ -81,5 +78,4 @@ function ItemCategoryDeleteAlert({
export default compose(
withAlertStoreConnect(),
withAlertActions,
withItemCategoriesActions,
)(ItemCategoryDeleteAlert);

View File

@@ -10,7 +10,9 @@ import { AppToaster } from 'components';
import { handleDeleteErrors } from 'containers/Items/utils';
import withItemsActions from 'containers/Items/withItemsActions';
import {
useDeleteItem
} from 'hooks/query';
import withAlertStoreConnect from 'containers/Alert/withAlertStoreConnect';
import withAlertActions from 'containers/Alert/withAlertActions';
@@ -26,14 +28,11 @@ function ItemDeleteAlert({
isOpen,
payload: { itemId },
// #withItemsActions
requestDeleteItem,
// #withAlertActions
closeAlert,
}) {
const { mutateAsync: deleteItem, isLoading } = useDeleteItem();
const { formatMessage } = useIntl();
const [isLoading, setLoading] = useState(false);
// handle cancel delete item alert.
const handleCancelItemDelete = () => {
@@ -41,8 +40,7 @@ function ItemDeleteAlert({
};
const handleConfirmDeleteItem = () => {
setLoading(true);
requestDeleteItem(itemId)
deleteItem(itemId)
.then(() => {
AppToaster.show({
message: formatMessage({
@@ -50,14 +48,12 @@ function ItemDeleteAlert({
}),
intent: Intent.SUCCESS,
});
queryCache.invalidateQueries('items-table');
})
.catch(({ errors }) => {
handleDeleteErrors(errors);
})
.finally(() => {
closeAlert(name);
setLoading(false);
});
};
@@ -84,5 +80,4 @@ function ItemDeleteAlert({
export default compose(
withAlertStoreConnect(),
withAlertActions,
withItemsActions,
)(ItemDeleteAlert);

View File

@@ -1,10 +1,10 @@
import React, { useState } from 'react';
import React from 'react';
import { FormattedMessage as T, useIntl } from 'react-intl';
import { Intent, Alert } from '@blueprintjs/core';
import { queryCache } from 'react-query';
import { AppToaster } from 'components';
import withItemsActions from 'containers/Items/withItemsActions';
import { useInactivateItem } from 'hooks/query';
import withAlertStoreConnect from 'containers/Alert/withAlertStoreConnect';
import withAlertActions from 'containers/Alert/withAlertActions';
@@ -20,24 +20,20 @@ function ItemInactivateAlert({
isOpen,
payload: { itemId },
// #withItemsActions
requestInactiveItem,
// #withAlertActions
closeAlert,
}) {
const { formatMessage } = useIntl();
const [isLoading, setLoading] = useState(false);
const { mutateAsync: inactivateItem, isLoading } = useInactivateItem();
// handle cancel inactivate alert.
// Handle cancel inactivate alert.
const handleCancelInactivateItem = () => {
closeAlert(name);
};
// Handle confirm item Inactive.
const handleConfirmItemInactive = () => {
setLoading(true);
requestInactiveItem(itemId)
inactivateItem(itemId)
.then(() => {
AppToaster.show({
message: formatMessage({
@@ -45,11 +41,9 @@ function ItemInactivateAlert({
}),
intent: Intent.SUCCESS,
});
queryCache.invalidateQueries('items-table');
})
.catch((error) => {})
.finally(() => {
setLoading(false);
closeAlert(name);
});
};
@@ -74,5 +68,4 @@ function ItemInactivateAlert({
export default compose(
withAlertStoreConnect(),
withAlertActions,
withItemsActions,
)(ItemInactivateAlert);

View File

@@ -0,0 +1,46 @@
function JournalBulkDeleteAlert({}) {
// Handle confirm journals bulk delete.
const handleConfirmBulkDelete = useCallback(() => {
requestDeleteBulkManualJournals(bulkDelete)
.then(() => {
setBulkDelete(false);
AppToaster.show({
message: formatMessage(
{ id: 'the_journals_has_been_deleted_successfully' },
{ count: selectedRowsCount },
),
intent: Intent.SUCCESS,
});
})
.catch((error) => {
setBulkDelete(false);
});
}, [
requestDeleteBulkManualJournals,
bulkDelete,
formatMessage,
selectedRowsCount,
]);
return (
<Alert
cancelButtonText={<T id={'cancel'} />}
confirmButtonText={
<T id={'delete_count'} values={{ count: selectedRowsCount }} />
}
icon="trash"
intent={Intent.DANGER}
isOpen={bulkDelete}
onCancel={handleCancelBulkDelete}
onConfirm={handleConfirmBulkDelete}
>
<p>
<T id={'once_delete_these_journals_you_will_not_able_restore_them'} />
</p>
</Alert>
);
}

View File

@@ -0,0 +1,68 @@
import React from 'react';
import { Intent, Alert } from '@blueprintjs/core';
import { FormattedMessage as T, useIntl } from 'react-intl';
import { useDeleteJournal } from 'hooks/query';
import { AppToaster } from 'components';
import withAlertActions from 'containers/Alert/withAlertActions';
import withAlertStoreConnect from 'containers/Alert/withAlertStoreConnect';
import { compose } from 'utils';
/**
* Journal delete alert.
*/
function JournalDeleteAlert({
name,
// #withAlertStoreConnect
isOpen,
payload: { manualJournalId, journalNumber },
// #withAlertActions
closeAlert,
}) {
const { formatMessage } = useIntl();
const { mutate: deleteJournalMutate } = useDeleteJournal();
// Handle cancel delete manual journal.
const handleCancelAlert = () => {
closeAlert(name);
};
// Handle confirm delete manual journal.
const handleConfirmManualJournalDelete = () => {
deleteJournalMutate(manualJournalId).then(() => {
AppToaster.show({
message: formatMessage(
{ id: 'the_journal_has_been_deleted_successfully' },
{ number: journalNumber },
),
intent: Intent.SUCCESS,
});
});
};
return (
<Alert
cancelButtonText={<T id={'cancel'} />}
confirmButtonText={<T id={'delete'} />}
icon="trash"
intent={Intent.DANGER}
isOpen={isOpen}
onCancel={handleCancelAlert}
onConfirm={handleConfirmManualJournalDelete}
>
<p>
<T id={'once_delete_this_journal_you_will_able_to_restore_it'} />
</p>
</Alert>
);
}
export default compose(
withAlertStoreConnect(),
withAlertActions,
)(JournalDeleteAlert);

View File

@@ -0,0 +1,73 @@
import React from 'react';
import { Intent, Alert } from '@blueprintjs/core';
import { FormattedMessage as T, useIntl } from 'react-intl';
import { usePublishJournal } from 'hooks/query';
import { AppToaster } from 'components';
import withAlertActions from 'containers/Alert/withAlertActions';
import withAlertStoreConnect from 'containers/Alert/withAlertStoreConnect';
import { compose } from 'utils';
/**
* Journal publish alert.
*/
function JournalPublishAlert({
name,
// #withAlertStoreConnect
isOpen,
payload: { manualJournalId, journalNumber },
// #withAlertActions
closeAlert,
}) {
const { formatMessage } = useIntl();
const { mutate: publishJournalMutate, isLoading } = usePublishJournal();
// Handle cancel manual journal alert.
const handleCancel = () => {
closeAlert(name);
};
// Handle publish manual journal confirm.
const handleConfirm = () => {
publishJournalMutate(manualJournalId)
.then(() => {
AppToaster.show({
message: formatMessage({
id: 'the_manual_journal_has_been_published',
}),
intent: Intent.SUCCESS,
});
})
.catch((error) => {
})
.finally(() => {
closeAlert(name);
});
};
return (
<Alert
cancelButtonText={<T id={'cancel'} />}
confirmButtonText={<T id={'publish'} />}
intent={Intent.WARNING}
isOpen={isOpen}
onCancel={handleCancel}
onConfirm={handleConfirm}
loading={isLoading}
>
<p>
<T id={'are_sure_to_publish_this_manual_journal'} />
</p>
</Alert>
);
}
export default compose(
withAlertStoreConnect(),
withAlertActions,
)(JournalPublishAlert)

View File

@@ -0,0 +1,72 @@
import React from 'react';
import { FormattedMessage as T, useIntl } from 'react-intl';
import { Intent, Alert } from '@blueprintjs/core';
import { AppToaster } from 'components';
import withAlertStoreConnect from 'containers/Alert/withAlertStoreConnect';
import withAlertActions from 'containers/Alert/withAlertActions';
import { useDeletePaymentMade } from 'hooks/query';
import { compose } from 'utils';
/**
* Payment made delete alert.
*/
function PaymentMadeDeleteAlert({
name,
// #withAlertStoreConnect
isOpen,
payload: { paymentMadeId },
// #withAlertActions
closeAlert,
}) {
const { formatMessage } = useIntl();
const {
mutateAsync: deletePaymentMadeMutate,
isLoading,
} = useDeletePaymentMade();
// Handle cancel payment made.
const handleCancelPaymentMadeDelete = () => {};
// Handle confirm delete payment made
const handleConfirmPaymentMadeDelete = () => {
deletePaymentMadeMutate(paymentMadeId)
.then(() => {
AppToaster.show({
message: formatMessage({
id: 'the_payment_made_has_been_deleted_successfully',
}),
intent: Intent.SUCCESS,
});
})
.finally(() => {
closeAlert(name);
});
};
return (
<Alert
cancelButtonText={<T id={'cancel'} />}
confirmButtonText={<T id={'delete'} />}
icon={'trash'}
intent={Intent.DANGER}
isOpen={isOpen}
onCancel={handleCancelPaymentMadeDelete}
onConfirm={handleConfirmPaymentMadeDelete}
loading={isLoading}
>
<p>
<T id={'once_delete_this_payment_made_you_will_able_to_restore_it'} />
</p>
</Alert>
);
}
export default compose(
withAlertStoreConnect(),
withAlertActions,
)(PaymentMadeDeleteAlert);

View File

@@ -1,16 +1,16 @@
import React, { useCallback, useState } from 'react';
import React from 'react';
import {
FormattedMessage as T,
FormattedHTMLMessage,
useIntl,
} from 'react-intl';
import { Intent, Alert } from '@blueprintjs/core';
import { queryCache } from 'react-query';
import { useDeletePaymentReceive } from 'hooks/query';
import { AppToaster } from 'components';
import withAlertStoreConnect from 'containers/Alert/withAlertStoreConnect';
import withAlertActions from 'containers/Alert/withAlertActions';
import withPaymentReceivesActions from 'containers/Sales/PaymentReceive/withPaymentReceivesActions';
import { compose } from 'utils';
@@ -24,14 +24,14 @@ function PaymentReceiveDeleteAlert({
isOpen,
payload: { paymentReceiveId },
// #withPaymentReceivesActions
requestDeletePaymentReceive,
// #withAlertActions
closeAlert,
}) {
const { formatMessage } = useIntl();
const [isLoading, setLoading] = useState(false);
const {
mutateAsync: deletePaymentReceiveMutate,
isLoading,
} = useDeletePaymentReceive();
// Handle cancel payment Receive.
const handleCancelDeleteAlert = () => {
@@ -39,9 +39,8 @@ function PaymentReceiveDeleteAlert({
};
// Handle confirm delete payment receive.
const handleConfirmPaymentReceiveDelete = useCallback(() => {
setLoading(true);
requestDeletePaymentReceive(paymentReceiveId)
const handleConfirmPaymentReceiveDelete = () => {
deletePaymentReceiveMutate(paymentReceiveId)
.then(() => {
AppToaster.show({
message: formatMessage({
@@ -49,14 +48,12 @@ function PaymentReceiveDeleteAlert({
}),
intent: Intent.SUCCESS,
});
queryCache.invalidateQueries('paymentReceives-table');
})
.catch(() => {})
.finally(() => {
closeAlert(name);
setLoading(false);
});
}, [paymentReceiveId, requestDeletePaymentReceive, formatMessage]);
};
return (
<Alert
@@ -81,5 +78,4 @@ function PaymentReceiveDeleteAlert({
export default compose(
withAlertStoreConnect(),
withAlertActions,
withPaymentReceivesActions,
)(PaymentReceiveDeleteAlert);

View File

@@ -1,12 +1,12 @@
import React, { useCallback, useState } from 'react';
import React from 'react';
import { FormattedMessage as T, useIntl } from 'react-intl';
import { Intent, Alert } from '@blueprintjs/core';
import { queryCache } from 'react-query';
import { useCloseReceipt } from 'hooks/query';
import { AppToaster } from 'components';
import withAlertStoreConnect from 'containers/Alert/withAlertStoreConnect';
import withAlertActions from 'containers/Alert/withAlertActions';
import withReceiptActions from 'containers/Sales/Receipt/withReceiptActions';
import { compose } from 'utils';
@@ -20,24 +20,20 @@ function ReceiptCloseAlert({
isOpen,
payload: { receiptId },
// #withReceiptActions
requestCloseReceipt,
// #withAlertActions
closeAlert,
}) {
const { formatMessage } = useIntl();
const [isLoading, setLoading] = useState(false);
const { mutateAsync: closeReceiptMutate, isLoading } = useCloseReceipt();
// handle cancel delete alert.
// handle cancel delete alert.
const handleCancelDeleteAlert = () => {
closeAlert(name);
};
// Handle confirm receipt close.
const handleConfirmReceiptClose = useCallback(() => {
setLoading(true);
requestCloseReceipt(receiptId)
const handleConfirmReceiptClose = () => {
closeReceiptMutate(receiptId)
.then(() => {
AppToaster.show({
message: formatMessage({
@@ -45,14 +41,12 @@ function ReceiptCloseAlert({
}),
intent: Intent.SUCCESS,
});
queryCache.invalidateQueries('receipts-table');
})
.catch((error) => {})
.finally(() => {
closeAlert(name);
setLoading(false);
});
}, [receiptId, requestCloseReceipt, formatMessage]);
};
return (
<Alert
@@ -74,5 +68,4 @@ function ReceiptCloseAlert({
export default compose(
withAlertStoreConnect(),
withAlertActions,
withReceiptActions,
)(ReceiptCloseAlert);

View File

@@ -1,4 +1,4 @@
import React, { useCallback, useState } from 'react';
import React from 'react';
import {
FormattedMessage as T,
FormattedHTMLMessage,
@@ -6,11 +6,12 @@ import {
} from 'react-intl';
import { Intent, Alert } from '@blueprintjs/core';
import { queryCache } from 'react-query';
import { useDeleteReceipt } from 'hooks/query';
import { AppToaster } from 'components';
import withAlertStoreConnect from 'containers/Alert/withAlertStoreConnect';
import withAlertActions from 'containers/Alert/withAlertActions';
import withReceiptActions from 'containers/Sales/Receipt/withReceiptActions';
import { compose } from 'utils';
@@ -24,24 +25,23 @@ function NameDeleteAlert({
isOpen,
payload: { receiptId },
// #withReceiptActions
requestDeleteReceipt,
// #withAlertActions
closeAlert,
}) {
const { formatMessage } = useIntl();
const [isLoading, setLoading] = useState(false);
const {
mutateAsync: deleteReceiptMutate,
isLoading
} = useDeleteReceipt();
// handle cancel delete alert.
// Handle cancel delete alert.
const handleCancelDeleteAlert = () => {
closeAlert(name);
};
// handle confirm delete receipt
const handleConfirmReceiptDelete = useCallback(() => {
setLoading(true);
requestDeleteReceipt(receiptId)
// Handle confirm delete receipt
const handleConfirmReceiptDelete = () => {
deleteReceiptMutate(receiptId)
.then(() => {
AppToaster.show({
message: formatMessage({
@@ -49,14 +49,12 @@ function NameDeleteAlert({
}),
intent: Intent.SUCCESS,
});
queryCache.invalidateQueries('receipts-table');
})
.catch(() => {})
.finally(() => {
setLoading(false);
closeAlert(name);
});
}, [receiptId, requestDeleteReceipt, formatMessage]);
};
return (
<Alert
@@ -81,5 +79,4 @@ function NameDeleteAlert({
export default compose(
withAlertStoreConnect(),
withAlertActions,
withReceiptActions,
)(NameDeleteAlert);

View File

@@ -8,11 +8,11 @@ import { Intent, Alert } from '@blueprintjs/core';
import { AppToaster } from 'components';
import { transformErrors } from 'containers/Customers/utils';
import withAlertStoreConnect from 'containers/Alert/withAlertStoreConnect';
import withAlertActions from 'containers/Alert/withAlertActions';
import withVendorActions from 'containers/Vendors/withVendorActions';
import { compose } from 'utils';
import {
useDeleteVendor
} from 'hooks/query';
/**
* Vendor delete alert.
@@ -24,24 +24,23 @@ function VendorDeleteAlert({
isOpen,
payload: { vendorId },
// #withVendorActions
requestDeleteVender,
// #withAlertActions
closeAlert,
}) {
const { formatMessage } = useIntl();
const [isLoading, setLoading] = useState(false);
const {
mutateAsync: deleteVendorMutate,
isLoading
} = useDeleteVendor();
// Handle cancel delete the vendor.
const handleCancelDeleteAlert = () => {
closeAlert(name);
};
// handle confirm delete vendor.
// Handle confirm delete vendor.
const handleConfirmDeleteVendor = useCallback(() => {
setLoading(true);
requestDeleteVender(vendorId)
deleteVendorMutate(vendorId)
.then(() => {
AppToaster.show({
message: formatMessage({
@@ -55,9 +54,8 @@ function VendorDeleteAlert({
})
.finally(() => {
closeAlert(name);
setLoading(false);
});
}, [requestDeleteVender, vendorId, formatMessage]);
}, [deleteVendorMutate, name, closeAlert, vendorId, formatMessage]);
return (
<Alert
@@ -80,7 +78,5 @@ function VendorDeleteAlert({
}
export default compose(
withAlertStoreConnect(),
withAlertActions,
withVendorActions,
)(VendorDeleteAlert);

View File

@@ -1,4 +1,4 @@
import React, { useCallback, useMemo, useState } from 'react';
import React, { useCallback } from 'react';
import {
NavbarGroup,
NavbarDivider,
@@ -11,36 +11,35 @@ import {
} from '@blueprintjs/core';
import { FormattedMessage as T, useIntl } from 'react-intl';
import classNames from 'classnames';
import { connect } from 'react-redux';
import { useHistory } from 'react-router-dom';
import DashboardActionsBar from 'components/Dashboard/DashboardActionsBar';
import { If, Icon, DashboardActionViewsList } from 'components';
import withResourceDetail from 'containers/Resources/withResourceDetails';
import { useCustomersListContext } from './CustomersListProvider';
import withCustomers from 'containers/Customers/withCustomers';
import withCustomersActions from 'containers/Customers/withCustomersActions';
import withAlertActions from 'containers/Alert/withAlertActions';
import { compose } from 'utils';
const CustomerActionsBar = ({
/**
* Customers actions bar.
*/
function CustomerActionsBar({
// #withCustomers
customersViews,
customersSelectedRows,
//#withCustomersActions
addCustomersTableQueries,
changeCustomerView,
// #withAlertActions
openAlert,
// #ownProps
onFilterChanged,
}) => {
}) {
const history = useHistory();
const { formatMessage } = useIntl();
const { customersViews } = useCustomersListContext();
const onClickNewCustomer = useCallback(() => {
history.push('/customers/new');
@@ -52,7 +51,6 @@ const CustomerActionsBar = ({
};
const handleTabChange = (viewId) => {
changeCustomerView(viewId.id || -1);
addCustomersTableQueries({
custom_view_id: viewId.id || null,
});
@@ -111,19 +109,9 @@ const CustomerActionsBar = ({
);
};
const mapStateToProps = (state, props) => ({
resourceName: 'customers',
});
const withCustomersActionsBar = connect(mapStateToProps);
export default compose(
withCustomersActionsBar,
withCustomersActions,
withResourceDetail(({ resourceFields }) => ({
resourceFields,
})),
withCustomers(({ customersViews, customersSelectedRows }) => ({
customersViews,
withCustomers(({ customersSelectedRows }) => ({
customersSelectedRows,
})),
withAlertActions,

View File

@@ -1,4 +1,4 @@
import React, { useCallback, useState } from 'react';
import React from 'react';
import classNames from 'classnames';
import { FormGroup, Position, Classes, ControlGroup } from '@blueprintjs/core';
import { DateInput } from '@blueprintjs/datetime';
@@ -13,21 +13,23 @@ import {
} from 'components';
import { FormattedMessage as T } from 'react-intl';
import withCurrencies from 'containers/Currencies/withCurrencies';
import { useCustomerFormContext } from './CustomerFormProvider';
import {
compose,
momentFormatter,
tansformDateValue,
inputIntent,
} from 'utils';
function CustomerFinancialPanel({
// #withCurrencies
currenciesList,
/**
* Customer financial panel.
*/
export default function CustomerFinancialPanel() {
const {
currencies,
customerId
} = useCustomerFormContext();
customerId,
}) {
return (
<div className={'tab-panel--financial'}>
<Row>
@@ -103,7 +105,7 @@ function CustomerFinancialPanel({
inline={true}
>
<CurrencySelectList
currenciesList={currenciesList}
currenciesList={currencies}
selectedCurrencyCode={value}
onCurrencySelected={(currency) => {
form.setFieldValue('currency_code', currency.currency_code);
@@ -118,7 +120,3 @@ function CustomerFinancialPanel({
</div>
);
}
export default compose(
withCurrencies(({ currenciesList }) => ({ currenciesList })),
)(CustomerFinancialPanel);

View File

@@ -13,40 +13,38 @@ import { FormattedMessage as T } from 'react-intl';
import classNames from 'classnames';
import { CLASSES } from 'common/classes';
import { useFormikContext } from 'formik';
import { saveInvoke } from 'utils';
import { Icon } from 'components';
import { useCustomerFormContext } from './CustomerFormProvider';
/**
* Customer floating actions bar.
*/
export default function CustomerFloatingActions({
onSubmitClick,
onCancelClick,
isSubmitting,
customerId,
}) {
const { resetForm, submitForm } = useFormikContext();
export default function CustomerFloatingActions() {
// Customer form context.
const { customerId, setSubmitPayload } = useCustomerFormContext();
// Formik context.
const { resetForm, submitForm, isSubmitting } = useFormikContext();
// Handle submit button click.
const handleSubmitBtnClick = (event) => {
saveInvoke(onSubmitClick, event, {
noRedirect: false,
});
setSubmitPayload({ noRedirect: false });
};
// Handle cancel button click.
const handleCancelBtnClick = (event) => {
saveInvoke(onCancelClick, event);
};
// handle clear button clicl.
const handleClearBtnClick = (event) => {
// saveInvoke(onClearClick, event);
resetForm();
};
// Handle submit & new button click.
const handleSubmitAndNewClick = (event) => {
submitForm();
saveInvoke(onSubmitClick, event, {
noRedirect: true,
});
setSubmitPayload({ noRedirect: true });
};
return (
@@ -55,6 +53,7 @@ export default function CustomerFloatingActions({
{/* ----------- Save and New ----------- */}
<Button
disabled={isSubmitting}
loading={isSubmitting}
intent={Intent.PRIMARY}
type="submit"
onClick={handleSubmitBtnClick}

View File

@@ -1,9 +1,9 @@
import React, { useState, useMemo, useCallback, useEffect } from 'react';
import React, { useMemo, useEffect } from 'react';
import * as Yup from 'yup';
import { Formik, Form } from 'formik';
import moment from 'moment';
import { Intent } from '@blueprintjs/core';
import { FormattedMessage as T, useIntl } from 'react-intl';
import { useIntl } from 'react-intl';
import classNames from 'classnames';
import { useHistory } from 'react-router-dom';
@@ -16,14 +16,10 @@ import CustomersTabs from 'containers/Customers/CustomersTabs';
import CustomerFloatingActions from './CustomerFloatingActions';
import withDashboardActions from 'containers/Dashboard/withDashboardActions';
import withCustomerDetail from 'containers/Customers/withCustomerDetail';
import withCustomersActions from 'containers/Customers/withCustomersActions';
import withMediaActions from 'containers/Media/withMediaActions';
import withCustomers from 'containers/Customers//withCustomers';
import withSettings from 'containers/Settings/withSettings';
import useMedia from 'hooks/useMedia';
import { compose, transformToForm } from 'utils';
import { useCustomerFormContext } from './CustomerFormProvider';
const defaultInitialValues = {
customer_type: 'business',
@@ -68,43 +64,20 @@ function CustomerForm({
// #withDashboardActions
changePageTitle,
// #withCustomers
customers,
// #withCustomerDetail
customer,
// #withSettings
baseCurrency,
// #withCustomersActions
requestSubmitCustomer,
requestEditCustomer,
// #withMediaActions
requestSubmitMedia,
requestDeleteMedia,
// #Props
customerId,
onFormSubmit,
onCancelForm,
}) {
const isNewMode = !customerId;
const [submitPayload, setSubmitPayload] = useState({});
const {
customer,
customerId,
submitPayload,
editCustomerMutate,
createCustomerMutate,
} = useCustomerFormContext();
const isNewMode = !customerId;
const history = useHistory();
const { formatMessage } = useIntl();
const {
setFiles,
saveMedia,
deletedFiles,
setDeletedFiles,
deleteMedia,
} = useMedia({
saveCallback: requestSubmitMedia,
deleteCallback: requestDeleteMedia,
});
const validationSchema = Yup.object().shape({
customer_type: Yup.string()
@@ -158,7 +131,7 @@ function CustomerForm({
currency_code: baseCurrency,
...transformToForm(customer, defaultInitialValues),
}),
[customer, defaultInitialValues],
[customer, baseCurrency],
);
useEffect(() => {
@@ -196,53 +169,14 @@ function CustomerForm({
};
if (customer && customer.id) {
requestEditCustomer(customer.id, formValues)
editCustomerMutate(customer.id, formValues)
.then(onSuccess)
.catch(onError);
} else {
requestSubmitCustomer(formValues).then(onSuccess).catch(onError);
createCustomerMutate(formValues).then(onSuccess).catch(onError);
}
};
const initialAttachmentFiles = useMemo(() => {
return customer && customer.media
? customer.media.map((attach) => ({
preview: attach.attachment_file,
upload: true,
metadata: { ...attach },
}))
: [];
}, []);
const handleDropFiles = useCallback((_files) => {
setFiles(_files.filter((file) => file.uploaded === false));
}, []);
const handleDeleteFile = useCallback(
(_deletedFiles) => {
_deletedFiles.forEach((deletedFile) => {
if (deletedFile.uploaded && deletedFile.metadata.id) {
setDeletedFiles([...deletedFiles, deletedFile.metadata.id]);
}
});
},
[setDeletedFiles, deletedFiles],
);
const handleCancelClick = useCallback(
(event) => {
history.goBack();
},
[history],
);
const handleSubmitClick = useCallback(
(event, payload) => {
setSubmitPayload({ ...payload });
},
[setSubmitPayload],
);
return (
<div className={classNames(CLASSES.PAGE_FORM, CLASSES.PAGE_FORM_CUSTOMER)}>
<Formik
@@ -250,42 +184,29 @@ function CustomerForm({
initialValues={initialValues}
onSubmit={handleFormSubmit}
>
{({ isSubmitting }) => (
<Form>
<div className={classNames(CLASSES.PAGE_FORM_HEADER_PRIMARY)}>
<CustomerFormPrimarySection />
</div>
<Form>
<div className={classNames(CLASSES.PAGE_FORM_HEADER_PRIMARY)}>
<CustomerFormPrimarySection />
</div>
<div className={'page-form__after-priamry-section'}>
<CustomerFormAfterPrimarySection />
</div>
<div className={'page-form__after-priamry-section'}>
<CustomerFormAfterPrimarySection />
</div>
<div className={classNames(CLASSES.PAGE_FORM_TABS)}>
<CustomersTabs customer={customerId} />
</div>
<div className={classNames(CLASSES.PAGE_FORM_TABS)}>
<CustomersTabs />
</div>
<CustomerFloatingActions
isSubmitting={isSubmitting}
customerId={customer}
onSubmitClick={handleSubmitClick}
onCancelClick={handleCancelClick}
/>
</Form>
)}
<CustomerFloatingActions />
</Form>
</Formik>
</div>
);
}
export default compose(
withCustomerDetail,
withCustomers(({ customers }) => ({
customers,
})),
withSettings(({ organizationSettings }) => ({
baseCurrency: organizationSettings?.baseCurrency,
})),
withDashboardActions,
withCustomersActions,
withMediaActions,
)(CustomerForm);

View File

@@ -1,10 +1,9 @@
import React, { useCallback } from 'react';
import { useParams, useHistory } from 'react-router-dom';
import { useQuery } from 'react-query';
import React from 'react';
import { useParams } from 'react-router-dom';
import { DashboardCard } from 'components';
import CustomerForm from 'containers/Customers/CustomerForm';
import DashboardInsider from 'components/Dashboard/DashboardInsider';
import CustomerForm from './CustomerForm';
import { CustomerFormProvider } from './CustomerFormProvider';
import withCustomersActions from './withCustomersActions';
import withCurrenciesActions from 'containers/Currencies/withCurrenciesActions';
@@ -13,60 +12,19 @@ import { compose } from 'utils';
import 'style/pages/Customers/PageForm.scss';
function CustomerFormPage({
// // #withDashboardActions
// changePageTitle,
// formik,
//#withCustomersActions
requestFetchCustomers,
requestFetchCustomer,
// #wihtCurrenciesActions
requestFetchCurrencies,
}) {
function CustomerFormPage() {
const { id } = useParams();
const history = useHistory();
// Handle fetch customers data table
const fetchCustomers = useQuery('customers-table', () =>
requestFetchCustomers({}),
);
// Handle fetch customer details.
const fetchCustomer = useQuery(
['customer', id],
(key, customerId) => requestFetchCustomer(customerId),
{ enabled: id && id },
);
// Handle fetch Currencies data table
const fetchCurrencies = useQuery('currencies', () =>
requestFetchCurrencies(),
);
const handleFormSubmit = useCallback((payload) => {}, [history]);
const handleCancel = useCallback(() => {
history.goBack();
}, [history]);
return (
<DashboardInsider
loading={
fetchCustomer.isFetching ||
fetchCustomers.isFetching ||
fetchCurrencies.isFetching
}
name={'customer-form'}
>
<CustomerFormProvider customerId={id}>
<DashboardCard page>
<CustomerForm
onFormSubmit={handleFormSubmit}
customerId={id}
onCancelForm={handleCancel}
/>
<CustomerForm />
</DashboardCard>
</DashboardInsider>
</CustomerFormProvider>
);
}
export default compose(withCustomersActions, withCurrenciesActions)(CustomerFormPage);
export default compose(
withCustomersActions,
withCurrenciesActions,
)(CustomerFormPage);

View File

@@ -0,0 +1,65 @@
import React, { useState, createContext } from 'react';
import DashboardInsider from 'components/Dashboard/DashboardInsider';
import {
useCustomers,
useCustomer,
useCurrencies,
useCreateCustomer,
useEditCustomer,
} from 'hooks/query';
const CustomerFormContext = createContext();
function CustomerFormProvider({ customerId, ...props }) {
// Handle fetch customer details.
const { data: customer, isFetching: isCustomerLoading } = useCustomer(
customerId,
{
enabled: !!customerId,
}
);
// Handle fetch customers data table
const {
data: { customers },
isFetching: isCustomersLoading,
} = useCustomers();
// Handle fetch Currencies data table
const { data: currencies, isFetching: isCurrenciesLoading } = useCurrencies();
// Form submit payload.
const [submitPayload, setSubmitPayload] = useState({});
const { mutateAsync: editCustomerMutate } = useEditCustomer();
const { mutateAsync: createCustomerMutate } = useCreateCustomer();
const provider = {
customerId,
customer,
customers,
currencies,
submitPayload,
isCustomerLoading,
isCustomersLoading,
isCurrenciesLoading,
setSubmitPayload,
editCustomerMutate,
createCustomerMutate
};
return (
<DashboardInsider
loading={isCustomerLoading || isCustomerLoading || isCurrenciesLoading}
name={'customer-form'}
>
<CustomerFormContext.Provider value={provider} {...props} />
</DashboardInsider>
);
}
const useCustomerFormContext = () => React.useContext(CustomerFormContext);
export { CustomerFormProvider, useCustomerFormContext };

View File

@@ -8,12 +8,11 @@ import {
Position,
Intent,
} from '@blueprintjs/core';
import { FormattedMessage as T, useIntl } from 'react-intl';
import { useIsValuePassed } from 'hooks';
import { useIntl } from 'react-intl';
import classNames from 'classnames';
import CustomersEmptyStatus from './CustomersEmptyStatus';
import { DataTable, Icon, Money, Choose, LoadingIndicator } from 'components';
import { DataTable, Icon, Money, Choose } from 'components';
import { CLASSES } from 'common/classes';
import withCustomers from './withCustomers';
@@ -25,7 +24,19 @@ const AvatarCell = (row) => {
return <span className="avatar">{firstLettersArgs(row.display_name)}</span>;
};
const CustomerTable = ({
const PhoneNumberAccessor = (row) => (
<div>
<div className={'work_phone'}>{row.work_phone}</div>
<div className={'personal_phone'}>{row.personal_phone}</div>
</div>
);
const BalanceAccessor = (row) => {
return (<Money amount={row.closing_balance} currency={row.currency_code} />);
};
function CustomerTable({
//#withCustomers
customers,
customersLoading,
@@ -42,9 +53,8 @@ const CustomerTable = ({
onDeleteCustomer,
onFetchData,
onSelectedRowsChange,
}) => {
}) {
const { formatMessage } = useIntl();
const isLoadedBefore = useIsValuePassed(loading, false);
// Customers actions list.
const renderContextMenu = useMemo(
@@ -125,21 +135,14 @@ const CustomerTable = ({
{
id: 'phone_number',
Header: formatMessage({ id: 'phone_number' }),
accessor: (row) => (
<div>
<div className={'work_phone'}>{row.work_phone}</div>
<div className={'personal_phone'}>{row.personal_phone}</div>
</div>
),
accessor: PhoneNumberAccessor,
className: 'phone_number',
width: 100,
},
{
id: 'receivable_balance',
Header: formatMessage({ id: 'receivable_balance' }),
accessor: (r) => (
<Money amount={r.closing_balance} currency={r.currency_code} />
),
accessor: BalanceAccessor,
className: 'receivable_balance',
width: 100,
},
@@ -193,35 +196,33 @@ const CustomerTable = ({
].every((condition) => condition === true);
return (
<div className={classNames(CLASSES.DASHBOARD_DATATABLE)}>
<LoadingIndicator loading={customersLoading && !isLoadedBefore}>
<Choose>
<Choose.When condition={showEmptyStatus}>
<CustomersEmptyStatus />
</Choose.When>
<Choose>
<Choose.When condition={showEmptyStatus}>
<CustomersEmptyStatus />
</Choose.When>
<Choose.Otherwise>
<DataTable
noInitialFetch={true}
columns={columns}
data={customers}
onFetchData={handleFetchData}
selectionColumn={true}
expandable={false}
sticky={true}
onSelectedRowsChange={handleSelectedRowsChange}
spinnerProps={{ size: 30 }}
rowContextMenu={rowContextMenu}
pagination={true}
manualSortBy={true}
pagesCount={customerPagination.pagesCount}
autoResetSortBy={false}
autoResetPage={false}
initialPageSize={customersTableQuery.page_size}
initialPageIndex={customersTableQuery.page - 1}
/>
</Choose.Otherwise>
</Choose>
</LoadingIndicator>
<Choose.Otherwise>
<DataTable
noInitialFetch={true}
columns={columns}
data={customers}
onFetchData={handleFetchData}
selectionColumn={true}
expandable={false}
sticky={true}
onSelectedRowsChange={handleSelectedRowsChange}
spinnerProps={{ size: 30 }}
rowContextMenu={rowContextMenu}
pagination={true}
manualSortBy={true}
pagesCount={customerPagination.pagesCount}
autoResetSortBy={false}
autoResetPage={false}
initialPageSize={customersTableQuery.page_size}
initialPageIndex={customersTableQuery.page - 1}
/>
</Choose.Otherwise>
</Choose>
</div>
);
};

View File

@@ -1,17 +1,14 @@
import React, { useEffect, useState } from 'react';
import { useQuery } from 'react-query';
import { FormattedMessage as T, useIntl } from 'react-intl';
import React, { useEffect } from 'react';
import { useIntl } from 'react-intl';
import DashboardInsider from 'components/Dashboard/DashboardInsider';
import DashboardPageContent from 'components/Dashboard/DashboardPageContent';
import CustomerActionsBar from 'containers/Customers/CustomerActionsBar';
import CustomersAlerts from 'containers/Customers/CustomersAlerts';
import CustomersViewPage from 'containers/Customers/CustomersViewPage';
import { CustomersListProvider } from './CustomersListProvider';
import withCustomers from 'containers/Customers/withCustomers';
import withCustomersActions from 'containers/Customers/withCustomersActions';
import withResourceActions from 'containers/Resources/withResourcesActions';
import withViewsActions from 'containers/Views/withViewsActions';
import withDashboardActions from 'containers/Dashboard/withDashboardActions';
import { compose } from 'utils';
@@ -25,59 +22,29 @@ function CustomersList({
// #withDashboardActions
changePageTitle,
// #withResourceActions
requestFetchResourceViews,
// #withCustomers
customersTableQuery,
// #withCustomersActions
requestFetchCustomers,
addCustomersTableQueries,
}) {
const [tableLoading, setTableLoading] = useState(false);
const { formatMessage } = useIntl();
useEffect(() => {
changePageTitle(formatMessage({ id: 'customers_list' }));
}, [changePageTitle, formatMessage]);
// Fetch customers resource views and fields.
const fetchResourceViews = useQuery(
['resource-views', 'customers'],
(key, resourceName) => requestFetchResourceViews(resourceName),
);
const fetchCustomers = useQuery(
['customers-table', customersTableQuery],
(key, query) => requestFetchCustomers({ ...query }),
);
useEffect(() => {
if (tableLoading && !fetchCustomers.isFetching) {
setTableLoading(false);
}
}, [tableLoading, fetchCustomers.isFetching]);
return (
<DashboardInsider
loading={fetchResourceViews.isFetching}
name={'customers-list'}
>
<CustomersListProvider query={customersTableQuery}>
<CustomerActionsBar />
<DashboardPageContent>
<CustomersViewPage />
</DashboardPageContent>
<CustomersAlerts />
</DashboardInsider>
</CustomersListProvider>
);
}
export default compose(
withResourceActions,
withCustomersActions,
withDashboardActions,
withViewsActions,
withCustomers(({ customersTableQuery }) => ({ customersTableQuery })),
)(CustomersList);

View File

@@ -0,0 +1,41 @@
import React, { createContext } from 'react';
import DashboardInsider from 'components/Dashboard/DashboardInsider';
import { useResourceViews, useCustomers } from 'hooks/query';
const CustomersListContext = createContext();
function CustomersListProvider({ query, ...props }) {
// Fetch customers resource views and fields.
const {
data: customersViews,
isFetching: isCustomersViewsLoading,
} = useResourceViews('customers');
// Fetches customers data with pagination meta.
const {
data: { customers, pagination },
isFetching: isCustomersLoading,
} = useCustomers(query);
const state = {
customersViews,
customers,
pagination,
isCustomersViewsLoading,
isCustomersLoading,
isEmptyStatus: false,
};
return (
<DashboardInsider loading={isCustomersViewsLoading} name={'customers-list'}>
<CustomersListContext.Provider value={state} {...props} />
</DashboardInsider>
);
}
const useCustomersListContext = () => React.useContext(CustomersListContext);
export { CustomersListProvider, useCustomersListContext };

View File

@@ -6,7 +6,7 @@ import CustomerAttachmentTabs from './CustomerAttachmentTabs';
import CustomerFinancialPanel from './CustomerFinancialPanel';
import CustomerNotePanel from './CustomerNotePanel';
export default function CustomersTabs({ customer }) {
export default function CustomersTabs() {
const { formatMessage } = useIntl();
return (
@@ -20,7 +20,7 @@ export default function CustomersTabs({ customer }) {
<Tab
id={'financial'}
title={formatMessage({ id: 'financial_details' })}
panel={<CustomerFinancialPanel customerId={customer} />}
panel={<CustomerFinancialPanel />}
/>
<Tab
id={'address'}

View File

@@ -46,9 +46,9 @@ function CustomersViewPage({
>
<CustomersViewsTabs />
<CustomersTable
onDeleteCustomer={handleDeleteCustomer}
onEditCustomer={handleEditCustomer}
onSelectedRowsChange={handleSelectedRowsChange}
// onDeleteCustomer={handleDeleteCustomer}
// onEditCustomer={handleEditCustomer}
// onSelectedRowsChange={handleSelectedRowsChange}
/>
</Route>
</Switch>

View File

@@ -1,43 +1,25 @@
import React, { useEffect, useMemo } from 'react';
import React, { useMemo } from 'react';
import { Alignment, Navbar, NavbarGroup } from '@blueprintjs/core';
import { compose } from 'redux';
import { useParams, withRouter } from 'react-router-dom';
import { connect } from 'react-redux';
import { useParams } from 'react-router-dom';
import { DashboardViewsTabs } from 'components';
import withCustomers from 'containers/Customers/withCustomers';
import withCustomersActions from 'containers/Customers/withCustomersActions';
import withDashboardActions from 'containers/Dashboard/withDashboardActions';
import withViewDetail from 'containers/Views/withViewDetails';
import { pick } from 'lodash';
import { useCustomersListContext } from './CustomersListProvider';
/**
* Customers views tabs.
*/
function CustomersViewsTabs({
// #withViewDetail
viewId,
viewItem,
// #withCustomers
customersViews,
// #withCustomersActions
addCustomersTableQueries,
changeCustomerView,
// #withDashboardActions
setTopbarEditView,
changePageSubtitle,
}) {
const { custom_view_id: customViewId = null } = useParams();
const { customersViews } = useCustomersListContext();
useEffect(() => {
changePageSubtitle(customViewId && viewItem ? viewItem.name : '');
setTopbarEditView(customViewId);
}, [customViewId]);
const tabs = useMemo(() =>
customersViews.map(
(view) => ({
@@ -45,14 +27,13 @@ function CustomersViewsTabs({
}),
[customersViews],
),
[customersViews]
);
const handleTabsChange = (viewId) => {
changeCustomerView(viewId || -1);
addCustomersTableQueries({
custom_view_id: viewId || null,
});
setTopbarEditView(viewId);
};
return (
@@ -69,19 +50,7 @@ function CustomersViewsTabs({
);
}
const mapStateToProps = (state, ownProps) => ({
viewId: ownProps.match.params.custom_view_id,
});
const withCustomersViewsTabs = connect(mapStateToProps);
export default compose(
withRouter,
withDashboardActions,
withCustomersViewsTabs,
withCustomersActions,
withViewDetail(),
withCustomers(({ customersViews }) => ({
customersViews,
})),
)(CustomersViewsTabs);

View File

@@ -3,24 +3,29 @@ import { Intent } from '@blueprintjs/core';
import { Formik } from 'formik';
import { useIntl } from 'react-intl';
import { omit } from 'lodash';
import { useQuery, queryCache } from 'react-query';
import { AppToaster, DialogContent } from 'components';
import AccountFormDialogFields from './AccountFormDialogFields';
import withAccountsActions from 'containers/Accounts/withAccountsActions';
import withAccountDetail from 'containers/Accounts/withAccountDetail';
import withDialogActions from 'containers/Dialog/withDialogActions';
import {
EditAccountFormSchema,
CreateAccountFormSchema,
} from './AccountForm.schema';
import {
useAccounts,
useAccountsTypes,
useCreateAccount,
useAccount,
useEditAccount
} from 'hooks/query';
import { compose, transformToForm } from 'utils';
import { transformApiErrors, transformAccountToForm } from './utils';
import 'style/pages/Accounts/AccountFormDialog.scss';
// Default initial form values.
const defaultInitialValues = {
account_type: '',
parent_account_id: '',
@@ -34,16 +39,7 @@ const defaultInitialValues = {
* Account form dialog content.
*/
function AccountFormDialogContent({
// #withAccountDetail
account,
// #withAccountsActions
requestFetchAccounts,
requestFetchAccountTypes,
requestFetchAccount,
requestSubmitAccount,
requestEditAccount,
// #withDialogActions
closeDialog,
// #ownProp
@@ -61,6 +57,27 @@ function AccountFormDialogContent({
? CreateAccountFormSchema
: EditAccountFormSchema;
const { mutateAsync: createAccountMutate } = useCreateAccount();
const { mutateAsync: editAccountMutate } = useEditAccount();
// Fetches accounts list.
const {
data: accounts,
isLoading: isAccountsLoading,
} = useAccounts();
// Fetches accounts types.
const {
data: accountsTypes,
isLoading: isAccountsTypesLoading
} = useAccountsTypes();
// Fetches the specific account details.
const {
data: account,
isLoading: isAccountLoading,
} = useAccount(accountId, { enabled: !!accountId });
// Callbacks handles form submit.
const handleFormSubmit = (values, { setSubmitting, setErrors }) => {
const form = omit(values, ['subaccount']);
@@ -71,11 +88,6 @@ function AccountFormDialogContent({
// Handle request success.
const handleSuccess = () => {
closeDialog(dialogName);
queryCache.invalidateQueries('accounts-table');
setTimeout(() => {
queryCache.invalidateQueries('accounts-list');
}, 1000);
AppToaster.show({
message: formatMessage(
@@ -94,16 +106,16 @@ function AccountFormDialogContent({
};
// Handle request error.
const handleError = (errors) => {
const errorsTransformed = transformApiErrors(errors);
setErrors({ ...errorsTransformed });
setSubmitting(false);
// const errorsTransformed = transformApiErrors(errors);
// setErrors({ ...errorsTransformed });
// setSubmitting(false);
};
if (accountId) {
requestEditAccount(accountId, form)
editAccountMutate(accountId, form)
.then(handleSuccess)
.catch(handleError);
} else {
requestSubmitAccount({ form }).then(handleSuccess).catch(handleError);
createAccountMutate({ ...form }).then(handleSuccess).catch(handleError);
}
};
@@ -130,27 +142,10 @@ function AccountFormDialogContent({
closeDialog(dialogName);
}, [closeDialog, dialogName]);
// Fetches accounts list.
const fetchAccountsList = useQuery('accounts-list', () =>
requestFetchAccounts(),
);
// Fetches accounts types.
const fetchAccountsTypes = useQuery('accounts-types-list', () =>
requestFetchAccountTypes(),
);
// Fetch the given account id on edit mode.
const fetchAccount = useQuery(
['account', accountId],
(key, _id) => requestFetchAccount(_id),
{ enabled: accountId },
);
const isFetching =
fetchAccountsList.isFetching ||
fetchAccountsTypes.isFetching ||
fetchAccount.isFetching;
isAccountsLoading ||
isAccountsTypesLoading ||
isAccountLoading;
return (
<DialogContent isLoading={isFetching}>
@@ -160,6 +155,8 @@ function AccountFormDialogContent({
onSubmit={handleFormSubmit}
>
<AccountFormDialogFields
accounts={accounts}
accountsTypes={accountsTypes}
dialogName={dialogName}
action={action}
onClose={handleClose}
@@ -170,7 +167,5 @@ function AccountFormDialogContent({
}
export default compose(
withAccountsActions,
withAccountDetail,
withDialogActions,
)(AccountFormDialogContent);

View File

@@ -28,11 +28,9 @@ import { useAutofocus } from 'hooks';
* Account form dialogs fields.
*/
function AccountFormDialogFields({
// #ownPropscl
// #ownProps
onClose,
action,
// #withAccounts
accounts,
accountsTypes,
}) {

View File

@@ -4,10 +4,16 @@ import { FormGroup, InputGroup } from '@blueprintjs/core';
import { inputIntent } from 'utils';
import { Row, Col, MoneyInputGroup } from 'components';
import { FormattedMessage as T } from 'react-intl';
import { useAutofocus } from 'hooks';
import { decrementQuantity } from './utils';
import { toSafeNumber } from 'utils';
/**
* Decrement adjustment fields.
*/
function DecrementAdjustmentFields() {
const decrementFieldRef = useAutofocus();
return (
<Row className={'row--decrement-fields'}>
{/*------------ Quantity on hand -----------*/}
@@ -47,6 +53,7 @@ function DecrementAdjustmentFields() {
value={field.value}
allowDecimals={false}
allowNegativeValue={true}
inputRef={(ref) => (decrementFieldRef.current = ref)}
onChange={(value) => {
setFieldValue('quantity', value);
}}

View File

@@ -1,12 +1,15 @@
import React from 'react';
import { Field, FastField, ErrorMessage } from 'formik';
import { FormGroup, InputGroup } from '@blueprintjs/core';
import { useAutofocus } from 'hooks';
import { Row, Col, MoneyInputGroup } from 'components';
import { inputIntent, toSafeNumber } from 'utils';
import { FormattedMessage as T } from 'react-intl';
import { decrementQuantity, incrementQuantity } from './utils';
function IncrementAdjustmentFields() {
export default function IncrementAdjustmentFields() {
const incrementFieldRef = useAutofocus();
return (
<Row>
{/*------------ Quantity on hand -----------*/}
@@ -47,6 +50,7 @@ function IncrementAdjustmentFields() {
value={field.value}
allowDecimals={false}
allowNegativeValue={true}
inputRef={(ref) => (incrementFieldRef.current = ref)}
onChange={(value) => {
setFieldValue('quantity', value);
}}
@@ -131,5 +135,3 @@ function IncrementAdjustmentFields() {
</Row>
);
}
export default IncrementAdjustmentFields;

View File

@@ -2,29 +2,45 @@ import React from 'react';
import { Intent, Button, Classes } from '@blueprintjs/core';
import { useFormikContext } from 'formik';
import { FormattedMessage as T } from 'react-intl';
import { saveInvoke } from 'utils';
export default function InventoryAdjustmentFloatingActions({
onCloseClick,
onSubmitClick,
import { useInventoryAdjContext } from './InventoryAdjustmentFormProvider';
import withDialogActions from 'containers/Dialog/withDialogActions';
import { compose } from 'utils';
/**
* Inventory adjustment floating actions.
*/
function InventoryAdjustmentFloatingActions({
// #withDialogActions
closeDialog,
}) {
const { isSubmitting } = useFormikContext();
// Formik context.
const { isSubmitting, submitForm } = useFormikContext();
// Inventory adjustment dialog context.
const { dialogName, setSubmitPayload } = useInventoryAdjContext();
// handle submit as draft button click.
const handleSubmitDraftBtnClick = (event) => {
saveInvoke(onSubmitClick, event, {
publish: false,
});
setSubmitPayload({ publish: false });
submitForm();
};
// Handle submit make adjustment button click.
const handleSubmitMakeAdjustmentBtnClick = (event) => {
saveInvoke(onSubmitClick, event, {
publish: true,
});
setSubmitPayload({ publish: true });
submitForm();
};
// Handle close button click.
const handleCloseBtnClick = (event) => {
closeDialog(dialogName)
};
return (
<div className={Classes.DIALOG_FOOTER}>
<div className={Classes.DIALOG_FOOTER_ACTIONS}>
<Button onClick={onCloseClick} style={{ minWidth: '75px' }}>
<Button onClick={handleCloseBtnClick} style={{ minWidth: '75px' }}>
<T id={'close'} />
</Button>
@@ -49,3 +65,7 @@ export default function InventoryAdjustmentFloatingActions({
</div>
);
}
export default compose(
withDialogActions
)(InventoryAdjustmentFloatingActions);

View File

@@ -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 (
<Formik
validationSchema={CreateInventoryAdjustmentFormSchema}
initialValues={initialValues}
onSubmit={handleFormSubmit}
>
<InventoryAdjustmentFormContent />
</Formik>
);
}
export default compose(withDialogActions)(InventoryAdjustmentForm);

View File

@@ -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 (
<Form>
<InventoryAdjustmentFormDialogFields />
<InventoryAdjustmentFloatingActions />
</Form>
);
}

View File

@@ -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 (
<DialogContent isLoading={fetchAccount.isFetching || fetchItem.isFetching}>
<Formik
validationSchema={CreateInventoryAdjustmentFormSchema}
initialValues={initialValues}
onSubmit={handleFormSubmit}
>
<Form>
<InventoryAdjustmentFormDialogFields dialogName={dialogName} />
<InventoryAdjustmentFloatingActions
onSubmitClick={handleSubmitClick}
onCloseClick={handleCloseClick}
/>
</Form>
</Formik>
</DialogContent>
<InventoryAdjustmentFormProvider itemId={itemId} dialogName={dialogName}>
<InventoryAdjustmentForm />
</InventoryAdjustmentFormProvider>
);
}
export default compose(
withInventoryAdjustmentActions,
withDialogActions,
withAccountsActions,
withItem(({ item }) => ({
item: item
})),
withItemsActions,
)(InventoryAdjustmentFormDialogContent);

View File

@@ -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)}
/>
</FormGroup>
)}
@@ -115,7 +118,7 @@ function InventoryAdjustmentFormDialogFields({
className={'form-group--adjustment-account'}
>
<AccountsSuggestField
accounts={accountsList}
accounts={accounts}
onAccountSelected={(item) =>
form.setFieldValue('adjustment_account_id', item.id)
}
@@ -159,9 +162,3 @@ function InventoryAdjustmentFormDialogFields({
</div>
);
}
export default compose(
withAccounts(({ accountsList }) => ({
accountsList,
})),
)(InventoryAdjustmentFormDialogFields);

View File

@@ -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 (
<DialogContent isLoading={isAccountsLoading || isItemLoading}>
<InventoryAdjustmentContext.Provider value={provider} {...props} />
</DialogContent>
);
}
const useInventoryAdjContext = () => React.useContext(InventoryAdjustmentContext);
export { InventoryAdjustmentFormProvider, useInventoryAdjContext };

View File

@@ -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 (
<Form>
<div className={Classes.DIALOG_BODY}>
{/* ----------- Category name ----------- */}
<FastField name={'name'}>
{({ field, field: { value }, meta: { error, touched } }) => (
<FormGroup
label={<T id={'category_name'} />}
labelInfo={<FieldRequiredHint />}
className={'form-group--category-name'}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name="name" />}
inline={true}
>
<InputGroup
medium={true}
inputRef={(ref) => (categoryNameFieldRef.current = ref)}
{...field}
/>
</FormGroup>
)}
</FastField>
{/* ----------- Description ----------- */}
<FastField name={'description'}>
{({ field, field: { value }, meta: { error, touched } }) => (
<FormGroup
label={<T id={'description'} />}
className={'form-group--description'}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name="description" />}
inline={true}
>
<TextArea growVertically={true} large={true} {...field} />
</FormGroup>
)}
</FastField>
{/* ----------- Cost account ----------- */}
<FastField name={'cost_account_id'}>
{({ form, field: { value }, meta: { error, touched } }) => (
<FormGroup
label={<T id={'cost_account'} />}
inline={true}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name="cost_account_id" />}
className={classNames(
'form-group--cost-account',
'form-group--select-list',
Classes.FILL,
)}
>
<AccountsSelectList
accounts={accountsList}
onAccountSelected={(account) => {
form.setFieldValue('cost_account_id', account.id);
}}
defaultSelectText={<T id={'select_account'} />}
selectedAccountId={value}
filterByTypes={['cost_of_goods_sold']}
/>
</FormGroup>
)}
</FastField>
{/* ----------- Sell account ----------- */}
<FastField name={'sell_account_id'}>
{({ form, field: { value }, meta: { error, touched } }) => (
<FormGroup
label={<T id={'sell_account'} />}
inline={true}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name="sell_account_id" />}
className={classNames(
'form-group--sell-account',
'form-group--select-list',
Classes.FILL,
)}
>
<AccountsSelectList
accounts={accountsList}
onAccountSelected={(account) => {
form.setFieldValue('sell_account_id', account.id);
}}
defaultSelectText={<T id={'select_account'} />}
selectedAccountId={value}
filterByTypes={['income']}
/>
</FormGroup>
)}
</FastField>
{/* ----------- inventory account ----------- */}
<FastField name={'inventory_account_id'}>
{({ form, field: { value }, meta: { error, touched } }) => (
<FormGroup
label={<T id={'inventory_account'} />}
inline={true}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name="inventory_account_id" />}
className={classNames(
'form-group--sell-account',
'form-group--select-list',
Classes.FILL,
)}
>
<AccountsSelectList
accounts={accountsList}
onAccountSelected={(account) => {
form.setFieldValue('inventory_account_id', account.id);
}}
defaultSelectText={<T id={'select_account'} />}
selectedAccountId={value}
/>
</FormGroup>
)}
</FastField>
</div>
<div className={Classes.DIALOG_FOOTER}>
<div className={Classes.DIALOG_FOOTER_ACTIONS}>
<Button onClick={onClose}>
<T id={'close'} />
</Button>
<Button intent={Intent.PRIMARY} type="submit" disabled={isSubmitting}>
{itemCategoryId ? <T id={'edit'} /> : <T id={'submit'} />}
</Button>
</div>
</div>
</Form>
<Formik
validationSchema={
isNewMode ? CreateItemCategoryFormSchema : EditItemCategoryFormSchema
}
initialValues={initialValues}
onSubmit={handleFormSubmit}
>
<ItemCategoryFormContent />
</Formik>
);
}
export default compose(
withDialogActions,
)(ItemCategoryForm);

View File

@@ -0,0 +1,13 @@
import React from 'react';
import { Form } from 'formik';
import ItemCategoryFormFields from './ItemCategoryFormFields';
import ItemCategoryFormFooter from './ItemCategoryFormFooter';
export default function ItemCategoryForm() {
return (
<Form>
<ItemCategoryFormFields />
<ItemCategoryFormFooter />
</Form>
);
}

View File

@@ -1,168 +1,23 @@
import React, { useMemo, useCallback } from 'react';
import { Intent } from '@blueprintjs/core';
import { useQuery, queryCache } from 'react-query';
import { FormattedMessage as T, useIntl } from 'react-intl';
import { Formik } from 'formik';
import { AppToaster, DialogContent } from 'components';
import React from 'react';
import { ItemCategoryProvider } from './ItemCategoryProvider';
import ItemCategoryForm from './ItemCategoryForm';
import withItemCategories from 'containers/Items/withItemCategories';
import withItemCategoryDetail from 'containers/Items/withItemCategoryDetail';
import withItemCategoriesActions from 'containers/Items/withItemCategoriesActions';
import withAccounts from 'containers/Accounts/withAccounts';
import withAccountsActions from 'containers/Accounts/withAccountsActions';
import withDialogActions from 'containers/Dialog/withDialogActions';
import {
EditItemCategoryFormSchema,
CreateItemCategoryFormSchema,
} from './itemCategoryForm.schema';
import { compose, transformToForm } from 'utils';
import 'style/pages/ItemCategory/ItemCategoryDialog.scss'
const defaultInitialValues = {
name: '',
description: '',
cost_account_id: '',
sell_account_id: '',
inventory_account_id: '',
};
import 'style/pages/ItemCategory/ItemCategoryDialog.scss';
/**
* Item Category form dialog content.
*/
function ItemCategoryFormDialogContent({
// #withDialogActions
closeDialog,
// #withItemCategoryDetail
itemCategoryDetail,
// #withItemCategories
categoriesList,
// #withItemCategoriesActions
requestSubmitItemCategory,
requestEditItemCategory,
requestFetchItemCategories,
//# withAccount
accountsList,
// #withAccountsActions
requestFetchAccounts,
export default function ItemCategoryFormDialogContent({
// #ownProp
action,
itemCategoryId,
dialogName,
}) {
const isNewMode = !itemCategoryId;
const { formatMessage } = useIntl();
// Fetches categories list.
const fetchCategoriesList = useQuery(['items-categories-list'], () =>
requestFetchItemCategories(),
);
// Fetches accounts list.
const fetchAccountsList = useQuery('accounts-list', () =>
requestFetchAccounts(),
);
const initialValues = useMemo(
() => ({
...defaultInitialValues,
...transformToForm(itemCategoryDetail, defaultInitialValues),
}),
[],
);
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 };
const afterSubmit = () => {
closeDialog(dialogName);
queryCache.invalidateQueries('items-categories-list');
queryCache.invalidateQueries('accounts-list');
};
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);
};
const onError = (errors) => {
transformErrors(errors, { setErrors });
setSubmitting(false);
};
if (isNewMode) {
requestSubmitItemCategory(form).then(onSuccess).catch(onError);
} else {
requestEditItemCategory(itemCategoryId, form)
.then(onSuccess)
.catch(onError);
}
};
// Handles dialog close.
const handleClose = useCallback(() => {
closeDialog(dialogName);
}, [closeDialog, dialogName]);
return (
<DialogContent
isLoading={fetchCategoriesList.isFetching || fetchAccountsList.isFetching}
<ItemCategoryProvider
itemCategoryId={itemCategoryId}
dialogName={dialogName}
>
<Formik
validationSchema={
isNewMode ? CreateItemCategoryFormSchema : EditItemCategoryFormSchema
}
initialValues={initialValues}
onSubmit={handleFormSubmit}
>
{({ isSubmitting }) => (
<ItemCategoryForm
itemCategoryId={itemCategoryId}
accountsList={accountsList}
categoriesList={categoriesList}
isSubmitting={isSubmitting}
onClose={handleClose}
/>
)}
</Formik>
</DialogContent>
<ItemCategoryForm />
</ItemCategoryProvider>
);
}
export default compose(
withDialogActions,
withItemCategoryDetail(),
withItemCategories(({ categoriesList }) => ({
categoriesList,
})),
withAccounts(({ accountsList }) => ({
accountsList,
})),
withItemCategoriesActions,
withAccountsActions,
)(ItemCategoryFormDialogContent);

View File

@@ -0,0 +1,54 @@
import React from 'react';
import { Classes, FormGroup, InputGroup, TextArea } from '@blueprintjs/core';
import { FormattedMessage as T } from 'react-intl';
import { ErrorMessage, FastField } from 'formik';
import { useAutofocus } from 'hooks';
import { FieldRequiredHint } from 'components';
import { inputIntent } from 'utils';
/**
* Item category form fields.
*/
export default function ItemCategoryFormFields() {
const categoryNameFieldRef = useAutofocus();
return (
<div className={Classes.DIALOG_BODY}>
{/* ----------- Category name ----------- */}
<FastField name={'name'}>
{({ field, field: { value }, meta: { error, touched } }) => (
<FormGroup
label={<T id={'category_name'} />}
labelInfo={<FieldRequiredHint />}
className={'form-group--category-name'}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name="name" />}
inline={true}
>
<InputGroup
medium={true}
inputRef={(ref) => (categoryNameFieldRef.current = ref)}
{...field}
/>
</FormGroup>
)}
</FastField>
{/* ----------- Description ----------- */}
<FastField name={'description'}>
{({ field, field: { value }, meta: { error, touched } }) => (
<FormGroup
label={<T id={'description'} />}
className={'form-group--description'}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name="description" />}
inline={true}
>
<TextArea growVertically={true} large={true} {...field} />
</FormGroup>
)}
</FastField>
</div>
);
}

View File

@@ -0,0 +1,43 @@
import React from 'react';
import { Classes, Button, Intent } from '@blueprintjs/core';
import { FormattedMessage as T } from 'react-intl';
import { useFormikContext } from 'formik';
import withDialogActions from 'containers/Dialog/withDialogActions';
import { useItemCategoryContext } from './ItemCategoryProvider';
import { compose } from 'utils';
/**
* Item category form footer.
*/
function ItemCategoryFormFooter({
// #withDialogActions
closeDialog,
}) {
// Item category context.
const { isNewMode, dialogName } = useItemCategoryContext();
// Formik context.
const { isSubmitting } = useFormikContext();
// Handle close button click.
const handleCloseBtnClick = () => {
closeDialog(dialogName);
};
return (
<div className={Classes.DIALOG_FOOTER}>
<div className={Classes.DIALOG_FOOTER_ACTIONS}>
<Button onClick={handleCloseBtnClick}>
<T id={'close'} />
</Button>
<Button intent={Intent.PRIMARY} type="submit" disabled={isSubmitting}>
{isNewMode ? <T id={'submit'} /> : <T id={'edit'} />}
</Button>
</div>
</div>
);
}
export default compose(withDialogActions)(ItemCategoryFormFooter);

View File

@@ -0,0 +1,57 @@
import React, { createContext } from 'react';
import { DialogContent } from 'components';
import {
useItemCategory,
useEditItemCategory,
useCreateItemCategory,
} from 'hooks/query';
const ItemCategoryContext = createContext();
/**
* Accounts chart data provider.
*/
function ItemCategoryProvider({ itemCategoryId, dialogName, ...props }) {
const { data: itemCategory, isFetching: isItemCategoryLoading } = useItemCategory(
itemCategoryId,
{
enabled: !!itemCategoryId,
},
);
// Create and edit item category mutations.
const { mutateAsync: createItemCategoryMutate } = useCreateItemCategory();
const { mutateAsync: editItemCategoryMutate } = useEditItemCategory();
// Detarmines whether the new mode form.
const isNewMode = !itemCategoryId;
const isEditMode = !isNewMode;
// Provider state.
const provider = {
itemCategoryId,
dialogName,
itemCategory,
isItemCategoryLoading,
createItemCategoryMutate,
editItemCategoryMutate,
isNewMode,
isEditMode
};
return (
<DialogContent
isLoading={isItemCategoryLoading}
name={'item-category-form'}
>
<ItemCategoryContext.Provider value={provider} {...props} />
</DialogContent>
);
}
const useItemCategoryContext = () =>
React.useContext(ItemCategoryContext);
export { ItemCategoryProvider, useItemCategoryContext };

View File

@@ -1,5 +1,5 @@
import React, { lazy } from 'react';
import { FormattedMessage as T, useIntl } from 'react-intl';
import { FormattedMessage as T } from 'react-intl';
import { Dialog, DialogSuspense } from 'components';
import withDialogRedux from 'components/DialogReduxConnect';

View File

@@ -7,9 +7,6 @@ const Schema = Yup.object().shape({
.required()
.max(DATATYPES_LENGTH.STRING)
.label(formatMessage({ id: 'category_name_' })),
cost_account_id: Yup.number().nullable(),
sell_account_id: Yup.number().nullable(),
inventory_account_id: Yup.number().nullable(),
description: Yup.string().trim().max(DATATYPES_LENGTH.TEXT).nullable(),
});

View File

@@ -7,7 +7,11 @@ import { orderingLinesIndexes, repeatValue } from 'utils';
import 'style/components/DataTable/DataTableEditable.scss';
/**
* Editable items entries table.
*/
export default function EditableItemsEntriesTable({
items,
defaultEntry,
minLinesNumber = 2,
linesNumber = 5,
@@ -61,6 +65,7 @@ export default function EditableItemsEntriesTable({
onUpdateData={(entries) => {
form.setFieldValue('entries', entries);
}}
items={items}
entries={value}
errors={error}
filterPurchasableItems={filterPurchasableItems}

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