mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-17 05:10:31 +00:00
feat: customer form styling.
feat: customers list.
This commit is contained in:
@@ -12,8 +12,6 @@ import {
|
||||
Checkbox,
|
||||
Spinner,
|
||||
ContextMenu,
|
||||
Menu,
|
||||
MenuItem,
|
||||
} from '@blueprintjs/core';
|
||||
import classnames from 'classnames';
|
||||
import { FixedSizeList } from 'react-window';
|
||||
|
||||
@@ -12,11 +12,7 @@ const CustomerBillingAddress = ({
|
||||
getFieldProps,
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
className={
|
||||
'customer-form__tabs-section customer-form__tabs-section--address'
|
||||
}
|
||||
>
|
||||
<div className={'tab-panel--address'}>
|
||||
<Row>
|
||||
<Col xs={6}>
|
||||
<h4>
|
||||
|
||||
@@ -28,7 +28,7 @@ export default function CustomerFinancialPanel({
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={'customer-form__tabs-section customer-form__tabs-section--financial'}>
|
||||
<div className={'tab-panel--financial'}>
|
||||
<Row>
|
||||
<Col xs={6}>
|
||||
<FormGroup
|
||||
|
||||
@@ -143,10 +143,46 @@ function CustomerForm({
|
||||
|
||||
useEffect(() => {
|
||||
customer && customer.id
|
||||
? changePageTitle(formatMessage({ id: 'edit_customer_details' }))
|
||||
? changePageTitle(formatMessage({ id: 'edit_customer' }))
|
||||
: changePageTitle(formatMessage({ id: 'new_customer' }));
|
||||
}, [changePageTitle, customer, formatMessage]);
|
||||
|
||||
const handleFormSubmit = (values, { setSubmitting, resetForm, setErrors }) => {
|
||||
const formValues = { ...values, status: payload.publish };
|
||||
if (customer && customer.id) {
|
||||
requestEditCustomer(customer.id, formValues)
|
||||
.then((response) => {
|
||||
AppToaster.show({
|
||||
message: formatMessage({
|
||||
id: 'the_item_customer_has_been_successfully_edited',
|
||||
}),
|
||||
intent: Intent.SUCCESS,
|
||||
});
|
||||
setSubmitting(false);
|
||||
resetForm();
|
||||
saveInvokeSubmit({ action: 'update', ...payload });
|
||||
})
|
||||
.catch((errors) => {
|
||||
setSubmitting(false);
|
||||
});
|
||||
} else {
|
||||
requestSubmitCustomer(formValues)
|
||||
.then((response) => {
|
||||
AppToaster.show({
|
||||
message: formatMessage({
|
||||
id: 'the_customer_has_been_successfully_created',
|
||||
}),
|
||||
intent: Intent.SUCCESS,
|
||||
});
|
||||
setSubmitting(false);
|
||||
saveInvokeSubmit({ action: 'new', ...payload });
|
||||
})
|
||||
.catch((errors) => {
|
||||
setSubmitting(false);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const {
|
||||
setFieldValue,
|
||||
getFieldProps,
|
||||
@@ -160,42 +196,7 @@ function CustomerForm({
|
||||
initialValues: {
|
||||
...initialValues,
|
||||
},
|
||||
onSubmit: (values, { setSubmitting, resetForm, setErrors }) => {
|
||||
const formValues = { ...values, status: payload.publish };
|
||||
if (customer && customer.id) {
|
||||
requestEditCustomer(customer.id, formValues)
|
||||
.then((response) => {
|
||||
AppToaster.show({
|
||||
message: formatMessage({
|
||||
id: 'the_item_customer_has_been_successfully_edited',
|
||||
}),
|
||||
intent: Intent.SUCCESS,
|
||||
});
|
||||
setSubmitting(false);
|
||||
resetForm();
|
||||
saveInvokeSubmit({ action: 'update', ...payload });
|
||||
})
|
||||
.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');
|
||||
setSubmitting(false);
|
||||
saveInvokeSubmit({ action: 'new', ...payload });
|
||||
})
|
||||
.catch((errors) => {
|
||||
setSubmitting(false);
|
||||
});
|
||||
}
|
||||
},
|
||||
onSubmit: handleFormSubmit,
|
||||
});
|
||||
|
||||
|
||||
|
||||
@@ -32,13 +32,14 @@ export default function CustomerFormPrimarySection({
|
||||
);
|
||||
|
||||
// Handle salutation field select.
|
||||
const handleSalutationSelect = (salutation) => {
|
||||
const handleSalutationSelect = useCallback((salutation) => {
|
||||
setFieldValue('salutation', salutation.label);
|
||||
};
|
||||
}, [setFieldValue]);
|
||||
|
||||
// Handle display name field select.
|
||||
const handleDisplayNameSelect = (displayName) => {
|
||||
const handleDisplayNameSelect = useCallback((displayName) => {
|
||||
setFieldValue('display_name', displayName.label);
|
||||
};
|
||||
}, [setFieldValue]);
|
||||
|
||||
return (
|
||||
<div className={'customer-form__primary-section-content'}>
|
||||
@@ -116,6 +117,7 @@ export default function CustomerFormPrimarySection({
|
||||
firstName={values.first_name}
|
||||
lastName={values.last_name}
|
||||
company={values.company_name}
|
||||
salutation={values.salutation}
|
||||
onItemSelect={handleDisplayNameSelect}
|
||||
popoverProps={{ minimal: true }}
|
||||
/>
|
||||
|
||||
@@ -7,28 +7,21 @@ import ErrorMessage from 'components/ErrorMessage';
|
||||
|
||||
export default function CustomerNotePanel({ errors, touched, getFieldProps }) {
|
||||
return (
|
||||
<div
|
||||
className={
|
||||
'customer-form__tabs-section customer-form__tabs-section--note'
|
||||
}
|
||||
>
|
||||
<Row>
|
||||
<Col xs={6}>
|
||||
<FormGroup
|
||||
label={<T id={'note'} />}
|
||||
className={classNames('form-group--select-list', Classes.FILL)}
|
||||
intent={errors.note && touched.note && Intent.DANGER}
|
||||
helperText={
|
||||
<ErrorMessage name="payment_date" {...{ errors, touched }} />
|
||||
}
|
||||
>
|
||||
<TextArea
|
||||
intent={errors.note && touched.note && Intent.DANGER}
|
||||
{...getFieldProps('note')}
|
||||
/>
|
||||
</FormGroup>
|
||||
</Col>
|
||||
</Row>
|
||||
<div className={'tab-panel--note'}>
|
||||
<FormGroup
|
||||
label={<T id={'note'} />}
|
||||
className={classNames('form-group--note', Classes.FILL)}
|
||||
intent={errors.note && touched.note && Intent.DANGER}
|
||||
helperText={
|
||||
<ErrorMessage name="payment_date" {...{ errors, touched }} />
|
||||
}
|
||||
>
|
||||
<TextArea
|
||||
intent={errors.note && touched.note && Intent.DANGER}
|
||||
{...getFieldProps('note')}
|
||||
/>
|
||||
</FormGroup>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useState, useEffect, useCallback, useMemo } from 'react';
|
||||
import React, { useState, useCallback, useMemo } from 'react';
|
||||
import {
|
||||
Button,
|
||||
Popover,
|
||||
@@ -6,17 +6,23 @@ import {
|
||||
MenuItem,
|
||||
MenuDivider,
|
||||
Position,
|
||||
Intent,
|
||||
} from '@blueprintjs/core';
|
||||
|
||||
import { FormattedMessage as T, useIntl } from 'react-intl';
|
||||
import DataTable from 'components/DataTable';
|
||||
import Icon from 'components/Icon';
|
||||
import { Money } from 'components';
|
||||
import { useUpdateEffect } from 'hooks';
|
||||
|
||||
import LoadingIndicator from 'components/LoadingIndicator';
|
||||
import withCustomers from './withCustomers';
|
||||
|
||||
import { compose } from 'utils';
|
||||
import { compose, firstLettersArgs, saveInvoke } from 'utils';
|
||||
|
||||
const AvatarCell = (row) => {
|
||||
return <span className="avatar">{firstLettersArgs(row.display_name)}</span>;
|
||||
};
|
||||
|
||||
const CustomerTable = ({
|
||||
loading,
|
||||
@@ -32,7 +38,6 @@ const CustomerTable = ({
|
||||
onSelectedRowsChange,
|
||||
}) => {
|
||||
const { formatMessage } = useIntl();
|
||||
|
||||
const [initialMount, setInitialMount] = useState(false);
|
||||
|
||||
useUpdateEffect(() => {
|
||||
@@ -41,36 +46,64 @@ const CustomerTable = ({
|
||||
}
|
||||
}, [customersLoading, setInitialMount]);
|
||||
|
||||
const handleEditCustomer = useCallback(
|
||||
(customer) => () => {
|
||||
onEditCustomer && onEditCustomer(customer);
|
||||
// Customers actions list.
|
||||
const renderContextMenu = useMemo(
|
||||
() => ({ customer, onEditCustomer, onDeleteCustomer }) => {
|
||||
const handleEditCustomer = () => {
|
||||
saveInvoke(onEditCustomer, customer);
|
||||
};
|
||||
const handleDeleteCustomer = () => {
|
||||
saveInvoke(onDeleteCustomer, customer);
|
||||
};
|
||||
return (
|
||||
<Menu>
|
||||
<MenuItem
|
||||
icon={<Icon icon="reader-18" />}
|
||||
text={formatMessage({ id: 'view_details' })}
|
||||
/>
|
||||
<MenuDivider />
|
||||
<MenuItem
|
||||
icon={<Icon icon="pen-18" />}
|
||||
text={formatMessage({ id: 'edit_customer' })}
|
||||
onClick={handleEditCustomer}
|
||||
/>
|
||||
<MenuItem
|
||||
icon={<Icon icon="trash-16" iconSize={16} />}
|
||||
text={formatMessage({ id: 'delete_customer' })}
|
||||
intent={Intent.DANGER}
|
||||
onClick={handleDeleteCustomer}
|
||||
/>
|
||||
</Menu>
|
||||
);
|
||||
},
|
||||
[onEditCustomer],
|
||||
[formatMessage],
|
||||
);
|
||||
|
||||
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>
|
||||
));
|
||||
// Renders actions table cell.
|
||||
const renderActionsCell = useMemo(() => ({ cell }) => (
|
||||
<Popover
|
||||
content={renderContextMenu({
|
||||
customer: cell.row.original,
|
||||
onEditCustomer,
|
||||
onDeleteCustomer,
|
||||
})}
|
||||
position={Position.RIGHT_BOTTOM}
|
||||
>
|
||||
<Button icon={<Icon icon="more-h-16" iconSize={16} />} />
|
||||
</Popover>
|
||||
), [onDeleteCustomer, onEditCustomer, renderContextMenu]);
|
||||
|
||||
const columns = useMemo(
|
||||
() => [
|
||||
{
|
||||
id: 'avatar',
|
||||
Header: '',
|
||||
accessor: AvatarCell,
|
||||
className: 'avatar',
|
||||
width: 50,
|
||||
disableResizing: true,
|
||||
disableSortBy: true,
|
||||
},
|
||||
{
|
||||
id: 'display_name',
|
||||
Header: formatMessage({ id: 'display_name' }),
|
||||
@@ -95,26 +128,20 @@ const CustomerTable = ({
|
||||
{
|
||||
id: 'receivable_balance',
|
||||
Header: formatMessage({ id: 'receivable_balance' }),
|
||||
// accessor: '',
|
||||
accessor: (r) => <Money amount={r.closing_balance} currency={'USD'} />,
|
||||
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>
|
||||
),
|
||||
Cell: renderActionsCell,
|
||||
className: 'actions',
|
||||
width: 50,
|
||||
width: 70,
|
||||
disableResizing: true,
|
||||
disableSortBy: true,
|
||||
},
|
||||
],
|
||||
[actionMenuList, formatMessage],
|
||||
[formatMessage, renderActionsCell],
|
||||
);
|
||||
|
||||
const selectionColumn = useMemo(
|
||||
@@ -138,6 +165,13 @@ const CustomerTable = ({
|
||||
[onSelectedRowsChange],
|
||||
);
|
||||
|
||||
const rowContextMenu = (cell) =>
|
||||
renderContextMenu({
|
||||
customer: cell.row.original,
|
||||
onEditCustomer,
|
||||
onDeleteCustomer,
|
||||
});
|
||||
|
||||
return (
|
||||
<LoadingIndicator loading={loading} mount={false}>
|
||||
<DataTable
|
||||
@@ -146,11 +180,12 @@ const CustomerTable = ({
|
||||
data={customers}
|
||||
selectionColumn={selectionColumn}
|
||||
onFetchData={handleFetchDate}
|
||||
expandable={true}
|
||||
treeGraph={true}
|
||||
expandable={false}
|
||||
treeGraph={false}
|
||||
onSelectedRowsChange={handleSelectedRowsChange}
|
||||
loading={customersLoading && !initialMount}
|
||||
spinnerProps={{ size: 30 }}
|
||||
rowContextMenu={rowContextMenu}
|
||||
/>
|
||||
</LoadingIndicator>
|
||||
);
|
||||
|
||||
@@ -135,11 +135,10 @@ function CustomersList({
|
||||
filter_roles: filterConditions || '',
|
||||
});
|
||||
},
|
||||
[fetchCustomers],
|
||||
[addCustomersTableQueries],
|
||||
);
|
||||
|
||||
// Handle Customers bulk delete button click.,
|
||||
|
||||
const handleBulkDelete = useCallback(
|
||||
(customersIds) => {
|
||||
setBulkDelete(customersIds);
|
||||
@@ -184,7 +183,7 @@ function CustomersList({
|
||||
|
||||
<DashboardPageContent>
|
||||
<CustomersTable
|
||||
loadong={tableLoading}
|
||||
loading={tableLoading}
|
||||
onDeleteCustomer={handleDeleteCustomer}
|
||||
onEditCustomer={handleEditCustomer}
|
||||
onfetchData={handleFetchData}
|
||||
|
||||
@@ -501,7 +501,6 @@ export default {
|
||||
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.',
|
||||
@@ -804,4 +803,6 @@ export default {
|
||||
' Changing full amount will change all credits and payment were applied, Is this okay?',
|
||||
address_line_1: 'Address line 1',
|
||||
address_line_2: 'Address line 2',
|
||||
website: 'Website',
|
||||
notes: 'Notes',
|
||||
};
|
||||
|
||||
@@ -47,7 +47,7 @@ export const fetchCustomers = ({ query }) => {
|
||||
new Promise((resolve, reject) => {
|
||||
const pageQuery = getState().items.tableQuery;
|
||||
dispatch({
|
||||
type: t.ITEMS_TABLE_LOADING,
|
||||
type: t.CUSTOMERS_TABLE_LOADING,
|
||||
payload: { loading: true },
|
||||
});
|
||||
ApiService.get(`customers`, { params: { ...pageQuery, ...query } })
|
||||
|
||||
@@ -13,37 +13,36 @@
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#{$self}__header{
|
||||
.bp3-form-group{
|
||||
max-width: 500px;
|
||||
|
||||
.bp3-form-group{
|
||||
max-width: 500px;
|
||||
&.bp3-inline{
|
||||
|
||||
.bp3-label{
|
||||
min-width: 150px;
|
||||
}
|
||||
|
||||
.bp3-form-content{
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
.bp3-form-content{
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.form-group--contact_name{
|
||||
max-width: 100%;
|
||||
.form-group--contact_name{
|
||||
max-width: 600px;
|
||||
|
||||
.bp3-control-group > *{
|
||||
flex-shrink: unset;
|
||||
.bp3-control-group > *{
|
||||
flex-shrink: unset;
|
||||
|
||||
&:not(:last-child) {
|
||||
padding-right: 10px;
|
||||
}
|
||||
&:not(:last-child) {
|
||||
padding-right: 10px;
|
||||
}
|
||||
|
||||
&.input-group--salutation-list{
|
||||
width: 25%;
|
||||
}
|
||||
&.input-group--first-name,
|
||||
&.input-group--last-name{
|
||||
width: 37%;
|
||||
}
|
||||
&.input-group--salutation-list{
|
||||
width: 25%;
|
||||
}
|
||||
&.input-group--first-name,
|
||||
&.input-group--last-name{
|
||||
width: 37%;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -76,29 +75,52 @@
|
||||
margin-top: 20px;
|
||||
max-width: 1000px;
|
||||
|
||||
.bp3-form-group{
|
||||
max-width: 440px;
|
||||
|
||||
.bp3-label{
|
||||
min-width: 145px;
|
||||
}
|
||||
.bp3-form-content{
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
textarea.bp3-input{
|
||||
max-width: 100%;
|
||||
width: 100%;
|
||||
min-height: 50px;
|
||||
}
|
||||
}
|
||||
|
||||
h4{
|
||||
font-weight: 500;
|
||||
color: #888;
|
||||
margin-bottom: 1.2rem;
|
||||
font-size: 14px;
|
||||
}
|
||||
// Tab panels.
|
||||
.tab-panel{
|
||||
|
||||
&--address{
|
||||
.bp3-form-group{
|
||||
max-width: 440px;
|
||||
|
||||
&.bp3-inline{
|
||||
.bp3-label{
|
||||
min-width: 145px;
|
||||
}
|
||||
}
|
||||
|
||||
.bp3-form-content{
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
textarea.bp3-input{
|
||||
max-width: 100%;
|
||||
width: 100%;
|
||||
min-height: 50px;
|
||||
}
|
||||
}
|
||||
}
|
||||
&--note{
|
||||
.form-group--note{
|
||||
.bp3-form-group{
|
||||
max-width: 600px;
|
||||
}
|
||||
textarea{
|
||||
width: 100%;
|
||||
min-height: 100px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dropzone-container{
|
||||
max-width: 600px;
|
||||
}
|
||||
}
|
||||
|
||||
.bp3-tabs{
|
||||
@@ -136,115 +158,24 @@
|
||||
}
|
||||
}
|
||||
|
||||
.dashboard__insider--customers-list{
|
||||
|
||||
.customer-form{
|
||||
.bigcapital-datatable{
|
||||
|
||||
.avatar.td{
|
||||
|
||||
&__primary-section{
|
||||
background-color: #fafafa;
|
||||
padding: 40px 22px 5px;
|
||||
margin: -20px -20px 26px;
|
||||
|
||||
&-content{
|
||||
width: 600px;
|
||||
.avatar{
|
||||
height: 30px;
|
||||
width: 30px;
|
||||
display: inline-block;
|
||||
background: #f3e2f6;
|
||||
border-radius: 50%;
|
||||
line-height: 30px;
|
||||
text-align: center;
|
||||
font-weight: 400;
|
||||
font-size: 14px;
|
||||
color: #93639a;
|
||||
}
|
||||
}
|
||||
}
|
||||
&__after-primary-section{
|
||||
&-content{
|
||||
width: 600px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
// .customer-form {
|
||||
// padding: 25px;
|
||||
// padding-bottom: 90px;
|
||||
// width: 100%;
|
||||
// margin-bottom: 30px;
|
||||
|
||||
|
||||
|
||||
// .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 {
|
||||
//
|
||||
// }
|
||||
|
||||
// &__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;
|
||||
// // }
|
||||
// }
|
||||
|
||||
@@ -17,17 +17,21 @@ export default class ContactsController extends BaseController {
|
||||
check('work_phone').optional().trim().escape(),
|
||||
check('personal_phone').optional().trim().escape(),
|
||||
|
||||
check('billing_address_1').optional().trim().escape(),
|
||||
check('billing_address_2').optional().trim().escape(),
|
||||
check('billing_address_city').optional().trim().escape(),
|
||||
check('billing_address_country').optional().trim().escape(),
|
||||
check('billing_address_email').optional().isEmail().trim().escape(),
|
||||
check('billing_address_zipcode').optional().trim().escape(),
|
||||
check('billing_address_postcode').optional().trim().escape(),
|
||||
check('billing_address_phone').optional().trim().escape(),
|
||||
check('billing_address_state').optional().trim().escape(),
|
||||
|
||||
check('shipping_address_1').optional().trim().escape(),
|
||||
check('shipping_address_2').optional().trim().escape(),
|
||||
check('shipping_address_city').optional().trim().escape(),
|
||||
check('shipping_address_country').optional().trim().escape(),
|
||||
check('shipping_address_email').optional().isEmail().trim().escape(),
|
||||
check('shipping_address_zip_code').optional().trim().escape(),
|
||||
check('shipping_address_postcode').optional().trim().escape(),
|
||||
check('shipping_address_phone').optional().trim().escape(),
|
||||
check('shipping_address_state').optional().trim().escape(),
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ export default class CustomersController extends ContactsController {
|
||||
...this.contactDTOSchema,
|
||||
...this.contactNewDTOSchema,
|
||||
...this.customerDTOSchema,
|
||||
...this.createCustomerDTOSchema,
|
||||
],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.newCustomer.bind(this)),
|
||||
@@ -77,10 +78,24 @@ export default class CustomersController extends ContactsController {
|
||||
get customerDTOSchema() {
|
||||
return [
|
||||
check('customer_type').exists().trim().escape(),
|
||||
check('opening_balance').optional().isNumeric().toInt(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Create customer DTO schema.
|
||||
*/
|
||||
get createCustomerDTOSchema() {
|
||||
return [
|
||||
check('opening_balance').optional().isNumeric().toInt(),
|
||||
check('opening_balance_at').optional().isISO8601(),
|
||||
|
||||
check('currency_code').optional().trim().escape(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* List param query schema.
|
||||
*/
|
||||
get validateListQuerySchema() {
|
||||
return [
|
||||
query('column_sort_by').optional().trim().escape(),
|
||||
|
||||
@@ -7,7 +7,10 @@ exports.up = function(knex) {
|
||||
table.string('contact_type');
|
||||
|
||||
table.decimal('balance', 13, 3).defaultTo(0);
|
||||
table.string('currency_code', 3);
|
||||
|
||||
table.decimal('opening_balance', 13, 3).defaultTo(0);
|
||||
table.date('opening_balance_at');
|
||||
|
||||
table.string('first_name').nullable();
|
||||
table.string('last_name').nullable();
|
||||
@@ -19,12 +22,12 @@ exports.up = function(knex) {
|
||||
table.string('work_phone').nullable();
|
||||
table.string('personal_phone').nullable();
|
||||
|
||||
table.string('billing_address_1').nullable();
|
||||
table.string('billing_address_2').nullable();
|
||||
table.string('billing_address1').nullable();
|
||||
table.string('billing_address2').nullable();
|
||||
table.string('billing_address_city').nullable();
|
||||
table.string('billing_address_country').nullable();
|
||||
table.string('billing_address_email').nullable();
|
||||
table.string('billing_address_zipcode').nullable();
|
||||
table.string('billing_address_postcode').nullable();
|
||||
table.string('billing_address_phone').nullable();
|
||||
table.string('billing_address_state').nullable(),
|
||||
|
||||
@@ -33,7 +36,7 @@ exports.up = function(knex) {
|
||||
table.string('shipping_address_city').nullable();
|
||||
table.string('shipping_address_country').nullable();
|
||||
table.string('shipping_address_email').nullable();
|
||||
table.string('shipping_address_zipcode').nullable();
|
||||
table.string('shipping_address_postcode').nullable();
|
||||
table.string('shipping_address_phone').nullable();
|
||||
table.string('shipping_address_state').nullable();
|
||||
|
||||
|
||||
@@ -47,7 +47,10 @@ export interface IContact extends IContactAddress{
|
||||
contactType: string,
|
||||
|
||||
balance: number,
|
||||
currencyCode: string,
|
||||
|
||||
openingBalance: number,
|
||||
openingBalanceAt: Date,
|
||||
|
||||
firstName: string,
|
||||
lastName: string,
|
||||
@@ -64,7 +67,10 @@ export interface IContact extends IContactAddress{
|
||||
export interface IContactNewDTO {
|
||||
contactType?: string,
|
||||
|
||||
currencyCode?: string,
|
||||
|
||||
openingBalance?: number,
|
||||
openingBalanceAt?: string,
|
||||
|
||||
firstName?: string,
|
||||
lastName?: string,
|
||||
@@ -81,8 +87,6 @@ export interface IContactNewDTO {
|
||||
export interface IContactEditDTO {
|
||||
contactType?: string,
|
||||
|
||||
openingBalance?: number,
|
||||
|
||||
firstName?: string,
|
||||
lastName?: string,
|
||||
companyName?: string,
|
||||
@@ -104,7 +108,10 @@ export interface ICustomer extends IContact {
|
||||
export interface ICustomerNewDTO extends IContactAddressDTO {
|
||||
customerType: string,
|
||||
|
||||
currencyCode: string,
|
||||
|
||||
openingBalance?: number,
|
||||
openingBalanceAt?: string,
|
||||
|
||||
firstName?: string,
|
||||
lastName?: string,
|
||||
@@ -121,8 +128,6 @@ export interface ICustomerNewDTO extends IContactAddressDTO {
|
||||
export interface ICustomerEditDTO extends IContactAddressDTO {
|
||||
customerType: string,
|
||||
|
||||
openingBalance?: number,
|
||||
|
||||
firstName?: string,
|
||||
lastName?: string,
|
||||
companyName?: string,
|
||||
@@ -142,7 +147,10 @@ export interface IVendor extends IContact {
|
||||
contactService: 'vendor',
|
||||
}
|
||||
export interface IVendorNewDTO extends IContactAddressDTO {
|
||||
currencyCode: string,
|
||||
|
||||
openingBalance?: number,
|
||||
openingBalanceAt?: string,
|
||||
|
||||
firstName?: string,
|
||||
lastName?: string,
|
||||
@@ -157,8 +165,6 @@ export interface IVendorNewDTO extends IContactAddressDTO {
|
||||
active?: boolean,
|
||||
};
|
||||
export interface IVendorEditDTO extends IContactAddressDTO {
|
||||
openingBalance?: number,
|
||||
|
||||
firstName?: string,
|
||||
lastName?: string,
|
||||
companyName?: string,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { difference, upperFirst } from 'lodash';
|
||||
import { difference, upperFirst, omit } from 'lodash';
|
||||
import { ServiceError } from "exceptions";
|
||||
import TenancyService from 'services/Tenancy/TenancyService';
|
||||
import {
|
||||
@@ -38,17 +38,39 @@ export default class ContactsService {
|
||||
return contact;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts contact DTO object to model object attributes to insert or update.
|
||||
* @param {IContactNewDTO | IContactEditDTO} contactDTO
|
||||
*/
|
||||
private transformContactObj(contactDTO: IContactNewDTO | IContactEditDTO) {
|
||||
return {
|
||||
...omit(contactDTO, [
|
||||
'billingAddress1', 'billingAddress2',
|
||||
'shippingAddress1', 'shippingAddress2',
|
||||
]),
|
||||
billing_address_1: contactDTO?.billingAddress1,
|
||||
billing_address_2: contactDTO?.billingAddress2,
|
||||
shipping_address_1: contactDTO?.shippingAddress1,
|
||||
shipping_address_2: contactDTO?.shippingAddress2,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new contact on the storage.
|
||||
* @param {number} tenantId
|
||||
* @param {TContactService} contactService
|
||||
* @param {IContactDTO} contactDTO
|
||||
*/
|
||||
async newContact(tenantId: number, contactDTO: IContactNewDTO, contactService: TContactService) {
|
||||
async newContact(
|
||||
tenantId: number,
|
||||
contactDTO: IContactNewDTO,
|
||||
contactService: TContactService,
|
||||
) {
|
||||
const { contactRepository } = this.tenancy.repositories(tenantId);
|
||||
const contactObj = this.transformContactObj(contactDTO);
|
||||
|
||||
this.logger.info('[contacts] trying to insert contact to the storage.', { tenantId, contactDTO });
|
||||
const contact = await contactRepository.insert({ contactService, ...contactDTO });
|
||||
const contact = await contactRepository.insert({ contactService, ...contactObj });
|
||||
|
||||
this.logger.info('[contacts] contact inserted successfully.', { tenantId, contact });
|
||||
return contact;
|
||||
@@ -63,10 +85,12 @@ export default class ContactsService {
|
||||
*/
|
||||
async editContact(tenantId: number, contactId: number, contactDTO: IContactEditDTO, contactService: TContactService) {
|
||||
const { Contact } = this.tenancy.models(tenantId);
|
||||
const contactObj = this.transformContactObj(contactDTO);
|
||||
|
||||
const contact = await this.getContactByIdOrThrowError(tenantId, contactId, contactService);
|
||||
|
||||
this.logger.info('[contacts] trying to edit the given contact details.', { tenantId, contactId, contactDTO });
|
||||
await Contact.query().findById(contactId).patch({ ...contactDTO })
|
||||
await Contact.query().findById(contactId).patch({ ...contactObj })
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -12,12 +12,15 @@ import {
|
||||
ICustomerEditDTO,
|
||||
ICustomer,
|
||||
IPaginationMeta,
|
||||
ICustomersFilter
|
||||
ICustomersFilter,
|
||||
IContactNewDTO,
|
||||
IContactEditDTO
|
||||
} from 'interfaces';
|
||||
import { ServiceError } from 'exceptions';
|
||||
import TenancyService from 'services/Tenancy/TenancyService';
|
||||
import DynamicListingService from 'services/DynamicListing/DynamicListService';
|
||||
import events from 'subscribers/events';
|
||||
import moment from 'moment';
|
||||
|
||||
@Service()
|
||||
export default class CustomersService {
|
||||
@@ -41,7 +44,7 @@ export default class CustomersService {
|
||||
* @param {ICustomerNewDTO|ICustomerEditDTO} customerDTO
|
||||
* @returns {IContactDTO}
|
||||
*/
|
||||
private customerToContactDTO(customerDTO: ICustomerNewDTO | ICustomerEditDTO) {
|
||||
private customerToContactDTO(customerDTO: ICustomerNewDTO|ICustomerEditDTO): IContactNewDTO|IContactEditDTO {
|
||||
return {
|
||||
...omit(customerDTO, ['customerType']),
|
||||
contactType: customerDTO.customerType,
|
||||
@@ -50,6 +53,18 @@ export default class CustomersService {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms new customer DTO to contact.
|
||||
* @param customerDTO
|
||||
*/
|
||||
private transformNewCustomerDTO(customerDTO: ICustomerNewDTO): IContactNewDTO {
|
||||
return {
|
||||
...this.customerToContactDTO(customerDTO),
|
||||
openingBalanceAt: customerDTO?.openingBalanceAt
|
||||
? moment(customerDTO.openingBalanceAt).toMySqlDateTime() : null,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new customer.
|
||||
* @param {number} tenantId
|
||||
@@ -62,8 +77,8 @@ export default class CustomersService {
|
||||
): Promise<ICustomer> {
|
||||
this.logger.info('[customer] trying to create a new customer.', { tenantId, customerDTO });
|
||||
|
||||
const contactDTO = this.customerToContactDTO(customerDTO)
|
||||
const customer = await this.contactService.newContact(tenantId, contactDTO, 'customer');
|
||||
const customerObj = this.transformNewCustomerDTO(customerDTO);
|
||||
const customer = await this.contactService.newContact(tenantId, customerObj, 'customer');
|
||||
|
||||
this.logger.info('[customer] created successfully.', { tenantId, customerDTO });
|
||||
await this.eventDispatcher.dispatch(events.customers.onCreated, {
|
||||
|
||||
Reference in New Issue
Block a user