diff --git a/client/src/containers/Items/ItemForm.js b/client/src/containers/Items/ItemForm.js index fa41d397d..1576aee72 100644 --- a/client/src/containers/Items/ItemForm.js +++ b/client/src/containers/Items/ItemForm.js @@ -6,7 +6,7 @@ import { } from '@blueprintjs/core'; import { queryCache } from 'react-query'; import { useHistory } from 'react-router-dom'; -import { pick } from 'lodash'; +import { pick, pickBy } from 'lodash'; import { useIntl } from 'react-intl'; import classNames from 'classnames'; @@ -23,7 +23,24 @@ import useMedia from 'hooks/useMedia'; import withItemDetail from 'containers/Items/withItemDetail'; import withDashboardActions from 'containers/Dashboard/withDashboardActions'; -import { compose } from 'utils'; +import { compose, transformToForm } from 'utils'; + +const defaultInitialValues = { + active: true, + name: '', + type: 'service', + sku: '', + cost_price: '', + sell_price: '', + cost_account_id: '', + sell_account_id: '', + inventory_account_id: '', + category_id: '', + note: '', + sellable: true, + purchasable: true, +}; + /** * Item form. @@ -71,54 +88,60 @@ function ItemForm({ .required() .label(formatMessage({ id: 'item_type_' })), sku: Yup.string().trim(), - cost_price: Yup.number(), - sell_price: Yup.number(), + cost_price: Yup.number() + .when(['purchasable'], { + is: true, + then: Yup.number().required(), + otherwise: Yup.number().nullable(true), + }), + sell_price: Yup.number() + .when(['sellable'], { + is: true, + then: Yup.number().required(), + otherwise: Yup.number().nullable(true), + }), cost_account_id: Yup.number() - .required() + .when(['purchasable'], { + is: true, + then: Yup.number().required(), + otherwise: Yup.number().nullable(true), + }) .label(formatMessage({ id: 'cost_account_id' })), sell_account_id: Yup.number() - .required() + .when(['sellable'], { + is: true, + then: Yup.number().required(), + otherwise: Yup.number().nullable(), + }) .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(), + inventory_account_id: Yup.number() + .when(['type'], { + is: (value) => value === 'inventory', + then: Yup.number().required(), + otherwise: Yup.number().nullable(), + }) + .label(formatMessage({ id: 'inventory_account' })), + category_id: Yup.number().positive().nullable(), stock: Yup.string() || Yup.boolean(), sellable: Yup.boolean().required(), purchasable: Yup.boolean().required(), }); - const defaultInitialValues = useMemo( - () => ({ - active: true, - name: '', - type: 'service', - sku: '', - cost_price: 0, - sell_price: 0, - cost_account_id: null, - sell_account_id: null, - inventory_account_id: null, - category_id: null, - note: '', - sellable: true, - purchasable: true, - }), - [], - ); + /** + * Initial values in create and edit mode. + */ const initialValues = useMemo( () => ({ - ...(itemDetail - ? { - ...pick(itemDetail, Object.keys(defaultInitialValues)), - } - : { - ...defaultInitialValues, - }), + ...defaultInitialValues, + + /** + * 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(itemDetail, defaultInitialValues), }), - [itemDetail, defaultInitialValues], + [], ); useEffect(() => { @@ -127,6 +150,14 @@ function ItemForm({ : changePageTitle(formatMessage({ id: 'new_item' })); }, [changePageTitle, isNewMode, formatMessage]); + const transformApiErrors = (errors) => { + const fields = {}; + if (errors.find(e => e.type === 'ITEM.NAME.ALREADY.EXISTS')) { + fields.name = formatMessage({ id: 'the_name_used_before' }) + } + return fields; + } + // Handles the form submit. const handleFormSubmit = (values, { setSubmitting, resetForm, setErrors }) => { setSubmitting(true); @@ -141,7 +172,7 @@ function ItemForm({ 'the_item_has_been_successfully_edited', }, { - number: itemDetail.id, + number: itemId, }, ), intent: Intent.SUCCESS, @@ -150,13 +181,18 @@ function ItemForm({ history.push('/items'); queryCache.removeQueries(['items-table']); }; - const onError = (response) => { + const onError = ({ response }) => { setSubmitting(false); + + if (response.data.errors) { + const _errors = transformApiErrors(response.data.errors); + setErrors({ ..._errors }); + } }; if (isNewMode) { requestSubmitItem(form).then(onSuccess).catch(onError); } else { - requestEditItem(form).then(onSuccess).catch(onError); + requestEditItem(itemId, form).then(onSuccess).catch(onError); } }; @@ -181,7 +217,7 @@ function ItemForm({ } else { changePageSubtitle(''); } - }, [values.item_type]); + }, [values.item_type, changePageSubtitle, formatMessage]); const initialAttachmentFiles = useMemo(() => { return itemDetail && itemDetail.media @@ -217,24 +253,24 @@ function ItemForm({
diff --git a/client/src/containers/Items/ItemFormBody.js b/client/src/containers/Items/ItemFormBody.js index d1e5aa4a4..c9b07d671 100644 --- a/client/src/containers/Items/ItemFormBody.js +++ b/client/src/containers/Items/ItemFormBody.js @@ -36,15 +36,15 @@ function ItemFormBody({
- {/*------------- Sellable checkbox ------------- */} - + } + label={ +

+ +

+ } checked={values.sellable} {...getFieldProps('sellable')} /> @@ -54,26 +54,22 @@ function ItemFormBody({ } className={'form-group--item-selling-price'} - intent={ - errors.selling_price && touched.selling_price && Intent.DANGER - } + intent={errors.sell_price && touched.sell_price && Intent.DANGER} helperText={ - + } inline={true} > { - setFieldValue('selling_price', value); + setFieldValue('sell_price', value); }} inputGroupProps={{ medium: true, intent: - errors.selling_price && - touched.selling_price && - Intent.DANGER, + errors.sell_price && touched.sell_price && Intent.DANGER, }} disabled={!values.sellable} /> @@ -107,18 +103,18 @@ function ItemFormBody({ filterByTypes={['income']} /> - {/*------------- Purchasable checbox ------------- */} - + } + label={ +

+ +

+ } defaultChecked={values.purchasable} {...getFieldProps('purchasable')} /> @@ -169,7 +165,7 @@ function ItemFormBody({ { - setFieldValue('cost_account_id', account.id) + setFieldValue('cost_account_id', account.id); }} defaultSelectText={} selectedAccountId={values.cost_account_id} @@ -187,4 +183,4 @@ export default compose( withAccounts(({ accountsList }) => ({ accountsList, })), -)(ItemFormBody); \ No newline at end of file +)(ItemFormBody); diff --git a/client/src/containers/Items/ItemFormInventorySection.js b/client/src/containers/Items/ItemFormInventorySection.js index ab6db5888..6321e7200 100644 --- a/client/src/containers/Items/ItemFormInventorySection.js +++ b/client/src/containers/Items/ItemFormInventorySection.js @@ -1,11 +1,17 @@ import React from 'react'; -import { FormGroup, Intent, InputGroup, Classes } from '@blueprintjs/core'; -import { AccountsSelectList, ErrorMessage, Col, Row } from 'components'; +import { + FormGroup, + Intent, + InputGroup, + Position, +} from '@blueprintjs/core'; +import { DateInput } from '@blueprintjs/datetime'; +import { AccountsSelectList, ErrorMessage, Col, Row, Hint } from 'components'; +import { CLASSES } from 'common/classes'; import { FormattedMessage as T } from 'react-intl'; import classNames from 'classnames'; import withAccounts from 'containers/Accounts/withAccounts'; - -import { compose } from 'utils'; +import { compose, tansformDateValue, momentFormatter } from 'utils'; /** * Item form inventory sections. @@ -21,12 +27,12 @@ function ItemFormInventorySection({ }) { return (
+

+ +

+ -

- -

- {/*------------- Inventory account ------------- */} } @@ -45,7 +51,7 @@ function ItemFormInventorySection({ className={classNames( 'form-group--item-inventory_account', 'form-group--select-list', - Classes.FILL, + CLASSES.FILL, )} > } - className={'form-group--item-stock'} + label={} + labelInfo={} + className={'form-group--opening_quantity'} inline={true} > } + labelInfo={} + className={classNames( + 'form-group--select-list', + 'form-group--opening_date', + CLASSES.FILL, + )} + inline={true} + > + { + setFieldValue('opening_date', value); + }} + popoverProps={{ position: Position.BOTTOM, minimal: true }} + /> + + + + + } + className={'form-group--opening_average_rate'} inline={true} > diff --git a/client/src/containers/Items/ItemFormPrimarySection.js b/client/src/containers/Items/ItemFormPrimarySection.js index 99957e731..5ed70a709 100644 --- a/client/src/containers/Items/ItemFormPrimarySection.js +++ b/client/src/containers/Items/ItemFormPrimarySection.js @@ -9,7 +9,7 @@ import { Position, Tooltip, } from '@blueprintjs/core'; -import { FormattedMessage as T, useIntl } from 'react-intl'; +import { FormattedMessage as T } from 'react-intl'; import { CategoriesSelectList, ErrorMessage, @@ -39,6 +39,14 @@ function ItemFormPrimarySection({ // #withItemCategories categoriesList, }) { + + const itemTypeHintContent = ( + <> +
{'Service: '}{'Services that you provide to customers. '}
+
{'Inventory: '}{'Products you buy and/or sell and that you track quantities of.'}
+
{'Non-Inventory: '}{'Products you buy and/or sell but don’t need to (or can’t) track quantities of, for example, nuts and bolts used in an installation.'}
+ ); + return (
@@ -47,7 +55,12 @@ function ItemFormPrimarySection({ } - labelInfo={} + labelInfo={ + + + + + } className={'form-group--item-type'} intent={errors.type && touched.type && Intent.DANGER} helperText={} @@ -56,49 +69,21 @@ function ItemFormPrimarySection({ { - setFieldValue('item_type', value); + setFieldValue('type', value); })} - selectedValue={values.item_type} + selectedValue={values.type} > - - - } + label={} value="service" /> - - - } + label={} value="inventory" /> - - - } - value="non_inventory" + label={} + value="non-inventory" /> @@ -121,7 +106,7 @@ function ItemFormPrimarySection({ {/*----------- SKU ----------*/} } + label={} labelInfo={} className={'form-group--item-sku'} intent={errors.sku && touched.sku && Intent.DANGER} diff --git a/client/src/containers/Items/ItemsDataTable.js b/client/src/containers/Items/ItemsDataTable.js index 98e4c1c90..30e8b675a 100644 --- a/client/src/containers/Items/ItemsDataTable.js +++ b/client/src/containers/Items/ItemsDataTable.js @@ -7,6 +7,7 @@ import { MenuDivider, Position, Intent, + Tag, } from '@blueprintjs/core'; import { FormattedMessage as T, useIntl } from 'react-intl'; import { Icon, DataTable, Money, If, Choose } from 'components'; @@ -15,6 +16,7 @@ import LoadingIndicator from 'components/LoadingIndicator'; import withItems from 'containers/Items/withItems'; import { compose } from 'utils'; + const ItemsDataTable = ({ loading, @@ -87,43 +89,58 @@ const ItemsDataTable = ({ { Header: formatMessage({ id: 'item_name' }), accessor: 'name', - className: 'actions', + className: 'name', + width: 180, }, { - Header: formatMessage({ id: 'sku' }), + Header: formatMessage({ id: 'item_code' }), accessor: 'sku', className: 'sku', + width: 120, + }, + { + Header: formatMessage({ id: 'item_type' }), + accessor: (row) => + row.type ? ( + + {formatMessage({ id: row.type })} + + ) : ( + '' + ), + className: 'item_type', + width: 120, }, { Header: formatMessage({ id: 'category' }), accessor: 'category.name', className: 'category', + width: 150, }, { Header: formatMessage({ id: 'sell_price' }), accessor: (row) => , className: 'sell-price', + width: 150, }, { Header: formatMessage({ id: 'cost_price' }), accessor: (row) => , className: 'cost-price', + width: 150, + }, + { + Header: formatMessage({ id: 'quantity_on_hand' }), + accessor: 'quantity_on_hand', + className: 'quantity_on_hand', + width: 140, + }, + { + Header: formatMessage({ id: 'average_rate' }), + accessor: 'average_cost_rate', + className: 'average_cost_rate', + width: 140, }, - // { - // Header: 'Cost Account', - // accessor: 'cost_account.name', - // className: "cost-account", - // }, - // { - // Header: 'Sell Account', - // accessor: 'sell_account.name', - // className: "sell-account", - // }, - // { - // Header: 'Inventory Account', - // accessor: 'inventory_account.name', - // className: "inventory-account", - // }, { id: 'actions', Cell: ({ cell }) => ( diff --git a/client/src/containers/Items/ItemsList.js b/client/src/containers/Items/ItemsList.js index 68184c796..6f034cf9b 100644 --- a/client/src/containers/Items/ItemsList.js +++ b/client/src/containers/Items/ItemsList.js @@ -97,6 +97,16 @@ function ItemsList({ intent: Intent.SUCCESS, }); setDeleteItem(false); + }).catch(({ errors }) => { + if (errors.find(error => error.type === 'ITEM_HAS_ASSOCIATED_TRANSACTINS')) { + AppToaster.show({ + message: formatMessage({ + id: 'the_item_has_associated_transactions', + }), + intent: Intent.DANGER, + }); + } + setDeleteItem(false); }); }, [requestDeleteItem, deleteItem, formatMessage]); @@ -189,8 +199,6 @@ function ItemsList({ return ( { resolve(response); }) .catch((error) => { - reject(error); + reject(error?.response?.data); }); }); }; diff --git a/client/src/style/pages/items.scss b/client/src/style/pages/items.scss index 4cf87aa39..3ee60621e 100644 --- a/client/src/style/pages/items.scss +++ b/client/src/style/pages/items.scss @@ -81,56 +81,21 @@ } -// .item-form{ -// padding: 22px; -// padding-bottom: 90px; -// &__primary-section{ -// background-color: #FAFAFA; -// padding: 40px 22px 22px; -// margin: -22px -22px 22px; -// background-color: #FAFAFA; -// } -// &__accounts-section{ -// h4{ -// margin: 0; -// font-weight: 500; -// margin-bottom: 20px; -// color: #828282; -// } -// > div:first-of-type{ -// padding-right: 15px !important; -// } +.dashboard__insider--items-list{ -// > div ~ div{ -// padding-left: 15px !important; -// border-left: 1px solid #e8e8e8; -// } -// } -// .#{$ns}-form-group{ -// .#{$ns}-label{ -// width: 130px; -// } + .bigcapital-datatable{ -// .#{$ns}-form-content{ -// width: 250px; -// } -// } + .table{ + .tbody{ + .item_type.td{ -// .form-group--item-type, -// .form-group--item-name{ - -// .#{$ns}-form-content{ -// width: 350px; -// } -// } - -// .form-group--active{ -// margin-bottom: 5px; - -// .bp3-control.bp3-checkbox{ -// margin: 0; -// } -// } -// } \ No newline at end of file + .bp3-tag{ + font-size: 13px; + } + } + } + } + } +} \ No newline at end of file diff --git a/client/src/utils.js b/client/src/utils.js index 7c2b2f7fa..8abe4c145 100644 --- a/client/src/utils.js +++ b/client/src/utils.js @@ -277,4 +277,11 @@ export const itemsStartWith = (items, char) => { export const saveInvoke = (func, ...rest) => { return func && func(...rest); +} + +export const transformToForm = (obj, emptyInitialValues) => { + return _.pickBy( + obj, + (val, key) => val !== null && Object.keys(emptyInitialValues).includes(key), + ) } \ No newline at end of file