diff --git a/client/jsconfig.json b/client/jsconfig.json
index a344c94b1..ee1b584ef 100644
--- a/client/jsconfig.json
+++ b/client/jsconfig.json
@@ -1,6 +1,7 @@
{
"compilerOptions": {
+ "jsx": "react",
"baseUrl": "src"
},
"include": ["src"]
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 (
+
+
+
+
+ }
+ text={}
+ onClick={onClickNewVendor}
+ />
+
+
+
+ ) : (
+ `${filterCount} ${formatMessage({ id: 'filters_applied' })}`
+ )
+ }
+ icon={}
+ />
+
+ }
+ text={}
+ />
+ }
+ text={}
+ />
+
+
+ );
+}
+
+export default VendorActionsBar;
diff --git a/client/src/containers/Vendors/VendorAttahmentTab.js b/client/src/containers/Vendors/VendorAttahmentTab.js
new file mode 100644
index 000000000..71a4038e3
--- /dev/null
+++ b/client/src/containers/Vendors/VendorAttahmentTab.js
@@ -0,0 +1,20 @@
+import React from 'react';
+import Dragzone from 'components/Dragzone';
+
+/**
+ * Vendor Attahment Tab.
+ */
+function VendorAttahmentTab() {
+ return (
+
+
+
+ );
+}
+
+export default VendorAttahmentTab;
diff --git a/client/src/containers/Vendors/VendorFinanicalPanelTab.js b/client/src/containers/Vendors/VendorFinanicalPanelTab.js
new file mode 100644
index 000000000..d4b15ef79
--- /dev/null
+++ b/client/src/containers/Vendors/VendorFinanicalPanelTab.js
@@ -0,0 +1,108 @@
+import React from 'react';
+import classNames from 'classnames';
+import { FormGroup, Position, Classes } from '@blueprintjs/core';
+import { DateInput } from '@blueprintjs/datetime';
+import { FastField, ErrorMessage } from 'formik';
+import { MoneyInputGroup, CurrencySelectList, Row, Col } from 'components';
+import { FormattedMessage as T } from 'react-intl';
+
+import withCurrencies from 'containers/Currencies/withCurrencies';
+
+import {
+ compose,
+ momentFormatter,
+ tansformDateValue,
+ inputIntent,
+} from 'utils';
+
+/**
+ * Vendor Finaniceal Panel Tab.
+ */
+function VendorFinanicalPanelTab({
+ // #withCurrencies
+ currenciesList,
+
+ // #OwnProps
+ vendorId,
+}) {
+ return (
+
+
+
+ {/*------------ Opening balance at -----------*/}
+
+ {({ form, field: { value }, meta: { error, touched } }) => (
+ }
+ className={classNames('form-group--select-list', Classes.FILL)}
+ intent={inputIntent({ error, touched })}
+ inline={true}
+ helperText={}
+ >
+
+
+ )}
+
+ {/*------------ Opening balance -----------*/}
+
+ {({ field, field: { value }, meta: { error, touched } }) => (
+ }
+ className={classNames(
+ 'form-group--opening-balance',
+ Classes.FILL,
+ )}
+ intent={inputIntent({ error, touched })}
+ inline={true}
+ >
+
+
+ )}
+
+
+ {/*------------ Currency -----------*/}
+
+ {({ form, field: { value }, meta: { error, touched } }) => (
+ }
+ className={classNames(
+ 'form-group--select-list',
+ 'form-group--balance-currency',
+ Classes.FILL,
+ )}
+ inline={true}
+ >
+ {
+ form.setFieldValue('currency_code', currency.currency_code);
+ }}
+ disabled={vendorId}
+ />
+
+ )}
+
+
+
+
+ );
+}
+
+export default compose(
+ withCurrencies(({ currenciesList }) => ({ currenciesList })),
+)(VendorFinanicalPanelTab);
diff --git a/client/src/containers/Vendors/VendorFloatingActions.js b/client/src/containers/Vendors/VendorFloatingActions.js
new file mode 100644
index 000000000..0f8203faf
--- /dev/null
+++ b/client/src/containers/Vendors/VendorFloatingActions.js
@@ -0,0 +1,50 @@
+import React from 'react';
+import { Intent, Button } from '@blueprintjs/core';
+import { FormattedMessage as T } from 'react-intl';
+import classNames from 'classnames';
+import { CLASSES } from 'common/classes';
+import { saveInvoke } from 'utils';
+
+
+export default function VendorFloatingActions({
+ onSubmitClick,
+ onSubmitAndNewClick,
+ onCancelClick,
+ isSubmitting,
+ vendor,
+}) {
+ 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 (
+
+ );
+ },
+ [formatMessage],
+ );
+
+ // Renders actions table cell.
+ const renderActionsCell = useMemo(
+ () => ({ cell }) => (
+
+ } />
+
+ ),
+ [onDeleteVendor, onEditVendor, renderContextMenu],
+ );
+
+ // Table columns.
+ const columns = useMemo(
+ () => [
+ {
+ id: 'avatar',
+ Header: '',
+ accessor: AvatarCell,
+ className: 'avatar',
+ width: 50,
+ disableResizing: true,
+ disableSortBy: true,
+ },
+ {
+ id: 'display_name',
+ Header: formatMessage({ id: 'display_name' }),
+ accessor: 'display_name',
+ className: 'display_name',
+ width: 150,
+ },
+ {
+ id: 'company_name',
+ Header: formatMessage({ id: 'company_name' }),
+ accessor: 'company_name',
+ className: 'company_name',
+ width: 150,
+ },
+ {
+ id: 'phone_number',
+ Header: formatMessage({ id: 'phone_number' }),
+ accessor: (row) => (
+
+
{row.work_phone}
+
{row.personal_phone}
+
+ ),
+ className: 'phone_number',
+ width: 100,
+ },
+ {
+ id: 'receivable_balance',
+ Header: formatMessage({ id: 'receivable_balance' }),
+ accessor: (r) => ,
+ className: 'receivable_balance',
+ width: 100,
+ },
+ {
+ id: 'actions',
+ Cell: renderActionsCell,
+ className: 'actions',
+ width: 70,
+ disableResizing: true,
+ disableSortBy: true,
+ },
+ ],
+ [formatMessage, renderActionsCell],
+ );
+
+ //Handle fetch data table
+ const handleFetchData = useCallback(
+ ({ pageIndex, pageSize, sortBy }) => {
+ addVendorsTableQueries({
+ page: pageIndex + 1,
+ page_size: pageSize,
+ ...(sortBy.length > 0
+ ? {
+ column_sort_order: sortBy[0].id,
+ sort_order: sortBy[0].desc ? 'desc' : 'asc',
+ }
+ : {}),
+ });
+ },
+ [addVendorsTableQueries],
+ );
+
+ const handleSelectedRowsChange = useCallback(
+ (selectedRows) => {
+ onSelectedRowsChange &&
+ onSelectedRowsChange(selectedRows.map((s) => s.original));
+ },
+ [onSelectedRowsChange],
+ );
+
+ const rowContextMenu = (cell) =>
+ renderContextMenu({
+ vendor: cell.row.original,
+ onEditVendor,
+ onDeleteVendor,
+ });
+
+ console.log(vendorsCurrentPage, 'vendorsCurrentPage');
+ return (
+
+
+
+ );
+}
+
+export default compose(
+ withVendors(
+ ({
+ vendorItems,
+ vendorsLoading,
+ vendorTableQuery,
+ vendorsPageination,
+ }) => ({
+ vendorItems,
+ vendorsLoading,
+ vendorsPageination,
+ vendorTableQuery,
+ }),
+ ),
+ withVendorsActions,
+)(VendorsTable);
diff --git a/client/src/containers/Vendors/VendorsTabs.js b/client/src/containers/Vendors/VendorsTabs.js
new file mode 100644
index 000000000..9e96b2991
--- /dev/null
+++ b/client/src/containers/Vendors/VendorsTabs.js
@@ -0,0 +1,44 @@
+import React from 'react';
+import { Tabs, Tab } from '@blueprintjs/core';
+import { FormattedMessage as T, useIntl } from 'react-intl';
+import classNames from 'classnames';
+import { CLASSES } from 'common/classes';
+import VendorFinanicalPanelTab from './VendorFinanicalPanelTab';
+import VendorAttahmentTab from './VendorAttahmentTab';
+import CustomerAddressTabs from 'containers/Customers/CustomerAddressTabs';
+import CustomerNotePanel from 'containers/Customers/CustomerNotePanel';
+
+export default function VendorTabs({ vendor }) {
+ const { formatMessage } = useIntl();
+ return (
+
+
+ }
+ />
+ }
+ />
+ }
+ />
+ }
+ />
+
+
+ );
+}
diff --git a/client/src/containers/Vendors/withVendorActions.js b/client/src/containers/Vendors/withVendorActions.js
index 0fa2ed9d0..21fe83ab6 100644
--- a/client/src/containers/Vendors/withVendorActions.js
+++ b/client/src/containers/Vendors/withVendorActions.js
@@ -4,17 +4,17 @@ import {
editVendor,
deleteVendor,
fetchVendorsTable,
+ fetchVendor,
} from 'store/vendors/vendors.actions';
import t from 'store/types';
-
const mapDipatchToProps = (dispatch) => ({
requestSubmitVendor: (form) => dispatch(submitVendor({ form })),
- requestEditVendor: (id, form) => dispatch(editVendor(id, form)),
+ requestEditVendor: (id, form) => dispatch(editVendor({ id, form })),
+ requsetFetchVendor: (id) => dispatch(fetchVendor({ id })),
requestFetchVendorsTable: (query = {}) =>
dispatch(fetchVendorsTable({ query: { ...query } })),
requestDeleteVender: (id) => dispatch(deleteVendor({ id })),
-
changeVendorView: (id) =>
dispatch({
type: t.VENDORS_SET_CURRENT_VIEW,
@@ -29,4 +29,3 @@ const mapDipatchToProps = (dispatch) => ({
});
export default connect(null, mapDipatchToProps);
-
diff --git a/client/src/containers/Vendors/withVendorDetail.js b/client/src/containers/Vendors/withVendorDetail.js
new file mode 100644
index 000000000..260f217a0
--- /dev/null
+++ b/client/src/containers/Vendors/withVendorDetail.js
@@ -0,0 +1,10 @@
+import { connect } from 'react-redux';
+import { getVendorByIdFactory } from 'store/vendors/vendors.selectors';
+
+export default () => {
+ const getVendorById = getVendorByIdFactory();
+ const mapStateToProps = (state, props) => ({
+ vendor: getVendorById(state, props),
+ });
+ return connect(mapStateToProps);
+};
diff --git a/client/src/lang/en/index.js b/client/src/lang/en/index.js
index 235d57ac6..8076e4b5f 100644
--- a/client/src/lang/en/index.js
+++ b/client/src/lang/en/index.js
@@ -823,4 +823,22 @@ export default {
the_item_has_associated_transactions: 'The item has associated transactions.',
customer_has_sales_invoices: 'Customer has sales invoices',
account_name_is_already_used: 'Account name is already used.',
+ vendors: 'Vendors',
+ vendor_email: 'Vendor Email',
+ new_vendor: 'New Vendor',
+ edit_vendor: 'Edit Vendor',
+ delete_vendor: 'Delete Vendor',
+ vendors_list: 'Vendors List',
+ the_vendor_has_been_successfully_created:
+ 'The vendor has been successfully created.',
+ the_vendor_has_been_successfully_deleted:
+ 'The vendor has been successfully deleted.',
+ the_vendors_has_been_successfully_deleted:
+ 'The vendors has been successfully deleted.',
+ the_item_vendor_has_been_successfully_edited:
+ 'The item vendor has been successfully edited.',
+ once_delete_this_vendor_you_will_able_to_restore_it: `Once you delete this vendor, you won\'t be able to restore it later. Are you sure you want to delete this vendor?`,
+ once_delete_these_vendors_you_will_not_able_restore_them:
+ "Once you delete these vendors, you won't be able to retrieve them later. Are you sure you want to delete them?",
+ vendor_has_bills: 'Vendor has bills',
};
diff --git a/client/src/routes/dashboard.js b/client/src/routes/dashboard.js
index 9f0ee6c12..8b2fb8ef6 100644
--- a/client/src/routes/dashboard.js
+++ b/client/src/routes/dashboard.js
@@ -205,6 +205,29 @@ export default [
breadcrumb: 'Customers',
},
+ // Vendors
+ {
+ path: `/vendors/:id/edit`,
+ component: LazyLoader({
+ loader: () => import('containers/Vendors/Vendor'),
+ }),
+ breadcrumb: 'Edit Vendor',
+ },
+ {
+ path: `/vendors/new`,
+ component: LazyLoader({
+ loader: () => import('containers/Vendors/Vendor'),
+ }),
+ breadcrumb: 'New Vendor',
+ },
+ {
+ path: `/vendors`,
+ component: LazyLoader({
+ loader: () => import('containers/Vendors/VendorsList'),
+ }),
+ breadcrumb: 'Vendors',
+ },
+
//Estimates
{
path: `/estimates/:id/edit`,
@@ -280,14 +303,16 @@ export default [
{
path: `/payment-receive/:id/edit`,
component: LazyLoader({
- loader: () => import('containers/Sales/PaymentReceive/PaymentReceiveFormPage'),
+ loader: () =>
+ import('containers/Sales/PaymentReceive/PaymentReceiveFormPage'),
}),
breadcrumb: 'Edit',
},
{
path: `/payment-receive/new`,
component: LazyLoader({
- loader: () => import('containers/Sales/PaymentReceive/PaymentReceiveFormPage'),
+ loader: () =>
+ import('containers/Sales/PaymentReceive/PaymentReceiveFormPage'),
}),
breadcrumb: 'New Payment Receive',
},
@@ -355,8 +380,7 @@ export default [
{
path: `/payment-mades`,
component: LazyLoader({
- loader: () =>
- import('containers/Purchases/PaymentMades/PaymentMadeList'),
+ loader: () => import('containers/Purchases/PaymentMades/PaymentMadeList'),
}),
breadcrumb: 'Payment Made List',
},
diff --git a/client/src/store/vendors/vendors.actions.js b/client/src/store/vendors/vendors.actions.js
index b5b2c13de..120089f5a 100644
--- a/client/src/store/vendors/vendors.actions.js
+++ b/client/src/store/vendors/vendors.actions.js
@@ -9,15 +9,14 @@ export const fetchVendorsTable = ({ query }) => {
type: t.VENDORS_TABLE_LOADING,
payload: { loading: true },
});
-
ApiService.get(`vendors`, { params: { ...pageQuery, ...query } })
.then((response) => {
dispatch({
type: t.VENDORS_PAGE_SET,
payload: {
vendors: response.data.vendors,
- pagination: response.data.pagination,
customViewId: response.data.customViewId || -1,
+ paginationMeta: response.data.pagination,
},
});
dispatch({
@@ -37,7 +36,6 @@ export const fetchVendorsTable = ({ query }) => {
type: t.VENDORS_TABLE_LOADING,
payload: { loading: false },
});
-
resolve(response);
})
.catch((error) => {
@@ -81,18 +79,32 @@ export const submitVendor = ({ form }) => {
new Promise((resolve, reject) => {
ApiService.post('vendors', form)
.then((response) => {
- dispatch({
- type: t.SET_DASHBOARD_REQUEST_COMPLETED,
- });
resolve(response);
})
.catch((error) => {
const { response } = error;
const { data } = response;
- dispatch({
- type: t.SET_DASHBOARD_REQUEST_COMPLETED,
- });
reject(data?.errors);
});
});
};
+
+export const fetchVendor = ({ id }) => {
+ return (dispatch) =>
+ new Promise((resolve, reject) => {
+ ApiService.get(`vendors/${id}`)
+ .then((response) => {
+ dispatch({
+ type: t.VENDOR_SET,
+ payload: {
+ id,
+ vendor: response.data.vendor,
+ },
+ });
+ resolve(response);
+ })
+ .catch((error) => {
+ reject(error);
+ });
+ });
+};
diff --git a/client/src/store/vendors/vendors.reducer.js b/client/src/store/vendors/vendors.reducer.js
index 8a3737910..9eeb4a883 100644
--- a/client/src/store/vendors/vendors.reducer.js
+++ b/client/src/store/vendors/vendors.reducer.js
@@ -1,5 +1,8 @@
import { createReducer } from '@reduxjs/toolkit';
-import { createTableQueryReducers } from 'store/queryReducers';
+import {
+ viewPaginationSetReducer,
+ createTableQueryReducers,
+} from 'store/journalNumber.reducer';
import t from 'store/types';
@@ -7,19 +10,23 @@ const initialState = {
items: {},
views: {},
loading: false,
+ currentViewId: -1,
+
tableQuery: {
page_size: 5,
page: 1,
},
- currentViewId: -1,
};
-
-const reducer = createReducer(initialState, {
+export default createReducer(initialState, {
+ [t.VENDOR_SET]: (state, action) => {
+ const { id, vendor } = action.payload;
+ const _vendors = state.items[id] || {};
+ state.items[id] = { ..._vendors, ...vendor };
+ },
[t.VENDORS_TABLE_LOADING]: (state, action) => {
const { loading } = action.payload;
state.loading = loading;
},
-
[t.VENDORS_ITEMS_SET]: (state, action) => {
const { vendors } = action.payload;
const _vendors = {};
@@ -33,24 +40,21 @@ const reducer = createReducer(initialState, {
..._vendors,
};
},
-
[t.VENDORS_PAGE_SET]: (state, action) => {
- const { customViewId, vendors, pagination } = action.payload;
+ const { customViewId, vendors, paginationMeta } = action.payload;
const viewId = customViewId || -1;
const view = state.views[viewId] || {};
-
state.views[viewId] = {
...view,
pages: {
...(state.views?.[viewId]?.pages || {}),
- [pagination.page]: {
+ [paginationMeta.total]: {
ids: vendors.map((i) => i.id),
},
},
};
},
-
[t.VENDOR_DELETE]: (state, action) => {
const { id } = action.payload;
@@ -58,39 +62,6 @@ const reducer = createReducer(initialState, {
delete state.items[id];
}
},
-
- [t.VENDORS_SET_CURRENT_VIEW]: (state, action) => {
- state.currentViewId = action.currentViewId;
- },
-
- [t.VENDORS_PAGINATION_SET]: (state, action) => {
- const { pagination, customViewId } = action.payload;
-
- const mapped = {
- pageSize: parseInt(pagination.pageSize, 10),
- page: parseInt(pagination.page, 10),
- total: parseInt(pagination.total, 10),
- };
- const paginationMeta = {
- ...mapped,
- pagesCount: Math.ceil(mapped.total / mapped.pageSize),
- pageIndex: Math.max(mapped.page - 1, 0),
- };
-
- state.views = {
- ...state.views,
- [customViewId]: {
- ...(state.views?.[customViewId] || {}),
- paginationMeta,
- },
- };
- },
-
- // [t.VENDOR_SET]: (state, action) => {
- // const { id, vendor } = action.payload;
- // const _venders = state.items[id] || {};
- // state.items[id] = { ..._venders, ...vendor };
- // },
+ // ...viewPaginationSetReducer(t.VENDORS_PAGINATION_SET),
+ ...createTableQueryReducers('VENDORS'),
});
-
-export default createTableQueryReducers('vendors', reducer);
diff --git a/client/src/store/vendors/vendors.selectors.js b/client/src/store/vendors/vendors.selectors.js
index 708338074..202dc3c36 100644
--- a/client/src/store/vendors/vendors.selectors.js
+++ b/client/src/store/vendors/vendors.selectors.js
@@ -1,10 +1,18 @@
import { createSelector } from '@reduxjs/toolkit';
-import { pickItemsFromIds, paginationLocationQuery } from 'store/selectors';
+import {
+ pickItemsFromIds,
+ paginationLocationQuery,
+ defaultPaginationMeta,
+} from 'store/selectors';
const vendorsTableQuery = (state) => {
return state.vendors.tableQuery;
};
+const vendorByIdSelector = (state, props) => {
+ return state.vendors.items[props.vendorId];
+};
+
export const getVendorsTableQuery = createSelector(
paginationLocationQuery,
vendorsTableQuery,
@@ -18,7 +26,10 @@ export const getVendorsTableQuery = createSelector(
const vendorsPageSelector = (state, props, query) => {
const viewId = state.vendors.currentViewId;
- return state.vendors.views?.[viewId]?.pages?.[query.page];
+ const currentView = state.vendors.views?.[viewId];
+ const currentPageId = currentView?.pages;
+ return currentView?.pages?.[currentPageId];
+ // return state.vendors.views?.[viewId]?.pages?.[query.page];
};
const vendorsItemsSelector = (state) => state.vendors.items;
@@ -41,14 +52,13 @@ const vendorsPaginationSelector = (state, props) => {
export const getVendorsPaginationMetaFactory = () =>
createSelector(vendorsPaginationSelector, (vendorPage) => {
- return vendorPage?.paginationMeta || {};
+ return {
+ ...defaultPaginationMeta(),
+ ...(vendorPage?.paginationMeta || {}),
+ };
});
-const vendorByIdSelector = (state, props) => {
- return state.vendors.items[props.vendorId];
-};
-
-export const getEstimateByIdFactory = () =>
+export const getVendorByIdFactory = () =>
createSelector(vendorByIdSelector, (vendor) => {
return vendor;
});