@@ -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 }}
/>
diff --git a/client/src/containers/Customers/CustomerNotePanel.js b/client/src/containers/Customers/CustomerNotePanel.js
index f3806bb17..32cd1f8e7 100644
--- a/client/src/containers/Customers/CustomerNotePanel.js
+++ b/client/src/containers/Customers/CustomerNotePanel.js
@@ -7,28 +7,21 @@ import ErrorMessage from 'components/ErrorMessage';
export default function CustomerNotePanel({ errors, touched, getFieldProps }) {
return (
-
-
-
- }
- className={classNames('form-group--select-list', Classes.FILL)}
- intent={errors.note && touched.note && Intent.DANGER}
- helperText={
-
- }
- >
-
-
-
-
+
+ }
+ className={classNames('form-group--note', Classes.FILL)}
+ intent={errors.note && touched.note && Intent.DANGER}
+ helperText={
+
+ }
+ >
+
+
);
}
+
\ No newline at end of file
diff --git a/client/src/containers/Customers/CustomerTable.js b/client/src/containers/Customers/CustomerTable.js
index 16a885895..308f77840 100644
--- a/client/src/containers/Customers/CustomerTable.js
+++ b/client/src/containers/Customers/CustomerTable.js
@@ -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
{firstLettersArgs(row.display_name)};
+};
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 (
+
+ );
},
- [onEditCustomer],
+ [formatMessage],
);
- const handleDeleteCustomer = useCallback(
- (customer) => () => {
- onDeleteCustomer && onDeleteCustomer(customer);
- },
- [onDeleteCustomer],
- );
- const actionMenuList = useCallback((customer) => (
-
- ));
+ // Renders actions table cell.
+ const renderActionsCell = useMemo(() => ({ cell }) => (
+
+ } />
+
+ ), [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) =>
,
className: 'receivable_balance',
width: 100,
},
-
{
id: 'actions',
- Cell: ({ cell }) => (
-
- } />
-
- ),
+ 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 (
);
diff --git a/client/src/containers/Customers/CustomersList.js b/client/src/containers/Customers/CustomersList.js
index a239963d7..215ed6769 100644
--- a/client/src/containers/Customers/CustomersList.js
+++ b/client/src/containers/Customers/CustomersList.js
@@ -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({
{
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 } })
diff --git a/client/src/style/pages/customer.scss b/client/src/style/pages/customer.scss
index 0d3f5364d..4adc67368 100644
--- a/client/src/style/pages/customer.scss
+++ b/client/src/style/pages/customer.scss
@@ -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;
-// // }
-// }
diff --git a/server/src/api/controllers/Contacts/Contacts.ts b/server/src/api/controllers/Contacts/Contacts.ts
index 8ba52e7b7..d9be41efe 100644
--- a/server/src/api/controllers/Contacts/Contacts.ts
+++ b/server/src/api/controllers/Contacts/Contacts.ts
@@ -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(),
diff --git a/server/src/api/controllers/Contacts/Customers.ts b/server/src/api/controllers/Contacts/Customers.ts
index 4e25c6871..e2acdd6f3 100644
--- a/server/src/api/controllers/Contacts/Customers.ts
+++ b/server/src/api/controllers/Contacts/Customers.ts
@@ -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(),
diff --git a/server/src/database/migrations/20200104232644_create_contacts_table.js b/server/src/database/migrations/20200104232644_create_contacts_table.js
index ccc7e13f0..9583f46db 100644
--- a/server/src/database/migrations/20200104232644_create_contacts_table.js
+++ b/server/src/database/migrations/20200104232644_create_contacts_table.js
@@ -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();
diff --git a/server/src/interfaces/Contact.ts b/server/src/interfaces/Contact.ts
index c2be84a73..502523bbe 100644
--- a/server/src/interfaces/Contact.ts
+++ b/server/src/interfaces/Contact.ts
@@ -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,
diff --git a/server/src/services/Contacts/ContactsService.ts b/server/src/services/Contacts/ContactsService.ts
index 20cd25895..428afe164 100644
--- a/server/src/services/Contacts/ContactsService.ts
+++ b/server/src/services/Contacts/ContactsService.ts
@@ -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 })
}
/**
diff --git a/server/src/services/Contacts/CustomersService.ts b/server/src/services/Contacts/CustomersService.ts
index bae4df453..3649b810a 100644
--- a/server/src/services/Contacts/CustomersService.ts
+++ b/server/src/services/Contacts/CustomersService.ts
@@ -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 {
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, {