feat: customer form styling.

feat: customers list.
This commit is contained in:
Ahmed Bouhuolia
2020-11-08 21:44:20 +02:00
parent 982420c8e5
commit 9241f8b8a5
17 changed files with 305 additions and 282 deletions

View File

@@ -12,8 +12,6 @@ import {
Checkbox, Checkbox,
Spinner, Spinner,
ContextMenu, ContextMenu,
Menu,
MenuItem,
} from '@blueprintjs/core'; } from '@blueprintjs/core';
import classnames from 'classnames'; import classnames from 'classnames';
import { FixedSizeList } from 'react-window'; import { FixedSizeList } from 'react-window';

View File

@@ -12,11 +12,7 @@ const CustomerBillingAddress = ({
getFieldProps, getFieldProps,
}) => { }) => {
return ( return (
<div <div className={'tab-panel--address'}>
className={
'customer-form__tabs-section customer-form__tabs-section--address'
}
>
<Row> <Row>
<Col xs={6}> <Col xs={6}>
<h4> <h4>

View File

@@ -28,7 +28,7 @@ export default function CustomerFinancialPanel({
); );
return ( return (
<div className={'customer-form__tabs-section customer-form__tabs-section--financial'}> <div className={'tab-panel--financial'}>
<Row> <Row>
<Col xs={6}> <Col xs={6}>
<FormGroup <FormGroup

View File

@@ -143,10 +143,46 @@ function CustomerForm({
useEffect(() => { useEffect(() => {
customer && customer.id customer && customer.id
? changePageTitle(formatMessage({ id: 'edit_customer_details' })) ? changePageTitle(formatMessage({ id: 'edit_customer' }))
: changePageTitle(formatMessage({ id: 'new_customer' })); : changePageTitle(formatMessage({ id: 'new_customer' }));
}, [changePageTitle, customer, formatMessage]); }, [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 { const {
setFieldValue, setFieldValue,
getFieldProps, getFieldProps,
@@ -160,42 +196,7 @@ function CustomerForm({
initialValues: { initialValues: {
...initialValues, ...initialValues,
}, },
onSubmit: (values, { setSubmitting, resetForm, setErrors }) => { onSubmit: handleFormSubmit,
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);
});
}
},
}); });

View File

@@ -32,13 +32,14 @@ export default function CustomerFormPrimarySection({
); );
// Handle salutation field select. // Handle salutation field select.
const handleSalutationSelect = (salutation) => { const handleSalutationSelect = useCallback((salutation) => {
setFieldValue('salutation', salutation.label); setFieldValue('salutation', salutation.label);
}; }, [setFieldValue]);
// Handle display name field select. // Handle display name field select.
const handleDisplayNameSelect = (displayName) => { const handleDisplayNameSelect = useCallback((displayName) => {
setFieldValue('display_name', displayName.label); setFieldValue('display_name', displayName.label);
}; }, [setFieldValue]);
return ( return (
<div className={'customer-form__primary-section-content'}> <div className={'customer-form__primary-section-content'}>
@@ -116,6 +117,7 @@ export default function CustomerFormPrimarySection({
firstName={values.first_name} firstName={values.first_name}
lastName={values.last_name} lastName={values.last_name}
company={values.company_name} company={values.company_name}
salutation={values.salutation}
onItemSelect={handleDisplayNameSelect} onItemSelect={handleDisplayNameSelect}
popoverProps={{ minimal: true }} popoverProps={{ minimal: true }}
/> />

View File

@@ -7,28 +7,21 @@ import ErrorMessage from 'components/ErrorMessage';
export default function CustomerNotePanel({ errors, touched, getFieldProps }) { export default function CustomerNotePanel({ errors, touched, getFieldProps }) {
return ( return (
<div <div className={'tab-panel--note'}>
className={ <FormGroup
'customer-form__tabs-section customer-form__tabs-section--note' label={<T id={'note'} />}
} className={classNames('form-group--note', Classes.FILL)}
> intent={errors.note && touched.note && Intent.DANGER}
<Row> helperText={
<Col xs={6}> <ErrorMessage name="payment_date" {...{ errors, touched }} />
<FormGroup }
label={<T id={'note'} />} >
className={classNames('form-group--select-list', Classes.FILL)} <TextArea
intent={errors.note && touched.note && Intent.DANGER} intent={errors.note && touched.note && Intent.DANGER}
helperText={ {...getFieldProps('note')}
<ErrorMessage name="payment_date" {...{ errors, touched }} /> />
} </FormGroup>
>
<TextArea
intent={errors.note && touched.note && Intent.DANGER}
{...getFieldProps('note')}
/>
</FormGroup>
</Col>
</Row>
</div> </div>
); );
} }

View File

@@ -1,4 +1,4 @@
import React, { useState, useEffect, useCallback, useMemo } from 'react'; import React, { useState, useCallback, useMemo } from 'react';
import { import {
Button, Button,
Popover, Popover,
@@ -6,17 +6,23 @@ import {
MenuItem, MenuItem,
MenuDivider, MenuDivider,
Position, Position,
Intent,
} from '@blueprintjs/core'; } from '@blueprintjs/core';
import { FormattedMessage as T, useIntl } from 'react-intl'; import { FormattedMessage as T, useIntl } from 'react-intl';
import DataTable from 'components/DataTable'; import DataTable from 'components/DataTable';
import Icon from 'components/Icon'; import Icon from 'components/Icon';
import { Money } from 'components';
import { useUpdateEffect } from 'hooks'; import { useUpdateEffect } from 'hooks';
import LoadingIndicator from 'components/LoadingIndicator'; import LoadingIndicator from 'components/LoadingIndicator';
import withCustomers from './withCustomers'; 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 = ({ const CustomerTable = ({
loading, loading,
@@ -32,7 +38,6 @@ const CustomerTable = ({
onSelectedRowsChange, onSelectedRowsChange,
}) => { }) => {
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();
const [initialMount, setInitialMount] = useState(false); const [initialMount, setInitialMount] = useState(false);
useUpdateEffect(() => { useUpdateEffect(() => {
@@ -41,36 +46,64 @@ const CustomerTable = ({
} }
}, [customersLoading, setInitialMount]); }, [customersLoading, setInitialMount]);
const handleEditCustomer = useCallback( // Customers actions list.
(customer) => () => { const renderContextMenu = useMemo(
onEditCustomer && onEditCustomer(customer); () => ({ 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( // Renders actions table cell.
(customer) => () => { const renderActionsCell = useMemo(() => ({ cell }) => (
onDeleteCustomer && onDeleteCustomer(customer); <Popover
}, content={renderContextMenu({
[onDeleteCustomer], customer: cell.row.original,
); onEditCustomer,
const actionMenuList = useCallback((customer) => ( onDeleteCustomer,
<Menu> })}
<MenuItem text={<T id={'view_details'} />} /> position={Position.RIGHT_BOTTOM}
<MenuDivider /> >
<MenuItem <Button icon={<Icon icon="more-h-16" iconSize={16} />} />
text={<T id={'edit_customer'} />} </Popover>
onClick={handleEditCustomer(customer)} ), [onDeleteCustomer, onEditCustomer, renderContextMenu]);
/>
<MenuItem
text={<T id={'delete_customer'} />}
onClick={handleDeleteCustomer(customer)}
/>
</Menu>
));
const columns = useMemo( const columns = useMemo(
() => [ () => [
{
id: 'avatar',
Header: '',
accessor: AvatarCell,
className: 'avatar',
width: 50,
disableResizing: true,
disableSortBy: true,
},
{ {
id: 'display_name', id: 'display_name',
Header: formatMessage({ id: 'display_name' }), Header: formatMessage({ id: 'display_name' }),
@@ -95,26 +128,20 @@ const CustomerTable = ({
{ {
id: 'receivable_balance', id: 'receivable_balance',
Header: formatMessage({ id: 'receivable_balance' }), Header: formatMessage({ id: 'receivable_balance' }),
// accessor: '', accessor: (r) => <Money amount={r.closing_balance} currency={'USD'} />,
className: 'receivable_balance', className: 'receivable_balance',
width: 100, width: 100,
}, },
{ {
id: 'actions', id: 'actions',
Cell: ({ cell }) => ( Cell: renderActionsCell,
<Popover
content={actionMenuList(cell.row.original)}
position={Position.RIGHT_BOTTOM}
>
<Button icon={<Icon icon="more-h-16" iconSize={16} />} />
</Popover>
),
className: 'actions', className: 'actions',
width: 50, width: 70,
disableResizing: true,
disableSortBy: true,
}, },
], ],
[actionMenuList, formatMessage], [formatMessage, renderActionsCell],
); );
const selectionColumn = useMemo( const selectionColumn = useMemo(
@@ -138,6 +165,13 @@ const CustomerTable = ({
[onSelectedRowsChange], [onSelectedRowsChange],
); );
const rowContextMenu = (cell) =>
renderContextMenu({
customer: cell.row.original,
onEditCustomer,
onDeleteCustomer,
});
return ( return (
<LoadingIndicator loading={loading} mount={false}> <LoadingIndicator loading={loading} mount={false}>
<DataTable <DataTable
@@ -146,11 +180,12 @@ const CustomerTable = ({
data={customers} data={customers}
selectionColumn={selectionColumn} selectionColumn={selectionColumn}
onFetchData={handleFetchDate} onFetchData={handleFetchDate}
expandable={true} expandable={false}
treeGraph={true} treeGraph={false}
onSelectedRowsChange={handleSelectedRowsChange} onSelectedRowsChange={handleSelectedRowsChange}
loading={customersLoading && !initialMount} loading={customersLoading && !initialMount}
spinnerProps={{ size: 30 }} spinnerProps={{ size: 30 }}
rowContextMenu={rowContextMenu}
/> />
</LoadingIndicator> </LoadingIndicator>
); );

View File

@@ -135,11 +135,10 @@ function CustomersList({
filter_roles: filterConditions || '', filter_roles: filterConditions || '',
}); });
}, },
[fetchCustomers], [addCustomersTableQueries],
); );
// Handle Customers bulk delete button click., // Handle Customers bulk delete button click.,
const handleBulkDelete = useCallback( const handleBulkDelete = useCallback(
(customersIds) => { (customersIds) => {
setBulkDelete(customersIds); setBulkDelete(customersIds);
@@ -184,7 +183,7 @@ function CustomersList({
<DashboardPageContent> <DashboardPageContent>
<CustomersTable <CustomersTable
loadong={tableLoading} loading={tableLoading}
onDeleteCustomer={handleDeleteCustomer} onDeleteCustomer={handleDeleteCustomer}
onEditCustomer={handleEditCustomer} onEditCustomer={handleEditCustomer}
onfetchData={handleFetchData} onfetchData={handleFetchData}

View File

@@ -501,7 +501,6 @@ export default {
billing_address: 'Billing Address', billing_address: 'Billing Address',
shipping_address: 'Shipping Address', shipping_address: 'Shipping Address',
customers_list: 'Customers List', customers_list: 'Customers List',
edit_customer_details: 'Edit Customer Details',
receivable_balance: 'Receivable balance', receivable_balance: 'Receivable balance',
the_customer_has_been_successfully_created: the_customer_has_been_successfully_created:
'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?', ' Changing full amount will change all credits and payment were applied, Is this okay?',
address_line_1: 'Address line 1', address_line_1: 'Address line 1',
address_line_2: 'Address line 2', address_line_2: 'Address line 2',
website: 'Website',
notes: 'Notes',
}; };

View File

@@ -47,7 +47,7 @@ export const fetchCustomers = ({ query }) => {
new Promise((resolve, reject) => { new Promise((resolve, reject) => {
const pageQuery = getState().items.tableQuery; const pageQuery = getState().items.tableQuery;
dispatch({ dispatch({
type: t.ITEMS_TABLE_LOADING, type: t.CUSTOMERS_TABLE_LOADING,
payload: { loading: true }, payload: { loading: true },
}); });
ApiService.get(`customers`, { params: { ...pageQuery, ...query } }) ApiService.get(`customers`, { params: { ...pageQuery, ...query } })

View File

@@ -13,37 +13,36 @@
overflow: hidden; overflow: hidden;
} }
#{$self}__header{ .bp3-form-group{
max-width: 500px;
.bp3-form-group{ &.bp3-inline{
max-width: 500px;
.bp3-label{ .bp3-label{
min-width: 150px; min-width: 150px;
} }
.bp3-form-content{
width: 100%;
}
} }
.bp3-form-content{
width: 100%;
}
}
.form-group--contact_name{ .form-group--contact_name{
max-width: 100%; max-width: 600px;
.bp3-control-group > *{ .bp3-control-group > *{
flex-shrink: unset; flex-shrink: unset;
&:not(:last-child) { &:not(:last-child) {
padding-right: 10px; padding-right: 10px;
} }
&.input-group--salutation-list{ &.input-group--salutation-list{
width: 25%; width: 25%;
} }
&.input-group--first-name, &.input-group--first-name,
&.input-group--last-name{ &.input-group--last-name{
width: 37%; width: 37%;
}
} }
} }
} }
@@ -76,29 +75,52 @@
margin-top: 20px; margin-top: 20px;
max-width: 1000px; 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{ h4{
font-weight: 500; font-weight: 500;
color: #888; color: #888;
margin-bottom: 1.2rem; margin-bottom: 1.2rem;
font-size: 14px; 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{ .bp3-tabs{
@@ -136,115 +158,24 @@
} }
} }
.dashboard__insider--customers-list{
.customer-form{ .bigcapital-datatable{
.avatar.td{
&__primary-section{ .avatar{
background-color: #fafafa; height: 30px;
padding: 40px 22px 5px; width: 30px;
margin: -20px -20px 26px; display: inline-block;
background: #f3e2f6;
&-content{ border-radius: 50%;
width: 600px; 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;
// // }
// }

View File

@@ -17,17 +17,21 @@ export default class ContactsController extends BaseController {
check('work_phone').optional().trim().escape(), check('work_phone').optional().trim().escape(),
check('personal_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_city').optional().trim().escape(),
check('billing_address_country').optional().trim().escape(), check('billing_address_country').optional().trim().escape(),
check('billing_address_email').optional().isEmail().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_phone').optional().trim().escape(),
check('billing_address_state').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_city').optional().trim().escape(),
check('shipping_address_country').optional().trim().escape(), check('shipping_address_country').optional().trim().escape(),
check('shipping_address_email').optional().isEmail().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_phone').optional().trim().escape(),
check('shipping_address_state').optional().trim().escape(), check('shipping_address_state').optional().trim().escape(),

View File

@@ -26,6 +26,7 @@ export default class CustomersController extends ContactsController {
...this.contactDTOSchema, ...this.contactDTOSchema,
...this.contactNewDTOSchema, ...this.contactNewDTOSchema,
...this.customerDTOSchema, ...this.customerDTOSchema,
...this.createCustomerDTOSchema,
], ],
this.validationResult, this.validationResult,
asyncMiddleware(this.newCustomer.bind(this)), asyncMiddleware(this.newCustomer.bind(this)),
@@ -77,10 +78,24 @@ export default class CustomersController extends ContactsController {
get customerDTOSchema() { get customerDTOSchema() {
return [ return [
check('customer_type').exists().trim().escape(), 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() { get validateListQuerySchema() {
return [ return [
query('column_sort_by').optional().trim().escape(), query('column_sort_by').optional().trim().escape(),

View File

@@ -7,7 +7,10 @@ exports.up = function(knex) {
table.string('contact_type'); table.string('contact_type');
table.decimal('balance', 13, 3).defaultTo(0); table.decimal('balance', 13, 3).defaultTo(0);
table.string('currency_code', 3);
table.decimal('opening_balance', 13, 3).defaultTo(0); table.decimal('opening_balance', 13, 3).defaultTo(0);
table.date('opening_balance_at');
table.string('first_name').nullable(); table.string('first_name').nullable();
table.string('last_name').nullable(); table.string('last_name').nullable();
@@ -19,12 +22,12 @@ exports.up = function(knex) {
table.string('work_phone').nullable(); table.string('work_phone').nullable();
table.string('personal_phone').nullable(); table.string('personal_phone').nullable();
table.string('billing_address_1').nullable(); table.string('billing_address1').nullable();
table.string('billing_address_2').nullable(); table.string('billing_address2').nullable();
table.string('billing_address_city').nullable(); table.string('billing_address_city').nullable();
table.string('billing_address_country').nullable(); table.string('billing_address_country').nullable();
table.string('billing_address_email').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_phone').nullable();
table.string('billing_address_state').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_city').nullable();
table.string('shipping_address_country').nullable(); table.string('shipping_address_country').nullable();
table.string('shipping_address_email').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_phone').nullable();
table.string('shipping_address_state').nullable(); table.string('shipping_address_state').nullable();

View File

@@ -47,7 +47,10 @@ export interface IContact extends IContactAddress{
contactType: string, contactType: string,
balance: number, balance: number,
currencyCode: string,
openingBalance: number, openingBalance: number,
openingBalanceAt: Date,
firstName: string, firstName: string,
lastName: string, lastName: string,
@@ -64,7 +67,10 @@ export interface IContact extends IContactAddress{
export interface IContactNewDTO { export interface IContactNewDTO {
contactType?: string, contactType?: string,
currencyCode?: string,
openingBalance?: number, openingBalance?: number,
openingBalanceAt?: string,
firstName?: string, firstName?: string,
lastName?: string, lastName?: string,
@@ -81,8 +87,6 @@ export interface IContactNewDTO {
export interface IContactEditDTO { export interface IContactEditDTO {
contactType?: string, contactType?: string,
openingBalance?: number,
firstName?: string, firstName?: string,
lastName?: string, lastName?: string,
companyName?: string, companyName?: string,
@@ -104,7 +108,10 @@ export interface ICustomer extends IContact {
export interface ICustomerNewDTO extends IContactAddressDTO { export interface ICustomerNewDTO extends IContactAddressDTO {
customerType: string, customerType: string,
currencyCode: string,
openingBalance?: number, openingBalance?: number,
openingBalanceAt?: string,
firstName?: string, firstName?: string,
lastName?: string, lastName?: string,
@@ -121,8 +128,6 @@ export interface ICustomerNewDTO extends IContactAddressDTO {
export interface ICustomerEditDTO extends IContactAddressDTO { export interface ICustomerEditDTO extends IContactAddressDTO {
customerType: string, customerType: string,
openingBalance?: number,
firstName?: string, firstName?: string,
lastName?: string, lastName?: string,
companyName?: string, companyName?: string,
@@ -142,7 +147,10 @@ export interface IVendor extends IContact {
contactService: 'vendor', contactService: 'vendor',
} }
export interface IVendorNewDTO extends IContactAddressDTO { export interface IVendorNewDTO extends IContactAddressDTO {
currencyCode: string,
openingBalance?: number, openingBalance?: number,
openingBalanceAt?: string,
firstName?: string, firstName?: string,
lastName?: string, lastName?: string,
@@ -157,8 +165,6 @@ export interface IVendorNewDTO extends IContactAddressDTO {
active?: boolean, active?: boolean,
}; };
export interface IVendorEditDTO extends IContactAddressDTO { export interface IVendorEditDTO extends IContactAddressDTO {
openingBalance?: number,
firstName?: string, firstName?: string,
lastName?: string, lastName?: string,
companyName?: string, companyName?: string,

View File

@@ -1,5 +1,5 @@
import { Inject, Service } from 'typedi'; import { Inject, Service } from 'typedi';
import { difference, upperFirst } from 'lodash'; import { difference, upperFirst, omit } from 'lodash';
import { ServiceError } from "exceptions"; import { ServiceError } from "exceptions";
import TenancyService from 'services/Tenancy/TenancyService'; import TenancyService from 'services/Tenancy/TenancyService';
import { import {
@@ -38,17 +38,39 @@ export default class ContactsService {
return contact; 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. * Creates a new contact on the storage.
* @param {number} tenantId * @param {number} tenantId
* @param {TContactService} contactService * @param {TContactService} contactService
* @param {IContactDTO} contactDTO * @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 { contactRepository } = this.tenancy.repositories(tenantId);
const contactObj = this.transformContactObj(contactDTO);
this.logger.info('[contacts] trying to insert contact to the storage.', { tenantId, 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 }); this.logger.info('[contacts] contact inserted successfully.', { tenantId, contact });
return contact; return contact;
@@ -63,10 +85,12 @@ export default class ContactsService {
*/ */
async editContact(tenantId: number, contactId: number, contactDTO: IContactEditDTO, contactService: TContactService) { async editContact(tenantId: number, contactId: number, contactDTO: IContactEditDTO, contactService: TContactService) {
const { Contact } = this.tenancy.models(tenantId); const { Contact } = this.tenancy.models(tenantId);
const contactObj = this.transformContactObj(contactDTO);
const contact = await this.getContactByIdOrThrowError(tenantId, contactId, contactService); const contact = await this.getContactByIdOrThrowError(tenantId, contactId, contactService);
this.logger.info('[contacts] trying to edit the given contact details.', { tenantId, contactId, contactDTO }); 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 })
} }
/** /**

View File

@@ -12,12 +12,15 @@ import {
ICustomerEditDTO, ICustomerEditDTO,
ICustomer, ICustomer,
IPaginationMeta, IPaginationMeta,
ICustomersFilter ICustomersFilter,
IContactNewDTO,
IContactEditDTO
} from 'interfaces'; } from 'interfaces';
import { ServiceError } from 'exceptions'; import { ServiceError } from 'exceptions';
import TenancyService from 'services/Tenancy/TenancyService'; import TenancyService from 'services/Tenancy/TenancyService';
import DynamicListingService from 'services/DynamicListing/DynamicListService'; import DynamicListingService from 'services/DynamicListing/DynamicListService';
import events from 'subscribers/events'; import events from 'subscribers/events';
import moment from 'moment';
@Service() @Service()
export default class CustomersService { export default class CustomersService {
@@ -41,7 +44,7 @@ export default class CustomersService {
* @param {ICustomerNewDTO|ICustomerEditDTO} customerDTO * @param {ICustomerNewDTO|ICustomerEditDTO} customerDTO
* @returns {IContactDTO} * @returns {IContactDTO}
*/ */
private customerToContactDTO(customerDTO: ICustomerNewDTO | ICustomerEditDTO) { private customerToContactDTO(customerDTO: ICustomerNewDTO|ICustomerEditDTO): IContactNewDTO|IContactEditDTO {
return { return {
...omit(customerDTO, ['customerType']), ...omit(customerDTO, ['customerType']),
contactType: 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. * Creates a new customer.
* @param {number} tenantId * @param {number} tenantId
@@ -62,8 +77,8 @@ export default class CustomersService {
): Promise<ICustomer> { ): Promise<ICustomer> {
this.logger.info('[customer] trying to create a new customer.', { tenantId, customerDTO }); this.logger.info('[customer] trying to create a new customer.', { tenantId, customerDTO });
const contactDTO = this.customerToContactDTO(customerDTO) const customerObj = this.transformNewCustomerDTO(customerDTO);
const customer = await this.contactService.newContact(tenantId, contactDTO, 'customer'); const customer = await this.contactService.newContact(tenantId, customerObj, 'customer');
this.logger.info('[customer] created successfully.', { tenantId, customerDTO }); this.logger.info('[customer] created successfully.', { tenantId, customerDTO });
await this.eventDispatcher.dispatch(events.customers.onCreated, { await this.eventDispatcher.dispatch(events.customers.onCreated, {