├── .idea ├── .name ├── watcherTasks.xml ├── encodings.xml ├── vcs.xml ├── modules.xml ├── jsLibraryMappings.xml ├── dictionaries │ └── Tom.xml ├── simpleCrm12.iml └── misc.xml ├── .meteor ├── .gitignore ├── release ├── platforms ├── .id ├── .finished-upgraders ├── packages └── versions ├── imports ├── ui │ ├── app │ │ ├── notification.html │ │ ├── Index.html │ │ ├── test2.jsx │ │ ├── test1.jsx │ │ ├── app-not-found.jsx │ │ └── Layout.jsx │ ├── stylesheets │ │ └── sbAdmin2 │ │ │ ├── mixins.less │ │ │ ├── variables.less │ │ │ ├── sb-admin-2.js │ │ │ └── sb-admin-2.less │ ├── redux │ │ ├── order-list-actions.jsx │ │ ├── customer-list-actions.jsx │ │ ├── middlewares.jsx │ │ ├── store.jsx │ │ ├── DevTools.jsx │ │ ├── customer_actions.jsx │ │ ├── reducers.jsx │ │ └── order-actions.jsx │ ├── components │ │ ├── grid │ │ │ ├── GridRow.jsx │ │ │ ├── GridHeaderRow.jsx │ │ │ ├── GridColumn.jsx │ │ │ └── GridHeaderColumn.jsx │ │ ├── empty.jsx │ │ ├── PaginatedPanel.jsx │ │ ├── SelectInput.jsx │ │ ├── DateInput.jsx │ │ ├── ModalMessageBox.jsx │ │ ├── CollapsiblePanel.jsx │ │ ├── TextInput.jsx │ │ ├── NumberInput.jsx │ │ ├── loading-crossfade-component.jsx │ │ └── AsyncSelectInput.jsx │ ├── security │ │ ├── accounts-ui-wrapper.jsx │ │ ├── accounts-button.jsx │ │ ├── login.jsx │ │ └── register.jsx │ ├── products │ │ ├── products-list-wrapper.jsx │ │ └── products-list.jsx │ ├── dashboard │ │ └── Dashboard.jsx │ ├── sales │ │ ├── OrdersList.jsx │ │ ├── AllOrdersContainer.jsx │ │ ├── OrdersListItem.jsx │ │ ├── OrderLinesList.jsx │ │ ├── TopOrdersContainer.jsx │ │ ├── OrderContainer.jsx │ │ ├── OrderLineEdit.jsx │ │ └── OrderHeaderEdit.jsx │ ├── customers │ │ ├── CustomersList.jsx │ │ ├── AllCustomersContainer.jsx │ │ ├── TopCustomersContainer.jsx │ │ ├── CustomerContainer.jsx │ │ └── CustomerEditForm.jsx │ └── search │ │ └── GlobalSearch.jsx ├── startup │ ├── client │ │ ├── accounts.js │ │ └── routes.jsx │ └── server │ │ ├── register-api.js │ │ └── fixtures.js └── api │ ├── products │ ├── products.js │ ├── methods.js │ └── server │ │ └── publications.js │ ├── sales-regions │ ├── sales-region.js │ └── server │ │ └── publications.js │ ├── customers │ ├── customer-company.js │ ├── methods.js │ └── server │ │ └── publications.js │ ├── lib │ └── collection-helpers.js │ └── orders │ ├── methods.js │ ├── server │ └── publications.js │ └── order.js ├── server └── main.js ├── client ├── main.js ├── main.less └── css │ ├── page-transitions.css │ ├── react-autocomplete.css │ ├── order-lines-table.css │ ├── flipCOPY.css │ ├── s-alert-defaultCOPY.css │ └── react-selectCOPY.min.css ├── lib ├── order-logic.js ├── validation-helpers.js ├── order-logic.tests.js └── collection-schema.js ├── package.json ├── .eslintrc └── README.md /.idea/.name: -------------------------------------------------------------------------------- 1 | simpleCrm12 -------------------------------------------------------------------------------- /.meteor/.gitignore: -------------------------------------------------------------------------------- 1 | local 2 | -------------------------------------------------------------------------------- /imports/ui/app/notification.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.meteor/release: -------------------------------------------------------------------------------- 1 | METEOR@1.3-beta.12 2 | -------------------------------------------------------------------------------- /.meteor/platforms: -------------------------------------------------------------------------------- 1 | server 2 | browser 3 | -------------------------------------------------------------------------------- /imports/ui/stylesheets/sbAdmin2/mixins.less: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /imports/startup/client/accounts.js: -------------------------------------------------------------------------------- 1 | Accounts.ui.config({ 2 | passwordSignupFields: "USERNAME_ONLY" 3 | }); 4 | 5 | -------------------------------------------------------------------------------- /server/main.js: -------------------------------------------------------------------------------- 1 | import '../imports/startup/server/fixtures.js'; 2 | import '../imports/startup/server/register-api.js'; -------------------------------------------------------------------------------- /client/main.js: -------------------------------------------------------------------------------- 1 | import '../imports/startup/client/routes.jsx' 2 | import '../imports/startup/client/accounts' 3 | import '../imports/ui/stylesheets/sbAdmin2/sb-admin-2' -------------------------------------------------------------------------------- /.idea/watcherTasks.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.idea/encodings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /client/main.less: -------------------------------------------------------------------------------- 1 | @import "{}/imports/ui/stylesheets/sbAdmin2/mixins.less"; 2 | @import "{}/imports/ui/stylesheets/sbAdmin2/sb-admin-2.less"; 3 | @import "{}/imports/ui/stylesheets/sbAdmin2/variables.less"; -------------------------------------------------------------------------------- /imports/ui/redux/order-list-actions.jsx: -------------------------------------------------------------------------------- 1 | 2 | 3 | export function toggleExpanded() { 4 | return (dispatch, getState) => { 5 | dispatch ({ 6 | type: 'TOGGLE_ORDER_LIST_EXPANDED' 7 | }); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /imports/ui/redux/customer-list-actions.jsx: -------------------------------------------------------------------------------- 1 | 2 | 3 | export function toggleExpanded() { 4 | return (dispatch, getState) => { 5 | dispatch ({ 6 | type: 'TOGGLE_CUSTOMER_LIST_EXPANDED' 7 | }); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /imports/api/products/products.js: -------------------------------------------------------------------------------- 1 | import { createCollection } from '../lib/collection-helpers.js'; 2 | 3 | // Make it available to the rest of the app 4 | const Products = new createCollection("Products", Schemas.ProductSchema); 5 | 6 | export default Products; -------------------------------------------------------------------------------- /imports/api/sales-regions/sales-region.js: -------------------------------------------------------------------------------- 1 | import { createCollection } from '../lib/collection-helpers.js'; 2 | 3 | // Make it available to the rest of the app 4 | const SalesRegions = new createCollection("SalesRegions", Schemas.SalesRegionSchema); 5 | 6 | export default SalesRegions; -------------------------------------------------------------------------------- /imports/ui/components/grid/GridRow.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const GridRow = React.createClass({ 4 | render() { 5 | return
6 | { this.props.children } 7 |
; 8 | } 9 | }); 10 | 11 | export default GridRow -------------------------------------------------------------------------------- /imports/api/customers/customer-company.js: -------------------------------------------------------------------------------- 1 | 2 | import { createCollection } from '../lib/collection-helpers.js'; 3 | 4 | // Make it available to the rest of the app 5 | const CustomerCompanies = createCollection("Companies", Schemas.CustomerCompaniesSchema); 6 | 7 | export default CustomerCompanies; 8 | -------------------------------------------------------------------------------- /imports/ui/components/grid/GridHeaderRow.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const GridHeaderRow = React.createClass({ 4 | render() { 5 | return
6 | { this.props.children } 7 |
; 8 | } 9 | }); 10 | 11 | export default GridHeaderRow -------------------------------------------------------------------------------- /imports/ui/components/grid/GridColumn.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const GridColumn = React.createClass({ 4 | render() { 5 | return
6 | { this.props.children } 7 |
; 8 | } 9 | }); 10 | 11 | export default GridColumn -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /imports/ui/components/grid/GridHeaderColumn.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const GridHeaderColumn = React.createClass({ 4 | render() { 5 | return
6 | { this.props.children } 7 |
; 8 | } 9 | }); 10 | 11 | export default GridHeaderColumn -------------------------------------------------------------------------------- /.meteor/.id: -------------------------------------------------------------------------------- 1 | # This file contains a token that is unique to your project. 2 | # Check it into your repository along with the rest of this directory. 3 | # It can be used for purposes such as: 4 | # - ensuring you don't accidentally deploy one app on top of another 5 | # - providing package authors with aggregated statistics 6 | 7 | r24bcm15dfjw2a9a4y 8 | -------------------------------------------------------------------------------- /imports/ui/app/Index.html: -------------------------------------------------------------------------------- 1 | 2 | SimpleCRM 3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | -------------------------------------------------------------------------------- /.idea/jsLibraryMappings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /imports/startup/server/register-api.js: -------------------------------------------------------------------------------- 1 | import '../../api/customers/methods.js'; 2 | import '../../api/customers/server/publications.js'; 3 | import '../../api/orders/methods.js'; 4 | import '../../api/orders/server/publications.js'; 5 | import '../../api/products/methods.js'; 6 | import '../../api/products/server/publications.js'; 7 | import '../../api/sales-regions/server/publications.js'; -------------------------------------------------------------------------------- /imports/api/sales-regions/server/publications.js: -------------------------------------------------------------------------------- 1 | import SalesRegions from '../sales-region'; 2 | 3 | const SalesRegionListFields = { 4 | name: 1, 5 | createdAt: 1 6 | }; 7 | 8 | Meteor.publish('SalesRegions.All', function () { 9 | return SalesRegions.find( 10 | {}, 11 | { 12 | fields: SalesRegionListFields 13 | } 14 | ); 15 | }); 16 | 17 | -------------------------------------------------------------------------------- /imports/ui/stylesheets/sbAdmin2/variables.less: -------------------------------------------------------------------------------- 1 | // Variables 2 | 3 | @gray-darker: lighten(#000, 13.5%); 4 | @gray-dark: lighten(#000, 20%); 5 | @gray: lighten(#000, 33.5%); 6 | @gray-light: lighten(#000, 60%); 7 | @gray-lighter: lighten(#000, 93.5%); 8 | @gray-lightest: lighten(#000, 97.25%); 9 | @brand-primary: #428bca; 10 | @brand-success: #5cb85c; 11 | @brand-info: #5bc0de; 12 | @brand-warning: #f0ad4e; 13 | @brand-danger: #d9534f; -------------------------------------------------------------------------------- /.idea/dictionaries/Tom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | autoform 5 | autosuggest 6 | debounce 7 | dropdown 8 | glyphicon 9 | minimongo 10 | mixins 11 | mongo 12 | navbar 13 | redux 14 | upsert 15 | upserting 16 | 17 | 18 | -------------------------------------------------------------------------------- /.meteor/.finished-upgraders: -------------------------------------------------------------------------------- 1 | # This file contains information which helps Meteor properly upgrade your 2 | # app when you run 'meteor update'. You should check it into version control 3 | # with your project. 4 | 5 | notices-for-0.9.0 6 | notices-for-0.9.1 7 | 0.9.4-platform-file 8 | notices-for-facebook-graph-api-2 9 | 1.2.0-standard-minifiers-package 10 | 1.2.0-meteor-platform-split 11 | 1.2.0-cordova-changes 12 | 1.2.0-breaking-changes 13 | 1.3.0-split-minifiers-package 14 | -------------------------------------------------------------------------------- /imports/ui/redux/middlewares.jsx: -------------------------------------------------------------------------------- 1 | // middleware allows you to do something in between the dispatch 2 | // and handing it off to the reducer 3 | 4 | // console.log our state changes 5 | logger = store => next => action => { 6 | log('[Dispatching]', action); 7 | // essentially call 'dispatch' 8 | let result = next(action); 9 | log('[Store]', store.getState()); 10 | return result; 11 | }; 12 | 13 | function log() { 14 | if (__debug_redux) { 15 | console.log.apply(console, arguments); 16 | } 17 | } 18 | __debug_redux = true; 19 | -------------------------------------------------------------------------------- /imports/ui/redux/store.jsx: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware, compose } from 'redux'; 2 | import thunk from 'redux-thunk'; 3 | 4 | import rootReducer from './reducers.jsx'; 5 | import DevTools from './DevTools.jsx'; 6 | 7 | const enhancer = compose( 8 | // Middleware you want to use in development: 9 | applyMiddleware(thunk), 10 | // Required! Enable Redux DevTools with the monitors you chose 11 | DevTools.instrument() 12 | ); 13 | 14 | const store = createStore( rootReducer, {} , enhancer ); 15 | 16 | export default store; -------------------------------------------------------------------------------- /client/css/page-transitions.css: -------------------------------------------------------------------------------- 1 | /* 2 | * React Page Transitions 3 | */ 4 | 5 | .example-enter { 6 | -webkit-transition: opacity 0.3s ease-in-out; 7 | transition: opacity 0.3s ease-in-out; 8 | opacity: 0; 9 | /*position: absolute;*/ 10 | } 11 | 12 | .example-enter.example-enter-active { 13 | opacity: 1; 14 | } 15 | 16 | .example-leave { 17 | -webkit-transition: opacity 0.3s ease-in-out; 18 | transition: opacity 0.3s ease-in-out; 19 | opacity: 1; 20 | /*position: absolute;*/ 21 | } 22 | 23 | .example-leave.example-leave-active { 24 | opacity: 0; 25 | } -------------------------------------------------------------------------------- /.idea/simpleCrm12.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /lib/order-logic.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | export function lineValue(orderLine) { 4 | const val = +(orderLine.quantity * orderLine.unitPrice).toFixed(2); 5 | //console.log("function lineValue", val); 6 | return val; 7 | } 8 | 9 | export function recalculateOrderTotals(order) { 10 | //console.log("function recalculateOrderTotals", order); 11 | 12 | let total = 0; 13 | let arrayLength = order.orderLines.length; 14 | for (var i = 0; i < arrayLength; i++) { 15 | order.orderLines[i].lineValue = lineValue(order.orderLines[i]); 16 | total += order.orderLines[i].lineValue; 17 | } 18 | order.totalValue = +total.toFixed(2); 19 | 20 | //console.log("orderValue", total); 21 | return order; 22 | } -------------------------------------------------------------------------------- /imports/ui/security/accounts-ui-wrapper.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom' 3 | 4 | AccountsUIWrapper = React.createClass({ 5 | componentDidMount() { 6 | //console.log("AccountsUIWrapper.componentDidMount()"); 7 | // Use Meteor Blaze to render login buttons 8 | 9 | //console.log("Template.loginButtons", Template); 10 | 11 | this.view = Blaze.render(Template.loginButtons, 12 | ReactDOM.findDOMNode(this.refs.loginButtonsContainer)); 13 | }, 14 | componentWillUnmount() { 15 | // Clean up Blaze view 16 | Blaze.remove(this.view); 17 | }, 18 | render() { 19 | // Just render a placeholder container that will be filled in 20 | return ; 21 | } 22 | }); -------------------------------------------------------------------------------- /imports/ui/app/test2.jsx: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | 3 | 4 | const Test2 = React.createClass({ 5 | 6 | render() { 7 | //console.log("render()", this.props); 8 | 9 | return ( 10 |
11 |

Test 2

12 |

nisi ut 13 | aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse 14 | cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in 15 | culpa qui officia deserunt mollit anim id est laborum. async 16 |

17 |

18 | culpa qui officia deserunt mollit anim id est laborum. 19 |

20 | 21 | 22 | ); 23 | } 24 | }); 25 | 26 | export default Test2; 27 | -------------------------------------------------------------------------------- /imports/ui/redux/DevTools.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { createDevTools } from 'redux-devtools'; 3 | 4 | // Monitors are separate packages, and you can make a custom one 5 | import LogMonitor from 'redux-devtools-log-monitor'; 6 | import DockMonitor from 'redux-devtools-dock-monitor'; 7 | 8 | // createDevTools takes a monitor and produces a DevTools component 9 | const DevTools = createDevTools( 10 | // Monitors are individually adjustable with props. 11 | // Consult their repositories to learn about those props. 12 | // Here, we put LogMonitor inside a DockMonitor. 13 | // Docs: https://github.com/gaearon/redux-devtools-dock-monitor 14 | 19 | 20 | 21 | ); 22 | 23 | export default DevTools; -------------------------------------------------------------------------------- /imports/ui/app/test1.jsx: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | 3 | 4 | const Test1 = React.createClass({ 5 | 6 | render() { 7 | //console.log("render()", this.props); 8 | 9 | return ( 10 |
11 |

Test 1

12 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit. Do eiusmod tempor incididunt ut labore et 13 | dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut 14 | aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse 15 | cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in 16 | culpa qui officia deserunt mollit anim id est laborum.

17 |
18 | ); 19 | } 20 | }); 21 | 22 | export default Test1; 23 | -------------------------------------------------------------------------------- /imports/ui/components/empty.jsx: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | 3 | 4 | const Empty = React.createClass({ 5 | propTypes: { 6 | //order: React.PropTypes.object, 7 | //onSave: React.PropTypes.func.isRequired 8 | }, 9 | 10 | //getDefaultProps() { 11 | // return { 12 | // order: {} 13 | // }; 14 | //}, 15 | 16 | //getInitialState() { 17 | // //console.log("Empty.getInitialState(): props", this.props); 18 | // 19 | // return { 20 | // isValid: false 21 | // }; 22 | //}, 23 | 24 | //mixins: [ ReactMeteorData ], 25 | 26 | //getMeteorData() { 27 | // //console.log("Empty.getMeteorData"); 28 | // return { 29 | // 30 | // }; 31 | //}, 32 | 33 | 34 | render() { 35 | //console.log("render()", this.props); 36 | 37 | return ( 38 | 39 |

40 | 41 | 42 | ); 43 | } 44 | }); 45 | 46 | export default Empty; 47 | -------------------------------------------------------------------------------- /imports/ui/products/products-list-wrapper.jsx: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | import { VelocityComponent, velocityHelpers, VelocityTransitionGroup } from 'velocity-react'; 3 | 4 | import Products from '../../api/products/products'; 5 | import ProductsList from './products-list.jsx'; 6 | 7 | const ProductsListWrapper = React.createClass({ 8 | 9 | mixins: [ ReactMeteorData ], 10 | 11 | getMeteorData() { 12 | //console.log("Empty.getMeteorData"); 13 | Meteor.subscribe("Products.public"); 14 | return { 15 | products: Products.find( 16 | {}, 17 | { 18 | sort: {name: 1} 19 | } 20 | ).fetch() 21 | }; 22 | }, 23 | 24 | render() { 25 | //console.log("render()", this.props); 26 | return ( 27 |
28 | 29 |
30 | ); 31 | } 32 | }); 33 | 34 | export default ProductsListWrapper; 35 | -------------------------------------------------------------------------------- /imports/ui/dashboard/Dashboard.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react';// 2 | import { VelocityComponent, velocityHelpers, VelocityTransitionGroup } from 'velocity-react'; 3 | 4 | import TopOrdersContainer from '../sales/TopOrdersContainer.jsx'; 5 | import TopCustomersContainer from '../customers/TopCustomersContainer.jsx'; 6 | 7 | import store from '../redux/store.jsx'; 8 | 9 | 10 | const Content = React.createClass({ 11 | 12 | render() { 13 | return ( 14 |
15 |
16 |
17 |

simple crm dashboard

18 |
19 |
20 | < TopOrdersContainer store={store} /> 21 | < TopCustomersContainer store={store} /> 22 |
23 | ); 24 | } 25 | }); 26 | 27 | 28 | const Dashboard = React.createClass({ 29 | 30 | getInitialState() { 31 | return { 32 | showChild: false 33 | }; 34 | }, 35 | 36 | render() { 37 | return ( 38 | 39 | ); 40 | } 41 | }); 42 | 43 | export default Dashboard; 44 | -------------------------------------------------------------------------------- /imports/ui/stylesheets/sbAdmin2/sb-admin-2.js: -------------------------------------------------------------------------------- 1 | //Loads the correct sidebar on window load, 2 | //collapses the sidebar on window resize. 3 | // Sets the min-height of #page-wrapper to window size 4 | $(function() { 5 | $(window).bind("load resize", function() { 6 | var topOffset = 50; 7 | var width = (this.window.innerWidth > 0) ? this.window.innerWidth : this.screen.width; 8 | if (width < 768) { 9 | $('div.navbar-collapse').addClass('collapse'); 10 | topOffset = 100; // 2-row-menu 11 | } else { 12 | $('div.navbar-collapse').removeClass('collapse'); 13 | } 14 | 15 | var height = ((this.window.innerHeight > 0) ? this.window.innerHeight : this.screen.height) - 1; 16 | height = height - topOffset; 17 | if (height < 1) height = 1; 18 | if (height > topOffset) { 19 | $("#page-wrapper").css("min-height", (height) + "px"); 20 | } 21 | }); 22 | 23 | var url = window.location; 24 | var element = $('ul.nav a').filter(function() { 25 | return this.href == url; 26 | }).addClass('active').parent().parent().addClass('in').parent(); 27 | if (element.is('li')) { 28 | element.addClass('active'); 29 | } 30 | }); -------------------------------------------------------------------------------- /imports/ui/sales/OrdersList.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import OrdersListItem from './OrdersListItem.jsx'; 4 | 5 | const OrdersList = React.createClass({ 6 | propTypes: { 7 | orders: React.PropTypes.array.isRequired 8 | }, 9 | 10 | renderOrderListItems() { 11 | //console.log("orders2", this.data.orders) 12 | 13 | // Get tasks from this.data.tasks 14 | return this.props.orders.map((order) => { 15 | return ( 16 | 17 | ); 18 | }); 19 | }, 20 | 21 | render() { 22 | //console.log("orders2", this.data.orders) 23 | 24 | // Get tasks from this.data.tasks 25 | return ( 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | {this.renderOrderListItems()} 36 | 37 |
Order DateCustomerTotal
38 | ); 39 | }, 40 | 41 | }); 42 | 43 | export default OrdersList; 44 | -------------------------------------------------------------------------------- /imports/api/lib/collection-helpers.js: -------------------------------------------------------------------------------- 1 | /* 2 | This is the representation of the mongo collection. It exists on both the 3 | client and server side, but will have different data. The data is moved 4 | from client to server by the publications. Data is written back to the 5 | collection via the Methods as we have explicitly denied permissions to 6 | allow writing straight to the table in accordance with best practices. 7 | */ 8 | 9 | export function createCollection(collectionName, schema) { 10 | 11 | class GenericCollection extends Mongo.Collection { 12 | } 13 | 14 | // Make it available to the rest of the app 15 | const definedCollection = new GenericCollection(collectionName); 16 | 17 | // Deny all client-side updates since we will be using methods to manage this collection 18 | definedCollection.deny({ 19 | insert() { 20 | return true; 21 | }, 22 | update() { 23 | return true; 24 | }, 25 | remove() { 26 | return true; 27 | } 28 | }); 29 | 30 | // Bolt that schema onto the collection so that all mutator 31 | // calls are automatically checked against the schema. 32 | // Collection2 is what's allowing this to happen 33 | definedCollection.attachSchema(schema); 34 | 35 | return definedCollection; 36 | } 37 | -------------------------------------------------------------------------------- /imports/ui/security/accounts-button.jsx: -------------------------------------------------------------------------------- 1 | // This component is for a basic account button dropdown - with access to account options and logging out. 2 | 3 | import React from 'react'; 4 | import ReactDOM from 'react-dom'; 5 | 6 | AccountsButton = React.createClass({ 7 | mixins: [ ReactMeteorData ], 8 | getMeteorData() { 9 | return { 10 | currentUser: Meteor.user() 11 | }; 12 | }, 13 | logout(event) { 14 | Meteor.logout(() => { 15 | FlowRouter.go('Login'); 16 | }); 17 | }, 18 | render() { 19 | if (this.data.currentUser) { 20 | return ( 21 |
  • 22 | 23 | { this.data.currentUser.username} 24 | 25 | 26 | 30 |
  • 31 | ); 32 | } else { 33 | return ( 34 |
  • 35 | Login 36 |
  • 37 | ); 38 | } 39 | } 40 | }); 41 | 42 | export default AccountsButton; 43 | -------------------------------------------------------------------------------- /imports/api/products/methods.js: -------------------------------------------------------------------------------- 1 | import { SimpleSchema } from 'meteor/aldeed:simple-schema'; 2 | 3 | import Products from './products.js'; 4 | 5 | // Manual form specific update method that knows how to unpack the single 6 | // object we get from autoform. 7 | export const upsert = new ValidatedMethod({ 8 | 9 | // register the name 10 | name: 'Products.upsert', 11 | 12 | validate(args) { 13 | //console.log("Products.upsert.validate(args) ", args); 14 | 15 | Schemas.ProductSchema.clean(args.data, { removeEmptyStrings: false }); 16 | 17 | var schemaContext = Schemas.ProductSchema.namedContext("ProductForm"); 18 | schemaContext.validate(args.data); 19 | 20 | //console.log("validation succeeded"); 21 | }, 22 | 23 | // the actual database updating part 24 | // validate has already been run at this point 25 | run(args) { 26 | return Products.upsert(args.productId, {$set: args.data}); 27 | } 28 | }); 29 | 30 | export const remove = new ValidatedMethod({ 31 | 32 | name: 'products.remove', 33 | 34 | validate: new SimpleSchema({ 35 | productId: { type: String } 36 | }).validator(), 37 | 38 | run({ productId }) { 39 | //console.log("Products.methods.remove", productId); 40 | const product = Products.findOne(productId); 41 | 42 | // TODO: Security 43 | 44 | Products.remove(productId); 45 | } 46 | }); 47 | -------------------------------------------------------------------------------- /lib/validation-helpers.js: -------------------------------------------------------------------------------- 1 | 2 | export function validateItemAgainstSchema(item, schema) { 3 | //console.log("validateItemAgainstSchema(): item ", item); 4 | 5 | const errors = {}; 6 | 7 | // Perform the validation on a clone of the item as we need to call clean first 8 | // and clean will actually update the original item. That's bad if we are typing 9 | // a sentence with spaces in as it will become impossible to type a space as the 10 | // on change validation will remove the space. Also, we probably don't want the 11 | // data the user typed being changed without them expecting it. 12 | let clonedItem = _.clone(item); 13 | 14 | schema.clean(clonedItem); 15 | 16 | const schemaContext = schema.namedContext("validateItem"); 17 | schemaContext.validate(clonedItem); 18 | 19 | schemaContext.invalidKeys().forEach(invalidKey => { 20 | const errMessage = schemaContext.keyErrorMessage(invalidKey.name); 21 | if (invalidKey.name !== "_id") { 22 | errors[invalidKey.name] = errMessage; 23 | console.log("errMessage", errMessage); 24 | } 25 | }); 26 | 27 | return errors; 28 | } 29 | 30 | export function validateItemAndAddValidationResults(item, schema) { 31 | // validate and set (or bolt on) error messages 32 | item.errors = validateItemAgainstSchema(item, schema); 33 | item.isValid = (Object.keys(item.errors).length === 0); 34 | } 35 | -------------------------------------------------------------------------------- /client/css/react-autocomplete.css: -------------------------------------------------------------------------------- 1 | /* 2 | * react-autocomplete 3 | * Notes on theming here: https://github.com/moroshko/react-autosuggest#theme-optional 4 | * Really need to work out how to use Bootstrap's theme on this control 5 | */ 6 | 7 | .react-autosuggest__container { 8 | position: relative; 9 | } 10 | 11 | .react-autosuggest__input { 12 | width: 240px; 13 | height: 30px; 14 | padding: 10px 20px; 15 | font-family: 'Open Sans', sans-serif; 16 | font-weight: 300; 17 | font-size: 16px; 18 | border: 1px solid #aaa; 19 | border-radius: 4px; 20 | } 21 | 22 | .react-autosuggest__input:focus { 23 | outline: none; 24 | } 25 | 26 | .react-autosuggest__container--open .react-autosuggest__input { 27 | border-bottom-left-radius: 0; 28 | border-bottom-right-radius: 0; 29 | } 30 | 31 | .react-autosuggest__suggestions-container { 32 | position: relative; 33 | top: -1px; 34 | width: 280px; 35 | margin: 0; 36 | padding: 0; 37 | list-style-type: none; 38 | border: 1px solid #aaa; 39 | background-color: #fff; 40 | font-family: 'Open Sans', sans-serif; 41 | font-weight: 300; 42 | font-size: 16px; 43 | border-bottom-left-radius: 4px; 44 | border-bottom-right-radius: 4px; 45 | z-index: 2; 46 | } 47 | 48 | .react-autosuggest__suggestion { 49 | cursor: pointer; 50 | padding: 10px 20px; 51 | } 52 | 53 | .react-autosuggest__suggestion--focused { 54 | background-color: #ddd; 55 | } -------------------------------------------------------------------------------- /imports/api/orders/methods.js: -------------------------------------------------------------------------------- 1 | import Orders from './order.js'; 2 | import { SimpleSchema } from 'meteor/aldeed:simple-schema'; 3 | 4 | 5 | // Manual form specific update method that knows how to unpack the single 6 | // object we get from autoform. 7 | export const upsert = new ValidatedMethod({ 8 | 9 | // register the name 10 | name: 'orders.upsert', 11 | 12 | validate(args) { 13 | //console.log("orders.upsert.validate(args) "); 14 | 15 | Schemas.OrderSchema.clean(args.data, { removeEmptyStrings: false }); 16 | 17 | var schemaContext = Schemas.OrderSchema.namedContext("OrderForm"); 18 | schemaContext.validate(args.data); 19 | 20 | //console.log("validation succeeded"); 21 | }, 22 | 23 | 24 | // the actual database updating part 25 | // validate has already been run at this point 26 | run(args) { 27 | //console.log("order", JSON.stringify(args.data)); 28 | 29 | return Orders.upsert(args.orderId, {$set: args.data}); 30 | } 31 | }); 32 | 33 | export const remove = new ValidatedMethod({ 34 | 35 | name: 'orders.remove', 36 | 37 | validate: new SimpleSchema({ 38 | orderId: { type: String } 39 | }).validator(), 40 | 41 | run({ orderId }) { 42 | //console.log("Orders.methods.remove", orderId); 43 | 44 | const order = Orders.findOne(orderId); 45 | 46 | // TODO: Security 47 | 48 | Orders.remove(orderId); 49 | }, 50 | }); 51 | -------------------------------------------------------------------------------- /.meteor/packages: -------------------------------------------------------------------------------- 1 | # Meteor packages used by this project, one per line. 2 | # Check this file (and the other files in this directory) into your repository. 3 | # 4 | # 'meteor add' and 'meteor remove' will edit this file for you, 5 | # but you can also edit it by hand. 6 | 7 | meteor-base # Packages every Meteor app needs to have 8 | mobile-experience # Packages for a great mobile UX 9 | mongo # The database Meteor supports right now 10 | blaze-html-templates # Compile .html files into Meteor Blaze views 11 | session # Client-side reactive dictionary for your app 12 | jquery # Helpful client-side library 13 | tracker # Meteor's client-side reactive programming library 14 | 15 | es5-shim # ECMAScript 5 compatibility for older browsers. 16 | #gadicc:ecmascript-hot@0.0.1-modules.7 17 | ecmascript # Enable ECMAScript2015+ syntax in app code 18 | 19 | mdg:validated-method 20 | aldeed:simple-schema 21 | meteortoys:allthings 22 | arillo:flow-router-helpers 23 | aldeed:collection2 24 | reactive-dict 25 | reactive-var 26 | fortawesome:fontawesome 27 | alanning:roles 28 | rajit:bootstrap3-datepicker 29 | react-meteor-data 30 | kadira:flow-router 31 | underscore 32 | matb33:collection-hooks 33 | meteorhacks:aggregate 34 | accounts-password 35 | accounts-ui 36 | skinnygeek1010:flux-helpers 37 | less 38 | bootswatch:paper 39 | msavin:debugonly 40 | standard-minifier-css 41 | standard-minifier-js 42 | practicalmeteor:chai 43 | avital:mocha 44 | -------------------------------------------------------------------------------- /imports/ui/customers/CustomersList.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import accounting from 'accounting'; 3 | 4 | const CustomersList = React.createClass({ 5 | propTypes: { 6 | customers: React.PropTypes.array.isRequired 7 | }, 8 | 9 | renderCustomerListItems() { 10 | //console.log("customers2", this.props.customers) 11 | 12 | return this.props.customers.map((customer) => { 13 | 14 | return ( 15 | 16 | {customer.name} 17 | {customer.postcode} 18 | {customer.ordersCount} 19 | {accounting.formatMoney(customer.ordersTotalValue, "£")} 20 | Edit 21 | 22 | ); 23 | }); 24 | }, 25 | 26 | render() { 27 | //console.log("customers2", this.props.customers) 28 | 29 | return ( 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | {this.renderCustomerListItems()} 40 | 41 |
    Customer namePostcodeNo. ordersTotal spend
    42 | ); 43 | } 44 | }); 45 | 46 | export default CustomersList; 47 | -------------------------------------------------------------------------------- /imports/ui/components/PaginatedPanel.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const PaginatedPanel = React.createClass({ 4 | propTypes: { 5 | parentGotData: React.PropTypes.bool.isRequired, 6 | panelTitle: React.PropTypes.string, 7 | itemType: React.PropTypes.string, 8 | newItemLink: React.PropTypes.string 9 | 10 | }, 11 | 12 | shouldComponentUpdate() { 13 | //console.log("PaginatedPanel.shouldComponentUpdate() ", this.props.parentGotData); 14 | 15 | // Don't re-render if there are no records, which there won't be 16 | // after the first render (when the initial subscription happens 17 | // and before the data is actually retrieved) 18 | return (this.props.parentGotData); 19 | }, 20 | 21 | render() { 22 | //console.log("PaginatedPanel render"); 23 | 24 | return ( 25 |
    26 |
    27 |
    28 | 32 |

    {this.props.panelTitle}

    33 |
    34 |
    35 | {this.props.children} 36 |
    37 |
    38 |
    39 | ); 40 | } 41 | }); 42 | 43 | export default PaginatedPanel; 44 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "simpleCrm12", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/tomRedox/simpleCRM.git" 12 | }, 13 | "keywords": [], 14 | "author": "", 15 | "license": "ISC", 16 | "bugs": { 17 | "url": "https://github.com/tomRedox/simpleCRM/issues" 18 | }, 19 | "homepage": "https://github.com/tomRedox/simpleCRM#readme", 20 | "dependencies": { 21 | "accounting": "^0.4.1", 22 | "externalify": "^0.1.0", 23 | "history": "^2.0.0", 24 | "moment": "^2.11.2", 25 | "react": "^0.14.7", 26 | "react-addons-css-transition-group": "^0.14.7", 27 | "react-autosuggest": "^3.5.0", 28 | "react-bootstrap-datetimepicker": "0.0.22", 29 | "react-collapse": "^2.0.0", 30 | "react-dom": "^0.14.7", 31 | "react-height": "^2.0.4", 32 | "react-motion": "^0.4.2", 33 | "react-mounter": "^1.0.0", 34 | "react-redux": "^4.4.0", 35 | "react-router": "^2.0.0-rc6", 36 | "react-s-alert": "^1.0.3", 37 | "react-select": "^1.0.0-beta9", 38 | "react-transform-hmr": "^1.0.2", 39 | "redux": "^3.3.1", 40 | "redux-devtools": "^3.1.1", 41 | "redux-devtools-dock-monitor": "^1.1.0", 42 | "redux-devtools-log-monitor": "^1.0.5", 43 | "redux-thunk": "^1.0.3", 44 | "string-humanize": "^1.0.0", 45 | "velocity-react": "^1.1.3" 46 | }, 47 | "devDependencies": { 48 | "eslint": "^1.10.3", 49 | "eslint-plugin-react": "^3.16.1" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /imports/api/customers/methods.js: -------------------------------------------------------------------------------- 1 | import CustomerCompanies from './customer-company'; 2 | import { SimpleSchema } from 'meteor/aldeed:simple-schema'; 3 | 4 | CustomerCompanies.methods = {}; 5 | 6 | // Manual form specific update method that knows how to unpack the single 7 | // object we get from autoform. 8 | export const upsert = new ValidatedMethod({ 9 | 10 | // register the name 11 | name: 'CustomerCompanies.upsert', 12 | 13 | validate(args) { 14 | //console.log("CustomerCompanies.upsert.validate(args) ", args); 15 | 16 | Schemas.CustomerCompaniesSchema.clean(args.data, { removeEmptyStrings: false }); 17 | 18 | var schemaContext = Schemas.CustomerCompaniesSchema.namedContext("customerEditReactForm"); 19 | schemaContext.validate(args.data); 20 | 21 | //console.log("validation succeeded"); 22 | }, 23 | 24 | 25 | // the actual database updating part 26 | // validate has already been run at this point 27 | run(args) { 28 | //console.log("run: args", args); 29 | return CustomerCompanies.upsert(args.customerId, {$set: args.data}); 30 | } 31 | }); 32 | 33 | export const remove = new ValidatedMethod({ 34 | 35 | name: 'CustomerCompanies.remove', 36 | 37 | validate: new SimpleSchema({ 38 | customerId: { type: String } 39 | }).validator(), 40 | 41 | run({ customerId }) { 42 | //console.log("CustomerCompanies.methods.remove", customerId); 43 | const order = CustomerCompanies.findOne(customerId); 44 | 45 | // TODO: Add UserId check here 46 | 47 | CustomerCompanies.remove(customerId); 48 | }, 49 | }); 50 | -------------------------------------------------------------------------------- /imports/ui/security/login.jsx: -------------------------------------------------------------------------------- 1 | // This component is for a basic account button dropdown - with access to account options and logging out. 2 | 3 | import React from 'react'; 4 | import ReactDOM from 'react-dom'; 5 | 6 | Login = React.createClass({ 7 | onSubmit(event) { 8 | event.preventDefault(); 9 | let emailAddress = ReactDOM.findDOMNode(this.refs.emailAddress).value.trim(); 10 | let password = ReactDOM.findDOMNode(this.refs.password).value.trim(); 11 | 12 | Meteor.loginWithPassword( emailAddress, password, ( error ) => { 13 | if ( error ) { 14 | console.log(error); 15 | } else { 16 | FlowRouter.go( 'Home' ); 17 | } 18 | }); 19 | }, 20 | render() { 21 | return ( 22 |
    23 |

    Login

    24 |
    25 |
    26 | 27 | 28 |
    29 |
    30 | 34 | 35 |
    36 |
    37 | 38 |
    39 |
    40 |
    41 | ); 42 | } 43 | }); 44 | 45 | export default Login; 46 | -------------------------------------------------------------------------------- /imports/ui/sales/AllOrdersContainer.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import { connect } from 'react-redux'; 3 | 4 | import Orders from '../../api/orders/order'; 5 | import OrdersList from './OrdersList.jsx'; 6 | import PaginatedPanel from '../components/PaginatedPanel.jsx'; 7 | 8 | 9 | const AllOrdersContainer = React.createClass({ 10 | 11 | mixins: [ReactMeteorData], 12 | 13 | getMeteorData() { 14 | //console.log("getMeteorData()"); 15 | 16 | var data = {}; 17 | 18 | var handle = Meteor.subscribe('Orders.public'); 19 | 20 | if (handle.ready()) { 21 | //console.log("orders", orders); 22 | data.orders = Orders.find( 23 | {}, 24 | { 25 | sort: {createdAt: -1}, 26 | //limit: this.getRecordsToShow() 27 | } 28 | ).fetch(); 29 | } 30 | 31 | data.dataReady = handle; 32 | return data; 33 | }, 34 | 35 | render() { 36 | //console.log("OrdersListWrapper.render() "); 37 | 38 | return ( 39 | 46 | 49 | 50 | ); 51 | } 52 | }); 53 | 54 | AllOrdersContainer.propTypes = {}; 55 | 56 | function mapStateToProps(state) { 57 | //console.log("TopOrdersContainer.mapStateToProps", state) 58 | return {}; 59 | } 60 | 61 | export default connect(mapStateToProps, {})(AllOrdersContainer); 62 | -------------------------------------------------------------------------------- /imports/ui/sales/OrdersListItem.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import accounting from 'accounting'; 4 | import Alert from 'react-s-alert'; 5 | 6 | import ModalMessageBox from '../components/ModalMessageBox.jsx'; 7 | 8 | 9 | const OrdersListItem = React.createClass({ 10 | propTypes: { 11 | order: React.PropTypes.object 12 | }, 13 | 14 | deleteOrder() { 15 | //console.log("Deleting order ", this.props.order._id); 16 | 17 | Meteor.call('Orders.methods.remove', { 18 | orderId: this.props.order._id 19 | }, (err, res) => { 20 | if (err) { 21 | Alert.error(err); 22 | } else { 23 | Alert.success("Order deleted successfully"); 24 | } 25 | }); 26 | }, 27 | 28 | render() { 29 | return ( 30 | //console.log("orders2", this.data.orders) 31 | 32 | 33 | {this.props.order.createdAt.toLocaleDateString()} 34 | {this.props.order.customerName} 35 | {accounting.formatMoney(this.props.order.totalValue, '£')} 36 | 37 | Delete 38 | 44 | 45 | Edit 46 | 47 | ); 48 | } 49 | }); 50 | 51 | export default OrdersListItem; -------------------------------------------------------------------------------- /lib/order-logic.tests.js: -------------------------------------------------------------------------------- 1 | import { mocha } from 'meteor/avital:mocha'; 2 | import { chai, assert } from "meteor/practicalmeteor:chai"; 3 | import { lineValue, recalculateOrderTotals } from './order-logic' 4 | 5 | //// This is what breaks it for me 6 | ////const { describe, it } = mocha; 7 | 8 | describe('order-logic.js', () => { 9 | 10 | describe('lineValue', () => { 11 | it('calculates value correctly when quantity is zero', () => { 12 | const orderLine = {quantity: 0, unitPrice: 3}; 13 | expect(lineValue(orderLine)).to.equal(0); 14 | }); 15 | 16 | it('calculates unit price correctly when values set', () => { 17 | const orderLine = {quantity: 4, unitPrice: 5}; 18 | expect(lineValue(orderLine)).to.equal(20); 19 | }); 20 | }); 21 | 22 | 23 | describe('recalculateOrderTotals', () => { 24 | it('calculates value correctly for no lines', () => { 25 | const order = {orderLines: []}; 26 | 27 | const result = recalculateOrderTotals(order); 28 | 29 | expect(result.totalValue).to.equal(0); 30 | }); 31 | 32 | it('calculates value correctly for one line', () => { 33 | const orderLine1 = {quantity: 2, unitPrice: 3}; 34 | const order = {orderLines: [orderLine1]}; 35 | 36 | const result = recalculateOrderTotals(order); 37 | 38 | expect(result.totalValue).to.equal(6); 39 | }); 40 | 41 | it('calculates value correctly for two lines', () => { 42 | const orderLine1 = {quantity: 2, unitPrice: 3}; 43 | const orderLine2 = {quantity: 6, unitPrice: 4}; 44 | const order = {orderLines: [orderLine1, orderLine2]}; 45 | 46 | const result = recalculateOrderTotals(order); 47 | 48 | expect(result.totalValue).to.equal(30); 49 | }); 50 | }) 51 | 52 | }) -------------------------------------------------------------------------------- /client/css/order-lines-table.css: -------------------------------------------------------------------------------- 1 | 2 | /* 3 | * Order lines table 4 | * http://bootsnipp.com/snippets/featured/responsive-shopping-cart 5 | */ 6 | .table#cart > tbody > tr > td, .table > tfoot > tr > td { 7 | vertical-align: middle; 8 | } 9 | 10 | .table#cart > thead > tr > th { 11 | vertical-align: middle; 12 | } 13 | 14 | table#cart tbody td { 15 | vertical-align: middle; 16 | } 17 | 18 | table#cart tbody td .form-control { 19 | text-align: right; 20 | } 21 | 22 | table#cart tbody td .numeric { 23 | text-align: right; 24 | } 25 | 26 | /* This is the behaviour for when the the table is less than the max-width wide.*/ 27 | @media screen and (max-width: 600px) { 28 | 29 | /* these are the form control fields (qty, unit price etc.)*/ 30 | table#cart tbody td .form-control, table#cart tbody td .sub-total { 31 | width: 25%; 32 | display: inline !important; 33 | text-align: right; 34 | } 35 | 36 | .actions .btn { 37 | width: 36%; 38 | margin: 1.5em 0; 39 | } 40 | 41 | .actions .btn-info { 42 | float: left; 43 | } 44 | 45 | #deleteOrderLineButton { 46 | float: left; 47 | } 48 | 49 | table#cart thead { 50 | display: none; 51 | } 52 | 53 | table#cart tbody td { 54 | display: block; 55 | padding: .6rem; 56 | min-width: 320px; 57 | } 58 | 59 | /*table#cart tbody tr td:first-child { background: #333; color: #fff; }*/ 60 | /* 61 | This is giving us the row labels for the number fields on each row 62 | It does this by parroting back the text we set in the data-th attribute in the jsx 63 | */ 64 | table#cart tbody td:before { 65 | content: attr(data-th); 66 | font-weight: bold; 67 | display: inline-block; 68 | width: 8rem; 69 | } 70 | 71 | table#cart tfoot td { 72 | display: block; 73 | } 74 | 75 | table#cart tfoot td .btn { 76 | display: block; 77 | } 78 | 79 | } -------------------------------------------------------------------------------- /imports/ui/customers/AllCustomersContainer.jsx: -------------------------------------------------------------------------------- 1 | 2 | import React, { Component, PropTypes } from 'react'; 3 | import { connect } from 'react-redux'; 4 | 5 | import CustomerCompanies from '../../api/customers/customer-company'; 6 | 7 | import CustomersList from './CustomersList.jsx'; 8 | import PaginatedPanel from '../components/PaginatedPanel.jsx'; 9 | 10 | 11 | const AllCustomersContainer = React.createClass({ 12 | 13 | mixins: [ReactMeteorData], 14 | 15 | getMeteorData() { 16 | //console.log("getMeteorData()"); 17 | 18 | var data = {}; 19 | 20 | var handle = Meteor.subscribe('CustomerCompanies.public'); 21 | 22 | if (handle.ready()) { 23 | //console.log("customers", customers); 24 | data.customers = CustomerCompanies.find( 25 | {}, 26 | { 27 | sort: {name: 1}, 28 | //limit: this.getRecordsToShow() 29 | } 30 | ).fetch(); 31 | } 32 | 33 | data.dataReady = handle; 34 | return data; 35 | }, 36 | 37 | render() { 38 | //console.log("CustomersListWrapper.render() "); 39 | 40 | return ( 41 | 48 | 51 | 52 | ); 53 | } 54 | }); 55 | 56 | AllCustomersContainer.propTypes = { 57 | // add prop types here 58 | }; 59 | 60 | function mapStateToProps(state) { 61 | //console.log("TopCustomersContainer.mapStateToProps", state) 62 | return { 63 | // add bindings here 64 | }; 65 | } 66 | 67 | export default connect(mapStateToProps, { 68 | // add function bindings here 69 | })(AllCustomersContainer); 70 | -------------------------------------------------------------------------------- /imports/ui/security/register.jsx: -------------------------------------------------------------------------------- 1 | // This component is for a basic account button dropdown - with access to account options and logging out. 2 | 3 | import React from 'react'; 4 | import ReactDOM from 'react-dom'; 5 | 6 | Register = React.createClass({ 7 | getInitialState() { 8 | return { 9 | status: '' 10 | }; 11 | }, 12 | onSubmit(event) { 13 | event.preventDefault(); 14 | let emailAddress = ReactDOM.findDOMNode(this.refs.emailAddress).value.trim(); 15 | let password = ReactDOM.findDOMNode(this.refs.password).value.trim(); 16 | 17 | Accounts.createUser({ 18 | username: emailAddress, 19 | email: emailAddress, 20 | password 21 | },(error) => { 22 | if (error) { 23 | ReactDOM.findDOMNode(this.refs.error).value = error; 24 | } else { 25 | ReactDOM.findDOMNode(this.refs.emailAddress).value = ''; 26 | ReactDOM.findDOMNode(this.refs.password).value = ''; 27 | this.setState({ 28 | status: `Success! User ${emailAddress} Created.` 29 | }); 30 | } 31 | }); 32 | }, 33 | render() { 34 | return ( 35 |
    36 |

    Register a new user

    37 |
    38 |
    39 | 40 | 41 |
    42 |
    43 | 44 | 45 |
    46 |
    47 | 48 |
    49 |

    {this.state.status}

    50 |
    51 |
    52 | 53 | ); 54 | } 55 | }); 56 | 57 | export default Register; 58 | -------------------------------------------------------------------------------- /imports/ui/app/app-not-found.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default class AppNotFound extends React.Component { 4 | render() { 5 | return ( 6 |
    7 |
    8 |
    9 |
    10 |
    11 |

    12 | Oops

    14 |

    Page not found - 404 error

    15 |
    16 |
    17 |

    The page you are looking for might have been removed, had its name 18 | changed, or is temporarily unavailable. Please try the following:

    19 | 20 |
      21 |
    • Make sure that the Web site address 22 | displayed in the address bar of your browser is spelled and 23 | formatted correctly. 24 |
    • 25 |
    • If you reached this page by clicking a link, 26 | contact us to alert 27 | us that the link is incorrectly formatted. 28 |
    • 29 |
    • Forget that this ever happened, and go our Home page :) 31 |
    • 32 |
    33 |
    34 |
    35 |
    36 |
    37 |
    38 | ); 39 | } 40 | } -------------------------------------------------------------------------------- /imports/ui/sales/OrderLinesList.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import GridRow from '../components/grid/GridRow.jsx' 4 | import GridColumn from '../components/grid/GridColumn.jsx' 5 | import GridHeaderColumn from '../components/grid/GridHeaderColumn.jsx' 6 | import GridHeaderRow from '../components/grid/GridHeaderRow.jsx' 7 | import OrderLineEdit from './OrderLineEdit.jsx'; 8 | 9 | 10 | // App component - represents the whole app 11 | const OrderLinesList = React.createClass({ 12 | propTypes: { 13 | order: React.PropTypes.object.isRequired, 14 | onChildChange: React.PropTypes.func.isRequired, 15 | onProductChange: React.PropTypes.func.isRequired, 16 | deleteOrderLine: React.PropTypes.func 17 | }, 18 | 19 | renderOrderLines() { 20 | //console.log("customers2", this.data.customers) 21 | 22 | // Get tasks from this.data.tasks 23 | if (!this.props.order.orderLines) { 24 | return; 25 | } 26 | 27 | return this.props.order.orderLines.map((orderLine) => { 28 | 29 | return ( 30 | 31 |
    32 | 33 | 40 | 41 |
    42 | ); 43 | }); 44 | }, 45 | 46 | render() { 47 | //console.log("OrderLinesList.render()"); 48 | return ( 49 |
    50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | {this.renderOrderLines()} 58 |
    59 | ); 60 | } 61 | }); 62 | 63 | export default OrderLinesList; 64 | -------------------------------------------------------------------------------- /imports/api/products/server/publications.js: -------------------------------------------------------------------------------- 1 | import Products from '../products'; 2 | 3 | const ProductListFields = { 4 | name: 1, 5 | price: 1, 6 | createdAt: 1 7 | }; 8 | 9 | Meteor.publish('Products.public', function () { 10 | 11 | // TODO: Security 12 | //if (!this.userId) { 13 | // return this.ready(); 14 | //} 15 | 16 | return Products.find( 17 | {}, 18 | { 19 | fields: ProductListFields 20 | } 21 | ); 22 | }); 23 | 24 | Meteor.publish('Product.get', function (_id) { 25 | //console.log("publication match ", Products.find({_id: custId}).fetch()); 26 | 27 | // TODO: Security 28 | //if (!this.userId) { 29 | // return this.ready(); 30 | //} 31 | 32 | return Products.find( 33 | { 34 | _id 35 | }, 36 | { 37 | fields: ProductListFields 38 | } 39 | ); 40 | }); 41 | 42 | Meteor.publish('Products.searchByName', function (searchTerm) { 43 | //console.log("Products.searchByName - " + 44 | // searchTerm + " - ", Products.find({name: new RegExp(searchTerm)}).fetch()); 45 | 46 | 47 | // the 'i' makes the search case insensitive 48 | return Products.find( 49 | { 50 | name: new RegExp(searchTerm, 'i') 51 | }, 52 | { 53 | fields: ProductListFields 54 | } 55 | ); 56 | }); 57 | 58 | Meteor.methods({ 59 | 'Products.fullTextSearch.method'({ searchValue }) { 60 | 61 | if (Meteor.isServer) { 62 | const results = Products.find( 63 | {$text: {$search: searchValue}}, 64 | { 65 | // `fields` is where we can add MongoDB projections. Here we're causing 66 | // each document published to include a property named `score`, which 67 | // contains the document's search rank, a numerical value, with more 68 | // relevant documents having a higher score. 69 | fields: { 70 | score: {$meta: "textScore"}, 71 | name: 1 72 | }, 73 | // This indicates that we wish the publication to be sorted by the 74 | // `score` property specified in the projection fields above. 75 | sort: { 76 | score: {$meta: "textScore"} 77 | } 78 | } 79 | ); 80 | //console.log('Products.fullTextSearch results ', results.fetch()); 81 | 82 | return results.fetch(); 83 | } 84 | } 85 | }); -------------------------------------------------------------------------------- /imports/ui/customers/TopCustomersContainer.jsx: -------------------------------------------------------------------------------- 1 | 2 | import React, { Component, PropTypes } from 'react'; 3 | import { connect } from 'react-redux'; 4 | 5 | import CustomerCompanies from '../../api/customers/customer-company'; 6 | import CustomersList from './CustomersList.jsx'; 7 | import CollapsiblePanel from '../components/CollapsiblePanel.jsx'; 8 | 9 | import { toggleExpanded } from '../redux/customer-list-actions.jsx'; 10 | 11 | const MINIMISED_RECORD_COUNT = 3; 12 | const EXPANDED_RECORD_COUNT = 9; 13 | 14 | const TopCustomersContainer = React.createClass({ 15 | 16 | mixins: [ReactMeteorData], 17 | 18 | getMeteorData() { 19 | //console.log("getMeteorData()"); 20 | 21 | var data = {}; 22 | 23 | var handle = Meteor.subscribe('CustomerCompanies.topCustomerCompanies', this.getRecordsToShow()); 24 | 25 | if (handle.ready()) { 26 | //const customers = CustomerCompanies.find().fetch(); 27 | //console.log("customers", customers); 28 | data.customers = CustomerCompanies.find( 29 | {}, 30 | { 31 | sort: {ordersTotalValue: -1}, 32 | limit: this.getRecordsToShow() 33 | } 34 | ).fetch(); 35 | } 36 | return data; 37 | }, 38 | 39 | getRecordsToShow() { 40 | let recordsToShow = MINIMISED_RECORD_COUNT; 41 | if (this.props.expanded) { 42 | recordsToShow = EXPANDED_RECORD_COUNT; 43 | } 44 | return recordsToShow; 45 | }, 46 | 47 | render() { 48 | //console.log("CustomersListWrapper.render() "); 49 | 50 | return ( 51 | 59 | 62 | 63 | ); 64 | } 65 | }); 66 | 67 | TopCustomersContainer.propTypes = { 68 | expanded: PropTypes.bool.isRequired 69 | }; 70 | 71 | function mapStateToProps(state) { 72 | //console.log("TopCustomersContainer.mapStateToProps", state) 73 | return { 74 | expanded: state.userInterface.customerList.expanded 75 | }; 76 | } 77 | 78 | export default connect(mapStateToProps, { 79 | toggleExpanded 80 | })(TopCustomersContainer); 81 | -------------------------------------------------------------------------------- /imports/ui/customers/CustomerContainer.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import { connect } from 'react-redux'; 3 | 4 | import CustomerEditForm from './CustomerEditForm.jsx'; 5 | 6 | import { editCustomer, selectCustomer, selectNewCustomer, saveCustomer } from '../redux/customer_actions.jsx'; 7 | import CustomerCompanies from '../../api/customers/customer-company'; 8 | import SalesRegions from '../../api/sales-regions/sales-region'; 9 | 10 | export const CustomerContainer = React.createClass({ 11 | 12 | componentWillMount() { 13 | //console.log("CustomerContainer.componentWillMount()", this.props); 14 | 15 | const customerId = FlowRouter.getParam('_id'); 16 | 17 | if (customerId) { 18 | this.sub = Meteor.subscribe('CustomerCompany.get', customerId, this.setCustomerInState); 19 | } else { 20 | this.props.selectNewCustomer(); 21 | } 22 | 23 | }, 24 | 25 | setCustomerInState() { 26 | this.props.selectCustomer(FlowRouter.getParam('_id')); 27 | }, 28 | 29 | componentWillUnmount() { 30 | if (this.sub) { 31 | this.sub.stop(); 32 | } 33 | }, 34 | 35 | shouldComponentUpdate() { 36 | //console.log("shouldComponentUpdate", this.sub); 37 | return (!this.sub || this.sub.ready); 38 | }, 39 | 40 | render() { 41 | //console.log("CustomerContainer.render()", this.props); 42 | 43 | if (this.sub && !this.sub.ready) { 44 | return (

    Loading

    ); 45 | } 46 | 47 | //debugger // checkout this.props with debugger! 48 | return ( 49 | ); 57 | } 58 | }); 59 | 60 | CustomerContainer.propTypes = { 61 | customer: PropTypes.object, 62 | onSave: PropTypes.func.isRequired, 63 | onChange: PropTypes.func.isRequired, 64 | selectCustomer: PropTypes.func.isRequired, 65 | selectNewCustomer: PropTypes.func.isRequired, 66 | 67 | }; 68 | 69 | function mapStateToProps(state) { 70 | return { 71 | customer: state.userInterface.customerBeingEdited 72 | }; 73 | } 74 | 75 | export default connect(mapStateToProps, { 76 | onSave: saveCustomer, 77 | onChange: editCustomer, 78 | selectCustomer, 79 | selectNewCustomer 80 | })(CustomerContainer); -------------------------------------------------------------------------------- /imports/ui/components/SelectInput.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import humanize from 'string-humanize'; 3 | import Select from 'react-select'; 4 | 5 | SelectInput = React.createClass({ 6 | // list out our required and optional properties for this class 7 | propTypes: { 8 | name: React.PropTypes.string.isRequired, 9 | label: React.PropTypes.string.isRequired, 10 | value: React.PropTypes.string, 11 | options: React.PropTypes.array.isRequired, 12 | onChange: React.PropTypes.func.isRequired, 13 | valueKey: React.PropTypes.string.isRequired, 14 | labelKey: React.PropTypes.string.isRequired, 15 | error: React.PropTypes.string, 16 | hideLabel: React.PropTypes.bool 17 | }, 18 | 19 | 20 | onChangeHandler(selectedOption) { 21 | //console.log("selectInput event ", event) 22 | this.props.onChange({ 23 | name: this.props.name, 24 | labelKey: this.props.labelKey, 25 | valueKey: this.props.valueKey, 26 | selectedOption //1.0.0 this is the selected row object, not just the id 27 | }); 28 | }, 29 | 30 | renderLabel() { 31 | if (!this.props.hideLabel) { 32 | return ( 33 | 34 | ); 35 | } 36 | }, 37 | 38 | render() { 39 | // This is for bootstrap, we want to wrap our label and textbox in a 'form-group' 40 | // class, and also to add 'has-error' (which gives us a red outline) if the data is in error 41 | var wrapperClass = 'form-group'; 42 | if (this.props.error && this.props.error.length > 0) { 43 | wrapperClass += " " + 'has-error'; 44 | } 45 | 46 | const humanizedName = humanize(this.props.name); 47 | 48 | return ( 49 |
    50 | {this.renderLabel()} 51 |
    52 |