Merge remote-tracking branch 'origin/master'

This commit is contained in:
Ahmed Bouhuolia
2020-11-18 21:58:29 +02:00
22 changed files with 1347 additions and 72 deletions

View File

@@ -83,8 +83,8 @@ export default [
href: '/customers',
},
{
text: <T id={'new_customers'} />,
href: '/customers/new',
text: <T id={'vendors'} />,
href: '/vendors',
},
],
},

View File

@@ -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 (
<DashboardInsider
loading={
fetchCurrencies.isFetching ||
fetchVendors.isFetching ||
fetchVendor.isFetching
}
name={'vendor-form'}
>
<VendorFrom
onFormSubmit={handleFormSubmit}
vendorId={id}
onCancelForm={handleCancel}
/>
</DashboardInsider>
);
}
export default compose(withCurrenciesActions, withVendorActions)(Vendor);

View File

@@ -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 (
<DashboardActionsBar>
<NavbarGroup>
<DashboardActionViewsList resourceName={'vendors'} views={[]} />
<NavbarDivider />
<Button
className={Classes.MINIMAL}
icon={<Icon icon={'plus'} />}
text={<T id={'new_vendor'} />}
onClick={onClickNewVendor}
/>
<NavbarDivider />
<Popover
// content={}
interactionKind={PopoverInteractionKind.CLICK}
position={Position.BOTTOM_LEFT}
>
<Button
className={classNames(Classes.MINIMAL, 'button--filter')}
text={
filterCount <= 0 ? (
<T id={'filter'} />
) : (
`${filterCount} ${formatMessage({ id: 'filters_applied' })}`
)
}
icon={<Icon icon="filter-16" iconSize={16} />}
/>
</Popover>
<Button
className={Classes.MINIMAL}
icon={<Icon icon="file-import-16" iconSize={16} />}
text={<T id={'import'} />}
/>
<Button
className={Classes.MINIMAL}
icon={<Icon icon="file-export-16" iconSize={16} />}
text={<T id={'export'} />}
/>
</NavbarGroup>
</DashboardActionsBar>
);
}
export default VendorActionsBar;

View File

@@ -0,0 +1,20 @@
import React from 'react';
import Dragzone from 'components/Dragzone';
/**
* Vendor Attahment Tab.
*/
function VendorAttahmentTab() {
return (
<div>
<Dragzone
initialFiles={[]}
onDrop={null}
onDeleteFile={[]}
hint={'Attachments: Maxiumum size: 20MB'}
/>
</div>
);
}
export default VendorAttahmentTab;

View File

@@ -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 (
<div className={'tab-panel--financial'}>
<Row>
<Col xs={6}>
{/*------------ Opening balance at -----------*/}
<FastField name={'opening_balance_at'}>
{({ form, field: { value }, meta: { error, touched } }) => (
<FormGroup
label={<T id={'opening_balance_at'} />}
className={classNames('form-group--select-list', Classes.FILL)}
intent={inputIntent({ error, touched })}
inline={true}
helperText={<ErrorMessage name="opening_balance_at" />}
>
<DateInput
{...momentFormatter('YYYY/MM/DD')}
value={tansformDateValue(value)}
popoverProps={{ position: Position.BOTTOM, minimal: true }}
disabled={vendorId}
/>
</FormGroup>
)}
</FastField>
{/*------------ Opening balance -----------*/}
<FastField name={'opening_balance'}>
{({ field, field: { value }, meta: { error, touched } }) => (
<FormGroup
label={<T id={'opening_balance'} />}
className={classNames(
'form-group--opening-balance',
Classes.FILL,
)}
intent={inputIntent({ error, touched })}
inline={true}
>
<MoneyInputGroup
value={value}
onChange={field.onChange}
prefix={'$'}
inputGroupProps={{
fill: true,
...field,
}}
disabled={vendorId}
/>
</FormGroup>
)}
</FastField>
{/*------------ Currency -----------*/}
<FastField name={'currency_code'}>
{({ form, field: { value }, meta: { error, touched } }) => (
<FormGroup
label={<T id={'currency'} />}
className={classNames(
'form-group--select-list',
'form-group--balance-currency',
Classes.FILL,
)}
inline={true}
>
<CurrencySelectList
currenciesList={currenciesList}
selectedCurrencyCode={value}
onCurrencySelected={(currency) => {
form.setFieldValue('currency_code', currency.currency_code);
}}
disabled={vendorId}
/>
</FormGroup>
)}
</FastField>
</Col>
</Row>
</div>
);
}
export default compose(
withCurrencies(({ currenciesList }) => ({ currenciesList })),
)(VendorFinanicalPanelTab);

View File

@@ -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 (
<div className={classNames(CLASSES.PAGE_FORM_FLOATING_ACTIONS)}>
<Button
disabled={isSubmitting}
intent={Intent.PRIMARY}
type="submit"
onClick={(event) => {
saveInvoke(onSubmitClick, event);
}}
>
{vendor ? <T id={'edit'} /> : <T id={'save'} />}
</Button>
<Button
disabled={isSubmitting}
intent={Intent.PRIMARY}
className={'ml1'}
name={'save_and_new'}
type="submit"
onClick={(event) => {
saveInvoke(onSubmitAndNewClick, event);
}}
>
<T id={'save_new'} />
</Button>
<Button
className={'ml1'}
onClick={(event) => {
saveInvoke(onCancelClick, event);
}}
>
<T id={'close'} />
</Button>
</div>
);
}

View File

@@ -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 (
<div className={classNames(CLASSES.PAGE_FORM, CLASSES.PAGE_FORM_CUSTOMER)}>
<Formik
validationSchema={
isNewMode ? CreateVendorFormSchema : EditVendorFormSchema
}
initialValues={initialValues}
onSubmit={handleFormSubmit}
>
{({ isSubmitting }) => (
<Form>
<VendorFormPrimarySection />
<VendorFormAfterPrimarySection />
<VendorTabs vendor={vendorId} />
<VendorFloatingActions
isSubmitting={isSubmitting}
vendor={vendorId}
onCancelClick={handleCancelClick}
onSubmitAndNewClick={handleSubmitAndNewClick}
/>
</Form>
)}
</Formik>
</div>
);
}
export default compose(
withVendorDetail(),
withDashboardActions,
withVendorActions,
)(VendorForm);

View File

@@ -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;

View File

@@ -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 (
<div className={classNames(CLASSES.PAGE_FORM_HEADER)}>
{/*------------ Vendor email -----------*/}
<FastField name={'email'}>
{({ field, meta: { error, touched } }) => (
<FormGroup
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name={'email'} />}
className={'form-group--email'}
label={<T id={'vendor_email'} />}
inline={true}
>
<InputGroup {...field} />
</FormGroup>
)}
</FastField>
{/*------------ Phone number -----------*/}
<FormGroup
className={'form-group--phone-number'}
label={<T id={'phone_number'} />}
inline={true}
>
<ControlGroup>
<FastField name={'work_phone'}>
{({ field, meta: { error, touched } }) => (
<InputGroup
intent={inputIntent({ error, touched })}
placeholder={'Work'}
{...field}
/>
)}
</FastField>
<FastField name={'personal_phone'}>
{({ field, meta: { error, touched } }) => (
<InputGroup
intent={inputIntent({ error, touched })}
placeholder={'Mobile'}
{...field}
/>
)}
</FastField>
</ControlGroup>
</FormGroup>
{/*------------ Vendor website -----------*/}
<FastField name={'website'}>
{({ field, meta: { error, touched } }) => (
<FormGroup
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name={'website'} />}
className={'form-group--website'}
label={<T id={'website'} />}
inline={true}
>
<InputGroup {...field} />
</FormGroup>
)}
</FastField>
</div>
);
}
export default VendorFormAfterPrimarySection;

View File

@@ -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 (
<div className={classNames(CLASSES.PAGE_FORM_HEADER)}>
<div className={classNames(CLASSES.PAGE_FORM_HEADER_PRIMARY)}>
{/**----------- Vendor name -----------*/}
<FormGroup
className={classNames('form-group--contact_name')}
label={<T id={'contact_name'} />}
inline={true}
>
<ControlGroup>
<FastField name={'salutation'}>
{({ form, field: { value }, meta: { error, touched } }) => (
<SalutationList
onItemSelect={(salutation) => {
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',
)}
/>
)}
</FastField>
<FastField name={'first_name'}>
{({ field, meta: { error, touched } }) => (
<InputGroup
placeholder={'First Name'}
intent={inputIntent({ error, touched })}
className={classNames('input-group--first-name')}
{...field}
/>
)}
</FastField>
<FastField name={'last_name'}>
{({ field, meta: { error, touched } }) => (
<InputGroup
placeholder={'Last Name'}
intent={inputIntent({ error, touched })}
className={classNames('input-group--last-name')}
{...field}
/>
)}
</FastField>
</ControlGroup>
</FormGroup>
{/*----------- Company Name -----------*/}
<FastField name={'company_name'}>
{({ field, meta: { error, touched } }) => (
<FormGroup
className={classNames('form-group--company_name')}
label={<T id={'company_name'} />}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name={'company_name'} />}
inline={true}
>
<InputGroup {...field} />
</FormGroup>
)}
</FastField>
{/*----------- Display Name -----------*/}
<Field name={'display_name'}>
{({ form, field: { value }, meta: { error, touched } }) => (
<FormGroup
helperText={<ErrorMessage name={'display_name'} />}
intent={inputIntent({ error, touched })}
label={
<>
<T id={'display_name'} />
<FieldRequiredHint />
<Hint />
</>
}
className={classNames(
CLASSES.FORM_GROUP_LIST_SELECT,
CLASSES.FILL,
)}
inline={true}
>
<DisplayNameList
firstName={form.values.first_name}
lastName={form.values.last_name}
company={form.values.company_name}
salutation={form.values.salutation}
onItemSelect={(displayName) => {
form.setFieldValue('display_name', displayName.label);
}}
selectedItem={value}
popoverProps={{ minimal: true }}
/>
</FormGroup>
)}
</Field>
</div>
</div>
);
}
export default VendorFormPrimarySection;

View File

@@ -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 (
<Navbar className="navbar--dashboard-views">
<NavbarGroup align={Alignment.LEFT}>
<DashboardViewsTabs
initialViewId={customViewId}
resourceName={'vendors'}
tabs={tabs}
/>
</NavbarGroup>
</Navbar>
);
}
export default compose(
withRouter,
withDashboardActions,
withVendors(({ vendorViews }) => ({ vendorViews })),
)(VendorViewsTabs);

View File

@@ -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 (
<DashboardInsider name={'vendors-list'}>
<VendorActionsBar selectedRows={selectedRows} />
<DashboardPageContent>
<Switch>
<Route
exact={true}
// path={}
>
<VendorsViewsTabs />
<VendorsTable
loading={fetchVendors.isFetching}
onDeleteVendor={handleDeleteVendor}
onEditVendor={handleEditVendor}
onSelectedRowsChange={handleSelectedRowsChange}
/>
</Route>
</Switch>
<Alert
cancelButtonText={<T id={'cancel'} />}
confirmButtonText={<T id={'delete'} />}
icon="trash"
intent={Intent.DANGER}
isOpen={deleteVendor}
onCancel={handleCancelDeleteVendor}
onConfirm={handleConfirmDeleteVendor}
>
<p>
<FormattedHTMLMessage
id={'once_delete_this_vendor_you_will_able_to_restore_it'}
/>
</p>
</Alert>
</DashboardPageContent>
</DashboardInsider>
);
}
export default compose(
withVendorActions,
withDashboardActions,
withVendors(({ vendorTableQuery }) => ({ vendorTableQuery })),
)(VendorsList);

View File

@@ -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 <span className="avatar">{firstLettersArgs(row.display_name)}</span>;
};
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 (
<Menu>
<MenuItem
icon={<Icon icon="reader-18" />}
text={formatMessage({ id: 'view_details' })}
/>
<MenuDivider />
<MenuItem
icon={<Icon icon="pen-18" />}
text={formatMessage({ id: 'edit_vendor' })}
onClick={handleEditVendor}
/>
<MenuItem
icon={<Icon icon="trash-16" iconSize={16} />}
text={formatMessage({ id: 'delete_vendor' })}
intent={Intent.DANGER}
onClick={handleDeleteVendor}
/>
</Menu>
);
},
[formatMessage],
);
// Renders actions table cell.
const renderActionsCell = useMemo(
() => ({ cell }) => (
<Popover
content={renderContextMenu({
vendor: cell.row.original,
onEditVendor,
onDeleteVendor,
})}
position={Position.RIGHT_BOTTOM}
>
<Button icon={<Icon icon="more-h-16" iconSize={16} />} />
</Popover>
),
[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) => (
<div>
<div className={'work_phone'}>{row.work_phone}</div>
<div className={'personal_phone'}>{row.personal_phone}</div>
</div>
),
className: 'phone_number',
width: 100,
},
{
id: 'receivable_balance',
Header: formatMessage({ id: 'receivable_balance' }),
accessor: (r) => <Money amount={r.closing_balance} currency={'USD'} />,
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 (
<LoadingIndicator loading={vendorsLoading && !isLoadedBefore} mount={false}>
<DataTable
noInitialFetch={true}
columns={columns}
data={vendorItems}
onFetchData={handleFetchData}
selectionColumn={true}
expandable={false}
sticky={true}
onSelectedRowsChange={handleSelectedRowsChange}
spinnerProps={{ size: 30 }}
rowContextMenu={rowContextMenu}
pagination={true}
manualSortBy={true}
pagesCount={vendorsPageination.pagesCount}
autoResetSortBy={false}
autoResetPage={false}
initialPageSize={vendorTableQuery.page_size}
initialPageIndex={vendorTableQuery.page - 1}
/>
</LoadingIndicator>
);
}
export default compose(
withVendors(
({
vendorItems,
vendorsLoading,
vendorTableQuery,
vendorsPageination,
}) => ({
vendorItems,
vendorsLoading,
vendorsPageination,
vendorTableQuery,
}),
),
withVendorsActions,
)(VendorsTable);

View File

@@ -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 (
<div className={classNames(CLASSES.PAGE_FORM_TABS)}>
<Tabs
animate={true}
id={'vendor-tabs'}
large={true}
defaultSelectedTabId="financial"
>
<Tab
id={'financial'}
title={formatMessage({ id: 'financial_details' })}
panel={<VendorFinanicalPanelTab vendorId={vendor} />}
/>
<Tab
id={'address'}
title={formatMessage({ id: 'address' })}
panel={<CustomerAddressTabs />}
/>
<Tab
id="notes"
title={formatMessage({ id: 'notes' })}
panel={<CustomerNotePanel />}
/>
<Tab
id={'attachement'}
title={formatMessage({ id: 'attachement' })}
panel={<VendorAttahmentTab />}
/>
</Tabs>
</div>
);
}

View File

@@ -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);

View File

@@ -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);
};

View File

@@ -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',
};

View File

@@ -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',
},

View File

@@ -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);
});
});
};

View File

@@ -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);

View File

@@ -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;
});