mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-17 05:10:31 +00:00
re-structure to monorepo.
This commit is contained in:
78
packages/webapp/src/containers/Items/ItemForm.schema.tsx
Normal file
78
packages/webapp/src/containers/Items/ItemForm.schema.tsx
Normal 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;
|
||||
94
packages/webapp/src/containers/Items/ItemForm.tsx
Normal file
94
packages/webapp/src/containers/Items/ItemForm.tsx
Normal 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;
|
||||
}
|
||||
}
|
||||
`;
|
||||
268
packages/webapp/src/containers/Items/ItemFormBody.tsx
Normal file
268
packages/webapp/src/containers/Items/ItemFormBody.tsx
Normal 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);
|
||||
@@ -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;
|
||||
`;
|
||||
112
packages/webapp/src/containers/Items/ItemFormFormik.tsx
Normal file
112
packages/webapp/src/containers/Items/ItemFormFormik.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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);
|
||||
15
packages/webapp/src/containers/Items/ItemFormPage.tsx
Normal file
15
packages/webapp/src/containers/Items/ItemFormPage.tsx
Normal 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} />;
|
||||
}
|
||||
172
packages/webapp/src/containers/Items/ItemFormPrimarySection.tsx
Normal file
172
packages/webapp/src/containers/Items/ItemFormPrimarySection.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
90
packages/webapp/src/containers/Items/ItemFormProvider.tsx
Normal file
90
packages/webapp/src/containers/Items/ItemFormProvider.tsx
Normal 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 };
|
||||
191
packages/webapp/src/containers/Items/ItemsActionsBar.tsx
Normal file
191
packages/webapp/src/containers/Items/ItemsActionsBar.tsx
Normal 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);
|
||||
47
packages/webapp/src/containers/Items/ItemsAlerts.tsx
Normal file
47
packages/webapp/src/containers/Items/ItemsAlerts.tsx
Normal 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,
|
||||
},
|
||||
];
|
||||
176
packages/webapp/src/containers/Items/ItemsDataTable.tsx
Normal file
176
packages/webapp/src/containers/Items/ItemsDataTable.tsx
Normal 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);
|
||||
40
packages/webapp/src/containers/Items/ItemsEmptyStatus.tsx
Normal file
40
packages/webapp/src/containers/Items/ItemsEmptyStatus.tsx
Normal 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>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
57
packages/webapp/src/containers/Items/ItemsFooter.tsx
Normal file
57
packages/webapp/src/containers/Items/ItemsFooter.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
57
packages/webapp/src/containers/Items/ItemsList.tsx
Normal file
57
packages/webapp/src/containers/Items/ItemsList.tsx
Normal 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);
|
||||
75
packages/webapp/src/containers/Items/ItemsListProvider.tsx
Normal file
75
packages/webapp/src/containers/Items/ItemsListProvider.tsx
Normal 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 };
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
53
packages/webapp/src/containers/Items/ItemsViewsTabs.tsx
Normal file
53
packages/webapp/src/containers/Items/ItemsViewsTabs.tsx
Normal 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);
|
||||
234
packages/webapp/src/containers/Items/components.tsx
Normal file
234
packages/webapp/src/containers/Items/components.tsx
Normal 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,
|
||||
},
|
||||
],
|
||||
[],
|
||||
);
|
||||
};
|
||||
251
packages/webapp/src/containers/Items/utils.tsx
Normal file
251
packages/webapp/src/containers/Items/utils.tsx
Normal 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);
|
||||
}
|
||||
14
packages/webapp/src/containers/Items/withItem.tsx
Normal file
14
packages/webapp/src/containers/Items/withItem.tsx
Normal 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);
|
||||
};
|
||||
22
packages/webapp/src/containers/Items/withItems.tsx
Normal file
22
packages/webapp/src/containers/Items/withItems.tsx
Normal 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);
|
||||
};
|
||||
13
packages/webapp/src/containers/Items/withItemsActions.tsx
Normal file
13
packages/webapp/src/containers/Items/withItemsActions.tsx
Normal 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);
|
||||
Reference in New Issue
Block a user