mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-16 04:40:32 +00:00
557 lines
16 KiB
JavaScript
557 lines
16 KiB
JavaScript
import React, { useState, useMemo, useCallback } from 'react';
|
|
import * as Yup from 'yup';
|
|
import { useFormik } from 'formik';
|
|
import {
|
|
FormGroup,
|
|
MenuItem,
|
|
Intent,
|
|
InputGroup,
|
|
HTMLSelect,
|
|
Button,
|
|
Classes,
|
|
Checkbox,
|
|
} from '@blueprintjs/core';
|
|
import { Row, Col } from 'react-grid-system';
|
|
import { FormattedMessage as T, useIntl } from 'react-intl';
|
|
import { Select } from '@blueprintjs/select';
|
|
|
|
import AppToaster from 'components/AppToaster';
|
|
import AccountsConnect from 'connectors/Accounts.connector';
|
|
import ItemsConnect from 'connectors/Items.connect';
|
|
import { compose } from 'utils';
|
|
import ErrorMessage from 'components/ErrorMessage';
|
|
import classNames from 'classnames';
|
|
import Icon from 'components/Icon';
|
|
import ItemCategoryConnect from 'connectors/ItemsCategory.connect';
|
|
import MoneyInputGroup from 'components/MoneyInputGroup';
|
|
import { useHistory } from 'react-router-dom';
|
|
import Dragzone from 'components/Dragzone';
|
|
import MediaConnect from 'connectors/Media.connect';
|
|
import useMedia from 'hooks/useMedia';
|
|
|
|
|
|
const ItemForm = ({
|
|
requestSubmitItem,
|
|
|
|
accounts,
|
|
categories,
|
|
|
|
requestSubmitMedia,
|
|
requestDeleteMedia,
|
|
}) => {
|
|
const [selectedAccounts, setSelectedAccounts] = useState({});
|
|
const history = useHistory();
|
|
const { formatMessage } = useIntl();
|
|
|
|
const {
|
|
files,
|
|
setFiles,
|
|
saveMedia,
|
|
deletedFiles,
|
|
setDeletedFiles,
|
|
deleteMedia,
|
|
} = useMedia({
|
|
saveCallback: requestSubmitMedia,
|
|
deleteCallback: requestDeleteMedia,
|
|
});
|
|
|
|
const ItemTypeDisplay = useMemo(
|
|
() => [
|
|
{ value: null, label: formatMessage({id:'select_item_type'}) },
|
|
{ value: 'service', label: formatMessage({id:'service'}) },
|
|
{ value: 'inventory', label: formatMessage({id:'inventory'}) },
|
|
{ value: 'non-inventory', label: formatMessage({id:'non_inventory'}) },
|
|
],
|
|
[]
|
|
);
|
|
|
|
const validationSchema = Yup.object().shape({
|
|
active: Yup.boolean(),
|
|
name: Yup.string().required().label(formatMessage({id:'item_name_'})),
|
|
type: Yup.string().trim().required().label(formatMessage({id:'item_type_'})),
|
|
sku: Yup.string().trim(),
|
|
cost_price: Yup.number(),
|
|
sell_price: Yup.number(),
|
|
cost_account_id: Yup.number().required().label(formatMessage({id:'cost_account_id'})),
|
|
sell_account_id: Yup.number().required().label(formatMessage({id:'sell_account_id'})),
|
|
inventory_account_id: Yup.number().when('type', {
|
|
is: (value) => value === 'inventory',
|
|
then: Yup.number().required(),
|
|
otherwise: Yup.number().nullable(),
|
|
}),
|
|
category_id: Yup.number().nullable(),
|
|
stock: Yup.string() || Yup.boolean(),
|
|
});
|
|
|
|
const initialValues = useMemo(
|
|
() => ({
|
|
active: true,
|
|
name: '',
|
|
type: '',
|
|
sku: '',
|
|
cost_price: 0,
|
|
sell_price: 0,
|
|
cost_account_id: null,
|
|
sell_account_id: null,
|
|
inventory_account_id: null,
|
|
category_id: null,
|
|
note: '',
|
|
}),
|
|
[]
|
|
);
|
|
|
|
const {
|
|
getFieldProps,
|
|
setFieldValue,
|
|
values,
|
|
touched,
|
|
errors,
|
|
handleSubmit,
|
|
isSubmitting,
|
|
} = useFormik({
|
|
enableReinitialize: true,
|
|
validationSchema: validationSchema,
|
|
initialValues: {
|
|
...initialValues,
|
|
},
|
|
onSubmit: (values, { setSubmitting }) => {
|
|
const saveItem = (mediaIds) => {
|
|
const formValues = { ...values, media_ids: mediaIds };
|
|
|
|
return requestSubmitItem(formValues).then((response) => {
|
|
AppToaster.show({
|
|
message: formatMessage({
|
|
id: 'service_has_been_successful_created',
|
|
}, {
|
|
name: values.name,
|
|
service: formatMessage({ id: 'item' }),
|
|
}),
|
|
intent: Intent.SUCCESS,
|
|
});
|
|
});
|
|
};
|
|
|
|
Promise.all([saveMedia(), deleteMedia()]).then(
|
|
([savedMediaResponses]) => {
|
|
const mediaIds = savedMediaResponses.map((res) => res.data.media.id);
|
|
return saveItem(mediaIds);
|
|
}
|
|
);
|
|
},
|
|
});
|
|
|
|
const accountItem = useCallback(
|
|
(item, { handleClick }) => (
|
|
<MenuItem
|
|
key={item.id}
|
|
text={item.name}
|
|
label={item.code}
|
|
onClick={handleClick}
|
|
/>
|
|
),
|
|
[]
|
|
);
|
|
|
|
// Filter Account Items
|
|
const filterAccounts = (query, account, _index, exactMatch) => {
|
|
const normalizedTitle = account.name.toLowerCase();
|
|
const normalizedQuery = query.toLowerCase();
|
|
if (exactMatch) {
|
|
return normalizedTitle === normalizedQuery;
|
|
} else {
|
|
return `${account.code} ${normalizedTitle}`.indexOf(normalizedQuery) >= 0;
|
|
}
|
|
};
|
|
|
|
const onItemAccountSelect = useCallback(
|
|
(filedName) => {
|
|
return (account) => {
|
|
setSelectedAccounts({
|
|
...selectedAccounts,
|
|
[filedName]: account,
|
|
});
|
|
setFieldValue(filedName, account.id);
|
|
};
|
|
},
|
|
[setFieldValue, selectedAccounts]
|
|
);
|
|
|
|
const categoryItem = useCallback(
|
|
(item, { handleClick }) => (
|
|
<MenuItem text={item.name} onClick={handleClick} />
|
|
),
|
|
[]
|
|
);
|
|
|
|
const getSelectedAccountLabel = useCallback(
|
|
(fieldName, defaultLabel) => {
|
|
return typeof selectedAccounts[fieldName] !== 'undefined'
|
|
? selectedAccounts[fieldName].name
|
|
: defaultLabel;
|
|
},
|
|
[selectedAccounts]
|
|
);
|
|
|
|
const requiredSpan = useMemo(() => <span class='required'>*</span>, []);
|
|
const infoIcon = useMemo(() => <Icon icon='info-circle' iconSize={12} />, []);
|
|
|
|
const handleMoneyInputChange = (fieldKey) => (e, value) => {
|
|
setFieldValue(fieldKey, value);
|
|
};
|
|
|
|
const initialAttachmentFiles = useMemo(() => {
|
|
return [];
|
|
}, []);
|
|
|
|
const handleDropFiles = useCallback((_files) => {
|
|
setFiles(_files.filter((file) => file.uploaded === false));
|
|
}, []);
|
|
|
|
const handleDeleteFile = useCallback(
|
|
(_deletedFiles) => {
|
|
_deletedFiles.forEach((deletedFile) => {
|
|
if (deletedFile.uploaded && deletedFile.metadata.id) {
|
|
setDeletedFiles([...deletedFiles, deletedFile.metadata.id]);
|
|
}
|
|
});
|
|
},
|
|
[setDeletedFiles, deletedFiles]
|
|
);
|
|
|
|
const handleCancelClickBtn = () => {
|
|
history.goBack();
|
|
};
|
|
|
|
return (
|
|
<div class='item-form'>
|
|
<form onSubmit={handleSubmit}>
|
|
<div class='item-form__primary-section'>
|
|
<Row>
|
|
<Col xs={7}>
|
|
<FormGroup
|
|
medium={true}
|
|
label={<T id={'item_type'} />}
|
|
labelInfo={requiredSpan}
|
|
className={'form-group--item-type'}
|
|
intent={errors.type && touched.type && Intent.DANGER}
|
|
helperText={
|
|
<ErrorMessage {...{ errors, touched }} name='type' />
|
|
}
|
|
inline={true}
|
|
>
|
|
<HTMLSelect
|
|
fill={true}
|
|
options={ItemTypeDisplay}
|
|
{...getFieldProps('type')}
|
|
/>
|
|
</FormGroup>
|
|
|
|
<FormGroup
|
|
label={<T id={'item_name'} />}
|
|
labelInfo={requiredSpan}
|
|
className={'form-group--item-name'}
|
|
intent={errors.name && touched.name && Intent.DANGER}
|
|
helperText={
|
|
<ErrorMessage {...{ errors, touched }} name='name' />
|
|
}
|
|
inline={true}
|
|
>
|
|
<InputGroup
|
|
medium={true}
|
|
intent={errors.name && touched.name && Intent.DANGER}
|
|
{...getFieldProps('name')}
|
|
/>
|
|
</FormGroup>
|
|
|
|
<FormGroup
|
|
label={<T id={'sku'} />}
|
|
labelInfo={infoIcon}
|
|
className={'form-group--item-sku'}
|
|
intent={errors.sku && touched.sku && Intent.DANGER}
|
|
helperText={
|
|
<ErrorMessage {...{ errors, touched }} name='sku' />
|
|
}
|
|
inline={true}
|
|
>
|
|
<InputGroup
|
|
medium={true}
|
|
intent={errors.sku && touched.sku && Intent.DANGER}
|
|
{...getFieldProps('sku')}
|
|
/>
|
|
</FormGroup>
|
|
|
|
<FormGroup
|
|
label={<T id={'category'} />}
|
|
labelInfo={infoIcon}
|
|
inline={true}
|
|
intent={
|
|
errors.category_id && touched.category_id && Intent.DANGER
|
|
}
|
|
helperText={
|
|
<ErrorMessage {...{ errors, touched }} name='category' />
|
|
}
|
|
className={classNames(
|
|
'form-group--select-list',
|
|
'form-group--category',
|
|
Classes.FILL
|
|
)}
|
|
>
|
|
<Select
|
|
items={categories}
|
|
itemRenderer={categoryItem}
|
|
itemPredicate={filterAccounts}
|
|
popoverProps={{ minimal: true }}
|
|
onItemSelect={onItemAccountSelect('category_id')}
|
|
>
|
|
<Button
|
|
fill={true}
|
|
rightIcon='caret-down'
|
|
text={getSelectedAccountLabel(
|
|
'category_id',
|
|
formatMessage({id:'select_category'})
|
|
)}
|
|
/>
|
|
</Select>
|
|
</FormGroup>
|
|
|
|
<FormGroup
|
|
label={' '}
|
|
inline={true}
|
|
className={'form-group--active'}
|
|
>
|
|
<Checkbox
|
|
inline={true}
|
|
label={<T id={'active'}/>}
|
|
defaultChecked={values.active}
|
|
{...getFieldProps('active')}
|
|
/>
|
|
</FormGroup>
|
|
</Col>
|
|
|
|
<Col xs={3}>
|
|
<Dragzone
|
|
initialFiles={initialAttachmentFiles}
|
|
onDrop={handleDropFiles}
|
|
onDeleteFile={handleDeleteFile}
|
|
hint={'Attachments: Maxiumum size: 20MB'}
|
|
className={'mt2'}
|
|
/>
|
|
</Col>
|
|
</Row>
|
|
</div>
|
|
|
|
<Row gutterWidth={16} className={'item-form__accounts-section'}>
|
|
<Col width={404}>
|
|
<h4><T id={'purchase_information'}/></h4>
|
|
|
|
<FormGroup
|
|
label={<T id={'selling_price'}/>}
|
|
className={'form-group--item-selling-price'}
|
|
intent={
|
|
errors.selling_price && touched.selling_price && Intent.DANGER
|
|
}
|
|
helperText={
|
|
<ErrorMessage {...{ errors, touched }} name='selling_price' />
|
|
}
|
|
inline={true}
|
|
>
|
|
<MoneyInputGroup
|
|
value={values.selling_price}
|
|
prefix={'$'}
|
|
onChange={handleMoneyInputChange('selling_price')}
|
|
inputGroupProps={{
|
|
medium: true,
|
|
intent:
|
|
errors.selling_price &&
|
|
touched.selling_price &&
|
|
Intent.DANGER,
|
|
}}
|
|
/>
|
|
</FormGroup>
|
|
|
|
<FormGroup
|
|
label={<T id={'account'} />}
|
|
labelInfo={infoIcon}
|
|
inline={true}
|
|
intent={
|
|
errors.sell_account_id &&
|
|
touched.sell_account_id &&
|
|
Intent.DANGER
|
|
}
|
|
helperText={
|
|
<ErrorMessage {...{ errors, touched }} name='sell_account_id' />
|
|
}
|
|
className={classNames(
|
|
'form-group--sell-account',
|
|
'form-group--select-list',
|
|
Classes.FILL
|
|
)}
|
|
>
|
|
<Select
|
|
items={accounts}
|
|
itemRenderer={accountItem}
|
|
itemPredicate={filterAccounts}
|
|
popoverProps={{ minimal: true }}
|
|
onItemSelect={onItemAccountSelect('sell_account_id')}
|
|
>
|
|
<Button
|
|
fill={true}
|
|
rightIcon='caret-down'
|
|
text={getSelectedAccountLabel(
|
|
'sell_account_id',
|
|
formatMessage({id:'select_account'})
|
|
)}
|
|
/>
|
|
</Select>
|
|
</FormGroup>
|
|
</Col>
|
|
|
|
<Col width={404}>
|
|
<h4>
|
|
<T id={'sales_information'} />
|
|
</h4>
|
|
|
|
<FormGroup
|
|
label={<T id={'cost_price'} />}
|
|
className={'form-group--item-cost-price'}
|
|
intent={errors.cost_price && touched.cost_price && Intent.DANGER}
|
|
helperText={
|
|
<ErrorMessage {...{ errors, touched }} name='cost_price' />
|
|
}
|
|
inline={true}
|
|
>
|
|
<MoneyInputGroup
|
|
value={values.cost_price}
|
|
prefix={'$'}
|
|
onChange={handleMoneyInputChange('cost_price')}
|
|
inputGroupProps={{
|
|
medium: true,
|
|
intent:
|
|
errors.cost_price && touched.cost_price && Intent.DANGER,
|
|
}}
|
|
/>
|
|
</FormGroup>
|
|
|
|
<FormGroup
|
|
label={<T id={'account'} />}
|
|
labelInfo={infoIcon}
|
|
inline={true}
|
|
intent={
|
|
errors.cost_account_id &&
|
|
touched.cost_account_id &&
|
|
Intent.DANGER
|
|
}
|
|
helperText={
|
|
<ErrorMessage {...{ errors, touched }} name='cost_account_id' />
|
|
}
|
|
className={classNames(
|
|
'form-group--cost-account',
|
|
'form-group--select-list',
|
|
Classes.FILL
|
|
)}
|
|
>
|
|
<Select
|
|
items={accounts}
|
|
itemRenderer={accountItem}
|
|
itemPredicate={filterAccounts}
|
|
popoverProps={{ minimal: true }}
|
|
onItemSelect={onItemAccountSelect('cost_account_id')}
|
|
>
|
|
<Button
|
|
fill={true}
|
|
rightIcon='caret-down'
|
|
text={getSelectedAccountLabel(
|
|
'cost_account_id',
|
|
formatMessage({id:'select_account'})
|
|
|
|
)}
|
|
/>
|
|
</Select>
|
|
</FormGroup>
|
|
</Col>
|
|
</Row>
|
|
|
|
<Row className={'item-form__accounts-section mt2'}>
|
|
<Col width={404}>
|
|
<h4>
|
|
<T id={'inventory_information'} />
|
|
</h4>
|
|
|
|
<FormGroup
|
|
label={<T id={'inventory_account'}/>}
|
|
inline={true}
|
|
intent={
|
|
errors.inventory_account_id &&
|
|
touched.inventory_account_id &&
|
|
Intent.DANGER
|
|
}
|
|
helperText={
|
|
<ErrorMessage
|
|
{...{ errors, touched }}
|
|
name='inventory_account_id'
|
|
/>
|
|
}
|
|
className={classNames(
|
|
'form-group--item-inventory_account',
|
|
'form-group--select-list',
|
|
Classes.FILL
|
|
)}
|
|
>
|
|
<Select
|
|
items={accounts}
|
|
itemRenderer={accountItem}
|
|
itemPredicate={filterAccounts}
|
|
popoverProps={{ minimal: true }}
|
|
onItemSelect={onItemAccountSelect('inventory_account_id')}
|
|
>
|
|
<Button
|
|
fill={true}
|
|
rightIcon='caret-down'
|
|
text={getSelectedAccountLabel(
|
|
'inventory_account_id',
|
|
formatMessage({id:'select_account'})
|
|
|
|
)}
|
|
/>
|
|
</Select>
|
|
</FormGroup>
|
|
|
|
<FormGroup
|
|
label={<T id={'opening_stock'}/>}
|
|
className={'form-group--item-stock'}
|
|
inline={true}
|
|
>
|
|
<InputGroup
|
|
medium={true}
|
|
intent={errors.stock && Intent.DANGER}
|
|
{...getFieldProps('stock')}
|
|
/>
|
|
</FormGroup>
|
|
</Col>
|
|
</Row>
|
|
|
|
<div class='form__floating-footer'>
|
|
<Button intent={Intent.PRIMARY} disabled={isSubmitting} type='submit'>
|
|
<T id={'save'}/>
|
|
</Button>
|
|
|
|
<Button className={'ml1'} disabled={isSubmitting}>
|
|
<T id={'save_as_draft'}/>
|
|
</Button>
|
|
|
|
<Button className={'ml1'} onClick={handleCancelClickBtn}>
|
|
<T id={'close'} />
|
|
</Button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default compose(
|
|
AccountsConnect,
|
|
ItemsConnect,
|
|
ItemCategoryConnect,
|
|
MediaConnect
|
|
)(ItemForm);
|