diff --git a/app/models/customer.rb b/app/models/customer.rb index 0b52773..58ffa33 100644 --- a/app/models/customer.rb +++ b/app/models/customer.rb @@ -1,2 +1,3 @@ class Customer < ApplicationRecord + scope :company_name_contains, -> (value) { where('company_name ILIKE ?', "%#{value.join}%") } end diff --git a/app/resources/customer_resource.rb b/app/resources/customer_resource.rb index 4c5077b..568651a 100644 --- a/app/resources/customer_resource.rb +++ b/app/resources/customer_resource.rb @@ -1,12 +1,17 @@ class CustomerResource < JSONAPI::Resource + extend ModelFilter attributes :company_name, - :contact_name - :contact_title - :address - :city - :region - :postal_code - :country - :phone - :fax + :contact_name, + :contact_title, + :address, + :city, + :region, + :postal_code, + :country, + :phone, + :fax, + :created_at + + paginator :paged + model_filters :company_name_contains end diff --git a/client/src/api/normalize.js b/client/src/api/normalize.js index 648c398..3d8e152 100644 --- a/client/src/api/normalize.js +++ b/client/src/api/normalize.js @@ -58,6 +58,18 @@ const serializers = { }), }, + customers: { + serializer: new Serializer('customers', { + keyForAttribute: 'camelCase', + attributes: [ + 'companyName' + ], + }), + deserializer: new Deserializer({ + keyForAttribute: 'camelCase' + }), + }, + roles: { serializer: new Serializer('roles', { keyForAttribute: 'camelCase', diff --git a/client/src/components/App.js b/client/src/components/App.js index 87a753f..d2097a1 100644 --- a/client/src/components/App.js +++ b/client/src/components/App.js @@ -38,6 +38,9 @@ export class App extends Component { <NavItem> <NavLink href="/#/categories">Categories</NavLink> </NavItem> + <NavItem> + <NavLink href="/#/customers">Customers</NavLink> + </NavItem> <NavItem> { userIsAdmin && <NavLink href="/#/users">Users</NavLink> diff --git a/client/src/components/Customers/CustomerEdit.js b/client/src/components/Customers/CustomerEdit.js new file mode 100644 index 0000000..3930973 --- /dev/null +++ b/client/src/components/Customers/CustomerEdit.js @@ -0,0 +1,48 @@ +import React, { Component, PropTypes } from 'react'; +import { push } from 'react-router-redux'; +import { connect } from 'react-redux'; + +import { ErrorAlert, Loading, EditHeader } from '../UI'; +import { withResource } from '../../hocs'; +import CustomerForm from './CustomerForm'; +import { getMany, fetchList } from '../../store/api'; + +export class CustomerEdit extends Component { + componentWillMount() { + const { params, fetchResource } = this.props; + if (params.id) { + fetchResource({ id: params.id }); + } + } + + render() { + const { isNew, error, loading, resource, onSubmit } = this.props; + + if (error) { + return (<ErrorAlert {...error} />); + } + + if (loading) { + return (<Loading />); + } + + return ( + <div> + <EditHeader {...this.props}>{ isNew ? 'New Customer' : resource.company_name }</EditHeader> + <CustomerForm initialValues={resource} onSubmit={onSubmit}></CustomerForm> + </div> + ); + } +} + +export const mapStateToProps = (state, props) => ({ + roles: getMany(state) +}); + +export const mapDispatchToProps = dispatch => ({ + redirectToIndex: () => dispatch(push('/customers')) +}); + +export default connect(mapStateToProps, mapDispatchToProps)( + withResource('customers')(CustomerEdit), +); diff --git a/client/src/components/Customers/CustomerForm.js b/client/src/components/Customers/CustomerForm.js new file mode 100644 index 0000000..6570bb3 --- /dev/null +++ b/client/src/components/Customers/CustomerForm.js @@ -0,0 +1,88 @@ +import React, { Component, PropTypes } from 'react'; +import { isEmpty } from 'lodash'; +import { Field, reduxForm } from 'redux-form'; +import { Button, Form } from 'reactstrap'; + +import { InputField, MultiselectField, required } from '../../forms'; + +class CustomerForm extends Component { + render() { + const { handleSubmit, pristine, reset, submitting } = this.props; + + return ( + <Form onSubmit={handleSubmit}> + <div> + <Field + name="companyName" + label="Company name" + component={InputField} + /> + <Field + name="contactName" + label="Contact name" + component={InputField} + /> + <Field + name="contactTitle" + label="Contact title" + component={InputField} + /> + <Field + name="address" + label="Address" + component={InputField} + /> + <Field + name="city" + label="City" + component={InputField} + /> + <Field + name="Region" + label="Region" + component={InputField} + /> + + <Field + name="postalCode" + label="Postal code" + component={InputField} + /> + + <Field + name="country" + label="Country" + component={InputField} + /> + + <Field + name="phone" + label="Phone" + component={InputField} + /> + + <Field + name="fax" + label="Fax" + component={InputField} + /> + </div> + <div> + <Button disabled={pristine || submitting} color="primary">Submit</Button> + <Button disabled={pristine || submitting} onClick={reset}>Undo Changes</Button> + </div> + </Form> + ); + } +} + +const validate = (values) => { + const errors = required(values, 'email'); + return errors; +}; + +export default reduxForm({ + enableReinitialize: true, + form: 'customer', + validate, +})(CustomerForm); diff --git a/client/src/components/Customers/CustomerList.js b/client/src/components/Customers/CustomerList.js new file mode 100644 index 0000000..d820fa7 --- /dev/null +++ b/client/src/components/Customers/CustomerList.js @@ -0,0 +1,59 @@ +import React, { Component, PropTypes } from 'react'; +import { Link } from 'react-router'; +import { find, keyBy } from 'lodash'; +import { Button } from 'reactstrap'; + +import { ListTable } from '../UI'; +import { withResourceList } from '../../hocs'; +import CustomerListFilter from './CustomerListFilter'; + +const formatDate = date => (new Date(date)).toLocaleString(); + +export class CustomerList extends Component { + componentWillMount() { + const { resourceList } = this.props; + this.props.fetchResourceList({ sort: '-companyName', ...resourceList.params }); + } + + render() { + const { onFilter } = this.props; + const columns = [ + { + attribute: 'companyName', + header: 'Company Name', + rowRender: customer => <Link to={`/customers/${customer.id}`}>{customer.companyName}</Link>, + sortable: true, + }, + { + attribute: 'contactName', + header: 'Contact Name', + rowRender: customer => <Link to={`/customers/${customer.id}`}>{customer.contactName}</Link>, + sortable: true, + }, + { + attribute: 'createdAt', + header: 'Created At', + rowRender: customer => formatDate(customer.confirmedAt), + sortable: true, + } + ]; + + return ( + <div> + <Button tag={Link} to={'/customers/new'}>New Customer</Button> + + <CustomerListFilter + onSubmit={onFilter}> + </CustomerListFilter> + + <ListTable {...this.props} columns={columns} /> + </div> + ); + } +} + +export const mapStateToProps = state => ({ + filter: get(state, 'form.customerListFilter.values') || {} +}); + +export default withResourceList('customers')(CustomerList); diff --git a/client/src/components/Customers/CustomerListFilter.js b/client/src/components/Customers/CustomerListFilter.js new file mode 100644 index 0000000..d21ce42 --- /dev/null +++ b/client/src/components/Customers/CustomerListFilter.js @@ -0,0 +1,35 @@ +import React, { Component, PropTypes } from 'react'; +import { isEmpty } from 'lodash'; +import { Field, reduxForm } from 'redux-form'; +import { Form, Row, Col } from 'reactstrap'; + +import { InputField, SelectField } from '../../forms'; + +class CustomerListFilter extends Component { + render() { + const { handleSubmit, onSubmit } = this.props; + + const submitOnChange = () => setTimeout(() => handleSubmit(onSubmit)(), 0); + + + return ( + <Form onSubmit={handleSubmit}> + <Row> + <Col md={8}> + <Field + name="company_name_contains" + label="Company Name Contains" + component={InputField} + onChange={submitOnChange} + /> + </Col> + </Row> + </Form> + ); + } +} + +export default reduxForm({ + form: 'customerListFilter', + destroyOnUnmount: false, +})(CustomerListFilter); diff --git a/client/src/components/Customers/index.js b/client/src/components/Customers/index.js new file mode 100644 index 0000000..ad0a602 --- /dev/null +++ b/client/src/components/Customers/index.js @@ -0,0 +1,2 @@ +export CustomerList from './CustomerList'; +export CustomerEdit from './CustomerEdit'; diff --git a/client/src/components/Routes.js b/client/src/components/Routes.js index 9e8cfbe..4114d43 100644 --- a/client/src/components/Routes.js +++ b/client/src/components/Routes.js @@ -8,6 +8,7 @@ import Dashboard from './Dashboard'; import { PostList, PostEdit } from './Posts'; import { CategoryList, CategoryEdit } from './Categories'; import { UserList, UserEdit } from './Users'; +import { CustomerList, CustomerEdit } from './Customers'; import { Login } from './Auth'; const UserIsAuthenticated = UserAuthWrapper({ authSelector: getUser }); @@ -34,6 +35,9 @@ export class Routes extends PureComponent { <Route path="/categories" component={CategoryList}/> <Route path="/users" component={UserIsAdmin(UserList)}/> <Route path="/users/:id" component={UserIsAdmin(UserEdit)}/> + <Route path="/customers" component={UserIsAdmin(CustomerList)}/> + <Route path="/customers/new" component={UserIsAdmin(CustomerEdit)}/> + <Route path="/customers/:id" component={UserIsAdmin(CustomerEdit)}/> </Route> <Route path="/login" component={Login}/> </Router>