Merge remote-tracking branch 'origin/customers'

This commit is contained in:
Ahmed Bouhuolia
2020-06-21 19:51:18 +02:00
17 changed files with 1360 additions and 69 deletions

View File

@@ -67,7 +67,7 @@ export default [
divider: true,
},
{
text: 'Financial accounting',
text: <T id={'financial_accounting'} />,
label: true,
},
{

View File

@@ -6,15 +6,36 @@ import DashboardInsider from 'components/Dashboard/DashboardInsider';
import CustomerForm from 'containers/Customers/CustomerForm';
import withDashboardActions from 'containers/Dashboard/withDashboardActions';
import withCustomersActions from './withCustomersActions';
import { compose } from 'utils';
function Customer({}) {
function Customer({
// #withDashboardActions
changePageTitle,
formik,
//#withCustomersActions
requestFetchCustomers,
}) {
const { id } = useParams();
const history = useHistory();
const fetchCustomers = useQuery('customers-list', () =>
requestFetchCustomers({}),
);
const fetchCustomerDatails =useQuery(id && ['customer-detail',id],()=>requestFetchCustomers())
return (
<DashboardInsider name={'customer-form'}>
<CustomerForm />
<DashboardInsider
// formik={formik}
loading={ fetchCustomerDatails.isFetching || fetchCustomers.isFetching}
name={'customer-form'}
>
<CustomerForm customerId={id} />
</DashboardInsider>
);
}
export default Customer;
export default compose(withDashboardActions, withCustomersActions)(Customer);

View File

@@ -0,0 +1,132 @@
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 FilterDropdown from 'components/FilterDropdown';
import { If } from 'components';
import withResourceDetail from 'containers/Resources/withResourceDetails';
import withDashboardActions from 'containers/Dashboard/withDashboardActions';
import addCustomersTableQueries from 'containers/Customers/withCustomersActions';
import { compose } from 'utils';
import withCustomersActions from 'containers/Customers/withCustomersActions';
const CustomerActionsBar = ({
// #withResourceDetail
resourceFields,
//#withCustomersActions
addCustomersTableQueries,
// #ownProps
selectedRows = [],
onFilterChanged,
onBulkDelete,
}) => {
const [filterCount, setFilterCount] = useState(0);
const history = useHistory();
const { formatMessage } = useIntl();
const onClickNewCustomer = useCallback(() => {
history.push('/customers/new');
}, [history]);
const filterDropdown = FilterDropdown({
fields: resourceFields,
onFilterChange: (filterConditions) => {
setFilterCount(filterConditions.length || 0);
addCustomersTableQueries({
filter_roles: filterConditions || '',
});
onFilterChanged && onFilterChanged(filterConditions);
},
});
const hasSelectedRows = useMemo(() => selectedRows.length > 0, [
selectedRows,
]);
const handleBulkDelete = useCallback(() => {
onBulkDelete && onBulkDelete(selectedRows.map((r) => r.id));
}, [onBulkDelete, selectedRows]);
return (
<DashboardActionsBar>
<NavbarGroup>
<Button
className={Classes.MINIMAL}
icon={<Icon icon={'plus'} />}
text={<T id={'new_customer'} />}
onClick={onClickNewCustomer}
/>
<NavbarDivider />
<Popover
content={filterDropdown}
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>
<If condition={hasSelectedRows}>
<Button
className={Classes.MINIMAL}
icon={<Icon icon="trash-16" iconSize={16} />}
text={<T id={'delete'} />}
intent={Intent.DANGER}
onClick={handleBulkDelete}
/>
</If>
<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>
);
};
const mapStateToProps = (state, props) => ({
resourceName: 'customers',
});
const withCustomersActionsBar = connect(mapStateToProps);
export default compose(
withCustomersActionsBar,
withResourceDetail(({ resourceFields }) => ({
resourceFields,
})),
withCustomersActions,
)(CustomerActionsBar);

View File

@@ -0,0 +1,246 @@
import React, { useState, useMemo, useCallback, useEffect } from 'react';
import * as Yup from 'yup';
import { useFormik } from 'formik';
import {
FormGroup,
MenuItem,
Intent,
InputGroup,
HTMLSelect,
Button,
Classes,
} from '@blueprintjs/core';
import { Row, Col } from 'react-grid-system';
import { FormattedMessage as T, useIntl } from 'react-intl';
import ErrorMessage from 'components/ErrorMessage';
const CustomerBillingAddress = ({
formik: { errors, touched, setFieldValue, getFieldProps },
}) => {
return (
<div className={'customer-form'}>
<Row gutterWidth={16} className={'customer-form__tabs-section'}>
<Col width={404}>
<h4>
<T id={'billing_address'} />
</h4>
<FormGroup
label={<T id={'country'} />}
className={'form-group--journal-number'}
intent={
errors.billing_address_country &&
touched.billing_address_country &&
Intent.DANGER
}
inline={true}
helperText={
<ErrorMessage
name="billing_address_country"
{...{ errors, touched }}
/>
}
>
<InputGroup
intent={
errors.billing_address_country &&
touched.billing_address_country &&
Intent.DANGER
}
{...getFieldProps('billing_address_country')}
/>
</FormGroup>
<FormGroup
label={<T id={'city_town'} />}
className={'form-group--journal-number'}
intent={
errors.billing_address_city &&
touched.billing_address_city &&
Intent.DANGER
}
inline={true}
helperText={
<ErrorMessage
name="billing_address_city"
{...{ errors, touched }}
/>
}
>
<InputGroup
intent={
errors.billing_address_city &&
touched.billing_address_city &&
Intent.DANGER
}
{...getFieldProps('billing_address_city')}
/>
</FormGroup>
<FormGroup
label={<T id={'state'} />}
className={'form-group--journal-number'}
intent={
errors.billing_address_state &&
touched.billing_address_state &&
Intent.DANGER
}
inline={true}
helperText={
<ErrorMessage
name="billing_address_state"
{...{ errors, touched }}
/>
}
>
<InputGroup
intent={
errors.billing_address_state &&
touched.billing_address_state &&
Intent.DANGER
}
{...getFieldProps('billing_address_state')}
/>
</FormGroup>
<FormGroup
label={<T id={'zip_code'} />}
intent={
errors.billing_address_zipcode &&
touched.billing_address_zipcode &&
Intent.DANGER
}
inline={true}
helperText={
<ErrorMessage
name="billing_address_zipcode"
{...{ errors, touched }}
/>
}
>
<InputGroup
intent={
errors.billing_address_zipcode &&
touched.billing_address_zipcode &&
Intent.DANGER
}
{...getFieldProps('billing_address_zipcode')}
/>
</FormGroup>
</Col>
<Col width={404}>
<h4>
<T id={'shipping_address'} />
</h4>
<FormGroup
label={<T id={'country'} />}
className={'form-group--journal-number'}
intent={
errors.shipping_address_country &&
touched.shipping_address_country &&
Intent.DANGER
}
inline={true}
helperText={
<ErrorMessage
name="shipping_address_country"
{...{ errors, touched }}
/>
}
>
<InputGroup
intent={
errors.shipping_address_country &&
touched.shipping_address_country &&
Intent.DANGER
}
{...getFieldProps('shipping_address_country')}
/>
</FormGroup>
<FormGroup
label={<T id={'city_town'} />}
className={'form-group--journal-number'}
intent={
errors.shipping_address_city &&
touched.shipping_address_city &&
Intent.DANGER
}
inline={true}
helperText={
<ErrorMessage
name="shipping_address_city"
{...{ errors, touched }}
/>
}
>
<InputGroup
intent={
errors.shipping_address_city &&
touched.shipping_address_city &&
Intent.DANGER
}
{...getFieldProps('shipping_address_city')}
/>
</FormGroup>
<FormGroup
label={<T id={'state'} />}
className={'form-group--journal-number'}
intent={
errors.shipping_address_state &&
touched.shipping_address_state &&
Intent.DANGER
}
inline={true}
helperText={
<ErrorMessage
name="shipping_address_state"
{...{ errors, touched }}
/>
}
>
<InputGroup
intent={
errors.shipping_address_state &&
touched.shipping_address_state &&
Intent.DANGER
}
{...getFieldProps('shipping_address_state')}
/>
</FormGroup>
<FormGroup
label={<T id={'zip_code'} />}
intent={
errors.shipping_address_zipcode &&
touched.shipping_address_zipcode &&
Intent.DANGER
}
inline={true}
helperText={
<ErrorMessage
name="shipping_address_zipcode"
{...{ errors, touched }}
/>
}
>
<InputGroup
intent={
errors.shipping_address_zipcode &&
touched.shipping_address_zipcode &&
Intent.DANGER
}
{...getFieldProps('shipping_address_zipcode')}
/>
</FormGroup>
</Col>
</Row>
</div>
);
};
export default CustomerBillingAddress;

View File

@@ -0,0 +1,23 @@
import React, {
useMemo,
useState,
useEffect,
useRef,
useCallback,
} from 'react';
import Dragzone from 'components/Dragzone';
function CustomerAttachmentTabs() {
return (
<div>
<Dragzone
initialFiles={[]}
onDrop={null}
onDeleteFile={[]}
hint={'Attachments: Maxiumum size: 20MB'}
/>
</div>
);
}
export default CustomerAttachmentTabs;

View File

@@ -9,8 +9,6 @@ import {
Button,
Classes,
Checkbox,
RadioGroup,
Radio,
} from '@blueprintjs/core';
import { Row, Col } from 'react-grid-system';
import { FormattedMessage as T, useIntl } from 'react-intl';
@@ -21,28 +19,51 @@ import classNames from 'classnames';
import AppToaster from 'components/AppToaster';
import ErrorMessage from 'components/ErrorMessage';
import Icon from 'components/Icon';
import MoneyInputGroup from 'components/MoneyInputGroup';
import Dragzone from 'components/Dragzone';
import CustomersTabs from 'containers/Customers/CustomersTabs';
import RadioCustomer from 'containers/Customers/RadioCustomer';
import withDashboardActions from 'containers/Dashboard/withDashboardActions';
import withCustomerDetail from './withCustomerDetail';
import withCustomersActions from './withCustomersActions';
import RadioCustomer from './RadioCustomer';
import withCustomerDetail from 'containers/Customers/withCustomerDetail';
import withCustomersActions from 'containers/Customers/withCustomersActions';
import withMediaActions from 'containers/Media/withMediaActions';
import withCustomers from 'containers/Customers//withCustomers';
import useMedia from 'hooks/useMedia';
import { compose, handleStringChange } from 'utils';
import withCustomers from './withCustomers';
import { compose } from 'utils';
function CustomerForm({
// #withDashboardActions
changePageTitle,
//#withCustomers
customers,
//#withCustomerDetail
customerDetail,
//#withCustomersActions
requestSubmitCustomer,
requestFetchCustomers,
requestEditCustomer,
// #withMediaActions
requestSubmitMedia,
requestDeleteMedia,
}) {
const { formatMessage } = useIntl();
const history = useHistory();
const {
setFiles,
saveMedia,
deletedFiles,
setDeletedFiles,
deleteMedia,
} = useMedia({
saveCallback: requestSubmitMedia,
deleteCallback: requestDeleteMedia,
});
const validationSchema = Yup.object().shape({
customer_type: Yup.string()
@@ -51,40 +72,79 @@ function CustomerForm({
.label(formatMessage({ id: 'customer_type_' })),
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(),
work_phone: Yup.string(),
work_phone: Yup.number(),
active: Yup.boolean(),
billing_address_city: Yup.string().trim(),
billing_address_country: Yup.string().trim(),
billing_address_email: Yup.string().email(),
billing_address_zipcode: Yup.number().nullable(),
billing_address_phone: Yup.number(),
billing_address_state: Yup.string().trim(),
shipping_address_city: Yup.string().trim(),
shipping_address_country: Yup.string().trim(),
shipping_address_email: Yup.string().email(),
shipping_address_zipcode: Yup.number().nullable(),
shipping_address_phone: Yup.number(),
shipping_address_state: Yup.string().trim(),
});
useEffect(() => {
changePageTitle(formatMessage({ id: 'new_customer' }));
}, [changePageTitle, formatMessage]);
//business
const initialValues = useMemo(
const defaultInitialValues = useMemo(
() => ({
customer_type: 'business',
first_name: '',
last_name: '',
company_name: '',
display_name: '',
// email: '',
email: '',
work_phone: '',
active: true,
billing_address_city: '',
billing_address_country: '',
billing_address_zipcode: null,
billing_address_phone: '',
billing_address_state: '',
shipping_address_city: '',
shipping_address_country: '',
shipping_address_zipcode: null,
shipping_address_phone: '',
shipping_address_state: '',
}),
[],
);
const {
getFieldProps,
setFieldValue,
values,
touched,
errors,
handleSubmit,
isSubmitting,
} = useFormik({
const initialValues = useMemo(
() => ({
...(customerDetail
? {
...pick(customerDetail, Object.keys(defaultInitialValues)),
}
: {
...defaultInitialValues,
}),
}),
[customerDetail, defaultInitialValues],
);
useEffect(() => {
customerDetail && customerDetail.id
? changePageTitle(formatMessage({ id: 'edit_customer_details' }))
: changePageTitle(formatMessage({ id: 'new_customer' }));
}, [changePageTitle, customerDetail, formatMessage]);
const formik = useFormik({
enableReinitialize: true,
validationSchema: validationSchema,
initialValues: {
@@ -92,46 +152,176 @@ function CustomerForm({
},
onSubmit: (values, { setSubmitting, resetForm, setErrors }) => {
requestSubmitCustomer(values)
.then((response) => {
AppToaster.show({
message: formatMessage({
id: 'the_customer_has_been_successfully_created',
}),
intent: Intent.SUCCESS,
const formValues = { ...values };
if (customerDetail && customerDetail.id) {
requestEditCustomer(customerDetail.id, formValues)
.then((response) => {
AppToaster.show({
message: formatMessage({
id: 'the_item_customer_has_been_successfully_edited',
}),
intent: Intent.SUCCESS,
});
setSubmitting(false);
history.push('/customers');
resetForm();
})
.catch((errors) => {
setSubmitting(false);
});
})
.catch((errors) => {
setSubmitting(false);
});
} else {
requestSubmitCustomer(formValues)
.then((response) => {
AppToaster.show({
message: formatMessage({
id: 'the_customer_has_been_successfully_created',
}),
intent: Intent.SUCCESS,
});
history.push('/customers');
})
.catch((errors) => {
setSubmitting(false);
});
}
},
});
const requiredSpan = useMemo(() => <span class="required">*</span>, []);
const handleCustomerTypeCahange = useCallback(
(value) => {
setFieldValue('customer_type', value);
formik.setFieldValue('customer_type', value);
},
[setFieldValue],
[formik.setFieldValue],
);
console.log(customers, 'ER');
console.log(formik.values, 'ER');
const { errors, touched, getFieldProps, values, isSubmitting } = useMemo(
() => formik,
[formik],
);
const fetch = useQuery('customers-table', (key) => requestFetchCustomers());
const initialAttachmentFiles = useMemo(() => {
return customerDetail && customerDetail.media
? customerDetail.media.map((attach) => ({
preview: attach.attachment_file,
upload: true,
metadata: { ...attach },
}))
: [];
}, []);
const handleDropFiles = useCallback((_files) => {
setFiles(_files.filter((file) => file.uploaded === false));
}, []);
const handleDeleteFile = useCallback(
(_deletedFiles) => {
_deletedFiles.forEach((deletedFile) => {
if (deletedFile.uploaded && deletedFile.metadata.id) {
setDeletedFiles([...deletedFiles, deletedFile.metadata.id]);
}
});
},
[setDeletedFiles, deletedFiles],
);
const handleCancelClickBtn = () => {
history.goBack();
};
return (
<div className={'customer-form'}>
<form onSubmit={handleSubmit}>
<form onSubmit={formik.handleSubmit}>
<div className={'customer-form__primary-section'}>
<RadioCustomer
selectedValue={values.customer_type}
selectedValue={formik.values.customer_type}
onChange={handleCustomerTypeCahange}
className={'form-group--customer-type'}
/>
<Row>
<Col md={3.5} className={'form-group--contact-name'}>
<FormGroup
label={<T id={'contact_name'} />}
inline={true}
intent={
formik.errors.first_name &&
formik.touched.first_name &&
Intent.DANGER
}
helperText={
<ErrorMessage name={'first_name'} {...{ errors, touched }} />
}
// className={'form-group--contact-name'}
>
<InputGroup
placeholder={'First Name'}
intent={
formik.errors.first_name &&
formik.touched.first_name &&
Intent.DANGER
}
{...formik.getFieldProps('first_name')}
/>
</FormGroup>
</Col>
<Col md={2}>
<FormGroup
inline={true}
intent={
formik.errors.last_name &&
formik.touched.last_name &&
Intent.DANGER
}
helperText={
<ErrorMessage name={'last_name'} {...{ errors, touched }} />
}
// className={'form-group--contact-name'}
>
<InputGroup
placeholder={'Last Name'}
intent={
formik.errors.last_name &&
formik.touched.last_name &&
Intent.DANGER
}
{...formik.getFieldProps('last_name')}
/>
</FormGroup>
</Col>
</Row>
{/* Company Name */}
<FormGroup
label={<T id={'company_name'} />}
className={'form-group--company_name'}
labelInfo={requiredSpan}
intent={
formik.errors.company_name &&
formik.touched.company_name &&
Intent.DANGER
}
inline={true}
helperText={
<ErrorMessage {...{ errors, touched }} name={'company_name'} />
}
>
<InputGroup
intent={
formik.errors.company_name &&
formik.touched.company_name &&
Intent.DANGER
}
{...formik.getFieldProps('company_name')}
/>
</FormGroup>
{/* Display Name */}
<FormGroup
label={<T id={'display_name'} />}
className={'form-group--name'}
intent={
errors.display_name && touched.display_name && Intent.DANGER
formik.errors.display_name &&
formik.touched.display_name &&
Intent.DANGER
}
inline={true}
helperText={
@@ -140,23 +330,96 @@ function CustomerForm({
>
<InputGroup
intent={
errors.display_name && touched.display_name && Intent.DANGER
formik.errors.display_name &&
formik.touched.display_name &&
Intent.DANGER
}
{...getFieldProps('display_name')}
{...formik.getFieldProps('display_name')}
/>
</FormGroup>
</div>
<Row>
{/* Email */}
<Col md={6}>
<FormGroup
label={<T id={'email'} />}
intent={
formik.errors.email && formik.touched.email && Intent.DANGER
}
helperText={
<ErrorMessage name={'email'} {...{ errors, touched }} />
}
className={'form-group--email'}
inline={true}
>
<InputGroup
intent={
formik.errors.email && formik.touched.email && Intent.DANGER
}
{...formik.getFieldProps('email')}
/>
</FormGroup>
</Col>
{/* Active checkbox */}
<FormGroup label={' '} inline={true} className={'form-group--active'}>
<Checkbox
inline={true}
label={<T id={'active'} />}
defaultChecked={formik.values.active}
{...formik.getFieldProps('active')}
/>
</FormGroup>
</Row>
<FormGroup
label={<T id={'phone_number'} />}
intent={
formik.errors.work_phone &&
formik.touched.work_phone &&
Intent.DANGER
}
helperText={
<ErrorMessage name={'work_phone'} {...{ errors, touched }} />
}
className={'form-group--phone-number'}
inline={true}
>
<InputGroup
intent={
formik.errors.work_phone &&
formik.touched.work_phone &&
Intent.DANGER
}
{...formik.getFieldProps('work_phone')}
/>
</FormGroup>
<CustomersTabs formik={formik} />
<div class="form__floating-footer">
<Button intent={Intent.PRIMARY} disabled={isSubmitting} type="submit">
<T id={'save'} />
{customerDetail && customerDetail.id ? (
<T id={'edit'} />
) : (
<T id={'save'} />
)}
</Button>
<Button
disabled={isSubmitting}
intent={Intent.PRIMARY}
className={'ml1'}
name={'save_and_new'}
>
<T id={'save_new'} />
</Button>
<Button className={'ml1'} disabled={isSubmitting}>
<T id={'save_as_draft'} />
</Button>
<Button className={'ml1'}>
<Button className={'ml1'} onClick={handleCancelClickBtn}>
<T id={'close'} />
</Button>
</div>

View File

@@ -0,0 +1,36 @@
import React from 'react';
import {
Button,
Classes,
FormGroup,
InputGroup,
Intent,
TextArea,
MenuItem,
} from '@blueprintjs/core';
import { FormattedMessage as T, useIntl } from 'react-intl';
import ErrorMessage from 'components/ErrorMessage';
const CustomerBillingAddress = ({
formik: { errors, touched, setFieldValue, getFieldProps },
}) => {
return (
<div>
<FormGroup
label={<T id={'note'} />}
// className={'form-group--description'}
intent={errors.note && touched.note && Intent.DANGER}
helperText={<ErrorMessage name="note" {...{ errors, touched }} />}
inline={true}
>
<TextArea
growVertically={true}
large={true}
{...getFieldProps('note')}
/>
</FormGroup>
</div>
);
};
export default CustomerBillingAddress;

View File

@@ -0,0 +1,164 @@
import React, { useState, useEffect, useCallback, useMemo } from 'react';
import {
Button,
Popover,
Menu,
MenuItem,
MenuDivider,
Position,
} from '@blueprintjs/core';
import { FormattedMessage as T, useIntl } from 'react-intl';
import DataTable from 'components/DataTable';
import Icon from 'components/Icon';
import { useUpdateEffect } from 'hooks';
import LoadingIndicator from 'components/LoadingIndicator';
import withCustomers from './withCustomers';
import { compose } from 'utils';
const CustomerTable = ({
loading,
//#withCustomers
customers,
customersLoading,
//#props
onEditCustomer,
onDeleteCustomer,
onFetchData,
onSelectedRowsChange,
}) => {
const { formatMessage } = useIntl();
const [initialMount, setInitialMount] = useState(false);
useUpdateEffect(() => {
if (!customersLoading) {
setInitialMount(true);
}
}, [customersLoading, setInitialMount]);
const handleEditCustomer = useCallback(
(customer) => () => {
onEditCustomer && onEditCustomer(customer);
},
[onEditCustomer],
);
const handleDeleteCustomer = useCallback(
(customer) => () => {
onDeleteCustomer && onDeleteCustomer(customer);
},
[onDeleteCustomer],
);
const actionMenuList = useCallback((customer) => (
<Menu>
<MenuItem text={<T id={'view_details'} />} />
<MenuDivider />
<MenuItem
text={<T id={'edit_customer'} />}
onClick={handleEditCustomer(customer)}
/>
<MenuItem
text={<T id={'delete_customer'} />}
onClick={handleDeleteCustomer(customer)}
/>
</Menu>
));
const columns = useMemo(
() => [
{
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: 'work_phone',
className: 'phone_number',
width: 100,
},
{
id: 'receivable_balance',
Header: formatMessage({ id: 'receivable_balance' }),
// accessor: '',
className: 'receivable_balance',
width: 100,
},
{
id: 'actions',
Cell: ({ cell }) => (
<Popover
content={actionMenuList(cell.row.original)}
position={Position.RIGHT_BOTTOM}
>
<Button icon={<Icon icon="more-h-16" iconSize={16} />} />
</Popover>
),
className: 'actions',
width: 50,
},
],
[actionMenuList, formatMessage],
);
const selectionColumn = useMemo(
() => ({
minWidth: 42,
width: 42,
maxWidth: 42,
}),
[],
);
const handleFetchDate = useCallback((...args) => {
onFetchData && onFetchData(...args);
});
const handleSelectedRowsChange = useCallback(
(selectedRows) => {
onSelectedRowsChange &&
onSelectedRowsChange(selectedRows.map((s) => s.original));
},
[onSelectedRowsChange],
);
return (
<LoadingIndicator loading={loading} mount={false}>
<DataTable
noInitialFetch={true}
columns={columns}
data={customers}
selectionColumn={selectionColumn}
onFetchData={handleFetchDate}
expandable={true}
treeGraph={true}
onSelectedRowsChange={handleSelectedRowsChange}
loading={customersLoading && !initialMount}
spinnerProps={{ size: 30 }}
/>
</LoadingIndicator>
);
};
export default compose(
withCustomers(({ customers, customersLoading }) => ({
customers,
customersLoading,
})),
)(CustomerTable);

View File

@@ -0,0 +1,232 @@
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 CustomersTable from 'containers/Customers/CustomerTable';
import CustomerActionsBar from 'containers/Customers/CustomerActionsBar';
import withCustomersActions from 'containers/Customers/withCustomersActions';
import withResourceActions from 'containers/Resources/withResourcesActions';
import withDashboardActions from 'containers/Dashboard/withDashboardActions';
import { compose } from 'utils';
function CustomersList({
// #withDashboardActions
changePageTitle,
// #withResourceActions
requestFetchResourceViews,
requestFetchResourceFields,
//#withCustomersActions
requestFetchCustomers,
requestDeleteCustomer,
requestDeleteBulkCustomers,
addCustomersTableQueries,
}) {
const [deleteCustomer, setDeleteCustomer] = useState(false);
const [selectedRows, setSelectedRows] = useState([]);
const [tableLoading, setTableLoading] = useState(false);
const [bulkDelete, setBulkDelete] = useState(false);
const { formatMessage } = useIntl();
const history = useHistory();
useEffect(() => {
changePageTitle(formatMessage({ id: 'customers_list' }));
}, [changePageTitle]);
// Fetch customers resource views and fields.
// const fetchHook = useQuery('resource-customers', () => {
// return Promise.all([
// requestFetchResourceViews('customers'),
// requestFetchResourceFields('customers'),
// ]);
// });
const fetchCustomers = useQuery('customers-table', () => {
requestFetchCustomers({});
});
const handleEditCustomer = useCallback(
(cusomter) => {
history.push(`/customers/${cusomter.id}/edit`);
},
[history],
);
// Handle click delete customer.
const handleDeleteCustomer = useCallback(
(customer) => {
setDeleteCustomer(customer);
},
[setDeleteCustomer],
);
// Handle cancel delete the customer.
const handleCancelDeleteCustomer = useCallback(() => {
setDeleteCustomer(false);
}, [setDeleteCustomer]);
// handle confirm delete customer.
const handleConfirmDeleteCustomer = useCallback(() => {
requestDeleteCustomer(deleteCustomer.id).then(() => {
AppToaster.show({
message: formatMessage({
id: 'the_customer_has_been_successfully_deleted',
}),
intent: Intent.SUCCESS,
});
setDeleteCustomer(false);
});
}, [requestDeleteCustomer, deleteCustomer, formatMessage]);
// Handle fetch data table.
const handleFetchData = useCallback(
({ pageIndex, pageSize, sortBy }) => {
addCustomersTableQueries({
...(sortBy.length > 0
? {
column_sort_order: sortBy[0].id,
sort_order: sortBy[0].desc ? 'desc' : 'asc',
}
: {}),
});
fetchCustomers.refetch();
},
[fetchCustomers, addCustomersTableQueries],
);
// Handle selected rows change.
const handleSelectedRowsChange = useCallback(
(customer) => {
setSelectedRows(customer);
},
[setSelectedRows],
);
useEffect(() => {
if (tableLoading && !fetchCustomers.isFetching) {
setTableLoading(false);
}
}, [tableLoading, fetchCustomers.isFetching]);
// Calculates the data table selected rows count.
const selectedRowsCount = useMemo(() => Object.values(selectedRows).length, [
selectedRows,
]);
// Handle filter change to re-fetch the items.
const handleFilterChanged = useCallback(
(filterConditions) => {
addCustomersTableQueries({
filter_roles: filterConditions || '',
});
},
[fetchCustomers],
);
// Handle items bulk delete button click.,
const handleBulkDelete = useCallback(
(itemsIds) => {
setBulkDelete(itemsIds);
},
[setBulkDelete],
);
// Handle cancel accounts bulk delete.
const handleCancelBulkDelete = useCallback(() => {
setBulkDelete(false);
}, []);
// Handle confirm items bulk delete.
const handleConfirmBulkDelete = useCallback(() => {
requestDeleteBulkCustomers(bulkDelete)
.then(() => {
setBulkDelete(false);
AppToaster.show({
message: formatMessage({
id: 'the_customers_has_been_successfully_deleted',
}),
intent: Intent.SUCCESS,
});
})
.catch((errors) => {
setBulkDelete(false);
});
}, [requestDeleteBulkCustomers, bulkDelete, formatMessage]);
return (
<DashboardInsider
loading={fetchCustomers.isFetching}
name={'customers-list'}
>
<CustomerActionsBar
selectedRows={selectedRows}
onFilterChanged={handleFilterChanged}
onBulkDelete={handleBulkDelete}
/>
<DashboardPageContent>
<CustomersTable
loadong={tableLoading}
onDeleteCustomer={handleDeleteCustomer}
onEditCustomer={handleEditCustomer}
onfetchData={handleFetchData}
onSelectedRowsChange={handleSelectedRowsChange}
/>
<Alert
cancelButtonText={<T id={'cancel'} />}
confirmButtonText={<T id={'delete'} />}
icon="trash"
intent={Intent.DANGER}
isOpen={deleteCustomer}
onCancel={handleCancelDeleteCustomer}
onConfirm={handleConfirmDeleteCustomer}
>
<p>
<FormattedHTMLMessage
id={'once_delete_this_customer_you_will_able_to_restore_it'}
/>
</p>
</Alert>
<Alert
cancelButtonText={<T id={'cancel'} />}
confirmButtonText={`${formatMessage({
id: 'delete',
})} (${selectedRowsCount})`}
icon="trash"
intent={Intent.DANGER}
isOpen={bulkDelete}
onCancel={handleCancelBulkDelete}
onConfirm={handleConfirmBulkDelete}
>
<p>
<T
id={'once_delete_these_customers_you_will_not_able_restore_them'}
/>
</p>
</Alert>
</DashboardPageContent>
</DashboardInsider>
);
}
export default compose(
withResourceActions,
withDashboardActions,
withCustomersActions,
)(CustomersList);

View File

@@ -0,0 +1,41 @@
import React, { useState, useCallback } from 'react';
import { Tabs, Tab } from '@blueprintjs/core';
import { FormattedMessage as T, useIntl } from 'react-intl';
import CustomerAddressTabs from './CustomerAddressTabs';
import CustomerNotTabs from './CustomerNotTabs';
import CustomerAttachmentTabs from './CustomerAttachmentTabs';
function CustomersTabs({ formik }) {
const [animate, setAnimate] = useState(true);
const { formatMessage } = useIntl();
const handleChangeTabs = useCallback(() => {}, []);
return (
<div>
<Tabs animate={animate} id={'customer-tabs'} large={true}>
<Tab
id={'other'}
title={formatMessage({ id: 'other' })}
panel={'Other'}
/>
<Tab
id={'address'}
title={formatMessage({ id: 'address' })}
panel={<CustomerAddressTabs formik={formik} />}
/>
<Tab
id={'attachement'}
title={formatMessage({ id: 'attachement' })}
panel={<CustomerAttachmentTabs />}
/>
<Tab
id={'note'}
title={formatMessage({ id: 'note' })}
panel={<CustomerNotTabs formik={formik} />}
/>
</Tabs>
</div>
);
}
export default CustomersTabs;

View File

@@ -3,15 +3,16 @@ import {
fetchCustomers,
submitCustomer,
editCustomer,
deleteCustomer,
} from 'store/customers/customers.actions';
import t from 'store/types';
export const mapDispatchToProps = (dispatch) => ({
requestFetchCustomers: (query) => dispatch(fetchCustomers({ query })),
// requestDeleteCustomer: (id) => dispatch(deleteCustomer({ id })),
requestDeleteCustomer: (id) => dispatch(deleteCustomer({ id })),
// requestDeleteBulkCustomers:(ids)=>dispatch(deleteBulkCustomers({ids})),
requestSubmitCustomer: (form) => dispatch(submitCustomer({ form })),
// requestEditCustomer: (id, form) => dispatch(editCustomer({ id, form })),
requestEditCustomer: (id, form) => dispatch(editCustomer({ id, form })),
addCustomersTableQueries: (queries) =>
dispatch({

View File

@@ -450,7 +450,7 @@ export default {
as: 'As',
receivable_aging_summary: 'Receivable Aging Summary',
customers: 'Customers',
new_customers:'New Customers',
new_customers: 'New Customers',
customer_type_: 'Customer type',
display_name_: 'Display name',
new_customer: 'New Customer',
@@ -471,5 +471,35 @@ export default {
next: 'Next',
previous: 'Previous',
showing_current_page_to_total: 'Showing {currentPage} to {totalPages} of {total} entries',
new_child_account: 'New Child Account'
new_child_account: 'New Child Account',
display_name: 'Display Name',
contact_name: 'Contact Name',
company_name: 'Company Name',
other: 'Other',
address: 'Address',
attachement: 'Attachement',
country: 'Country',
city_town: 'City/Town',
state: 'State',
zip_code: 'ZIP/Code',
streat: 'Streat',
edit_customer: 'Edit Customer',
delete_customer: 'Delete Customer',
billing_address: 'Billing Address',
shipping_address: 'Shipping Address',
customers_list: 'Customers List',
edit_customer_details: 'Edit Customer Details',
receivable_balance:'Receivable balance',
the_customer_has_been_successfully_created:
'The customer has been successfully created.',
the_customer_has_been_successfully_deleted:
'The customer has been successfully deleted.',
the_customers_has_been_successfully_deleted:
'The customers have been successfully deleted.',
the_item_customer_has_been_successfully_edited:
'The item customer has been successfully edited.',
once_delete_this_customer_you_will_able_to_restore_it: `Once you delete this customer, you won\'t be able to restore it later. Are you sure you want to delete this cusomter?`,
once_delete_these_customers_you_will_not_able_restore_them:
"Once you delete these customers, you won't be able to retrieve them later. Are you sure you want to delete them?",
financial_accounting: 'Financial accounting'
};

View File

@@ -183,10 +183,11 @@ export default [
{
path: `/customers/:id/edit`,
component: LazyLoader({
// loader: () => import(),
loader: () => import('containers/Customers/Customer'),
}),
breadcrumb: 'Edit Customer',
},
{
path: `/customers/new`,
component: LazyLoader({
@@ -199,7 +200,7 @@ export default [
{
path: `/customers`,
component: LazyLoader({
// loader: () => import(''),
loader: () => import('containers/Customers/CustomersList'),
}),
breadcrumb: 'Customers',
},

View File

@@ -68,6 +68,7 @@ export const fetchCustomers = ({ query }) => {
type: t.CUSTOMER_SET,
customers: response.data.customers.results,
});
dispatch({
type: t.CUSTOMERS_PAGE_SET,
customers: response.data.customers.results,
@@ -91,3 +92,18 @@ export const fetchCustomers = ({ query }) => {
});
});
};
export const deleteCustomer = ({ id }) => {
return (dispatch) =>
new Promise((resolve, reject) => {
ApiService.delete(`customers/${id}`)
.then((response) => {
dispatch({ type: t.CUSTOMER_DELETE, id });
resolve(response);
})
.catch((error) => {
reject(error.response.data.errors || []);
});
});
};

View File

@@ -36,10 +36,14 @@ const customersReducer = createReducer(initialState, {
delete state.items[action.id];
}
},
[t.CUSTOMERS_TABLE_LOADING]: (state, action) => {
const { loading } = action.payload;
state.loading = !!loading;
},
});
export default createTableQueryReducers('customers', customersReducer);
export const getCustomerById = (state, id) => {
return state.customers[id];
return state.customers.items[id];
};

View File

@@ -1,6 +1,6 @@
import { pickItemsFromIds } from 'store/selectors';
export const getCustomersItems = (state, viewId) => {
const customersView = state.customers.views[viewId || -1];
const customersItems = state.customers.items;

View File

@@ -1,24 +1,105 @@
.customer-form {
padding: 22px;
padding: 25px;
padding-bottom: 90px;
width: 100%;
// padding-bottom: 90px;
margin-bottom: 30px;
.bp3-form-group {
// margin: 25px 20px 20px;
.bp3-label {
min-width: 100px;
width: 130px;
}
.bp3-form-content {
width: 45%;
width: 375px;
}
textarea {
min-width: 100%;
max-width: 100%;
width: 100%;
max-height: 120px;
}
}
.form-group--customer-type {
.bp3-label {
position: relative;
display: inline-block;
margin: 0 50px 30px 0px;
}
}
// .form-group--contact-name {
// .bp3-input-group .bp3-input {
// position: relative;
// // display: none;
// width: 50%;
// }
// // .row {
// // width: fit-content;
// // }
// // .#{$ns}-form-content{
// // width: 350px;
// // }
// }
h1 {
font-size: 14px;
margin-bottom: 20px;
}
&__primary-section {
background-color: #fafafa;
padding: 40px 22px 22px;
margin: -22px -22px 22px;
background-color: #fafafa;
}
&__tabs-section {
position: relative;
h4 {
margin: 0;
font-weight: 500;
margin-bottom: 20px;
font-size: 14px;
color: #828282;
}
> div:first-of-type {
padding-right: 40px !important;
}
> div ~ div {
padding-left: 40px !important;
}
}
.dropzone-container {
align-self: end;
}
.dropzone {
width: 300px;
height: 100px;
margin-right: 20px;
}
}
.form-group--contact-name {
.bp3-form-group.bp3-inline label.bp3-label {
line-height: 30px;
display: inline-block;
margin: 0 45px 0 0;
width: 200px;
}
.bp3-input-group .bp3-input {
position: relative;
// display: none;
width: 100%;
// margin-left: 30px;
}
// .row {
// width: fit-content;
// }
// .#{$ns}-form-content{
// width: 350px;
// }
}