diff --git a/client/src/common/classes.js b/client/src/common/classes.js index dca1f9ad9..213774aed 100644 --- a/client/src/common/classes.js +++ b/client/src/common/classes.js @@ -11,6 +11,7 @@ const CLASSES = { PAGE_FORM_HEADER_FIELDS: 'page-form__header-fields', PAGE_FORM_HEADER_BIG_NUMBERS: 'page-form__big-numbers', PAGE_FORM_TABS: 'page-form__tabs', + PAGE_FORM_BODY: 'page-form__body', PAGE_FORM_FOOTER: 'page-form__footer', PAGE_FORM_FLOATING_ACTIONS: 'page-form__floating-actions', @@ -22,6 +23,7 @@ const CLASSES = { PAGE_FORM_PAYMENT_MADE: 'page-form--payment-made', PAGE_FORM_PAYMENT_RECEIVE: 'page-form--payment-receive', PAGE_FORM_CUSTOMER: 'page-form--customer', + PAGE_FORM_ITEM: 'page-form--item', FORM_GROUP_LIST_SELECT: 'form-group--select-list', diff --git a/client/src/containers/Items/ItemForm.js b/client/src/containers/Items/ItemForm.js index 74fd054ed..fa41d397d 100644 --- a/client/src/containers/Items/ItemForm.js +++ b/client/src/containers/Items/ItemForm.js @@ -1,69 +1,55 @@ -import React, { useState, useMemo, useCallback, useEffect } from 'react'; +import React, { useMemo, useCallback, useEffect } from 'react'; import * as Yup from 'yup'; -import { useFormik, Formik } from 'formik'; +import { useFormik } from 'formik'; import { - FormGroup, - MenuItem, - Intent, - InputGroup, - HTMLSelect, - Button, - Classes, - Checkbox, + Intent } from '@blueprintjs/core'; -import { Row, Col } from 'react-grid-system'; -import { FormattedMessage as T, useIntl } from 'react-intl'; import { queryCache } from 'react-query'; import { useHistory } from 'react-router-dom'; import { pick } from 'lodash'; +import { useIntl } from 'react-intl'; import classNames from 'classnames'; +import { CLASSES } from 'common/classes'; import AppToaster from 'components/AppToaster'; -import ErrorMessage from 'components/ErrorMessage'; -import Icon from 'components/Icon'; -import MoneyInputGroup from 'components/MoneyInputGroup'; -import Dragzone from 'components/Dragzone'; -import { - ListSelect, - AccountsSelectList, - CategoriesSelectList, -} from 'components'; +import ItemFormPrimarySection from './ItemFormPrimarySection'; +import ItemFormBody from './ItemFormBody'; +import ItemFormFloatingActions from './ItemFormFloatingActions'; +import ItemFormInventorySection from './ItemFormInventorySection'; import withItemsActions from 'containers/Items/withItemsActions'; -import withItemCategories from 'containers/Items/withItemCategories'; -import withAccounts from 'containers/Accounts/withAccounts'; import withMediaActions from 'containers/Media/withMediaActions'; import useMedia from 'hooks/useMedia'; import withItemDetail from 'containers/Items/withItemDetail'; import withDashboardActions from 'containers/Dashboard/withDashboardActions'; -import withAccountDetail from 'containers/Accounts/withAccountDetail'; import { compose } from 'utils'; -const ItemForm = ({ +/** + * Item form. + */ +function ItemForm({ // #withItemActions requestSubmitItem, requestEditItem, - accountsList, + itemId, itemDetail, onFormSubmit, - onCancelForm, // #withDashboardActions changePageTitle, - - // #withItemCategories - categoriesList, + changePageSubtitle, // #withMediaActions requestSubmitMedia, requestDeleteMedia, -}) => { - const [payload, setPayload] = useState({}); +}) { + const isNewMode = !itemId; const history = useHistory(); const { formatMessage } = useIntl(); + const { setFiles, saveMedia, @@ -75,16 +61,6 @@ const ItemForm = ({ 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' }) }, - ], - [formatMessage], - ); - const validationSchema = Yup.object().shape({ active: Yup.boolean(), name: Yup.string() @@ -118,7 +94,7 @@ const ItemForm = ({ () => ({ active: true, name: '', - type: '', + type: 'service', sku: '', cost_price: 0, sell_price: 0, @@ -127,8 +103,8 @@ const ItemForm = ({ inventory_account_id: null, category_id: null, note: '', - sellable: null, - purchasable: null, + sellable: true, + purchasable: true, }), [], ); @@ -145,18 +121,44 @@ const ItemForm = ({ [itemDetail, defaultInitialValues], ); - const saveInvokeSubmit = useCallback( - (payload) => { - onFormSubmit && onFormSubmit(payload); - }, - [onFormSubmit], - ); - useEffect(() => { - itemDetail && itemDetail.id + (!isNewMode) ? changePageTitle(formatMessage({ id: 'edit_item_details' })) : changePageTitle(formatMessage({ id: 'new_item' })); - }, [changePageTitle, itemDetail, formatMessage]); + }, [changePageTitle, isNewMode, formatMessage]); + + // Handles the form submit. + const handleFormSubmit = (values, { setSubmitting, resetForm, setErrors }) => { + setSubmitting(true); + const form = { ...values }; + + const onSuccess = (response) => { + AppToaster.show({ + message: formatMessage( + { + id: (isNewMode) ? + 'service_has_been_successful_created' : + 'the_item_has_been_successfully_edited', + }, + { + number: itemDetail.id, + }, + ), + intent: Intent.SUCCESS, + }); + setSubmitting(false); + history.push('/items'); + queryCache.removeQueries(['items-table']); + }; + const onError = (response) => { + setSubmitting(false); + }; + if (isNewMode) { + requestSubmitItem(form).then(onSuccess).catch(onError); + } else { + requestEditItem(form).then(onSuccess).catch(onError); + } + }; const { getFieldProps, @@ -169,78 +171,17 @@ const ItemForm = ({ } = useFormik({ enableReinitialize: true, validationSchema: validationSchema, - initialValues: { - ...initialValues, - }, - onSubmit: (values, { setSubmitting, resetForm, setErrors }) => { - const saveItem = (mediaIds) => { - const formValues = { ...values }; - if (itemDetail && itemDetail.id) { - requestEditItem(itemDetail.id, formValues) - .then((response) => { - AppToaster.show({ - message: formatMessage( - { - id: 'the_item_has_been_successfully_edited', - }, - { - number: itemDetail.id, - }, - ), - intent: Intent.SUCCESS, - }); - setSubmitting(false); - saveInvokeSubmit({ action: 'update', ...payload }); - history.push('/items'); - resetForm(); - }) - .catch((errors) => { - setSubmitting(false); - }); - } else { - requestSubmitItem(formValues).then((response) => { - AppToaster.show({ - message: formatMessage( - { - id: 'service_has_been_successful_created', - }, - { - name: values.name, - service: formatMessage({ id: 'item' }), - }, - ), - intent: Intent.SUCCESS, - }); - queryCache.removeQueries(['items-table']); - history.push('/items'); - }); - } - }; - - Promise.all([saveMedia(), deleteMedia()]).then( - ([savedMediaResponses]) => { - const mediaIds = savedMediaResponses.map((res) => res.data.media.id); - return saveItem(mediaIds); - }, - ); - }, + initialValues, + onSubmit: handleFormSubmit }); - const onItemAccountSelect = useCallback( - (filedName) => { - return (account) => { - setFieldValue(filedName, account.id); - }; - }, - [setFieldValue], - ); - - const requiredSpan = useMemo(() => *, []); - const infoIcon = useMemo(() => , []); - - const handleMoneyInputChange = (fieldKey) => (e, value) => { - setFieldValue(fieldKey, value); - }; + useEffect(() => { + if (values.item_type) { + changePageSubtitle(formatMessage({ id: values.item_type })); + } else { + changePageSubtitle(''); + } + }, [values.item_type]); const initialAttachmentFiles = useMemo(() => { return itemDetail && itemDetail.media @@ -251,9 +192,10 @@ const ItemForm = ({ })) : []; }, [itemDetail]); + const handleDropFiles = useCallback((_files) => { setFiles(_files.filter((file) => file.uploaded === false)); - }, []); + }, [setFiles]); const handleDeleteFile = useCallback( (_deletedFiles) => { @@ -266,349 +208,49 @@ const ItemForm = ({ [setDeletedFiles, deletedFiles], ); - const handleCancelClickBtn = () => { + const handleCancelBtnClick = useCallback(() => { history.goBack(); - }; + }, [history]); return ( -
+
-
- - - {/* Item type */} - } - labelInfo={requiredSpan} - className={'form-group--item-type'} - intent={errors.type && touched.type && Intent.DANGER} - helperText={ - - } - inline={true} - > - - - - {/* Item name */} - } - labelInfo={requiredSpan} - className={'form-group--item-name'} - intent={errors.name && touched.name && Intent.DANGER} - helperText={ - - } - inline={true} - > - - - - {/* SKU */} - } - labelInfo={infoIcon} - className={'form-group--item-sku'} - intent={errors.sku && touched.sku && Intent.DANGER} - helperText={ - - } - inline={true} - > - - - - {/* Item category */} - } - labelInfo={infoIcon} - inline={true} - intent={ - errors.category_id && touched.category_id && Intent.DANGER - } - helperText={ - - } - className={classNames( - 'form-group--select-list', - 'form-group--category', - Classes.FILL, - )} - > - - - - {/* Active checkbox */} - - } - defaultChecked={values.active} - {...getFieldProps('active')} - /> - - - - - - - -
- - - -

- -

- - } - className={'form-group--item-selling-price'} - intent={ - errors.selling_price && touched.selling_price && Intent.DANGER - } - helperText={ - - } - inline={true} - > - - - - {/* Selling account */} - } - labelInfo={infoIcon} - inline={true} - intent={ - errors.sell_account_id && - touched.sell_account_id && - Intent.DANGER - } - helperText={ - - } - className={classNames( - 'form-group--sell-account', - 'form-group--select-list', - Classes.FILL, - )} - > - } - selectedAccountId={values.sell_account_id} - disabled={!values.sellable} - /> - - - {/* sellable checkbox */} - - } - checked={values.sellable} - {...getFieldProps('sellable')} - /> - - - - -

- -

- - {/* Cost price */} - } - className={'form-group--item-cost-price'} - intent={errors.cost_price && touched.cost_price && Intent.DANGER} - helperText={ - - } - inline={true} - > - - - - } - labelInfo={infoIcon} - inline={true} - intent={ - errors.cost_account_id && - touched.cost_account_id && - Intent.DANGER - } - helperText={ - - } - className={classNames( - 'form-group--cost-account', - 'form-group--select-list', - Classes.FILL, - )} - > - } - selectedAccountId={values.cost_account_id} - disabled={!values.purchasable} - /> - - - {/* purchasable checkbox */} - - } - defaultChecked={values.purchasable} - {...getFieldProps('purchasable')} - /> - - -
- - - -

- -

- - } - inline={true} - intent={ - errors.inventory_account_id && - touched.inventory_account_id && - Intent.DANGER - } - helperText={ - - } - className={classNames( - 'form-group--item-inventory_account', - 'form-group--select-list', - Classes.FILL, - )} - > - } - selectedAccountId={values.inventory_account_id} - /> - - - } - className={'form-group--item-stock'} - inline={true} - > - - - -
- -
); }; export default compose( - withAccounts(({ accountsList }) => ({ - accountsList, - })), - withAccountDetail, withItemsActions, withItemDetail, - withItemCategories(({ categoriesList }) => ({ - categoriesList, - })), withDashboardActions, withMediaActions, )(ItemForm); diff --git a/client/src/containers/Items/ItemFormBody.js b/client/src/containers/Items/ItemFormBody.js new file mode 100644 index 000000000..3bf6b1832 --- /dev/null +++ b/client/src/containers/Items/ItemFormBody.js @@ -0,0 +1,188 @@ +import React from 'react'; +import { + FormGroup, + Intent, + InputGroup, + Classes, + Checkbox, +} from '@blueprintjs/core'; +import { + AccountsSelectList, + MoneyInputGroup, + ErrorMessage, + Col, + Row, + Hint, +} from 'components'; +import { FormattedMessage as T } from 'react-intl'; +import classNames from 'classnames'; +import withAccounts from 'containers/Accounts/withAccounts'; + +import { compose } from 'utils'; + +/** + * Item form body. + */ +function ItemFormBody({ + getFieldProps, + touched, + errors, + values, + setFieldValue, + + accountsList, +}) { + return ( +
+ + + + {/*------------- Sellable checkbox ------------- */} + + } + checked={values.sellable} + {...getFieldProps('sellable')} + /> + + + {/*------------- Selling price ------------- */} + } + className={'form-group--item-selling-price'} + intent={ + errors.selling_price && touched.selling_price && Intent.DANGER + } + helperText={ + + } + inline={true} + > + { + setFieldValue('selling_price', value); + }} + inputGroupProps={{ + medium: true, + intent: + errors.selling_price && + touched.selling_price && + Intent.DANGER, + }} + disabled={!values.sellable} + /> + + + {/*------------- Selling account ------------- */} + } + labelInfo={} + inline={true} + intent={ + errors.sell_account_id && touched.sell_account_id && Intent.DANGER + } + helperText={ + + } + className={classNames( + 'form-group--sell-account', + 'form-group--select-list', + Classes.FILL, + )} + > + { + setFieldValue('sell_account_id', account.id); + }} + defaultSelectText={} + selectedAccountId={values.sell_account_id} + disabled={!values.sellable} + /> + + + + + + {/*------------- Purchasable checbox ------------- */} + + } + defaultChecked={values.purchasable} + {...getFieldProps('purchasable')} + /> + + + {/*------------- Cost price ------------- */} + } + className={'form-group--item-cost-price'} + intent={errors.cost_price && touched.cost_price && Intent.DANGER} + helperText={ + + } + inline={true} + > + { + setFieldValue('cost_price', value); + }} + inputGroupProps={{ + medium: true, + intent: + errors.cost_price && touched.cost_price && Intent.DANGER, + }} + disabled={!values.purchasable} + /> + + + {/*------------- Cost account ------------- */} + } + labelInfo={} + inline={true} + intent={ + errors.cost_account_id && touched.cost_account_id && Intent.DANGER + } + helperText={ + + } + className={classNames( + 'form-group--cost-account', + 'form-group--select-list', + Classes.FILL, + )} + > + { + setFieldValue('cost_account_id', account.id) + }} + defaultSelectText={} + selectedAccountId={values.cost_account_id} + disabled={!values.purchasable} + /> + + + +
+ ); +} + +export default compose( + withAccounts(({ accountsList }) => ({ + accountsList, + })), +)(ItemFormBody); \ No newline at end of file diff --git a/client/src/containers/Items/ItemFormFloatingActions.js b/client/src/containers/Items/ItemFormFloatingActions.js new file mode 100644 index 000000000..82297b60d --- /dev/null +++ b/client/src/containers/Items/ItemFormFloatingActions.js @@ -0,0 +1,54 @@ +import React from 'react'; +import { + Button, + Intent, + FormGroup, + Checkbox +} from '@blueprintjs/core'; +import { FormattedMessage as T } from 'react-intl'; +import { saveInvoke } from 'utils'; +import classNames from 'classnames'; + +import { CLASSES } from 'common/classes'; + +/** + * Item form floating actions. + */ +export default function ItemFormFloatingActions({ + isSubmitting, + itemId, + onCancelClick +}) { + const handleCancelBtnClick = (event) => { + saveInvoke(onCancelClick, event.currentTarget.value); + }; + return ( +
+ + + + + + + {/*----------- Active ----------*/} + + } + // defaultChecked={values.active} + // {...getFieldProps('active')} + /> + +
+ ); +} diff --git a/client/src/containers/Items/ItemFormInventorySection.js b/client/src/containers/Items/ItemFormInventorySection.js new file mode 100644 index 000000000..ab6db5888 --- /dev/null +++ b/client/src/containers/Items/ItemFormInventorySection.js @@ -0,0 +1,94 @@ +import React from 'react'; +import { FormGroup, Intent, InputGroup, Classes } from '@blueprintjs/core'; +import { AccountsSelectList, ErrorMessage, Col, Row } from 'components'; +import { FormattedMessage as T } from 'react-intl'; +import classNames from 'classnames'; +import withAccounts from 'containers/Accounts/withAccounts'; + +import { compose } from 'utils'; + +/** + * Item form inventory sections. + */ +function ItemFormInventorySection({ + errors, + touched, + setFieldValue, + values, + getFieldProps, + + accountsList, +}) { + return ( +
+ + +

+ +

+ + {/*------------- Inventory account ------------- */} + } + inline={true} + intent={ + errors.inventory_account_id && + touched.inventory_account_id && + Intent.DANGER + } + helperText={ + + } + className={classNames( + 'form-group--item-inventory_account', + 'form-group--select-list', + Classes.FILL, + )} + > + { + setFieldValue('inventory_account_id', account.id); + }} + defaultSelectText={} + selectedAccountId={values.inventory_account_id} + /> + + + } + className={'form-group--item-stock'} + inline={true} + > + + + + + + + +
+
+ ); +} + +export default compose( + withAccounts(({ accountsList }) => ({ + accountsList, + })), +)(ItemFormInventorySection); diff --git a/client/src/containers/Items/ItemFormPrimarySection.js b/client/src/containers/Items/ItemFormPrimarySection.js new file mode 100644 index 000000000..99957e731 --- /dev/null +++ b/client/src/containers/Items/ItemFormPrimarySection.js @@ -0,0 +1,185 @@ +import React, { useMemo } from 'react'; +import { + FormGroup, + Intent, + InputGroup, + RadioGroup, + Classes, + Radio, + Position, + Tooltip, +} from '@blueprintjs/core'; +import { FormattedMessage as T, useIntl } from 'react-intl'; +import { + CategoriesSelectList, + ErrorMessage, + Hint, + Col, + Row, + FieldRequiredHint, +} from 'components'; +import classNames from 'classnames'; +import { CLASSES } from 'common/classes'; + +import withItemCategories from 'containers/Items/withItemCategories'; +import withAccounts from 'containers/Accounts/withAccounts'; + +import { compose, handleStringChange } from 'utils'; + +/** + * Item form primary section. + */ +function ItemFormPrimarySection({ + getFieldProps, + setFieldValue, + errors, + touched, + values, + + // #withItemCategories + categoriesList, +}) { + return ( +
+ + + {/*----------- Item type ----------*/} + } + labelInfo={} + className={'form-group--item-type'} + intent={errors.type && touched.type && Intent.DANGER} + helperText={} + inline={true} + > + { + setFieldValue('item_type', value); + })} + selectedValue={values.item_type} + > + + + + } + value="service" + /> + + + + } + value="inventory" + /> + + + + } + value="non_inventory" + /> + + + + {/*----------- Item name ----------*/} + } + labelInfo={} + className={'form-group--item-name'} + intent={errors.name && touched.name && Intent.DANGER} + helperText={} + inline={true} + > + + + + {/*----------- SKU ----------*/} + } + labelInfo={} + className={'form-group--item-sku'} + intent={errors.sku && touched.sku && Intent.DANGER} + helperText={} + inline={true} + > + + + + {/*----------- Item category ----------*/} + } + labelInfo={} + inline={true} + intent={errors.category_id && touched.category_id && Intent.DANGER} + helperText={ + + } + className={classNames( + 'form-group--select-list', + 'form-group--category', + Classes.FILL, + )} + > + { + setFieldValue('item_category_id', category.id); + }} + popoverProps={{ minimal: true }} + /> + + + + + {/* */} + + +
+ ); +} + +export default compose( + withAccounts(({ accountsList }) => ({ + accountsList, + })), + withItemCategories(({ categoriesList }) => ({ + categoriesList, + })), +)(ItemFormPrimarySection); diff --git a/client/src/lang/en/index.js b/client/src/lang/en/index.js index 6b6327beb..bea6f7c8a 100644 --- a/client/src/lang/en/index.js +++ b/client/src/lang/en/index.js @@ -805,4 +805,6 @@ export default { address_line_2: 'Address line 2', website: 'Website', notes: 'Notes', + i_purchase_this_item: 'I purchase this item from a vendor.', + i_sell_this_item: 'I sell this item to a customer.', }; diff --git a/client/src/style/pages/items.scss b/client/src/style/pages/items.scss index 5566fb298..4cf87aa39 100644 --- a/client/src/style/pages/items.scss +++ b/client/src/style/pages/items.scss @@ -1,56 +1,136 @@ +.page-form--item{ + $self: '.page-form'; + padding: 20px; -.item-form{ - padding: 22px; - padding-bottom: 90px; - - &__primary-section{ - background-color: #FAFAFA; - padding: 40px 22px 22px; - margin: -22px -22px 22px; - background-color: #FAFAFA; + #{$self}__header{ + padding: 0; } - &__accounts-section{ - h4{ - margin: 0; + #{$self}__primary-section{ + padding: 30px 22px 0; + margin: -20px -20px 24px; + overflow: hidden; + } + + #{$self}__body{ + .bp3-form-group{ + max-width: 500px; + margin-bottom: 14px; + + &.bp3-inline{ + + .bp3-label{ + min-width: 140px; + } + } + .bp3-form-content{ + width: 100%; + } + } + + h3{ font-weight: 500; - margin-bottom: 20px; - color: #828282; + font-size: 14px; + margin-bottom: 1.4rem; } - > div:first-of-type{ - padding-right: 15px !important; - } - - > div ~ div{ - padding-left: 15px !important; - border-left: 1px solid #e8e8e8; - } - } - - .#{$ns}-form-group{ - .#{$ns}-label{ - width: 130px; - } - - .#{$ns}-form-content{ - width: 250px; - } - } - - .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; + .bp3-control{ + + h3{ + display: inline; + margin-bottom: 0; + } + } + + .form-group--sellable, + .form-group--purchasable{ + margin-bottom: 1rem; } } -} \ No newline at end of file + + #{$self}__section{ + max-width: 850px; + margin-bottom: 1rem; + + .bp3-form-group{ + max-width: 400px; + } + + &--selling-cost{ + border-bottom: 1px solid #eaeaea; + margin-bottom: 1.25rem; + padding-bottom: 0.25rem; + } + } + + #{$self}__floating-actions{ + margin-left: -20px; + margin-right: -20px; + + .form-group--active{ + display: inline-block; + margin: 0; + margin-left: 40px; + } + } + + .bp3-tooltip-indicator{ + border-bottom: 1px dashed #d0d0d0; + } +} + + +// .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; +// } + +// > div ~ div{ +// padding-left: 15px !important; +// border-left: 1px solid #e8e8e8; +// } +// } + +// .#{$ns}-form-group{ +// .#{$ns}-label{ +// width: 130px; +// } + +// .#{$ns}-form-content{ +// width: 250px; +// } +// } + +// .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