diff --git a/client/src/config/sidebarMenu.js b/client/src/config/sidebarMenu.js index 4befccffe..a7bfc0332 100644 --- a/client/src/config/sidebarMenu.js +++ b/client/src/config/sidebarMenu.js @@ -83,8 +83,8 @@ export default [ href: '/customers', }, { - text: , - href: '/customers/new', + text: , + href: '/vendors', }, ], }, diff --git a/client/src/containers/Vendors/Vendor.js b/client/src/containers/Vendors/Vendor.js new file mode 100644 index 000000000..e7e4ed825 --- /dev/null +++ b/client/src/containers/Vendors/Vendor.js @@ -0,0 +1,65 @@ +import React, { useCallback } from 'react'; +import { useParams, useHistory } from 'react-router-dom'; +import { useQuery } from 'react-query'; + +import VendorFrom from './VendorForm'; +import DashboardInsider from 'components/Dashboard/DashboardInsider'; + +import withVendorActions from './withVendorActions'; +import withCurrenciesActions from 'containers/Currencies/withCurrenciesActions'; + +import { compose } from 'utils'; + +function Vendor({ + // #withVendorActions + requestFetchVendorsTable, + requsetFetchVendor, + + // #wihtCurrenciesActions + requestFetchCurrencies, +}) { + const { id } = useParams(); + const history = useHistory(); + + // Handle fetch Currencies data table + const fetchCurrencies = useQuery('currencies', () => + requestFetchCurrencies(), + ); + + // Handle fetch vendors data table + const fetchVendors = useQuery('vendor-list', () => + requestFetchVendorsTable({}), + ); + + // Handle fetch vendor details. + const fetchVendor = useQuery( + ['vendor', id], + (_id, vendorId) => requsetFetchVendor(vendorId), + { enabled: id && id }, + ); + + const handleFormSubmit = useCallback(() => {}, []); + + const handleCancel = useCallback(() => { + history.goBack(); + }, [history]); + + return ( + + + + ); +} + +export default compose(withCurrenciesActions, withVendorActions)(Vendor); diff --git a/client/src/containers/Vendors/VendorActionsBar.js b/client/src/containers/Vendors/VendorActionsBar.js new file mode 100644 index 000000000..eddc6ce52 --- /dev/null +++ b/client/src/containers/Vendors/VendorActionsBar.js @@ -0,0 +1,80 @@ +import React, { useCallback, useMemo, useState } from 'react'; +import { + NavbarGroup, + NavbarDivider, + Button, + Classes, + Intent, + Popover, + Position, + PopoverInteractionKind, +} from '@blueprintjs/core'; +import { FormattedMessage as T, useIntl } from 'react-intl'; +import classNames from 'classnames'; +import { connect } from 'react-redux'; +import { useHistory } from 'react-router-dom'; + +import Icon from 'components/Icon'; +import DashboardActionsBar from 'components/Dashboard/DashboardActionsBar'; +import { DashboardActionViewsList } from 'components'; +import withResourceDetail from 'containers/Resources/withResourceDetails'; +import { compose } from 'utils'; + +function VendorActionsBar({ + // #ownProps + selectedRows = [], +}) { + const [filterCount, setFilterCount] = useState(0); + const history = useHistory(); + const { formatMessage } = useIntl(); + + const onClickNewVendor = useCallback(() => { + history.push('/vendors/new'); + }, [history]); + + + return ( + + + + + + + + + ); +} diff --git a/client/src/containers/Vendors/VendorForm.js b/client/src/containers/Vendors/VendorForm.js new file mode 100644 index 000000000..3a529d4fd --- /dev/null +++ b/client/src/containers/Vendors/VendorForm.js @@ -0,0 +1,176 @@ +import React, { useState, useMemo, useCallback, useEffect } from 'react'; +import * as Yup from 'yup'; +import { Formik, Form } from 'formik'; +import moment from 'moment'; +import { Intent } from '@blueprintjs/core'; +import { FormattedMessage as T, useIntl } from 'react-intl'; +import classNames from 'classnames'; +import { useHistory } from 'react-router-dom'; + +import { CLASSES } from 'common/classes'; +import AppToaster from 'components/AppToaster'; +import { + CreateVendorFormSchema, + EditVendorFormSchema, +} from './VendorForm.schema'; + +import VendorFormPrimarySection from './VendorFormPrimarySection'; +import VendorFormAfterPrimarySection from './VendorFormAfterPrimarySection'; +import VendorTabs from './VendorsTabs'; +import VendorFloatingActions from './VendorFloatingActions'; + +import withDashboardActions from 'containers/Dashboard/withDashboardActions'; +import withVendorDetail from './withVendorDetail'; +import withVendorActions from './withVendorActions'; + +import { compose, transformToForm } from 'utils'; + +const defaultInitialValues = { + salutation: '', + first_name: '', + last_name: '', + company_name: '', + display_name: '', + + email: '', + work_phone: '', + personal_phone: '', + website: '', + note: '', + active: true, + + billing_address_country: '', + billing_address_1: '', + billing_address_2: '', + billing_address_city: '', + billing_address_state: '', + billing_address_postcode: '', + billing_address_phone: '', + + shipping_address_country: '', + shipping_address_1: '', + shipping_address_2: '', + shipping_address_city: '', + shipping_address_state: '', + shipping_address_postcode: '', + shipping_address_phone: '', + + opening_balance: '', + currency_code: '', + opening_balance_at: moment(new Date()).format('YYYY-MM-DD'), +}; + +/** + * Vendor form. + */ +function VendorForm({ + // #withDashboardActions + changePageTitle, + + // #withVendorDetailsActions + vendor, + // #withVendorActions + requestSubmitVendor, + requestEditVendor, + + // #OwnProps + vendorId, +}) { + const isNewMode = !vendorId; + const [submitPayload, setSubmitPayload] = useState({}); + + const history = useHistory(); + const { formatMessage } = useIntl(); + + /** + * Initial values in create and edit mode. + */ + const initialValues = useMemo( + () => ({ + ...defaultInitialValues, + ...transformToForm(vendor, defaultInitialValues), + }), + [defaultInitialValues], + ); + console.log(isNewMode, 'Val'); + useEffect(() => { + !isNewMode + ? changePageTitle(formatMessage({ id: 'edit_vendor' })) + : changePageTitle(formatMessage({ id: 'new_vendor' })); + }, [changePageTitle, isNewMode, formatMessage]); + + //Handles the form submit. + const handleFormSubmit = ( + values, + { setSubmitting: resetForm, setErrors }, + ) => { + const requestForm = { ...values }; + + const onSuccess = () => { + AppToaster.show({ + message: formatMessage({ + id: isNewMode + ? 'the_vendor_has_been_successfully_created' + : 'the_item_vendor_has_been_successfully_edited', + }), + intent: Intent.SUCCESS, + }); + setSubmitPayload(false); + resetForm(); + + if (!submitPayload.noRedirect) { + history.push('/vendors'); + } + }; + + const onError = () => { + setSubmitPayload(false); + }; + + if (vendor && vendor.id) { + requestEditVendor(vendor.id, requestForm).then(onSuccess).catch(onError); + } else { + requestSubmitVendor(requestForm).then(onSuccess).catch(onError); + } + }; + + const handleCancelClick = useCallback(() => { + history.goBack(); + }, [history]); + + const handleSubmitAndNewClick = useCallback(() => { + setSubmitPayload({ noRedirect: true }); + }); + + return ( +
+ + {({ isSubmitting }) => ( +
+ + + + + + )} +
+
+ ); +} + +export default compose( + withVendorDetail(), + withDashboardActions, + withVendorActions, +)(VendorForm); diff --git a/client/src/containers/Vendors/VendorForm.schema.js b/client/src/containers/Vendors/VendorForm.schema.js new file mode 100644 index 000000000..1edbde25e --- /dev/null +++ b/client/src/containers/Vendors/VendorForm.schema.js @@ -0,0 +1,44 @@ +import * as Yup from 'yup'; +import { formatMessage } from 'services/intl'; + +const Schema = Yup.object().shape({ + salutation: Yup.string().trim(), + first_name: Yup.string().trim(), + last_name: Yup.string().trim(), + company_name: Yup.string().trim(), + display_name: Yup.string() + .trim() + .required() + .label(formatMessage({ id: 'display_name_' })), + + email: Yup.string().email().nullable(), + work_phone: Yup.number(), + personal_phone: Yup.number(), + website: Yup.string().url().nullable(), + + active: Yup.boolean(), + note: Yup.string().trim(), + + billing_address_country: Yup.string().trim(), + billing_address_1: Yup.string().trim(), + billing_address_2: Yup.string().trim(), + billing_address_city: Yup.string().trim(), + billing_address_state: Yup.string().trim(), + billing_address_postcode: Yup.number().nullable(), + billing_address_phone: Yup.number(), + + shipping_address_country: Yup.string().trim(), + shipping_address_1: Yup.string().trim(), + shipping_address_2: Yup.string().trim(), + shipping_address_city: Yup.string().trim(), + shipping_address_state: Yup.string().trim(), + shipping_address_postcode: Yup.number().nullable(), + shipping_address_phone: Yup.number(), + + opening_balance: Yup.number().nullable(), + currency_code: Yup.string(), + opening_balance_at: Yup.date(), +}); + +export const CreateVendorFormSchema = Schema; +export const EditVendorFormSchema = Schema; diff --git a/client/src/containers/Vendors/VendorFormAfterPrimarySection.js b/client/src/containers/Vendors/VendorFormAfterPrimarySection.js new file mode 100644 index 000000000..c23ba5d52 --- /dev/null +++ b/client/src/containers/Vendors/VendorFormAfterPrimarySection.js @@ -0,0 +1,75 @@ +import React from 'react'; +import { FormGroup, InputGroup, ControlGroup } from '@blueprintjs/core'; +import { FastField, ErrorMessage } from 'formik'; +import { FormattedMessage as T } from 'react-intl'; +import classNames from 'classnames'; +import { CLASSES } from 'common/classes'; +import { inputIntent } from 'utils'; + + +/** + * Vendor form after primary section. + */ +function VendorFormAfterPrimarySection() { + return ( +
+ {/*------------ Vendor email -----------*/} + + {({ field, meta: { error, touched } }) => ( + } + className={'form-group--email'} + label={} + inline={true} + > + + + )} + + {/*------------ Phone number -----------*/} + } + inline={true} + > + + + {({ field, meta: { error, touched } }) => ( + + )} + + + {({ field, meta: { error, touched } }) => ( + + )} + + + + {/*------------ Vendor website -----------*/} + + {({ field, meta: { error, touched } }) => ( + } + className={'form-group--website'} + label={} + inline={true} + > + + + )} + +
+ ); +} + +export default VendorFormAfterPrimarySection; diff --git a/client/src/containers/Vendors/VendorFormPrimarySection.js b/client/src/containers/Vendors/VendorFormPrimarySection.js new file mode 100644 index 000000000..f9621d861 --- /dev/null +++ b/client/src/containers/Vendors/VendorFormPrimarySection.js @@ -0,0 +1,124 @@ +import React from 'react'; +import classNames from 'classnames'; +import { FormGroup, InputGroup, ControlGroup } from '@blueprintjs/core'; +import { FastField, Field, ErrorMessage } from 'formik'; +import { FormattedMessage as T } from 'react-intl'; +import { + Hint, + FieldRequiredHint, + SalutationList, + DisplayNameList, +} from 'components'; + +import { CLASSES } from 'common/classes'; +import { inputIntent } from 'utils'; + +/** + * Vendor form primary section. + */ +function VendorFormPrimarySection() { + return ( +
+
+ {/**----------- Vendor name -----------*/} + } + inline={true} + > + + + {({ form, field: { value }, meta: { error, touched } }) => ( + { + form.setFieldValue('salutation', salutation.label); + }} + selectedItem={value} + popoverProps={{ minimal: true }} + className={classNames( + CLASSES.FORM_GROUP_LIST_SELECT, + CLASSES.FILL, + 'input-group--salutation-list', + 'select-list--fill-button', + )} + /> + )} + + + + {({ field, meta: { error, touched } }) => ( + + )} + + + + {({ field, meta: { error, touched } }) => ( + + )} + + + + + {/*----------- Company Name -----------*/} + + {({ field, meta: { error, touched } }) => ( + } + intent={inputIntent({ error, touched })} + helperText={} + inline={true} + > + + + )} + + {/*----------- Display Name -----------*/} + + {({ form, field: { value }, meta: { error, touched } }) => ( + } + intent={inputIntent({ error, touched })} + label={ + <> + + + + + } + className={classNames( + CLASSES.FORM_GROUP_LIST_SELECT, + CLASSES.FILL, + )} + inline={true} + > + { + form.setFieldValue('display_name', displayName.label); + }} + selectedItem={value} + popoverProps={{ minimal: true }} + /> + + )} + +
+
+ ); +} + +export default VendorFormPrimarySection; diff --git a/client/src/containers/Vendors/VendorViewsTabs.js b/client/src/containers/Vendors/VendorViewsTabs.js new file mode 100644 index 000000000..a9c64b6d6 --- /dev/null +++ b/client/src/containers/Vendors/VendorViewsTabs.js @@ -0,0 +1,57 @@ +import React, { useEffect, useMemo } from 'react'; +import { Alignment, Navbar, NavbarGroup } from '@blueprintjs/core'; +import { compose } from 'redux'; +import { useParams, withRouter, useHistory } from 'react-router-dom'; +import { connect } from 'react-redux'; + +import { DashboardViewsTabs } from 'components'; + +import withVendors from './withVendors'; +import withVendorActions from './withVendorActions'; +import withDashboardActions from 'containers/Dashboard/withDashboardActions'; +import { pick } from 'lodash'; + +/** + * Customers views tabs. + */ +function VendorViewsTabs({ + // #withViewDetail + viewId, + viewItem, + + // #withVendors + vendorViews, + + // #withDashboardActions + setTopbarEditView, + changePageSubtitle, +}) { + const { custom_view_id: customViewId = null } = useParams(); + + const tabs = useMemo(() => + vendorViews.map( + (view) => ({ + ...pick(view, ['name', 'id']), + }), + [vendorViews], + ), + ); + + return ( + + + + + + ); +} + +export default compose( + withRouter, + withDashboardActions, + withVendors(({ vendorViews }) => ({ vendorViews })), +)(VendorViewsTabs); diff --git a/client/src/containers/Vendors/VendorsList.js b/client/src/containers/Vendors/VendorsList.js new file mode 100644 index 000000000..6dc928f30 --- /dev/null +++ b/client/src/containers/Vendors/VendorsList.js @@ -0,0 +1,161 @@ +import React, { useEffect, useCallback, useState, useMemo } from 'react'; +import { Route, Switch, useHistory } from 'react-router-dom'; +import { Intent, Alert } from '@blueprintjs/core'; +import { useQuery } from 'react-query'; +import { + FormattedMessage as T, + FormattedHTMLMessage, + useIntl, +} from 'react-intl'; + +import AppToaster from 'components/AppToaster'; +import DashboardInsider from 'components/Dashboard/DashboardInsider'; +import DashboardPageContent from 'components/Dashboard/DashboardPageContent'; + +import VendorsTable from './VendorsTable'; +import VendorActionsBar from './VendorActionsBar'; +import VendorsViewsTabs from './VendorViewsTabs'; + +import withVendors from './withVendors'; +import withVendorActions from './withVendorActions'; +import withResourceActions from 'containers/Resources/withResourcesActions'; +import withViewsActions from 'containers/Views/withViewsActions'; +import withDashboardActions from 'containers/Dashboard/withDashboardActions'; + +import { compose } from 'utils'; + +function VendorsList({ + // #withDashboardActions + changePageTitle, + + // #withVendors + vendorTableQuery, + + // #withVendorActions + requestDeleteVender, + requestFetchVendorsTable, + addVendorsTableQueries, +}) { + const [deleteVendor, setDeleteVendor] = useState(false); + const [selectedRows, setSelectedRows] = useState([]); + const [tableLoading, setTableLoading] = useState(false); + + const { formatMessage } = useIntl(); + const history = useHistory(); + + useEffect(() => { + changePageTitle(formatMessage({ id: 'vendors_list' })); + }, [changePageTitle, formatMessage]); + + // Handle fetch customers data table + const fetchVendors = useQuery(['vendors-table', vendorTableQuery], () => + requestFetchVendorsTable(), + ); + + // Handle Edit vendor data table + const handleEditVendor = useCallback( + (vendor) => { + history.push(`/vendors/${vendor.id}/edit`); + }, + [history], + ); + // Handle click delete vendor. + const handleDeleteVendor = useCallback( + (vendor) => { + setDeleteVendor(vendor); + }, + [setDeleteVendor], + ); + + // Handle cancel delete the vendor. + const handleCancelDeleteVendor = useCallback(() => { + setDeleteVendor(false); + }, [setDeleteVendor]); + + // Transform API errors in toasts messages. + const transformErrors = useCallback((errors) => { + if (errors.some((e) => e.type === 'VENDOR.HAS.BILLS')) { + AppToaster.show({ + message: formatMessage({ + id: 'vendor_has_bills', + }), + intent: Intent.DANGER, + }); + } + }, []); + + // handle confirm delete vendor. + const handleConfirmDeleteVendor = useCallback(() => { + requestDeleteVender(deleteVendor.id) + .then(() => { + setDeleteVendor(false); + AppToaster.show({ + message: formatMessage({ + id: 'the_vendor_has_been_successfully_deleted', + }), + intent: Intent.SUCCESS, + }); + }) + .catch((errors) => { + setDeleteVendor(false); + transformErrors(errors); + }); + }, [requestDeleteVender, deleteVendor, formatMessage]); + + // Handle selected rows change. + const handleSelectedRowsChange = useCallback( + (vendor) => { + setSelectedRows(vendor); + }, + [setSelectedRows], + ); + + useEffect(() => { + if (tableLoading && !fetchVendors.isFetching) { + setTableLoading(false); + } + }, [tableLoading, fetchVendors]); + + return ( + + + + + + + + + + } + confirmButtonText={} + icon="trash" + intent={Intent.DANGER} + isOpen={deleteVendor} + onCancel={handleCancelDeleteVendor} + onConfirm={handleConfirmDeleteVendor} + > +

+ +

+
+
+
+ ); +} + +export default compose( + withVendorActions, + withDashboardActions, + withVendors(({ vendorTableQuery }) => ({ vendorTableQuery })), +)(VendorsList); diff --git a/client/src/containers/Vendors/VendorsTable.js b/client/src/containers/Vendors/VendorsTable.js new file mode 100644 index 000000000..ac85d9213 --- /dev/null +++ b/client/src/containers/Vendors/VendorsTable.js @@ -0,0 +1,226 @@ +import React, { useRef, useEffect, useCallback, useMemo } from 'react'; +import { + Button, + Popover, + Menu, + MenuItem, + MenuDivider, + Position, + Intent, +} from '@blueprintjs/core'; +import { FormattedMessage as T, useIntl } from 'react-intl'; +import { useIsValuePassed } from 'hooks'; + +import LoadingIndicator from 'components/LoadingIndicator'; +import { DataTable, Icon, Money } from 'components'; + +import withVendors from './withVendors'; +import withVendorsActions from './withVendorActions'; + +import { compose, firstLettersArgs, saveInvoke } from 'utils'; + +const AvatarCell = (row) => { + return {firstLettersArgs(row.display_name)}; +}; + +function VendorsTable({ + // #withVendors + vendorsCurrentPage, + vendorsLoading, + vendorsPageination, + vendorTableQuery, + vendorItems, + + // #withVendorsActions + addVendorsTableQueries, + + // #OwnProps + loading, + onEditVendor, + onDeleteVendor, + onSelectedRowsChange, +}) { + const { formatMessage } = useIntl(); + const isLoadedBefore = useIsValuePassed(vendorsLoading, false); + + // Vendor actions list. + const renderContextMenu = useMemo( + () => ({ vendor, onEditVendor, onDeleteVendor }) => { + const handleEditVendor = () => { + saveInvoke(onEditVendor, vendor); + }; + const handleDeleteVendor = () => { + saveInvoke(onDeleteVendor, vendor); + }; + return ( + + } + text={formatMessage({ id: 'view_details' })} + /> + + } + text={formatMessage({ id: 'edit_vendor' })} + onClick={handleEditVendor} + /> + } + text={formatMessage({ id: 'delete_vendor' })} + intent={Intent.DANGER} + onClick={handleDeleteVendor} + /> + + ); + }, + [formatMessage], + ); + + // Renders actions table cell. + const renderActionsCell = useMemo( + () => ({ cell }) => ( + +