├── .gitignore ├── LICENSE ├── README.md ├── client ├── .gitignore ├── .npmrc ├── README.md ├── app.js ├── app.json ├── app │ ├── Application.js │ ├── Application.scss │ ├── controller │ │ └── Action.js │ ├── model │ │ ├── Action.js │ │ ├── Base.js │ │ ├── Event.js │ │ ├── Filter.js │ │ ├── Office.js │ │ ├── Organization.js │ │ ├── Person.js │ │ └── Session.js │ ├── profile │ │ ├── Phone.js │ │ ├── Phone.scss │ │ └── Tablet.js │ ├── store │ │ ├── Actions.js │ │ ├── Events.js │ │ ├── Filters.js │ │ ├── Menu.js │ │ ├── Offices.js │ │ ├── Organizations.js │ │ └── People.js │ ├── util │ │ ├── Errors.js │ │ └── State.js │ └── view │ │ ├── auth │ │ ├── Login.js │ │ ├── Login.scss │ │ └── LoginController.js │ │ ├── history │ │ ├── Browse.js │ │ ├── Browse.scss │ │ ├── BrowseController.js │ │ └── BrowseModel.js │ │ ├── home │ │ ├── Home.js │ │ ├── HomeController.js │ │ ├── HomeEvents.js │ │ ├── HomeEvents.scss │ │ ├── HomeHeader.js │ │ ├── HomeHeader.scss │ │ └── HomeModel.js │ │ ├── main │ │ ├── MainController.js │ │ ├── MainModel.js │ │ ├── Menu.js │ │ ├── Menu.scss │ │ └── MenuController.js │ │ ├── office │ │ ├── Browse.js │ │ ├── BrowseController.js │ │ ├── BrowseModel.js │ │ ├── Show.js │ │ ├── Show.scss │ │ ├── ShowController.js │ │ ├── ShowDetails.js │ │ ├── ShowDetails.scss │ │ ├── ShowModel.js │ │ ├── ShowPeople.js │ │ └── Wizard.js │ │ ├── organization │ │ ├── Browse.js │ │ ├── BrowseController.js │ │ ├── BrowseModel.js │ │ ├── Show.js │ │ ├── ShowController.js │ │ ├── ShowModel.js │ │ ├── ShowPeople.js │ │ ├── Wizard.js │ │ ├── WizardController.js │ │ └── WizardModel.js │ │ ├── person │ │ ├── Browse.js │ │ ├── BrowseController.js │ │ ├── BrowseModel.js │ │ ├── Show.js │ │ ├── ShowController.js │ │ ├── ShowDetails.js │ │ ├── ShowDetails.scss │ │ ├── ShowHeader.js │ │ ├── ShowHeader.scss │ │ ├── ShowModel.js │ │ ├── ShowOffice.js │ │ ├── ShowOrg.js │ │ ├── ShowOrg.scss │ │ ├── ShowTools.js │ │ ├── ShowTools.scss │ │ ├── Wizard.js │ │ ├── WizardController.js │ │ └── WizardModel.js │ │ ├── phone │ │ ├── history │ │ │ └── Browse.js │ │ ├── main │ │ │ ├── Main.js │ │ │ └── MainController.js │ │ ├── office │ │ │ └── Browse.js │ │ ├── organisation │ │ │ └── Browse.js │ │ └── person │ │ │ ├── Browse.js │ │ │ ├── BrowseController.js │ │ │ ├── BrowseFilters.js │ │ │ └── ListSwiperItem.js │ │ ├── tablet │ │ ├── history │ │ │ ├── Browse.js │ │ │ └── BrowseToolbar.js │ │ ├── main │ │ │ └── Main.js │ │ ├── office │ │ │ ├── Browse.js │ │ │ ├── BrowseController.js │ │ │ └── BrowseToolbar.js │ │ ├── organization │ │ │ ├── Browse.js │ │ │ ├── BrowseController.js │ │ │ └── BrowseToolbar.js │ │ └── person │ │ │ ├── Browse.js │ │ │ ├── BrowseController.js │ │ │ └── BrowseToolbar.js │ │ ├── viewport │ │ ├── ViewportController.js │ │ └── ViewportModel.js │ │ └── widgets │ │ ├── Browse.js │ │ ├── BrowseController.js │ │ ├── BrowseToolbar.js │ │ ├── HistoryPanel.js │ │ ├── HistoryView.js │ │ ├── HistoryView.scss │ │ ├── MapView.js │ │ ├── MapView.scss │ │ ├── Show.js │ │ ├── Show.scss │ │ ├── ShowController.js │ │ ├── ShowHeader.js │ │ ├── ShowHeader.scss │ │ ├── Sidebar.js │ │ ├── Sidebar.scss │ │ ├── Wizard.js │ │ ├── Wizard.scss │ │ └── WizardController.js ├── build.xml ├── index.html ├── overrides │ └── util │ │ └── Format.js ├── package.json ├── packages │ └── local │ │ └── coworkee │ │ ├── build.xml │ │ ├── overrides │ │ ├── LoadMask.js │ │ ├── dataview │ │ │ └── listswiper │ │ │ │ └── ListSwiper.js │ │ ├── field │ │ │ └── Field.js │ │ └── init.js │ │ ├── package.json │ │ └── sass │ │ ├── etc │ │ └── all.scss │ │ ├── src │ │ ├── Button.scss │ │ ├── Mask.scss │ │ ├── Panel.scss │ │ ├── Toolbar.scss │ │ ├── dataview │ │ │ ├── DataView.scss │ │ │ ├── ListItem.scss │ │ │ └── listswiper │ │ │ │ └── Stepper.scss │ │ ├── field │ │ │ └── Search.scss │ │ ├── grid │ │ │ └── column │ │ │ │ └── Column.scss │ │ └── tab │ │ │ └── Panel.scss │ │ └── var │ │ └── all.scss ├── resources │ └── images │ │ ├── auth-background.jpg │ │ └── loading.png ├── webpack.config.js └── workspace.json └── server ├── .npmrc ├── api ├── actions.js ├── auth.js ├── events.js ├── offices.js ├── organizations.js └── people.js ├── app.js ├── bin └── www ├── config.json ├── data ├── Offices.json ├── Organizations.json └── People.json ├── models ├── action.js ├── index.js ├── office.js ├── organization.js └── person.js ├── package.json ├── public ├── api │ └── portraits │ │ ├── men │ │ ├── 0.jpg │ │ ├── 1.jpg │ │ ├── 10.jpg │ │ ├── 11.jpg │ │ ├── 12.jpg │ │ ├── 13.jpg │ │ ├── 14.jpg │ │ ├── 15.jpg │ │ ├── 16.jpg │ │ ├── 17.jpg │ │ ├── 18.jpg │ │ ├── 19.jpg │ │ ├── 2.jpg │ │ ├── 20.jpg │ │ ├── 21.jpg │ │ ├── 22.jpg │ │ ├── 23.jpg │ │ ├── 24.jpg │ │ ├── 25.jpg │ │ ├── 26.jpg │ │ ├── 3.jpg │ │ ├── 4.jpg │ │ ├── 5.jpg │ │ ├── 6.jpg │ │ ├── 7.jpg │ │ ├── 8.jpg │ │ └── 9.jpg │ │ └── women │ │ ├── 0.jpg │ │ ├── 1.jpg │ │ ├── 10.jpg │ │ ├── 11.jpg │ │ ├── 12.jpg │ │ ├── 13.jpg │ │ ├── 14.jpg │ │ ├── 15.jpg │ │ ├── 16.jpg │ │ ├── 17.jpg │ │ ├── 18.jpg │ │ ├── 19.jpg │ │ ├── 2.jpg │ │ ├── 20.jpg │ │ ├── 21.jpg │ │ ├── 22.jpg │ │ ├── 23.jpg │ │ ├── 3.jpg │ │ ├── 4.jpg │ │ ├── 5.jpg │ │ ├── 6.jpg │ │ ├── 7.jpg │ │ ├── 8.jpg │ │ └── 9.jpg └── stylesheets │ └── style.css ├── utils ├── config.js ├── data.js ├── errors.js ├── helpers.js └── session.js └── views ├── error.jade └── layout.jade /.gitignore: -------------------------------------------------------------------------------- 1 | /.vscode 2 | /client/app.watch.lock 3 | /client/bootstrap* 4 | /client/index.js* 5 | /server/*.db 6 | /server/node_modules 7 | /client/packages/local/ext-google 8 | *.log -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Sencha, Inc 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ext JS Employee Directory 2 | Ext JS Sample Application - Employee Directory (Coworkee) 3 | 4 | ## Getting started 5 | ### Prerequisite 6 | - Login to Sencha [NPM Repository](http://docs.sencha.com/extjs/7.0.0/guides/getting_started/open_tooling.html#getting_started-_-open_tooling_-_step_2__login_to_the_npm_repository) 7 | 8 | `npm login --registry=https://npm.sencha.com --scope=@sencha` 9 | 10 | ### Install the server 11 | Install the server node.js dependencies: 12 | 13 | $ cd server 14 | $ npm install 15 | 16 | ### Build the client 17 | Install the Ext JS framework for the application: 18 | 19 | $ cd client 20 | $ npm install 21 | 22 | Development build: 23 | 24 | $ npm start 25 | 26 | Production build: 27 | 28 | $ npm run production 29 | 30 | ### Run the app 31 | 32 | $ cd server 33 | $ npm start 34 | 35 | Note: by default, `npm start` will use the **development** build. To run the production 36 | build, use the following command instead: 37 | 38 | $ npm start -- --client-environment=production 39 | 40 | Open your browser on http://localhost:3000 41 | 42 | #### Network access 43 | 44 | By default, the server is setup to expose the Ext.Direct API through `localhost`. This 45 | address can be changed via the [`direct.server`](server/config.json#L16) option (e.g. 46 | `192.168.1.2`), in which case the client must be launched using the same address (e.g. 47 | `https://192.168.1.2:3000`). If the client needs to be accessed with a different address, 48 | you first need to enable CORS using [`cors.enabled: true`](server/config.json#L3). 49 | 50 | #### Cordova / PhoneGap 51 | If the app is ran inside 52 | [Cordova (or PhoneGap)](https://docs.sencha.com/cmd/guides/cordova_phonegap.html), it's 53 | required to change the following configs: 54 | 55 | - change the Ext.Direct API endpoint in the client app ([`app.json#js`](client/app.json#L254)) by the absolute URL 56 | - change the server IP/hostname ([`direct.server` option](server/config.json#L16)) by an accessible endpoint 57 | - enable CORS ([`cors.enabled: true`](server/config.json#L3)) 58 | -------------------------------------------------------------------------------- /client/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | /ext 3 | /node_modules 4 | /packages/local/ext-google 5 | bootstrap.* 6 | classic.json* 7 | modern.json* 8 | .sencha 9 | -------------------------------------------------------------------------------- /client/.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false -------------------------------------------------------------------------------- /client/app.js: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is generated and updated by Sencha Cmd. You can edit this file as 3 | * needed for your application, but these edits will have to be merged by 4 | * Sencha Cmd when upgrading. 5 | */ 6 | Ext.application({ 7 | name: 'App', 8 | 9 | extend: 'App.Application' 10 | 11 | //------------------------------------------------------------------------- 12 | // Most customizations should be made to App.Application. If you need to 13 | // customize this file, doing so below this section reduces the likelihood 14 | // of merge conflicts when upgrading to new versions of Sencha Cmd. 15 | //------------------------------------------------------------------------- 16 | }); 17 | -------------------------------------------------------------------------------- /client/app/Application.js: -------------------------------------------------------------------------------- 1 | Ext.define('App.Application', { 2 | extend: 'Ext.app.Application', 3 | 4 | name: 'App', 5 | 6 | // Since all the files in the ./app folder should be included in the final build, let's 7 | // require all application classes (App.*) and avoid redundant 'requires' in each files. 8 | requires: [ 9 | 'App.*', 10 | 'Ext.MessageBox' 11 | ], 12 | 13 | profiles: [ 14 | 'Phone', 15 | 'Tablet' 16 | ], 17 | 18 | controllers: [ 19 | 'Action' // creates one global instance of the Action controller 20 | ], 21 | 22 | stores: [ 23 | 'Menu' // creates one global instance of the Menu store (Ext.getStore('Menu')) 24 | ], 25 | 26 | viewport: { 27 | controller: 'viewport', 28 | viewModel: 'viewport' 29 | }, 30 | 31 | defaultToken: 'home', 32 | 33 | launch: function(profile) { 34 | // The viewport controller requires xtype defined by profiles, so let's perform extra 35 | // initialization when the application and its dependencies are fully accessible. 36 | Ext.Viewport.getController().onLaunch(); 37 | Ext.getBody().removeCls('launching'); 38 | this.callParent([profile]); 39 | }, 40 | 41 | onAppUpdate: function () { 42 | Ext.Msg.confirm('Application Update', 'This application has an update, reload?', 43 | function (choice) { 44 | if (choice === 'yes') { 45 | window.location.reload(); 46 | } 47 | } 48 | ); 49 | } 50 | }); 51 | -------------------------------------------------------------------------------- /client/app/controller/Action.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This global controller is responsible for executing actions for a specific Person record, 3 | * such as opening Skype or the email application, but also for logging these actions to the 4 | * server. View controllers can interact with this controller by firing the 'actionlog' or 5 | * 'actionexec' event, for example: 6 | * 7 | * this.fireEvent('actionlog', 'profile', record) 8 | * this.fireEvent('actionexec', 'skype', record) 9 | */ 10 | Ext.define('App.controller.Action', { 11 | extend: 'Ext.app.Controller', 12 | 13 | listen: { 14 | controller: { 15 | '*': { 16 | actionlog: 'log', 17 | actionexec: 'exec' 18 | } 19 | } 20 | }, 21 | 22 | log: function(action, record) { 23 | Ext.create('App.model.Action', { 24 | type: action, 25 | recipient_id: record.getId() 26 | }).save(); 27 | }, 28 | 29 | exec: function(action, record) { 30 | if (!record) { 31 | return false; 32 | } 33 | 34 | switch (action) { 35 | case 'email': 36 | record.mailTo(); 37 | break; 38 | case 'linkedin': 39 | record.linkedIn(); 40 | break; 41 | case 'phone': 42 | record.phoneCall(); 43 | break; 44 | case 'skype': 45 | record.skypeCall(); 46 | break; 47 | default: 48 | Ext.error('Unknown action: ' + action); 49 | return false; 50 | } 51 | 52 | this.log(action, record); 53 | return true; 54 | } 55 | }); 56 | -------------------------------------------------------------------------------- /client/app/model/Action.js: -------------------------------------------------------------------------------- 1 | Ext.define('App.model.Action', { 2 | extend: 'App.model.Base', 3 | 4 | fields: [ 5 | { name: 'type', type: 'string' }, 6 | { name: 'subject', type: 'string' }, 7 | { name: 'recipient_id', reference: 'Person' }, 8 | { name: 'created', type: 'date', dateFormat: 'C', persist: false } 9 | ], 10 | 11 | toUrl: function() { 12 | return Ext.String.format('person/{0}', this.getRecipient().get('username')); 13 | }, 14 | 15 | proxy: { 16 | api: { 17 | prefix: 'Server.actions' 18 | } 19 | } 20 | }); 21 | -------------------------------------------------------------------------------- /client/app/model/Base.js: -------------------------------------------------------------------------------- 1 | // http://docs.sencha.com/extjs/latest/guides/core_concepts/data_package.html 2 | Ext.define('App.model.Base', { 3 | extend: 'Ext.data.Model', 4 | identifier: 'uuid', 5 | 6 | requires: [ 7 | 'Ext.direct.RemotingProvider', 8 | 'Ext.data.identifier.Uuid' 9 | ], 10 | 11 | fields: [ 12 | // Calculated fields 13 | // https://docs.sencha.com/extjs/latest/modern/Ext.data.field.Field.html#cfg-calculate 14 | { name: 'url', calculate: function (data) { 15 | return Ext.String.format('{0:lowercase}/{1}', 16 | this.owner.entityName, 17 | data.id); 18 | }} 19 | ], 20 | 21 | schema: { 22 | // Setting the models namespace to produce proper association getter names. 23 | // http://docs.sencha.com/extjs/latest/modern/Ext.data.schema.Schema.html#ext-data-schema-schema_relative-naming 24 | namespace: 'App.model', 25 | 26 | proxy: { 27 | type: 'direct', 28 | api: { 29 | create: 'insert', 30 | read: 'list', 31 | update: 'update', 32 | destroy: 'remove' 33 | }, 34 | reader: { 35 | type: 'json', 36 | rootProperty: 'data', 37 | messageProperty: 'message' 38 | } 39 | } 40 | }, 41 | 42 | toUrl: function() { 43 | return this.get('url'); 44 | }, 45 | 46 | toEditUrl: function() { 47 | return this.toUrl() + '/edit'; 48 | } 49 | }); 50 | -------------------------------------------------------------------------------- /client/app/model/Event.js: -------------------------------------------------------------------------------- 1 | Ext.define('App.model.Event', { 2 | extend: 'App.model.Base', 3 | 4 | fields: [ 5 | { name: 'date', type: 'date' }, 6 | { name: 'type', type: 'string' }, 7 | { name: 'person_id', reference: 'Person' }, 8 | 9 | // Calculated fields 10 | // 'days_to_now' is used in store to locally sort and group events. 11 | { name: 'days_to_now', calculate: function (data) { 12 | return data.date? Ext.Date.diff( 13 | Ext.Date.clearTime(new Date()), 14 | Ext.Date.clearTime(data.date), 15 | 'd') : null; 16 | }} 17 | ], 18 | 19 | proxy: { 20 | api: { 21 | prefix: 'Server.events' 22 | } 23 | } 24 | }); 25 | -------------------------------------------------------------------------------- /client/app/model/Filter.js: -------------------------------------------------------------------------------- 1 | Ext.define('App.model.Filter', { 2 | extend: 'Ext.data.Model', 3 | 4 | idProperty: 'value', 5 | 6 | fields: [ 7 | { name: 'value', type: 'string' }, 8 | { name: 'label', type: 'string' }, 9 | { name: 'count', type: 'int' } 10 | ] 11 | }); 12 | -------------------------------------------------------------------------------- /client/app/model/Office.js: -------------------------------------------------------------------------------- 1 | Ext.define('App.model.Office', { 2 | extend: 'App.model.Base', 3 | 4 | fields: [ 5 | { name: 'name', type: 'string' }, 6 | { name: 'address', type: 'string' }, 7 | { name: 'postcode', type: 'string' }, 8 | { name: 'region', type: 'string' }, 9 | { name: 'city', type: 'string' }, 10 | { name: 'country', type: 'string' }, 11 | { name: 'headcount', type: 'int', persist: false }, 12 | { name: 'location', type: 'auto', defaultValue: { 13 | "latitude": 37.4256448, 14 | "longitude": -122.1703694 15 | }} 16 | ], 17 | 18 | proxy: { 19 | api: { 20 | prefix: 'Server.offices' 21 | } 22 | } 23 | }); 24 | -------------------------------------------------------------------------------- /client/app/model/Organization.js: -------------------------------------------------------------------------------- 1 | Ext.define('App.model.Organization', { 2 | extend: 'App.model.Base', 3 | 4 | fields: [ 5 | { name: 'name', type: 'string' }, 6 | { name: 'manager_id', reference: 'Person' }, 7 | { name: 'headcount', type: 'int', persist: false } 8 | ], 9 | 10 | proxy: { 11 | api: { 12 | prefix: 'Server.organizations' 13 | } 14 | } 15 | }); 16 | -------------------------------------------------------------------------------- /client/app/model/Session.js: -------------------------------------------------------------------------------- 1 | Ext.define('App.model.Session', { 2 | extend: 'Ext.data.Model', 3 | 4 | fields: [ 5 | { name: 'token', type: 'string' }, 6 | { name: 'expires', type: 'date' }, 7 | { name: 'user', reference: 'Person' } 8 | ], 9 | 10 | statics: { 11 | login: function(username, password) { 12 | return new Ext.Promise(function (resolve, reject) { 13 | Server.auth.login({ 14 | username: username, 15 | password: password 16 | }, function(result, response, success) { 17 | if (!success) { 18 | return reject(result.message); 19 | } 20 | 21 | var session = App.model.Session.loadData(result); 22 | if (!session.isValid()) { 23 | return reject({ errors: { 24 | username: 'Login failed: invalid session' 25 | }}); 26 | } 27 | 28 | resolve(session); 29 | }); 30 | }); 31 | } 32 | }, 33 | 34 | isValid: function() { 35 | return !Ext.isEmpty(this.get('token')) 36 | && this.get('expires') > new Date() 37 | && this.getUser() !== null; 38 | }, 39 | 40 | logout: function() { 41 | return new Ext.Promise(function (resolve, reject) { 42 | Server.auth.logout({}, resolve); 43 | }); 44 | } 45 | }); 46 | -------------------------------------------------------------------------------- /client/app/profile/Phone.js: -------------------------------------------------------------------------------- 1 | Ext.define('App.profile.Phone', { 2 | extend: 'Ext.app.Profile', 3 | 4 | views: { 5 | historybrowse: 'App.view.phone.history.Browse', 6 | main: 'App.view.phone.main.Main', 7 | officebrowse: 'App.view.phone.office.Browse', 8 | organizationbrowse: 'App.view.phone.organization.Browse', 9 | personbrowse: 'App.view.phone.person.Browse', 10 | personbrowsefilters: 'App.view.phone.person.BrowseFilters', 11 | personlistswiperitem: 'App.view.phone.person.ListSwiperItem' 12 | }, 13 | 14 | isActive: function () { 15 | return Ext.platformTags.phone; 16 | }, 17 | 18 | launch: function () { 19 | // Add a class to the body el to identify the phone profile so we can 20 | // override CSS styles easily. The framework adds x-phone so we could 21 | // use it but this way the app controls a class that is always present 22 | // when this profile isActive, regardless of the actual device type. 23 | Ext.getBody().addCls('phone-profile'); 24 | } 25 | }); 26 | -------------------------------------------------------------------------------- /client/app/profile/Phone.scss: -------------------------------------------------------------------------------- 1 | .phone-profile { 2 | .x-list.listing { 3 | border-top: 1px solid $neutral-light-color; 4 | 5 | .x-listitem-body-el { 6 | position: relative; 7 | flex: 1 1; 8 | } 9 | 10 | .x-listitem-body { 11 | .picture { 12 | flex: 0 0 auto; 13 | } 14 | 15 | .item-details { 16 | padding: 0 8px; 17 | } 18 | 19 | .item-title { 20 | @include ellipsis; 21 | font-weight: bold; 22 | } 23 | 24 | .item-caption { 25 | @include ellipsis; 26 | font-size: 12px; 27 | } 28 | 29 | .item-stats { 30 | font-size: 11px; 31 | text-align: right; 32 | } 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /client/app/profile/Tablet.js: -------------------------------------------------------------------------------- 1 | Ext.define('App.profile.Tablet', { 2 | extend: 'Ext.app.Profile', 3 | 4 | views: { 5 | historybrowse: 'App.view.tablet.history.Browse', 6 | historybrowsetoolbar: 'App.view.tablet.history.BrowseToolbar', 7 | main: 'App.view.tablet.main.Main', 8 | officebrowse: 'App.view.tablet.office.Browse', 9 | officebrowsetoolbar: 'App.view.tablet.office.BrowseToolbar', 10 | organizationbrowse: 'App.view.tablet.organization.Browse', 11 | organizationbrowsetoolbar: 'App.view.tablet.organization.BrowseToolbar', 12 | personbrowse: 'App.view.tablet.person.Browse', 13 | personbrowsetoolbar: 'App.view.tablet.person.BrowseToolbar' 14 | }, 15 | 16 | isActive: function () { 17 | return !Ext.platformTags.phone; 18 | }, 19 | 20 | launch: function () { 21 | // Add a class to the body el to identify the phone profile so we can 22 | // override CSS styles easily. The framework adds x-phone so we could 23 | // use it but this way the app controls a class that is always present 24 | // when this profile isActive, regardless of the actual device type. 25 | Ext.getBody().addCls('tablet-profile'); 26 | } 27 | }); 28 | -------------------------------------------------------------------------------- /client/app/store/Actions.js: -------------------------------------------------------------------------------- 1 | Ext.define('App.store.Actions', { 2 | extend: 'Ext.data.Store', 3 | alias: 'store.actions', 4 | 5 | model: 'App.model.Action', 6 | remoteFilter: true, 7 | remoteSort: true, 8 | 9 | sorters: { 10 | property: 'created', 11 | direction: 'DESC' 12 | } 13 | }); 14 | -------------------------------------------------------------------------------- /client/app/store/Events.js: -------------------------------------------------------------------------------- 1 | Ext.define('App.store.Events', { 2 | extend: 'Ext.data.Store', 3 | alias: 'store.events', 4 | 5 | model: 'App.model.Event', 6 | remoteFilter: true, 7 | remoteSort: true, 8 | autoLoad: false 9 | }); 10 | -------------------------------------------------------------------------------- /client/app/store/Filters.js: -------------------------------------------------------------------------------- 1 | Ext.define('App.store.Filters', { 2 | extend: 'Ext.data.Store', 3 | alias: 'store.filters', 4 | 5 | config: { 6 | service: null, 7 | field: null, 8 | label: null 9 | }, 10 | 11 | model: 'App.model.Filter', 12 | remoteFilter: false, 13 | remoteSort: false, 14 | pageSize: null, 15 | autoLoad: true, 16 | 17 | sorters: { 18 | property: 'label', 19 | direction: 'ASC' 20 | }, 21 | 22 | proxy: { 23 | type: 'direct', 24 | reader: { 25 | type: 'json', 26 | rootProperty: 'data', 27 | messageProperty: 'message' 28 | } 29 | }, 30 | 31 | updateService: function(service) { 32 | var proxy = this.getProxy(), 33 | api = proxy.getApi() || {}; 34 | api.read = 'Server.' + service + '.filters'; 35 | proxy.setApi(api); 36 | }, 37 | 38 | updateField: function(field) { 39 | var proxy = this.getProxy(), 40 | params = proxy.getExtraParams(); 41 | 42 | if (Ext.isEmpty(field)) { 43 | delete params.field; 44 | } else { 45 | params.field = field; 46 | } 47 | }, 48 | 49 | updateLabel: function(label) { 50 | var proxy = this.getProxy(), 51 | params = proxy.getExtraParams(); 52 | 53 | if (Ext.isEmpty(label)) { 54 | delete params.label; 55 | } else { 56 | params.label = label; 57 | } 58 | } 59 | }); 60 | -------------------------------------------------------------------------------- /client/app/store/Menu.js: -------------------------------------------------------------------------------- 1 | Ext.define('App.store.Menu', { 2 | extend: 'Ext.data.Store', 3 | alias: 'store.menu', 4 | 5 | data: [{ 6 | id: 'home', 7 | xtype: 'home', 8 | text: 'Home', 9 | icon: 'home' 10 | }, { 11 | id: 'people', 12 | xtype: 'personbrowse', 13 | text: 'Employees', 14 | icon: 'users' 15 | }, { 16 | id: 'organizations', 17 | xtype: 'organizationbrowse', 18 | text: 'Organizations', 19 | icon: 'sitemap' 20 | }, { 21 | id: 'offices', 22 | xtype: 'officebrowse', 23 | text: 'Offices', 24 | icon: 'globe' 25 | }, { 26 | id: 'history', 27 | xtype: 'historybrowse', 28 | text: 'Activity', 29 | icon: 'history' 30 | }] 31 | }); 32 | -------------------------------------------------------------------------------- /client/app/store/Offices.js: -------------------------------------------------------------------------------- 1 | Ext.define('App.store.Offices', { 2 | extend: 'Ext.data.Store', 3 | alias: 'store.offices', 4 | 5 | model: 'App.model.Office', 6 | remoteFilter: true, 7 | remoteSort: true, 8 | sorters: 'name' 9 | }); 10 | -------------------------------------------------------------------------------- /client/app/store/Organizations.js: -------------------------------------------------------------------------------- 1 | Ext.define('App.store.Organizations', { 2 | extend: 'Ext.data.Store', 3 | alias: 'store.organizations', 4 | 5 | model: 'App.model.Organization', 6 | remoteFilter: true, 7 | remoteSort: true, 8 | sorters: 'name' 9 | }); 10 | -------------------------------------------------------------------------------- /client/app/store/People.js: -------------------------------------------------------------------------------- 1 | Ext.define('App.store.People', { 2 | extend: 'Ext.data.Store', 3 | alias: 'store.people', 4 | 5 | model: 'App.model.Person', 6 | remoteFilter: true, 7 | remoteSort: true, 8 | sorters: 'lastname' 9 | }); 10 | -------------------------------------------------------------------------------- /client/app/util/Errors.js: -------------------------------------------------------------------------------- 1 | Ext.define('App.util.Errors', { 2 | requires: ['Ext.Toast'], 3 | 4 | statics: { 5 | toForm: function(errors) { 6 | var values = {}; 7 | 8 | if (Ext.isObject(errors)) { 9 | errors = errors.errors; 10 | }; 11 | 12 | if (Ext.isArray(errors)) { 13 | errors.forEach(function(error) { 14 | var name = error.id || error.field || error.path; 15 | var value = error.msg || error.message; 16 | if (name && value) { 17 | values[name] = value; 18 | } 19 | }); 20 | } else { 21 | values = errors; 22 | } 23 | 24 | return values; 25 | }, 26 | 27 | process: function(error, form) { 28 | if (!error) { 29 | return false; 30 | } 31 | 32 | if (Ext.isFunction(error.hasException)) { 33 | // The given error is an Ext.data.operation.Operation 34 | if (!error.hasException()) { 35 | return false; 36 | } 37 | 38 | error = error.getError() || 'An unknown error has occurred'; 39 | } 40 | 41 | if (Ext.isObject(error)) { 42 | if (error.code === -32096) { // READONLY_SESSION 43 | // The session is read-only (demo version), let's display a temporary message 44 | // and return false since this exception should not be considered as an error. 45 | Ext.toast(error.message, 2000); 46 | return false; 47 | } 48 | if (error.code === -32001 && form) { 49 | form.setErrors(this.toForm(error)); 50 | } else { 51 | Ext.Msg.alert(error.name + ' Error', error.message); 52 | } 53 | } else if (Ext.isString(error)) { 54 | Ext.Msg.alert('Error', error); 55 | } 56 | 57 | return true; 58 | } 59 | } 60 | }); 61 | -------------------------------------------------------------------------------- /client/app/util/State.js: -------------------------------------------------------------------------------- 1 | Ext.define('App.util.State', { 2 | 3 | singleton: true, 4 | 5 | requires: [ 6 | 'Ext.util.LocalStorage' 7 | ], 8 | 9 | store: new Ext.util.LocalStorage({ 10 | id: 'app-state' 11 | }), 12 | 13 | get: function(key, defaultValue) { 14 | var value = this.store.getItem(key); 15 | return value === undefined? defaultValue : Ext.decode(value); 16 | }, 17 | 18 | set: function(key, value) { 19 | if (value == null) { // !== undefined && !== null 20 | this.store.removeItem(key); 21 | } else { 22 | this.store.setItem(key, Ext.encode(value)); 23 | } 24 | }, 25 | 26 | clear: function(key) { 27 | this.set(key, null); 28 | } 29 | }); 30 | -------------------------------------------------------------------------------- /client/app/view/auth/Login.js: -------------------------------------------------------------------------------- 1 | Ext.define('App.view.auth.Login', { 2 | extend: 'Ext.Container', 3 | xtype: 'authlogin', 4 | 5 | controller: 'authlogin', 6 | 7 | cls: 'auth-login', 8 | 9 | layout: { 10 | type: 'vbox', 11 | align: 'center', 12 | pack: 'center' 13 | }, 14 | 15 | items: [{ 16 | cls: 'auth-header', 17 | html: 18 | ''+ 19 | '
Coworkee
'+ 20 | '
Employee directory
' 21 | }, { 22 | xtype: 'formpanel', 23 | reference: 'form', 24 | layout: 'vbox', 25 | ui: 'auth', 26 | 27 | items: [{ 28 | xtype: 'textfield', 29 | name: 'username', 30 | placeholder: 'Username', 31 | required: true 32 | }, { 33 | xtype: 'passwordfield', 34 | name: 'password', 35 | placeholder: 'Password', 36 | required: true 37 | }, { 38 | xtype: 'button', 39 | text: 'LOG IN', 40 | iconAlign: 'right', 41 | iconCls: 'x-fa fa-angle-right', 42 | handler: 'onLoginTap', 43 | ui: 'action' 44 | }] 45 | }, { 46 | cls: 'auth-footer', 47 | html: 48 | '
Ext JS example
'+ 49 | ''+ 50 | ''+ 51 | 'Sencha'+ 52 | '' 53 | }] 54 | }); 55 | -------------------------------------------------------------------------------- /client/app/view/auth/Login.scss: -------------------------------------------------------------------------------- 1 | // Note: image from https://pixabay.com (Creative Commons CC0) 2 | $auth-color: white; 3 | $auth-background-color: #123d40; 4 | $auth-background-image: url(get-resource-path('images/auth-background.jpg')); 5 | 6 | @include panel-ui( 7 | $ui: 'auth', 8 | $background-color: transparent, 9 | $body-background-color: transparentize($auth-background-color, 0.75), 10 | $body-color: $auth-color, 11 | $body-padding: 24px 12 | ); 13 | 14 | .auth-login { 15 | @include background-size(cover); 16 | background-image: $auth-background-image; 17 | background-position: center; 18 | 19 | > .x-body-el { 20 | background-color: rgba(black, 0.25); 21 | } 22 | 23 | .x-formpanel { 24 | width: 256px; 25 | 26 | .x-button { 27 | // Balance button vertical spacing with form fields 28 | margin: $field-vertical-spacing/2 0; 29 | 30 | .x-big { 31 | margin: $field-vertical-spacing-big/2 0; 32 | } 33 | } 34 | } 35 | 36 | .auth-header, 37 | .auth-footer { 38 | color: $auth-color; 39 | text-align: center; 40 | padding: 16px; 41 | 42 | a { 43 | color: $auth-color; 44 | text-decoration: none; 45 | } 46 | } 47 | 48 | .auth-header { 49 | .logo { 50 | @include single-text-shadow; 51 | color: $auth-color; 52 | font-size: 40px; 53 | line-height: 1; 54 | } 55 | 56 | .title, .caption { 57 | white-space: nowrap; 58 | } 59 | 60 | .title { 61 | font-size: 31px; 62 | font-weight: bold; 63 | } 64 | 65 | .caption { 66 | font-size: 15px; 67 | text-transform: uppercase; 68 | } 69 | } 70 | 71 | .auth-footer { 72 | .logo { 73 | font-size: 18px; 74 | margin-right: 2px; 75 | margin-left: -4px; 76 | 77 | &::before { 78 | vertical-align: middle; 79 | } 80 | } 81 | 82 | .label { 83 | font-weight: bold; 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /client/app/view/auth/LoginController.js: -------------------------------------------------------------------------------- 1 | Ext.define('App.view.auth.LoginController', { 2 | extend: 'Ext.app.ViewController', 3 | alias: 'controller.authlogin', 4 | 5 | init: function() { 6 | this.callParent(arguments); 7 | this.lookup('form').setValues({ 8 | username: 'norma.flores', 9 | password: 'wvyrEDvxI' 10 | }); 11 | }, 12 | 13 | onLoginTap: function() { 14 | var me = this, 15 | form = me.lookup('form'), 16 | values = form.getValues(); 17 | 18 | form.clearErrors(); 19 | 20 | Ext.Viewport.setMasked({ xtype: 'loadmask' }); 21 | 22 | App.model.Session.login(values.username, values.password) 23 | .then(function(session) { 24 | me.fireEvent('login', session); 25 | }) 26 | .catch(function(errors) { 27 | form.setErrors(App.util.Errors.toForm(errors)); 28 | }) 29 | .then(function(session) { 30 | Ext.Viewport.setMasked(false); 31 | }); 32 | } 33 | }); 34 | -------------------------------------------------------------------------------- /client/app/view/history/Browse.js: -------------------------------------------------------------------------------- 1 | Ext.define('App.view.history.Browse', { 2 | extend: 'App.view.widgets.Browse', 3 | 4 | fields: { 5 | office: { 6 | property: 'recipient.office_id' 7 | }, 8 | organization: { 9 | property: 'recipient.organization_id' 10 | }, 11 | recipient: { 12 | property: 'recipient_id' 13 | }, 14 | type: { 15 | property: 'type' 16 | } 17 | }, 18 | 19 | controller: 'historybrowse', 20 | viewModel: { 21 | type: 'historybrowse' 22 | }, 23 | 24 | cls: 'historybrowse', 25 | bind: { 26 | store: '{history}' 27 | } 28 | }); 29 | -------------------------------------------------------------------------------- /client/app/view/history/Browse.scss: -------------------------------------------------------------------------------- 1 | .historybrowse { 2 | .x-list { 3 | .date, 4 | .time { 5 | text-align: right; 6 | } 7 | 8 | .time { 9 | font-weight: bold; 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /client/app/view/history/BrowseController.js: -------------------------------------------------------------------------------- 1 | Ext.define('App.view.history.BrowseController', { 2 | extend: 'App.view.widgets.BrowseController', 3 | alias: 'controller.historybrowse', 4 | 5 | control: { 6 | '#': { 7 | reset: 'refresh' 8 | } 9 | }, 10 | 11 | refresh: function() { 12 | var vm = this.getViewModel(); 13 | vm.getStore('offices').reload(); 14 | vm.getStore('organizations').reload(); 15 | vm.getStore('recipients').reload(); 16 | vm.getStore('types').reload(); 17 | }, 18 | 19 | onDeleteAction: function(list, data) { 20 | var store = this.getViewModel().getStore('history'); 21 | store.remove(data.record); 22 | store.save(); 23 | } 24 | }); 25 | -------------------------------------------------------------------------------- /client/app/view/history/BrowseModel.js: -------------------------------------------------------------------------------- 1 | Ext.define('App.view.history.BrowserModel', { 2 | extend: 'Ext.app.ViewModel', 3 | alias: 'viewmodel.historybrowse', 4 | 5 | stores: { 6 | history: { 7 | type: 'actions', 8 | grouper: { 9 | groupFn: function(record) { 10 | var date = Ext.Date.clearTime(new Date(record.get('created'))), 11 | today = Ext.Date.clearTime(new Date()); 12 | 13 | if (Ext.Date.isEqual(date, today)) { 14 | return 'Today'; 15 | } else if (Ext.Date.isEqual(date, Ext.Date.subtract(today, Ext.Date.DAY, 1))) { 16 | return 'Yesterday' 17 | } else { 18 | return Ext.Date.format(date, 'D, F jS, Y'); 19 | } 20 | } 21 | } 22 | }, 23 | recipients: { 24 | type: 'filters', 25 | service: 'actions', 26 | field: 'recipient_id', 27 | label: [ 28 | 'recipient.firstname', 29 | 'recipient.lastname' 30 | ] 31 | }, 32 | offices: { 33 | type: 'filters', 34 | service: 'actions', 35 | field: 'recipient.office_id', 36 | label: 'recipient.office.name' 37 | }, 38 | organizations: { 39 | type: 'filters', 40 | service: 'actions', 41 | field: 'recipient.organization_id', 42 | label: 'recipient.organization.name' 43 | }, 44 | types: { 45 | type: 'filters', 46 | service: 'actions', 47 | field: 'type' 48 | } 49 | } 50 | }); 51 | -------------------------------------------------------------------------------- /client/app/view/home/Home.js: -------------------------------------------------------------------------------- 1 | Ext.define('App.view.home.Home', { 2 | extend: 'Ext.Panel', 3 | xtype: 'home', 4 | 5 | controller: 'home', 6 | viewModel: { 7 | type: 'home' 8 | }, 9 | 10 | config: { 11 | route: null 12 | }, 13 | 14 | eventedConfig: { 15 | /** 16 | * Make the config trigger an event on change to allow the controller to monitor it. 17 | * https://www.sencha.com/blog/using-sencha-ext-config/ 18 | */ 19 | route: null 20 | }, 21 | 22 | platformConfig: { 23 | '!phone': { 24 | header: { 25 | hidden: true 26 | } 27 | } 28 | }, 29 | 30 | cls: 'home', 31 | scrollable: 'y', 32 | 33 | items: [{ 34 | xtype : 'homeheader' 35 | }, { 36 | xtype: 'container', 37 | userCls: [ 38 | 'page-constrained', 39 | 'blocks' 40 | ], 41 | 42 | items: [{ 43 | xtype: 'container', 44 | userCls: 'blocks-column', 45 | items: [{ 46 | xtype: 'homeevents', 47 | ui: 'block flat' 48 | }] 49 | }, { 50 | xtype: 'container', 51 | userCls: 'blocks-column', 52 | items: [{ 53 | xtype: 'historypanel', 54 | bind: '{history}', 55 | ui: 'block flat', 56 | header: { 57 | items: { 58 | showall: { 59 | ui: 'block flat' 60 | } 61 | } 62 | }, 63 | listeners: { 64 | childtap: 'onHistoryChildTap' 65 | } 66 | }] 67 | }] 68 | }], 69 | 70 | reset: function() { 71 | this.fireEvent('reset'); 72 | return this; 73 | } 74 | }); 75 | 76 | -------------------------------------------------------------------------------- /client/app/view/home/HomeController.js: -------------------------------------------------------------------------------- 1 | Ext.define('App.view.home.HomeController', { 2 | extend: 'Ext.app.ViewController', 3 | alias: 'controller.home', 4 | 5 | control: { 6 | '#': { 7 | routechange: 'onRouteChange', 8 | reset: 'refresh' 9 | } 10 | }, 11 | 12 | init: function() { 13 | this.callParent(arguments); 14 | this.update(); 15 | }, 16 | 17 | initViewModel: function(vm) { 18 | vm.bind('{range}', this.onRangeChange, this); 19 | }, 20 | 21 | update: function() { 22 | var me = this, 23 | vm = me.getViewModel(), 24 | now = new Date(), 25 | hours = now.getHours(); 26 | 27 | vm.set({ 28 | time: now, 29 | greeting: 30 | Ext.Date.isWeekend(now)? "Enjoy your weekend" : 31 | hours < 13? "Good morning" : 32 | hours < 17? "Good afternoon" : 33 | "Good evening" 34 | }); 35 | 36 | Ext.defer(function() { 37 | // The view might have been destroyed (e.g. user deauthentication) 38 | if (!me.destroyed) { 39 | me.update(); 40 | } 41 | }, (60 - now.getSeconds()) * 1000); 42 | }, 43 | 44 | refresh: function() { 45 | var vm = this.getViewModel(); 46 | vm.getStore('history').load(); 47 | vm.getStore('events').load(); 48 | }, 49 | 50 | onRangeChange: function(range) { 51 | var D = Ext.Date, 52 | store = this.getViewModel().getStore('events'), 53 | today = D.clearTime(new Date()), 54 | direction = 'DESC', 55 | filters = []; 56 | 57 | switch (range) { 58 | case 'upcoming': 59 | direction = 'ASC'; 60 | filters.push({ 61 | property: 'startDate', 62 | value: D.add(today, D.DAY, 1) 63 | }); 64 | break; 65 | case 'past': 66 | filters.push({ 67 | property: 'endDate', 68 | value: D.add(today, D.DAY, -7) 69 | }); 70 | break; 71 | case 'recent': 72 | default: 73 | filters.push({ 74 | property: 'startDate', 75 | value: D.add(today, D.DAY, -7) 76 | }, { 77 | property: 'endDate', 78 | value: D.add(today, D.DAY, 1) 79 | }); 80 | break; 81 | } 82 | 83 | store.clearFilter(true); 84 | store.filter(filters, false, false); 85 | store.sort('date', direction); 86 | }, 87 | 88 | onRouteChange: function(view, route) { 89 | var matches = (route || '').match(/(recent|upcoming|past)/g); 90 | if (matches) { 91 | this.getViewModel().set('range', matches[0]); 92 | } 93 | }, 94 | 95 | onEventChildTap: function(view, location) { 96 | var record = location.record; 97 | if (record) { 98 | this.redirectTo(record.getPerson()); 99 | } 100 | }, 101 | 102 | onHistoryChildTap: function(view, location) { 103 | var record = location.record; 104 | if (record) { 105 | this.redirectTo(record.getRecipient()); 106 | } 107 | }, 108 | 109 | onHistoryAllTap: function() { 110 | this.redirectTo('history'); 111 | } 112 | }); 113 | -------------------------------------------------------------------------------- /client/app/view/home/HomeEvents.js: -------------------------------------------------------------------------------- 1 | Ext.define('App.view.home.HomeEvents', { 2 | extend: 'Ext.Panel', 3 | xtype: 'homeevents', 4 | 5 | cls: 'home-events', 6 | 7 | tbar: { 8 | layout: { 9 | pack: 'center' 10 | }, 11 | 12 | items: [{ 13 | xtype: 'segmentedbutton', 14 | defaultUI: 'segmented flat', 15 | bind: '{range}', 16 | items: [{ 17 | text: 'Upcoming', 18 | value: 'upcoming' 19 | }, { 20 | text: 'Recent', 21 | value: 'recent' 22 | }, { 23 | text: 'Past', 24 | value: 'past' 25 | }] 26 | }] 27 | }, 28 | 29 | items: [{ 30 | xtype: 'dataview', 31 | bind: '{events}', 32 | minHeight: 80, 33 | inline: true, 34 | ui: 'light', 35 | 36 | selectable: { 37 | disabled: true 38 | }, 39 | 40 | itemTpl: [ 41 | '
', 42 | '
{date:date("M j")}
', 43 | '
', 44 | '', 45 | 'Birthday', 46 | 'Anniversary', 47 | 'Arrival', 48 | 'Departure', 49 | '', 50 | '
', 51 | '
', 52 | '
', 53 | '
', 54 | '
', 55 | '
{person.fullname}
', 56 | '
{person.title}
', 57 | '
', 58 | '', 59 | '{person.birthday:dateDiff(values.date, "y")} old', 60 | '{person.started:dateDiff(values.date, "y")} ', 61 | ' ', 62 | '', 63 | '
', 64 | '
', 65 | '
' 66 | ], 67 | 68 | listeners: { 69 | childtap: 'onEventChildTap' 70 | } 71 | }] 72 | }); 73 | -------------------------------------------------------------------------------- /client/app/view/home/HomeEvents.scss: -------------------------------------------------------------------------------- 1 | .home-events { 2 | .x-dataview-item { 3 | cursor: pointer; 4 | margin: 0 0 $blocks-spacing 0; 5 | width: calc(50% - #{$blocks-spacing/2}); 6 | 7 | &:nth-child(odd) { 8 | margin-right: $blocks-spacing; 9 | } 10 | 11 | .phone-profile & { 12 | @media screen and (max-width: 400px) { 13 | margin-right: 0; 14 | width: 100%; 15 | } 16 | } 17 | 18 | .tablet-profile & { 19 | @media screen and (max-width: 400px), (min-width: 600px) and (max-width: 800px) { 20 | margin-right: 0; 21 | width: 100%; 22 | } 23 | } 24 | } 25 | 26 | .event-header { 27 | background-color: $neutral-highlight-color; 28 | color: $neutral-dark-color; 29 | display: inline-block; 30 | font-size: 12px; 31 | padding: 6px 16px; 32 | 33 | .date, .title { 34 | display: inline-block; 35 | } 36 | 37 | .date { 38 | font-weight: bold; 39 | } 40 | 41 | .title { 42 | &:before { 43 | content: '|'; 44 | margin: 0 4px; 45 | } 46 | } 47 | 48 | &.type-birthday { 49 | background-color: $base-light-color; 50 | color: $base-dark-color; 51 | } 52 | 53 | &.type-ended { 54 | background-color: $neutral-light-color; 55 | border: 1px solid $neutral-color; 56 | color: $neutral-dark-color; 57 | } 58 | } 59 | 60 | .event-content { 61 | display: flex; 62 | flex-direction: row; 63 | align-items: center; 64 | padding: 10px; 65 | 66 | > .picture { 67 | flex: 0 0 auto; 68 | margin-right: 12px; 69 | } 70 | 71 | > .details { 72 | display: flex; 73 | flex-direction: column; 74 | align-items: stretch; 75 | overflow: hidden; 76 | line-height: 1.4; 77 | 78 | > * { 79 | @include ellipsis; 80 | } 81 | } 82 | 83 | .person-name { 84 | font-size: 16px; 85 | font-weight: bold; 86 | } 87 | 88 | .person-title, 89 | .person-years { 90 | font-size: 13px; 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /client/app/view/home/HomeHeader.js: -------------------------------------------------------------------------------- 1 | Ext.define('App.view.home.HomeHeader', { 2 | extend: 'Ext.Container', 3 | xtype: 'homeheader', 4 | 5 | cls: 'home-header', 6 | layout: 'vbox', 7 | 8 | items: [{ 9 | xtype: 'component', 10 | cls: 'header-message', 11 | tpl: [ 12 | '
', 13 | '
{greeting}
', 14 | '
{firstname}
', 15 | '
' 16 | ], 17 | bind: { 18 | data: { 19 | firstname: '{user.firstname}', 20 | greeting: '{greeting}' 21 | } 22 | } 23 | }, { 24 | xtype: 'container', 25 | layout: 'hbox', 26 | userCls: [ 27 | 'page-constrained', 28 | 'header-info' 29 | ], 30 | items: [{ 31 | xtype: 'component', 32 | cls: 'header-links', 33 | flex: 1, 34 | tpl: [ 35 | '', 36 | '
', 37 | '
', 38 | '
Manager
', 39 | '{fullname}', 40 | '
', 41 | '
', 42 | '', 43 | '
', 44 | '', 45 | '
Organization
', 46 | '{name}', 47 | '
', 48 | '
', 49 | '', 50 | '
', 51 | '', 52 | '
Office
', 53 | '{name}', 54 | '
', 55 | '
' 56 | ], 57 | bind: { 58 | record: '{user}' 59 | } 60 | }, { 61 | xtype: 'component', 62 | cls: 'header-clock', 63 | tpl: [ 64 | '
{time:date("l, F d")}
', 65 | '
{time:date("G:ia")}
' 66 | ], 67 | bind: { 68 | data: { 69 | time: '{time}' 70 | } 71 | } 72 | }] 73 | }] 74 | }); 75 | -------------------------------------------------------------------------------- /client/app/view/home/HomeHeader.scss: -------------------------------------------------------------------------------- 1 | .home-header { 2 | .header-message { 3 | color: $base-color; 4 | font-size: 32px; 5 | padding: 8vh 16px 6vh 16px; 6 | text-align: center; 7 | 8 | .greeting, .person-name { 9 | display: inline-block; 10 | margin: 5px; 11 | } 12 | 13 | .greeting { 14 | white-space: nowrap; 15 | } 16 | 17 | .person-name { 18 | @include border-radius; 19 | border: 3px solid; 20 | font-weight: 600; 21 | padding: 4px 16px 8px; 22 | } 23 | 24 | .x-big & { 25 | font-size: 22px; 26 | 27 | .person-name { 28 | border-width: 2px; 29 | } 30 | } 31 | } 32 | 33 | .header-info { 34 | line-height: 1.65; 35 | padding: 0 16px; 36 | 37 | &::before { 38 | @include single-box-shadow; 39 | position: absolute; 40 | display: block; 41 | height: 32px; 42 | content: ''; 43 | left: 0; 44 | right: 0; 45 | top: 0; 46 | } 47 | } 48 | 49 | .header-links { 50 | > .x-innerhtml { 51 | display: flex; 52 | } 53 | 54 | .item { 55 | font-size: 14px; 56 | padding: 8px; 57 | overflow: hidden; 58 | text-align: center; 59 | } 60 | 61 | .icon, .picture { 62 | border: 4px solid white; 63 | display: block; 64 | margin: 0 auto 4px auto; 65 | height: 54px; 66 | width: 54px 67 | } 68 | 69 | .icon { 70 | @include border-radius(50%); 71 | background-color: $base-color; 72 | color: contrasted($base-light-color, -25%); 73 | 74 | &::before { 75 | font-size: 28px; 76 | } 77 | } 78 | 79 | .title, .link { 80 | @include ellipsis; 81 | } 82 | 83 | .title { 84 | color: $neutral-medium-dark-color; 85 | font-size: 12px; 86 | text-transform: uppercase; 87 | } 88 | 89 | .link { 90 | display: block; 91 | font-size: 13px; 92 | font-weight: normal; 93 | } 94 | } 95 | 96 | .header-clock { 97 | align-items: flex-end; 98 | display: flex; 99 | font-weight: normal; 100 | padding-bottom: 16px; 101 | text-align: right; 102 | 103 | .date { 104 | color: $base-color; 105 | } 106 | 107 | .time { 108 | background-color: $base-color; 109 | color: white; 110 | font-size: 12px; 111 | padding: 2px 4px; 112 | text-align: center; 113 | } 114 | } 115 | 116 | @media screen and (max-width: 600px) { 117 | .header-clock { 118 | display: none; 119 | } 120 | 121 | .header-links .item { 122 | padding: 8px; 123 | flex: 1; 124 | width: 0; 125 | } 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /client/app/view/home/HomeModel.js: -------------------------------------------------------------------------------- 1 | Ext.define('App.view.home.HomeModel', { 2 | extend: 'Ext.app.ViewModel', 3 | alias: 'viewmodel.home', 4 | 5 | data: { 6 | greeting: null, 7 | range: 'upcoming', 8 | time: null 9 | }, 10 | 11 | stores: { 12 | history: { 13 | type: 'actions', 14 | autoLoad: true, 15 | pageSize: 8 16 | }, 17 | events: { 18 | type: 'events', 19 | autoLoad: false, // loaded from HomeController 20 | pageSize: 8 21 | } 22 | } 23 | }); 24 | -------------------------------------------------------------------------------- /client/app/view/main/MainModel.js: -------------------------------------------------------------------------------- 1 | Ext.define('App.view.main.MainModel', { 2 | extend: 'Ext.app.ViewModel', 3 | alias: 'viewmodel.main', 4 | 5 | data: { 6 | user: null 7 | } 8 | }); 9 | -------------------------------------------------------------------------------- /client/app/view/main/Menu.js: -------------------------------------------------------------------------------- 1 | Ext.define('App.view.main.Menu', { 2 | extend: 'App.view.widgets.Sidebar', 3 | xtype: 'mainmenu', 4 | 5 | config: { 6 | selection: null 7 | }, 8 | 9 | controller: 'mainmenu', 10 | 11 | cls: 'main-menu', 12 | layout: 'vbox', 13 | weighted: true, 14 | 15 | items: { 16 | trigger: { 17 | xtype: 'button', 18 | handler: 'onTriggerTap', 19 | iconCls: 'x-fa fa-bars', 20 | ui: 'large flat dark', 21 | docked: 'top' 22 | }, 23 | navigator: { 24 | xtype: 'dataview', 25 | scrollable: 'y', 26 | store: 'Menu', 27 | weight: 0, 28 | flex: 1, 29 | ui: 'dark large', 30 | selectable: { 31 | deselectable: false 32 | }, 33 | itemTpl: [ 34 | '', 35 | '{text}' 36 | ], 37 | listeners: { 38 | childtap: 'onMenuChildTap' 39 | } 40 | }, 41 | profile: { 42 | xtype: 'button', 43 | handler: 'onProfileTap', 44 | ui: 'large flat dark picture', 45 | iconCls: 'picture', 46 | textAlign: 'left', 47 | weight: 10, 48 | bind: { 49 | icon: '{user.picture}', 50 | text: '
{user.firstname}
'+ 51 | '
{user.username}
' 52 | } 53 | }, 54 | logout: { 55 | xtype: 'button', 56 | handler: 'onLogoutTap', 57 | iconCls: 'x-fa fa-power-off', 58 | text: 'Log out', 59 | textAlign: 'left', 60 | ui: 'large flat dark', 61 | weight: 20 62 | } 63 | }, 64 | 65 | updateSelection: function(value) { 66 | this.child('#navigator').setSelection(value); 67 | } 68 | }); 69 | -------------------------------------------------------------------------------- /client/app/view/main/Menu.scss: -------------------------------------------------------------------------------- 1 | .main-menu { 2 | .x-dataview-item { 3 | cursor: pointer; 4 | 5 | .icon { 6 | display: inline-block; 7 | font-size: $sidebar-icon-size; 8 | line-height: 1; 9 | margin-right: $sidebar-icon-horizontal-spacing; 10 | height: $sidebar-icon-size; 11 | width: $sidebar-icon-size; 12 | text-align: center; 13 | vertical-align: middle; 14 | 15 | .x-big & { 16 | font-size: $sidebar-icon-size-big; 17 | margin-right: $sidebar-icon-horizontal-spacing-big; 18 | height: $sidebar-icon-size-big; 19 | width: $sidebar-icon-size-big; 20 | } 21 | } 22 | } 23 | 24 | .x-dataview-dark { 25 | border-bottom: 2px solid contrasted($neutral-dark-color, 5%); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /client/app/view/main/MenuController.js: -------------------------------------------------------------------------------- 1 | Ext.define('App.view.main.MenuController', { 2 | extend: 'Ext.app.ViewController', 3 | alias: 'controller.mainmenu', 4 | 5 | collapse: function() { 6 | this.getView().setExpanded(false); 7 | }, 8 | 9 | onTriggerTap: function() { 10 | var view = this.getView(); 11 | view.setExpanded(!view.getExpanded()); 12 | }, 13 | 14 | onMenuChildTap: function(menu, location) { 15 | var record = location.record; 16 | if (record) { 17 | this.redirectTo(record.getId()); 18 | this.collapse(); 19 | } 20 | }, 21 | 22 | onProfileTap: function() { 23 | this.redirectTo(this.getViewModel().get('user')); 24 | this.collapse(); 25 | }, 26 | 27 | onLogoutTap: function() { 28 | if (this.fireEvent('logout')) { 29 | this.collapse(); 30 | } 31 | } 32 | }); 33 | -------------------------------------------------------------------------------- /client/app/view/office/Browse.js: -------------------------------------------------------------------------------- 1 | Ext.define('App.view.office.Browse', { 2 | extend: 'App.view.widgets.Browse', 3 | 4 | fields: { 5 | country: { 6 | property: 'country' 7 | } 8 | }, 9 | 10 | controller: 'officebrowse', 11 | viewModel: { 12 | type: 'officebrowse' 13 | }, 14 | 15 | cls: 'officebrowse', 16 | bind: { 17 | store: '{offices}' 18 | } 19 | }); 20 | -------------------------------------------------------------------------------- /client/app/view/office/BrowseController.js: -------------------------------------------------------------------------------- 1 | Ext.define('App.view.office.BrowseController', { 2 | extend: 'App.view.widgets.BrowseController', 3 | alias: 'controller.officebrowse', 4 | 5 | control: { 6 | '#': { 7 | reset: 'refresh' 8 | } 9 | }, 10 | 11 | refresh: function() { 12 | var vm = this.getViewModel(); 13 | vm.getStore('countries').reload(); 14 | }, 15 | 16 | onCreate: function() { 17 | this.redirectTo('office/create'); 18 | } 19 | }); 20 | -------------------------------------------------------------------------------- /client/app/view/office/BrowseModel.js: -------------------------------------------------------------------------------- 1 | Ext.define('App.view.office.BrowseModel', { 2 | extend: 'Ext.app.ViewModel', 3 | alias: 'viewmodel.officebrowse', 4 | 5 | stores: { 6 | offices: { 7 | type: 'offices', 8 | grouper: { 9 | groupFn: function(record) { 10 | return record.get('name')[0]; 11 | } 12 | } 13 | }, 14 | countries: { 15 | type: 'filters', 16 | service: 'offices', 17 | field: 'country', 18 | label: 'country' 19 | } 20 | } 21 | }); 22 | -------------------------------------------------------------------------------- /client/app/view/office/Show.js: -------------------------------------------------------------------------------- 1 | Ext.define('App.view.office.Show', { 2 | extend: 'App.view.widgets.Show', 3 | xtype: 'officeshow', 4 | 5 | controller: 'officeshow', 6 | viewModel: { 7 | type: 'officeshow' 8 | }, 9 | 10 | title: 'Office', 11 | 12 | items: { 13 | header: { 14 | items: { 15 | title: { 16 | tpl: [ 17 | '
', 18 | '
{name}
', 19 | '
{city}, {country}
' 20 | ] 21 | } 22 | } 23 | }, 24 | 25 | map: { 26 | xtype: 'mapview', 27 | userCls: 'office-map', 28 | weight: -5, 29 | bind: { 30 | markers: '{markers}' 31 | } 32 | }, 33 | 34 | content: { 35 | items: { 36 | left: { 37 | items: { 38 | details: { 39 | xtype: 'officeshowdetails' 40 | }, 41 | 42 | people: { 43 | xtype: 'officeshowpeople' 44 | } 45 | } 46 | } 47 | } 48 | } 49 | } 50 | }); 51 | -------------------------------------------------------------------------------- /client/app/view/office/Show.scss: -------------------------------------------------------------------------------- 1 | .office-map { 2 | height: 32vh; 3 | max-height: 192px; 4 | min-height: 96px; 5 | } 6 | -------------------------------------------------------------------------------- /client/app/view/office/ShowController.js: -------------------------------------------------------------------------------- 1 | Ext.define('App.view.office.ShowController', { 2 | extend: 'App.view.widgets.ShowController', 3 | alias: 'controller.officeshow', 4 | 5 | onRecordChange: function(view, record) { 6 | var vm = this.getViewModel(), 7 | people = vm.getStore('people'), 8 | history = vm.getStore('history'), 9 | markers = vm.getStore('markers'); 10 | 11 | if (record) { 12 | people.filter('office_id', record.get('id')); 13 | history.filter('recipient.office_id', record.get('id')); 14 | markers.loadRecords(record); 15 | } else { 16 | people.removeAll(); 17 | history.removeAll(); 18 | markers.removeAll(); 19 | } 20 | 21 | this.callParent(arguments); 22 | }, 23 | 24 | onPeopleHeadcountTap: function() { 25 | this.redirectTo('people/office/' + this.getRecord().getId()) 26 | }, 27 | 28 | onHistoryAllTap: function() { 29 | this.redirectTo('history/office/' + this.getRecord().getId()); 30 | } 31 | }); 32 | -------------------------------------------------------------------------------- /client/app/view/office/ShowDetails.js: -------------------------------------------------------------------------------- 1 | Ext.define('App.view.office.ShowDetails', { 2 | extend: 'Ext.Panel', 3 | xtype: 'officeshowdetails', 4 | 5 | cls: 'office-details', 6 | title: 'Details', 7 | 8 | tpl: [ 9 | '
', 10 | '
', 11 | '
Address
', 12 | '
', 13 | '
{address}
', 14 | '', 15 | '
{city}, {region} {postcode}
', 16 | '
{country}
', 17 | '', 18 | '
{city}, {country}
', 19 | '
', 20 | '
', 21 | '
', 22 | '
' 23 | ], 24 | 25 | bind: { 26 | record: '{record}' 27 | } 28 | }); 29 | -------------------------------------------------------------------------------- /client/app/view/office/ShowDetails.scss: -------------------------------------------------------------------------------- 1 | .office-details { 2 | .block-section { 3 | min-width: 20%; 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /client/app/view/office/ShowModel.js: -------------------------------------------------------------------------------- 1 | Ext.define('App.view.office.ShowModel', { 2 | extend: 'Ext.app.ViewModel', 3 | alias: 'viewmodel.officeshow', 4 | 5 | stores: { 6 | markers: {}, 7 | 8 | people: { 9 | type: 'people', 10 | pageSize: 12 11 | }, 12 | 13 | history: { 14 | type: 'actions', 15 | pageSize: 12 16 | } 17 | } 18 | }); 19 | -------------------------------------------------------------------------------- /client/app/view/office/ShowPeople.js: -------------------------------------------------------------------------------- 1 | Ext.define('App.view.office.ShowPeople', { 2 | extend: 'Ext.Panel', 3 | xtype: 'officeshowpeople', 4 | 5 | cls: 'office-people', 6 | iconCls: 'x-fa fa-users', 7 | title: 'Employees', 8 | 9 | header: { 10 | items: [{ 11 | xtype: 'button', 12 | handler: 'onPeopleHeadcountTap', 13 | iconCls: 'x-fa fa-users', 14 | ui: 'block', 15 | weigth: 10, 16 | bind: { 17 | text: '{record.headcount}', 18 | tooltip: 'Show employees of the {record.name} office.' 19 | } 20 | }] 21 | }, 22 | 23 | items: [{ 24 | xtype: 'dataview', 25 | bind: '{people}', 26 | ui: 'thumbnails', 27 | minHeight: 80, 28 | inline: true, 29 | emptyText: 'This office is empty', 30 | itemTpl: '
', 31 | listeners: { 32 | childtap: 'onPeopleChildTap' 33 | } 34 | }] 35 | }); 36 | -------------------------------------------------------------------------------- /client/app/view/office/Wizard.js: -------------------------------------------------------------------------------- 1 | Ext.define('App.view.office.Wizard', { 2 | extend: 'App.view.widgets.Wizard', 3 | xtype: [ 4 | 'officewizard', 5 | 'officecreate', 6 | 'officeedit' 7 | ], 8 | 9 | bind: { 10 | title: '{record.phantom? "Add" : "Edit"} Office' 11 | }, 12 | 13 | screens: [{ 14 | title: 'General', 15 | iconCls: 'x-fa fa-info', 16 | items: [{ 17 | xtype: 'textfield', 18 | reference: 'name', 19 | label: 'Name', 20 | required: true, 21 | bind: '{record.name}' 22 | }, { 23 | xtype: 'textfield', 24 | reference: 'address', 25 | label: 'Address', 26 | required: true, 27 | bind: '{record.address}' 28 | }, { 29 | xtype: 'textfield', 30 | reference: 'city', 31 | label: 'City', 32 | required: true, 33 | bind: '{record.city}' 34 | }, { 35 | xtype: 'textfield', 36 | reference: 'postcode', 37 | label: 'ZIP/Postal', 38 | bind: '{record.postcode}' 39 | }, { 40 | xtype: 'textfield', 41 | reference: 'country', 42 | label: 'Country', 43 | required: true, 44 | bind: '{record.country}' 45 | }, { 46 | xtype: 'textfield', 47 | reference: 'region', 48 | label: 'Region', 49 | bind: '{record.region}' 50 | }] 51 | }] 52 | }); 53 | -------------------------------------------------------------------------------- /client/app/view/organization/Browse.js: -------------------------------------------------------------------------------- 1 | Ext.define('App.view.organization.Browse', { 2 | extend: 'App.view.widgets.Browse', 3 | 4 | fields: { 5 | manager: { 6 | property: 'manager_id' 7 | } 8 | }, 9 | 10 | controller: 'organizationbrowse', 11 | viewModel: { 12 | type: 'organizationbrowse' 13 | }, 14 | 15 | cls: 'organizationbrowse', 16 | bind: { 17 | store: '{organizations}' 18 | } 19 | }); 20 | -------------------------------------------------------------------------------- /client/app/view/organization/BrowseController.js: -------------------------------------------------------------------------------- 1 | Ext.define('App.view.organization.BrowseController', { 2 | extend: 'App.view.widgets.BrowseController', 3 | alias: 'controller.organizationbrowse', 4 | 5 | control: { 6 | '#': { 7 | reset: 'refresh' 8 | } 9 | }, 10 | 11 | refresh: function() { 12 | var vm = this.getViewModel(); 13 | vm.getStore('managers').reload(); 14 | }, 15 | 16 | onCreate: function() { 17 | this.redirectTo('organization/create'); 18 | } 19 | }); 20 | -------------------------------------------------------------------------------- /client/app/view/organization/BrowseModel.js: -------------------------------------------------------------------------------- 1 | Ext.define('App.view.organization.BrowseModel', { 2 | extend: 'Ext.app.ViewModel', 3 | alias: 'viewmodel.organizationbrowse', 4 | 5 | stores: { 6 | organizations: { 7 | type: 'organizations', 8 | grouper: { 9 | groupFn: function(record) { 10 | return record.get('name')[0]; 11 | } 12 | } 13 | }, 14 | managers: { 15 | type: 'filters', 16 | service: 'organizations', 17 | field: 'manager_id', 18 | label: [ 19 | 'manager.firstname', 20 | 'manager.lastname' 21 | ] 22 | } 23 | } 24 | }); 25 | -------------------------------------------------------------------------------- /client/app/view/organization/Show.js: -------------------------------------------------------------------------------- 1 | Ext.define('App.view.organization.Show', { 2 | extend: 'App.view.widgets.Show', 3 | xtype: 'organizationshow', 4 | 5 | controller: 'organizationshow', 6 | viewModel: { 7 | type: 'organizationshow' 8 | }, 9 | 10 | title: 'Organization', 11 | 12 | items: { 13 | header: { 14 | items: { 15 | title: { 16 | tpl: [ 17 | '
', 18 | '
{name}
', 19 | '
Managed by {manager.fullname}
' 20 | ] 21 | } 22 | } 23 | }, 24 | 25 | content: { 26 | items: { 27 | left: { 28 | items: { 29 | people: { 30 | xtype: 'organizationshowpeople' 31 | } 32 | } 33 | } 34 | } 35 | } 36 | } 37 | }); 38 | -------------------------------------------------------------------------------- /client/app/view/organization/ShowController.js: -------------------------------------------------------------------------------- 1 | Ext.define('App.view.organization.ShowController', { 2 | extend: 'App.view.widgets.ShowController', 3 | alias: 'controller.organizationshow', 4 | 5 | onRecordChange: function(view, record) { 6 | var vm = this.getViewModel(), 7 | people = vm.getStore('people'), 8 | history = vm.getStore('history'); 9 | 10 | if (record) { 11 | people.filter('organization_id', record.get('id')); 12 | history.filter('recipient.organization_id', record.get('id')); 13 | } else { 14 | people.removeAll(); 15 | history.removeAll(); 16 | } 17 | 18 | this.callParent(arguments); 19 | }, 20 | 21 | onPeopleHeadcountTap: function() { 22 | this.redirectTo('people/organization/' + this.getRecord().getId()) 23 | }, 24 | 25 | onHistoryAllTap: function() { 26 | this.redirectTo('history/organization/' + this.getRecord().getId()); 27 | } 28 | }); 29 | -------------------------------------------------------------------------------- /client/app/view/organization/ShowModel.js: -------------------------------------------------------------------------------- 1 | Ext.define('App.view.organization.ShowModel', { 2 | extend: 'Ext.app.ViewModel', 3 | alias: 'viewmodel.organizationshow', 4 | 5 | stores: { 6 | people: { 7 | type: 'people', 8 | pageSize: 12 9 | }, 10 | 11 | history: { 12 | type: 'actions', 13 | pageSize: 12 14 | } 15 | } 16 | }); 17 | -------------------------------------------------------------------------------- /client/app/view/organization/ShowPeople.js: -------------------------------------------------------------------------------- 1 | Ext.define('App.view.organization.ShowPeople', { 2 | extend: 'Ext.Panel', 3 | xtype: 'organizationshowpeople', 4 | 5 | cls: 'organization-people', 6 | iconCls: 'x-fa fa-users', 7 | title: 'Employees', 8 | 9 | header: { 10 | items: [{ 11 | xtype: 'button', 12 | handler: 'onPeopleHeadcountTap', 13 | iconCls: 'x-fa fa-users', 14 | ui: 'block', 15 | weigth: 10, 16 | bind: { 17 | text: '{record.headcount}', 18 | tooltip: 'Show employees of the {record.name} organization.' 19 | } 20 | }] 21 | }, 22 | 23 | items: [{ 24 | xtype: 'dataview', 25 | bind: '{people}', 26 | ui: 'thumbnails', 27 | minHeight: 80, 28 | inline: true, 29 | emptyText: 'This organization is empty', 30 | itemTpl: '
', 31 | listeners: { 32 | childtap: 'onPeopleChildTap' 33 | } 34 | }] 35 | }); 36 | -------------------------------------------------------------------------------- /client/app/view/organization/Wizard.js: -------------------------------------------------------------------------------- 1 | Ext.define('App.view.organization.Wizard', { 2 | extend: 'App.view.widgets.Wizard', 3 | xtype: [ 4 | 'organizationwizard', 5 | 'organizationcreate', 6 | 'organizationedit' 7 | ], 8 | 9 | controller: { 10 | type: 'organizationwizard' 11 | }, 12 | 13 | viewModel: { 14 | type: 'organizationwizard' 15 | }, 16 | 17 | bind: { 18 | title: '{record.phantom? "Add" : "Edit"} Organization' 19 | }, 20 | 21 | screens: [{ 22 | title: 'General', 23 | iconCls: 'x-fa fa-info', 24 | items: [{ 25 | xtype: 'textfield', 26 | reference: 'name', 27 | label: 'Name', 28 | required: true, 29 | bind: '{record.name}' 30 | }, { 31 | xtype: 'combobox', 32 | label: 'Manager', 33 | displayField: 'label', 34 | valueField: 'value', 35 | queryMode: 'local', 36 | forceSelection: true, 37 | required: true, 38 | bind: { 39 | value: '{record.manager_id}', 40 | store: '{managers}' 41 | } 42 | }] 43 | }] 44 | }); 45 | -------------------------------------------------------------------------------- /client/app/view/organization/WizardController.js: -------------------------------------------------------------------------------- 1 | Ext.define('App.view.organization.WizardController', { 2 | extend: 'App.view.widgets.WizardController', 3 | alias: 'controller.organizationwizard', 4 | 5 | control: { 6 | '#': { 7 | reset: 'refresh' 8 | } 9 | }, 10 | 11 | refresh: function() { 12 | this.getViewModel().getStore('managers').reload(); 13 | } 14 | }); 15 | -------------------------------------------------------------------------------- /client/app/view/organization/WizardModel.js: -------------------------------------------------------------------------------- 1 | Ext.define('App.view.organization.WizardModel', { 2 | extend: 'Ext.app.ViewModel', 3 | alias: 'viewmodel.organizationwizard', 4 | 5 | stores: { 6 | managers: { 7 | type: 'filters', 8 | service: 'people', 9 | field: 'person.id', 10 | label: [ 11 | 'firstname', 12 | 'lastname' 13 | ] 14 | } 15 | } 16 | }); 17 | -------------------------------------------------------------------------------- /client/app/view/person/Browse.js: -------------------------------------------------------------------------------- 1 | Ext.define('App.view.person.Browse', { 2 | extend: 'App.view.widgets.Browse', 3 | 4 | fields: { 5 | office: { 6 | property: 'office_id' 7 | }, 8 | organization: { 9 | property: 'organization_id' 10 | } 11 | }, 12 | 13 | controller: 'personbrowse', 14 | viewModel: { 15 | type: 'personbrowse' 16 | }, 17 | 18 | cls: 'personbrowse', 19 | bind: { 20 | store: '{people}' 21 | } 22 | }); 23 | -------------------------------------------------------------------------------- /client/app/view/person/BrowseController.js: -------------------------------------------------------------------------------- 1 | Ext.define('App.view.person.BrowseController', { 2 | extend: 'App.view.widgets.BrowseController', 3 | alias: 'controller.personbrowse', 4 | 5 | control: { 6 | '#': { 7 | reset: 'refresh' 8 | } 9 | }, 10 | 11 | refresh: function() { 12 | var vm = this.getViewModel(); 13 | vm.getStore('offices').reload(); 14 | vm.getStore('organizations').reload(); 15 | }, 16 | 17 | onCreate: function() { 18 | this.redirectTo('person/create'); 19 | } 20 | }); 21 | -------------------------------------------------------------------------------- /client/app/view/person/BrowseModel.js: -------------------------------------------------------------------------------- 1 | Ext.define('App.view.person.BrowseModel', { 2 | extend: 'Ext.app.ViewModel', 3 | alias: 'viewmodel.personbrowse', 4 | 5 | stores: { 6 | people: { 7 | type: 'people', 8 | grouper: { 9 | groupFn: function(record) { 10 | return record.get('lastname')[0]; 11 | } 12 | } 13 | }, 14 | offices: { 15 | type: 'filters', 16 | service: 'people', 17 | field: 'office_id', 18 | label: 'office.name' 19 | }, 20 | organizations: { 21 | type: 'filters', 22 | service: 'people', 23 | field: 'organization_id', 24 | label: 'organization.name' 25 | } 26 | } 27 | }); 28 | -------------------------------------------------------------------------------- /client/app/view/person/Show.js: -------------------------------------------------------------------------------- 1 | Ext.define('App.view.person.Show', { 2 | extend: 'App.view.widgets.Show', 3 | xtype: 'personshow', 4 | 5 | controller: 'personshow', 6 | viewModel: { 7 | type: 'personshow' 8 | }, 9 | 10 | title: 'Profile', 11 | 12 | items: { 13 | header: { 14 | xtype: 'personshowheader' 15 | }, 16 | 17 | tools: { 18 | xtype: 'personshowtools', 19 | weight: -5 20 | }, 21 | 22 | content: { 23 | items: { 24 | left: { 25 | items: { 26 | details: { 27 | xtype: 'personshowdetails' 28 | }, 29 | 30 | office: { 31 | xtype: 'personshowoffice' 32 | }, 33 | 34 | organization: { 35 | xtype: 'personshoworg' 36 | } 37 | } 38 | } 39 | } 40 | } 41 | } 42 | }); 43 | -------------------------------------------------------------------------------- /client/app/view/person/ShowController.js: -------------------------------------------------------------------------------- 1 | Ext.define('App.view.person.ShowController', { 2 | extend: 'App.view.widgets.ShowController', 3 | alias: 'controller.personshow', 4 | 5 | doAction: function(type) { 6 | this.fireEvent('actionexec', type, this.getRecord(), true); 7 | }, 8 | 9 | onRecordChange: function(view, record) { 10 | var vm = this.getViewModel(), 11 | history = vm.getStore('history'), 12 | coworkers = vm.getStore('coworkers'); 13 | 14 | history.removeAll(); 15 | coworkers.removeAll(); 16 | 17 | if (record) { 18 | history.filter('recipient_id', record.get('id')); 19 | history.load(); 20 | 21 | coworkers.filter([ 22 | { property: 'organization_id', value: record.get('organization_id') }, 23 | { property: 'id', value: record.get('id'), operator: '!=' } 24 | ]); 25 | 26 | coworkers.load(); 27 | } 28 | 29 | this.callParent(arguments); 30 | }, 31 | 32 | onCallTap: function() { 33 | this.doAction('phone'); 34 | }, 35 | 36 | onSkypeTap: function() { 37 | this.doAction('skype'); 38 | }, 39 | 40 | onEmailTap: function() { 41 | this.doAction('email'); 42 | }, 43 | 44 | onLinkedInTap: function() { 45 | this.doAction('linkedin'); 46 | }, 47 | 48 | onOfficeHeadcountTap: function() { 49 | var office = this.getRecord().getOffice(); 50 | this.redirectTo('people/office/' + office.getId()) 51 | }, 52 | 53 | onOrganizationHeadcountTap: function() { 54 | var organization = this.getRecord().getOrganization(); 55 | this.redirectTo('people/organization/' + organization.getId()) 56 | } 57 | }); 58 | -------------------------------------------------------------------------------- /client/app/view/person/ShowDetails.js: -------------------------------------------------------------------------------- 1 | Ext.define('App.view.person.ShowDetails', { 2 | extend: 'Ext.Panel', 3 | xtype: 'personshowdetails', 4 | 5 | cls: 'person-details', 6 | title: 'Details', 7 | 8 | bind: { 9 | record: '{record}' 10 | }, 11 | 12 | tpl: [ 13 | '
', 14 | '
', 15 | '
Username
', 16 | '
{username}
', 17 | '
', 18 | '', 19 | '
', 20 | '
Phone
', 21 | '
{phone}
', 22 | '
', 23 | '
', 24 | '', 25 | '
', 26 | '
Extension
', 27 | '
{extension}
', 28 | '
', 29 | '
', 30 | '
', 31 | '
', 32 | '
', 33 | '
Email
', 34 | '
{email}
', 35 | '
', 36 | '', 37 | '
', 38 | '
Skype
', 39 | '
{skype}
', 40 | '
', 41 | '
', 42 | '', 43 | '
', 44 | '
LinkedIn
', 45 | '
{linkedin}
', 46 | '
', 47 | '
', 48 | '
', 49 | '
', 50 | '
', 51 | '
Birthday
', 52 | '
{birthday:date("F jS, Y")}
', 53 | '
{birthday:dateDiff(new Date())}
', 54 | '
', 55 | '
', 56 | '
Entry Date
', 57 | '
{started:date("F jS, Y")}
', 58 | '', 59 | '
{started:dateDiff(new Date())}
', 60 | '
', 61 | '
', 62 | '', 63 | '
', 64 | '
Exit Date
', 65 | '
{ended:date("F jS, Y")}
', 66 | '
{started:dateDiff(values.ended)}
', 67 | '
', 68 | '
', 69 | '
' 70 | ] 71 | }); 72 | -------------------------------------------------------------------------------- /client/app/view/person/ShowDetails.scss: -------------------------------------------------------------------------------- 1 | .person-details { 2 | .block-section { 3 | min-width: 25%; 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /client/app/view/person/ShowHeader.js: -------------------------------------------------------------------------------- 1 | Ext.define('App.view.person.ShowHeader', { 2 | extend: 'App.view.widgets.ShowHeader', 3 | xtype: 'personshowheader', 4 | 5 | mixins: [ 6 | 'Ext.mixin.Responsive' 7 | ], 8 | 9 | requires: [ 10 | 'Ext.Image' 11 | ], 12 | 13 | responsiveConfig: { 14 | 'width < 600': { 15 | layout: { 16 | vertical: true, 17 | align: 'center', 18 | pack: 'center' 19 | } 20 | }, 21 | 22 | 'width > 599': { 23 | layout: { 24 | vertical: false, 25 | align: 'end', 26 | pack: 'start' 27 | } 28 | } 29 | }, 30 | 31 | cls: [ 32 | 'show-header', 33 | 'person-header' 34 | ], 35 | 36 | items: { 37 | image: { 38 | xtype: 'image', 39 | weight: -10, 40 | userCls: [ 41 | 'header-picture', 42 | 'picture' 43 | ], 44 | bind: { 45 | src: '{record.picture}' 46 | } 47 | }, 48 | 49 | title: { 50 | tpl: [ 51 | '
{firstname} {lastname}
', 52 | '
{title}
' 53 | ] 54 | } 55 | } 56 | }); 57 | -------------------------------------------------------------------------------- /client/app/view/person/ShowHeader.scss: -------------------------------------------------------------------------------- 1 | .person-header { 2 | overflow: visible; 3 | 4 | > .x-body-el { 5 | overflow: visible; 6 | } 7 | 8 | .header-picture { 9 | @include single-box-shadow($hoff: 0, $voff: 4px, $spread: -2px); 10 | margin: 0 $show-header-spacing $show-header-spacing 0; 11 | border: 8px solid $panel-header-background-color; 12 | height: $show-header-picture-size; 13 | width: $show-header-picture-size; 14 | } 15 | 16 | @media screen and (min-width: 600px) { 17 | .header-picture { 18 | margin-bottom: -$show-header-picture-size/2; 19 | } 20 | } 21 | 22 | @media screen and (max-width: 599px) { 23 | > .x-body-el { 24 | padding-bottom: 32px; 25 | } 26 | 27 | .header-picture { 28 | margin-left: auto; 29 | margin-right: auto; 30 | } 31 | 32 | .header-title { 33 | text-align: center; 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /client/app/view/person/ShowModel.js: -------------------------------------------------------------------------------- 1 | Ext.define('App.view.person.ShowModel', { 2 | extend: 'Ext.app.ViewModel', 3 | alias: 'viewmodel.personshow', 4 | 5 | stores: { 6 | coworkers: { 7 | type: 'people', 8 | pageSize: 12 9 | }, 10 | 11 | history: { 12 | type: 'actions', 13 | pageSize: 12 14 | } 15 | } 16 | }); 17 | -------------------------------------------------------------------------------- /client/app/view/person/ShowOffice.js: -------------------------------------------------------------------------------- 1 | Ext.define('App.view.person.ShowOffice', { 2 | extend: 'Ext.Panel', 3 | xtype: 'personshowoffice', 4 | 5 | cls: 'person-office', 6 | iconCls: 'x-fa fa-globe', 7 | 8 | bind: { 9 | title: 10 | ''+ 11 | '{record.office.name}'+ 12 | ''+ 13 | '
'+ 14 | '{record.office.city}, '+ 15 | '{record.office.country}'+ 16 | '
' 17 | }, 18 | 19 | header: { 20 | items: [{ 21 | xtype: 'button', 22 | handler: 'onOfficeHeadcountTap', 23 | iconCls: 'x-fa fa-users', 24 | ui: 'block', 25 | weigth: 10, 26 | bind: { 27 | text: '{record.office.headcount}', 28 | tooltip: 'Show employees of the {record.office.name} office.' 29 | } 30 | }], 31 | }, 32 | 33 | items: [{ 34 | xtype: 'mapview', 35 | bind: { 36 | markers: '{record.office}' 37 | } 38 | }] 39 | }); 40 | -------------------------------------------------------------------------------- /client/app/view/person/ShowOrg.js: -------------------------------------------------------------------------------- 1 | Ext.define('App.view.person.ShowOrg', { 2 | extend: 'Ext.Panel', 3 | xtype: 'personshoworg', 4 | 5 | cls: 'person-org', 6 | iconCls: 'x-fa fa-sitemap', 7 | 8 | bind: { 9 | title: 10 | ''+ 11 | '{record.organization.name}'+ 12 | ''+ 13 | '
'+ 14 | 'Managed by '+ 15 | ''+ 16 | '{record.organization.manager.fullname}'+ 17 | ''+ 18 | '
' 19 | }, 20 | 21 | header: { 22 | items: [{ 23 | xtype: 'button', 24 | handler: 'onOrganizationHeadcountTap', 25 | iconCls: 'x-fa fa-users', 26 | ui: 'block', 27 | weigth: 10, 28 | bind: { 29 | text: '{record.organization.headcount}', 30 | tooltip: 'Show employees of the {record.organization.name} organization.' 31 | } 32 | }] 33 | }, 34 | 35 | items: [{ 36 | xtype: 'dataview', 37 | ui: 'thumbnails', 38 | minHeight: 80, 39 | inline: true, 40 | itemTpl: '
', 41 | bind: { 42 | emptyText: '{record.fullname} is the only employee in this organization', 43 | store: '{coworkers}' 44 | }, 45 | listeners: { 46 | childtap: 'onPeopleChildTap' 47 | } 48 | }] 49 | }); 50 | -------------------------------------------------------------------------------- /client/app/view/person/ShowOrg.scss: -------------------------------------------------------------------------------- 1 | .person-org { 2 | .x-dataview-item { 3 | cursor: pointer; 4 | width: 20%; 5 | } 6 | .picture { 7 | @include border-radius(0); 8 | margin-top: -1px; 9 | padding-bottom: 100%; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /client/app/view/person/ShowTools.js: -------------------------------------------------------------------------------- 1 | Ext.define('App.view.person.ShowTools', { 2 | extend: 'Ext.Container', 3 | xtype: 'personshowtools', 4 | 5 | mixins: [ 6 | 'Ext.mixin.Responsive' 7 | ], 8 | 9 | cls: 'person-tools', 10 | 11 | layout: { 12 | type: 'box', 13 | align: 'center' 14 | }, 15 | 16 | responsiveConfig: { 17 | 'width < 600': { 18 | layout: { 19 | vertical: true 20 | } 21 | }, 22 | 'width > 599': { 23 | layout: { 24 | vertical: false 25 | } 26 | } 27 | }, 28 | 29 | items: [{ 30 | xtype: 'toolbar', 31 | flex: 1, 32 | 33 | items: [{ 34 | iconCls: 'x-fa fa-phone', 35 | handler: 'onCallTap', 36 | ui: 'action-phone', 37 | bind: { 38 | tooltip: 'Call {record.phone}' 39 | } 40 | },{ 41 | iconCls: 'x-fa fa-skype', 42 | handler: 'onSkypeTap', 43 | ui: 'action-skype', 44 | bind: { 45 | tooltip: 'Skype with {record.skype}' 46 | } 47 | },{ 48 | iconCls: 'x-fa fa-envelope', 49 | handler: 'onEmailTap', 50 | ui: 'action-email', 51 | bind: { 52 | tooltip: 'Send email to {record.email}' 53 | } 54 | },{ 55 | iconCls: 'x-fa fa-linkedin', 56 | handler: 'onLinkedInTap', 57 | ui: 'action-linkedin', 58 | bind: { 59 | tooltip: 'See {record.linkedin} LinkedIn profile' 60 | } 61 | }] 62 | }, { 63 | xtype: 'component', 64 | cls: 'location', 65 | tpl: [ 66 | '
{office.city}
' 67 | //'
11:00 pm
' 68 | ], 69 | bind: { 70 | record: '{record}' 71 | } 72 | }] 73 | }); 74 | -------------------------------------------------------------------------------- /client/app/view/person/ShowTools.scss: -------------------------------------------------------------------------------- 1 | .person-tools { 2 | z-index: 1; 3 | 4 | > .x-body-el { 5 | padding: 12px 8px 24px 8px; 6 | } 7 | 8 | .location { 9 | background-color: contrasted($background-color, 4%); 10 | padding: 8px 16px; 11 | text-align: center; 12 | 13 | .time, 14 | .city { 15 | display: inline-block; 16 | font-size: 13px; 17 | } 18 | 19 | .time::before { 20 | content: '|'; 21 | margin: 0 4px; 22 | } 23 | 24 | .city { 25 | font-weight: bold; 26 | } 27 | } 28 | 29 | @media screen and (min-width: 600px) { 30 | .x-toolbar { 31 | margin-left: $show-header-picture-size + $show-header-spacing * 2; 32 | margin-right: $show-header-spacing; 33 | } 34 | } 35 | 36 | @media screen and (max-width: 599px) { 37 | margin-top: -38px; 38 | 39 | > .x-body-el { 40 | flex-direction: column; 41 | padding-bottom: 0; 42 | } 43 | 44 | .location { 45 | margin-top: $show-header-spacing; 46 | width: 100%; 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /client/app/view/person/WizardController.js: -------------------------------------------------------------------------------- 1 | Ext.define('App.view.person.WizardController', { 2 | extend: 'App.view.widgets.WizardController', 3 | alias: 'controller.personwizard', 4 | 5 | control: { 6 | '#': { 7 | reset: 'refresh' 8 | } 9 | }, 10 | 11 | refresh: function() { 12 | var vm = this.getViewModel(); 13 | vm.getStore('offices').reload(); 14 | vm.getStore('organizations').reload(); 15 | }, 16 | 17 | onNameFieldsBlur: function() { 18 | var me = this, 19 | vm = me.getViewModel(), 20 | record = vm.get('record'), 21 | firstname = record.get('firstname'), 22 | lastname = record.get('lastname'), 23 | username = record.get('username'); 24 | 25 | // Don't try to generate a username if the user manually entered a value in 26 | // the username field or if the firstname and/or lastname fields are empty. 27 | if (Ext.isEmpty(firstname) || Ext.isEmpty(lastname) || 28 | (!Ext.isEmpty(username) && me._generatedUsername !== username)) { 29 | return; 30 | } 31 | 32 | me._generatedUsername = true; 33 | Server.people.generateUsername({ 34 | firstname: firstname, 35 | lastname: lastname 36 | }, function(result, response, success) { 37 | if (success && me._generatedUsername === true) { 38 | me._generatedUsername = result; 39 | record.set('username', result); 40 | } 41 | }); 42 | }, 43 | 44 | onUsernameChange: function(field, value) { 45 | // If the username field changed and is different from the last generated value, then 46 | // the user has manually entered a value that we don't want to overwrite, so let's 47 | // cancel the current generating process (if any) and reset the generated value. 48 | if (value !== this._generatedUsername) { 49 | this._generatedUsername = false; 50 | } 51 | }, 52 | 53 | doPasswordMatch: function(value) { 54 | return this.lookup('password').getValue() !== value? 55 | 'Passwords do not match' : 56 | true; 57 | } 58 | }); 59 | -------------------------------------------------------------------------------- /client/app/view/person/WizardModel.js: -------------------------------------------------------------------------------- 1 | Ext.define('App.view.person.WizardModel', { 2 | extend: 'Ext.app.ViewModel', 3 | alias: 'viewmodel.personwizard', 4 | 5 | stores: { 6 | offices: { 7 | type: 'filters', 8 | service: 'offices', 9 | field: 'office.id', 10 | label: 'office.name' 11 | }, 12 | organizations: { 13 | type: 'filters', 14 | service: 'organizations', 15 | field: 'organization.id', 16 | label: 'organization.name' 17 | } 18 | } 19 | }); 20 | -------------------------------------------------------------------------------- /client/app/view/phone/history/Browse.js: -------------------------------------------------------------------------------- 1 | Ext.define('App.view.phone.history.Browse', { 2 | extend: 'App.view.history.Browse', 3 | // xtype: 'historybrowse', -- set by profile 4 | 5 | requires: [ 6 | 'Ext.dataview.listswiper.ListSwiper', 7 | 'Ext.dataview.plugin.ListPaging' 8 | ], 9 | 10 | items: [{ 11 | xtype: 'list', 12 | bind: '{history}', 13 | emptyText: 'No activity was found', 14 | striped: true, 15 | grouped: true, 16 | ui: 'listing', 17 | 18 | selectable: { 19 | disabled: true 20 | }, 21 | 22 | plugins: [{ 23 | type: 'listpaging', 24 | autoPaging: true 25 | }, { 26 | type: 'listswiper', 27 | right: [{ 28 | iconCls: 'x-fa fa-trash', 29 | commit: 'onDeleteAction', 30 | undoable: true, 31 | text: 'Delete', 32 | ui: 'remove' 33 | }] 34 | }], 35 | 36 | itemTpl: [ 37 | '
', 38 | '', 39 | '
', 40 | '
', 41 | '
', 42 | '
{recipient.fullname}
', 43 | '
{subject}
', 44 | '
', 45 | '
', 46 | '
{created:date("Y/m/d")}
', 47 | '
{created:date("H:i")}
', 48 | '
' 49 | ] 50 | }] 51 | }); 52 | -------------------------------------------------------------------------------- /client/app/view/phone/main/Main.js: -------------------------------------------------------------------------------- 1 | Ext.define('App.view.phone.main.Main', { 2 | extend: 'Ext.Container', 3 | // xtype: 'main', -- set by profile 4 | 5 | controller: 'phone-main', 6 | 7 | cls: 'phone-profile', 8 | layout: 'card', 9 | 10 | items: [{ 11 | xtype: 'panel', 12 | layout: 'card', 13 | reference: 'views', 14 | defaults: { 15 | header: { 16 | ui: 'dark', 17 | defaults: { 18 | ui: 'flat dark large', 19 | }, 20 | items: { 21 | menu: { 22 | xtype: 'button', 23 | iconCls: 'x-fa fa-bars', 24 | weight: -10, 25 | handler: function () { 26 | Ext.fireEvent('togglemainmenu'); 27 | } 28 | } 29 | } 30 | } 31 | }, 32 | lbar: { 33 | xtype: 'mainmenu', 34 | reference: 'mainmenu', 35 | ui: 'dark slide', 36 | zIndex: 4, 37 | items: { 38 | trigger: false 39 | } 40 | } 41 | }, { 42 | xtype: 'container', 43 | reference: 'navigation', 44 | layout: 'card', 45 | defaults: { 46 | header: { 47 | ui: 'dark', 48 | defaults: { 49 | ui: 'flat dark large', 50 | }, 51 | items: { 52 | back: { 53 | xtype: 'button', 54 | iconCls: 'x-fa fa-chevron-left', 55 | weight: -10, 56 | handler: function () { 57 | Ext.fireEvent('navigationback'); 58 | } 59 | } 60 | } 61 | } 62 | } 63 | }] 64 | }); 65 | -------------------------------------------------------------------------------- /client/app/view/phone/main/MainController.js: -------------------------------------------------------------------------------- 1 | Ext.define('App.view.phone.main.MainController', { 2 | extend: 'App.view.main.MainController', 3 | alias: 'controller.phone-main', 4 | 5 | getContainerForViewId: function(id) { 6 | var regex = /^(person|office|organization)(create|edit|show)$/; 7 | return this.lookup(id.match(regex)? 'navigation' : 'views'); 8 | } 9 | }); 10 | -------------------------------------------------------------------------------- /client/app/view/phone/office/Browse.js: -------------------------------------------------------------------------------- 1 | Ext.define('App.view.phone.office.Browse', { 2 | extend: 'App.view.office.Browse', 3 | // xtype: 'officebrowse', -- set by profile 4 | 5 | requires: [ 6 | 'Ext.dataview.listswiper.ListSwiper', 7 | 'Ext.dataview.plugin.ListPaging' 8 | ], 9 | 10 | header: { 11 | items: { 12 | create: { 13 | xtype: 'button', 14 | iconCls: 'x-fa fa-plus', 15 | handler: 'onCreate', 16 | weight: 10 17 | } 18 | } 19 | }, 20 | 21 | items: [{ 22 | xtype: 'list', 23 | bind: '{offices}', 24 | indexBar: true, 25 | striped: true, 26 | grouped: true, 27 | ui: 'listing', 28 | 29 | selectable: { 30 | disabled: true 31 | }, 32 | 33 | plugins: [{ 34 | type: 'listpaging', 35 | autoPaging: true 36 | }, { 37 | type: 'listswiper', 38 | right: [{ 39 | iconCls: 'x-fa fa-pencil', 40 | commit: 'onEditAction', 41 | text: 'Edit', 42 | ui: 'edit' 43 | }] 44 | }], 45 | 46 | itemTpl: [ 47 | '
', 48 | '
{name}
', 49 | '
{city}, {country}
', 50 | '
', 51 | '
{headcount:plural("employee")}
' 52 | ], 53 | 54 | listeners: { 55 | childtap: 'onChildActivate' 56 | } 57 | }] 58 | }); 59 | -------------------------------------------------------------------------------- /client/app/view/phone/organisation/Browse.js: -------------------------------------------------------------------------------- 1 | Ext.define('App.view.phone.organization.Browse', { 2 | extend: 'App.view.organization.Browse', 3 | // xtype: 'organizationbrowse', -- set by profile 4 | 5 | requires: [ 6 | 'Ext.dataview.listswiper.ListSwiper', 7 | 'Ext.dataview.plugin.ListPaging' 8 | ], 9 | 10 | header: { 11 | items: { 12 | create: { 13 | xtype: 'button', 14 | iconCls: 'x-fa fa-plus', 15 | handler: 'onCreate', 16 | weight: 10 17 | } 18 | } 19 | }, 20 | 21 | items: [{ 22 | xtype: 'list', 23 | bind: '{organizations}', 24 | indexBar: true, 25 | striped: true, 26 | grouped: true, 27 | ui: 'listing', 28 | 29 | selectable: { 30 | disabled: true 31 | }, 32 | 33 | plugins: [{ 34 | type: 'listpaging', 35 | autoPaging: true 36 | }, { 37 | type: 'listswiper', 38 | right: [{ 39 | iconCls: 'x-fa fa-pencil', 40 | commit: 'onEditAction', 41 | text: 'Edit', 42 | ui: 'edit' 43 | }] 44 | }], 45 | 46 | itemTpl: [ 47 | '
', 48 | '
{name}
', 49 | '
{manager.fullname}
', 50 | '
', 51 | '
{headcount:plural("employee")}
' 52 | ], 53 | 54 | listeners: { 55 | childtap: 'onChildActivate' 56 | } 57 | }] 58 | }); 59 | -------------------------------------------------------------------------------- /client/app/view/phone/person/Browse.js: -------------------------------------------------------------------------------- 1 | Ext.define('App.view.phone.person.Browse', { 2 | extend: 'App.view.person.Browse', 3 | // xtype: 'personbrowse', -- set by profile 4 | 5 | requires: [ 6 | 'Ext.dataview.listswiper.ListSwiper', 7 | 'Ext.dataview.plugin.ListPaging' 8 | ], 9 | 10 | controller: 'phone-personbrowse', 11 | 12 | layout: 'fit', 13 | 14 | header: { 15 | items: { 16 | create: { 17 | xtype: 'button', 18 | iconCls: 'x-fa fa-plus', 19 | handler: 'onCreate', 20 | weight: 10 21 | } 22 | } 23 | }, 24 | 25 | items: [{ 26 | xtype: 'list', 27 | reference: 'list', 28 | bind: '{people}', 29 | striped: true, 30 | grouped: true, 31 | ui: 'listing', 32 | selectable: { 33 | disabled: true 34 | }, 35 | plugins: [{ 36 | type: 'listpaging', 37 | autoPaging: true 38 | }, { 39 | type: 'listswiper', 40 | left: [{ 41 | iconCls: 'x-fa fa-skype', 42 | commit: 'onSkypeAction', 43 | text: 'Skype', 44 | ui: 'skype', 45 | data: { 46 | subject: 'skype' 47 | } 48 | }, { 49 | iconCls: 'x-fa fa-envelope-o', 50 | commit: 'onEmailAction', 51 | text: 'Email', 52 | ui: 'email', 53 | data: { 54 | subject: 'email' 55 | } 56 | }], 57 | right: [{ 58 | iconCls: 'x-fa fa-pencil', 59 | commit: 'onEditAction', 60 | text: 'Edit', 61 | ui: 'edit' 62 | }], 63 | widget: { 64 | xtype: 'personlistswiperitem' 65 | } 66 | }], 67 | itemConfig: { 68 | xtype: 'listitem', 69 | items: [{ 70 | xtype: 'button', 71 | handler: 'onPhoneTap', 72 | iconCls: 'x-fa fa-phone', 73 | userCls: 'x-item-no-tap', 74 | docked: 'right', 75 | ui: 'flat' 76 | }] 77 | }, 78 | itemTpl: [ 79 | '
', 80 | '
', 81 | '
{fullname}
', 82 | '
{title}
', 83 | '
' 84 | ], 85 | listeners: { 86 | childtap: 'onChildActivate' 87 | } 88 | }] 89 | }); 90 | -------------------------------------------------------------------------------- /client/app/view/phone/person/BrowseController.js: -------------------------------------------------------------------------------- 1 | Ext.define('App.view.phone.person.BrowseController', { 2 | extend: 'App.view.person.BrowseController', 3 | alias: 'controller.phone-personbrowse', 4 | 5 | onPhoneTap: function(button, event) { 6 | var list = this.lookup('list'), 7 | record = list.mapToRecord(event); 8 | 9 | this.doAction('phone', record); 10 | }, 11 | 12 | onSkypeAction: function(list, data) { 13 | this.doAction('skype', data.record); 14 | }, 15 | 16 | onEmailAction: function(list, data) { 17 | this.doAction('email', data.record); 18 | }, 19 | 20 | doAction: function(type, record) { 21 | this.fireEvent('actionexec', type, record); 22 | } 23 | }); 24 | -------------------------------------------------------------------------------- /client/app/view/phone/person/BrowseFilters.js: -------------------------------------------------------------------------------- 1 | Ext.define('App.view.phone.person.BrowseFilters', { 2 | extend: 'Ext.Container', 3 | // xtype: 'personbrowsefilters', -- set by profile 4 | 5 | layout: 'vbox', 6 | 7 | items: [{ 8 | xtype: 'searchfield', 9 | placeholder: 'Search' 10 | }] 11 | }); 12 | 13 | 14 | -------------------------------------------------------------------------------- /client/app/view/phone/person/ListSwiperItem.js: -------------------------------------------------------------------------------- 1 | Ext.define('App.view.phone.person.ListSwiperItem', { 2 | extend: 'Ext.dataview.listswiper.Stepper', 3 | // xtype: 'personlistswiperitem', -- set by profile 4 | 5 | tpl: [ 6 | '
', 7 | '', 8 | '{text}', 9 | '', 10 | '', 11 | '{[values[values.subject]]}', 12 | '', 13 | '
' 14 | ] 15 | }); 16 | -------------------------------------------------------------------------------- /client/app/view/tablet/history/Browse.js: -------------------------------------------------------------------------------- 1 | Ext.define('App.view.tablet.history.Browse', { 2 | extend: 'App.view.history.Browse', 3 | // xtype: 'historybrowse', -- set by profile 4 | 5 | requires: [ 6 | 'Ext.plugin.ListPaging' 7 | ], 8 | 9 | tbar: { 10 | xtype: 'historybrowsetoolbar' 11 | }, 12 | 13 | items: [{ 14 | xtype: 'grid', 15 | emptyText: 'No activity was found to match your search', 16 | bind: '{history}', 17 | ui: 'listing', 18 | 19 | selectable: { 20 | disabled: true 21 | }, 22 | 23 | plugins: [{ 24 | type: 'listpaging', 25 | autoPaging: true 26 | }], 27 | 28 | columns: [{ 29 | dataIndex: 'type', 30 | align: 'center', 31 | width: 75, 32 | cell: { 33 | cls: 'history-visual', 34 | encodeHtml: false 35 | }, 36 | tpl: [ 37 | '', 38 | '
' 39 | ] 40 | }, { 41 | text: 'Name / Title', 42 | dataIndex: 'recipient.lastname', 43 | flex: 1, 44 | cell: { 45 | encodeHtml: false 46 | }, 47 | tpl: [ 48 | '', 49 | '{fullname}', 50 | '
{title}
', 51 | '
' 52 | ] 53 | }, { 54 | text: 'Organization', 55 | dataIndex: 'recipient.organization.name', 56 | flex: 1, 57 | cell: { 58 | encodeHtml: false 59 | }, 60 | tpl: [ 61 | '', 62 | '{name}', 63 | '' 64 | ] 65 | }, { 66 | text: 'Office', 67 | dataIndex: 'recipient.office.name', 68 | flex: 1, 69 | cell: { 70 | encodeHtml: false 71 | }, 72 | tpl: [ 73 | '', 74 | '{name}', 75 | '
{city}, {country}
', 76 | '
' 77 | ] 78 | }, { 79 | xtype: 'datecolumn', 80 | dataIndex: 'created', 81 | format: 'Y-m-d H:i', 82 | text: 'Date', 83 | flex: 1 84 | }] 85 | }] 86 | }); 87 | -------------------------------------------------------------------------------- /client/app/view/tablet/history/BrowseToolbar.js: -------------------------------------------------------------------------------- 1 | Ext.define('App.view.tablet.history.BrowseToolbar', { 2 | extend: 'App.view.widgets.BrowseToolbar', 3 | // xtype: 'historybrowsetoolbar', -- set by profile 4 | 5 | items: { 6 | employees: { 7 | xtype: 'combobox', 8 | valueField: 'value', 9 | displayField: 'label', 10 | placeholder: 'All Employees', 11 | queryMode: 'local', 12 | weight: 10, 13 | bind: { 14 | selection: '{filters.recipient}', 15 | store: '{recipients}' 16 | } 17 | }, 18 | organizations: { 19 | xtype: 'combobox', 20 | valueField: 'value', 21 | displayField: 'label', 22 | placeholder: 'All Organizations', 23 | queryMode: 'local', 24 | weight: 11, 25 | bind: { 26 | selection: '{filters.organization}', 27 | store: '{organizations}' 28 | } 29 | }, 30 | offices: { 31 | xtype: 'combobox', 32 | valueField: 'value', 33 | displayField: 'label', 34 | placeholder: 'All Offices', 35 | queryMode: 'local', 36 | weight: 12, 37 | bind: { 38 | selection: '{filters.office}', 39 | store: '{offices}' 40 | } 41 | }, 42 | actions: { 43 | xtype: 'combobox', 44 | valueField: 'value', 45 | displayField: 'label', 46 | placeholder: 'All Actions', 47 | queryMode: 'local', 48 | weight: 13, 49 | bind: { 50 | selection: '{filters.type}', 51 | store: '{types}' 52 | } 53 | } 54 | } 55 | }); 56 | -------------------------------------------------------------------------------- /client/app/view/tablet/main/Main.js: -------------------------------------------------------------------------------- 1 | Ext.define('App.view.tablet.main.Main', { 2 | extend: 'Ext.Panel', 3 | // xtype: 'main', -- set by profile 4 | 5 | controller: 'main', 6 | 7 | layout: 'card', 8 | 9 | defaults: { 10 | header: { 11 | defaults: { 12 | ui: 'flat large' 13 | } 14 | } 15 | }, 16 | 17 | lbar: { 18 | xtype: 'mainmenu', 19 | reference: 'mainmenu', 20 | ui: 'dark micro', 21 | zIndex: 4 22 | } 23 | }); 24 | -------------------------------------------------------------------------------- /client/app/view/tablet/office/Browse.js: -------------------------------------------------------------------------------- 1 | Ext.define('App.view.tablet.office.Browse', { 2 | extend: 'App.view.office.Browse', 3 | // xtype: 'officebrowse', -- set by profile 4 | 5 | requires: [ 6 | 'Ext.plugin.ListPaging' 7 | ], 8 | 9 | controller: 'tablet-officebrowse', 10 | 11 | tbar: { 12 | xtype: 'officebrowsetoolbar' 13 | }, 14 | 15 | items: [{ 16 | xtype: 'grid', 17 | emptyText: 'No office was found to match your search', 18 | bind: '{offices}', 19 | ui: 'listing', 20 | 21 | selectable: { 22 | disabled: true 23 | }, 24 | 25 | plugins: [{ 26 | type: 'listpaging', 27 | autoPaging: true 28 | }], 29 | 30 | columns: [{ 31 | text: 'Name', 32 | dataIndex: 'name', 33 | flex: 2, 34 | cell: { 35 | encodeHtml: false 36 | }, 37 | tpl: '{name}' 38 | }, { 39 | text: 'Address', 40 | dataIndex: 'country', 41 | flex: 2, 42 | cell: { 43 | encodeHtml: false 44 | }, 45 | tpl: [ 46 | '
{city}, {country}
', 47 | '
{address}
' 48 | ] 49 | }, { 50 | text: 'Headcount', 51 | dataIndex: 'headcount', 52 | flex: 1, 53 | cell: { 54 | encodeHtml: false 55 | }, 56 | tpl: [ 57 | '', 58 | '{headcount:plural("employee")}', 59 | '' 60 | ] 61 | }], 62 | 63 | listeners: { 64 | childdoubletap: 'onChildActivate' 65 | } 66 | }] 67 | }); 68 | -------------------------------------------------------------------------------- /client/app/view/tablet/office/BrowseController.js: -------------------------------------------------------------------------------- 1 | Ext.define('App.view.tablet.office.BrowseController', { 2 | extend: 'App.view.office.BrowseController', 3 | alias: 'controller.tablet-officebrowse', 4 | 5 | onCreate: function() { 6 | // The creation form can be accessed either by clicking the "create" button (dialog) 7 | // or via the #office/create url (page) - default config matches the "page" view. 8 | // Note that this dialog will be destroyed on close. 9 | Ext.create({ 10 | xtype: 'officecreate', 11 | record: Ext.create('App.model.Office'), 12 | centered: true, 13 | floated: true, 14 | modal: true, 15 | ui: 'dialog', 16 | toolbar: { 17 | docked: 'bottom' 18 | } 19 | }).show(); 20 | } 21 | }); 22 | -------------------------------------------------------------------------------- /client/app/view/tablet/office/BrowseToolbar.js: -------------------------------------------------------------------------------- 1 | Ext.define('App.view.tablet.office.BrowseToolbar', { 2 | extend: 'App.view.widgets.BrowseToolbar', 3 | // xtype: 'officebrowsetoolbar', -- set by profile 4 | 5 | items: { 6 | countries: { 7 | xtype: 'combobox', 8 | valueField: 'value', 9 | displayField: 'label', 10 | placeholder: 'All Country', 11 | queryMode: 'local', 12 | weight: 10, 13 | bind: { 14 | selection: '{filters.country}', 15 | store: '{countries}' 16 | } 17 | }, 18 | create: { 19 | xtype: 'button', 20 | iconCls: 'x-fa fa-plus', 21 | handler: 'onCreate', 22 | text: 'Create', 23 | weight: 50 24 | } 25 | } 26 | }); 27 | -------------------------------------------------------------------------------- /client/app/view/tablet/organization/Browse.js: -------------------------------------------------------------------------------- 1 | Ext.define('App.view.tablet.organization.Browse', { 2 | extend: 'App.view.organization.Browse', 3 | // xtype: 'organizationbrowse', -- set by profile 4 | 5 | requires: [ 6 | 'Ext.plugin.ListPaging' 7 | ], 8 | 9 | controller: 'tablet-organizationbrowse', 10 | 11 | tbar: { 12 | xtype: 'organizationbrowsetoolbar' 13 | }, 14 | 15 | items: [{ 16 | xtype: 'grid', 17 | emptyText: 'No organization was found to match your search', 18 | bind: '{organizations}', 19 | ui: 'listing', 20 | 21 | selectable: { 22 | disabled: true 23 | }, 24 | 25 | plugins: [{ 26 | type: 'listpaging', 27 | autoPaging: true 28 | }], 29 | 30 | columns: [{ 31 | text: 'Name', 32 | dataIndex: 'name', 33 | flex: 2, 34 | cell: { 35 | encodeHtml: false 36 | }, 37 | tpl: '{name}' 38 | }, { 39 | text: 'Manager', 40 | dataIndex: 'manager.lastname', 41 | flex: 2, 42 | cell: { 43 | encodeHtml: false 44 | }, 45 | tpl: [ 46 | '', 47 | '
', 48 | '{fullname}', 49 | '
', 50 | '
', 51 | '{office.name}, ', 52 | '{office.city} ({office.country})', 53 | '
', 54 | '
' 55 | ] 56 | }, { 57 | text: 'Headcount', 58 | dataIndex: 'headcount', 59 | flex: 1, 60 | cell: { 61 | encodeHtml: false 62 | }, 63 | tpl: [ 64 | '', 65 | '{headcount:plural("employee")}', 66 | '' 67 | ] 68 | }], 69 | 70 | listeners: { 71 | childdoubletap: 'onChildActivate' 72 | } 73 | }] 74 | }); 75 | -------------------------------------------------------------------------------- /client/app/view/tablet/organization/BrowseController.js: -------------------------------------------------------------------------------- 1 | Ext.define('App.view.tablet.organization.BrowseController', { 2 | extend: 'App.view.organization.BrowseController', 3 | alias: 'controller.tablet-organizationbrowse', 4 | 5 | onCreate: function() { 6 | // The creation form can be accessed either by clicking the "create" button (dialog) 7 | // or via the #organization/create url (page) - default config matches the "page" 8 | // view. Note that this dialog will be destroyed on close. 9 | Ext.create({ 10 | xtype: 'organizationcreate', 11 | record: Ext.create('App.model.Organization'), 12 | centered: true, 13 | floated: true, 14 | modal: true, 15 | ui: 'dialog', 16 | toolbar: { 17 | docked: 'bottom' 18 | } 19 | }).show(); 20 | } 21 | }); 22 | -------------------------------------------------------------------------------- /client/app/view/tablet/organization/BrowseToolbar.js: -------------------------------------------------------------------------------- 1 | Ext.define('App.view.tablet.organization.BrowseToolbar', { 2 | extend: 'App.view.widgets.BrowseToolbar', 3 | // xtype: 'organizationbrowsetoolbar', -- set by profile 4 | 5 | items: { 6 | managers: { 7 | xtype: 'combobox', 8 | valueField: 'value', 9 | displayField: 'label', 10 | placeholder: 'All Managers', 11 | queryMode: 'local', 12 | weight: 10, 13 | bind: { 14 | selection: '{filters.manager}', 15 | store: '{managers}' 16 | } 17 | }, 18 | create: { 19 | xtype: 'button', 20 | iconCls: 'x-fa fa-plus', 21 | handler: 'onCreate', 22 | text: 'Create', 23 | weight: 50 24 | } 25 | } 26 | }); 27 | -------------------------------------------------------------------------------- /client/app/view/tablet/person/Browse.js: -------------------------------------------------------------------------------- 1 | Ext.define('App.view.tablet.person.Browse', { 2 | extend: 'App.view.person.Browse', 3 | // xtype: 'personbrowse', -- set by profile 4 | 5 | requires: [ 6 | 'Ext.plugin.ListPaging' 7 | ], 8 | 9 | controller: 'tablet-personbrowse', 10 | 11 | tbar: { 12 | xtype: 'personbrowsetoolbar' 13 | }, 14 | 15 | items: [{ 16 | xtype: 'grid', 17 | emptyText: 'No employee was found to match your search', 18 | bind: '{people}', 19 | ui: 'listing', 20 | 21 | selectable: { 22 | disabled: true 23 | }, 24 | 25 | plugins: [{ 26 | type: 'listpaging', 27 | autoPaging: true 28 | }], 29 | 30 | columnMenu: { 31 | items: { 32 | groupByThis: false, 33 | showInGroups: false 34 | } 35 | }, 36 | 37 | columns: [{ 38 | dataIndex: 'picture', 39 | menuDisabled: true, 40 | hideable: false, 41 | sortable: false, 42 | align: 'center', 43 | width: 58, 44 | cell: { 45 | encodeHtml: false 46 | }, 47 | tpl: '
' 48 | }, { 49 | text: 'Name / Title', 50 | dataIndex: 'lastname', 51 | flex: 1, 52 | cell: { 53 | encodeHtml: false 54 | }, 55 | tpl: [ 56 | '{fullname}', 57 | '
{title}
' 58 | ] 59 | }, { 60 | text: 'Organization', 61 | dataIndex: 'organization.name', 62 | flex: 1, 63 | cell: { 64 | encodeHtml: false 65 | }, 66 | tpl: [ 67 | '', 68 | '{name}', 69 | '
', 70 | 'Managed by {manager.fullname}', 71 | '
', 72 | '
' 73 | ] 74 | }, { 75 | text: 'Office', 76 | dataIndex: 'office.name', 77 | flex: 1, 78 | cell: { 79 | encodeHtml: false 80 | }, 81 | tpl: [ 82 | '', 83 | '{name}', 84 | '
{city}, {country}
', 85 | '
' 86 | ] 87 | }, { 88 | sortable: false, 89 | dataIndex: 'email', 90 | text: 'Email/Phone', 91 | flex: 1, 92 | cell: { 93 | encodeHtml: false 94 | }, 95 | tpl: [ 96 | '
{email}
', 97 | '
{phone}
' 98 | ] 99 | }], 100 | 101 | listeners: { 102 | childdoubletap: 'onChildActivate' 103 | } 104 | }] 105 | }); 106 | -------------------------------------------------------------------------------- /client/app/view/tablet/person/BrowseController.js: -------------------------------------------------------------------------------- 1 | Ext.define('App.view.tablet.person.BrowseController', { 2 | extend: 'App.view.person.BrowseController', 3 | alias: 'controller.tablet-personbrowse', 4 | 5 | onCreate: function() { 6 | // The creation form can be accessed either by clicking the "create" button (dialog) 7 | // or via the #person/create url (page) - default config matches the "page" view. 8 | // Note that this dialog will be destroyed on close. 9 | Ext.create({ 10 | xtype: 'personcreate', 11 | record: Ext.create('App.model.Person'), 12 | centered: true, 13 | floated: true, 14 | modal: true, 15 | ui: 'dialog', 16 | toolbar: { 17 | docked: 'bottom' 18 | } 19 | }).show(); 20 | } 21 | }); 22 | -------------------------------------------------------------------------------- /client/app/view/tablet/person/BrowseToolbar.js: -------------------------------------------------------------------------------- 1 | Ext.define('App.view.tablet.person.BrowseToolbar', { 2 | extend: 'App.view.widgets.BrowseToolbar', 3 | // xtype: 'personbrowsetoolbar', -- set by profile 4 | 5 | items: { 6 | organizations: { 7 | xtype: 'combobox', 8 | valueField: 'value', 9 | displayField: 'label', 10 | placeholder: 'All Organizations', 11 | queryMode: 'local', 12 | weight: 11, 13 | bind: { 14 | selection: '{filters.organization}', 15 | store: '{organizations}' 16 | } 17 | }, 18 | offices: { 19 | xtype: 'combobox', 20 | valueField: 'value', 21 | displayField: 'label', 22 | placeholder: 'All Offices', 23 | queryMode: 'local', 24 | weight: 10, 25 | bind: { 26 | selection: '{filters.office}', 27 | store: '{offices}' 28 | } 29 | }, 30 | create: { 31 | xtype: 'button', 32 | iconCls: 'x-fa fa-plus', 33 | handler: 'onCreate', 34 | text: 'Create', 35 | weight: 50 36 | } 37 | } 38 | }); 39 | -------------------------------------------------------------------------------- /client/app/view/viewport/ViewportModel.js: -------------------------------------------------------------------------------- 1 | Ext.define('App.view.viewport.ViewportModel', { 2 | extend: 'Ext.app.ViewModel', 3 | alias: 'viewmodel.viewport', 4 | 5 | data: { 6 | user: null 7 | } 8 | }); 9 | -------------------------------------------------------------------------------- /client/app/view/widgets/Browse.js: -------------------------------------------------------------------------------- 1 | Ext.define('App.view.widgets.Browse', { 2 | extend: 'Ext.Panel', 3 | xtype: 'browse', 4 | 5 | config: { 6 | route: null, 7 | store: null, 8 | fields: { 9 | search: { 10 | property: '#search', 11 | defaultValue: null 12 | } 13 | } 14 | }, 15 | 16 | eventedConfig: { 17 | /** 18 | * Make the config trigger an event on change to allow the controller to monitor it. 19 | * https://www.sencha.com/blog/using-sencha-ext-config/ 20 | */ 21 | route: null, 22 | store: null 23 | }, 24 | 25 | controller: 'browse', 26 | viewModel: { 27 | data: { 28 | filters: null 29 | } 30 | }, 31 | 32 | layout: 'fit', 33 | 34 | reset: function() { 35 | this.fireEvent('reset'); 36 | return this; 37 | } 38 | }); 39 | -------------------------------------------------------------------------------- /client/app/view/widgets/BrowseController.js: -------------------------------------------------------------------------------- 1 | Ext.define('App.view.widgets.BrowseController', { 2 | extend: 'Ext.app.ViewController', 3 | alias: 'controller.browse', 4 | 5 | control: { 6 | '#': { 7 | routechange: 'onRouteChange', 8 | storechange: 'onStoreChange' 9 | } 10 | }, 11 | 12 | initViewModel: function(vm) { 13 | vm.bind( 14 | { bindTo: '{filters}', deep: true }, 15 | Ext.Function.createBuffered(function() { 16 | if (!this.destroyed) { 17 | // The view might have been destroyed (e.g. user deauthentication) 18 | this.updateFilters() 19 | } 20 | }, 500, this, {})); 21 | }, 22 | 23 | updateFilters: function(reload) { 24 | var view = this.getView(), 25 | store = view.getStore(), 26 | collection = store && store.getFilters(), 27 | filters = this.getViewModel().get('filters'), 28 | fields = view.getFields(), 29 | dirty = !!reload, 30 | item, value; 31 | 32 | if (!collection) { 33 | return; 34 | } 35 | 36 | Ext.Object.each(fields, function(key, field) { 37 | value = filters[key]; 38 | if (value && value.isModel) { 39 | value = value.get('value'); 40 | } 41 | 42 | key = field.property || key; 43 | item = collection.get(key); 44 | if ((item && item.getValue()) == value) { 45 | return; 46 | } 47 | 48 | dirty = true; 49 | if (value == null) { 50 | store.removeFilter(key, true); 51 | } else { 52 | store.filter(key, value, true); 53 | } 54 | }); 55 | 56 | if (dirty) { 57 | store.removeAll(); 58 | store.load(); 59 | } 60 | }, 61 | 62 | onRouteChange: function(view, route) { 63 | var me = this, 64 | vm = me.getViewModel(), 65 | regex = /([^\/]+)\/([^\/]+)/g, 66 | fields = me.getView().getFields() || {}, 67 | filters = {}, 68 | field, value; 69 | 70 | Ext.Object.each(fields, function(key, value) { 71 | filters[key] = value.defaultValue || null 72 | }); 73 | 74 | while (match = regex.exec(route)) { 75 | field = match[1]; 76 | value = match[2]; 77 | if (Ext.isDefined(filters[field])) { 78 | filters[field] = field !== 'search'? 79 | Ext.create(App.model.Filter, { value: value }) : 80 | value; 81 | } 82 | } 83 | 84 | vm.set('filters', filters); 85 | me.updateFilters(); 86 | }, 87 | 88 | onStoreChange: function() { 89 | this.updateFilters(true); 90 | }, 91 | 92 | onChildActivate: function(dataview, location) { 93 | var record = location.record; 94 | if (record) { 95 | this.redirectTo(record); 96 | } 97 | }, 98 | 99 | onEditAction: function(list, data) { 100 | this.redirectTo(data.record.toEditUrl()); 101 | }, 102 | 103 | onRefreshTap: function() { 104 | var store = this.getView().getStore(); 105 | if (store) { 106 | store.reload(); 107 | } 108 | }, 109 | 110 | onClearFiltersTap: function() { 111 | this.getViewModel().set('filters', {}); 112 | } 113 | }); 114 | -------------------------------------------------------------------------------- /client/app/view/widgets/BrowseToolbar.js: -------------------------------------------------------------------------------- 1 | Ext.define('App.view.widgets.BrowseToolbar', { 2 | extend: 'Ext.Toolbar', 3 | xtype: 'personbrowsetoolbar', 4 | 5 | cls: 'browse-toolbar', 6 | weighted: true, 7 | ui: 'tools', 8 | 9 | defaults: { 10 | ui: 'action' 11 | }, 12 | 13 | items: { 14 | search: { 15 | xtype: 'searchfield', 16 | reference: 'search', 17 | placeholder: 'Search', 18 | userCls: 'expandable', 19 | bind: '{filters.search}', 20 | weight: 0 21 | }, 22 | refresh: { 23 | iconCls: 'x-fa fa-refresh', 24 | handler: 'onRefreshTap', 25 | tooltip: 'Refresh', 26 | weight: 30 27 | }, 28 | clear: { 29 | iconCls: 'x-fa fa-undo', 30 | handler: 'onClearFiltersTap', 31 | tooltip: 'Clear Filters', 32 | weight: 20 33 | } 34 | } 35 | }); 36 | -------------------------------------------------------------------------------- /client/app/view/widgets/HistoryPanel.js: -------------------------------------------------------------------------------- 1 | Ext.define('App.view.widgets.HistoryPanel', { 2 | extend: 'Ext.Panel', 3 | xtype: 'historypanel', 4 | 5 | config: { 6 | store: null 7 | }, 8 | 9 | cls: 'historypanel', 10 | defaultBindProperty: 'store', 11 | referenceHolder: true, 12 | title: 'Recent Activity', 13 | 14 | header: { 15 | items: { 16 | showall: { 17 | xtype: 'button', 18 | reference: 'showallbutton', 19 | tooltip: 'Show all activity', 20 | handler: 'onHistoryAllTap', 21 | iconCls: 'x-fa fa-history', 22 | ui: 'block' 23 | } 24 | } 25 | }, 26 | 27 | items: [{ 28 | xtype: 'historyview', 29 | reference: 'historyview', 30 | displayField: 'subject', 31 | emptyText: 'No activity was found', 32 | selectable: { 33 | disabled: true 34 | } 35 | }], 36 | 37 | initialize: function() { 38 | var me = this; 39 | me.callParent(arguments); 40 | me.relayEvents(me.lookup('historyview'), ['childtap']); 41 | }, 42 | 43 | applyStore: function(value) { 44 | return value? Ext.getStore(value) : null; 45 | }, 46 | 47 | updateStore: function(curr, prev) { 48 | var listeners = { 49 | datachanged: 'updateButtonState', 50 | scope: this 51 | }; 52 | 53 | if (prev && prev.isStore) { 54 | prev.un(listeners); 55 | } 56 | if (curr && curr.isStore) { 57 | curr.on(listeners); 58 | } 59 | 60 | this.lookup('historyview').setStore(curr); 61 | this.updateButtonState(curr); 62 | }, 63 | 64 | updateButtonState: function(store) { 65 | this.lookup('showallbutton').setDisabled(!store || !store.getCount()); 66 | } 67 | }); 68 | -------------------------------------------------------------------------------- /client/app/view/widgets/HistoryView.js: -------------------------------------------------------------------------------- 1 | Ext.define('App.view.widgets.HistoryView', { 2 | extend: 'Ext.dataview.DataView', 3 | xtype: 'historyview', 4 | 5 | config: { 6 | displayField: 'recipient.fullname' 7 | }, 8 | 9 | cls: 'historyview', 10 | ui: 'history light', 11 | emptyText: 'No history', 12 | deferEmptyText: false, 13 | minHeight: 80, 14 | inline: true, 15 | 16 | 17 | updateDisplayField: function(value) { 18 | this.setItemTpl([ 19 | '
', 20 | '
', 21 | '', 22 | '
', 23 | '
', 24 | '
', 25 | '
{', value, '}
', 26 | '
{created:date(\'F j, Y\')}
', 27 | '
', 28 | '
' 29 | ]); 30 | }, 31 | 32 | itemCls: 'history-item' 33 | }); 34 | -------------------------------------------------------------------------------- /client/app/view/widgets/HistoryView.scss: -------------------------------------------------------------------------------- 1 | $historyitem-badge-size: dynamic(22px); 2 | 3 | @include dataview-ui( 4 | $ui: 'history', 5 | $background-color: $neutral-light-color, 6 | $item-background-color: $neutral-light-color, 7 | $item-padding: 5px 15px, 8 | $item-padding-big: 5px 15px 9 | ); 10 | 11 | .history-visual { 12 | white-space: nowrap; 13 | 14 | .action, .picture { 15 | display: inline-block; 16 | position: relative; 17 | vertical-align: middle; 18 | } 19 | 20 | .action { 21 | border-radius: 50%; 22 | font-size: 14px; 23 | height: $historyitem-badge-size; 24 | width: $historyitem-badge-size; 25 | line-height: $historyitem-badge-size; 26 | text-align: center; 27 | z-index: 1; 28 | } 29 | 30 | .picture { 31 | margin-left: -$historyitem-badge-size*0.4; 32 | z-index: 0; 33 | } 34 | } 35 | 36 | .historyview { 37 | .history-item { 38 | cursor: pointer; 39 | 40 | .tablet-profile & { 41 | width: 100%; 42 | 43 | @media screen and (max-width: 600px) { 44 | width: 50%; 45 | } 46 | @media screen and (max-width: 400px) { 47 | width: 100%; 48 | } 49 | } 50 | 51 | .phone-profile.x-portrait & { 52 | width: 100%; 53 | } 54 | 55 | .phone-profile.x-landscape & { 56 | width: 50%; 57 | } 58 | } 59 | 60 | .history-item-wrapper { 61 | align-items: center; 62 | display: flex; 63 | flex-direction: row; 64 | } 65 | 66 | .history-details { 67 | padding: 8px; 68 | line-height: 1.4; 69 | flex: 1; 70 | width: 0; 71 | 72 | > * { 73 | @include ellipsis; 74 | } 75 | 76 | .display { 77 | display: block; 78 | font-size: 15px; 79 | font-weight: 600; 80 | } 81 | 82 | .date { 83 | font-size: 13px; 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /client/app/view/widgets/MapView.js: -------------------------------------------------------------------------------- 1 | Ext.define('App.view.widgets.MapView', { 2 | extend: 'Ext.ux.google.Map', 3 | xtype: 'mapview', 4 | 5 | cls: 'mapview', 6 | 7 | markerTemplate: { 8 | title: '{name}', 9 | animation: 'DROP', 10 | position: { 11 | lat: '{location.latitude}', 12 | lng: '{location.longitude}' 13 | } 14 | }, 15 | 16 | // https://developers.google.com/maps/documentation/javascript/reference#MapOptions 17 | mapOptions: { 18 | disableDoubleClickZoom: true, 19 | disableDefaultUI: true, 20 | scrollwheel: false, 21 | zoom: 8, 22 | 23 | styles: [{ 24 | featureType: "all", 25 | elementType: "all", 26 | stylers: [ 27 | { visibility: "simplified" } 28 | ] 29 | }, { 30 | featureType: "administrative", 31 | elementType: "all", 32 | stylers: [ 33 | { visibility: "on"}, 34 | { lightness: 33 } 35 | ] 36 | }, { 37 | featureType: "landscape", 38 | elementType: "all", 39 | stylers: [ 40 | { color: "#eaeaea" } 41 | ] 42 | }, { 43 | featureType: "poi.park", 44 | elementType: "geometry", 45 | stylers: [ 46 | { color: "#c5dac6" } 47 | ] 48 | }, { 49 | featureType: "poi.park", 50 | elementType: "labels", 51 | stylers: [ 52 | { visibility: "off" } 53 | ] 54 | }, { 55 | featureType: "road", 56 | elementType: "geometry", 57 | stylers: [ 58 | { hue: "#bbc0c4" }, 59 | { saturation: -93 }, 60 | { lightness: 20 } 61 | ] 62 | }, { 63 | featureType: "water", 64 | elementType: "all", 65 | stylers: [ 66 | { visibility: "on" }, 67 | { color: "#acbcc9" } 68 | ] 69 | }] 70 | }, 71 | 72 | updateMarkers: function(current, previous) { 73 | var me = this, 74 | listeners = { 75 | refresh: 'onStoreRefresh', 76 | scope: me 77 | }; 78 | 79 | me.callParent(arguments); 80 | 81 | if (previous) { 82 | previous.un(listeners); 83 | } 84 | 85 | if (current) { 86 | current.on(listeners); 87 | me.onStoreRefresh(current); 88 | } 89 | }, 90 | 91 | onStoreRefresh: function(store) { 92 | var records = store.getRange(); 93 | if (records.length === 1) { 94 | this.setMapCenter(records[0]); 95 | } else { 96 | this.fitMarkersInView(records); 97 | } 98 | } 99 | }); 100 | -------------------------------------------------------------------------------- /client/app/view/widgets/MapView.scss: -------------------------------------------------------------------------------- 1 | .mapview { 2 | height: 192px; 3 | } 4 | -------------------------------------------------------------------------------- /client/app/view/widgets/Show.js: -------------------------------------------------------------------------------- 1 | Ext.define('App.view.widgets.Show', { 2 | extend: 'Ext.Panel', 3 | 4 | controller: { 5 | type: 'wizard' 6 | }, 7 | 8 | viewModel: { 9 | data: { 10 | record: null 11 | } 12 | }, 13 | 14 | eventedConfig: { 15 | /** 16 | * Make the config trigger an event on change to allow the controller to monitor it. 17 | * https://www.sencha.com/blog/using-sencha-ext-config/ 18 | */ 19 | record: null 20 | }, 21 | 22 | platformConfig: { 23 | phone: { 24 | header: { 25 | items: { 26 | edit: { 27 | xtype: 'button', 28 | iconCls: 'x-fa fa-pencil', 29 | handler: 'onEditTap', 30 | weight: 10 31 | } 32 | } 33 | } 34 | }, 35 | 36 | '!phone': { 37 | header: { 38 | hidden: true 39 | } 40 | } 41 | }, 42 | 43 | scrollable: { 44 | y: 'scroll' 45 | }, 46 | 47 | weighted: true, 48 | 49 | defaults: { 50 | userCls: 'page-constrained' 51 | }, 52 | 53 | items: { 54 | header: { 55 | xtype: 'showheader', 56 | weight: -10 57 | }, 58 | 59 | content: { 60 | weighted: true, 61 | userCls: [ 62 | 'page-constrained', 63 | 'blocks' 64 | ], 65 | 66 | defaults: { 67 | userCls: 'blocks-column', 68 | weighted: true, 69 | 70 | defaults: { 71 | ui: 'block' 72 | } 73 | }, 74 | 75 | items: { 76 | left: { 77 | weighted: true 78 | }, 79 | 80 | right: { 81 | weighted: true, 82 | 83 | items: { 84 | history: { 85 | xtype: 'historypanel', 86 | bind: '{history}', 87 | ui: 'block', 88 | listeners: { 89 | 'childtap': 'onHistoryChildTap' 90 | } 91 | } 92 | } 93 | } 94 | } 95 | } 96 | } 97 | }); 98 | -------------------------------------------------------------------------------- /client/app/view/widgets/Show.scss: -------------------------------------------------------------------------------- 1 | $show-header-padding: dynamic(8px); 2 | $show-header-spacing: dynamic(16px); 3 | $show-header-picture-size: dynamic(150px); 4 | -------------------------------------------------------------------------------- /client/app/view/widgets/ShowController.js: -------------------------------------------------------------------------------- 1 | Ext.define('App.view.widgets.ShowController', { 2 | extend: 'Ext.app.ViewController', 3 | alias: 'controller.show', 4 | 5 | control: { 6 | '#': { 7 | recordchange: 'onRecordChange' 8 | } 9 | }, 10 | 11 | getRecord: function() { 12 | return this.getViewModel().get('record'); 13 | }, 14 | 15 | onRecordChange: function(view, record) { 16 | this.getViewModel().set('record', record); 17 | 18 | // Scroll to the top of the view but make sure that the view is still 19 | // valid since the record is reset to null when the view is destroyed. 20 | if (!view.destroying && !view.destroyed) { 21 | view.getScrollable().scrollTo(null, 0, true); 22 | } 23 | }, 24 | 25 | onEditTap: function() { 26 | this.redirectTo(this.getRecord().toEditUrl()); 27 | }, 28 | 29 | onPeopleChildTap: function(view, location) { 30 | var record = location.record; 31 | if (record) { 32 | this.redirectTo(record); 33 | } 34 | }, 35 | 36 | onHistoryChildTap: function(view, location) { 37 | var record = location.record; 38 | if (record) { 39 | this.redirectTo(record.getRecipient()); 40 | } 41 | }, 42 | 43 | onHistoryAllTap: function() { 44 | this.redirectTo('history/recipient/' + this.getRecord().getId()); 45 | } 46 | }); 47 | -------------------------------------------------------------------------------- /client/app/view/widgets/ShowHeader.js: -------------------------------------------------------------------------------- 1 | Ext.define('App.view.widgets.ShowHeader', { 2 | extend: 'Ext.Container', 3 | xtype: 'showheader', 4 | 5 | cls: 'show-header', 6 | weighted: true, 7 | 8 | layout: { 9 | type: 'hbox', 10 | align: 'end' 11 | }, 12 | 13 | items: { 14 | title: { 15 | xtype: 'component', 16 | userCls: 'header-title', 17 | flex: 1, 18 | bind: { 19 | record: '{record}' 20 | } 21 | }, 22 | 23 | edit: { 24 | xtype: 'button', 25 | iconCls: 'x-fa fa-pencil', 26 | handler: 'onEditTap', 27 | text: 'Edit', 28 | weight: 10, 29 | ui: 'flat', 30 | 31 | platformConfig: { 32 | phone: { 33 | hidden: true 34 | } 35 | } 36 | } 37 | } 38 | }); 39 | -------------------------------------------------------------------------------- /client/app/view/widgets/ShowHeader.scss: -------------------------------------------------------------------------------- 1 | .show-header { 2 | @include single-box-shadow; 3 | min-height: 128px; 4 | position: relative; 5 | z-index: 1; 6 | 7 | > .x-body-el { 8 | padding: 16px 24px; 9 | } 10 | 11 | .header-title { 12 | line-height: 1.4; 13 | padding: 6px; 14 | 15 | .icon { 16 | border-right: 1px solid; 17 | color: $base-color; 18 | float: left; 19 | font-size: 38px; 20 | margin-right: 8px; 21 | padding-right: 8px; 22 | } 23 | 24 | .name, 25 | .desc { 26 | white-space: nowrap; 27 | } 28 | 29 | .name { 30 | color: $base-color; 31 | font-size: 24px; 32 | font-weight: 400; 33 | } 34 | 35 | .desc { 36 | font-size: 16px; 37 | } 38 | } 39 | } 40 | 41 | -------------------------------------------------------------------------------- /client/app/view/widgets/Sidebar.js: -------------------------------------------------------------------------------- 1 | Ext.define('App.view.widgets.Sidebar', { 2 | extend: 'Ext.Container', 3 | xtype: 'sidebar', 4 | 5 | config: { 6 | expanded: false 7 | }, 8 | 9 | classCls: 'sidebar', 10 | 11 | initialize: function() { 12 | var me = this; 13 | 14 | me.callParent(); 15 | 16 | me.el.insertFirst({ 17 | cls: me.getBaseCls() + '-mask', 18 | tag: 'div' 19 | }).on({ 20 | tap: 'onMaskTap', 21 | scope: me 22 | }); 23 | }, 24 | 25 | updateExpanded: function(value) { 26 | this.toggleCls('expanded', value); 27 | }, 28 | 29 | updateMode: function(curr, prev) { 30 | this.replaceCls(prev, curr); 31 | }, 32 | 33 | toggleExpanded: function() { 34 | this.setExpanded(!this.getExpanded()); 35 | }, 36 | 37 | onMaskTap: function(ev) { 38 | this.setExpanded(false); 39 | ev.preventDefault(); 40 | } 41 | }); 42 | -------------------------------------------------------------------------------- /client/app/view/widgets/Sidebar.scss: -------------------------------------------------------------------------------- 1 | $sidebar-background-color: dynamic($neutral-dark-color); 2 | $sidebar-color: dynamic($neutral-light-color); 3 | $sidebar-font-size: dynamic(14px); 4 | $sidebar-font-size-big: dynamic(14px); 5 | $sidebar-item-padding: dynamic(16px); 6 | $sidebar-item-padding-big: dynamic(18px); 7 | $sidebar-icon-size: dynamic(21px); 8 | $sidebar-icon-size-big: dynamic(21px); 9 | $sidebar-icon-horizontal-spacing: dynamic($sidebar-item-padding); 10 | $sidebar-icon-horizontal-spacing-big: dynamic($sidebar-item-padding-big); 11 | $sidebar-slide-width: dynamic(75vw); 12 | $sidebar-micro-width: dynamic($sidebar-icon-size + $sidebar-item-padding*2); 13 | $sidebar-micro-expanded-width: dynamic(256px); 14 | 15 | .sidebar { 16 | $sidebar-picture-size: $sidebar-icon-size*1.6; 17 | $sidebar-picture-size-big: $sidebar-icon-size-big*1.8; 18 | $sidebar-picture-spacing: (2*$sidebar-item-padding + $sidebar-icon-size - $sidebar-picture-size)/2; 19 | $sidebar-picture-spacing-big: (2*$sidebar-item-padding-big + $sidebar-icon-size-big - $sidebar-picture-size-big)/2; 20 | 21 | background-color: $sidebar-background-color; 22 | color: $sidebar-color; 23 | overflow: visible; 24 | 25 | > .x-dock { 26 | overflow: visible; 27 | } 28 | 29 | @include button-ui( 30 | $ui: 'large', 31 | $icon-horizontal-spacing: $sidebar-icon-horizontal-spacing, 32 | $icon-horizontal-spacing-big: $sidebar-icon-horizontal-spacing-big 33 | ); 34 | 35 | @include button-ui( 36 | $ui: 'picture', 37 | $icon-horizontal-spacing: $sidebar-picture-spacing, 38 | $icon-horizontal-spacing-big: $sidebar-picture-spacing-big, 39 | $icon-size: $sidebar-picture-size, 40 | $icon-size-big: $sidebar-picture-size-big, 41 | $icon-font-size: $sidebar-picture-size, 42 | $icon-font-size-big: $sidebar-picture-size-big, 43 | $padding: $sidebar-picture-spacing, 44 | $padding-big: $sidebar-picture-spacing-big 45 | ); 46 | } 47 | 48 | .sidebar-body-el { 49 | z-index: 2; 50 | } 51 | 52 | .sidebar-micro { 53 | width: $sidebar-micro-width; 54 | } 55 | 56 | .sidebar-micro-body-el { 57 | @include transition-property(width); 58 | @include transition-duration(0.25s); 59 | width: $sidebar-micro-width; 60 | 61 | > div { 62 | width: $sidebar-micro-expanded-width; 63 | } 64 | 65 | .expanded > .x-dock > & { 66 | width: $sidebar-micro-expanded-width; 67 | } 68 | } 69 | 70 | .sidebar-slide { 71 | width: 0; 72 | } 73 | 74 | .sidebar-slide-body-el { 75 | @include transition-property(margin-left); 76 | @include transition-duration(0.25s); 77 | margin-top: $sidebar-icon-size-big + $sidebar-item-padding-big * 2; 78 | margin-left: -$sidebar-slide-width; 79 | width: $sidebar-slide-width; 80 | 81 | .expanded > & { 82 | margin-left: 0; 83 | } 84 | } 85 | 86 | .sidebar-mask { 87 | background: black; 88 | display: none; 89 | opacity: .005; 90 | position: fixed; 91 | top: 0; 92 | left: 0; 93 | bottom: 0; 94 | right: 0; 95 | z-index: 1; 96 | 97 | .sidebar.expanded > & { 98 | display: block; 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /client/app/view/widgets/Wizard.scss: -------------------------------------------------------------------------------- 1 | .wizard-screen { 2 | align-items: center; 3 | 4 | > .x-body-el { 5 | max-width: $form-max-width; 6 | width: 100%; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /client/app/view/widgets/WizardController.js: -------------------------------------------------------------------------------- 1 | Ext.define('App.view.widgets.WizardController', { 2 | extend: 'Ext.app.ViewController', 3 | alias: 'controller.wizard', 4 | 5 | requires: [ 6 | 'Ext.History' 7 | ], 8 | 9 | getItemCount: function(tabs) { 10 | return tabs.getInnerItems().length; 11 | }, 12 | 13 | getActiveIndex: function(tabs) { 14 | return tabs.getInnerItems().indexOf(tabs.getActiveItem()); 15 | }, 16 | 17 | advance: function(increment) { 18 | var me = this, 19 | tabs = me.lookup('tabs'), 20 | index = me.getActiveIndex(tabs), 21 | count = me.getItemCount(tabs), 22 | next = index + increment; 23 | 24 | tabs.setActiveItem(Math.max(0, Math.min(count-1, next))); 25 | }, 26 | 27 | resync: function() { 28 | var me = this, 29 | vm = me.getViewModel(), 30 | tabs = me.lookup('tabs'), 31 | prev = me.lookup('prev'), 32 | next = me.lookup('next'), 33 | index = me.getActiveIndex(tabs), 34 | count = me.getItemCount(tabs), 35 | single = count < 2; 36 | 37 | tabs.getTabBar().setHidden(single); 38 | prev.setDisabled(index <= 0).setHidden(single); 39 | next.setDisabled(index == -1 || index >= count-1).setHidden(single); 40 | }, 41 | 42 | finalize: function() { 43 | var view = this.getView(); 44 | if (view.getFloated()) { 45 | view.close(); 46 | } else { 47 | Ext.History.back(); 48 | } 49 | }, 50 | 51 | onSubmitTap: function() { 52 | var me = this, 53 | form = me.getView(), 54 | record = me.getViewModel().get('record'); 55 | 56 | if (!form.validate()) { 57 | return; 58 | } 59 | 60 | if (!record.isDirty()) { 61 | me.finalize(); 62 | return; 63 | } 64 | 65 | form.setMasked({ xtype: 'loadmask' }); 66 | form.clearErrors(); 67 | record.save({ 68 | callback: function(result, operation) { 69 | form.setMasked(false); 70 | if (!App.util.Errors.process(operation, form)) { 71 | me.finalize(); 72 | } 73 | } 74 | }); 75 | }, 76 | 77 | onCancelTap: function() { 78 | this.finalize(); 79 | }, 80 | 81 | onPrevTap: function() { 82 | this.advance(-1); 83 | }, 84 | 85 | onNextTap: function() { 86 | this.advance(1); 87 | }, 88 | 89 | onScreenAdd: function() { 90 | this.resync(); 91 | }, 92 | 93 | onScreenRemove: function(tabs) { 94 | if (!tabs.destroying) { 95 | this.resync(); 96 | } 97 | }, 98 | 99 | onScreenActivate: function(tabs) { 100 | // This event is triggered when the view is being destroyed! 101 | if (!tabs.destroying) { 102 | this.resync(); 103 | } 104 | } 105 | }); 106 | -------------------------------------------------------------------------------- /client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Coworkee | Ext JS Example 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /client/overrides/util/Format.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Exposes new template modifiers (e.g. '{birthday:dateDiff(date, "y")}', etc.) 3 | */ 4 | Ext.define('App.overrides.util.Format', { 5 | override: 'Ext.util.Format', 6 | 7 | dateDiff: function(v0, v1, unit) { 8 | var seconds, name, diff; 9 | 10 | if (!unit || unit == 'auto') { 11 | seconds = Math.floor((+v1 - v0)/1000); 12 | unit = 13 | seconds < 1 ? 'ms' : // 1 second 14 | seconds < 60 ? 's' : // 1 minute 15 | seconds < 3600 ? 'mi' : // 60 minutes 16 | seconds < 86400 ? 'h' : // 24 hours 17 | seconds < 604800 ? 'd' : // 7 days 18 | seconds < 2419200 ? 'w' : // 4 weeks 19 | seconds < 31622400 ? 'mo' : // 366 days 20 | 'y'; 21 | } 22 | 23 | switch (unit) { 24 | case 'ms': name = 'millisecond'; break; 25 | case 's': name = 'second'; break; 26 | case 'mi': name = 'minute'; break; 27 | case 'h': name = 'hour'; break; 28 | case 'd': name = 'day'; break; 29 | case 'w': name = 'week'; break; 30 | case 'mo': name = 'month'; break; 31 | case 'y': name = 'year'; break; 32 | default: 33 | } 34 | 35 | diff = Ext.Date.diff(v0, v1, unit); 36 | return Ext.util.Format.plural(diff, name); 37 | }, 38 | 39 | actionIconCls: function(type) { 40 | switch (type) { 41 | case 'profile': type = 'user'; break; 42 | case 'email': type = 'envelope'; break; 43 | default: 44 | } 45 | 46 | return 'x-fa fa-' + type; 47 | } 48 | }); 49 | -------------------------------------------------------------------------------- /client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "coworkee", 3 | "product": "ext", 4 | "version": "0.0.1", 5 | "description": "Coworkee Demo Application", 6 | "scripts": { 7 | "clean": "rimraf build", 8 | "start": "webpack-dev-server --env.environment=development", 9 | "production": "npm run clean && webpack --env.environment=production --env.treeshake=true" 10 | }, 11 | "extbuild": { 12 | "defaultenvironment": "development", 13 | "defaultverbose": "no" 14 | }, 15 | "dependencies": { 16 | "@sencha/ext-modern": "^7.0.0", 17 | "@sencha/ext-google": "^7.0.0", 18 | "@sencha/ext-modern-theme-material": "^7.0.0", 19 | "@sencha/ext-modern-theme-triton": "^7.0.0", 20 | "@sencha/ext": "^7.0.0", 21 | "escape-string-regexp": "^1.0.5", 22 | "express": "^4.14.0", 23 | "minimist": "^1.2.0", 24 | "sw-precache": "^4.1.0" 25 | }, 26 | "devDependencies": { 27 | "gulp": "^3.9.1", 28 | "@sencha/ext-webpack-plugin": "~7.0.0", 29 | "command-line-args": "^5.0.2", 30 | "cross-env": "^5.2.0", 31 | "portfinder": "^1.0.18", 32 | "react": "16.6.3", 33 | "react-hot-loader": "4.3.12", 34 | "html-webpack-plugin": "^3.2.0", 35 | "webpack": "^4.21.0", 36 | "webpack-cli": "^3.1.2", 37 | "webpack-dev-server": "^3.1.9" 38 | }, 39 | "repository": { 40 | "type": "git", 41 | "url": "" 42 | }, 43 | "keywords": [ 44 | "Ext JS", 45 | "Sencha", 46 | "HTML5" 47 | ], 48 | "author": "Sencha, Inc.", 49 | "license": "ISC", 50 | "bugs": { 51 | "url": "https://github.com/" 52 | }, 53 | "homepage": "http://www.sencha.com" 54 | } 55 | -------------------------------------------------------------------------------- /client/packages/local/coworkee/overrides/LoadMask.js: -------------------------------------------------------------------------------- 1 | Ext.define('App.overrides.LoadMask', { 2 | override: 'Ext.LoadMask', 3 | 4 | config: { 5 | message: '' 6 | } 7 | }); 8 | -------------------------------------------------------------------------------- /client/packages/local/coworkee/overrides/dataview/listswiper/ListSwiper.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Since all list swiping gestures of the app should provide the same user experiences, let's 3 | * define some common config in this override (e.g. "stepper" behavior, direction lock, etc.) 4 | */ 5 | Ext.define('App.override.dataview.listswiper.ListSwiper', { 6 | override: 'Ext.dataview.listswiper.ListSwiper', 7 | 8 | config: { 9 | directionLock: false, 10 | 11 | widget: { 12 | xtype: 'listswiperstepper', 13 | undo: { 14 | iconCls: 'x-fa fa-undo' 15 | } 16 | } 17 | } 18 | }); 19 | -------------------------------------------------------------------------------- /client/packages/local/coworkee/overrides/field/Field.js: -------------------------------------------------------------------------------- 1 | Ext.define('App.override.field.Field', { 2 | override: 'Ext.field.Field', 3 | 4 | config: { 5 | requiredMessage: 'This field is required', 6 | 7 | labelTextAlign: 'right', 8 | 9 | errorTip: { 10 | anchor: true, 11 | align: 'l-r?', 12 | ui: 'tooltip invalid' 13 | } 14 | }, 15 | 16 | platformConfig: { 17 | phone: { 18 | errorTarget: 'under' 19 | } 20 | } 21 | }); 22 | -------------------------------------------------------------------------------- /client/packages/local/coworkee/overrides/init.js: -------------------------------------------------------------------------------- 1 | Ext.namespace('Ext.theme.is')['coworkee'] = true; 2 | Ext.theme.name = 'coworkee'; 3 | -------------------------------------------------------------------------------- /client/packages/local/coworkee/sass/etc/all.scss: -------------------------------------------------------------------------------- 1 | /* 2 | * This is is imported by virtue of "sass.etc" in "app.json". 3 | */ 4 | @function contrasted($color, $percent: 50%) { 5 | @if $percent < 0 { 6 | $dark: lighten($color, -$percent); 7 | $light: darken($color, -$percent); 8 | } @else { 9 | $dark: darken($color, $percent); 10 | $light: lighten($color, $percent); 11 | } 12 | 13 | @return if(brightness($color) > 50, $dark, $light); 14 | } 15 | -------------------------------------------------------------------------------- /client/packages/local/coworkee/sass/src/Button.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * This file contains Ext.Button visual themes (ui) 3 | * http://docs.sencha.com/extjs/latest/modern/Ext.Button.html#sass-mixins 4 | */ 5 | 6 | .x-button .x-text-el { 7 | .title { 8 | text-transform: uppercase; 9 | } 10 | .value { 11 | opacity: 0.5; 12 | font-size: 13px; 13 | } 14 | } 15 | 16 | @include button-ui( 17 | $ui: 'block', 18 | $border-radius: 0, 19 | $border-width: 0, 20 | $icon-size: 28px, 21 | $icon-size-big: 28px, 22 | $icon-font-size: 20px, 23 | $icon-font-size-big: 20px, 24 | $font-size: 18px, 25 | $font-size-big: 18px, 26 | $font-weight: bold, 27 | $line-height: 1.4, 28 | $line-height-big: 1.4, 29 | $icon-only-padding: 16px, 30 | $icon-only-padding-big: 16px, 31 | $padding: 16px, 32 | $padding-big: 16px 33 | ); 34 | 35 | @include button-ui( 36 | $ui: 'action', 37 | $border-radius: 0, 38 | $border-width: 0 39 | ); 40 | 41 | @include button-ui( 42 | $ui: 'large', 43 | $font-size: 14px, 44 | $font-size-big: 14px, 45 | $icon-size: 21px, 46 | $icon-size-big: 21px, 47 | $icon-font-size: 21px, 48 | $icon-font-size-big: 21px, 49 | $icon-only-padding: 16px, 50 | $icon-only-padding-big: 18px, 51 | $padding: 16px, 52 | $padding-big: 18px 53 | ); 54 | 55 | @include button-ui( 56 | $ui: 'flat', 57 | $background-color: rgba(white, 0), 58 | $color: $neutral-medium-dark-color, 59 | $pressed-color: $base-color 60 | ); 61 | 62 | @include button-ui( 63 | $ui: 'dark', 64 | $color: $neutral-light-color, 65 | $pressed-color: $neutral-light-color, 66 | $background-color: $neutral-dark-color, 67 | $pressed-background-color: $base-color 68 | ); 69 | 70 | @include button-ui( 71 | $ui: 'segmented', 72 | $border-color: transparent, 73 | $border-style: solid, 74 | $border-width: 0 0 2px 0, 75 | $hovered-border-color: $neutral-light-color, 76 | $pressed-border-color: $base-color, 77 | $icon-only-padding: $tab-padding, 78 | $icon-only-padding-big: $tab-padding-big, 79 | $padding: $tab-padding, 80 | $padding-big: $tab-padding-big 81 | ); 82 | -------------------------------------------------------------------------------- /client/packages/local/coworkee/sass/src/Mask.scss: -------------------------------------------------------------------------------- 1 | .x-mask { 2 | &.x-has-message { 3 | .x-loading-spinner-outer { 4 | height: auto; 5 | } 6 | } 7 | 8 | &.x-loading-mask { 9 | .x-mask-inner { 10 | background-color: transparent; 11 | } 12 | } 13 | } 14 | 15 | @include st-loading-spinner(26px, $loading-spinner-color, 4px, 2px); 16 | -------------------------------------------------------------------------------- /client/packages/local/coworkee/sass/src/Panel.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * This file contains Ext.Panel/Header/Title visual themes (ui) 3 | * http://docs.sencha.com/extjs/latest/modern/Ext.Panel.html#sass-mixins 4 | */ 5 | 6 | @include panel-ui( 7 | $ui: 'block', 8 | $body-background-color: $neutral-light-color, 9 | $header-background-color: $neutral-light-color, 10 | $header-color: $neutral-medium-dark-color, 11 | $header-icon-size: 34px, 12 | $header-icon-size-big: 38px, 13 | $header-icon-font-size: 18px, 14 | $header-icon-font-size-big: 20px, 15 | $header-font-size: 18px, 16 | $header-font-size-big: 20px, 17 | $header-line-height: 1.2, 18 | $header-line-height-big: 1.2 19 | ); 20 | 21 | .x-paneltitle-block { 22 | .x-icon-el { 23 | background-color: $base-color; 24 | color: contrasted($base-light-color, -25%); 25 | border-radius: 50%; 26 | } 27 | } 28 | 29 | @include panel-ui( 30 | $ui: 'dialog', 31 | $body-background-color: $background-color, 32 | $header-background-color: $background-color, 33 | $header-border-width: 0, 34 | $header-icon-size: 24px, 35 | $header-icon-size-big: 24px, 36 | $header-font-size: 18px, 37 | $header-font-size-big: 18px, 38 | $header-line-height: 2, 39 | $header-line-height-big: 2, 40 | $header-title-padding: 16px 20px, 41 | $header-title-padding-big: 20px 24px 42 | ); 43 | 44 | @include panel-ui( 45 | $ui: 'invalid', 46 | $anchor-margin: 0, 47 | $anchor-height: 8px, 48 | $anchor-width: 12px, 49 | $border-color: $invalid-base-color, 50 | $border-width: 1px, 51 | $border-style: none, 52 | $body-background-color: $invalid-base-color, 53 | $body-border-style: none, 54 | $body-border-width: 0, 55 | $body-color: $invalid-text-color 56 | ); 57 | 58 | @include panel-ui( 59 | $ui: 'flat', 60 | $body-background-color: transparent, 61 | $header-background-color: transparent 62 | ); 63 | 64 | @include panel-ui( 65 | $ui: 'dark', 66 | $header-background-color: $neutral-dark-color, 67 | $header-color: $neutral-light-color 68 | ); 69 | 70 | -------------------------------------------------------------------------------- /client/packages/local/coworkee/sass/src/Toolbar.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * This file contains Ext.Toolbar visual themes (ui) 3 | * http://docs.sencha.com/extjs/latest/modern/Ext.Toolbar.html#sass-mixins 4 | */ 5 | 6 | @include toolbar-ui( 7 | $ui: 'tools', 8 | $background-color: $neutral-light-color, 9 | $box-shadow: 0 4px 2px -3px rgba(black, 0.2) inset 10 | ); 11 | 12 | @include toolbar-ui( 13 | $ui: 'flat', 14 | $background-color: transparent 15 | ); 16 | -------------------------------------------------------------------------------- /client/packages/local/coworkee/sass/src/dataview/DataView.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * This file contains Ext.dataview.DataView visual themes (ui) 3 | * http://docs.sencha.com/extjs/latest/modern/Ext.dataview.DataView.html#sass-mixins 4 | */ 5 | 6 | @include dataview-ui( 7 | $ui: 'light', 8 | $item-color: $neutral-dark-color, 9 | $item-hovered-color: contrasted($neutral-dark-color, -5%), 10 | $item-selected-color: contrasted($neutral-dark-color, -20%), 11 | $item-pressed-color: contrasted($neutral-dark-color, -10%), 12 | $item-background-color: $neutral-light-color, 13 | $item-alt-background-color: contrasted($neutral-light-color, -5%), 14 | $item-hovered-background-color: contrasted($neutral-light-color, 5%), 15 | $item-selected-background-color: $base-color, 16 | $item-pressed-background-color: contrasted($neutral-light-color, 10%) 17 | ); 18 | 19 | @include dataview-ui( 20 | $ui: 'dark', 21 | $background-color: $neutral-dark-color, 22 | $item-color: $neutral-light-color, 23 | $item-hovered-color: contrasted($neutral-light-color, -5%), 24 | $item-selected-color: contrasted($neutral-light-color, -20%), 25 | $item-pressed-color: contrasted($neutral-light-color, -10%), 26 | $item-background-color: $neutral-dark-color, 27 | $item-alt-background-color: contrasted($neutral-dark-color, -5%), 28 | $item-hovered-background-color: contrasted($neutral-dark-color, 5%), 29 | $item-selected-background-color: $base-color, 30 | $item-pressed-background-color: contrasted($neutral-dark-color, 15%) 31 | ); 32 | 33 | @include dataview-ui( 34 | $ui: 'large', 35 | $item-font-weight: 400, 36 | $item-font-size: 14px, 37 | $item-font-size-big: 14px, 38 | $item-padding: 16px, 39 | $item-padding-big: 18px 40 | ); 41 | 42 | @include dataview-ui( 43 | $ui: 'thumbnails', 44 | $background-color: $neutral-light-color 45 | ); 46 | 47 | .x-dataview-thumbnails { 48 | .x-dataview-item { 49 | .thumbnail { 50 | @include background-size('cover'); 51 | cursor: pointer; 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /client/packages/local/coworkee/sass/src/dataview/ListItem.scss: -------------------------------------------------------------------------------- 1 | .x-listitem-listing { 2 | .item-caption, 3 | .item-title, 4 | .item-stats, 5 | .item-info { 6 | @include ellipsis; 7 | 8 | > .x-fa { 9 | @include opacity(0.75); 10 | font-size: 14px; 11 | } 12 | } 13 | 14 | .item-title { 15 | font-weight: bold; 16 | } 17 | 18 | .item-caption { 19 | font-size: 12px; 20 | } 21 | 22 | .item-stats { 23 | font-size: 11px; 24 | } 25 | 26 | .item-details { 27 | padding: 0 8px; 28 | } 29 | 30 | .x-big & { 31 | .item-title { 32 | font-size: 14px; 33 | } 34 | .item-caption { 35 | font-size: 13px; 36 | } 37 | .item-stats { 38 | font-size: 12px; 39 | } 40 | } 41 | } 42 | 43 | .x-listitem-listing-inner-el { 44 | width: 100%; 45 | 46 | > .x-innerhtml { 47 | display: flex; 48 | flex: 1; 49 | flex-direction: row; 50 | align-items: center; 51 | } 52 | 53 | .item-details { 54 | flex: 1 1 0px; 55 | width: 0; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /client/packages/local/coworkee/sass/src/dataview/listswiper/Stepper.scss: -------------------------------------------------------------------------------- 1 | .x-listswiperstepper { 2 | .x-text { 3 | line-height: 1.4; 4 | } 5 | 6 | .subject { 7 | @include transition-property(height); 8 | @include transition-duration(0.25s); 9 | @include ellipsis; 10 | 11 | display: block; 12 | font-size: 12px; 13 | height: 0; 14 | } 15 | 16 | &.x-active { 17 | .subject { 18 | height: 18px; 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /client/packages/local/coworkee/sass/src/field/Search.scss: -------------------------------------------------------------------------------- 1 | .x-searchfield { 2 | background-color: $textfield-input-background-color; 3 | 4 | &.expandable { 5 | .x-input-el { 6 | padding-left: 0; 7 | } 8 | 9 | .x-body-wrap-el { 10 | @include transition-property(width); 11 | @include transition-duration(0.5s); 12 | width: 180px; 13 | } 14 | 15 | &.x-empty:not(.x-focused) { 16 | .x-body-wrap-el { 17 | width: 92px; 18 | } 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /client/packages/local/coworkee/sass/src/grid/column/Column.scss: -------------------------------------------------------------------------------- 1 | .x-gridcolumn { 2 | .x-title-el { 3 | @include single-text-shadow(); 4 | cursor: pointer; 5 | } 6 | 7 | .x-trigger-el { 8 | cursor: pointer; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /client/packages/local/coworkee/sass/src/tab/Panel.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * This file contains Ext.tab.Panel visual themes (ui) 3 | * http://docs.sencha.com/extjs/latest/modern/Ext.tab.Bar.html#sass-mixins 4 | * http://docs.sencha.com/extjs/latest/modern/Ext.tab.Tab.html#sass-mixins 5 | */ 6 | 7 | @include tabbar-ui( 8 | $ui: 'flat', 9 | $background-color: transparent, 10 | $border-color: mix($neutral-light-color, $neutral-highlight-color, 50%), 11 | $border-style: solid, 12 | $border-width: 0 0 1px 0, 13 | $horizontal-spacing: 0 14 | ); 15 | 16 | @include tab-ui( 17 | $ui: 'flat', 18 | $active-background-color: transparent, 19 | $active-border-color: $base-color, 20 | $background-color: transparent, 21 | $border-color: transparent, 22 | $border-style: solid, 23 | $border-width: 0 0 3px 0, 24 | $border-radius: 0, 25 | $border-radius-big: 0, 26 | $color: $neutral-medium-dark-color, 27 | $hovered-border-color: $neutral-light-color, 28 | $focused-border-color: $neutral-highlight-color, 29 | $focused-color: $neutral-dark-color, 30 | $focused-outline-style: none 31 | ); 32 | -------------------------------------------------------------------------------- /client/resources/images/auth-background.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sencha-extjs-examples/Coworkee-Open-Tooling/24d6c233623d8c69717d9e9916972586843eceda/client/resources/images/auth-background.jpg -------------------------------------------------------------------------------- /client/resources/images/loading.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sencha-extjs-examples/Coworkee-Open-Tooling/24d6c233623d8c69717d9e9916972586843eceda/client/resources/images/loading.png -------------------------------------------------------------------------------- /client/webpack.config.js: -------------------------------------------------------------------------------- 1 | const ExtWebpackPlugin = require('@sencha/ext-webpack-plugin'); 2 | const path = require('path'); 3 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 4 | const webpack = require('webpack'); 5 | const portfinder = require('portfinder'); 6 | 7 | module.exports = async function (env) { 8 | var browserprofile 9 | var watchprofile 10 | var buildenvironment = env.environment || process.env.npm_package_extbuild_defaultenvironment 11 | if (buildenvironment == 'production') { 12 | browserprofile = false 13 | watchprofile = 'no' 14 | } 15 | else { 16 | if (env.browser == undefined) {env.browser = 'yes'} 17 | browserprofile = env.browser || 'yes' 18 | watchprofile = env.watch || 'yes' 19 | } 20 | const isProd = buildenvironment === 'production' 21 | var buildprofile = env.profile || process.env.npm_package_extbuild_defaultprofile 22 | var buildenvironment = env.environment || process.env.npm_package_extbuild_defaultenvironment 23 | var buildverbose = env.verbose || process.env.npm_package_extbuild_defaultverbose 24 | if (buildprofile == 'all') { buildprofile = '' } 25 | if (env.treeshake == undefined) {env.treeshake = 'no'} 26 | var treeshake = env.treeshake ? env.treeshake : 'no' 27 | var basehref = env.basehref || '/' 28 | var mode = isProd ? 'production': 'development' 29 | 30 | portfinder.basePort = (env && env.port) || 1962; 31 | return portfinder.getPortPromise().then(port => { 32 | const nodeEnv = env && env.prod ? 'production' : 'development' 33 | const isProd = nodeEnv === 'production' 34 | const plugins = [ 35 | new HtmlWebpackPlugin({ 36 | template: 'index.html', 37 | hash: true, 38 | inject: "body" 39 | }), 40 | new ExtWebpackPlugin({ 41 | framework: 'extjs', 42 | port: port, 43 | emit: 'yes', 44 | browser: 'no', 45 | treeshake: treeshake, 46 | watch: watchprofile, 47 | profile: buildprofile, 48 | environment: buildenvironment, 49 | verbose: buildverbose 50 | }) 51 | ] 52 | return { 53 | performance: { hints: false }, 54 | mode: mode, 55 | devtool: (mode === 'development') ? 'inline-source-map' : false, 56 | context: path.join(__dirname, './'), 57 | entry: { 58 | main: "./app.js" 59 | }, 60 | output: { 61 | path: path.resolve(__dirname, './'), 62 | filename: '[name].js' 63 | }, 64 | module: { 65 | rules: [ 66 | { 67 | test: /.js$/, 68 | exclude: /node_modules/ 69 | } 70 | ] 71 | }, 72 | plugins: plugins, 73 | devServer: { 74 | contentBase: './', 75 | historyApiFallback: true, 76 | host: '0.0.0.0', 77 | hot: false, 78 | port, 79 | disableHostCheck: false, 80 | compress: isProd, 81 | inline: !isProd, 82 | stats: { 83 | entrypoints: false, 84 | assets: false, 85 | children: false, 86 | chunks: false, 87 | hash: false, 88 | modules: false, 89 | publicPath: false, 90 | timings: false, 91 | version: false, 92 | warnings: false, 93 | colors: { 94 | green: '' 95 | } 96 | } 97 | } 98 | } 99 | }); 100 | } 101 | -------------------------------------------------------------------------------- /client/workspace.json: -------------------------------------------------------------------------------- 1 | { 2 | "apps": [], 3 | "frameworks": { 4 | "ext": "node_modules/@sencha/ext" 5 | }, 6 | "build": { 7 | "dir": "${workspace.dir}/build" 8 | }, 9 | "packages": { 10 | "dir": "${workspace.dir}/packages/local,${workspace.dir}/packages,${workspace.dir}/node_modules/@sencha/ext-${toolkit.name},${workspace.dir}/node_modules/@sencha/ext-${toolkit.name}-treegrid,${workspace.dir}/node_modules/@sencha/ext-${toolkit.name}-theme-base,${workspace.dir}/node_modules/@sencha/ext-${toolkit.name}-theme-ios,${workspace.dir}/node_modules/@sencha/ext-${toolkit.name}-theme-material,${workspace.dir}/node_modules/@sencha/ext-${toolkit.name}-theme-aria,${workspace.dir}/node_modules/@sencha/ext-${toolkit.name}-theme-neutral,${workspace.dir}/node_modules/@sencha/ext-${toolkit.name}-theme-classic,${workspace.dir}/node_modules/@sencha/ext-${toolkit.name}-theme-gray,${workspace.dir}/node_modules/@sencha/ext-${toolkit.name}-theme-crisp,${workspace.dir}/node_modules/@sencha/ext-${toolkit.name}-theme-crisp-touch,${workspace.dir}/node_modules/@sencha/ext-${toolkit.name}-theme-neptune,${workspace.dir}/node_modules/@sencha/ext-${toolkit.name}-theme-neptune-touch,${workspace.dir}/node_modules/@sencha/ext-${toolkit.name}-theme-triton,${workspace.dir}/node_modules/@sencha/ext-${toolkit.name}-theme-graphite,${workspace.dir}/node_modules/@sencha/ext-calendar,${workspace.dir}/node_modules/@sencha/ext-charts,${workspace.dir}/node_modules/@sencha/ext-d3,${workspace.dir}/node_modules/@sencha/ext-exporter,${workspace.dir}/node_modules/@sencha/ext-pivot,${workspace.dir}/node_modules/@sencha/ext-pivot-d3,${workspace.dir}/node_modules/@sencha/ext-ux,${workspace.dir}/node_modules/@sencha/ext-google", 11 | "extract": "${workspace.dir}/packages/remote" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /server/.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false -------------------------------------------------------------------------------- /server/api/actions.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var helpers = require('../utils/helpers.js'); 4 | var session = require('../utils/session.js'); 5 | var errors = require('../utils/errors.js'); 6 | var models = require('../models'); 7 | 8 | var Service = { 9 | list: function(params, callback, sid, req) { 10 | session.verify(req).then(function(session) { 11 | return models.Action.scope('nested').findAndCount( 12 | helpers.sequelizify(params, models.Action, { 13 | where: { person_id: session.user.get('id') } 14 | })); 15 | }).then(function(results) { 16 | callback(null, { 17 | total: results.count, 18 | data: results.rows 19 | }); 20 | }).catch(function(err) { 21 | callback(err); 22 | }); 23 | }, 24 | 25 | insert: function(params, callback, sid, req) { 26 | session.verify(req).then(function(session) { 27 | return models.Person.lookup(params.recipient_id).then(function(person) { 28 | var subject = models.Action.subject(params.type, person); 29 | if (subject === null) { 30 | throw errors.types.invalidParams({ 31 | path: 'type', message: 'Invalid action type' 32 | }); 33 | } 34 | 35 | return session.user.createAction({ 36 | recipient_id: params.recipient_id, 37 | type: params.type, 38 | subject: subject 39 | }); 40 | }); 41 | }).then(function(row) { 42 | callback(null, { 43 | data: row 44 | }); 45 | }).catch(function(err) { 46 | callback(err); 47 | }); 48 | }, 49 | 50 | update: function(params, callback, sid, req) { 51 | session.verify(req).then(function() { 52 | // NOTE(SB): the direct proxy requires methods for all CRUD actions 53 | throw errors.types.notImplemented(); 54 | }).catch(function(err) { 55 | callback(err); 56 | }); 57 | }, 58 | 59 | remove: function(params, callback, sid, req) { 60 | session.verify(req).then(function() { 61 | var ids = helpers.idsFromParams(params); 62 | if (ids.length === 0) { 63 | throw errors.types.invalidParams({ 64 | path: 'id', message: 'Missing required parameter: id', 65 | }); 66 | } 67 | 68 | return models.Action.destroy({ 69 | where: { 70 | id: { $in: ids } 71 | } 72 | }); 73 | }).then(function() { 74 | callback(null); 75 | }).catch(function(err) { 76 | callback(err); 77 | }); 78 | }, 79 | 80 | filters: function(params, callback, sid, req) { 81 | session.verify(req).then(function(session) { 82 | return helpers.fetchFilters(params, models.Action, { 83 | where: { person_id: session.user.get('id') } 84 | }); 85 | }).then(function(results) { 86 | callback(null, { 87 | data: results 88 | }); 89 | }).catch(function(err) { 90 | callback(err); 91 | }); 92 | } 93 | } 94 | 95 | module.exports = Service; 96 | -------------------------------------------------------------------------------- /server/api/auth.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var session = require('../utils/session.js'); 4 | var errors = require('../utils/errors.js'); 5 | 6 | var Service = { 7 | /** 8 | * @param {String} params.username The user username or email. 9 | * @param {String} params.password The user (hashed) password. 10 | */ 11 | login: function(params, callback, sid, req, res) { 12 | session.initiate(params.username, params.password, res).then(function(data) { 13 | callback(null, data); 14 | }).catch(function(err) { 15 | callback(err); 16 | }); 17 | }, 18 | 19 | logout: function(params, callback, sid, req) { 20 | session.verify(req).then(function() { 21 | callback(null, true); 22 | }).catch(function(err) { 23 | callback(err); 24 | }); 25 | }, 26 | 27 | /** 28 | * Returns the currently authenticated user. 29 | */ 30 | user: function(params, callback, sid, req) { 31 | session.verify(req).then(function(session) { 32 | callback(null, session.user); 33 | }).catch(function(err) { 34 | callback(err); 35 | }); 36 | } 37 | }; 38 | 39 | module.exports = Service; 40 | -------------------------------------------------------------------------------- /server/bin/www: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * Module dependencies. 5 | */ 6 | 7 | var app = require('../app'); 8 | var debug = require('debug')('server:server'); 9 | var http = require('http'); 10 | 11 | /** 12 | * Get port from environment and store in Express. 13 | */ 14 | 15 | var port = normalizePort(process.env.PORT || '3000'); 16 | app.set('port', port); 17 | 18 | /** 19 | * Create HTTP server. 20 | */ 21 | 22 | var server = http.createServer(app); 23 | 24 | /** 25 | * Listen on provided port, on all network interfaces. 26 | */ 27 | 28 | server.listen(port); 29 | server.on('error', onError); 30 | server.on('listening', onListening); 31 | 32 | /** 33 | * Normalize a port into a number, string, or false. 34 | */ 35 | 36 | function normalizePort(val) { 37 | var port = parseInt(val, 10); 38 | 39 | if (isNaN(port)) { 40 | // named pipe 41 | return val; 42 | } 43 | 44 | if (port >= 0) { 45 | // port number 46 | return port; 47 | } 48 | 49 | return false; 50 | } 51 | 52 | /** 53 | * Event listener for HTTP server "error" event. 54 | */ 55 | 56 | function onError(error) { 57 | if (error.syscall !== 'listen') { 58 | throw error; 59 | } 60 | 61 | var bind = typeof port === 'string' 62 | ? 'Pipe ' + port 63 | : 'Port ' + port; 64 | 65 | // handle specific listen errors with friendly messages 66 | switch (error.code) { 67 | case 'EACCES': 68 | console.error(bind + ' requires elevated privileges'); 69 | process.exit(1); 70 | break; 71 | case 'EADDRINUSE': 72 | console.error(bind + ' is already in use'); 73 | process.exit(1); 74 | break; 75 | default: 76 | throw error; 77 | } 78 | } 79 | 80 | /** 81 | * Event listener for HTTP server "listening" event. 82 | */ 83 | 84 | function onListening() { 85 | var addr = server.address(); 86 | var bind = typeof addr === 'string' 87 | ? 'pipe ' + addr 88 | : 'port ' + addr.port; 89 | debug('Listening on ' + bind); 90 | } 91 | -------------------------------------------------------------------------------- /server/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "cors": { 3 | "enabled": false 4 | }, 5 | 6 | "cron": { 7 | "reset": "0 * * * *" 8 | }, 9 | 10 | "direct": { 11 | "rootNamespace": "Server", 12 | "apiName": "API", 13 | "apiUrl": "/api", 14 | "classRouteUrl": "/api", 15 | "classPath": "api", 16 | "server": "localhost", 17 | "port": "3000", 18 | "protocol": "http", 19 | "timeout": 30000, 20 | "cacheAPI": false, 21 | "relativeUrl": true, 22 | "appendRequestResponseObjects": true, 23 | "enableProcessors": false, 24 | "enableMetadata": true, 25 | "responseHelper": true 26 | }, 27 | 28 | "database": { 29 | "dialect": "sqlite", 30 | "storage": ".//data.db", 31 | "logging": false, 32 | "define": { 33 | "createdAt": "created", 34 | "updatedAt": "updated", 35 | "deletedAt": "deleted", 36 | "underscored": true 37 | } 38 | }, 39 | 40 | "session": { 41 | "secret": "62P59nE68F38q0q2wvHho58oR38aY7U9", 42 | "duration": 86400, 43 | "readonly": false 44 | }, 45 | 46 | "client": { 47 | "path": "../client" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /server/data/Offices.json: -------------------------------------------------------------------------------- 1 | [{"id":"2725949a-a1a5-45f8-ab29-4605629f9b49","name":"Fairfield","address":"200 Manufacturers Plaza","postcode":null,"city":"Energodar","region":null,"country":"Ukraine","location":{"latitude":47.49865,"longitude":34.6574}}, 2 | {"id":"74190840-82d6-4c0c-9e3e-715c389fc66d","name":"Lukken","address":"66855 Bashford Lane","postcode":null,"city":"Novogireyevo","region":null,"country":"Russia","location":{"latitude":55.75378,"longitude":37.81885}}, 3 | {"id":"73954bfa-4c30-4c3f-8ac7-cc1e4fd99266","name":"Pepper Wood","address":"98510 Rutledge Court","postcode":null,"city":"Pamiątkowo","region":null,"country":"Poland","location":{"latitude":52.55334,"longitude":16.68094}}, 4 | {"id":"38905470-accb-4905-8091-8418f6ea8ed9","name":"Dryden","address":"26241 Mosinee Terrace","postcode":"87090 CEDEX 9","city":"Limoges","region":"Limousin","country":"France","location":{"latitude":45.8315,"longitude":1.2578}}, 5 | {"id":"96edb1c4-04fe-463b-b8d0-346226a7b60e","name":"Welch","address":"3 Corscot Drive","postcode":null,"city":"Marystown","region":"Newfoundland and Labrador","country":"Canada","location":{"latitude":47.16663,"longitude":-55.14829}}, 6 | {"id":"f9a3451b-39ce-4d23-84c6-2dce8e18e84b","name":"Northfield","address":"39149 Carberry Avenue","postcode":null,"city":"Achanizo","region":null,"country":"Peru","location":{"latitude":-15.80611,"longitude":-73.96694}}, 7 | {"id":"fcab7f8d-0d89-43b0-a11a-12415a4bb9c1","name":"Dottie","address":"714 Armistice Alley","postcode":null,"city":"El Mida","region":null,"country":"Tunisia","location":{"latitude":36.72556,"longitude":10.85528}}, 8 | {"id":"dbcc6a21-5563-4792-95c3-bfdc446bc17f","name":"Little Fleur","address":"6739 Veith Junction","postcode":null,"city":"Sidomukti","region":null,"country":"Indonesia","location":{"latitude":-8.2051,"longitude":113.8279}}, 9 | {"id":"716f14d9-268b-4c3b-8d45-37b22971179e","name":"Prairieview","address":"5 3rd Court","postcode":"0162","city":"Oslo","region":"Oslo","country":"Norway","location":{"latitude":59.9127,"longitude":10.7461}}, 10 | {"id":"1f5e2af7-d959-4ff1-8a97-b88bbd51a40f","name":"Forest Dale","address":"4057 Miller Road","postcode":"85732","city":"Tucson","region":"Arizona","country":"United States","location":{"latitude":32.0848,"longitude":-110.7122}}, 11 | {"id": "3f06f7a7-dc80-4987-81dd-9cd807008ee5","name":"Manitowish","address":"9 Grim Center","postcode":"987-2224","city":"Yokosuka","region":null,"country":"Japan","location":{"latitude":35.28361,"longitude":139.66722}}] 12 | -------------------------------------------------------------------------------- /server/data/Organizations.json: -------------------------------------------------------------------------------- 1 | [{"id":"9f2cea7a-2147-4a3f-aebf-f0956591c6e8","name":"Services"}, 2 | {"id":"026d3aba-4193-442d-97fb-3da46c95bc5d","name":"Marketing"}, 3 | {"id":"3b23e955-dea8-4fd9-858b-0515a690ad6c","name":"Human Resources"}, 4 | {"id":"469eda61-b88e-4dcb-99cd-aab5e45c1fe8","name":"Financial"}, 5 | {"id":"26c446ac-21ea-4fd5-954b-2cc6c71f539f","name":"Sales"}, 6 | {"id":"c145efd4-1127-41dd-95e0-c3d5b96207fd","name":"Operational"}, 7 | {"id":"f467e5f1-fb74-40bd-9499-83fcad1b51b3","name":"Customer Service"}, 8 | {"id":"4e10c31c-dbb5-48e7-9f05-fc9b04cd9c7c","name":"Management"}, 9 | {"id":"f8379d72-0a57-4c25-86c8-e9761e4b8086","name":"Engineering"}, 10 | {"id":"82dc9f3c-35b4-4813-9e82-98da195b0500","name":"Strategy"}] 11 | -------------------------------------------------------------------------------- /server/models/action.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports = function (sequelize, DataTypes) { 4 | var Model = sequelize.define("Action", { 5 | id: { 6 | type: DataTypes.UUID, 7 | defaultValue: DataTypes.UUIDV4, 8 | allowNull: false, 9 | primaryKey: true, 10 | validate: { 11 | isUUID: 4 12 | } 13 | }, 14 | type: { 15 | type: DataTypes.STRING, 16 | allowNull: false, 17 | searchable: true, 18 | validate: { 19 | notEmpty: true 20 | } 21 | }, 22 | subject: { 23 | type: DataTypes.STRING, 24 | searchable: true, 25 | validate: { 26 | notEmpty: true 27 | } 28 | } 29 | }); 30 | 31 | Model.associate = function (models) { 32 | Model.belongsTo(models.Person, { as: 'recipient', constraints: false }); 33 | Model.belongsTo(models.Person, {as: 'actions' }); 34 | Model.addScope('nested', { 35 | include: [{ 36 | model: models.Person, 37 | as: 'recipient', 38 | include: [{ 39 | model: models.Office, 40 | as: 'office' 41 | }, { 42 | model: models.Organization, 43 | as: 'organization' 44 | }] 45 | }] 46 | }); 47 | }; 48 | 49 | Model.subject = function (action, recipient) { 50 | switch (action) { 51 | case 'phone': 52 | var extension = recipient.get('extension'); 53 | return recipient.get('phone') + (extension ? ':' + extension : ''); 54 | case 'profile': 55 | return recipient.get('username'); 56 | case 'email': 57 | case 'linkedin': 58 | case 'skype': 59 | return recipient.get(action); 60 | default: 61 | return null; 62 | } 63 | }; 64 | 65 | return Model; 66 | }; 67 | -------------------------------------------------------------------------------- /server/models/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This file collects all the models from the models directory and associates them if needed. 3 | */ 4 | 5 | "use strict"; 6 | 7 | var fs = require("fs"); 8 | var path = require("path"); 9 | var Sequelize = require("sequelize"); 10 | var env = process.env.NODE_ENV || "development"; 11 | var config = require(path.join(__dirname, '..', 'utils', 'config')).database; 12 | var sequelize = new Sequelize(config.database, config.username, config.password, config); 13 | var db = {}; 14 | 15 | fs.readdirSync(__dirname) 16 | .filter(function(file) { 17 | return (file.indexOf(".") !== 0) && (file !== "index.js"); 18 | }) 19 | .forEach(function(file) { 20 | var model = sequelize.import(path.join(__dirname, file)); 21 | db[model.name] = model; 22 | }); 23 | 24 | Object.keys(db).forEach(function(modelName) { 25 | if ("associate" in db[modelName]) { 26 | db[modelName].associate(db); 27 | } 28 | }); 29 | 30 | db.sequelize = sequelize; 31 | db.Sequelize = Sequelize; 32 | 33 | module.exports = db; 34 | -------------------------------------------------------------------------------- /server/models/office.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports = function(sequelize, DataTypes) { 4 | var Model = sequelize.define("Office", { 5 | id: { 6 | type: DataTypes.UUID, 7 | defaultValue: DataTypes.UUIDV4, 8 | allowNull: false, 9 | primaryKey: true, 10 | validate: { 11 | isUUID: 4 12 | } 13 | }, 14 | name: { 15 | type: DataTypes.STRING, 16 | allowNull: false, 17 | searchable: true, 18 | unique: { 19 | msg: 'An office with this name already exists.' 20 | }, 21 | validate: { 22 | notEmpty: true 23 | } 24 | }, 25 | address: { 26 | type: DataTypes.STRING, 27 | allowNull: false, 28 | searchable: true, 29 | validate: { 30 | notEmpty: true 31 | } 32 | }, 33 | postcode: { 34 | type: DataTypes.STRING, 35 | allowNull: true, 36 | searchable: true 37 | }, 38 | region: { 39 | type: DataTypes.STRING, 40 | allowNull: true, 41 | searchable: true 42 | }, 43 | city: { 44 | type: DataTypes.STRING, 45 | allowNull: false, 46 | searchable: true, 47 | validate: { 48 | notEmpty: true 49 | } 50 | }, 51 | country: { 52 | type: DataTypes.STRING, 53 | allowNull: false, 54 | searchable: true, 55 | validate: { 56 | notEmpty: true 57 | } 58 | }, 59 | location: { 60 | type: DataTypes.TEXT, 61 | allowNull: false, 62 | get: function () { 63 | return JSON.parse(this.getDataValue('location')); 64 | }, 65 | set: function (value) { 66 | return this.setDataValue('location', JSON.stringify(value)); 67 | } 68 | } 69 | }); 70 | 71 | Model.associate = function(models) { 72 | Model.hasMany(models.Person, { as: 'members' }); 73 | 74 | // http://stackoverflow.com/a/37817966 75 | Model.addScope('nested', { 76 | attributes: { 77 | include: [[sequelize.literal('(SELECT COUNT(*) FROM People WHERE People.office_id = Office.id)'), 'headcount']] 78 | } 79 | }); 80 | }; 81 | 82 | return Model; 83 | }; 84 | -------------------------------------------------------------------------------- /server/models/organization.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports = function(sequelize, DataTypes) { 4 | var Model = sequelize.define("Organization", { 5 | id: { 6 | type: DataTypes.UUID, 7 | defaultValue: DataTypes.UUIDV4, 8 | allowNull: false, 9 | primaryKey: true, 10 | validate: { 11 | isUUID: 4 12 | } 13 | }, 14 | name: { 15 | type: DataTypes.STRING, 16 | allowNull: false, 17 | searchable: true, 18 | unique: { 19 | msg: 'An organization with this name already exists.' 20 | }, 21 | validate: { 22 | notEmpty: true 23 | } 24 | } 25 | }); 26 | 27 | Model.associate = function(models) { 28 | Model.hasMany(models.Person, { as: 'members' }); 29 | Model.belongsTo(models.Person, { as: 'manager', constraints: false }); 30 | 31 | // http://stackoverflow.com/a/37817966 32 | Model.addScope('nested', { 33 | attributes: { 34 | include: [[sequelize.literal('(SELECT COUNT(*) FROM People WHERE People.organization_id = Organization.id)'), 'headcount']] 35 | }, 36 | include: [{ 37 | model: models.Person, 38 | as: 'manager', 39 | include: [{ 40 | model: models.Office, 41 | as: 'office' 42 | }] 43 | }] 44 | }); 45 | }; 46 | 47 | return Model; 48 | }; 49 | -------------------------------------------------------------------------------- /server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "server", 3 | "version": "0.0.2", 4 | "private": true, 5 | "scripts": { 6 | "start": "nodemon ./bin/www" 7 | }, 8 | "dependencies": { 9 | "array-unique": "~0.3.2", 10 | "body-parser": "~1.18.3", 11 | "console-stamp": "~0.2.7", 12 | "cookie-parser": "~1.4.3", 13 | "cors": "~2.8.5", 14 | "debug": "~4.1.0", 15 | "deepmerge": "~2.2.1", 16 | "express": "~4.16.4", 17 | "extdirect": "~2.0.5", 18 | "jade": "~1.11.0", 19 | "jsonwebtoken": "~8.4.0", 20 | "latinize": "~0.4.0", 21 | "morgan": "~1.9.1", 22 | "node-cron": "~2.0.3", 23 | "sequelize": "~4.41.2", 24 | "serve-favicon": "~2.5.0", 25 | "sqlite3": "~4.0.4", 26 | "yargs": "~12.0.5" 27 | }, 28 | "devDependencies": { 29 | "nodemon": "^1.18.6" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /server/public/api/portraits/men/0.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sencha-extjs-examples/Coworkee-Open-Tooling/24d6c233623d8c69717d9e9916972586843eceda/server/public/api/portraits/men/0.jpg -------------------------------------------------------------------------------- /server/public/api/portraits/men/1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sencha-extjs-examples/Coworkee-Open-Tooling/24d6c233623d8c69717d9e9916972586843eceda/server/public/api/portraits/men/1.jpg -------------------------------------------------------------------------------- /server/public/api/portraits/men/10.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sencha-extjs-examples/Coworkee-Open-Tooling/24d6c233623d8c69717d9e9916972586843eceda/server/public/api/portraits/men/10.jpg -------------------------------------------------------------------------------- /server/public/api/portraits/men/11.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sencha-extjs-examples/Coworkee-Open-Tooling/24d6c233623d8c69717d9e9916972586843eceda/server/public/api/portraits/men/11.jpg -------------------------------------------------------------------------------- /server/public/api/portraits/men/12.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sencha-extjs-examples/Coworkee-Open-Tooling/24d6c233623d8c69717d9e9916972586843eceda/server/public/api/portraits/men/12.jpg -------------------------------------------------------------------------------- /server/public/api/portraits/men/13.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sencha-extjs-examples/Coworkee-Open-Tooling/24d6c233623d8c69717d9e9916972586843eceda/server/public/api/portraits/men/13.jpg -------------------------------------------------------------------------------- /server/public/api/portraits/men/14.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sencha-extjs-examples/Coworkee-Open-Tooling/24d6c233623d8c69717d9e9916972586843eceda/server/public/api/portraits/men/14.jpg -------------------------------------------------------------------------------- /server/public/api/portraits/men/15.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sencha-extjs-examples/Coworkee-Open-Tooling/24d6c233623d8c69717d9e9916972586843eceda/server/public/api/portraits/men/15.jpg -------------------------------------------------------------------------------- /server/public/api/portraits/men/16.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sencha-extjs-examples/Coworkee-Open-Tooling/24d6c233623d8c69717d9e9916972586843eceda/server/public/api/portraits/men/16.jpg -------------------------------------------------------------------------------- /server/public/api/portraits/men/17.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sencha-extjs-examples/Coworkee-Open-Tooling/24d6c233623d8c69717d9e9916972586843eceda/server/public/api/portraits/men/17.jpg -------------------------------------------------------------------------------- /server/public/api/portraits/men/18.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sencha-extjs-examples/Coworkee-Open-Tooling/24d6c233623d8c69717d9e9916972586843eceda/server/public/api/portraits/men/18.jpg -------------------------------------------------------------------------------- /server/public/api/portraits/men/19.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sencha-extjs-examples/Coworkee-Open-Tooling/24d6c233623d8c69717d9e9916972586843eceda/server/public/api/portraits/men/19.jpg -------------------------------------------------------------------------------- /server/public/api/portraits/men/2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sencha-extjs-examples/Coworkee-Open-Tooling/24d6c233623d8c69717d9e9916972586843eceda/server/public/api/portraits/men/2.jpg -------------------------------------------------------------------------------- /server/public/api/portraits/men/20.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sencha-extjs-examples/Coworkee-Open-Tooling/24d6c233623d8c69717d9e9916972586843eceda/server/public/api/portraits/men/20.jpg -------------------------------------------------------------------------------- /server/public/api/portraits/men/21.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sencha-extjs-examples/Coworkee-Open-Tooling/24d6c233623d8c69717d9e9916972586843eceda/server/public/api/portraits/men/21.jpg -------------------------------------------------------------------------------- /server/public/api/portraits/men/22.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sencha-extjs-examples/Coworkee-Open-Tooling/24d6c233623d8c69717d9e9916972586843eceda/server/public/api/portraits/men/22.jpg -------------------------------------------------------------------------------- /server/public/api/portraits/men/23.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sencha-extjs-examples/Coworkee-Open-Tooling/24d6c233623d8c69717d9e9916972586843eceda/server/public/api/portraits/men/23.jpg -------------------------------------------------------------------------------- /server/public/api/portraits/men/24.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sencha-extjs-examples/Coworkee-Open-Tooling/24d6c233623d8c69717d9e9916972586843eceda/server/public/api/portraits/men/24.jpg -------------------------------------------------------------------------------- /server/public/api/portraits/men/25.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sencha-extjs-examples/Coworkee-Open-Tooling/24d6c233623d8c69717d9e9916972586843eceda/server/public/api/portraits/men/25.jpg -------------------------------------------------------------------------------- /server/public/api/portraits/men/26.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sencha-extjs-examples/Coworkee-Open-Tooling/24d6c233623d8c69717d9e9916972586843eceda/server/public/api/portraits/men/26.jpg -------------------------------------------------------------------------------- /server/public/api/portraits/men/3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sencha-extjs-examples/Coworkee-Open-Tooling/24d6c233623d8c69717d9e9916972586843eceda/server/public/api/portraits/men/3.jpg -------------------------------------------------------------------------------- /server/public/api/portraits/men/4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sencha-extjs-examples/Coworkee-Open-Tooling/24d6c233623d8c69717d9e9916972586843eceda/server/public/api/portraits/men/4.jpg -------------------------------------------------------------------------------- /server/public/api/portraits/men/5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sencha-extjs-examples/Coworkee-Open-Tooling/24d6c233623d8c69717d9e9916972586843eceda/server/public/api/portraits/men/5.jpg -------------------------------------------------------------------------------- /server/public/api/portraits/men/6.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sencha-extjs-examples/Coworkee-Open-Tooling/24d6c233623d8c69717d9e9916972586843eceda/server/public/api/portraits/men/6.jpg -------------------------------------------------------------------------------- /server/public/api/portraits/men/7.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sencha-extjs-examples/Coworkee-Open-Tooling/24d6c233623d8c69717d9e9916972586843eceda/server/public/api/portraits/men/7.jpg -------------------------------------------------------------------------------- /server/public/api/portraits/men/8.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sencha-extjs-examples/Coworkee-Open-Tooling/24d6c233623d8c69717d9e9916972586843eceda/server/public/api/portraits/men/8.jpg -------------------------------------------------------------------------------- /server/public/api/portraits/men/9.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sencha-extjs-examples/Coworkee-Open-Tooling/24d6c233623d8c69717d9e9916972586843eceda/server/public/api/portraits/men/9.jpg -------------------------------------------------------------------------------- /server/public/api/portraits/women/0.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sencha-extjs-examples/Coworkee-Open-Tooling/24d6c233623d8c69717d9e9916972586843eceda/server/public/api/portraits/women/0.jpg -------------------------------------------------------------------------------- /server/public/api/portraits/women/1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sencha-extjs-examples/Coworkee-Open-Tooling/24d6c233623d8c69717d9e9916972586843eceda/server/public/api/portraits/women/1.jpg -------------------------------------------------------------------------------- /server/public/api/portraits/women/10.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sencha-extjs-examples/Coworkee-Open-Tooling/24d6c233623d8c69717d9e9916972586843eceda/server/public/api/portraits/women/10.jpg -------------------------------------------------------------------------------- /server/public/api/portraits/women/11.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sencha-extjs-examples/Coworkee-Open-Tooling/24d6c233623d8c69717d9e9916972586843eceda/server/public/api/portraits/women/11.jpg -------------------------------------------------------------------------------- /server/public/api/portraits/women/12.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sencha-extjs-examples/Coworkee-Open-Tooling/24d6c233623d8c69717d9e9916972586843eceda/server/public/api/portraits/women/12.jpg -------------------------------------------------------------------------------- /server/public/api/portraits/women/13.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sencha-extjs-examples/Coworkee-Open-Tooling/24d6c233623d8c69717d9e9916972586843eceda/server/public/api/portraits/women/13.jpg -------------------------------------------------------------------------------- /server/public/api/portraits/women/14.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sencha-extjs-examples/Coworkee-Open-Tooling/24d6c233623d8c69717d9e9916972586843eceda/server/public/api/portraits/women/14.jpg -------------------------------------------------------------------------------- /server/public/api/portraits/women/15.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sencha-extjs-examples/Coworkee-Open-Tooling/24d6c233623d8c69717d9e9916972586843eceda/server/public/api/portraits/women/15.jpg -------------------------------------------------------------------------------- /server/public/api/portraits/women/16.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sencha-extjs-examples/Coworkee-Open-Tooling/24d6c233623d8c69717d9e9916972586843eceda/server/public/api/portraits/women/16.jpg -------------------------------------------------------------------------------- /server/public/api/portraits/women/17.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sencha-extjs-examples/Coworkee-Open-Tooling/24d6c233623d8c69717d9e9916972586843eceda/server/public/api/portraits/women/17.jpg -------------------------------------------------------------------------------- /server/public/api/portraits/women/18.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sencha-extjs-examples/Coworkee-Open-Tooling/24d6c233623d8c69717d9e9916972586843eceda/server/public/api/portraits/women/18.jpg -------------------------------------------------------------------------------- /server/public/api/portraits/women/19.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sencha-extjs-examples/Coworkee-Open-Tooling/24d6c233623d8c69717d9e9916972586843eceda/server/public/api/portraits/women/19.jpg -------------------------------------------------------------------------------- /server/public/api/portraits/women/2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sencha-extjs-examples/Coworkee-Open-Tooling/24d6c233623d8c69717d9e9916972586843eceda/server/public/api/portraits/women/2.jpg -------------------------------------------------------------------------------- /server/public/api/portraits/women/20.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sencha-extjs-examples/Coworkee-Open-Tooling/24d6c233623d8c69717d9e9916972586843eceda/server/public/api/portraits/women/20.jpg -------------------------------------------------------------------------------- /server/public/api/portraits/women/21.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sencha-extjs-examples/Coworkee-Open-Tooling/24d6c233623d8c69717d9e9916972586843eceda/server/public/api/portraits/women/21.jpg -------------------------------------------------------------------------------- /server/public/api/portraits/women/22.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sencha-extjs-examples/Coworkee-Open-Tooling/24d6c233623d8c69717d9e9916972586843eceda/server/public/api/portraits/women/22.jpg -------------------------------------------------------------------------------- /server/public/api/portraits/women/23.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sencha-extjs-examples/Coworkee-Open-Tooling/24d6c233623d8c69717d9e9916972586843eceda/server/public/api/portraits/women/23.jpg -------------------------------------------------------------------------------- /server/public/api/portraits/women/3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sencha-extjs-examples/Coworkee-Open-Tooling/24d6c233623d8c69717d9e9916972586843eceda/server/public/api/portraits/women/3.jpg -------------------------------------------------------------------------------- /server/public/api/portraits/women/4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sencha-extjs-examples/Coworkee-Open-Tooling/24d6c233623d8c69717d9e9916972586843eceda/server/public/api/portraits/women/4.jpg -------------------------------------------------------------------------------- /server/public/api/portraits/women/5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sencha-extjs-examples/Coworkee-Open-Tooling/24d6c233623d8c69717d9e9916972586843eceda/server/public/api/portraits/women/5.jpg -------------------------------------------------------------------------------- /server/public/api/portraits/women/6.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sencha-extjs-examples/Coworkee-Open-Tooling/24d6c233623d8c69717d9e9916972586843eceda/server/public/api/portraits/women/6.jpg -------------------------------------------------------------------------------- /server/public/api/portraits/women/7.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sencha-extjs-examples/Coworkee-Open-Tooling/24d6c233623d8c69717d9e9916972586843eceda/server/public/api/portraits/women/7.jpg -------------------------------------------------------------------------------- /server/public/api/portraits/women/8.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sencha-extjs-examples/Coworkee-Open-Tooling/24d6c233623d8c69717d9e9916972586843eceda/server/public/api/portraits/women/8.jpg -------------------------------------------------------------------------------- /server/public/api/portraits/women/9.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sencha-extjs-examples/Coworkee-Open-Tooling/24d6c233623d8c69717d9e9916972586843eceda/server/public/api/portraits/women/9.jpg -------------------------------------------------------------------------------- /server/public/stylesheets/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding: 50px; 3 | font: 14px "Lucida Grande", Helvetica, Arial, sans-serif; 4 | } 5 | 6 | a { 7 | color: #00B7FF; 8 | } 9 | -------------------------------------------------------------------------------- /server/utils/config.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var merge = require('deepmerge'); 4 | var config = require('../config.json'); 5 | 6 | // Override main config (config.json) with potential local config (config.local.json): that's 7 | // useful when deploying the app on a server with different server url and port (Ext.Direct). 8 | try { 9 | config = merge(config, require('../config.local.json')); 10 | } catch (e) { 11 | } 12 | 13 | module.exports = config; 14 | -------------------------------------------------------------------------------- /server/utils/errors.js: -------------------------------------------------------------------------------- 1 | /** 2 | * http://www.jsonrpc.org/specification#error_object 3 | */ 4 | 5 | "use strict"; 6 | 7 | var extend = require('util')._extend; 8 | 9 | var codes = { 10 | INVALID_REQUEST: -32600, 11 | UNAUTHORIZED: -32099, 12 | AUTH_TOKEN_EXPIRED: -32098, 13 | AUTH_TOKEN_INVALID: -32097, 14 | READONLY_SESSION: -32096, 15 | INVALID_PARAMS: -32001, 16 | NOT_IMPLEMENTED: -32000 17 | }; 18 | 19 | function generate(message, error, code, data) { 20 | return { 21 | // Ext.Direct expects the error (object or string) to be store in data.message 22 | message: extend(extend({}, data), { 23 | message: message || 'Invalid Request', 24 | name: error || 'InvalidRequest', 25 | code: code == null? codes.INVALID_REQUEST : code 26 | }) 27 | }; 28 | }; 29 | 30 | var types = { 31 | unauthorized: function(data) { 32 | return generate( 33 | 'User is not authorized to perform this action', 34 | 'Unauthorized', 35 | codes.UNAUTHORIZED, 36 | data 37 | ); 38 | }, 39 | 40 | authTokenExpired: function(data) { 41 | return generate( 42 | 'Your session has expired, please login again', 43 | 'AuthTokenExpired', 44 | codes.AUTH_TOKEN_EXPIRED, 45 | data 46 | ); 47 | }, 48 | 49 | authTokenInvalid: function(data) { 50 | return generate( 51 | 'Your session is no longer valid, please login again', 52 | 'AuthTokenInvalid', 53 | codes.AUTH_TOKEN_INVALID, 54 | data 55 | ); 56 | }, 57 | 58 | notImplemented: function(data) { 59 | return generate( 60 | 'Not implemented', 61 | 'NotImplemented', 62 | codes.NOT_IMPLEMENTED, 63 | data 64 | ); 65 | }, 66 | 67 | invalidParams: function(data) { 68 | if (!Array.isArray(data)) { 69 | data = [data]; 70 | } 71 | 72 | return generate( 73 | 'Invalid parameters', 74 | 'InvalidParameters', 75 | codes.INVALID_PARAMS, 76 | { errors: data } 77 | ); 78 | }, 79 | 80 | readonly: function(data) { 81 | return generate( 82 | 'Read-only session, data not updated', 83 | 'ReadOnlySession', 84 | codes.READONLY_SESSION, 85 | data 86 | ); 87 | } 88 | }; 89 | 90 | module.exports = { 91 | codes: codes, 92 | 93 | types: types, 94 | 95 | generate: generate, 96 | 97 | parse: function(error) { 98 | switch (error.name) { 99 | case 'SequelizeValidationError': 100 | case 'SequelizeUniqueConstraintError': 101 | return types.invalidParams(error.errors.map(function(error) { 102 | return { message: error.message, path: error.path }; 103 | })); 104 | default: 105 | return error; 106 | } 107 | }, 108 | 109 | fromJwtError: function(data) { 110 | // https://github.com/auth0/node-jsonwebtoken#errors--codes 111 | if (data.name === 'TokenExpiredError') { 112 | return types.authTokenExpired(data); 113 | } else if (data.name === 'JsonWebTokenError') { 114 | return types.authTokenInvalid(data); 115 | } else { 116 | return types.unauthorized(data); 117 | } 118 | } 119 | }; 120 | -------------------------------------------------------------------------------- /server/utils/session.js: -------------------------------------------------------------------------------- 1 | /** 2 | * https://jwt.io/introduction/ 3 | */ 4 | 5 | "use strict"; 6 | 7 | var errors = require('./errors'); 8 | var config = require('./config'); 9 | var models = require('../models'); 10 | var jwt = require('jsonwebtoken'); 11 | 12 | module.exports = { 13 | 14 | readonly: config.session.readonly, 15 | 16 | initiate: function(username, password, res) { 17 | return models.Person.scope('nested').findOne({ 18 | where: { 19 | password: password, 20 | $or: [ 21 | { username: username }, 22 | { email: username } 23 | ] 24 | } 25 | }).then(function(user) { 26 | if (!user) { 27 | throw errors.types.invalidParams({ 28 | path: 'username', message: 'Invalid username and/or password' 29 | }); 30 | } 31 | 32 | var duration = config.session.duration; 33 | var expires = new Date(Date.now() + duration*1000); 34 | var token = jwt.sign( 35 | { user_id: user.get('id') }, 36 | config.session.secret, 37 | { expiresIn: duration }); 38 | 39 | return { 40 | user: user, 41 | token: token, 42 | expires: expires 43 | }; 44 | }); 45 | }, 46 | 47 | verify: function(request) { 48 | return new Promise(function(resolve, reject) { 49 | // https://jwt.io/introduction/#how-do-json-web-tokens-work- 50 | var header = request.headers && request.headers.authorization; 51 | var matches = header? /^Bearer (\S+)$/.exec(header) : null; 52 | var token = matches && matches[1]; 53 | 54 | if (!token) { 55 | return reject(errors.types.unauthorized('No authorization token was found')); 56 | } 57 | 58 | jwt.verify(token, config.session.secret, function(err, decoded) { 59 | if (err) { 60 | return reject(errors.fromJwtError(err)); 61 | } 62 | 63 | models.Person.scope('nested').findOne({ 64 | where: { 65 | id: decoded.user_id 66 | } 67 | }).then(function(user) { 68 | if (!user) { 69 | throw errors.types.authTokenInvalid(); 70 | } 71 | 72 | resolve({ 73 | user: user, 74 | token: token, 75 | expires: new Date(decoded.exp) 76 | }); 77 | }).catch(function(err) { 78 | reject(err); 79 | }); 80 | }); 81 | }); 82 | } 83 | }; 84 | -------------------------------------------------------------------------------- /server/views/error.jade: -------------------------------------------------------------------------------- 1 | extends layout 2 | 3 | block content 4 | h1= message 5 | h2= error.status 6 | pre #{error.stack} 7 | -------------------------------------------------------------------------------- /server/views/layout.jade: -------------------------------------------------------------------------------- 1 | doctype html 2 | html 3 | head 4 | title= title 5 | link(rel='stylesheet', href='/stylesheets/style.css') 6 | body 7 | block content 8 | --------------------------------------------------------------------------------