Merge remote-tracking branch 'origin/customers'

This commit is contained in:
Ahmed Bouhuolia
2020-06-14 14:34:35 +02:00
17 changed files with 538 additions and 45 deletions

View File

@@ -1,5 +1,5 @@
import React from 'react'
import { FormattedMessage as T} from 'react-intl';
import React from 'react';
import { FormattedMessage as T } from 'react-intl';
export default [
{
@@ -8,9 +8,9 @@ export default [
{
icon: 'homepage',
iconSize: 20,
text: <T id={'homepage'}/>,
text: <T id={'homepage'} />,
disabled: false,
href: '/homepage',
href: '/homepage',
},
{
divider: true,
@@ -18,18 +18,18 @@ export default [
{
icon: 'homepage',
iconSize: 20,
text: <T id={'items'}/>,
text: <T id={'items'} />,
children: [
{
text: <T id={'items_list'}/>,
text: <T id={'items_list'} />,
href: '/items',
},
{
text: <T id={'new_item'}/>,
text: <T id={'new_item'} />,
href: '/items/new',
},
{
text: <T id={'category_list'}/>,
text: <T id={'category_list'} />,
href: '/items/categories',
},
],
@@ -40,22 +40,22 @@ export default [
{
icon: 'balance-scale',
iconSize: 20,
text: <T id={'financial'}/>,
text: <T id={'financial'} />,
children: [
{
text: <T id={'accounts_chart'}/>,
text: <T id={'accounts_chart'} />,
href: '/accounts',
},
{
text: <T id={'manual_journal'}/>,
text: <T id={'manual_journal'} />,
href: '/manual-journals',
},
{
text: <T id={'make_journal'}/>,
text: <T id={'make_journal'} />,
href: '/make-journal-entry',
},
{
text: <T id={'exchange_rate'}/>,
text: <T id={'exchange_rate'} />,
href: '/ExchangeRates',
},
],
@@ -63,19 +63,19 @@ export default [
{
icon: 'university',
iconSize: 20,
text: <T id={'banking'}/>,
text: <T id={'banking'} />,
children: [],
},
{
icon: 'shopping-cart',
iconSize: 20,
text: <T id={'sales'}/>,
text: <T id={'sales'} />,
children: [],
},
{
icon: 'balance-scale',
iconSize: 20,
text: <T id={'purchases'}/>,
text: <T id={'purchases'} />,
children: [
{
icon: 'cut',
@@ -88,26 +88,26 @@ export default [
{
icon: 'analytics',
iconSize: 18,
text: <T id={'financial_reports'}/>,
text: <T id={'financial_reports'} />,
children: [
{
text: <T id={'balance_sheet'}/>,
text: <T id={'balance_sheet'} />,
href: '/balance-sheet',
},
{
text: <T id={'trial_balance_sheet'}/>,
text: <T id={'trial_balance_sheet'} />,
href: '/trial-balance-sheet',
},
{
text: <T id={'journal'}/>,
text: <T id={'journal'} />,
href: '/journal-sheet',
},
{
text: <T id={'general_ledger'}/>,
text: <T id={'general_ledger'} />,
href: '/general-ledger',
},
{
text: <T id={'profit_loss_sheet'}/>,
text: <T id={'profit_loss_sheet'} />,
href: '/profit-loss-sheet',
},
{
@@ -121,7 +121,7 @@ export default [
],
},
{
text: <T id={'expenses'}/>,
text: <T id={'expenses'} />,
icon: 'receipt',
iconSize: 18,
children: [
@@ -135,15 +135,30 @@ export default [
},
],
},
{
text: <T id={'customers'} />,
icon: 'receipt',
iconSize: 18,
children: [
{
text: <T id={'customers'} />,
// href: '/',
},
{
text: <T id={'new_customers'} />,
href: '/customers/new',
},
],
},
{
divider: true,
},
{
text: <T id={'preferences'}/>,
text: <T id={'preferences'} />,
href: '/preferences',
},
{
text: <T id={'auditing_system'}/>,
text: <T id={'auditing_system'} />,
href: '/auditing/list',
},
];

View File

@@ -0,0 +1,20 @@
import React, { useCallback } from 'react';
import { useParams, useHistory } from 'react-router-dom';
import { useQuery } from 'react-query';
import DashboardInsider from 'components/Dashboard/DashboardInsider';
import CustomerForm from 'containers/Customers/CustomerForm';
import withDashboardActions from 'containers/Dashboard/withDashboardActions';
import { compose } from 'utils';
function Customer({}) {
return (
<DashboardInsider name={'customer-form'}>
<CustomerForm />
</DashboardInsider>
);
}
export default Customer;

View File

@@ -0,0 +1,175 @@
import React, { useState, useMemo, useCallback, useEffect } from 'react';
import * as Yup from 'yup';
import { useFormik } from 'formik';
import {
FormGroup,
MenuItem,
Intent,
InputGroup,
Button,
Classes,
Checkbox,
RadioGroup,
Radio,
} from '@blueprintjs/core';
import { Row, Col } from 'react-grid-system';
import { FormattedMessage as T, useIntl } from 'react-intl';
import { queryCache, useQuery } from 'react-query';
import { useHistory } from 'react-router-dom';
import { pick } from 'lodash';
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 withDashboardActions from 'containers/Dashboard/withDashboardActions';
import withCustomerDetail from './withCustomerDetail';
import withCustomersActions from './withCustomersActions';
import RadioCustomer from './RadioCustomer';
import { compose, handleStringChange } from 'utils';
import withCustomers from './withCustomers';
function CustomerForm({
// #withDashboardActions
changePageTitle,
customers,
//#withCustomersActions
requestSubmitCustomer,
requestFetchCustomers,
}) {
const { formatMessage } = useIntl();
const validationSchema = Yup.object().shape({
customer_type: Yup.string()
.required()
.trim()
.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(),
});
useEffect(() => {
changePageTitle(formatMessage({ id: 'new_customer' }));
}, [changePageTitle, formatMessage]);
//business
const initialValues = useMemo(
() => ({
customer_type: 'business',
first_name: '',
last_name: '',
company_name: '',
display_name: '',
// email: '',
work_phone: '',
}),
[],
);
const {
getFieldProps,
setFieldValue,
values,
touched,
errors,
handleSubmit,
isSubmitting,
} = useFormik({
enableReinitialize: true,
validationSchema: validationSchema,
initialValues: {
...initialValues,
},
onSubmit: (values, { setSubmitting, resetForm, setErrors }) => {
requestSubmitCustomer(values)
.then((response) => {
AppToaster.show({
message: formatMessage({
id: 'the_customer_has_been_successfully_created',
}),
intent: Intent.SUCCESS,
});
})
.catch((errors) => {
setSubmitting(false);
});
},
});
const requiredSpan = useMemo(() => <span class="required">*</span>, []);
const handleCustomerTypeCahange = useCallback(
(value) => {
setFieldValue('customer_type', value);
},
[setFieldValue],
);
console.log(customers, 'ER');
const fetch = useQuery('customers-table', (key) => requestFetchCustomers());
return (
<div className={'customer-form'}>
<form onSubmit={handleSubmit}>
<div className={'customer-form__primary-section'}>
<RadioCustomer
selectedValue={values.customer_type}
onChange={handleCustomerTypeCahange}
/>
<FormGroup
label={<T id={'display_name'} />}
className={'form-group--name'}
intent={
errors.display_name && touched.display_name && Intent.DANGER
}
inline={true}
helperText={
<ErrorMessage {...{ errors, touched }} name={'display_name'} />
}
>
<InputGroup
intent={
errors.display_name && touched.display_name && Intent.DANGER
}
{...getFieldProps('display_name')}
/>
</FormGroup>
</div>
<div class="form__floating-footer">
<Button intent={Intent.PRIMARY} disabled={isSubmitting} type="submit">
<T id={'save'} />
</Button>
<Button className={'ml1'} disabled={isSubmitting}>
<T id={'save_as_draft'} />
</Button>
<Button className={'ml1'}>
<T id={'close'} />
</Button>
</div>
</form>
</div>
);
}
export default compose(
withCustomerDetail,
withCustomers(({ customers }) => ({
customers,
})),
withDashboardActions,
withCustomersActions,
)(CustomerForm);

View File

@@ -0,0 +1,22 @@
import React from 'react';
import { handleStringChange } from 'utils';
import { useIntl } from 'react-intl';
import { RadioGroup, Radio } from '@blueprintjs/core';
export default function RadioCustomer(props) {
const { onChange, ...rest } = props;
const { formatMessage } = useIntl();
return (
<RadioGroup
inline={true}
label={formatMessage({ id: 'customer_type' })}
onChange={handleStringChange((value) => {
onChange && onChange(value);
})}
{...rest}
>
<Radio label={formatMessage({ id: 'business' })} value="business" />
<Radio label={formatMessage({ id: 'individual' })} value="individual" />
</RadioGroup>
);
}

View File

@@ -0,0 +1,8 @@
import { connect } from 'react-redux';
import { getCustomerById } from 'store/customers/customers.reducer';
const mapStateToProps = (state, props) => ({
customerDetail: getCustomerById(state, props.customerId),
});
export default connect(mapStateToProps);

View File

@@ -0,0 +1,18 @@
import { connect } from 'react-redux';
import { getCustomersItems } from 'store/customers/customers.selectors';
import { getResourceViews } from 'store/customViews/customViews.selectors';
export default (mapState) => {
const mapStateToProps = (state, props) => {
const mapped = {
customersViews: getResourceViews(state, 'customers'),
customers: getCustomersItems(state, state.customers.currentViewId),
customersLoading: state.customers.loading,
customerErrors: state.customers.errors,
};
return mapState ? mapState(mapped, state, props) : mapped;
};
return connect(mapStateToProps);
};

View File

@@ -0,0 +1,23 @@
import { connect } from 'react-redux';
import {
fetchCustomers,
submitCustomer,
editCustomer,
} from 'store/customers/customers.actions';
import t from 'store/types';
export const mapDispatchToProps = (dispatch) => ({
requestFetchCustomers: (query) => dispatch(fetchCustomers({ query })),
// requestDeleteCustomer: (id) => dispatch(deleteCustomer({ id })),
// requestDeleteBulkCustomers:(ids)=>dispatch(deleteBulkCustomers({ids})),
requestSubmitCustomer: (form) => dispatch(submitCustomer({ form })),
// requestEditCustomer: (id, form) => dispatch(editCustomer({ id, form })),
addCustomersTableQueries: (queries) =>
dispatch({
type: t.CUSTOMERS_TABLE_QUERIES_ADD,
queries,
}),
});
export default connect(null, mapDispatchToProps);

View File

@@ -282,7 +282,6 @@ export default {
'The exchange rate has been successfully edited',
the_exchange_rate_has_been_successfully_created:
'The exchange rate has been successfully created',
the_user_details_has_been_updated: 'The user details has been updated',
the_category_has_been_successfully_created:
'The category has been successfully created.',
@@ -356,9 +355,7 @@ export default {
'There is exchange rate in this date with the same currency.',
the_exchange_rates_has_been_successfully_deleted:
'The exchange rates has been successfully deleted',
once_delete_this_expense_you_will_able_to_restore_it: `Once you delete this expense, you won\'t be able to restore it later. Are you sure you want to delete this expense?`,
january: 'January',
february: 'February',
march: 'March',
@@ -450,5 +447,16 @@ export default {
aging_before_days: 'Aging before days',
aging_periods: 'Aging periods',
as: 'As',
receivable_aging_summary: 'Receivable Aging Summary'
receivable_aging_summary: 'Receivable Aging Summary',
customers: 'Customers',
new_customers:'New Customers',
customer_type_: 'Customer type',
display_name_: 'Display name',
new_customer: 'New Customer',
customer_type: 'Customer Type',
business: 'business',
individual: 'Individual',
display_name:'Display Name',
the_customer_has_been_successfully_created:
'The customer has been successfully created.',
};

View File

@@ -151,7 +151,6 @@ export default [
}),
breadcrumb: 'Exchange Rates',
},
// Expenses
{
path: `/expenses/new`, // expenses/
@@ -175,4 +174,27 @@ export default [
}),
breadcrumb: 'Expenses List',
},
{
path: `/customers/:id/edit`,
component: LazyLoader({
// loader: () => import(),
}),
breadcrumb: 'Edit Customer',
},
{
path: `/customers/new`,
component: LazyLoader({
loader: () => import('containers/Customers/Customer'),
}),
breadcrumb: 'New Customer',
},
// Customers
{
path: `/customers`,
component: LazyLoader({
// loader: () => import(''),
}),
breadcrumb: 'Customers',
},
];

View File

@@ -0,0 +1,93 @@
import ApiService from 'services/ApiService';
import t from 'store/types';
export const submitCustomer = ({ form }) => {
return (dispatch) =>
new Promise((resolve, reject) => {
dispatch({
type: t.SET_DASHBOARD_REQUEST_LOADING,
});
ApiService.post('customers', form)
.then((response) => {
dispatch({
type: t.SET_DASHBOARD_REQUEST_COMPLETED,
});
resolve(response);
})
.catch((error) => {
const { response } = error;
const { data } = response;
dispatch({
type: t.SET_DASHBOARD_REQUEST_COMPLETED,
});
reject(data?.errors);
});
});
};
export const editCustomer = ({ form, id }) => {
return (dispatch) =>
new Promise((resolve, reject) => {
dispatch({
type: t.SET_DASHBOARD_REQUEST_LOADING,
});
ApiService.post(`customers/${id}`, form)
.then((response) => {
dispatch({
type: t.SET_DASHBOARD_REQUEST_COMPLETED,
});
resolve(response);
})
.catch((error) => {
const { response } = error;
const { data } = response;
dispatch({
type: t.SET_DASHBOARD_REQUEST_COMPLETED,
});
reject(data?.errors);
});
});
};
export const fetchCustomers = ({ query }) => {
return (dispatch, getState) =>
new Promise((resolve, reject) => {
const pageQuery = getState().items.tableQuery;
dispatch({
type: t.ITEMS_TABLE_LOADING,
payload: { loading: true },
});
dispatch({
type: t.SET_DASHBOARD_REQUEST_LOADING,
});
ApiService.get(`customers`, { params: { ...pageQuery, ...query } })
.then((response) => {
dispatch({
type: t.CUSTOMER_SET,
customers: response.data.customers.results,
});
dispatch({
type: t.CUSTOMERS_PAGE_SET,
customers: response.data.customers.results,
customViewId: response.data.customers.customViewId,
paginationMeta: response.data.customers.pagination,
});
dispatch({
type: t.CUSTOMERS_TABLE_LOADING,
payload: { loading: false },
});
dispatch({
type: t.SET_DASHBOARD_REQUEST_COMPLETED,
});
resolve(response);
})
.catch((error) => {
dispatch({
type: t.SET_DASHBOARD_REQUEST_COMPLETED,
});
reject(error);
});
});
};

View File

@@ -0,0 +1,45 @@
import t from 'store/types';
import { createReducer } from '@reduxjs/toolkit';
import { createTableQueryReducers } from 'store/queryReducers';
const initialState = {
items: {},
views: {},
loading: false,
currentViewId: -1,
errors: [],
};
const customersReducer = createReducer(initialState, {
[t.CUSTOMER_SET]: (state, action) => {
const _customers = {};
action.customers.forEach((customer) => {
_customers[customer.id] = customer;
});
state.items = {
...state.items,
..._customers,
};
},
[t.CUSTOMERS_PAGE_SET]: (state, action) => {
const viewId = action.customViewId || -1;
const view = state.views[viewId] || {};
state.views[viewId] = {
...view,
ids: action.customers.map((i) => i.id),
};
},
[t.CUSTOMER_DELETE]: (state, action) => {
if (typeof state.items[action.id] !== 'undefined') {
delete state.items[action.id];
}
},
});
export default createTableQueryReducers('customers', customersReducer);
export const getCustomerById = (state, id) => {
return state.customers[id];
};

View File

@@ -0,0 +1,10 @@
import { pickItemsFromIds } from 'store/selectors';
export const getCustomersItems = (state, viewId) => {
const customersView = state.customers.views[viewId || -1];
const customersItems = state.customers.items;
return typeof customersView === 'object'
? pickItemsFromIds(customersItems, customersView.ids) || []
: [];
};

View File

@@ -0,0 +1,8 @@
export default {
CUSTOMERS_ITEMS_SET: 'CUSTOMERS_ITEMS_SET',
CUSTOMER_SET: 'CUSTOMER_SET',
CUSTOMERS_PAGE_SET: 'CUSTOMERS_PAGE_SET',
CUSTOMERS_TABLE_LOADING: 'CUSTOMERS_TABLE_LOADING',
CUSTOMERS_TABLE_QUERIES_ADD: 'CUSTOMERS_TABLE_QUERIES_ADD',
CUSTOMER_DELETE:'CUSTOMER_DELETE'
};

View File

@@ -15,9 +15,9 @@ import itemCategories from './itemCategories/itemsCategory.reducer';
import settings from './settings/settings.reducer';
import manualJournals from './manualJournals/manualJournals.reducers';
import globalSearch from './search/search.reducer';
import exchangeRates from './ExchangeRate/exchange.reducer'
import exchangeRates from './ExchangeRate/exchange.reducer';
import globalErrors from './globalErrors/globalErrors.reducer';
import customers from './customers/customers.reducer';
export default combineReducers({
authentication,
@@ -37,4 +37,5 @@ export default combineReducers({
globalSearch,
exchangeRates,
globalErrors,
customers,
});

View File

@@ -16,6 +16,7 @@ import settings from './settings/settings.type';
import search from './search/search.type';
import register from './registers/register.type';
import exchangeRate from './ExchangeRate/exchange.type';
import customer from './customers/customers.type';
export default {
...authentication,
@@ -36,4 +37,5 @@ export default {
...search,
...register,
...exchangeRate,
...customer,
};

View File

@@ -12,8 +12,8 @@ $menu-item-color-active: $light-gray3;
$breadcrumbs-collapsed-icon: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='#6B8193' enable-background='new 0 0 16 16' xml:space='preserve'><g><circle cx='2' cy='8.03' r='2'/><circle cx='14' cy='8.03' r='2'/><circle cx='8' cy='8.03' r='2'/></g></svg>");
$sidebar-zindex: 15;
$pt-font-family: Noto Sans, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, Ubuntu, Cantarell, Open Sans, Helvetica Neue, Icons16, sans-serif;
$pt-font-family: Noto Sans, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto,
Oxygen, Ubuntu, Cantarell, Open Sans, Helvetica Neue, Icons16, sans-serif;
// Blueprint framework.
@import '@blueprintjs/core/src/blueprint.scss';
@@ -48,16 +48,16 @@ $pt-font-family: Noto Sans, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto,
@import 'pages/items';
@import 'pages/items-categories';
@import 'pages/invite-form.scss';
@import "pages/currency";
@import "pages/invite-user.scss";
@import 'pages/currency';
@import 'pages/invite-user.scss';
@import 'pages/exchange-rate.scss';
@import 'pages/customer.scss';
// Views
@import 'views/filter-dropdown';
@import 'views/sidebar';
.App{
.App {
min-width: 960px;
}
@@ -75,12 +75,11 @@ $pt-font-family: Noto Sans, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto,
}
}
.bigcapital--alt{
svg{
.bigcapital--alt {
svg {
path,
.path-13,
.path-1{
.path-1 {
fill: #fff;
}
}
@@ -90,7 +89,7 @@ body.authentication {
background-color: #fcfdff;
}
.bp3-toast{
.bp3-toast {
box-shadow: none;
}
@@ -118,4 +117,4 @@ body.authentication {
.bp3-datepicker-caption .bp3-html-select::after{
margin-right: 6px;
}
}

View File

@@ -0,0 +1,24 @@
.customer-form {
padding: 22px;
width: 100%;
// padding-bottom: 90px;
.bp3-form-group {
// margin: 25px 20px 20px;
.bp3-label {
min-width: 100px;
}
.bp3-form-content {
width: 45%;
}
}
&__primary-section {
background-color: #fafafa;
padding: 40px 22px 22px;
margin: -22px -22px 22px;
background-color: #fafafa;
}
}