re-structure to monorepo.

This commit is contained in:
a.bouhuolia
2023-02-03 01:02:31 +02:00
parent 8242ec64ba
commit 7a0a13f9d5
10400 changed files with 46966 additions and 17223 deletions

View File

@@ -0,0 +1,78 @@
// @ts-nocheck
import * as Yup from 'yup';
import { defaultTo } from 'lodash';
import intl from 'react-intl-universal';
import { DATATYPES_LENGTH } from '@/constants/dataTypes';
const Schema = Yup.object().shape({
active: Yup.boolean(),
name: Yup.string()
.required()
.min(0)
.max(DATATYPES_LENGTH.STRING)
.label(intl.get('item_name_')),
type: Yup.string()
.trim()
.required()
.min(0)
.max(DATATYPES_LENGTH.STRING)
.label(intl.get('item_type_')),
code: Yup.string().trim().min(0).max(DATATYPES_LENGTH.STRING),
cost_price: Yup.number()
.min(0)
.max(DATATYPES_LENGTH.DECIMAL_13_3)
.when(['purchasable'], {
is: true,
then: Yup.number()
.required()
.label(intl.get('cost_price_')),
otherwise: Yup.number().nullable(true),
}),
sell_price: Yup.number()
.min(0)
.max(DATATYPES_LENGTH.DECIMAL_13_3)
.when(['sellable'], {
is: true,
then: Yup.number()
.required()
.label(intl.get('sell_price_')),
otherwise: Yup.number().nullable(true),
}),
cost_account_id: Yup.number()
.when(['purchasable'], {
is: true,
then: Yup.number().required(),
otherwise: Yup.number().nullable(true),
})
.label(intl.get('cost_account_id')),
sell_account_id: Yup.number()
.when(['sellable'], {
is: true,
then: Yup.number().required(),
otherwise: Yup.number().nullable(),
})
.label(intl.get('sell_account_id')),
inventory_account_id: Yup.number()
.when(['type'], {
is: (value) => value === 'inventory',
then: Yup.number().required(),
otherwise: Yup.number().nullable(),
})
.label(intl.get('inventory_account')),
category_id: Yup.number().positive().nullable(),
stock: Yup.string() || Yup.boolean(),
sellable: Yup.boolean().required(),
purchasable: Yup.boolean().required(),
});
export const transformItemFormData = (item, defaultValue) => {
return {
...item,
sellable: !!defaultTo(item?.sellable, defaultValue.sellable),
purchasable: !!defaultTo(item?.purchasable, defaultValue.purchasable),
active: !!defaultTo(item?.active, defaultValue.active),
};
};
export const CreateItemFormSchema = Schema;
export const EditItemFormSchema = Schema;

View File

@@ -0,0 +1,94 @@
// @ts-nocheck
import React from 'react';
import intl from 'react-intl-universal';
import styled from 'styled-components';
import { useHistory } from 'react-router-dom';
import ItemFormFormik from './ItemFormFormik';
import { useDashboardPageTitle } from '@/hooks/state';
import { useItemFormContext, ItemFormProvider } from './ItemFormProvider';
import { DashboardInsider, DashboardCard } from '@/components';
/**
* Item form dashboard title.
* @returns {null}
*/
function ItemFormDashboardTitle() {
// Change page title dispatcher.
const changePageTitle = useDashboardPageTitle();
// Item form context.
const { isNewMode } = useItemFormContext();
// Changes the page title in new and edit mode.
React.useEffect(() => {
isNewMode
? changePageTitle(intl.get('new_item'))
: changePageTitle(intl.get('edit_item_details'));
}, [changePageTitle, isNewMode]);
return null;
}
/**
* Item form page loading state indicator.
* @returns {JSX}
*/
function ItemFormPageLoading({ children }) {
const { isFormLoading } = useItemFormContext();
return (
<DashboardItemFormPageInsider loading={isFormLoading} name={'item-form'}>
{children}
</DashboardItemFormPageInsider>
);
}
/**
* Item form of the page.
* @returns {JSX}
*/
export default function ItemForm({ itemId }) {
// History context.
const history = useHistory();
// Handle the form submit success.
const handleSubmitSuccess = (values, form, submitPayload) => {
if (submitPayload.redirect) {
history.push('/items');
}
};
// Handle cancel button click.
const handleFormCancel = () => {
history.goBack();
};
return (
<ItemFormProvider itemId={itemId}>
<ItemFormDashboardTitle />
<ItemFormPageLoading>
<DashboardCard page>
<ItemFormPageFormik
onSubmitSuccess={handleSubmitSuccess}
onCancel={handleFormCancel}
/>
</DashboardCard>
</ItemFormPageLoading>
</ItemFormProvider>
);
}
const DashboardItemFormPageInsider = styled(DashboardInsider)`
padding-bottom: 64px;
`;
const ItemFormPageFormik = styled(ItemFormFormik)`
.page-form {
&__floating-actions {
margin-left: -40px;
margin-right: -40px;
}
}
`;

View File

@@ -0,0 +1,268 @@
// @ts-nocheck
import React from 'react';
import { useFormikContext, FastField, ErrorMessage } from 'formik';
import {
FormGroup,
Classes,
TextArea,
Checkbox,
ControlGroup,
} from '@blueprintjs/core';
import {
AccountsSelectList,
MoneyInputGroup,
Col,
Row,
Hint,
InputPrependText,
} from '@/components';
import { FormattedMessage as T } from '@/components';
import classNames from 'classnames';
import { useItemFormContext } from './ItemFormProvider';
import withCurrentOrganization from '@/containers/Organization/withCurrentOrganization';
import { ACCOUNT_PARENT_TYPE } from '@/constants/accountTypes';
import {
sellDescriptionFieldShouldUpdate,
sellAccountFieldShouldUpdate,
sellPriceFieldShouldUpdate,
costPriceFieldShouldUpdate,
costAccountFieldShouldUpdate,
purchaseDescFieldShouldUpdate,
} from './utils';
import { compose, inputIntent } from '@/utils';
/**
* Item form body.
*/
function ItemFormBody({ organization: { base_currency } }) {
const { accounts } = useItemFormContext();
const { values } = useFormikContext();
return (
<div class="page-form__section page-form__section--selling-cost">
<Row>
<Col xs={6}>
{/*------------- Purchasable checbox ------------- */}
<FastField name={'sellable'} type="checkbox">
{({ form, field }) => (
<FormGroup inline={true} className={'form-group--sellable'}>
<Checkbox
inline={true}
label={
<h3>
<T id={'i_sell_this_item'} />
</h3>
}
name={'sellable'}
{...field}
/>
</FormGroup>
)}
</FastField>
{/*------------- Selling price ------------- */}
<FastField
name={'sell_price'}
sellable={values.sellable}
shouldUpdate={sellPriceFieldShouldUpdate}
>
{({ form, field: { value }, meta: { error, touched } }) => (
<FormGroup
label={<T id={'selling_price'} />}
className={'form-group--sell_price'}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name={'sell_price'} />}
inline={true}
>
<ControlGroup>
<InputPrependText text={base_currency} />
<MoneyInputGroup
value={value}
inputGroupProps={{ fill: true }}
disabled={!form.values.sellable}
onChange={(unformattedValue) => {
form.setFieldValue('sell_price', unformattedValue);
}}
/>
</ControlGroup>
</FormGroup>
)}
</FastField>
{/*------------- Selling account ------------- */}
<FastField
name={'sell_account_id'}
sellable={values.sellable}
accounts={accounts}
shouldUpdate={sellAccountFieldShouldUpdate}
>
{({ form, field: { value }, meta: { error, touched } }) => (
<FormGroup
label={<T id={'account'} />}
labelInfo={
<Hint content={<T id={'item.field.sell_account.hint'} />} />
}
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={accounts}
onAccountSelected={(account) => {
form.setFieldValue('sell_account_id', account.id);
}}
defaultSelectText={<T id={'select_account'} />}
selectedAccountId={value}
disabled={!form.values.sellable}
filterByParentTypes={[ACCOUNT_PARENT_TYPE.INCOME]}
popoverFill={true}
allowCreate={true}
/>
</FormGroup>
)}
</FastField>
<FastField
name={'sell_description'}
sellable={values.sellable}
shouldUpdate={sellDescriptionFieldShouldUpdate}
>
{({ form: { values }, field, meta: { error, touched } }) => (
<FormGroup
label={<T id={'description'} />}
className={'form-group--sell-description'}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name={'description'} />}
inline={true}
>
<TextArea
growVertically={true}
height={280}
{...field}
disabled={!values.sellable}
/>
</FormGroup>
)}
</FastField>
</Col>
<Col xs={6}>
{/*------------- Sellable checkbox ------------- */}
<FastField name={'purchasable'} type={'checkbox'}>
{({ field }) => (
<FormGroup inline={true} className={'form-group--purchasable'}>
<Checkbox
inline={true}
label={
<h3>
<T id={'i_purchase_this_item'} />
</h3>
}
{...field}
/>
</FormGroup>
)}
</FastField>
{/*------------- Cost price ------------- */}
<FastField
name={'cost_price'}
purchasable={values.purchasable}
shouldUpdate={costPriceFieldShouldUpdate}
>
{({ field, form, field: { value }, meta: { error, touched } }) => (
<FormGroup
label={<T id={'cost_price'} />}
className={'form-group--item-cost-price'}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name="cost_price" />}
inline={true}
>
<ControlGroup>
<InputPrependText text={base_currency} />
<MoneyInputGroup
value={value}
inputGroupProps={{ medium: true }}
disabled={!form.values.purchasable}
onChange={(unformattedValue) => {
form.setFieldValue('cost_price', unformattedValue);
}}
/>
</ControlGroup>
</FormGroup>
)}
</FastField>
{/*------------- Cost account ------------- */}
<FastField
name={'cost_account_id'}
purchasable={values.purchasable}
accounts={accounts}
shouldUpdate={costAccountFieldShouldUpdate}
>
{({ form, field: { value }, meta: { error, touched } }) => (
<FormGroup
label={<T id={'account'} />}
labelInfo={
<Hint content={<T id={'item.field.cost_account.hint'} />} />
}
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={accounts}
onAccountSelected={(account) => {
form.setFieldValue('cost_account_id', account.id);
}}
defaultSelectText={<T id={'select_account'} />}
selectedAccountId={value}
disabled={!form.values.purchasable}
filterByParentTypes={[ACCOUNT_PARENT_TYPE.EXPENSE]}
popoverFill={true}
allowCreate={true}
/>
</FormGroup>
)}
</FastField>
<FastField
name={'purchase_description'}
purchasable={values.purchasable}
shouldUpdate={purchaseDescFieldShouldUpdate}
>
{({ form: { values }, field, meta: { error, touched } }) => (
<FormGroup
label={<T id={'description'} />}
className={'form-group--purchase-description'}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name={'description'} />}
inline={true}
>
<TextArea
growVertically={true}
height={280}
{...field}
disabled={!values.purchasable}
/>
</FormGroup>
)}
</FastField>
</Col>
</Row>
</div>
);
}
export default compose(withCurrentOrganization())(ItemFormBody);

View File

@@ -0,0 +1,87 @@
// @ts-nocheck
import React from 'react';
import styled from 'styled-components';
import classNames from 'classnames';
import { Button, Intent, FormGroup, Checkbox } from '@blueprintjs/core';
import { FastField, useFormikContext } from 'formik';
import { CLASSES } from '@/constants/classes';
import { useItemFormContext } from './ItemFormProvider';
import { FormattedMessage as T } from '@/components';
import { saveInvoke } from '@/utils';
/**
* Item form floating actions.
*/
export default function ItemFormFloatingActions({ onCancel }) {
// Item form context.
const { setSubmitPayload, isNewMode } = useItemFormContext();
// Formik context.
const { isSubmitting } = useFormikContext();
// Handle cancel button click.
const handleCancelBtnClick = (event) => {
saveInvoke(onCancel, event);
};
// Handle submit button click.
const handleSubmitBtnClick = (event) => {
setSubmitPayload({ redirect: true });
};
// Handle submit & new button click.
const handleSubmitAndNewBtnClick = (event) => {
setSubmitPayload({ redirect: false });
};
return (
<div className={classNames(CLASSES.PAGE_FORM_FLOATING_ACTIONS)}>
<SaveButton
intent={Intent.PRIMARY}
disabled={isSubmitting}
loading={isSubmitting}
onClick={handleSubmitBtnClick}
type="submit"
className={'btn--submit'}
>
{isNewMode ? <T id={'save'} /> : <T id={'edit'} />}
</SaveButton>
<Button
className={classNames('ml1', 'btn--submit-new')}
disabled={isSubmitting}
onClick={handleSubmitAndNewBtnClick}
type="submit"
>
<T id={'save_new'} />
</Button>
<Button
disabled={isSubmitting}
className={'ml1'}
onClick={handleCancelBtnClick}
>
<T id={'close'} />
</Button>
{/*----------- Active ----------*/}
<FastField name={'active'} type={'checkbox'}>
{({ field }) => (
<FormGroup inline={true} className={'form-group--active'}>
<Checkbox
inline={true}
label={<T id={'active'} />}
name={'active'}
{...field}
/>
</FormGroup>
)}
</FastField>
</div>
);
}
const SaveButton = styled(Button)`
min-width: 100px;
`;

View File

@@ -0,0 +1,112 @@
// @ts-nocheck
import React from 'react';
import { Formik, Form } from 'formik';
import { Intent } from '@blueprintjs/core';
import intl from 'react-intl-universal';
import classNames from 'classnames';
import '@/style/pages/Items/Form.scss';
import { CLASSES } from '@/constants/classes';
import { AppToaster } from '@/components';
import ItemFormBody from './ItemFormBody';
import ItemFormPrimarySection from './ItemFormPrimarySection';
import ItemFormFloatingActions from './ItemFormFloatingActions';
import ItemFormInventorySection from './ItemFormInventorySection';
import {
transformSubmitRequestErrors,
useItemFormInitialValues,
} from './utils';
import { useItemFormContext } from './ItemFormProvider';
import { EditItemFormSchema, CreateItemFormSchema } from './ItemForm.schema';
import { safeInvoke } from '@/utils';
/**
* Item form.
*/
export default function ItemFormFormik({
// #ownProps
initialValues: initialValuesComponent,
onSubmitSuccess,
onSubmitError,
onCancel,
className,
}) {
// Item form context.
const {
itemId,
item,
accounts,
createItemMutate,
editItemMutate,
submitPayload,
isNewMode,
} = useItemFormContext();
// Initial values in create and edit mode.
const initialValues = useItemFormInitialValues(item, initialValuesComponent);
// Handles the form submit.
const handleFormSubmit = (values, form) => {
const { setSubmitting, resetForm, setErrors } = form;
const formValues = { ...values };
setSubmitting(true);
// Handle response succes.
const onSuccess = (response) => {
AppToaster.show({
message: intl.get(
isNewMode
? 'the_item_has_been_created_successfully'
: 'the_item_has_been_edited_successfully',
{
number: itemId,
},
),
intent: Intent.SUCCESS,
});
resetForm();
setSubmitting(false);
safeInvoke(onSubmitSuccess, values, form, submitPayload, response);
};
// Handle response error.
const onError = (errors) => {
setSubmitting(false);
if (errors) {
const _errors = transformSubmitRequestErrors(errors);
setErrors({ ..._errors });
}
safeInvoke(onSubmitError, values, form, submitPayload, errors);
};
if (isNewMode) {
createItemMutate(formValues).then(onSuccess).catch(onError);
} else {
editItemMutate([itemId, formValues]).then(onSuccess).catch(onError);
}
};
return (
<div class={classNames(CLASSES.PAGE_FORM_ITEM, className)}>
<Formik
enableReinitialize={true}
validationSchema={isNewMode ? CreateItemFormSchema : EditItemFormSchema}
initialValues={initialValues}
onSubmit={handleFormSubmit}
>
<Form>
<div class={classNames(CLASSES.PAGE_FORM_BODY)}>
<ItemFormPrimarySection />
<ItemFormBody accounts={accounts} />
<ItemFormInventorySection accounts={accounts} />
</div>
<ItemFormFloatingActions onCancel={onCancel} />
</Form>
</Formik>
</div>
);
}

View File

@@ -0,0 +1,70 @@
// @ts-nocheck
import React from 'react';
import { FastField, ErrorMessage } from 'formik';
import { FormGroup } from '@blueprintjs/core';
import { CLASSES } from '@/constants/classes';
import {
AccountsSelectList,
FormattedMessage as T,
Col,
Row,
} from '@/components';
import classNames from 'classnames';
import withCurrentOrganization from '@/containers/Organization/withCurrentOrganization';
import { accountsFieldShouldUpdate } from './utils';
import { compose, inputIntent } from '@/utils';
import { ACCOUNT_TYPE } from '@/constants/accountTypes';
import { useItemFormContext } from './ItemFormProvider';
/**
* Item form inventory sections.
*/
function ItemFormInventorySection({ organization: { base_currency } }) {
const { accounts } = useItemFormContext();
return (
<div class="page-form__section page-form__section--inventory">
<h3>
<T id={'inventory_information'} />
</h3>
<Row>
<Col xs={6}>
{/*------------- Inventory account ------------- */}
<FastField
name={'inventory_account_id'}
accounts={accounts}
shouldUpdate={accountsFieldShouldUpdate}
>
{({ form, field: { value }, meta: { touched, error } }) => (
<FormGroup
label={<T id={'inventory_account'} />}
inline={true}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name="inventory_account_id" />}
className={classNames(
'form-group--item-inventory_account',
'form-group--select-list',
CLASSES.FILL,
)}
>
<AccountsSelectList
accounts={accounts}
onAccountSelected={(account) => {
form.setFieldValue('inventory_account_id', account.id);
}}
defaultSelectText={<T id={'select_account'} />}
selectedAccountId={value}
filterByTypes={[ACCOUNT_TYPE.INVENTORY]}
/>
</FormGroup>
)}
</FastField>
</Col>
</Row>
</div>
);
}
export default compose(withCurrentOrganization())(ItemFormInventorySection);

View File

@@ -0,0 +1,15 @@
// @ts-nocheck
import React from 'react';
import { useParams } from 'react-router-dom';
import ItemForm from './ItemForm';
/**
* Item form page.
*/
export default function ItemFormPage() {
const { id } = useParams();
const idInteger = parseInt(id, 10);
return <ItemForm itemId={idInteger} />;
}

View File

@@ -0,0 +1,172 @@
// @ts-nocheck
import React, { useEffect, useRef } from 'react';
import {
FormGroup,
InputGroup,
RadioGroup,
Classes,
Radio,
Position,
} from '@blueprintjs/core';
import { ErrorMessage, FastField } from 'formik';
import { CLASSES } from '@/constants/classes';
import {
CategoriesSelectList,
Hint,
Col,
Row,
FieldRequiredHint,
FormattedMessage as T,
FormattedHTMLMessage,
} from '@/components';
import classNames from 'classnames';
import { useItemFormContext } from './ItemFormProvider';
import { handleStringChange, inputIntent } from '@/utils';
import { categoriesFieldShouldUpdate } from './utils';
/**
* Item form primary section.
*/
export default function ItemFormPrimarySection() {
// Item form context.
const { isNewMode, item, itemsCategories } = useItemFormContext();
const nameFieldRef = useRef(null);
useEffect(() => {
// Auto focus item name field once component mount.
if (nameFieldRef.current) {
nameFieldRef.current.focus();
}
}, []);
const itemTypeHintContent = (
<>
<div class="mb1">
<FormattedHTMLMessage id={'services_that_you_provide_to_customers'} />
</div>
<div class="mb1">
<FormattedHTMLMessage id={'products_you_buy_and_or_sell'} />
</div>
</>
);
return (
<div className={classNames(CLASSES.PAGE_FORM_HEADER_PRIMARY)}>
{/*----------- Item type ----------*/}
<FastField name={'type'}>
{({ form, field: { value }, meta: { touched, error } }) => (
<FormGroup
medium={true}
label={<T id={'item_type'} />}
labelInfo={
<span>
<FieldRequiredHint />
<Hint
content={itemTypeHintContent}
position={Position.BOTTOM_LEFT}
/>
</span>
}
className={'form-group--item-type'}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name="item_type" />}
inline={true}
>
<RadioGroup
inline={true}
onChange={handleStringChange((_value) => {
form.setFieldValue('type', _value);
})}
selectedValue={value}
disabled={!isNewMode && item.type === 'inventory'}
>
<Radio label={<T id={'service'} />} value="service" />
<Radio label={<T id={'inventory'} />} value="inventory" />
</RadioGroup>
</FormGroup>
)}
</FastField>
<Row>
<Col xs={7}>
{/*----------- Item name ----------*/}
<FastField name={'name'}>
{({ field, meta: { error, touched } }) => (
<FormGroup
label={<T id={'item_name'} />}
labelInfo={<FieldRequiredHint />}
className={'form-group--item-name'}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name={'name'} />}
inline={true}
>
<InputGroup
medium={true}
{...field}
intent={inputIntent({ error, touched })}
inputRef={(ref) => (nameFieldRef.current = ref)}
/>
</FormGroup>
)}
</FastField>
{/*----------- SKU ----------*/}
<FastField name={'code'}>
{({ field, meta: { error, touched } }) => (
<FormGroup
label={<T id={'item_code'} />}
className={'form-group--item_code'}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name={'code'} />}
inline={true}
>
<InputGroup
medium={true}
intent={inputIntent({ error, touched })}
{...field}
/>
</FormGroup>
)}
</FastField>
{/*----------- Item category ----------*/}
<FastField
name={'category_id'}
categories={itemsCategories}
shouldUpdate={categoriesFieldShouldUpdate}
>
{({ form, field: { value }, meta: { error, touched } }) => (
<FormGroup
label={<T id={'category'} />}
inline={true}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name="category_id" />}
className={classNames('form-group--category', Classes.FILL)}
>
<CategoriesSelectList
categories={itemsCategories}
selecetedCategoryId={value}
onCategorySelected={(category) => {
form.setFieldValue('category_id', category.id);
}}
/>
</FormGroup>
)}
</FastField>
</Col>
<Col xs={3}>
{/* <Dragzone
initialFiles={initialAttachmentFiles}
onDrop={handleDropFiles}
onDeleteFile={handleDeleteFile}
hint={'Attachments: Maxiumum size: 20MB'}
className={'mt2'}
/> */}
</Col>
</Row>
</div>
);
}

View File

@@ -0,0 +1,90 @@
// @ts-nocheck
import React, { createContext, useState } from 'react';
import { useLocation } from 'react-router-dom';
import {
useItem,
useSettingsItems,
useItemsCategories,
useCreateItem,
useEditItem,
useAccounts,
} from '@/hooks/query';
import { useWatchItemError } from './utils';
const ItemFormContext = createContext();
/**
* Accounts chart data provider.
*/
function ItemFormProvider({ itemId, ...props }) {
const { state } = useLocation();
const duplicateId = state?.action;
// Fetches the accounts list.
const { isLoading: isAccountsLoading, data: accounts } = useAccounts();
// Fetches the items categories list.
const {
isLoading: isItemsCategoriesLoading,
data: { itemsCategories },
} = useItemsCategories();
// Fetches the given item details.
const itemQuery = useItem(itemId || duplicateId, {
enabled: !!itemId || !!duplicateId,
});
const { isLoading: isItemLoading, data: item } = itemQuery;
// Watches and handles item not found response error.
useWatchItemError(itemQuery);
// Fetches item settings.
const {
isLoading: isItemsSettingsLoading,
isFetching: isItemsSettingsFetching,
} = useSettingsItems();
// Create and edit item mutations.
const { mutateAsync: editItemMutate } = useEditItem();
const { mutateAsync: createItemMutate } = useCreateItem();
// Holds data of submit button once clicked to form submit function.
const [submitPayload, setSubmitPayload] = useState({});
// Detarmines whether the form new mode.
const isNewMode = duplicateId || !itemId;
// Detarmines the form loading state.
const isFormLoading =
isItemsSettingsLoading ||
isAccountsLoading ||
isItemsCategoriesLoading ||
isItemLoading;
// Provider state.
const provider = {
itemId,
accounts,
item,
itemsCategories,
submitPayload,
isNewMode,
isFormLoading,
isAccountsLoading,
isItemsCategoriesLoading,
isItemLoading,
createItemMutate,
editItemMutate,
setSubmitPayload,
};
return <ItemFormContext.Provider value={provider} {...props} />;
}
const useItemFormContext = () => React.useContext(ItemFormContext);
export { ItemFormProvider, useItemFormContext };

View File

@@ -0,0 +1,191 @@
// @ts-nocheck
import React from 'react';
import { useHistory } from 'react-router-dom';
import {
NavbarGroup,
NavbarDivider,
Button,
Classes,
Intent,
Switch,
Alignment,
} from '@blueprintjs/core';
import {
DashboardActionsBar,
DashboardRowsHeightButton,
FormattedMessage as T,
} from '@/components';
import {
If,
Can,
Icon,
DashboardActionViewsList,
AdvancedFilterPopover,
DashboardFilterButton,
} from '@/components';
import { ItemAction, AbilitySubject } from '@/constants/abilityOption';
import { useItemsListContext } from './ItemsListProvider';
import { useRefreshItems } from '@/hooks/query/items';
import withItems from './withItems';
import withItemsActions from './withItemsActions';
import withAlertActions from '@/containers/Alert/withAlertActions';
import withSettings from '@/containers/Settings/withSettings';
import withSettingsActions from '@/containers/Settings/withSettingsActions';
import { compose } from '@/utils';
/**
* Items actions bar.
*/
function ItemsActionsBar({
// #withItems
itemsSelectedRows,
itemsFilterRoles,
// #withItemActions
setItemsTableState,
itemsInactiveMode,
// #withAlertActions
openAlert,
// #withSettings
itemsTableSize,
// #withSettingsActions
addSetting,
}) {
// Items list context.
const { itemsViews, fields } = useItemsListContext();
// Items refresh action.
const { refresh } = useRefreshItems();
// History context.
const history = useHistory();
// Handle `new item` button click.
const onClickNewItem = () => {
history.push('/items/new');
};
// Handle tab changing.
const handleTabChange = (view) => {
setItemsTableState({ viewSlug: view ? view.slug : null });
};
// Handle cancel/confirm items bulk.
const handleBulkDelete = () => {
openAlert('items-bulk-delete', { itemsIds: itemsSelectedRows });
};
// Handle inactive switch changing.
const handleInactiveSwitchChange = (event) => {
const checked = event.target.checked;
setItemsTableState({ inactiveMode: checked });
};
// Handle refresh button click.
const handleRefreshBtnClick = () => {
refresh();
};
// Handle table row size change.
const handleTableRowSizeChange = (size) => {
addSetting('items', 'tableSize', size);
};
return (
<DashboardActionsBar>
<NavbarGroup>
<DashboardActionViewsList
resourceName={'items'}
allMenuItem={true}
allMenuItemText={<T id={'all_items'} />}
views={itemsViews}
onChange={handleTabChange}
/>
<NavbarDivider />
<Can I={ItemAction.Create} a={AbilitySubject.Item}>
<Button
className={Classes.MINIMAL}
icon={<Icon icon="plus" />}
text={<T id={'new_item'} />}
onClick={onClickNewItem}
/>
</Can>
<AdvancedFilterPopover
advancedFilterProps={{
conditions: itemsFilterRoles,
defaultFieldKey: 'name',
fields: fields,
onFilterChange: (filterConditions) => {
setItemsTableState({ filterRoles: filterConditions });
},
}}
>
<DashboardFilterButton conditionsCount={itemsFilterRoles.length} />
</AdvancedFilterPopover>
<NavbarDivider />
<If condition={itemsSelectedRows.length}>
<Button
className={Classes.MINIMAL}
icon={<Icon icon="trash-16" iconSize={16} />}
text={<T id={'delete'} />}
intent={Intent.DANGER}
onClick={handleBulkDelete}
/>
</If>
<Button
className={Classes.MINIMAL}
icon={<Icon icon="file-import-16" iconSize={16} />}
text={<T id={'import'} />}
/>
<Button
className={Classes.MINIMAL}
icon={<Icon icon="file-export-16" iconSize={16} />}
text={<T id={'export'} />}
/>
<NavbarDivider />
<DashboardRowsHeightButton
initialValue={itemsTableSize}
onChange={handleTableRowSizeChange}
/>
<NavbarDivider />
<Can I={ItemAction.Edit} a={AbilitySubject.Item}>
<Switch
labelElement={<T id={'inactive'} />}
defaultChecked={itemsInactiveMode}
onChange={handleInactiveSwitchChange}
/>
</Can>
</NavbarGroup>
<NavbarGroup align={Alignment.RIGHT}>
<Button
className={Classes.MINIMAL}
icon={<Icon icon="refresh-16" iconSize={14} />}
onClick={handleRefreshBtnClick}
/>
</NavbarGroup>
</DashboardActionsBar>
);
}
export default compose(
withSettingsActions,
withItems(({ itemsSelectedRows, itemsTableState }) => ({
itemsSelectedRows,
itemsInactiveMode: itemsTableState.inactiveMode,
itemsFilterRoles: itemsTableState.filterRoles,
})),
withSettings(({ itemsSettings }) => ({
itemsTableSize: itemsSettings.tableSize,
})),
withItemsActions,
withAlertActions,
)(ItemsActionsBar);

View File

@@ -0,0 +1,47 @@
// @ts-nocheck
import React from 'react';
const ItemDeleteAlert = React.lazy(
() => import('@/containers/Alerts/Items/ItemDeleteAlert'),
);
const ItemInactivateAlert = React.lazy(
() => import('@/containers/Alerts/Items/ItemInactivateAlert'),
);
const ItemActivateAlert = React.lazy(
() => import('@/containers/Alerts/Items/ItemActivateAlert'),
);
const ItemBulkDeleteAlert = React.lazy(
() => import('@/containers/Alerts/Items/ItemBulkDeleteAlert'),
);
const cancelUnlockingPartialAlert = React.lazy(
() =>
import(
'@/containers/Alerts/TransactionLocking/cancelUnlockingPartialAlert'
),
);
/**
* Items alert.
*/
export default [
{
name: 'item-delete',
component: ItemDeleteAlert,
},
{
name: 'item-inactivate',
component: ItemInactivateAlert,
},
{
name: 'item-activate',
component: ItemActivateAlert,
},
{
name: 'items-bulk-delete',
component: ItemBulkDeleteAlert,
},
];

View File

@@ -0,0 +1,176 @@
// @ts-nocheck
import React from 'react';
import { useHistory } from 'react-router-dom';
import { TABLES } from '@/constants/tables';
import { FormattedMessage as T } from '@/components';
import {
DashboardContentTable,
DataTable,
TableSkeletonRows,
TableSkeletonHeader,
} from '@/components';
import ItemsEmptyStatus from './ItemsEmptyStatus';
import withItemsActions from './withItemsActions';
import withAlertsActions from '@/containers/Alert/withAlertActions';
import withDialogActions from '@/containers/Dialog/withDialogActions';
import withDrawerActions from '@/containers/Drawer/withDrawerActions';
import withSettings from '@/containers/Settings/withSettings';
import { useItemsListContext } from './ItemsListProvider';
import { useItemsTableColumns, ItemsActionMenuList } from './components';
import { useMemorizedColumnsWidths } from '@/hooks';
import { compose } from '@/utils';
/**
* Items datatable.
*/
function ItemsDataTable({
// #withItemsActions
setItemsTableState,
// #withDialogAction
openDialog,
// #withAlertsActions
openAlert,
// #withDrawerActions
openDrawer,
// #withSettings
itemsTableSize,
// #ownProps
tableProps,
}) {
// Items list context.
const { items, pagination, isItemsLoading, isEmptyStatus, isItemsFetching } =
useItemsListContext();
// Datatable columns.
const columns = useItemsTableColumns();
// History context.
const history = useHistory();
// Table row class names.
const rowClassNames = (row) => ({
inactive: !row.original.active,
});
// Local storage memorizing columns widths.
const [initialColumnsWidths, , handleColumnResizing] =
useMemorizedColumnsWidths(TABLES.ITEMS);
// Handle fetch data once the page index, size or sort by of the table change.
const handleFetchData = React.useCallback(
({ pageSize, pageIndex, sortBy }) => {
setItemsTableState({
pageIndex,
pageSize,
sortBy,
});
},
[setItemsTableState],
);
// Handle delete action Item.
const handleDeleteItem = ({ id }) => {
openAlert('item-delete', { itemId: id });
};
// Handle cancel/confirm item inactive.
const handleInactiveItem = ({ id }) => {
openAlert('item-inactivate', { itemId: id });
};
// Handle cancel/confirm item activate.
const handleActivateItem = ({ id }) => {
openAlert('item-activate', { itemId: id });
};
// Handle Edit item.
const handleEditItem = ({ id }) => {
history.push(`/items/${id}/edit`);
};
// Handle item make adjustment.
const handleMakeAdjustment = ({ id }) => {
openDialog('inventory-adjustment', { itemId: id });
};
// Display empty status instead of the table.
const handleDuplicate = ({ id }) => {
history.push(`/items/new?duplicate=${id}`, { action: id });
};
// Handle view detail item.
const handleViewDetailItem = ({ id }) => {
openDrawer('item-detail-drawer', { itemId: id });
};
// Cannot continue in case the items has empty status.
if (isEmptyStatus) {
return <ItemsEmptyStatus />;
}
// Handle cell click.
const handleCellClick = (cell, event) => {
openDrawer('item-detail-drawer', { itemId: cell.row.original.id });
};
return (
<DashboardContentTable>
<DataTable
columns={columns}
data={items}
loading={isItemsLoading}
headerLoading={isItemsLoading}
progressBarLoading={isItemsFetching}
noInitialFetch={true}
selectionColumn={true}
spinnerProps={{ size: 30 }}
expandable={false}
sticky={true}
rowClassNames={rowClassNames}
pagination={true}
manualSortBy={true}
manualPagination={true}
pagesCount={pagination.pagesCount}
autoResetSortBy={false}
autoResetPage={true}
TableLoadingRenderer={TableSkeletonRows}
TableHeaderSkeletonRenderer={TableSkeletonHeader}
ContextMenu={ItemsActionMenuList}
onFetchData={handleFetchData}
onCellClick={handleCellClick}
initialColumnsWidths={initialColumnsWidths}
onColumnResizing={handleColumnResizing}
size={itemsTableSize}
payload={{
onDeleteItem: handleDeleteItem,
onEditItem: handleEditItem,
onInactivateItem: handleInactiveItem,
onActivateItem: handleActivateItem,
onMakeAdjustment: handleMakeAdjustment,
onDuplicate: handleDuplicate,
onViewDetails: handleViewDetailItem,
}}
noResults={<T id={'there_is_no_items_in_the_table_yet'} />}
{...tableProps}
/>
</DashboardContentTable>
);
}
export default compose(
withItemsActions,
withAlertsActions,
withDrawerActions,
withDialogActions,
withSettings(({ itemsSettings }) => ({
itemsTableSize: itemsSettings.tableSize,
})),
)(ItemsDataTable);

View File

@@ -0,0 +1,40 @@
// @ts-nocheck
import React from 'react';
import { Button, Intent } from '@blueprintjs/core';
import { useHistory } from 'react-router-dom';
import { Can, FormattedMessage as T, EmptyStatus } from '@/components';
import { ItemAction, AbilitySubject } from '@/constants/abilityOption';
export default function ItemsEmptyStatus() {
const history = useHistory();
return (
<EmptyStatus
title={<T id={'manage_the_organization_s_services_and_products'} />}
description={
<p>
<T id={'here_a_list_of_your_organization_products_and_services'} />
</p>
}
action={
<>
<Can I={ItemAction.Create} a={AbilitySubject.Item}>
<Button
intent={Intent.PRIMARY}
large={true}
onClick={() => {
history.push('/items/new');
}}
>
<T id={'new_item'} />
</Button>
<Button intent={Intent.NONE} large={true}>
<T id={'learn_more'} />
</Button>
</Can>
</>
}
/>
);
}

View File

@@ -0,0 +1,57 @@
// @ts-nocheck
import React from 'react';
import { Intent, Button } from '@blueprintjs/core';
import { FormattedMessage as T } from '@/components';
export default function ItemFloatingFooter({
formik: { isSubmitting },
onSubmitClick,
onCancelClick,
itemDetail,
}) {
return (
<div class="form__floating-footer">
<Button
intent={Intent.PRIMARY}
disabled={isSubmitting}
type="submit"
onClick={() => {
onSubmitClick({ publish: true, redirect: true });
}}
>
{itemDetail && itemDetail.id ? <T id={'edit'} /> : <T id={'save'} />}
</Button>
<Button
disabled={isSubmitting}
intent={Intent.PRIMARY}
className={'ml1'}
name={'save_and_new'}
onClick={() => {
onSubmitClick({ publish: true, redirect: false });
}}
>
<T id={'save_new'} />
</Button>
<Button
className={'ml1'}
disabled={isSubmitting}
onClick={() => {
onSubmitClick({ publish: false, redirect: false });
}}
>
<T id={'save_as_draft'} />
</Button>
<Button
className={'ml1'}
onClick={() => {
onCancelClick && onCancelClick();
}}
>
<T id={'close'} />
</Button>
</div>
);
}

View File

@@ -0,0 +1,57 @@
// @ts-nocheck
import React from 'react';
import { compose } from '@/utils';
import '@/style/pages/Items/List.scss';
import { DashboardPageContent } from '@/components';
import { ItemsListProvider } from './ItemsListProvider';
import ItemsActionsBar from './ItemsActionsBar';
import ItemsViewsTabs from './ItemsViewsTabs';
import ItemsDataTable from './ItemsDataTable';
import withItems from './withItems';
import withItemsActions from './withItemsActions';
/**
* Items list.
*/
function ItemsList({
// #withItems
itemsTableState,
itemsTableStateChanged,
// #withItemsActions
resetItemsTableState,
}) {
// Resets items table query state once the page unmount.
React.useEffect(
() => () => {
resetItemsTableState();
},
[resetItemsTableState],
);
return (
<ItemsListProvider
tableState={itemsTableState}
tableStateChanged={itemsTableStateChanged}
>
<ItemsActionsBar />
<DashboardPageContent>
<ItemsViewsTabs />
<ItemsDataTable />
</DashboardPageContent>
</ItemsListProvider>
);
}
export default compose(
withItemsActions,
withItems(({ itemsTableState, itemsTableStateChanged }) => ({
itemsTableState,
itemsTableStateChanged,
})),
)(ItemsList);

View File

@@ -0,0 +1,75 @@
// @ts-nocheck
import React, { createContext } from 'react';
import { isEmpty } from 'lodash';
import {
getFieldsFromResourceMeta,
transformTableQueryToParams,
} from '@/utils';
import { transformItemsTableState } from './utils';
import { DashboardInsider } from '@/components';
import { useResourceViews, useResourceMeta, useItems } from '@/hooks/query';
const ItemsContext = createContext();
/**
* Items list provider.
*/
function ItemsListProvider({ tableState, tableStateChanged, ...props }) {
const tableQuery = transformItemsTableState(tableState);
// Fetch accounts resource views and fields.
const { data: itemsViews, isLoading: isViewsLoading } =
useResourceViews('items');
// Fetch the accounts resource fields.
const {
data: resourceMeta,
isLoading: isResourceLoading,
isFetching: isResourceFetching,
} = useResourceMeta('items');
// Handle fetching the items table based on the given query.
const {
data: { items, pagination, filterMeta },
isFetching: isItemsFetching,
isLoading: isItemsLoading,
} = useItems(
{
...transformTableQueryToParams(tableQuery),
},
{ keepPreviousData: true },
);
// Detarmines the datatable empty status.
const isEmptyStatus = !tableStateChanged && !isItemsLoading && isEmpty(items);
const state = {
itemsViews,
items,
pagination,
fields: getFieldsFromResourceMeta(resourceMeta.fields),
isViewsLoading,
isItemsLoading,
isItemsFetching: isItemsFetching,
isResourceLoading,
isResourceFetching,
isEmptyStatus,
};
return (
<DashboardInsider
loading={isViewsLoading || isResourceLoading}
name={'items-list'}
>
<ItemsContext.Provider value={state} {...props} />
</DashboardInsider>
);
}
const useItemsListContext = () => React.useContext(ItemsContext);
export { ItemsListProvider, useItemsListContext };

View File

@@ -0,0 +1,57 @@
// @ts-nocheck
import intl from 'react-intl-universal';
import withDrawerActions from '@/containers/Drawer/withDrawerActions';
import { RESOURCES_TYPES } from '@/constants/resourcesTypes';
import { AbilitySubject, ItemAction } from '@/constants/abilityOption';
/**
* Item univrsal search item select action.
*/
function ItemUniversalSearchSelectComponent({
// #ownProps
resourceType,
resourceId,
onAction,
// #withDrawerActions
openDrawer,
}) {
if (resourceType === RESOURCES_TYPES.ITEM) {
openDrawer('item-detail-drawer', { itemId: resourceId });
onAction && onAction();
}
return null;
}
export const ItemUniversalSearchSelectAction = withDrawerActions(
ItemUniversalSearchSelectComponent,
);
/**
* Transformes items to search.
* @param {*} item
* @returns
*/
const transfromItemsToSearch = (item) => ({
id: item.id,
text: item.name,
subText: item.code,
label: item.type_formatted,
reference: item,
});
/**
* Binds universal search invoice configure.
*/
export const universalSearchItemBind = () => ({
resourceType: RESOURCES_TYPES.ITEM,
optionItemLabel: intl.get('items'),
selectItemAction: ItemUniversalSearchSelectAction,
itemSelect: transfromItemsToSearch,
permission: {
ability: ItemAction.View,
subject: AbilitySubject.Item,
},
});

View File

@@ -0,0 +1,53 @@
// @ts-nocheck
import React from 'react';
import { Alignment, Navbar, NavbarGroup } from '@blueprintjs/core';
import { DashboardViewsTabs } from '@/components';
import { withRouter } from 'react-router-dom';
import withItems from './withItems';
import withItemsActions from './withItemsActions';
import { useItemsListContext } from './ItemsListProvider';
import { compose, transfromViewsToTabs } from '@/utils';
/**
* Items views tabs.
*/
function ItemsViewsTabs({
// #withItemsActions
setItemsTableState,
// #withItems
itemsCurrentView,
}) {
const { itemsViews } = useItemsListContext();
// Mapped items views.
const tabs = transfromViewsToTabs(itemsViews);
// Handles the active tab change.
const handleTabChange = (viewSlug) => {
setItemsTableState({ viewSlug });
};
return (
<Navbar className="navbar--dashboard-views">
<NavbarGroup align={Alignment.LEFT}>
<DashboardViewsTabs
currentViewSlug={itemsCurrentView}
resourceName={'items'}
tabs={tabs}
onChange={handleTabChange}
/>
</NavbarGroup>
</Navbar>
);
}
export default compose(
withRouter,
withItems(({ itemsTableState }) => ({
itemsCurrentView: itemsTableState?.viewSlug,
})),
withItemsActions,
)(ItemsViewsTabs);

View File

@@ -0,0 +1,234 @@
// @ts-nocheck
import React from 'react';
import intl from 'react-intl-universal';
import { isNumber } from 'lodash';
import {
Menu,
MenuDivider,
MenuItem,
Intent,
Tag,
Position,
Button,
Popover,
} from '@blueprintjs/core';
import { FormattedMessage as T, Icon, Money, If, Can } from '@/components';
import { isBlank, safeCallback } from '@/utils';
import {
AbilitySubject,
ItemAction,
InventoryAdjustmentAction,
} from '@/constants/abilityOption';
/**
* Publish accessor
*/
export const PublishAccessor = (r) => {
return r.is_published ? (
<Tag minimal={true}>
<T id={'published'} />
</Tag>
) : (
<Tag minimal={true} intent={Intent.WARNING}>
<T id={'draft'} />
</Tag>
);
};
export const TypeAccessor = (row) => {
return row.type ? (
<Tag minimal={true} round={true} intent={Intent.NONE}>
{intl.get(row.type)}
</Tag>
) : (
''
);
};
export const ItemCodeAccessor = (row) =>
row.type ? (
<Tag minimal={true} round={true} intent={Intent.NONE}>
{intl.get(row.type)}
</Tag>
) : (
''
);
export const QuantityOnHandCell = ({ cell: { value } }) => {
return isNumber(value) ? (
<span className={value < 0 ? 'quantity_on_hand' : null}>{value}</span>
) : null;
};
export const CostPriceCell = ({ cell: { value } }) => {
return !isBlank(value) ? <Money amount={value} currency={'USD'} /> : null;
};
export const SellPriceCell = ({ cell: { value } }) => {
return !isBlank(value) ? <Money amount={value} currency={'USD'} /> : null;
};
export const ItemTypeAccessor = (row) => {
return row.type_formatted ? (
<Tag minimal={true} round={true} intent={Intent.NONE}>
{row.type_formatted}
</Tag>
) : null;
};
export function ItemsActionMenuList({
row: { original },
payload: {
onEditItem,
onInactivateItem,
onActivateItem,
onMakeAdjustment,
onDeleteItem,
onDuplicate,
onViewDetails,
},
}) {
return (
<Menu>
<MenuItem
icon={<Icon icon="reader-18" />}
text={<T id={'view_details'} />}
onClick={safeCallback(onViewDetails, original)}
/>
<Can I={ItemAction.Edit} a={AbilitySubject.Item}>
<MenuDivider />
<MenuItem
icon={<Icon icon="pen-18" />}
text={intl.get('edit_item')}
onClick={safeCallback(onEditItem, original)}
/>
</Can>
<Can I={ItemAction.Create} a={AbilitySubject.Item}>
<MenuItem
icon={<Icon icon="duplicate-16" />}
text={intl.get('duplicate')}
onClick={safeCallback(onDuplicate, original)}
/>
</Can>
<Can I={ItemAction.Edit} a={AbilitySubject.Item}>
<If condition={original.active}>
<MenuItem
text={intl.get('inactivate_item')}
icon={<Icon icon="pause-16" iconSize={16} />}
onClick={safeCallback(onInactivateItem, original)}
/>
</If>
<If condition={!original.active}>
<MenuItem
text={intl.get('activate_item')}
icon={<Icon icon="play-16" iconSize={16} />}
onClick={safeCallback(onActivateItem, original)}
/>
</If>
</Can>
<Can
I={InventoryAdjustmentAction.Edit}
a={AbilitySubject.InventoryAdjustment}
>
<If condition={original.type === 'inventory'}>
<MenuItem
text={intl.get('make_adjustment')}
icon={<Icon icon={'swap-vert'} iconSize={16} />}
onClick={safeCallback(onMakeAdjustment, original)}
/>
</If>
</Can>
<Can I={ItemAction.Delete} a={AbilitySubject.Item}>
<MenuDivider />
<MenuItem
text={intl.get('delete_item')}
icon={<Icon icon="trash-16" iconSize={16} />}
onClick={safeCallback(onDeleteItem, original)}
intent={Intent.DANGER}
/>
</Can>
</Menu>
);
}
export const ItemsActionsTableCell = (props) => {
return (
<Popover
position={Position.RIGHT_BOTTOM}
content={<ItemsActionMenuList {...props} />}
>
<Button icon={<Icon icon="more-h-16" iconSize={16} />} />
</Popover>
);
};
/**
* Retrieve all items table columns.
*/
export const useItemsTableColumns = () => {
return React.useMemo(
() => [
{
id: 'name',
Header: intl.get('item_name'),
accessor: 'name',
className: 'name',
width: 180,
clickable: true,
textOverview: true,
},
{
id: 'code',
Header: intl.get('item_code'),
accessor: 'code',
className: 'code',
width: 120,
clickable: true,
},
{
id: 'type',
Header: intl.get('item_type'),
accessor: ItemTypeAccessor,
className: 'item_type',
width: 120,
clickable: true,
},
{
id: 'category',
Header: intl.get('category'),
accessor: 'category.name',
className: 'category',
width: 150,
clickable: true,
textOverview: true,
},
{
id: 'sell_price',
Header: intl.get('sell_price'),
accessor: 'sell_price_formatted',
align: 'right',
width: 150,
clickable: true,
},
{
id: 'cost_price',
Header: intl.get('cost_price'),
accessor: 'cost_price_formatted',
align: 'right',
width: 150,
clickable: true,
},
{
id: 'quantity_on_hand',
Header: intl.get('quantity_on_hand'),
accessor: 'quantity_on_hand',
Cell: QuantityOnHandCell,
align: 'right',
width: 140,
clickable: true,
},
],
[],
);
};

View File

@@ -0,0 +1,251 @@
// @ts-nocheck
import { useMemo } from 'react';
import intl from 'react-intl-universal';
import { Intent } from '@blueprintjs/core';
import { defaultTo, includes } from 'lodash';
import { useHistory } from 'react-router-dom';
import { AppToaster } from '@/components';
import {
transformToForm,
transformTableStateToQuery,
defaultFastFieldShouldUpdate,
} from '@/utils';
import { useSettingsSelector } from '@/hooks/state';
import { transformItemFormData } from './ItemForm.schema';
import { useWatch } from '@/hooks/utils';
const defaultInitialValues = {
active: 1,
name: '',
type: 'service',
code: '',
cost_price: '',
sell_price: '',
cost_account_id: '',
sell_account_id: '',
inventory_account_id: '',
category_id: '',
sellable: 1,
purchasable: true,
sell_description: '',
purchase_description: '',
};
/**
* Initial values in create and edit mode.
*/
export const useItemFormInitialValues = (item, initialValues) => {
const { items: itemsSettings } = useSettingsSelector();
return useMemo(
() => ({
...defaultInitialValues,
cost_account_id: defaultTo(itemsSettings?.preferredCostAccount, ''),
sell_account_id: defaultTo(itemsSettings?.preferredSellAccount, ''),
inventory_account_id: defaultTo(
itemsSettings?.preferredInventoryAccount,
'',
),
/**
* We only care about the fields in the form. Previously unfilled optional
* values such as `notes` come back from the API as null, so remove those
* as well.
*/
...transformToForm(
transformItemFormData(item, defaultInitialValues),
defaultInitialValues,
),
...initialValues,
}),
[item, itemsSettings, initialValues],
);
};
export const transitionItemTypeKeyToLabel = (itemTypeKey) => {
const table = {
service: intl.get('service'),
inventory: intl.get('inventory'),
};
return typeof table[itemTypeKey] === 'string' ? table[itemTypeKey] : '';
};
// handle delete errors.
export const handleDeleteErrors = (errors) => {
if (
errors.find((error) => error.type === 'ITEM_HAS_ASSOCIATED_TRANSACTINS')
) {
AppToaster.show({
message: intl.get('the_item_has_associated_transactions'),
intent: Intent.DANGER,
});
}
if (
errors.find(
(error) => error.type === 'ITEM_HAS_ASSOCIATED_INVENTORY_ADJUSTMENT',
)
) {
AppToaster.show({
message: intl.get(
'you_could_not_delete_item_that_has_associated_inventory_adjustments_transacions',
),
intent: Intent.DANGER,
});
}
if (
errors.find(
(error) => error.type === 'TYPE_CANNOT_CHANGE_WITH_ITEM_HAS_TRANSACTIONS',
)
) {
AppToaster.show({
message: intl.get(
'cannot_change_item_type_to_inventory_with_item_has_associated_transactions',
),
intent: Intent.DANGER,
});
}
if (
errors.find((error) => error.type === 'ITEM_HAS_ASSOCIATED_TRANSACTIONS')
) {
AppToaster.show({
message: intl.get('item.error.you_could_not_delete_item_has_associated'),
intent: Intent.DANGER,
});
}
};
/**
* Detarmines accounts fast field should update.
*/
export const accountsFieldShouldUpdate = (newProps, oldProps) => {
return (
newProps.accounts !== oldProps.accounts ||
defaultFastFieldShouldUpdate(newProps, oldProps)
);
};
/**
* Detarmines categories fast field should update.
*/
export const categoriesFieldShouldUpdate = (newProps, oldProps) => {
return (
newProps.categories !== oldProps.categories ||
defaultFastFieldShouldUpdate(newProps, oldProps)
);
};
/**
* Sell price fast field should update.
*/
export const sellPriceFieldShouldUpdate = (newProps, oldProps) => {
return (
newProps.sellable !== oldProps.sellable ||
defaultFastFieldShouldUpdate(newProps, oldProps)
);
};
/**
* Sell account fast field should update.
*/
export const sellAccountFieldShouldUpdate = (newProps, oldProps) => {
return (
newProps.accounts !== oldProps.accounts ||
newProps.sellable !== oldProps.sellable ||
defaultFastFieldShouldUpdate(newProps, oldProps)
);
};
/**
* Sell description fast field should update.
*/
export const sellDescriptionFieldShouldUpdate = (newProps, oldProps) => {
return (
newProps.sellable !== oldProps.sellable ||
defaultFastFieldShouldUpdate(newProps, oldProps)
);
};
export const costAccountFieldShouldUpdate = (newProps, oldProps) => {
return (
newProps.accounts !== oldProps.accounts ||
newProps.purchasable !== oldProps.purchasable ||
defaultFastFieldShouldUpdate(newProps, oldProps)
);
};
export const costPriceFieldShouldUpdate = (newProps, oldProps) => {
return (
newProps.purchasable !== oldProps.purchasable ||
defaultFastFieldShouldUpdate(newProps, oldProps)
);
};
export const purchaseDescFieldShouldUpdate = (newProps, oldProps) => {
return (
newProps.purchasable !== oldProps.purchasable ||
defaultFastFieldShouldUpdate(newProps, oldProps)
);
};
export function transformItemsTableState(tableState) {
return {
...transformTableStateToQuery(tableState),
inactive_mode: tableState.inactiveMode,
};
}
/**
* Transform API errors.
*/
export const transformSubmitRequestErrors = (error) => {
const {
response: {
data: { errors },
},
} = error;
const fields = {};
if (errors.find((e) => e.type === 'ITEM.NAME.ALREADY.EXISTS')) {
fields.name = intl.get('the_name_used_before');
}
if (errors.find((e) => e.type === 'INVENTORY_ACCOUNT_CANNOT_MODIFIED')) {
AppToaster.show({
message: intl.get('cannot_change_item_inventory_account'),
intent: Intent.DANGER,
});
}
if (
errors.find(
(e) => e.type === 'TYPE_CANNOT_CHANGE_WITH_ITEM_HAS_TRANSACTIONS',
)
) {
AppToaster.show({
message: intl.get(
'item.error.type_cannot_change_with_item_has_transactions',
),
intent: Intent.DANGER,
});
}
return fields;
};
/**
* Watches and handles item response not found error.
* @param {*} itemQuery
*/
export function useWatchItemError(itemQuery) {
const { error, isError } = itemQuery;
// History context.
const history = useHistory();
useWatch(() => {
if (isError && includes([400, 404], error.response.status)) {
AppToaster.show({
message: 'The given item not found.',
intent: Intent.DANGER,
});
history.push('/items');
}
}, isError);
}

View File

@@ -0,0 +1,14 @@
// @ts-nocheck
import { connect } from 'react-redux';
import { getItemById } from '@/store/items/items.reducer';
export default (mapState) => {
const mapStateToProps = (state, props) => {
const mapped = {
item: getItemById(state, props.itemId),
};
return mapState ? mapState(mapped, state, props) : mapped;
};
return connect(mapStateToProps);
};

View File

@@ -0,0 +1,22 @@
// @ts-nocheck
import {connect} from 'react-redux';
import {
getItemsTableStateFactory,
isItemsTableStateChangedFactory,
} from '@/store/items/items.selectors';
export default (mapState) => {
const getItemsTableState = getItemsTableStateFactory();
const isItemsTableStateChanged = isItemsTableStateChangedFactory();
const mapStateToProps = (state, props) => {
const mapped = {
itemsSelectedRows: state.items.selectedRows,
itemsTableState: getItemsTableState(state, props),
itemsTableStateChanged: isItemsTableStateChanged(state, props),
};
return mapState ? mapState(mapped, state, props) : mapped;
};
return connect(mapStateToProps);
};

View File

@@ -0,0 +1,13 @@
// @ts-nocheck
import { connect } from 'react-redux';
import {
setItemsTableState,
resetItemsTableState,
} from '@/store/items/items.actions';
export const mapDispatchToProps = (dispatch) => ({
setItemsTableState: (queries) => dispatch(setItemsTableState(queries)),
resetItemsTableState: () => dispatch(resetItemsTableState()),
});
export default connect(null, mapDispatchToProps);