From 93fbe1fc98d5902eedc9603055eefd88f279a959 Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Tue, 10 Nov 2020 17:41:53 +0200 Subject: [PATCH 1/3] feat: fix item form in edit mode. --- client/src/containers/Items/ItemForm.js | 103 +++++++++++------- client/src/containers/Items/ItemFormBody.js | 42 ++++--- .../Items/ItemFormInventorySection.js | 64 ++++++++--- .../Items/ItemFormPrimarySection.js | 55 ++++------ client/src/lang/en/index.js | 4 +- client/src/utils.js | 7 ++ 6 files changed, 159 insertions(+), 116 deletions(-) diff --git a/client/src/containers/Items/ItemForm.js b/client/src/containers/Items/ItemForm.js index fa41d397d..13553ae4b 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,23 @@ 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 +87,61 @@ 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(() => { @@ -141,7 +164,7 @@ function ItemForm({ 'the_item_has_been_successfully_edited', }, { - number: itemDetail.id, + number: itemId, }, ), intent: Intent.SUCCESS, @@ -156,7 +179,7 @@ function ItemForm({ if (isNewMode) { requestSubmitItem(form).then(onSuccess).catch(onError); } else { - requestEditItem(form).then(onSuccess).catch(onError); + requestEditItem(itemId, form).then(onSuccess).catch(onError); } }; 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..f5843cbce 100644 --- a/client/src/containers/Items/ItemFormPrimarySection.js +++ b/client/src/containers/Items/ItemFormPrimarySection.js @@ -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,48 +69,20 @@ function ItemFormPrimarySection({ { - setFieldValue('item_type', value); + setFieldValue('type', value); })} - selectedValue={values.item_type} + selectedValue={values.type} > - - - } + label={} value="service" /> - - - } + label={} value="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/lang/en/index.js b/client/src/lang/en/index.js index d6fbc28f7..f719a047e 100644 --- a/client/src/lang/en/index.js +++ b/client/src/lang/en/index.js @@ -100,7 +100,7 @@ export default { cost_price: 'Cost Price', inventory_information: 'Inventory Information', inventory_account: 'Inventory Account', - opening_stock: 'Opening Stock', + opening_quantity: 'Opening quantity', save: 'Save', save_as_draft: 'Save as Draft', active: 'Active', @@ -810,4 +810,6 @@ export default { i_purchase_this_item: 'I purchase this item from a vendor.', i_sell_this_item: 'I sell this item to a customer.', select_display_name_as:'Select display name as', + opening_date: 'Opening date', + item_code: 'Item code', }; 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 From 94b97af3a989352f16a548508f085fef46d7784d Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Tue, 10 Nov 2020 19:49:30 +0200 Subject: [PATCH 2/3] fix: edit item form. --- client/src/containers/Items/ItemForm.js | 31 +++++++--- .../Items/ItemFormPrimarySection.js | 4 +- client/src/containers/Items/ItemsDataTable.js | 51 ++++++++++------ client/src/containers/Items/ItemsList.js | 2 - client/src/lang/en/index.js | 3 + client/src/style/pages/items.scss | 61 ++++--------------- 6 files changed, 74 insertions(+), 78 deletions(-) diff --git a/client/src/containers/Items/ItemForm.js b/client/src/containers/Items/ItemForm.js index 13553ae4b..1576aee72 100644 --- a/client/src/containers/Items/ItemForm.js +++ b/client/src/containers/Items/ItemForm.js @@ -41,6 +41,7 @@ const defaultInitialValues = { purchasable: true, }; + /** * Item form. */ @@ -119,14 +120,13 @@ function ItemForm({ then: Yup.number().required(), otherwise: Yup.number().nullable(), }) - .label(formatMessage({ id: 'Inventory account' })), + .label(formatMessage({ id: 'inventory_account' })), category_id: Yup.number().positive().nullable(), stock: Yup.string() || Yup.boolean(), sellable: Yup.boolean().required(), purchasable: Yup.boolean().required(), }); - /** * Initial values in create and edit mode. */ @@ -150,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); @@ -173,8 +181,13 @@ 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); @@ -204,7 +217,7 @@ function ItemForm({ } else { changePageSubtitle(''); } - }, [values.item_type]); + }, [values.item_type, changePageSubtitle, formatMessage]); const initialAttachmentFiles = useMemo(() => { return itemDetail && itemDetail.media @@ -240,24 +253,24 @@ function ItemForm({
diff --git a/client/src/containers/Items/ItemFormPrimarySection.js b/client/src/containers/Items/ItemFormPrimarySection.js index f5843cbce..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, @@ -83,7 +83,7 @@ function ItemFormPrimarySection({ /> } - value="non_inventory" + value="non-inventory" /> 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..32844816d 100644 --- a/client/src/containers/Items/ItemsList.js +++ b/client/src/containers/Items/ItemsList.js @@ -189,8 +189,6 @@ function ItemsList({ return ( 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 From aabd42a7a1d3e9b93d3b94c4d106ffad10ffcde5 Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Tue, 10 Nov 2020 20:32:59 +0200 Subject: [PATCH 3/3] fix: handle delete items error. --- client/src/containers/Items/ItemsList.js | 10 ++++++++++ client/src/lang/en/index.js | 3 ++- client/src/store/items/items.actions.js | 2 +- 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/client/src/containers/Items/ItemsList.js b/client/src/containers/Items/ItemsList.js index 32844816d..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]); diff --git a/client/src/lang/en/index.js b/client/src/lang/en/index.js index ae209dffe..3b4bffaf8 100644 --- a/client/src/lang/en/index.js +++ b/client/src/lang/en/index.js @@ -814,5 +814,6 @@ export default { item_code: 'Item code', quantity_on_hand: 'Quantity on hand', average_rate: 'Average rate', - the_name_used_before: 'The name is already used.' + the_name_used_before: 'The name is already used.', + the_item_has_associated_transactions: 'The item has associated transactions.' }; diff --git a/client/src/store/items/items.actions.js b/client/src/store/items/items.actions.js index bbffc11fd..20e634bb4 100644 --- a/client/src/store/items/items.actions.js +++ b/client/src/store/items/items.actions.js @@ -80,7 +80,7 @@ export const deleteItem = ({ id }) => { resolve(response); }) .catch((error) => { - reject(error); + reject(error?.response?.data); }); }); };