├── Procfile ├── test ├── mocha.opts ├── configTest.js └── lib │ ├── routesTest.js │ └── handlers │ └── authTest.js ├── latest.dump ├── dist ├── compiled │ ├── assetManifest.json │ └── style.522e5d67.css └── css │ └── style.css ├── app ├── css │ ├── style.less │ ├── typography.less │ ├── manager.less │ ├── base.less │ ├── media_queries.less │ ├── overlay.less │ ├── animations.less │ └── employee.less ├── app.js ├── controllers │ ├── searchController.js │ └── orgchartController.js ├── vendor │ ├── shake.js │ ├── angular-ellipsis.js │ └── angular-cache-2.3.4.min.js ├── services.js └── directives.js ├── server ├── extensions │ ├── onPreResponse.js │ └── viewContext.js ├── routes │ ├── routes.js │ ├── authRoutes.js │ ├── apiProxyRoutes.js │ └── staticRoutes.js ├── views │ ├── partials │ │ ├── managerDetail.ejs │ │ ├── reportsListView.ejs │ │ ├── searchOverlay.ejs │ │ ├── phoneSelectorOverlay.ejs │ │ └── employeeDetail.ejs │ └── home.ejs ├── cache │ ├── orgchartDataConfig.js │ ├── cacheScheduler.js │ ├── orgchartCacheConfig.js │ ├── cache.js │ └── orgchartModel.js └── handlers │ ├── orgchartHandler.js │ ├── searchHandler.js │ └── authHandler.js ├── .gitignore ├── app.json ├── server.js ├── package.json ├── config.js ├── README.md ├── Gruntfile.js └── LICENSE /Procfile: -------------------------------------------------------------------------------- 1 | web: node server.js -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --reporter spec 2 | --recursive -------------------------------------------------------------------------------- /latest.dump: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lyric/Salesforce-OrgChart/master/latest.dump -------------------------------------------------------------------------------- /dist/compiled/assetManifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "dist/css/style.css": "dist/compiled/style.522e5d67.css", 3 | "dist/js/bundle.js": "dist/compiled/bundle.dd8eb7e3.js" 4 | } -------------------------------------------------------------------------------- /app/css/style.less: -------------------------------------------------------------------------------- 1 | 2 | @import "base.less"; 3 | @import "typography.less"; 4 | @import "employee.less"; 5 | @import "manager.less"; 6 | @import "overlay.less"; 7 | @import "animations.less"; 8 | @import "media_queries.less"; 9 | -------------------------------------------------------------------------------- /server/extensions/onPreResponse.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Exports all OnPreResponse extensions 3 | * @type {setViewContext|exports} - an array of extension functions 4 | */ 5 | 6 | var viewContext = require( './viewContext' ); 7 | 8 | module.exports = [ viewContext ]; 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | lib-cov 2 | lcov.info 3 | *.seed 4 | *.log 5 | *.csv 6 | *.dat 7 | *.out 8 | *.pid 9 | *.gz 10 | 11 | pids 12 | logs 13 | results 14 | build 15 | .grunt 16 | 17 | node_modules 18 | 19 | .idea/workspace.xml 20 | lib/public/js/ 21 | lib/public/css/ 22 | .idea/org-chart.iml 23 | .idea 24 | DF15-Org-Chart.iml -------------------------------------------------------------------------------- /server/routes/routes.js: -------------------------------------------------------------------------------- 1 | var staticRoutes = require( './staticRoutes' ).routes; 2 | var authRoutes = require( './authRoutes' ).routes; 3 | var apiProxyRoutes = require( './apiProxyRoutes' ).routes; 4 | 5 | /** 6 | * Export all routes 7 | * @type {Array} 8 | */ 9 | module.exports = staticRoutes.concat( authRoutes ).concat( apiProxyRoutes ); 10 | -------------------------------------------------------------------------------- /server/routes/authRoutes.js: -------------------------------------------------------------------------------- 1 | var Auth = require( '../handlers/authHandler' ); 2 | 3 | var routes = [ 4 | { 5 | method: 'POST', 6 | path: '/auth/signedrequest', 7 | handler: Auth.signedRequest 8 | }, 9 | { 10 | method: 'GET', 11 | path: '/auth/signedrequest', 12 | handler: Auth.badRequest 13 | } 14 | ]; 15 | 16 | exports.routes = routes; -------------------------------------------------------------------------------- /server/routes/apiProxyRoutes.js: -------------------------------------------------------------------------------- 1 | var searchHandler = require( '../handlers/searchHandler' ); 2 | var orgchartHandler = require( '../handlers/orgchartHandler' ); 3 | 4 | var routes = [ 5 | { 6 | method: 'GET', 7 | path: '/services/orgchart/{id}', 8 | handler: orgchartHandler 9 | }, 10 | { 11 | method: 'POST', 12 | path: '/services/orgchart', 13 | handler: searchHandler 14 | } 15 | ]; 16 | 17 | exports.routes = routes; 18 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Org Chart", 3 | "description": "An SF1 application that gives a graphical representation of an Org's employee hierarchy", 4 | "repository": "https://github.com/salesforce-it/Salesforce-OrgChart", 5 | "keywords": ["Salesforce", "Org", "Chart", "Node"], 6 | "success_url": "/welcome", 7 | "logo": "http://andybean.com/salesforce/Org%20Chart%20Icon%20for%20Heroku.png", 8 | "addons": [ 9 | "heroku-postgresql:hobby-dev", 10 | "herokuconnect" 11 | ] 12 | } -------------------------------------------------------------------------------- /server/views/partials/managerDetail.ejs: -------------------------------------------------------------------------------- 1 |
2 | 10 |
11 | -------------------------------------------------------------------------------- /server/cache/orgchartDataConfig.js: -------------------------------------------------------------------------------- 1 | var orgchartModel = require( './orgchartModel' ); 2 | var orgchartDataConfig = { 3 | users: { 4 | tableName: 'salesforce.user', 5 | getData: function ( data ) { 6 | return new orgchartModel( data ); 7 | } 8 | }, 9 | contacts: { 10 | tableName: 'salesforce.contact', 11 | getData: function ( data ) { 12 | return new orgchartModel( data ); 13 | } 14 | } 15 | }; 16 | 17 | module.exports = orgchartDataConfig; -------------------------------------------------------------------------------- /server/routes/staticRoutes.js: -------------------------------------------------------------------------------- 1 | var _ = require( 'lodash' ); 2 | var Config = require( '../../config' ); 3 | 4 | var route = { 5 | method: 'GET', 6 | path: '/dist/{param*}', 7 | handler: { 8 | directory: { 9 | path: './dist', 10 | lookupCompressed: true 11 | } 12 | } 13 | }; 14 | 15 | // add in caching config based on ENV 16 | cacheConfig = Config.getCacheConfig(); 17 | if ( cacheConfig ) { 18 | _.merge( route, { config: cacheConfig } ); 19 | } 20 | 21 | exports.routes = [ route ]; -------------------------------------------------------------------------------- /app/app.js: -------------------------------------------------------------------------------- 1 | require('./vendor/canvas-all'); 2 | require('angular-animate'); 3 | require('angular-sanitize'); 4 | require('angular-touch'); 5 | require('./vendor/shake.js'); 6 | require('./vendor/angular-cache-2.3.4.min'); 7 | 8 | var orgchart = require('./controllers/orgchartController'); 9 | var search = require('./controllers/searchController'); 10 | var services = require('./services'); 11 | var directives = require('./directives.js'); 12 | var ellipsis = require('./vendor/angular-ellipsis.js'); 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /app/css/typography.less: -------------------------------------------------------------------------------- 1 | 2 | // --------------------------------------------------- 3 | // Resets 4 | // --------------------------------------------------- 5 | 6 | 7 | @font-face { 8 | font-family: 'SalesforceSans'; 9 | src: url("https://www2.sfdcstatic.com/system/shared/common/assets/fonts/SalesforceSans/SalesforceSans-Regular.woff") format('woff'); 10 | font-weight: 300; 11 | font-style: normal; 12 | } 13 | 14 | body { 15 | /* Better Font Rendering =========== */ 16 | -webkit-font-smoothing: antialiased; 17 | -moz-osx-font-smoothing: grayscale; 18 | } 19 | -------------------------------------------------------------------------------- /server/views/partials/reportsListView.ejs: -------------------------------------------------------------------------------- 1 |
2 | 14 |
15 | -------------------------------------------------------------------------------- /server/extensions/viewContext.js: -------------------------------------------------------------------------------- 1 | var Config = require( './../../config' ); 2 | var Boom = require( 'hapi' ).error; 3 | 4 | function setViewContext( request, reply ) { 5 | var assetManifest = require( '../../dist/compiled/assetManifest.json' ); 6 | 7 | var response = request.response; 8 | if ( !assetManifest ) { 9 | // 500 error 10 | return reply( Boom.badImplementation( 'Missing assetManifest.json' ) ); 11 | } 12 | // Set default view context 13 | if ( response.variety === 'view' ) { 14 | var context = response.source.context; 15 | context.assetManifest = assetManifest; 16 | context.config = Config; 17 | } 18 | 19 | return reply(); 20 | } 21 | 22 | module.exports = setViewContext; 23 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | var Hapi = require( 'hapi' ), 2 | routes = require( './server/routes/routes.js' ), 3 | config = require( './config.js' ), 4 | util = require( 'util' ), 5 | onPreResponse = require( './server/extensions/onPreResponse' ), 6 | cacheScheduler = require( './server/cache/cacheScheduler' ); 7 | 8 | //prime the cache and set up the automatic refresh of data 9 | cacheScheduler.run(); 10 | 11 | var server = new Hapi.Server( config.host, config.port, config.server ); 12 | 13 | // routes 14 | server.route( routes ); 15 | 16 | // extensions 17 | server.ext( 'onPreResponse', onPreResponse ); 18 | 19 | // start 20 | server.start(); 21 | 22 | server.log( [ 'debug', 'hapi' ], util.format( 'Server running with config: %j facadeClient: %j', config, config.facadeClient() ) ); 23 | -------------------------------------------------------------------------------- /server/views/partials/searchOverlay.ejs: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
5 | 6 |
7 |
8 |
    9 |
  • 10 | {{searchs.name}} 11 |
  • 12 |
  • 13 | ... 14 |
  • 15 |
16 |
17 |
18 |
19 | -------------------------------------------------------------------------------- /server/handlers/orgchartHandler.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by casakawa on 8/25/15. 3 | */ 4 | var cache = require( '../cache/cache' ); 5 | var _ = require( 'lodash' ); 6 | 7 | /** 8 | * first grab the user configuration, then use that config to refer 9 | * either the contactCache or the userCache. 10 | */ 11 | 12 | var orgchartHandler = function ( request, reply ) { 13 | //search for a by email. 14 | var requestedUser = request.params.id; 15 | 16 | cache.getOrgchartData( function ( people ) { 17 | var currentUser = null; 18 | 19 | //search for currentUser 20 | for ( var i = 0; i < people.length; i++ ) { 21 | var person = people[ i ]; 22 | if ( person.email == requestedUser ) { 23 | currentUser = people[ i ]; 24 | break; 25 | } 26 | } 27 | 28 | var neighborhood = currentUser.getNeighborhood( people ); 29 | 30 | reply( _.merge( currentUser, neighborhood ) ); 31 | } ); 32 | }; 33 | 34 | module.exports = orgchartHandler; 35 | -------------------------------------------------------------------------------- /server/handlers/searchHandler.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by casakawa on 8/14/15. 3 | */ 4 | //this file holds the cached users array 5 | var cache = require( '../cache/cache' ); 6 | 7 | var searchHandler = function ( request, reply ) { 8 | 9 | //return array 10 | var returnArray = []; 11 | cache.getOrgchartData( function ( people ) { 12 | //catch the search term and convert to lowercase 13 | var searchTerm = request.payload.search.toLowerCase(); 14 | 15 | //loop through all users looking for matches 16 | for ( var i = 0; i < people.length; i++ ) { 17 | //also convert the names in the array to lowercase. 18 | var compareString = people[ i ].name.toLowerCase(); 19 | 20 | //adds any matches to the return array, this also handles partial matches 21 | if ( compareString.indexOf( searchTerm ) > -1 ) { 22 | returnArray.push( people[ i ] ); 23 | } 24 | } 25 | 26 | reply( returnArray ); 27 | } ); 28 | }; 29 | 30 | module.exports = searchHandler; 31 | -------------------------------------------------------------------------------- /server/handlers/authHandler.js: -------------------------------------------------------------------------------- 1 | var decode = require( 'salesforce-signed-request' ); 2 | var Config = require( '../../config.js' ); 3 | var Boom = require( 'hapi' ).error; 4 | 5 | var authHandlers = { 6 | // POST /auth/signedrequest 7 | signedRequest: function ( request, reply ) { 8 | 9 | if ( !Config.appSecret() ) { 10 | return reply( Boom.badImplementation( 'Missing AppSecret configuration' ) ); 11 | } 12 | var canvasContext = decode( request.payload.signed_request, Config.appSecret() ); 13 | 14 | if ( canvasContext instanceof Error ) { 15 | return reply( Boom.badRequest( canvasContext.message ) ); 16 | } 17 | 18 | var data = { 19 | canvasContext: canvasContext, 20 | config: Config 21 | }; 22 | 23 | reply.view( 'home', data ); 24 | }, 25 | 26 | // GET /auth/signedRequest 27 | badRequest: function ( request, reply ) { 28 | Boom.methodNotAllowed( 'signedrequest must be an HTTP POST' ); 29 | } 30 | }; 31 | 32 | module.exports = authHandlers; -------------------------------------------------------------------------------- /server/cache/cacheScheduler.js: -------------------------------------------------------------------------------- 1 | var orgConfig = require( './orgchartCacheConfig' ); 2 | var cache = require( './cache' ); 3 | var schedule = require( 'node-schedule' ); 4 | var pg = require( 'pg' ); 5 | 6 | function run() { 7 | console.log( 'Running the cache scheduler' ); 8 | //only allows for one client connection at a time. 9 | //(herokuConnect free tier only allows one connection at a time) 10 | pg.defaults.poolSize = 1; 11 | 12 | /** this queries the configuration settings, and then uses this information to 13 | * initiate and periodically refresh the cache. 14 | */ 15 | orgConfig.getConfig( buildCache ); 16 | 17 | //scheduler runs every ten mins 18 | var rule = new schedule.RecurrenceRule(); 19 | rule.minute = new schedule.Range( 0, 0, 10 ); 20 | 21 | schedule.scheduleJob( rule, function () { 22 | orgConfig.getConfig( buildCache ); 23 | } ); 24 | } 25 | 26 | //initiate the cache, and flush the other cache if it exists 27 | function buildCache() { 28 | 29 | cache.flush(); 30 | cache.getOrgchartData( function () { 31 | } ); 32 | } 33 | 34 | module.exports.run = run; -------------------------------------------------------------------------------- /test/configTest.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect; 2 | var config = require('../config'); 3 | 4 | 5 | describe('Config', function(){ 6 | 7 | it('should return app secret from ENV var', function(){ 8 | process.env.APP_SECRET = 'foo'; 9 | expect(config.appSecret()).to.equal('foo'); 10 | }); 11 | 12 | describe('facadeClient', function(){ 13 | 14 | it('should return facade url from ENV var', function () { 15 | 16 | var facadeurl = 'http://test.com'; 17 | process.env.FACADE_URL = facadeurl; 18 | expect(config.facadeClient().url).to.equal(facadeurl); 19 | delete process.env.FACADE_URL; 20 | }); 21 | 22 | it('should default to dev facade when no ENV var is set', function () { 23 | 24 | delete process.env.FACADE_URL; 25 | expect(config.facadeClient().url).to.equal('http://sfo-eapps-facd-ld1:8080'); 26 | 27 | }); 28 | 29 | it('should return a proxy URL based on ENV var', function(){ 30 | 31 | var proxy = 'http://localhost:8888'; 32 | process.env.DEBUG_PROXY = proxy; 33 | 34 | expect(config.facadeClient().proxy).to.equal(proxy); 35 | 36 | delete process.env.DEBUG_PROXY; 37 | }); 38 | }); 39 | 40 | 41 | }); -------------------------------------------------------------------------------- /server/views/partials/phoneSelectorOverlay.ejs: -------------------------------------------------------------------------------- 1 |
2 |
3 | 5 | 7 | 8 | 9 | 14 |
15 |
16 | 17 |
18 |
19 |

20 | It looks like your access token may have expired.

Wanna try refreshing it? 21 |

22 | 27 |
28 |
-------------------------------------------------------------------------------- /server/cache/orgchartCacheConfig.js: -------------------------------------------------------------------------------- 1 | var pg = require( 'pg' ); 2 | var isUser; 3 | var connectionString = process.env.DATABASE_URL; 4 | 5 | function getConfig( callback ) { 6 | console.log( 'Retrieving the org chart config' ); 7 | var hierarchyType = null; 8 | pg.connect( connectionString, function ( err, client, done ) { 9 | if ( err ) 10 | console.log( err ); 11 | 12 | //creates a query to grab the config for users or contacts 13 | var query = client.query( 'SELECT * FROM salesforce.orgchartconfig__c LIMIT 1' ); 14 | query.on( 'row', function ( row ) { 15 | hierarchyType = row.orghierarchytype__c; 16 | } ); 17 | 18 | query.on( 'end', function ( result ) { 19 | //true if config is users, or false if contacts. default to users if config is not set 20 | if ( hierarchyType ) { 21 | isUser = hierarchyType != 'Contacts'; 22 | } else { 23 | isUser = true; 24 | } 25 | 26 | console.log( 'Org Chart configured for user hierarchy? ', isUser ); 27 | 28 | done(); 29 | callback(); 30 | } ); 31 | } ); 32 | } 33 | 34 | module.exports = { 35 | getConfig: getConfig, 36 | isUserHierachy: function () { 37 | return isUser; 38 | } 39 | }; 40 | -------------------------------------------------------------------------------- /test/lib/routesTest.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect; 2 | var _ = require('lodash'); 3 | var routes = require('.././routes.js'); 4 | var Auth = require('.././authHandler.js'); 5 | 6 | describe('Routes', function(){ 7 | 8 | it('should export an array', function(){ 9 | expect(routes).to.be.a('array'); 10 | }); 11 | 12 | describe('/auth/signedrequest', function(){ 13 | 14 | var route; 15 | 16 | beforeEach(function(){ 17 | route = _.find(routes, {path: '/auth/signedrequest'}) 18 | }); 19 | 20 | it('should be defined', function() 21 | { 22 | expect(route).to.not.be.undefined; 23 | }); 24 | 25 | it('should be configured for POSTs', function(){ 26 | expect(route).to.have.property('method', 'POST') 27 | }); 28 | 29 | it('should be handled by Auth#signedRequest', function(){ 30 | expect(route.handler).to.equal(Auth.signedRequest); 31 | }); 32 | }); 33 | 34 | describe('/eapps_facade/services/', function(){ 35 | 36 | it('should have a /orgchart/{id} route', function(){ 37 | var route = _.find(routes, {path: '/eapps_facade/services/orgchart/{id}'}); 38 | expect(route).to.exist; 39 | expect(route.method).to.equal('GET'); 40 | }); 41 | 42 | it('should have /orgchart route', function(){ 43 | var route = _.find(routes, {path: '/eapps_facade/services/orgchart'}); 44 | expect(route).to.exist; 45 | }); 46 | 47 | 48 | }); 49 | 50 | }); 51 | 52 | -------------------------------------------------------------------------------- /app/controllers/searchController.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var app = angular.module('orgchart.controllers', []); 4 | 5 | app.controller('searchController', ['$scope', '$timeout', 'Search', '$sce', 'sharedFunctions', 6 | function ($scope, $timeout, Search, $sce, sharedFunctions) { 7 | 8 | $scope.searchEmployee = function (searchString) { 9 | 10 | Search.getSearch({search: searchString}).then(onSuccess, onError); 11 | 12 | function onSuccess(response) { 13 | 14 | if (response.data.length > 0) { 15 | $scope.search = response.data; 16 | } else { 17 | var currSearchString = $sce.trustAsHtml("" + searchString + ""); 18 | $scope.search = [ 19 | {"id": $scope.employee.id, "name": currSearchString + " is not found"} 20 | ]; 21 | } 22 | 23 | sharedFunctions.toggleSearchDisplay("flex"); 24 | 25 | } 26 | 27 | function onError(response) { 28 | if (response.status == 401) { 29 | $scope.toggleSearch(); 30 | $scope.toggleExpiredTokenOverlay(); 31 | } 32 | } 33 | }; 34 | 35 | var init = function () { 36 | var searchResultWrapper = document.getElementById('searchResultWrapper'); 37 | 38 | searchResultWrapper.addEventListener('touchmove', function (event) { 39 | event.preventDefault(); 40 | }); 41 | }; 42 | 43 | init(); 44 | 45 | } 46 | ]); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "OrgChart", 3 | "version": "1.0.0", 4 | "description": "Org Chart", 5 | "main": "server.js", 6 | "scripts": { 7 | "test": "mocha", 8 | "prepublish": "./node_modules/grunt-cli/bin/grunt deploy", 9 | "start": "node server.js" 10 | }, 11 | "author": { 12 | "name": "PDXBrewHub", 13 | "email": "pdxbrewhub@salesforce.com", 14 | "url": "https://git.soma.salesforce.com/PDXBrewHub" 15 | }, 16 | "license": "MIT", 17 | "repository": { 18 | "type": "git", 19 | "url": "https://git.soma.salesforce.com/PDXBrewHub/org-chart-client.git" 20 | }, 21 | "browserify-shim": { 22 | "./app/vendor/canvas-all.js": "Sfdc.canvas" 23 | }, 24 | "dependencies": { 25 | "angular-animate": "^1.4.4", 26 | "angular-sanitize": "^1.4.4", 27 | "angular-touch": "^1.4.4", 28 | "browserify": "^11.0.1", 29 | "ejs": "^2.3.3", 30 | "hapi": "^4.0.0", 31 | "lodash": "^2.4.1", 32 | "lodash-node": "^3.10.1", 33 | "node-schedule": "^0.2.9", 34 | "pg": "4.x", 35 | "request": "^2.34.0", 36 | "salesforce-signed-request": "0.0.1" 37 | }, 38 | "devDependencies": { 39 | "browserify-ngmin": "^0.1.0", 40 | "browserify-shim": "^3.5.0", 41 | "chai": "^1.9.0", 42 | "grunt-autoprefixer": "latest", 43 | "grunt-browserify": "^4.0.0", 44 | "grunt-cli": "latest", 45 | "grunt-contrib-clean": "^0.5.0", 46 | "grunt-contrib-compress": "^0.13.0", 47 | "grunt-contrib-concat": "latest", 48 | "grunt-contrib-cssmin": "latest", 49 | "grunt-contrib-less": "latest", 50 | "grunt-contrib-watch": "latest", 51 | "grunt-filerev": "^2.3.1", 52 | "grunt-filerev-assets": "^0.3.1", 53 | "grunt-purifycss": "^0.1.0", 54 | "mocha": "^1.17.1", 55 | "nock": "^0.27.3", 56 | "uglifyify": "^3.0.1" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /server/cache/cache.js: -------------------------------------------------------------------------------- 1 | var pg = require( 'pg' ); 2 | var connectionString = process.env.DATABASE_URL; 3 | var dataConfig = require( './orgchartDataConfig' ); 4 | var orgConfig = require( './orgchartCacheConfig' ); 5 | 6 | //this file holds the cached data 7 | var people = []; 8 | 9 | /** 10 | * Initializes a temp array to hold the queried data, then 11 | * once the query is complete assigns it to the people cache 12 | */ 13 | var prime = function ( config, callback ) { 14 | console.log( 'Populating people cache' ); 15 | 16 | var tempUsers = []; 17 | 18 | pg.connect( connectionString, function ( err, client, done ) { 19 | if ( err ) { 20 | console.log( err ); 21 | } 22 | 23 | //create a query to grab all contacts from the org 24 | var query = client.query( 'SELECT * FROM ' + config.tableName ); 25 | query.on( 'row', function ( row ) { 26 | /** grab all the data **/ 27 | var orgChartNode = config.getData( row ); 28 | 29 | //one orgChart node is now complete, push orgChartNode into the contacts list 30 | tempUsers.push( orgChartNode ); 31 | } ); 32 | 33 | query.on( 'end', function ( result ) { 34 | console.log( 'User cache populated, size: ', tempUsers.length ); 35 | people = tempUsers; 36 | done(); //call `done()` to release the client back to the pool 37 | callback( people ); 38 | } ); 39 | } ); 40 | }; 41 | 42 | module.exports = { 43 | getOrgchartData: function ( callback ) { 44 | if ( people.length > 0 ) { 45 | callback( people ); 46 | } else { 47 | var type = orgConfig.isUserHierachy() ? 'users' : 'contacts'; 48 | prime( dataConfig[ type ], function ( people ) { 49 | callback( people ); 50 | } ); 51 | } 52 | }, 53 | flush: function () { 54 | console.log( 'Flushed people' ); 55 | people = []; 56 | } 57 | }; -------------------------------------------------------------------------------- /app/css/manager.less: -------------------------------------------------------------------------------- 1 | // This section controls the animation sequence 2 | // for the managers on display. 3 | // 4 | 5 | #managerDetail { 6 | background-color: #FFFFFF; 7 | text-align: center; 8 | width: 100%; 9 | overflow-y: scroll; 10 | height: 325px; 11 | height: ~"calc(100% - 180px)"; // has to be escaped to prevent LESS from screwing it up. 12 | position: absolute; 13 | // The following line is key to protect z-order 14 | // for transformed elements 15 | transform: translate3D(0px,0px,-1px) rotateX(180deg); 16 | } 17 | 18 | .manager-image { 19 | height: 60px; 20 | width: 60px; 21 | background-size: 60px; 22 | border: 4px solid #fff; 23 | border-radius: 50%; 24 | display: inline-block; 25 | background-position: center; 26 | background-repeat: no-repeat; 27 | } 28 | 29 | // The spacing of the managers, and 30 | // the lines that connect the li elements 31 | .reporting-structure { 32 | // The following line is key to protect z-order 33 | // for transformed elements 34 | transform: translate3D(0px,0px,-1px) rotateX(180deg); 35 | position: absolute; 36 | width: 100%; 37 | padding-bottom: 50px; 38 | } 39 | .reporting-structure li { 40 | margin: 30px auto; 41 | position: relative; 42 | } 43 | 44 | // add connectors to before every li except for the first 45 | // add a connector bar on the last list item 46 | // the animation fadeIn is located in the animations less file 47 | .reporting-structure li::before, 48 | .reporting-structure li:nth-last-child(1)::after { 49 | animation: fadeIn 5s 0 1 ease normal; 50 | content: ''; 51 | position: absolute; 52 | top: -62px; 53 | left: 50%; 54 | width: 2px; 55 | height: 70px; 56 | background: #eee; 57 | z-index: -1; 58 | } 59 | .reporting-structure li:nth-last-child(1)::after{ 60 | top: 62px; 61 | } 62 | 63 | // hide the bar on the first item 64 | .reporting-structure li:first-child::before { 65 | display: none; 66 | } 67 | 68 | // The delay sequence for the list elements 69 | // Animation loop 70 | @n: 20; //number of iterations 71 | .loop (@i) when (@i < @n + 1) { 72 | 73 | .reporting-structure li:nth-last-child(@{i}) i, 74 | .reporting-structure li:nth-last-child(@{i}) p { animation-delay: (.04s * (@i - 1)); } 75 | .loop(@i + 1); 76 | } 77 | .loop(1); 78 | -------------------------------------------------------------------------------- /config.js: -------------------------------------------------------------------------------- 1 | Fs = require( 'fs' ); 2 | 3 | 4 | var ssl = function () { 5 | var useSSL = process.env.USE_SSL ? true : false; 6 | if ( useSSL ) { 7 | return { 8 | key: Fs.readFileSync( '/etc/apache2/ssl/host.key' ), 9 | cert: Fs.readFileSync( '/etc/apache2/ssl/server.crt' ) 10 | 11 | }; 12 | } 13 | }; 14 | 15 | var uri = function ( config ) { 16 | return (config.server.tls ? 'https://' : 'http://') + config.host + ':' + config.port 17 | }; 18 | 19 | var debug = function ( config ) { 20 | if ( config.isDevelopment() ) { 21 | return { 22 | request: [ 23 | 'hapi', // log request,handler,response flow 24 | 'error' // logs tagged with error 25 | ] 26 | } 27 | } 28 | }; 29 | 30 | var config = { 31 | host: '0.0.0.0', 32 | port: (parseInt( process.env.PORT, 10 ) || 5150), 33 | appSecret: function () { 34 | return process.env.APP_SECRET 35 | }, 36 | env: function () { 37 | return process.env.NODE_ENV || "development"; 38 | }, 39 | isDevelopment: function () { 40 | return this.env() === 'development'; 41 | }, 42 | //https://github.com/spumko/hapi/blob/master/docs/Reference.md#server-options 43 | server: { 44 | views: { 45 | engines: { 46 | 'ejs': 'ejs' 47 | }, 48 | path: './server/views', 49 | isCached: false 50 | }, 51 | tls: ssl() 52 | 53 | }, 54 | getCacheConfig: function() { 55 | return !this.isDevelopment() ? { cache: { expiresIn: 31536000000 } } : null 56 | }, 57 | facadeClient: function () { 58 | // STAGING URL : https://ngmobile-stage.corp.salesforce.com 59 | var url = process.env.FACADE_URL || 'http://sfo-eapps-facd-ld1:8080'; 60 | var proxy = process.env.DEBUG_PROXY || null; 61 | var ca = proxy ? Fs.readFileSync( './charles-proxy-ssl.crt' ) : null; 62 | 63 | return { 64 | url: url, 65 | proxy: proxy, 66 | ca: ca, 67 | strictSSL: true 68 | }; 69 | 70 | } 71 | 72 | }; 73 | 74 | config.uri = uri( config ); 75 | config.server.debug = debug( config ); 76 | 77 | module.exports = config; 78 | 79 | 80 | -------------------------------------------------------------------------------- /app/css/base.less: -------------------------------------------------------------------------------- 1 | // --------------------------------------------------- 2 | //variables 3 | // --------------------------------------------------- 4 | 5 | @link: #293f54; 6 | 7 | // --------------------------------------------------- 8 | // Resets 9 | // --------------------------------------------------- 10 | 11 | /* apply a natural box layout model to all elements */ 12 | *, *:before, *:after { 13 | -moz-box-sizing: border-box; -webkit-box-sizing: border-box; box-sizing: border-box; 14 | } 15 | 16 | body { 17 | font-family: 'SalesforceSans'; 18 | background: #fff; 19 | color: #999999; 20 | /* font-smoothing and tap-highlight */ 21 | font-smoothing: antialiased; 22 | -webkit-tap-highlight-color: rgba(0, 0, 0, 0); 23 | margin: 0; 24 | padding: 0; 25 | } 26 | 27 | // Links 28 | a, 29 | .icon-utility-call { 30 | color: @link; 31 | text-decoration: none; 32 | } 33 | 34 | // lists pattern from S1 guidelines 35 | //TODO: refactor for speed 36 | .list-horizontal { 37 | margin: 0; 38 | padding: 0; 39 | list-style:none; 40 | > li { 41 | &, 42 | > a { 43 | display:inline-block; 44 | *display:inline; 45 | zoom:1; 46 | } 47 | } 48 | } 49 | 50 | .list-plain { 51 | list-style: none; 52 | } 53 | 54 | p { 55 | margin: 0; 56 | padding: 0; 57 | } 58 | 59 | ul { 60 | list-style: none; 61 | margin: 0; 62 | padding: 0; 63 | } 64 | // --------------------------------------------------- 65 | // Miscellaneous 66 | // --------------------------------------------------- 67 | 68 | // box shadow, could be refactored into a mixin. 69 | .shadow { 70 | box-shadow: 0 2px 0 rgba(0,0,0,.1); 71 | } 72 | 73 | .hide { 74 | display: none !important; 75 | } 76 | 77 | .show { 78 | display: block !important; 79 | } 80 | 81 | .fadeIt { 82 | opacity: 0.01; 83 | } 84 | 85 | .unfadeIt { 86 | opacity: 1.0; 87 | } 88 | 89 | .showFlex { 90 | display: flex !important; 91 | } 92 | 93 | .capitalize { 94 | text-transform: capitalize; 95 | } 96 | 97 | .overflowScrolling-touch { 98 | -webkit-overflow-scrolling: touch; 99 | } 100 | 101 | .overflowScrolling-default { 102 | -webkit-overflow-scrolling: auto; 103 | } 104 | 105 | #mainWrapper { 106 | height: 500px; 107 | position: relative; 108 | background-color: #fff; 109 | } 110 | 111 | #detailWrapper { 112 | margin: 0; 113 | padding: 0; 114 | } 115 | 116 | -------------------------------------------------------------------------------- /app/css/media_queries.less: -------------------------------------------------------------------------------- 1 | /* Iphone 5 in Portrait */ 2 | @media only screen 3 | and (min-device-width : 320px) 4 | and (max-device-width : 568px) 5 | and (orientation : portrait) { 6 | // #managerDetail { 7 | // height: 325px; 8 | // } 9 | } 10 | 11 | /* Iphone 4S Portrait */ 12 | @media only screen 13 | and (min-device-width : 320px) 14 | and (max-device-width : 480px) 15 | and (orientation : portrait) { 16 | // #managerDetail { 17 | // height: 325px; 18 | // } 19 | } 20 | 21 | /* Retina Ipad Portrait */ 22 | @media only screen 23 | and (min-device-width : 768px) 24 | and (max-device-width : 1024px) 25 | and (orientation : portrait) 26 | and (-webkit-min-device-pixel-ratio: 2) { 27 | // #managerDetail { 28 | // height: 855px; 29 | // } 30 | #reportsListView { 31 | div { 32 | width: 90%; 33 | } 34 | .title { 35 | width: initial; 36 | margin: 0; 37 | } 38 | .name { 39 | width: initial; 40 | margin: 0; 41 | } 42 | } 43 | } 44 | 45 | /* Retina Ipad Landscape */ 46 | @media only screen 47 | and (min-device-width : 768px) 48 | and (max-device-width : 1024px) 49 | and (orientation : landscape) 50 | and (-webkit-min-device-pixel-ratio: 2) { 51 | // #managerDetail { 52 | // height: 575px; 53 | // } 54 | #reportsListView { 55 | div { 56 | width: 90%; 57 | } 58 | .title { 59 | width: initial; 60 | margin: 0; 61 | } 62 | .name { 63 | width: initial; 64 | margin: 0; 65 | } 66 | } 67 | } 68 | 69 | /* Prior Gen Ipad Portrait */ 70 | @media only screen 71 | and (min-device-width : 768px) 72 | and (max-device-width : 1024px) 73 | and (orientation : portrait) { 74 | // #managerDetail { 75 | // height: 795px; 76 | // } 77 | #reportsListView { 78 | div { 79 | width: 90%; 80 | } 81 | .title { 82 | width: initial; 83 | margin: 0; 84 | } 85 | .name { 86 | width: initial; 87 | margin: 0; 88 | } 89 | } 90 | } 91 | 92 | /* Prior Gen Ipad Landscape */ 93 | @media only screen 94 | and (min-device-width : 768px) 95 | and (max-device-width : 1024px) 96 | and (orientation : landscape) { 97 | // #managerDetail { 98 | // height: 535px; 99 | // } 100 | #reportsListView { 101 | div { 102 | width: 90%; 103 | } 104 | .title { 105 | width: initial; 106 | margin: 0; 107 | } 108 | .name { 109 | width: initial; 110 | margin: 0; 111 | } 112 | } 113 | } -------------------------------------------------------------------------------- /server/views/home.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Salesforce OrgChart 6 | 7 | 8 | 9 | 10 | 11 | 12 | <% if(config.isDevelopment()){ %> 13 | 14 | <% } else { var path = assetManifest[ 'dist/css/style.css' ]; %> 15 | 16 | <% } %> 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 27 | 29 | 30 | 31 | <% include partials/searchOverlay %> 32 | 33 | <% include partials/phoneSelectorOverlay %> 34 | 35 |
36 | 37 | <% include partials/managerDetail %> 38 | 39 | <% include partials/employeeDetail %> 40 | 41 | <% include partials/reportsListView %> 42 | 43 | <% if(typeof canvasContext !== 'undefined') { %> 44 | 48 | <% } %> 49 | 50 | 51 | 52 | <% if(config.isDevelopment()){ %> 53 | 54 | <% } else { var jsPath = assetManifest[ 'dist/js/bundle.js' ]; %> 55 | 56 | <% } %> 57 | 58 | <% // WORKAROUND FOR SF1 KEYBOARD BUG %> 59 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /app/css/overlay.less: -------------------------------------------------------------------------------- 1 | // styles for the overlay and search wrappers 2 | 3 | #searchResultWrapper { 4 | z-index: 14; 5 | position: absolute; 6 | top: 48px; 7 | left: 0; 8 | width: 100%; 9 | height: 100%; 10 | display: -webkit-box; 11 | display: -moz-box; 12 | display: -ms-flexbox; 13 | display: -webkit-flex; 14 | align-items: center; 15 | justify-content: center; 16 | } 17 | 18 | #searchResultWrapper_row { 19 | width: 100%; 20 | height: 100%; 21 | } 22 | 23 | #searchResultWrapper ul { 24 | /* margin: 0 42px 0 15px; */ 25 | height: 100%; 26 | padding: 5px 0; 27 | background-color: white; 28 | /* border-bottom-right-radius:5px; 29 | border-bottom-left-radius:5px; */ 30 | } 31 | 32 | #searchResultWrapper ul li { 33 | cursor: pointer; 34 | padding: 0; 35 | margin: 10px 10px; 36 | } 37 | 38 | #searchBoxWrapper { 39 | position: absolute; 40 | top: 5px; 41 | left: 50%; 42 | z-index: 15; 43 | width: 86%; 44 | margin-left: -47%; 45 | } 46 | 47 | #searchBox { 48 | width: 100%; 49 | height: 36px; 50 | padding: 6px 10px; 51 | border-radius: 5px; 52 | border: 1px solid #85929F; 53 | font-size: 16px; 54 | } 55 | 56 | #searchToggle { 57 | padding: 0; 58 | position: fixed; 59 | right: 0; 60 | top: 0; 61 | z-index: 999; 62 | height: 44px; 63 | width: 44px; 64 | background: 0; 65 | border: 0; 66 | outline: 0; 67 | } 68 | 69 | .icon-utility-search { 70 | color: #999; 71 | } 72 | 73 | .unknownSearch { 74 | color: black; 75 | } 76 | 77 | .icon-utility-close { 78 | color: white; 79 | } 80 | 81 | .icon-utility-close, .icon-utility-search { 82 | font-size: 15px; 83 | } 84 | 85 | #closeSelector { 86 | margin: 0; 87 | padding: 0; 88 | z-index: 15; 89 | position: absolute; 90 | top: -44px; 91 | right: -4px; 92 | } 93 | 94 | #phoneSelector { 95 | z-index: 15; 96 | position: absolute; 97 | height: 100%; 98 | width: 100%; 99 | padding: 0; 100 | margin: 0; 101 | display: -webkit-box; 102 | display: -moz-box; 103 | display: -ms-flexbox; 104 | display: -webkit-flex; 105 | display: flex; 106 | align-items: center; 107 | justify-content: center; 108 | } 109 | 110 | #expiredTokenOverlay { 111 | z-index: 15; 112 | position: absolute; 113 | height: 100%; 114 | width: 100%; 115 | padding: 0; 116 | margin: 0; 117 | display: -webkit-box; 118 | display: -moz-box; 119 | display: -ms-flexbox; 120 | display: -webkit-flex; 121 | display: flex; 122 | align-items: center; 123 | justify-content: center; 124 | } 125 | 126 | #expiredTokenMessage { 127 | width: 100%; 128 | padding: 10px; 129 | text-align: center; 130 | } 131 | 132 | #selectorRow { 133 | width: 260px; 134 | border-radius: 5px; 135 | background-color: white; 136 | padding: 14px; 137 | position: relative; 138 | } 139 | 140 | .selection li { 141 | border: 1px solid #dfe0e1; 142 | color: #5c7995; 143 | font-size: 16px; 144 | font-weight: 700; 145 | width: 100%; 146 | padding: 7px 0; 147 | margin: 0 auto 14px auto; 148 | text-align: center; 149 | cursor: pointer; 150 | border-radius: 5px; 151 | &:last-child { 152 | margin: 0 !important; 153 | } 154 | } 155 | 156 | #overlay { 157 | opacity: 0.5; 158 | background: #344A5F; 159 | width: 100%; 160 | height: 100%; 161 | z-index: 10; 162 | top: 0; 163 | left: 0; 164 | position: fixed; 165 | } -------------------------------------------------------------------------------- /server/cache/orgchartModel.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var OrgchartModel = function ( data ) { 4 | this.id = data.sfid; 5 | this.name = data.name; 6 | this.firstName = data.firstname; 7 | this.lastName = data.lastname; 8 | this.title = data.title; 9 | this.phones = buildPhoneArray( { 10 | 'desk': data.phone || null, 11 | 'mobilePhone': data.mobilephone || null, 12 | 'otherPhone': data.otherPhone || null 13 | } ); 14 | this.email = data.email; 15 | this.photo = { 16 | largePhotoUrl: data.photourl || data.fullphotourl || null, 17 | smallPhotoUrl: data.photourl || data.smallphotourl || null 18 | }; 19 | this.reportsToId = data.reportstoid || data.managerid; 20 | this.peers = []; 21 | this.directReports = []; 22 | this.reportsTo = []; 23 | }; 24 | 25 | function buildPhoneArray( phones ) { 26 | var phoneArray = []; 27 | 28 | for ( var i in phones ) { 29 | if ( phones[ i ] != null ) { 30 | var phoneObject = { 31 | type: i, 32 | value: phones[ i ] 33 | }; 34 | phoneArray.push( phoneObject ); 35 | } 36 | } 37 | 38 | return phoneArray; 39 | } 40 | 41 | function getManagerChain( currentPerson, orgchartData ) { 42 | var reportsTo = []; 43 | if ( !currentPerson.reportsTo ) { 44 | return; 45 | } 46 | 47 | function getManager( current, orgData ) { 48 | for ( var i = 0; i < orgData.length; i++ ) { 49 | var person = orgData[ i ]; 50 | if ( current.reportsToId === person.id ) { 51 | reportsTo.push( person ); 52 | getManager( person, orgData ) 53 | } else { 54 | clearNeighborhood( person ); 55 | } 56 | } 57 | } 58 | 59 | getManager( currentPerson, orgchartData ); 60 | 61 | return reportsTo.reverse(); 62 | } 63 | 64 | function getPeers( currentPerson, orgchartData ) { 65 | var peers = []; 66 | 67 | for ( var i = 0; i < orgchartData.length; i++ ) { 68 | var person = orgchartData[ i ]; 69 | //build peersArray (not sorted), all share the same boss. 70 | //The top of the hierarchy has no peers. 71 | if ( currentPerson.reportsToId != null && currentPerson.reportsToId === person.reportsToId && currentPerson.email != person.email ) { 72 | clearNeighborhood( person ); 73 | peers.push( person ); 74 | } 75 | } 76 | 77 | return peers; 78 | } 79 | 80 | function getDirectReports( currentPerson, orgchartData ) { 81 | var directReports = []; 82 | 83 | //build directReports list (not sorted), these are ones minions 84 | for ( var i = 0; i < orgchartData.length; i++ ) { 85 | var person = orgchartData[ i ]; 86 | 87 | if ( currentPerson.id === person.reportsToId ) { 88 | clearNeighborhood( person ); 89 | directReports.push( person ); 90 | } 91 | } 92 | 93 | return directReports; 94 | } 95 | 96 | function clearNeighborhood( person ) { 97 | person.peers = []; 98 | person.directReports = []; 99 | person.reportsTo = []; 100 | } 101 | 102 | OrgchartModel.prototype.getNeighborhood = function ( orgchartData ) { 103 | return { 104 | reportsTo: getManagerChain( this, orgchartData ), 105 | peers: getPeers( this, orgchartData ), 106 | directReports: getDirectReports( this, orgchartData ) 107 | } 108 | }; 109 | 110 | module.exports = OrgchartModel; -------------------------------------------------------------------------------- /app/vendor/shake.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * Find more about this plugin by visiting 4 | * http://alxgbsn.co.uk/ 5 | * 6 | * Copyright (c) 2010-2012 Alex Gibson 7 | * Released under MIT license 8 | * 9 | */ 10 | 11 | (function (window, document) { 12 | 13 | function Shake() { 14 | 15 | //feature detect 16 | this.hasDeviceMotion = 'ondevicemotion' in window; 17 | 18 | //default velocity threshold for shake to register 19 | this.threshold = 15; 20 | 21 | //use date to prevent multiple shakes firing 22 | this.lastTime = new Date(); 23 | 24 | //accelerometer values 25 | this.lastX = null; 26 | this.lastY = null; 27 | this.lastZ = null; 28 | 29 | //create custom event 30 | if (typeof document.CustomEvent === "function") { 31 | this.event = new document.CustomEvent('shake', { 32 | bubbles: true, 33 | cancelable: true 34 | }); 35 | } else if (typeof document.createEvent === "function") { 36 | this.event = document.createEvent('Event'); 37 | this.event.initEvent('shake', true, true); 38 | } else { 39 | return false; 40 | } 41 | } 42 | 43 | //reset timer values 44 | Shake.prototype.reset = function () { 45 | this.lastTime = new Date(); 46 | this.lastX = null; 47 | this.lastY = null; 48 | this.lastZ = null; 49 | }; 50 | 51 | //start listening for devicemotion 52 | Shake.prototype.start = function () { 53 | this.reset(); 54 | if (this.hasDeviceMotion) { window.addEventListener('devicemotion', this, false); } 55 | }; 56 | 57 | //stop listening for devicemotion 58 | Shake.prototype.stop = function () { 59 | 60 | if (this.hasDeviceMotion) { window.removeEventListener('devicemotion', this, false); } 61 | this.reset(); 62 | }; 63 | 64 | //calculates if shake did occur 65 | Shake.prototype.devicemotion = function (e) { 66 | 67 | var current = e.accelerationIncludingGravity, 68 | currentTime, 69 | timeDifference, 70 | deltaX = 0, 71 | deltaY = 0, 72 | deltaZ = 0; 73 | 74 | if ((this.lastX === null) && (this.lastY === null) && (this.lastZ === null)) { 75 | this.lastX = current.x; 76 | this.lastY = current.y; 77 | this.lastZ = current.z; 78 | return; 79 | } 80 | 81 | deltaX = Math.abs(this.lastX - current.x); 82 | deltaY = Math.abs(this.lastY - current.y); 83 | deltaZ = Math.abs(this.lastZ - current.z); 84 | 85 | if (((deltaX > this.threshold) && (deltaY > this.threshold)) || ((deltaX > this.threshold) && (deltaZ > this.threshold)) || ((deltaY > this.threshold) && (deltaZ > this.threshold))) { 86 | //calculate time in milliseconds since last shake registered 87 | currentTime = new Date(); 88 | timeDifference = currentTime.getTime() - this.lastTime.getTime(); 89 | 90 | if (timeDifference > 1000) { 91 | window.dispatchEvent(this.event); 92 | this.lastTime = new Date(); 93 | } 94 | } 95 | 96 | this.lastX = current.x; 97 | this.lastY = current.y; 98 | this.lastZ = current.z; 99 | 100 | }; 101 | 102 | //event handler 103 | Shake.prototype.handleEvent = function (e) { 104 | 105 | if (typeof (this[e.type]) === 'function') { 106 | return this[e.type](e); 107 | } 108 | }; 109 | 110 | //create a new instance of shake.js. 111 | var myShakeEvent = new Shake(); 112 | myShakeEvent && myShakeEvent.start(); 113 | 114 | }(window, document)); 115 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Salesforce Org Chart: Local Set-up 2 | ###### Salesforce Org Chart is web application that is being designed to run as a SF1 canvas app. This read me is a tutorial for setting up and running Org Chart locally. Please note, that this guide assumes that you already have Org Chart running on heroku. 3 | 4 | + [Prepare The App](README.md#Prepare) 5 | + [Building](README.md#Building) 6 | + [Local Back-up](README.md#Local) 7 | + [Testing](README.md#testing) 8 | + [Development Extras](README.md#development-extras) 9 | + [Deploying](README.md#deploying) 10 | 11 |   12 | 13 | --------------------- 14 | #### Prepare the app 15 | Get the project source and install dependencies: 16 | 17 | ```shell 18 | $ git clone [dest dir] 19 | $ npm install 20 | ``` 21 | 22 | Run the node server to see the site locally: 23 | 24 | ```shell 25 | $ APP_SECRET= node server.js 26 | ``` 27 | 28 | The application uses the following Environment variables: 29 | 30 | 31 | * `APP_SECRET` - **Required**, The secret key from your connected app. Used to decode the signed request. 32 | You will need a connected app in the desired salesforce org to get a client secret. [See this SF1 guide on setting up a connected app](https://developer.salesforce.com/docs/atlas.en-us.salesforce1.meta/salesforce1/canvas_custom_action_create_canvas_app_task.htm) 33 | * `NODE_ENV` - Used to determine the environment running. Defaults to "development". Used to adjust the logging for debugging. 34 | 35 | This will create a locally running node.js server, in order to run the UI application you must go through SF1. This requires: 36 | 37 | * A SF1 enabled org 38 | * A canvas connected app (see the `APP_SECRET` above) on that org. 39 | * The canvas app should be added to the Mobile Navigation of your org. 40 | 41 | Once those are in place you can login to the org and use SF1 through the browser by visiting `/one/one.app`. Then use the left-hand nav 42 | to visit your canvas app. 43 | 44 |   45 | 46 | --------------- 47 | #### Building 48 | 49 | There are several components of the application that require a build - namely the JavaScript and CSS. To build these you will need [gruntjs](http://gruntjs.com): 50 | 51 | * Install grunt (globally is recommended by them): 52 | 53 | ```shell 54 | $ npm install grunt 55 | ``` 56 | 57 | * The default task will build both the CSS and JS: 58 | 59 | ```shell 60 | $ grunt 61 | ``` 62 | 63 | For production build, set the `NODE_ENV`: 64 | 65 | ```shell 66 | $ NODE_ENV=production grunt 67 | ``` 68 | 69 | This will produce minified and GZipped JavaScript bundle with no source maps for debugging. 70 | 71 |   72 | 73 | -------------- 74 | #### Local Back-up 75 | 76 | + Install [PostGresSQL](http://www.postgresql.org/download/) 77 | + Capture the database and store it locally. 78 | ```shell 79 | $ heroku pg:backups capture 80 | $ curl -o latest.dump `heroku pg:backups public-url` 81 | $ pg_restore --verbose --clean --no-acl --no-owner -h localhost -U orgChartUser -d orgchart latest.dump 82 | ``` 83 | 84 |   85 | 86 | -------- 87 | 88 | #### Testing 89 | Install mocha with `-g` or better yet add [`node_modules` to your `PATH`](http://stackoverflow.com/a/15157360/42998) 90 | 91 | ```shell 92 | $ npm test 93 | ``` 94 | 95 | #### Development Extras: 96 | * Install the Chrome extension Livereload in order to automatically refresh your dev browser as you make changes in your editor 97 | * Get Nodemon to automatically restart your node server as you make js file changes. Use the -e flag to include the extensions we want to monitor. 98 | 99 | ```shell 100 | $ npm install -g nodemon 101 | $ nodemon server.js -e js,ejs 102 | ``` 103 | 104 | [Webstorm](http://www.jetbrains.com/webstorm/) can also be used to run the server and unit tests and is a good IDE for JS development -------------------------------------------------------------------------------- /app/services.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var module = angular.module('orgchart.services', []); 4 | 5 | var headers = function () { 6 | return { 7 | headers: { 8 | 'Access-Token': canvasContext.client.oauthToken, 9 | 'Instance-Url': canvasContext.client.instanceUrl 10 | } 11 | } 12 | }; 13 | 14 | //local storage caching 15 | module.service('orgChartCacheService', function ($http, $angularCacheFactory, $q) { 16 | 17 | //default cache definition 18 | var employeeCache = $angularCacheFactory('employeeCache', { 19 | maxAge: 43200000, //12 hours 20 | deleteOnExpire: 'aggressive', 21 | storageMode: 'localStorage', 22 | recycleFreq: 1000, 23 | capacity: 20 24 | }); 25 | 26 | this.getCacheValue = function (value) { 27 | return employeeCache.get(value); 28 | }; 29 | 30 | this.callService = function (url) { 31 | var promise = $http.get(url, headers()); 32 | 33 | promise.success(function (data, status, headers, config) { 34 | //set maxAge defined from node 35 | setCacheAge(headers('cache-control')); 36 | }); 37 | 38 | 39 | function setCacheAge(value) { 40 | // Split the string by = and , | This returns value,1,value 41 | var splitStr = value.split(/\s*[=|,]\s*/); 42 | var valueIndex = splitStr.indexOf('max-age'); 43 | 44 | // If the value is found, exists, is number 45 | if ((valueIndex != -1) && (splitStr.length > (valueIndex + 1)) && (!isNaN(splitStr[valueIndex + 1]))) { 46 | valueIndex = splitStr[valueIndex + 1]; 47 | employeeCache.setOptions({maxAge: (parseInt(valueIndex) * 1000)}); 48 | } 49 | } 50 | 51 | return promise; 52 | }; 53 | 54 | this.me = function (email) { 55 | 56 | var deferred = $q.defer(); 57 | 58 | var cacheValue = this.getCacheValue('me'); 59 | if (angular.isDefined(cacheValue).email == email) { 60 | deferred.resolve(cacheValue); 61 | } else { 62 | this.callService('/services/orgchart/' + canvasContext.context.user.email).then(function (response) { 63 | employeeCache.put('me', response.data); 64 | employeeCache.put(response.data.email, response.data); 65 | 66 | deferred.resolve(response.data, status); 67 | }, function (response) { 68 | deferred.reject(response); 69 | }); 70 | } 71 | 72 | return deferred.promise; 73 | }; 74 | 75 | this.getEmployee = function (email) { 76 | 77 | var deferred = $q.defer(); 78 | 79 | var cacheValue = this.getCacheValue(email); 80 | 81 | if (angular.isDefined(cacheValue)) { 82 | deferred.resolve(cacheValue); 83 | } else { 84 | this.callService('/services/orgchart/' + email).then(function (response) { 85 | employeeCache.put(response.data.email, response.data); 86 | 87 | deferred.resolve(response.data, status); 88 | }, function (response) { 89 | deferred.reject(response); 90 | }); 91 | } 92 | 93 | return deferred.promise; 94 | }; 95 | 96 | }); 97 | 98 | module.factory('Search', ['$http', function ($http) { 99 | return { 100 | getSearch: function (searchQuery) { 101 | //default 'application/json' content type. 102 | return $http.post('/services/orgchart', searchQuery, headers()); 103 | } 104 | }; 105 | }]); 106 | 107 | module.service('sharedFunctions', function () { 108 | //hides search results element 109 | this.toggleSearchDisplay = function (value) { 110 | var wrapperEle = document.getElementById("searchResultWrapper"); 111 | 112 | wrapperEle.className = value; 113 | }; 114 | }); -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | var Config = require('./config.js'); 2 | 3 | var browserifyTransforms = ['browserify-shim','browserify-ngmin']; 4 | 5 | var buildJSTasks = ['browserify', 'clean', 'filerev', 'filerev_assets']; 6 | 7 | if (!Config.isDevelopment()) { 8 | browserifyTransforms.push('uglifyify'); 9 | buildJSTasks.push('compress'); 10 | } 11 | 12 | var buildCSSTasks = ['less', 'autoprefixer', 'purifycss']; 13 | if(!Config.isDevelopment()){ 14 | buildCSSTasks.push('cssmin'); 15 | } 16 | 17 | module.exports = function (grunt) { 18 | 19 | grunt.initConfig({ 20 | pkg: grunt.file.readJSON('package.json'), 21 | 22 | isDev: Config.isDevelopment(), 23 | 24 | browserify: { 25 | './dist/js/bundle.js': ['./app/app.js'], 26 | options: { 27 | browserifyOptions: { 28 | debug: '<%= isDev %>' 29 | }, 30 | transform: browserifyTransforms 31 | } 32 | }, 33 | 34 | uglify: { 35 | js: { 36 | options: { 37 | mangle: false 38 | }, 39 | files: { 40 | 'dist/js/bundle.js': ['dist/js/bundle.js'] 41 | } 42 | } 43 | }, 44 | 45 | filerev: { 46 | options: { 47 | encoding: 'utf8', 48 | algorithm: 'md5', 49 | length: 8 50 | }, 51 | assets: { 52 | src: 'dist/{fonts,js,css,images}/*.{jpg,jpeg,gif,png,webp,css,js,woff}', 53 | dest: 'dist/compiled', 54 | filter: function(src){ 55 | return src.indexOf('<%= filerev.assets.dest %>') < 0; 56 | } 57 | } 58 | }, 59 | clean: ['<%= filerev.assets.dest %>'], 60 | 61 | filerev_assets: { 62 | dist: { 63 | options: { 64 | dest: '<%= filerev.assets.dest %>' + '/assetManifest.json', 65 | prettyPrint: true, 66 | cwd: 'app/' 67 | } 68 | } 69 | }, 70 | 71 | less: { 72 | style: { 73 | files: { 74 | "dist/css/style.css": "app/css/style.less" 75 | } 76 | } 77 | }, 78 | 79 | autoprefixer: { 80 | options: { 81 | browsers: ["last 2 Chrome versions", "ios >= 7"] 82 | }, 83 | dist: { 84 | files: { 85 | 'dist/css/style.css': 'dist/css/style.css' 86 | } 87 | } 88 | }, 89 | 90 | cssmin: { 91 | css: { 92 | src: 'dist/css/style.css', 93 | dest: 'dist/css/style.css' 94 | } 95 | }, 96 | 97 | purifycss: { 98 | options: {}, 99 | target: { 100 | src: ['app/*.js', 'server/views/**/*.ejs'], 101 | css: ['dist/css/style.css'], 102 | dest: 'dist/css/style.css' 103 | } 104 | }, 105 | 106 | watch: { 107 | css: { 108 | files: ['app/css/*'], 109 | tasks: ['buildCSS'], 110 | // NOTE: you need to install the livereload extension for chrome. 111 | options: { 112 | livereload: true 113 | } 114 | }, 115 | js: { 116 | files: ['app/*.js'], 117 | tasks: ['buildJavaScript'] 118 | } 119 | }, 120 | 121 | compress: { 122 | main: { 123 | options: { 124 | mode: 'gzip', 125 | level: '9' 126 | }, 127 | src: ['dist/js/bundle.js'], 128 | dest: 'dist/js/bundle.js.gz' 129 | } 130 | } 131 | }); 132 | 133 | grunt.registerTask('buildJavaScript', buildJSTasks); 134 | grunt.registerTask('buildCSS', buildCSSTasks); 135 | grunt.registerTask('default', ['buildCSS', 'buildJavaScript']); 136 | grunt.registerTask('deploy', ['buildCSS', 'cssmin', 'buildJavaScript', 'compress']); 137 | 138 | grunt.loadNpmTasks('grunt-contrib-less' ); 139 | grunt.loadNpmTasks('grunt-contrib-watch' ); 140 | grunt.loadNpmTasks('grunt-contrib-cssmin' ); 141 | grunt.loadNpmTasks('grunt-autoprefixer' ); 142 | grunt.loadNpmTasks('grunt-browserify' ); 143 | grunt.loadNpmTasks('grunt-contrib-compress' ); 144 | grunt.loadNpmTasks('grunt-filerev' ); 145 | grunt.loadNpmTasks('grunt-filerev-assets' ); 146 | grunt.loadNpmTasks('grunt-contrib-clean' ); 147 | grunt.loadNpmTasks('grunt-purifycss' ); 148 | 149 | }; 150 | -------------------------------------------------------------------------------- /test/lib/handlers/authTest.js: -------------------------------------------------------------------------------- 1 | var Hapi = require('hapi'); 2 | var expect = require('chai').expect; 3 | var _ = require('lodash'); 4 | 5 | var routes = require('../.././routes'); 6 | var AuthHandler = require('../.././auth'); 7 | var config = require('../../../config'); 8 | 9 | 10 | describe('AuthHandler', function () { 11 | 12 | var server; 13 | before(function(){ 14 | server = new Hapi.Server(_.merge({debug:false}, config.server)); 15 | server.route(routes); 16 | }); 17 | 18 | it('should return a 500 error if there is no APP_SECRET configured', function(done){ 19 | 20 | delete process.env.APP_SECRET; 21 | 22 | server.inject({ 23 | method: 'POST', 24 | url: '/auth/signedrequest', 25 | headers: { 26 | 'Content-Type': 'application/x-www-form-urlencoded', 27 | 'Content-Length': 5 28 | }, 29 | payload: 'foo=bar' 30 | }, function (response) { 31 | expect(response.statusCode).to.equal(500); 32 | done(); 33 | }); 34 | }); 35 | 36 | it('should decode a signed request', function (done) { 37 | 38 | var test_signed_request = "V/lsK06wzoefsXdKVc231sIkTxi5zYV2J2FW1hlu0VQ=.eyJ1c2VySWQiOiIwMDVSMDAwMDAwMERnSkEiLCJjbGllbnQiOnsib2F1dGhUb2tlbiI6IjAwRFIwMDAwMDAwOGJQQyFBUThBUVBVSFBrTEpTUEdDVkpYZjBOak5IQzVCRmZUNHJJQ0tmLlpxMWFrRGlHYXZWcTAzel90dnpiMWY2TlVwT1AzVjFrZG1xY2t1bzY4a0w0bDZwUXczNDZkX1Y4ZFEiLCJpbnN0YW5jZUlkIjoiXzpzaGlwbWVudDoiLCJ0YXJnZXRPcmlnaW4iOiJodHRwczovL21vYmlsZTEudC5zYWxlc2ZvcmNlLmNvbSIsImluc3RhbmNlVXJsIjoiaHR0cHM6Ly9tb2JpbGUxLnQuc2FsZXNmb3JjZS5jb20ifSwiaXNzdWVkQXQiOjEzODQ4MTkwNTksImNvbnRleHQiOnsiYXBwbGljYXRpb24iOnsiZGV2ZWxvcGVyTmFtZSI6InNoaXBtZW50IiwicmVmZXJlbmNlSWQiOiIwOUhSMDAwMDAwMDAwTjkiLCJhcHBsaWNhdGlvbklkIjoiMDZQUjAwMDAwMDAwMGNJIiwiY2FudmFzVXJsIjoiaHR0cHM6Ly9sb2NhbGhvc3Qvc2lnbmVkcmVxdWVzdCIsIm5hbWUiOiJzaGlwbWVudCIsInZlcnNpb24iOiIxLjAiLCJuYW1lc3BhY2UiOm51bGwsImF1dGhUeXBlIjoiU0lHTkVEX1JFUVVFU1QifSwib3JnYW5pemF0aW9uIjp7Im9yZ2FuaXphdGlvbklkIjoiMDBEUjAwMDAwMDA4YlBDTUFZIiwiY3VycmVuY3lJc29Db2RlIjoiVVNEIiwibXVsdGljdXJyZW5jeUVuYWJsZWQiOmZhbHNlLCJuYW1lIjoiU2FtJ3MgUzEgVGVzdCBPcmciLCJuYW1lc3BhY2VQcmVmaXgiOm51bGx9LCJlbnZpcm9ubWVudCI6eyJkaW1lbnNpb25zIjp7ImhlaWdodCI6IjkwMHB4IiwibWF4SGVpZ2h0IjoiMjAwMHB4IiwibWF4V2lkdGgiOiIxMDAwcHgiLCJ3aWR0aCI6IjgwMHB4In0sImRpc3BsYXlMb2NhdGlvbiI6IkNoYXR0ZXIiLCJ1aVRoZW1lIjoiVGhlbWUzIiwidmVyc2lvbiI6eyJhcGkiOiIyOS4wIiwic2Vhc29uIjoiV0lOVEVSIn0sImxvY2F0aW9uVXJsIjoiaHR0cHM6Ly9tb2JpbGUxLnQuc2FsZXNmb3JjZS5jb20vX3VpL2NvcmUvY2hhdHRlci91aS9DaGF0dGVyUGFnZSIsInBhcmFtZXRlcnMiOnt9fSwibGlua3MiOnsibG9naW5VcmwiOiJodHRwczovL21vYmlsZTEudC5zYWxlc2ZvcmNlLmNvbS8iLCJjaGF0dGVyRmVlZEl0ZW1zVXJsIjoiL3NlcnZpY2VzL2RhdGEvdjI5LjAvY2hhdHRlci9mZWVkLWl0ZW1zIiwiY2hhdHRlckZlZWRzVXJsIjoiL3NlcnZpY2VzL2RhdGEvdjI5LjAvY2hhdHRlci9mZWVkcyIsImNoYXR0ZXJHcm91cHNVcmwiOiIvc2VydmljZXMvZGF0YS92MjkuMC9jaGF0dGVyL2dyb3VwcyIsImNoYXR0ZXJVc2Vyc1VybCI6Ii9zZXJ2aWNlcy9kYXRhL3YyOS4wL2NoYXR0ZXIvdXNlcnMiLCJlbnRlcnByaXNlVXJsIjoiL3NlcnZpY2VzL1NvYXAvYy8yOS4wLzAwRFIwMDAwMDAwOGJQQyIsIm1ldGFkYXRhVXJsIjoiL3NlcnZpY2VzL1NvYXAvbS8yOS4wLzAwRFIwMDAwMDAwOGJQQyIsInBhcnRuZXJVcmwiOiIvc2VydmljZXMvU29hcC91LzI5LjAvMDBEUjAwMDAwMDA4YlBDIiwicXVlcnlVcmwiOiIvc2VydmljZXMvZGF0YS92MjkuMC9xdWVyeS8iLCJyZWNlbnRJdGVtc1VybCI6Ii9zZXJ2aWNlcy9kYXRhL3YyOS4wL3JlY2VudC8iLCJyZXN0VXJsIjoiL3NlcnZpY2VzL2RhdGEvdjI5LjAvIiwic2VhcmNoVXJsIjoiL3NlcnZpY2VzL2RhdGEvdjI5LjAvc2VhcmNoLyIsInNvYmplY3RVcmwiOiIvc2VydmljZXMvZGF0YS92MjkuMC9zb2JqZWN0cy8iLCJ1c2VyVXJsIjoiLzAwNVIwMDAwMDAwRGdKQUlBMCJ9LCJ1c2VyIjp7InVzZXJJZCI6IjAwNVIwMDAwMDAwRGdKQUlBMCIsImZpcnN0TmFtZSI6IlRlc3QiLCJsYXN0TmFtZSI6IlVzZXIiLCJlbWFpbCI6InNyZWFkeUBzYWxlc2ZvcmNlLmNvbSIsInVzZXJUeXBlIjoiU1RBTkRBUkQiLCJpc0RlZmF1bHROZXR3b3JrIjp0cnVlLCJwcm9maWxlSWQiOiIwMGVSMDAwMDAwMExzWU4iLCJwcm9maWxlUGhvdG9VcmwiOiJodHRwczovL2MubW9iaWxlMS5jb250ZW50LnQuZm9yY2UuY29tL3Byb2ZpbGVwaG90by8wMDUvRiIsIm5ldHdvcmtJZCI6bnVsbCwiY3VycmVuY3lJU09Db2RlIjoiVVNEIiwicm9sZUlkIjpudWxsLCJzaXRlVXJsUHJlZml4IjpudWxsLCJhY2Nlc3NpYmlsaXR5TW9kZUVuYWJsZWQiOmZhbHNlLCJzaXRlVXJsIjpudWxsLCJwcm9maWxlVGh1bWJuYWlsVXJsIjoiaHR0cHM6Ly9jLm1vYmlsZTEuY29udGVudC50LmZvcmNlLmNvbS9wcm9maWxlcGhvdG8vMDA1L1QiLCJsYW5ndWFnZSI6ImVuX1VTIiwidGltZVpvbmUiOiJBbWVyaWNhL0xvc19BbmdlbGVzIiwidXNlck5hbWUiOiJzcmVhZHlAc2ZvbmUubW9iaWxlMSIsImxvY2FsZSI6ImVuX1VTIiwiZnVsbE5hbWUiOiJUZXN0IFVzZXIifX0sImFsZ29yaXRobSI6IkhNQUNTSEEyNTYifQ=="; 39 | var access_token_inside_signed_request = '00DR00000008bPC!AQ8AQPUHPkLJSPGCVJXf0NjNHC5BFfT4rICKf.Zq1akDiGavVq03z_tvzb1f6NUpOP3V1kdmqckuo68kL4l6pQw346d_V8dQ'; 40 | var instance_url_inside_signed_request = 'https://mobile1.t.salesforce.com'; 41 | process.env.APP_SECRET = "6375554712652875436"; 42 | var payload = 'signed_request=' + test_signed_request; 43 | 44 | server.inject({ 45 | method: 'POST', 46 | url: '/auth/signedrequest', 47 | headers: { 48 | 'Content-Type': 'application/x-www-form-urlencoded', 49 | 'Content-Length': payload.length 50 | }, 51 | payload: payload 52 | }, function (response) { 53 | // expect(response.statusCode).to.equal(200); 54 | var result = response.result 55 | done(); 56 | }); 57 | 58 | 59 | }); 60 | 61 | 62 | }); -------------------------------------------------------------------------------- /server/views/partials/employeeDetail.ejs: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | 6 | 8 | 9 | 10 |
11 |
12 | 14 | 15 | 17 | 18 |
19 | 29 |
30 | 33 | 35 | 36 | 38 | 39 | 42 | 44 | 45 | 47 | 48 |
49 |
50 |
51 | 53 |
54 | 55 |

{{employee.name}}

56 | 57 |

58 | 59 |

List reports

60 | 61 |

Back

62 | 63 | 80 |
81 |
82 | -------------------------------------------------------------------------------- /app/vendor/angular-ellipsis.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Angular directive to truncate multi-line text to visible height 3 | * 4 | * @param bind (angular bound value to append) REQUIRED 5 | * @param ellipsisAppend (string) string to append at end of truncated text after ellipsis, can be HTML OPTIONAL 6 | * @param ellipsisSymbol (string) string to use as ellipsis, replaces default '...' OPTIONAL 7 | * @param ellipsisAppendClick (function) function to call if ellipsisAppend is clicked (ellipsisAppend must be clicked) OPTIONAL 8 | * 9 | * @example

10 | * @example

11 | * @example

12 | * @example

13 | * 14 | */ 15 | 16 | "use strict"; 17 | 18 | var app = angular.module('orgchart.directives'); 19 | 20 | app.directive('ellipsis', function ($timeout, $window) { 21 | 22 | return { 23 | restrict: 'A', 24 | scope: { 25 | ngBind: '=', 26 | ellipsisAppend: '@', 27 | ellipsisAppendClick: '&', 28 | ellipsisSymbol: '@' 29 | }, 30 | compile: function (elem, attr, linker) { 31 | 32 | return function (scope, element, attributes) { 33 | /* Window Resize Variables */ 34 | attributes.lastWindowResizeTime = 0; 35 | attributes.lastWindowResizeWidth = 0; 36 | attributes.lastWindowResizeHeight = 0; 37 | attributes.lastWindowTimeoutEvent = null; 38 | /* State Variables */ 39 | attributes.isTruncated = false; 40 | 41 | function buildEllipsis() { 42 | if (typeof(scope.ngBind) !== 'undefined') { 43 | var bindArray = scope.ngBind.split(" "), 44 | i = 0, 45 | ellipsisSymbol = (typeof(attributes.ellipsisSymbol) !== 'undefined') ? attributes.ellipsisSymbol : '…', 46 | appendString = (typeof(scope.ellipsisAppend) !== 'undefined' && scope.ellipsisAppend !== '') ? ellipsisSymbol + '' + scope.ellipsisAppend + '' : ellipsisSymbol; 47 | 48 | attributes.isTruncated = false; 49 | element.html(scope.ngBind); 50 | 51 | // If text has overflow 52 | if (isOverflowed(element)) { 53 | var bindArrayStartingLength = bindArray.length, 54 | initialMaxHeight = element[0].clientHeight; 55 | 56 | element.html(scope.ngBind + appendString); 57 | 58 | // Set complete text and remove one word at a time, until there is no overflow 59 | for (; i < bindArrayStartingLength; i++) { 60 | bindArray.pop(); 61 | element.html(bindArray.join(" ") + appendString); 62 | 63 | if (element[0].scrollHeight < initialMaxHeight || isOverflowed(element) === false) { 64 | attributes.isTruncated = true; 65 | break; 66 | } 67 | } 68 | 69 | // If append string was passed and append click function included 70 | if (ellipsisSymbol != appendString && typeof(scope.ellipsisAppendClick) !== 'undefined' && scope.ellipsisAppendClick !== '') { 71 | element.find('span').bind("click", function (e) { 72 | scope.$apply(scope.ellipsisAppendClick); 73 | }); 74 | } 75 | } 76 | } 77 | } 78 | 79 | /** 80 | * Test if element has overflow of text beyond height or max-height 81 | * 82 | * @param element (DOM object) 83 | * 84 | * @return bool 85 | * 86 | */ 87 | function isOverflowed(thisElement) { 88 | return thisElement[0].scrollHeight > thisElement[0].clientHeight; 89 | } 90 | 91 | /** 92 | * Watchers 93 | */ 94 | 95 | /** 96 | * Execute ellipsis truncate on ngBind update 97 | */ 98 | scope.$watch('ngBind', function () { 99 | buildEllipsis(); 100 | }); 101 | 102 | /** 103 | * Execute ellipsis truncate on ngBind update 104 | */ 105 | scope.$watch('ellipsisAppend', function () { 106 | buildEllipsis(); 107 | }); 108 | 109 | /** 110 | * When window width or height changes - re-init truncation 111 | */ 112 | angular.element($window).bind('resize', function () { 113 | $timeout.cancel(attributes.lastWindowTimeoutEvent); 114 | 115 | attributes.lastWindowTimeoutEvent = $timeout(function () { 116 | if (attributes.lastWindowResizeWidth != window.innerWidth || attributes.lastWindowResizeHeight != window.innerHeight) { 117 | buildEllipsis(); 118 | } 119 | 120 | attributes.lastWindowResizeWidth = window.innerWidth; 121 | attributes.lastWindowResizeHeight = window.innerHeight; 122 | }, 75); 123 | }); 124 | 125 | 126 | }; 127 | } 128 | }; 129 | }); 130 | -------------------------------------------------------------------------------- /app/css/animations.less: -------------------------------------------------------------------------------- 1 | // The animation for a new manager entering the stage 2 | // sourced from Animate.css - http://daneden.me/animate 3 | // Vendor specific prefixes as needed are added through 4 | // grunt on compile. 5 | 6 | .animated { 7 | animation-duration: 1s; 8 | animation-fill-mode: both; 9 | } 10 | 11 | .bounceIn { 12 | animation: bounceIn .350s; 13 | opacity: 1; 14 | } 15 | 16 | @keyframes bounceIn{ 17 | 0% { 18 | transform:scale(1) 19 | } 20 | 60% { 21 | transform:scale(1.3) 22 | } 23 | 100% { 24 | transform:scale(1) 25 | } 26 | } 27 | 28 | @keyframes bounceInDown { 29 | 0% { 30 | opacity: 0; 31 | transform: translateY(-2000px); 32 | } 33 | 34 | 60% { 35 | opacity: 1; 36 | transform: translateY(30px); 37 | } 38 | 39 | 80% { 40 | transform: translateY(-10px); 41 | } 42 | 43 | 100% { 44 | transform: translateY(0); 45 | } 46 | } 47 | 48 | .bounceInDown { 49 | animation-name: bounceInDown; 50 | } 51 | 52 | 53 | @keyframes fadeIn { 54 | 0% { 55 | opacity: 0; 56 | } 57 | 58 | 100% { 59 | opacity: 1; 60 | } 61 | } 62 | 63 | .fadeIn { 64 | animation-name: fadeIn; 65 | } 66 | 67 | 68 | @keyframes fadeInDown { 69 | 0% { 70 | opacity: 0; 71 | transform: translateY(-20px); 72 | } 73 | 74 | 100% { 75 | opacity: 1; 76 | transform: translateY(0); 77 | } 78 | } 79 | 80 | .fadeInDown { 81 | animation-name: fadeInDown; 82 | } 83 | 84 | 85 | @keyframes bounceInUp { 86 | 0% { 87 | opacity: 0; 88 | transform: translateY(2000px); 89 | } 90 | 91 | 60% { 92 | opacity: 1; 93 | transform: translateY(-30px); 94 | } 95 | 96 | 80% { 97 | transform: translateY(10px); 98 | } 99 | 100 | 100% { 101 | transform: translateY(0); 102 | } 103 | } 104 | 105 | .bounceInUp { 106 | animation-name: bounceInUp; 107 | } 108 | 109 | 110 | @keyframes bounceOut { 111 | 0% { 112 | transform: scale(1); 113 | } 114 | 115 | 25% { 116 | transform: scale(.95); 117 | } 118 | 119 | 50% { 120 | opacity: 1; 121 | transform: scale(1.1); 122 | } 123 | 124 | 100% { 125 | opacity: 0; 126 | transform: scale(.3); 127 | } 128 | } 129 | 130 | .bounceOut { 131 | animation-name: bounceOut; 132 | } 133 | 134 | @keyframes bounceOutDown { 135 | 0% { 136 | transform: translateY(0); 137 | } 138 | 139 | 20% { 140 | opacity: 1; 141 | transform: translateY(-20px); 142 | } 143 | 144 | 100% { 145 | opacity: 0; 146 | transform: translateY(2000px); 147 | } 148 | } 149 | 150 | .bounceOutDown { 151 | animation-name: bounceOutDown; 152 | } 153 | 154 | @keyframes fadeInUp { 155 | 0% { 156 | opacity: 0; 157 | transform: translateY(20px); 158 | } 159 | 160 | 100% { 161 | opacity: 1; 162 | transform: translateY(0); 163 | } 164 | } 165 | 166 | .fadeInUp { 167 | animation-name: fadeInUp; 168 | } 169 | 170 | @keyframes fadeInUpBig { 171 | 0% { 172 | opacity: 0; 173 | transform: translateY(2000px); 174 | } 175 | 176 | 100% { 177 | opacity: 1; 178 | transform: translateY(0); 179 | } 180 | } 181 | 182 | .fadeInUpBig { 183 | animation-name: fadeInUpBig; 184 | } 185 | 186 | @keyframes fadeOut { 187 | 0% { 188 | opacity: 1; 189 | } 190 | 191 | 100% { 192 | opacity: 0; 193 | } 194 | } 195 | 196 | .fadeOut { 197 | animation-name: fadeOut; 198 | } 199 | 200 | @keyframes fadeOutDown { 201 | 0% { 202 | opacity: 1; 203 | transform: translateY(0); 204 | } 205 | 206 | 100% { 207 | opacity: 0; 208 | transform: translateY(20px); 209 | } 210 | } 211 | 212 | .fadeOutDown { 213 | animation-name: fadeOutDown; 214 | } 215 | 216 | @keyframes fadeOutDownBig { 217 | 0% { 218 | opacity: 1; 219 | transform: translateY(0); 220 | } 221 | 222 | 100% { 223 | opacity: 0; 224 | transform: translateY(2000px); 225 | } 226 | } 227 | 228 | .fadeOutDownBig { 229 | animation-name: fadeOutDownBig; 230 | } 231 | 232 | @keyframes flipInX { 233 | 0% { 234 | transform: perspective(400px) rotateX(90deg); 235 | opacity: 0; 236 | } 237 | 238 | 40% { 239 | transform: perspective(400px) rotateX(-10deg); 240 | } 241 | 242 | 70% { 243 | transform: perspective(400px) rotateX(10deg); 244 | } 245 | 246 | 100% { 247 | transform: perspective(400px) rotateX(0deg); 248 | opacity: 1; 249 | } 250 | } 251 | 252 | .flipInX { 253 | backface-visibility: visible !important; 254 | animation-name: flipInX; 255 | } 256 | 257 | 258 | @keyframes flipInY { 259 | 0% { 260 | transform: perspective(400px) rotateY(90deg); 261 | opacity: 0; 262 | } 263 | 264 | 40% { 265 | transform: perspective(400px) rotateY(-10deg); 266 | } 267 | 268 | 70% { 269 | transform: perspective(400px) rotateY(10deg); 270 | } 271 | 272 | 100% { 273 | transform: perspective(400px) rotateY(0deg); 274 | opacity: 1; 275 | } 276 | } 277 | 278 | .flipInY { 279 | backface-visibility: visible !important; 280 | animation-name: flipInY; 281 | } 282 | 283 | @keyframes rubberBand { 284 | 0% { 285 | transform: scale(1); 286 | opacity: 0; 287 | } 288 | 289 | 30% { 290 | transform: scaleX(1.25) scaleY(0.75); 291 | } 292 | 293 | 40% { 294 | transform: scaleX(0.75) scaleY(1.25); 295 | } 296 | 297 | 60% { 298 | transform: scaleX(1.15) scaleY(0.85); 299 | } 300 | 301 | 100% { 302 | transform: scale(1); 303 | opacity: 1; 304 | } 305 | } 306 | 307 | .rubberBand { 308 | animation-name: rubberBand; 309 | } 310 | 311 | 312 | @keyframes tada { 313 | 0% { 314 | transform: scale(1); 315 | } 316 | 317 | 10%, 20% { 318 | transform: scale(0.9) rotate(-3deg); 319 | } 320 | 321 | 30%, 50%, 70%, 90% { 322 | transform: scale(1.1) rotate(3deg); 323 | } 324 | 325 | 40%, 60%, 80% { 326 | transform: scale(1.1) rotate(-3deg); 327 | } 328 | 329 | 100% { 330 | transform: scale(1) rotate(0); 331 | } 332 | } 333 | 334 | .tada { 335 | animation-name: tada; 336 | } 337 | 338 | @keyframes wobble { 339 | 0% { 340 | transform: translateX(0%); 341 | } 342 | 343 | 15% { 344 | transform: translateX(-25%) rotate(-5deg); 345 | } 346 | 347 | 30% { 348 | transform: translateX(20%) rotate(3deg); 349 | } 350 | 351 | 45% { 352 | transform: translateX(-15%) rotate(-3deg); 353 | } 354 | 355 | 60% { 356 | transform: translateX(10%) rotate(2deg); 357 | } 358 | 359 | 75% { 360 | transform: translateX(-5%) rotate(-1deg); 361 | } 362 | 363 | 100% { 364 | transform: translateX(0%); 365 | } 366 | } 367 | 368 | .wobble { 369 | animation-name: wobble; 370 | } 371 | -------------------------------------------------------------------------------- /app/directives.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var module = angular.module('orgchart.directives', [ 4 | 'orgchart.services', 5 | 'ngAnimate' 6 | ]); 7 | 8 | module.directive('animateOnPhoto', ['$animate', function($animate) { 9 | return function(scope, ele, attr) { 10 | attr.$observe('animateOnPhoto', function(value) { 11 | 12 | var animationClass = attr.animateName; 13 | var defaultImageURL = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAADICAMAAACahl6sAAADAFBMVEVQjrRVn81RmsdMkb2BrctNlcKTutR1rdGixNxLj7pUn89Ii7RNl8RqocRTodCxzeLB3fJSnMq50uZPmcZPm8l7qclRns7P4O+Xu9ROmcaHsc671ei52fFMlcGLtdFQmchzo8JOlcFOmMZKjriy1OxFha1alblLk75Fhq5qmrtTncpXm8a00OXR4vFimr7E2ep6tt1gmLrN3u1Gh69fp9XM4vNIibKryN5TnsydwduHvuJnnsCUw+REhq9KkbyZxubI3e1Pl8RUn82sy+Hf7/5EhKtHibJJjLZDgaiizOlJj7lSnMh1s9ten8fV6PlTlb5nq9XL3Ounx91EgqhJjbdXodC/2OyQt9KBut7F3O52psVPl8JKjbdQiq7D2uycyOfI4fVIirOavthglbZMjLNYkbViqdVwn76Es9Kp0Oywy9/E4PRPmMZVoM9bpdNapdTb7PzQ5fXB2OpNlMLQ5/lGhKrB1udLkLt6r9JNlsOeyuhDhq9tr9lwq8/X6vvM4/VJirHQ4vLJ3vBUkLVLkr3V5vbN5fhMh6zI2uq+1+mgwNdIhatFg6lYpNJXpNLa6vnb6/rU5fRTodLT5PPQ4fDc7PvZ6fjO3+5UodLd7fzV5vXe7v3X5/bM3ezS4/LW5vXY6fjV5fTU5PPW5/bY6PdSoNDT4/JToNBRoM/X6PfZ6vlSn9Da6/rS4vHQ4O9Qnczb7PvP3+5Rn8/c7fxSn8/O3u1Qns1SoM/R4fBRncxWotFLlMBRmshVodBRnctMlMBMlMFRns1Qm8lVotBHiLFUoM5Pm8pWotBRn85ToNFSns5Sn85WodBQnMpPnMpVodFLlMFDgadUoNBPmshOl8RTn9BQnctSnc1Qm8tSnsxMk79RmcVTns9Rnc1PnMtRnMtMlMJVo9JDgqlJjbhFh69OlsLG2+xFh7BZpNLQ4fFYpdNSnczW5/dOkbnS4/N8uN7O4PBQlcFuqMxCha5UotJTodFWo9JVotJXo9JSoNFZpdNSodFRoNFapdNZpNNYpNOaiSeMAAANB0lEQVR4AezPUarCQAwF0Owfmj3Ob78zQ0m5E6sitj5/hZdwzw6O7Bc2lokE5jJsv5DzYnMk4pt9jXRFOtr/RhpSap8RRVJ6jQTSinMkkFi8I4rU9BVpSK49Ix3p9UdEkZ7eI4YC7IgML2AckdULWHcxL8FkeAlD1EtQCS8hxItg5L+RWQQjP8QII4wwwggjjDASRTBywggjjDAyb9TRsYqDMByA8VfwATL2ATpkkoCLixCko5tOggixQ6cSvM1NzeIgWW4uHQq+VDkOupbCyQ23dDnumuQf4+8NPr6K4R+MVesMwVtSeAcZPpkyFPn8saIQnESlDH9Ro4DOawjJfTSFf4iLW+V2yOO22ct/OUTc3RBMGvkCdJudDMnTWL4o82fnQnBQSwVl4liI30hFiJsJaQ3oOJLqpiNr9ZkIuZJ61JJRJ0J2aNS1D76WD9keRgPQrls25IMMoxENXTSEFaMptX/XCul0YDQZRDoNWiHYuwwmpQuF5OVg2BE+BPLHk3SBEIYGAEQ55K5qcxkgJHc1yiHpBUZN7YYkAspbbjOExwIMutoLYZ4AFNgLOQpIJ2orhJ4FqIzZCWHlGVhqJyQ49cDeuY2Q3WcPTWxshBS9BQl8CD/ZCPFamJBvbsropakoDODbxeYIpt3VQLQGixh07hjtYXFFpXb2FGsRgc4VWMiKXu5DbwMZoeKDFcgQfJDBrDsE+xNke2ke9jIIrLe9Kj4kUh7Q104q5u7ZOdvgnvPg7/nH990f3842CbVarV6vZ+JLUpgk6zZPEBQS/iqFpWvjdbJQYIh7RRL3xgWF/KswnG/2JXWUbtYJzb8uG0JqtdMO5+iKNJ6Ok5VksZiQV9zda/2+4BZ2hP1prtRDpEzYP8UPeWl0FXLUEdVqtVKpwEbvGo9QBJ8x5ouyJO+5hInE4X3dMCpkMQk5ak+HIaQCQah8+sDb7JrB/5lfbC1Nj12QHEOccSuPIURk8SbB7pCPUV4HbsIx0bKjWcoQickjp7CQuyU2oRncTDBPORveMau0zJ4YRfaHGAhBABoD7K2FILbiokMilJQqsclBaBgG/5l0exGEdABii+ylcUzhMK1SfwtpgT1zFECEkICQ5AYbH6YJWaUeTBNnz7xjfwiCEKhqlhMSxDQuqxTBNH72zHdOACEkz8S+EEhCFGV4v8BkD9P0mBYpg2l8BTa5X6ch/JN0HXIjxNmJO/nG+92FFCeB7SG6rirKnMle6sA0g1ZpvtXZCmweJJx2hwCQuK4VOIxgGrdV8mCa6QKTUkjRdd0wDHtDGsl9k00K06StkgvTTJls0t+AgJAfHpNDegdbof25LUoaMTnkbb+IrqpK7PCJyWEQW5gZ6kDCXpNHAgCAEKpUq/aFNGK5Io/ZIG7G30qKWKRB7sxVTUhIb5FLuvk/yWe2lF43S6v8mbaHABLyIlvk89CDz9lLMT5x7oK0QyTZIYlEYze72o5nYXzClm+iA+n54kK7gRpQVSg/hDDlTvkH4rN8KX0iRdtPMzVVQogMugg57gi1XI4Fsp+lo5XLZV3Xr/b1fT/m001I76UJyUvH/hBFiQVy8kMaZyHb2zaGXDm8JCEBj/yQmIiQW8kvsknHFEWx/yLauuwQ708hIX9+L8sl7xZzkUAoL7nEf1tIyO7wsmRygi7y9kBux4G2K+aNJNc5/GXOfkLayNs4gIuTyWsYGiFD0pCAjQ5RA2pIBwxB3lCQ/MFLqRMQxCxMVVzEU8UOubWvgn+a00ugXtqb60EsvGBRISQ65NKRHLJOSDJrGbGbLFL60m7b+z6jdt2yHR3T+bX7vTo+Tz6/J8/k31bAYLAv/qQ93ZMtVsPkRVc4WxFNZK6m3tR7+knX12LRpLjuNzScfkZ8qH5RAEcFWXqqlu6e89+qrIEfnl6UhxbjtfPvHIfUL/wXsokMr6v1HHz/WWZdxv4vaR46mxx9vZ9f+n+1mse/vEMFiR+rNf3SDwZjPcMOY5N/sstp6eofDLQYrrXnvnDZY7Wa/a3IJsL41Zpa39ed/6geDkBQvNcCSMG6rhJj3Y4GtZLHt9FBmPmbKl1rD+qFqJ5NE/UOASSdPoEwd7tVEqjT8WBRpeD679L5RMxmvSG+dTXJ3Vxdjn61evfeSO+uAvmgKXI6LeI4QJjmbrVY+q7M+HV4Ua3a+o+chOMiQARBKJrNy5c8Qu2QKRyXCIJwdavHMtT3m3bF2HCgpl7r1m0uW62KPM8DZFVnCM5x3Ixl66I88Tv6NDzHGlzGrtRFdY6NGAaQKQWyt4cCEnJtpS7J2y671aeqGetzBJyX1qjdRgepVk8gya6UpjgHhwyunoZ/fwKEx/uGHXcnvSkt2XJgGIaLogLZ0xOS4XkSIFA9xHhSV8rb2j3novdW6kq592Zubg4gpCzLyCBMUwp5rLFvAZm9g9rRP4IKUpHlRlEUoTo1MtJpeIY2t3ooKpvNiiSZzmQyxdVV8/IyCgidnLyONAbqG0GY2RpCxrNBGh1EyGTSJDklSZKJogiaHkYI8c4SHMfhOE42NvKVSmVVZwhPkiQcE2cyxVi214jM8fhahMIwrFqtkum0LAjCKeQDIsgMsjVxRJBCKhW5sZGEeWMcRxEEy8Qt/0MSOxuLUbCLsJGNPK9AzGZw6AXZEwSZ59On+26KxQiGGfc+RhB/ZISiONhFmD8vy5VisQgQYKCDMKML+juaG9hvD2E8NQSOuiHLmgL1SrJcnpqampYkzGSiWJZNRj0LejsikYgJwyTY9DRseqkkQOOfIcuXRivk4OATZHr6T0h0VNc96W/o/U4QpjBuea5Xbi6N9aKHrBaLwuvX8o0bNxKtrRKGcdCR7uwMReP+5zf1SQsNh2OiKEySpuHAyuVySRAO4EVEcegJKZZKJZ7npxKJ6bk5jKIotrfXHWVmhnRheF05ZRwcx0nT01WAyLIsHBwUtUP+qykra2sbL1++BEhbInF4BgmH3VGIy/v1juZ2Ohc+gxweJtra2vL5vE0Q1lZWVl68eKHhEWqEQD3BZjuBiCLe0TEHEIJl6WQyeRSd9X/10+oRHQ6zkcirYDC4g+MJkiS3t7dtGxu6Q6AYDEWA6vv7+204jkNHKhIhwuFwJ6yKdeFrGF09uRxLEK9MJtPOzg6Mg4QFsdlsKgqEkCMmvvSk3iwYZnL/GAhsim+yPoc9zjBoIOqSlTWoni+X92EXDw8PgxwHlghYTm7E0YnmqzOW2mmazhEEQQ0MBCVJEkURTqoM95W1NcWhKDRCPmoL3AY3NzcFQYC7iQKBo8OCwYEzCE27C4UCM+G/2pPK3u7upOH/oQplMgEkCxCS53mY/MbGxiY0/ag1ekIgo3avVobFEC+4O5FC1ClQfTuT4ff3yUQike3owAYGFAsLFrfbHYIk511Ndy5X3Dd6QjOhEJ3LhVmWoCgqiGES1FTe8+bzmd3dXTi2VWSQVYBAjwycGQwF39mRYOdPX1FYWFhYe4VTmJ+wO+9ckEmHbwYuhDmGT149YhzHdWSzuCi2QeXM9vYu9AGIBgdaiJJZ15D//t8NliWHZx7+nPzOEGVNdne383nlm4gEjmdP36xECII9XRWQhI6UMEyBOXoUHx/1eTwTEI/HNzobnzk6SwiWA+hwAJFYjILjkE7uu3+BQDdEkPOh/MFuGSJnDAJRWDAr0NU1ILgFB0DnAj1FL8ClfhOaTFUzKFCcpu91U9PB/JGdPAsz8GX3W3KABSe+ONdFJOf8npKnKm/E+SQPazMPljhxsZFD13sbo8Hk6M6dlh+Tcvx3kBvk1xOw4MS67w2qcBCPEVT7BTy4oCJNgyVsoBnWWpPzEBHI0WjH+sBD9eTcvQ4CFB1eVZ3vvUspwxg+Kn4BCmqjmUAw2LAAmrMqhIIPoZZXfQZvkCdDEP1Z0e76YWF7lUJb0C/sMPLMgyWfEsyIoHgVIUVrrV7vq+sgp/M4d1vXVVkcWODsCCHjjhZhff4kIahDNCaPQTmoOClqfWzb1yXLb5CTBCceAEF7AaQBhKqIcH7lTBZc2GoSYjWkiBEbRikfKgdAdnyO7bt9OmaNEIYCOP4FbpSAQz++VOggZMjSPWQSoZDR73G8Dv3XBzlyVCgirQn5wy3mIe/HGYW80kGIHGsYhnEcrbXBORfn2Xdd1/c9m91Y8IWWZXl/aiFOGLjpzfDez9M0GWMsbxu25FgNUg2EEoZFHPvM6mHBXk0/1BMDHYQY4+ScsVYRSfG3kNwSwvbHxDiv68PCL+9DFQx40NCdMSFBMkWDHLeMxDJqYbnIjl7rnvK0rmskBhm3SUHyf5CEeXC+rwtB2klPYSfEjqJBTrgqFILRj4z2FEwYVVwKknsQZdlU9nigXNAg1UHuZ/f2q+5nlyBnd1jQIBerQaQOh1wP0iBShUOAfFbQBpEaHEAqkEh1ECneoRAq3gGkdIlkECqYkUOkXEcOoXIZCinTInlfCWC6C4hDUwYAAAAASUVORK5CYII='; 14 | ele.css("background-image","url('" + defaultImageURL + "')"); 15 | 16 | //no animateClass passed in. default to nothing. 17 | if (!angular.isDefined(animationClass)) { 18 | animationClass = ''; 19 | } 20 | 21 | $animate.removeClass(ele,animationClass); 22 | 23 | var image = new Image(); 24 | image.src = attr.animateOnPhoto; 25 | 26 | $animate.addClass(ele,animationClass); 27 | 28 | image.onload = function() { 29 | ele.css("background-image","url(" + attr.animateOnPhoto + ")"); 30 | }; 31 | 32 | image.isError = function() { 33 | ele.css("background-image","url('" + defaultImageURL + "')"); 34 | } 35 | }) 36 | } 37 | }]); 38 | 39 | module.directive('searchWatch', ['$timeout', 'sharedFunctions', function($timeout, sharedFunctions) { 40 | return function(scope, ele, attr) { 41 | 42 | var startTimer = false; 43 | var counter = 0; 44 | var timerObj; 45 | 46 | scope.$watch(attr.ngModel, function(queryString) { 47 | 48 | sharedFunctions.toggleSearchDisplay("hide"); 49 | 50 | if (angular.isDefined(queryString)) { 51 | 52 | if (queryString.length >= 3 && counter < 2) { 53 | startTimer = true; 54 | counter = 0; 55 | 56 | if (counter == 0) { 57 | scope.stopCounter(); 58 | scope.runCounter(); 59 | } 60 | } else { 61 | scope.stopCounter(); 62 | } 63 | } 64 | 65 | scope.runCounter = function() { 66 | if (counter < 2) { 67 | counter++; 68 | timerObj = $timeout(scope.runCounter, 100); 69 | } else { 70 | scope.searchEmployee(queryString); 71 | scope.stopCounter(); 72 | } 73 | }; 74 | 75 | scope.stopCounter = function() { 76 | startTimer = false; 77 | counter = 0; 78 | $timeout.cancel( timerObj ); 79 | } 80 | 81 | }) 82 | } 83 | }]); 84 | 85 | module.directive('searchToggle', ['$animate', 'sharedFunctions', function($animate, sharedFunctions) { 86 | return function (scope, ele, attr) { 87 | attr.$observe('searchToggle', function() { 88 | 89 | var managerDetailEle = angular.element(document.getElementById('managerDetail')); 90 | var managerDetailClassOn = 'overflowScrolling-touch'; 91 | var managerDetailClassOff = 'overflowScrolling-default'; 92 | 93 | var searchToggleEle = angular.element(document.getElementById('searchToggle')); 94 | var searchToggleOn = 'icon-utility-close'; 95 | var searchToggleOff = 'icon-utility-search'; 96 | 97 | if (attr.searchToggle == 'false') { 98 | managerDetailEle.removeClass(managerDetailClassOff).addClass(managerDetailClassOn); 99 | searchToggleEle.removeClass(searchToggleOn).addClass(searchToggleOff); 100 | } else { 101 | managerDetailEle.removeClass(managerDetailClassOn).addClass(managerDetailClassOff); 102 | searchToggleEle.removeClass(searchToggleOff).addClass(searchToggleOn); 103 | } 104 | }); 105 | } 106 | }]); -------------------------------------------------------------------------------- /app/css/employee.less: -------------------------------------------------------------------------------- 1 | // Path inspired animation sequence 2 | // from the the incredible work of 3 | // http://sparanoid.com/ 4 | // Tunghsiao Liu 5 | 6 | 7 | 8 | @d1: (@r * 0); 9 | @d2: (@r * 1); 10 | @d3: (@r * 2); 11 | @d4: (@r * 3); 12 | @d5: (@r * 4); 13 | @d6: (@r * 5); 14 | @d7: (@r * 6); 15 | @d8: (@r * 7); 16 | @d9: (@r * 8); 17 | 18 | @r: 15deg; 19 | @n: 8; 20 | 21 | .employee-image, 22 | .report-image { 23 | height: 100px; 24 | width: 100px; 25 | background-size: 100px; 26 | position: relative; 27 | z-index: 3; 28 | margin: 0px auto; 29 | border: 4px solid #fff; 30 | border-radius: 50%; 31 | display: inline-block; 32 | background-position: center; 33 | background-color: #FFF; 34 | background-repeat: no-repeat; 35 | } 36 | 37 | .report-image { 38 | border-width: 1px; 39 | } 40 | 41 | .employee-image { 42 | border: 2px solid #8D3EEA; 43 | } 44 | 45 | #swipePad { 46 | height: 100px; 47 | width: 100%; 48 | margin: -26px auto 2px auto; 49 | z-index: 10; 50 | } 51 | 52 | .peer-indicator { 53 | margin: 0 auto; 54 | width: 123px; 55 | } 56 | 57 | .report-image { 58 | height: 30px; 59 | width: 30px; 60 | background-size: 30px; 61 | margin: 0; 62 | } 63 | 64 | #employeeDetail { 65 | text-align: center; 66 | position: fixed; 67 | bottom: 0; 68 | left: 0; 69 | width: 100%; 70 | z-index: 5; 71 | height: 200px; 72 | } 73 | 74 | #employeeStrip { 75 | width: 100%; 76 | height: 46px; 77 | margin: 0; 78 | background: #f5f5f5; 79 | border-width: 1px 0 1px 0; 80 | position: absolute; 81 | font-size: 18pt; 82 | top: 0; 83 | } 84 | 85 | #employeeStrip a, 86 | #employeeStrip i { 87 | width: 44px; 88 | height: 44px; 89 | color: #293f54; 90 | display: inline-block; 91 | line-height: 44px; 92 | } 93 | 94 | #employeeLeftIcons, 95 | #employeeRightIcons { 96 | margin: 0 10px; 97 | } 98 | 99 | #employeeLeftIcons { 100 | float: left; 101 | } 102 | 103 | #employeeLeftIcons a { 104 | text-decoration: none; 105 | } 106 | 107 | #employeeRightIcons { 108 | float: right; 109 | } 110 | 111 | #employeeEmail { 112 | display: block; 113 | } 114 | 115 | #peersIcons { 116 | width: 185px; 117 | margin: 0 auto; 118 | i { 119 | color: #ccc; 120 | font-size: 14pt; 121 | } 122 | } 123 | 124 | .name, 125 | .title { 126 | color: #2A94D6; 127 | z-index: 999; 128 | display: block; 129 | overflow: hidden; 130 | white-space: nowrap; 131 | width: 200px; 132 | margin: 0 auto; 133 | text-overflow: ellipsis; 134 | } 135 | 136 | .title { 137 | color: #999; 138 | font-size: .8em; 139 | } 140 | 141 | .main-title { 142 | overflow: hidden; 143 | max-height: 40px; 144 | width: 180px; 145 | margin: 0 auto; 146 | height: 40px; 147 | font-size: .8em; 148 | } 149 | 150 | .feedback { 151 | position: absolute; 152 | bottom: 10px; 153 | right: 10px; 154 | width: initial; 155 | text-align: right; 156 | } 157 | 158 | // TODO: needs refactor to turn back into Less functions 159 | // Gah... 160 | 161 | .reports { 162 | background: #fff; 163 | list-style-type: none; 164 | position: relative; 165 | margin: 0; 166 | padding: 0; 167 | top: -270px; 168 | } 169 | .reports li { 170 | margin: 0 0 0 -15px; 171 | padding: 0; 172 | border: 0; 173 | position: absolute; 174 | height: 170px; 175 | width: 30px; 176 | transform-origin: 15px bottom; 177 | } 178 | 179 | .reports li span { 180 | transition: transform .4s ease; 181 | display: block; 182 | margin: 0; 183 | } 184 | 185 | .reports>li a { 186 | position: absolute; 187 | display: block; 188 | width: 30px; 189 | height: 30px; 190 | overflow: visible; 191 | text-align: center; 192 | border-radius: 50%; 193 | } 194 | 195 | .reports>li:nth-of-type(1) { transform: rotate(-108deg); } 196 | .reports>li:nth-of-type(2) { transform: rotate(-126deg); } 197 | .reports>li:nth-of-type(3) { transform: rotate(-144deg); } 198 | .reports>li:nth-of-type(4) { transform: rotate(-162deg); } 199 | .reports>li:nth-of-type(5) { transform: rotate(-180deg); } 200 | .reports>li:nth-of-type(6) { transform: rotate(-198deg); } 201 | .reports>li:nth-of-type(7) { transform: rotate(-216deg); } 202 | .reports>li:nth-of-type(8) { transform: rotate(-234deg); } 203 | .reports>li:nth-of-type(9) { transform: rotate(-252deg); } 204 | 205 | // custom styling for the First LI and the last LI 206 | .reports.expand li:nth-of-type(9) a { 207 | line-height: 22px; 208 | text-align: center; 209 | background: rgba(42, 148, 214, 0.5); 210 | border: 4px solid rgba(255, 255, 255, 0.4); 211 | color: white; 212 | font-size: .7em; 213 | ::before { 214 | content: '+'; 215 | margin-right: 2px; 216 | } 217 | } 218 | 219 | // Set the rotation to square off the span AND 220 | // add a rotation animation for entry 221 | .reports.expand li:nth-of-type(1) a span { 222 | transform: rotate(108deg); 223 | // disable spin for reports count 224 | // animation: spin1-expand .6s ease-out 1 backwards; 225 | } 226 | .reports.expand li:nth-of-type(2) a span { 227 | transform: rotate(126deg); 228 | animation: spin2-expand .6s ease-out 1 backwards; 229 | } 230 | .reports.expand li:nth-of-type(3) a span { 231 | transform: rotate(144deg); 232 | animation: spin3-expand .6s ease-out 1 backwards; 233 | } 234 | .reports.expand li:nth-of-type(4) a span { 235 | transform: rotate(162deg); 236 | animation: spin4-expand .6s ease-out 1 backwards; 237 | } 238 | .reports.expand li:nth-of-type(5) a span { 239 | transform: rotate(180deg); 240 | animation: spin5-expand .6s ease-out 1 backwards; 241 | } 242 | .reports.expand li:nth-of-type(6) a span { 243 | transform: rotate(198deg); 244 | animation: spin6-expand .6s ease-out 1 backwards; 245 | } 246 | .reports.expand li:nth-of-type(7) a span { 247 | transform: rotate(216deg); 248 | animation: spin7-expand .6s ease-out 1 backwards; 249 | } 250 | .reports.expand li:nth-of-type(8) a span { 251 | transform: rotate(234deg); 252 | animation: spin8-expand .6s ease-out 1 backwards; 253 | } 254 | .reports.expand li:nth-of-type(9) a span { 255 | transform: rotate(252deg); 256 | animation: spin9-expand .6s ease-out 1 backwards; 257 | } 258 | 259 | @keyframes spin1-expand { 260 | 0% { transform: rotate(108deg); } 261 | 60% { transform: rotate(-252deg); } 262 | 100% { transform: rotate(-252deg); } 263 | } 264 | @keyframes spin2-expand { 265 | 0% { transform: rotate(126deg); } 266 | 60% { transform: rotate(-234deg); } 267 | 100% { transform: rotate(-234deg); } 268 | } 269 | @keyframes spin3-expand { 270 | 0% { transform: rotate(144deg); } 271 | 60% { transform: rotate(-216deg); } 272 | 100% { transform: rotate(-216deg); } 273 | } 274 | @keyframes spin4-expand { 275 | 0% { transform: rotate(162deg); } 276 | 60% { transform: rotate(-198deg); } 277 | 100% { transform: rotate(-198deg); } 278 | } 279 | @keyframes spin5-expand { 280 | 0% { transform: rotate(180deg); } 281 | 60% { transform: rotate(-180deg); } 282 | 100% { transform: rotate(-180deg); } 283 | } 284 | @keyframes spin6-expand { 285 | 0% { transform: rotate(198deg); } 286 | 60% { transform: rotate(-162deg); } 287 | 100% { transform: rotate(-162deg); } 288 | } 289 | @keyframes spin7-expand { 290 | 0% { transform: rotate(216eg); } 291 | 60% { transform: rotate(-144deg); } 292 | 100% { transform: rotate(-144deg); } 293 | } 294 | @keyframes spin8-expand { 295 | 0% { transform: rotate(234deg); } 296 | 60% { transform: rotate(-126deg); } 297 | 100% { transform: rotate(-126deg); } 298 | } 299 | @keyframes spin9-expand { 300 | 0% { transform: rotate(252deg); } 301 | 60% { transform: rotate(-108deg); } 302 | 100% { transform: rotate(-108deg); } 303 | } 304 | .reports li a { 305 | top: 165px; 306 | animation: contract 0.35s ease-out 1 backwards; 307 | } 308 | .reports.expand li a { 309 | /* top of 10 to overshoot expansion*/ 310 | top: 10px; 311 | animation: expand .6s ease 1 backwards; 312 | } 313 | 314 | @keyframes expand { 315 | 0% { 316 | top: 165px; 317 | } 318 | 50% { 319 | top: -10px; 320 | } 321 | 70% { 322 | top: 15px; 323 | } 324 | 100% { 325 | top: 10px; 326 | } 327 | } 328 | 329 | @keyframes contract { 330 | 0% { 331 | top: 10px; 332 | } 333 | 40% { 334 | top: -25px; 335 | } 336 | 100% { 337 | top: 165px; 338 | } 339 | } 340 | 341 | .reports li:nth-of-type(1) a { animation-delay: 0.32s; } 342 | .reports li:nth-of-type(2) a { animation-delay: 0.28s; } 343 | .reports li:nth-of-type(3) a { animation-delay: 0.24s; } 344 | .reports li:nth-of-type(4) a { animation-delay: 0.20s; } 345 | .reports li:nth-of-type(5) a { animation-delay: 0.16s; } 346 | .reports li:nth-of-type(6) a { animation-delay: 0.12s; } 347 | .reports li:nth-of-type(7) a { animation-delay: 0.08s; } 348 | .reports li:nth-of-type(8) a { animation-delay: 0.04s; } 349 | .reports li:nth-of-type(9) a { animation-delay: 0s; } 350 | .reports.expand li:nth-of-type(1) a { animation-delay: 0s; } 351 | .reports.expand li:nth-of-type(2) a { animation-delay: 0.04s; } 352 | .reports.expand li:nth-of-type(3) a { animation-delay: 0.08s; } 353 | .reports.expand li:nth-of-type(4) a { animation-delay: 0.12s; } 354 | .reports.expand li:nth-of-type(5) a { animation-delay: 0.16s; } 355 | .reports.expand li:nth-of-type(6) a { animation-delay: 0.20s; } 356 | .reports.expand li:nth-of-type(7) a { animation-delay: 0.24s; } 357 | .reports.expand li:nth-of-type(8) a { animation-delay: 0.28s; } 358 | .reports.expand li:nth-of-type(9) a { animation-delay: 0.32s; } 359 | 360 | // List view of employees 361 | #reportsListView { 362 | width: 100%; 363 | height: 100%; 364 | z-index: 5; 365 | top: 150px; 366 | left: 0; 367 | position: fixed; 368 | overflow-y: scroll; //must be scroll for the momentum scrolling to work 369 | -webkit-overflow-scrolling: touch; //key for momentum scrolling 370 | 371 | // Hack to address some wierd issue with the last report being cut off on 372 | // list view 373 | ul { 374 | margin-bottom: 170px; 375 | } 376 | li { 377 | text-align: left; 378 | cursor: pointer; 379 | clear: both; 380 | border-top: 1px solid #dadee2; 381 | height: 45px; 382 | padding: 0 10px; 383 | :first-child { 384 | border-top: none; 385 | } 386 | i { 387 | float: left; 388 | margin: 12px 12px 8px 2px; 389 | } 390 | div { 391 | float: left; 392 | margin: 10px 10px 10px 5px; 393 | } 394 | } 395 | .name, 396 | .title { 397 | width: 240px; 398 | } 399 | } 400 | 401 | .tog { 402 | position: absolute; 403 | bottom: 10px; 404 | left: 10px; 405 | text-align: left; 406 | } 407 | 408 | .toggle { 409 | position: absolute; 410 | left: 10px; 411 | top: 70px; 412 | text-align: left; 413 | } 414 | 415 | .listViewBackground { 416 | } 417 | 418 | .employeeFocusListView { 419 | top: 40px !important; 420 | position: fixed !important; 421 | width: 100% !important; 422 | } -------------------------------------------------------------------------------- /app/controllers/orgchartController.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var module = angular.module('orgchart', [ 4 | 'ngSanitize', 5 | 'ngAnimate', 6 | 'ngTouch', 7 | 'orgchart.controllers', 8 | 'orgchart.services', 9 | 'orgchart.directives', 10 | 'jmdobry.angular-cache' 11 | ]); 12 | 13 | module.controller('orgchartController', ['$scope', 'sharedFunctions', 'orgChartCacheService', 14 | function ($scope, sharedFunctions, orgChartCacheService) { 15 | 16 | //defaults 17 | $scope.includePath = '../views/includes/'; 18 | $scope.initialLoadFlag = true; // Included as part of fix for 7.0.6 manager scroll bug 19 | $scope.listToggleFlag = true; 20 | $scope.ellipsisFlag = false; 21 | $scope.searchFlag = false; 22 | $scope.expiredTokenFlag = false; 23 | $scope.employee = {}; 24 | 25 | var me; 26 | 27 | $scope.searchClick = function (employee) { 28 | if (typeof employee.id == "undefined") { 29 | // No-op. The search term is invalid. 30 | return; 31 | } 32 | 33 | if (employee.id != $scope.employee.id || employee.id != me.id) { 34 | // Search for new employee. Do the things. 35 | $scope.getEmployee(employee); 36 | } 37 | 38 | $scope.toggleSearch(); 39 | }; 40 | 41 | $scope.repaintView = function (employee) { 42 | if ($scope.initialLoadFlag) { // 43 | $scope.toggleListView(); // Included as part of fix for 7.0.6 manager scroll bug 44 | $scope.initialLoadFlag = false; // 45 | } 46 | }; 47 | 48 | var onServiceError = function (response) { 49 | if (response.status == 401) { 50 | $scope.expiredTokenFlag = true; 51 | } else if ( response.status == 404 && typeof me == "undefined" ) { 52 | orgChartCacheService.getEmployee('marcb@salesforce.com') //fallback. if service cannot find current user. route to marc b. 53 | .then(function (data) { 54 | me = data; 55 | $scope.employee = data; 56 | $scope.employeeTitle = data.title; 57 | $scope.repaintView(data); 58 | }) 59 | } 60 | }; 61 | 62 | $scope.getEmployee = function (employee) { 63 | // Update employee object with whatever info we already have for the new employee 64 | $scope.employee = employee; 65 | $scope.employeeTitle = employee.title; 66 | 67 | var id = employee.email; 68 | 69 | // Then request the full employee object 70 | 71 | orgChartCacheService.getEmployee(id) 72 | .then(function (data) { 73 | 74 | $scope.employee = data; 75 | $scope.employeeTitle = data.title; 76 | 77 | }, onServiceError); 78 | }; 79 | 80 | $scope.hasReports = function () { 81 | var reportsCount = $scope.employee.directReports; 82 | 83 | if (typeof reportsCount == "undefined") { 84 | return false; 85 | } 86 | return reportsCount.length > 0; 87 | }; 88 | 89 | $scope.showReportsString = function () { 90 | var reportsCount = $scope.employee.directReports; 91 | 92 | if (typeof reportsCount == "undefined") { 93 | return false; 94 | } 95 | return reportsCount.length - 8 > 0; 96 | }; 97 | 98 | $scope.hasPeer = function (direction) { 99 | var peers = $scope.employee.peers; 100 | 101 | if (typeof peers == "undefined") { 102 | return false; 103 | } 104 | return peers.length > 0; 105 | }; 106 | 107 | $scope.getRightPeer = function () { 108 | $scope.getPeer('right'); 109 | }; 110 | 111 | $scope.getLeftPeer = function () { 112 | $scope.getPeer('left'); 113 | }; 114 | 115 | $scope.getPeer = function (direction) { 116 | var currentEmployee = $scope.employee.name; 117 | var peers = $scope.employee.peers; 118 | 119 | peers.sort(function (a, b) { 120 | return a.name.localeCompare(b.name); 121 | }); 122 | 123 | if (direction == 'left') { 124 | peers.reverse(); 125 | } 126 | 127 | var peer; 128 | var leftPeerFound = true; 129 | var rightPeerFound = true; 130 | 131 | for (var i = 0; i < peers.length; i++) { 132 | var n = peers[i].name.localeCompare(currentEmployee); 133 | if (direction == 'left') { 134 | if (n == -1) { 135 | leftPeerFound = true; 136 | peer = peers[i]; 137 | break; 138 | } else { 139 | leftPeerFound = false; 140 | } 141 | } else { 142 | if (n == 1) { 143 | rightPeerFound = true; 144 | peer = peers[i]; 145 | break; 146 | } else { 147 | rightPeerFound = false; 148 | } 149 | } 150 | } 151 | 152 | if ((!leftPeerFound) && (direction == 'left')) { 153 | peer = peers[0]; 154 | } 155 | 156 | if ((!rightPeerFound) && (direction == 'right')) { 157 | peer = peers[0]; 158 | } 159 | 160 | if (typeof peer !== "undefined") { 161 | $scope.getEmployee(peer); 162 | } 163 | }; 164 | 165 | $scope.toggleSearch = function () { 166 | $scope.searchFlag = !$scope.searchFlag; 167 | }; 168 | 169 | $scope.togglePhoneSelector = function (objLength) { 170 | 171 | var phoneObject = $scope.employee.phones; 172 | 173 | if (phoneObject.length > 1) { 174 | $scope.phoneSelectorFlag = !$scope.phoneSelectorFlag; 175 | } else { 176 | for (var i = 0; i < phoneObject.length; i++) { 177 | $scope.callEmployee(phoneObject[i].value); 178 | } 179 | } 180 | }; 181 | 182 | $scope.toggleExpiredTokenOverlay = function () { 183 | $scope.expiredTokenFlag = !$scope.expiredTokenFlag; 184 | }; 185 | 186 | $scope.toggleListView = function () { 187 | $scope.listToggleFlag = !$scope.listToggleFlag; 188 | }; 189 | 190 | $scope.callEmployee = function (number) { 191 | window.top.location = "tel:" + number; 192 | }; 193 | 194 | $scope.employeeFocusDetail = function (viewflag) { 195 | if (viewflag == true) { 196 | $scope.toggleListView(); 197 | } else { 198 | if ($scope.employee && $scope.employee.id) { 199 | Sfdc.canvas.client.publish(canvasContext.client, { 200 | name: 's1.navigateToSObject', 201 | payload: { 202 | recordId: $scope.employee.id, 203 | view: 'detail' 204 | } 205 | }); 206 | } 207 | } 208 | }; 209 | 210 | $scope.sendTokenExpired = function () { 211 | Sfdc.canvas.client.repost({refresh : true}); 212 | }; 213 | 214 | $scope.hasOverflow = function (directReportsSize) { 215 | return directReportsSize > 8 ? "hasOverflow" : "noOverflow"; 216 | }; 217 | 218 | $scope.reduceSearchResults = function (searchResults) { 219 | if (angular.isDefined(searchResults)) { 220 | var result = []; 221 | var resultUl = document.getElementById('searchResultWrapper').getElementsByTagName('UL'); 222 | var resultLi = 32; 223 | var eleHeight = resultUl[0].offsetHeight; 224 | var windowHeight = window.innerHeight; 225 | var sf1NavBar = 74; // sf1bar height and padding(50) && ellipsis(24px) 226 | var limit = (windowHeight - sf1NavBar) / resultLi; 227 | 228 | if ((eleHeight >= windowHeight) && (searchResults.length >= limit)) { 229 | var count = 1; //start index @ 1 230 | 231 | searchResults.forEach(function (value, key) { 232 | if (count <= Math.floor(limit)) { 233 | count++; 234 | result[key] = value; 235 | $scope.ellipsisFlag = true; 236 | } 237 | }); 238 | 239 | } else { 240 | result = searchResults; 241 | $scope.ellipsisFlag = false; 242 | } 243 | 244 | return result; 245 | } 246 | }; 247 | 248 | var init = function () { 249 | var mgrDetail = document.getElementById('managerDetail'); 250 | var empDetail = document.getElementById('employeeDetail'); 251 | var overlay = document.getElementById('overlay'); 252 | var phoneSel = document.getElementById('phoneSelector'); 253 | var startingPoint; 254 | var currentUserEmail = canvasContext.context.user.email; 255 | 256 | //async loading the initial model 257 | orgChartCacheService.me(currentUserEmail) 258 | .then(function (data) { 259 | $scope.employee = data; 260 | $scope.employeeTitle = data.title; 261 | me = data; 262 | $scope.repaintView(data); // Included as part of fix for 7.0.6 manager scroll bug 263 | 264 | }, onServiceError); 265 | 266 | // Swallow touchmove events in the employee area. 267 | empDetail.addEventListener('touchmove', function (event) { 268 | event.preventDefault(); 269 | }); 270 | 271 | overlay.addEventListener('touchmove', function (event) { 272 | event.preventDefault(); 273 | }); 274 | 275 | phoneSel.addEventListener('touchmove', function (event) { 276 | event.preventDefault(); 277 | }); 278 | 279 | mgrDetail.addEventListener('touchstart', function (event) { 280 | startingPoint = event.changedTouches[0].clientY; 281 | }); 282 | 283 | mgrDetail.addEventListener('touchmove', function (event) { 284 | var nextPoint = event.changedTouches[0].clientY; 285 | 286 | if ((startingPoint > nextPoint) && (mgrDetail.scrollTop <= 0)) { 287 | event.preventDefault(); 288 | } 289 | 290 | if ((startingPoint < nextPoint) && ((mgrDetail.clientHeight + mgrDetail.scrollTop) >= mgrDetail.scrollHeight)) { 291 | event.preventDefault(); 292 | } 293 | }); 294 | 295 | 296 | window.addEventListener('shake', function () { 297 | if ($scope.employee != me) { 298 | $scope.employee = me; 299 | $scope.employeeTitle = me.title; 300 | $scope.$apply(); 301 | } 302 | }, false); 303 | }; 304 | 305 | init(); 306 | 307 | } 308 | 309 | ]); 310 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | 203 | -------------------------------------------------------------------------------- /app/vendor/angular-cache-2.3.4.min.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Jason Dobry 3 | * @file angular-cache.min.js 4 | * @version 2.3.4 - Homepage 5 | * @copyright (c) 2013 -2014 Jason Dobry 6 | * @license MIT 7 | * 8 | * @overview angular-cache is a very useful replacement for Angular's $cacheFactory. 9 | */ 10 | !function (a, b, c) { 11 | "use strict"; 12 | function d() { 13 | this.$get = function () { 14 | function a(a, b, c) { 15 | for (var d = a[c], e = b(d); c > 0;) { 16 | var f = Math.floor((c + 1) / 2) - 1, g = a[f]; 17 | if (e >= b(g))break; 18 | a[f] = d, a[c] = g, c = f 19 | } 20 | } 21 | 22 | function c(a, b, c) { 23 | for (var d = a.length, e = a[c], f = b(e); ;) { 24 | var g = 2 * (c + 1), h = g - 1, i = null; 25 | if (d > h) { 26 | var j = a[h], k = b(j); 27 | f > k && (i = h) 28 | } 29 | if (d > g) { 30 | var l = a[g], m = b(l); 31 | m < (null === i ? f : b(a[h])) && (i = g) 32 | } 33 | if (null === i)break; 34 | a[c] = a[i], a[i] = e, c = i 35 | } 36 | } 37 | 38 | function d(a) { 39 | if (a && !b.isFunction(a))throw new Error("BinaryHeap(weightFunc): weightFunc: must be a function!"); 40 | a = a || function (a) { 41 | return a 42 | }, this.weightFunc = a, this.heap = [] 43 | } 44 | 45 | return d.prototype.push = function (b) { 46 | this.heap.push(b), a(this.heap, this.weightFunc, this.heap.length - 1) 47 | }, d.prototype.peek = function () { 48 | return this.heap[0] 49 | }, d.prototype.pop = function () { 50 | var a = this.heap[0], b = this.heap.pop(); 51 | return this.heap.length > 0 && (this.heap[0] = b, c(this.heap, this.weightFunc, 0)), a 52 | }, d.prototype.remove = function (d) { 53 | for (var e = this.heap.length, f = 0; e > f; f++)if (b.equals(this.heap[f], d)) { 54 | var g = this.heap[f], h = this.heap.pop(); 55 | return f !== e - 1 && (this.heap[f] = h, a(this.heap, this.weightFunc, f), c(this.heap, this.weightFunc, f)), g 56 | } 57 | return null 58 | }, d.prototype.removeAll = function () { 59 | this.heap = [] 60 | }, d.prototype.size = function () { 61 | return this.heap.length 62 | }, d 63 | } 64 | } 65 | 66 | function e() { 67 | function a(a, c) { 68 | c(b.isNumber(a) ? 0 > a ? "must be greater than zero!" : null : "must be a number!") 69 | } 70 | 71 | var d, e = function () { 72 | return{capacity: Number.MAX_VALUE, maxAge: null, deleteOnExpire: "none", onExpire: null, cacheFlushInterval: null, recycleFreq: 1e3, storageMode: "none", storageImpl: null, verifyIntegrity: !0, disabled: !1} 73 | }; 74 | this.setCacheDefaults = function (c) { 75 | var f = "$angularCacheFactoryProvider.setCacheDefaults(options): "; 76 | if (c = c || {}, !b.isObject(c))throw new Error(f + "options: must be an object!"); 77 | if ("disabled"in c && (c.disabled = c.disabled === !0), "capacity"in c && a(c.capacity, function (a) { 78 | if (a)throw new Error(f + "capacity: " + a) 79 | }), "deleteOnExpire"in c) { 80 | if (!b.isString(c.deleteOnExpire))throw new Error(f + "deleteOnExpire: must be a string!"); 81 | if ("none" !== c.deleteOnExpire && "passive" !== c.deleteOnExpire && "aggressive" !== c.deleteOnExpire)throw new Error(f + 'deleteOnExpire: accepted values are "none", "passive" or "aggressive"!') 82 | } 83 | if ("maxAge"in c && a(c.maxAge, function (a) { 84 | if (a)throw new Error(f + "maxAge: " + a) 85 | }), "recycleFreq"in c && a(c.recycleFreq, function (a) { 86 | if (a)throw new Error(f + "recycleFreq: " + a) 87 | }), "cacheFlushInterval"in c && a(c.cacheFlushInterval, function (a) { 88 | if (a)throw new Error(f + "cacheFlushInterval: " + a) 89 | }), "storageMode"in c) { 90 | if (!b.isString(c.storageMode))throw new Error(f + "storageMode: must be a string!"); 91 | if ("none" !== c.storageMode && "localStorage" !== c.storageMode && "sessionStorage" !== c.storageMode)throw new Error(f + 'storageMode: accepted values are "none", "localStorage" or "sessionStorage"!'); 92 | if ("storageImpl"in c) { 93 | if (!b.isObject(c.storageImpl))throw new Error(f + "storageImpl: must be an object!"); 94 | if (!("setItem"in c.storageImpl && "function" == typeof c.storageImpl.setItem))throw new Error(f + 'storageImpl: must implement "setItem(key, value)"!'); 95 | if (!("getItem"in c.storageImpl && "function" == typeof c.storageImpl.getItem))throw new Error(f + 'storageImpl: must implement "getItem(key)"!'); 96 | if (!("removeItem"in c.storageImpl) || "function" != typeof c.storageImpl.removeItem)throw new Error(f + 'storageImpl: must implement "removeItem(key)"!') 97 | } 98 | } 99 | if ("onExpire"in c && "function" != typeof c.onExpire)throw new Error(f + "onExpire: must be a function!"); 100 | d = b.extend({}, e(), c) 101 | }, this.setCacheDefaults({}), this.$get = ["$window", "BinaryHeap", function (e, f) { 102 | function g(a) { 103 | return a && b.isNumber(a) ? a.toString() : a 104 | } 105 | 106 | function h(a) { 107 | var b, c = {}; 108 | for (b in a)a.hasOwnProperty(b) && (c[b] = b); 109 | return c 110 | } 111 | 112 | function i(a) { 113 | var b, c = []; 114 | for (b in a)a.hasOwnProperty(b) && c.push(b); 115 | return c 116 | } 117 | 118 | function j(j, k) { 119 | function m(b) { 120 | a(b, function (a) { 121 | if (a)throw new Error("capacity: " + a); 122 | for (B.capacity = b; E.size() > B.capacity;)H.remove(E.peek().key, {verifyIntegrity: !1}) 123 | }) 124 | } 125 | 126 | function n(a) { 127 | if (!b.isString(a))throw new Error("deleteOnExpire: must be a string!"); 128 | if ("none" !== a && "passive" !== a && "aggressive" !== a)throw new Error('deleteOnExpire: accepted values are "none", "passive" or "aggressive"!'); 129 | B.deleteOnExpire = a 130 | } 131 | 132 | function o(b) { 133 | var c = i(C); 134 | if (null === b) { 135 | if (B.maxAge)for (var d = 0; d < c.length; d++) { 136 | var e = c[d]; 137 | "maxAge"in C[e] || (delete C[e].expires, D.remove(C[e])) 138 | } 139 | B.maxAge = b 140 | } else a(b, function (a) { 141 | if (a)throw new Error("maxAge: " + a); 142 | if (b !== B.maxAge) { 143 | B.maxAge = b; 144 | for (var d = (new Date).getTime(), e = 0; e < c.length; e++) { 145 | var f = c[e]; 146 | "maxAge"in C[f] || (D.remove(C[f]), C[f].expires = C[f].created + B.maxAge, D.push(C[f]), C[f].expires < d && H.remove(f, {verifyIntegrity: !1})) 147 | } 148 | } 149 | }) 150 | } 151 | 152 | function p() { 153 | for (var a = (new Date).getTime(), b = D.peek(); b && b.expires && b.expires < a;)H.remove(b.key, {verifyIntegrity: !1}), B.onExpire && B.onExpire(b.key, b.value), b = D.peek() 154 | } 155 | 156 | function q(b) { 157 | null === b ? (B.recycleFreqId && (clearInterval(B.recycleFreqId), delete B.recycleFreqId), B.recycleFreq = d.recycleFreq, B.recycleFreqId = setInterval(p, B.recycleFreq)) : a(b, function (a) { 158 | if (a)throw new Error("recycleFreq: " + a); 159 | B.recycleFreq = b, B.recycleFreqId && clearInterval(B.recycleFreqId), B.recycleFreqId = setInterval(p, B.recycleFreq) 160 | }) 161 | } 162 | 163 | function r(b) { 164 | null === b ? (B.cacheFlushIntervalId && (clearInterval(B.cacheFlushIntervalId), delete B.cacheFlushIntervalId), B.cacheFlushInterval = b) : a(b, function (a) { 165 | if (a)throw new Error("cacheFlushInterval: " + a); 166 | b !== B.cacheFlushInterval && (B.cacheFlushIntervalId && clearInterval(B.cacheFlushIntervalId), B.cacheFlushInterval = b, B.cacheFlushIntervalId = setInterval(H.removeAll, B.cacheFlushInterval)) 167 | }) 168 | } 169 | 170 | function s(a, c) { 171 | var d, f; 172 | if (!b.isString(a))throw new Error("storageMode: must be a string!"); 173 | if ("none" !== a && "localStorage" !== a && "sessionStorage" !== a)throw new Error('storageMode: accepted values are "none", "localStorage" or "sessionStorage"!'); 174 | if (("localStorage" === B.storageMode || "sessionStorage" === B.storageMode) && a !== B.storageMode) { 175 | for (d = i(C), f = 0; f < d.length; f++)I.removeItem(F + ".data." + d[f]); 176 | I.removeItem(F + ".keys") 177 | } 178 | if (B.storageMode = a, c) { 179 | if (!b.isObject(c))throw new Error("storageImpl: must be an object!"); 180 | if (!("setItem"in c && "function" == typeof c.setItem))throw new Error('storageImpl: must implement "setItem(key, value)"!'); 181 | if (!("getItem"in c && "function" == typeof c.getItem))throw new Error('storageImpl: must implement "getItem(key)"!'); 182 | if (!("removeItem"in c) || "function" != typeof c.removeItem)throw new Error('storageImpl: must implement "removeItem(key)"!'); 183 | I = c 184 | } else"localStorage" === B.storageMode ? I = e.localStorage : "sessionStorage" === B.storageMode && (I = e.sessionStorage); 185 | if ("none" !== B.storageMode && I)if (G)for (d = i(C), f = 0; f < d.length; f++)v(d[f]); else u() 186 | } 187 | 188 | function t(a, c, e) { 189 | if (a = a || {}, e = e || {}, c = !!c, !b.isObject(a))throw new Error("AngularCache.setOptions(cacheOptions, strict, options): cacheOptions: must be an object!"); 190 | if (w(e.verifyIntegrity), c && (a = b.extend({}, d, a)), "disabled"in a && (B.disabled = a.disabled === !0), "verifyIntegrity"in a && (B.verifyIntegrity = a.verifyIntegrity === !0), "capacity"in a && m(a.capacity), "deleteOnExpire"in a && n(a.deleteOnExpire), "maxAge"in a && o(a.maxAge), "recycleFreq"in a && q(a.recycleFreq), "cacheFlushInterval"in a && r(a.cacheFlushInterval), "storageMode"in a && s(a.storageMode, a.storageImpl), "onExpire"in a) { 191 | if (null !== a.onExpire && "function" != typeof a.onExpire)throw new Error("onExpire: must be a function!"); 192 | B.onExpire = a.onExpire 193 | } 194 | G = !0 195 | } 196 | 197 | function u() { 198 | var a = b.fromJson(I.getItem(F + ".keys")); 199 | if (I.removeItem(F + ".keys"), a && a.length) { 200 | for (var c = 0; c < a.length; c++) { 201 | var d = b.fromJson(I.getItem(F + ".data." + a[c])) || {}, e = d.maxAge || B.maxAge, f = d.deleteOnExpire || B.deleteOnExpire; 202 | if (e && (new Date).getTime() - d.created > e && "aggressive" === f)I.removeItem(F + ".data." + a[c]); else { 203 | var g = {created: d.created}; 204 | d.expires && (g.expires = d.expires), d.accessed && (g.accessed = d.accessed), d.maxAge && (g.maxAge = d.maxAge), d.deleteOnExpire && (g.deleteOnExpire = d.deleteOnExpire), H.put(a[c], d.value, g) 205 | } 206 | } 207 | v(null) 208 | } 209 | } 210 | 211 | function v(a) { 212 | "none" !== B.storageMode && I && (I.setItem(F + ".keys", b.toJson(i(C))), a && I.setItem(F + ".data." + a, b.toJson(C[a]))) 213 | } 214 | 215 | function w(a) { 216 | if ((a || a !== !1 && B.verifyIntegrity) && "none" !== B.storageMode && I) { 217 | var c = i(C); 218 | I.setItem(F + ".keys", b.toJson(c)); 219 | for (var d = 0; d < c.length; d++)I.setItem(F + ".data." + c[d], b.toJson(C[c[d]])) 220 | } 221 | } 222 | 223 | function x(a, c) { 224 | if ((c || c !== !1 && B.verifyIntegrity) && "none" !== B.storageMode && I) { 225 | var d = I.getItem(F + ".data." + a); 226 | if (!d && a in C)H.remove(a); else if (d) { 227 | var e = b.fromJson(d), f = e ? e.value : null; 228 | f && H.put(a, f) 229 | } 230 | } 231 | } 232 | 233 | function y(a) { 234 | if ("none" !== B.storageMode && I) { 235 | var c = a || i(C); 236 | I.setItem(F + ".keys", b.toJson(c)) 237 | } 238 | } 239 | 240 | function z(a) { 241 | "none" !== B.storageMode && I && a in C && I.setItem(F + ".data." + a, b.toJson(C[a])) 242 | } 243 | 244 | function A() { 245 | if ("none" !== B.storageMode && I) { 246 | for (var a = i(C), c = 0; c < a.length; c++)I.removeItem(F + ".data." + a[c]); 247 | I.setItem(F + ".keys", b.toJson([])) 248 | } 249 | } 250 | 251 | var B = b.extend({}, {id: j}), C = {}, D = new f(function (a) { 252 | return a.expires 253 | }), E = new f(function (a) { 254 | return a.accessed 255 | }), F = "angular-cache.caches." + j, G = !1, H = this, I = null; 256 | k = k || {}, this.put = function (c, d, e) { 257 | if (!B.disabled) { 258 | if (e = e || {}, c = g(c), !b.isString(c))throw new Error("AngularCache.put(key, value, options): key: must be a string!"); 259 | if (e && !b.isObject(e))throw new Error("AngularCache.put(key, value, options): options: must be an object!"); 260 | if (e.maxAge && null !== e.maxAge)a(e.maxAge, function (a) { 261 | if (a)throw new Error("AngularCache.put(key, value, options): maxAge: " + a) 262 | }); else { 263 | if (e.deleteOnExpire && !b.isString(e.deleteOnExpire))throw new Error("AngularCache.put(key, value, options): deleteOnExpire: must be a string!"); 264 | if (e.deleteOnExpire && "none" !== e.deleteOnExpire && "passive" !== e.deleteOnExpire && "aggressive" !== e.deleteOnExpire)throw new Error('AngularCache.put(key, value, options): deleteOnExpire: accepted values are "none", "passive" or "aggressive"!'); 265 | if (b.isUndefined(d))return 266 | } 267 | var f, h, i = (new Date).getTime(); 268 | return w(e.verifyIntegrity), C[c] ? (D.remove(C[c]), E.remove(C[c])) : C[c] = {key: c}, h = C[c], h.value = d, h.created = parseInt(e.created, 10) || h.created || i, h.accessed = parseInt(e.accessed, 10) || i, e.deleteOnExpire && (h.deleteOnExpire = e.deleteOnExpire), e.maxAge && (h.maxAge = e.maxAge), (h.maxAge || B.maxAge) && (h.expires = h.created + (h.maxAge || B.maxAge)), f = h.deleteOnExpire || B.deleteOnExpire, h.expires && "aggressive" === f && D.push(h), y(), z(c), E.push(h), E.size() > B.capacity && this.remove(E.peek().key, {verifyIntegrity: !1}), d 269 | } 270 | }, this.get = function (a, d) { 271 | if (!B.disabled) { 272 | if (b.isArray(a)) { 273 | var e = a, f = []; 274 | return b.forEach(e, function (a) { 275 | var c = H.get(a, d); 276 | b.isDefined(c) && f.push(c) 277 | }), f 278 | } 279 | if (a = g(a), d = d || {}, !b.isString(a))throw new Error("AngularCache.get(key, options): key: must be a string!"); 280 | if (d && !b.isObject(d))throw new Error("AngularCache.get(key, options): options: must be an object!"); 281 | if (d.onExpire && !b.isFunction(d.onExpire))throw new Error("AngularCache.get(key, options): onExpire: must be a function!"); 282 | if (x(a, d.verifyIntegrity), a in C) { 283 | var h = C[a], i = h.value, j = (new Date).getTime(), k = h.deleteOnExpire || B.deleteOnExpire; 284 | return E.remove(h), h.accessed = j, E.push(h), "passive" === k && "expires"in h && h.expires < j && (this.remove(a, {verifyIntegrity: !1}), B.onExpire ? B.onExpire(a, h.value, d.onExpire) : d.onExpire && d.onExpire(a, h.value), i = c), z(a), i 285 | } 286 | } 287 | }, this.remove = function (a, b) { 288 | b = b || {}, w(b.verifyIntegrity), E.remove(C[a]), D.remove(C[a]), "none" !== B.storageMode && I && I.removeItem(F + ".data." + a), delete C[a], y() 289 | }, this.removeAll = function () { 290 | A(), E.removeAll(), D.removeAll(), C = {} 291 | }, this.removeExpired = function (a) { 292 | a = a || {}; 293 | for (var b = (new Date).getTime(), c = i(C), d = {}, e = 0; e < c.length; e++)C[c[e]] && C[c[e]].expires && C[c[e]].expires < b && (d[c[e]] = C[c[e]].value); 294 | for (var f in d)H.remove(f); 295 | if (w(a.verifyIntegrity), a.asArray) { 296 | var g = []; 297 | for (f in d)g.push(d[f]); 298 | return g 299 | } 300 | return d 301 | }, this.destroy = function () { 302 | B.cacheFlushIntervalId && clearInterval(B.cacheFlushIntervalId), B.recycleFreqId && clearInterval(B.recycleFreqId), this.removeAll(), "none" !== B.storageMode && I && (I.removeItem(F + ".keys"), I.removeItem(F)), I = null, C = null, E = null, D = null, B = null, F = null, H = null; 303 | for (var a = i(this), b = 0; b < a.length; b++)this.hasOwnProperty(a[b]) && delete this[a[b]]; 304 | l[j] = null, delete l[j] 305 | }, this.info = function (a) { 306 | if (a) { 307 | if (C[a]) { 308 | var c = {created: C[a].created, accessed: C[a].accessed, expires: C[a].expires, maxAge: C[a].maxAge || B.maxAge, deleteOnExpire: C[a].deleteOnExpire || B.deleteOnExpire, isExpired: !1}; 309 | return c.maxAge && (c.isExpired = (new Date).getTime() - c.created > c.maxAge), c 310 | } 311 | return C[a] 312 | } 313 | return b.extend({}, B, {size: E && E.size() || 0}) 314 | }, this.keySet = function () { 315 | return h(C) 316 | }, this.keys = function () { 317 | return i(C) 318 | }, this.setOptions = t, t(k, !0, {verifyIntegrity: !1}) 319 | } 320 | 321 | function k(a, c) { 322 | if (a in l)throw new Error("cacheId " + a + " taken!"); 323 | if (!b.isString(a))throw new Error("cacheId must be a string!"); 324 | return l[a] = new j(a, c), l[a] 325 | } 326 | 327 | var l = {}; 328 | return k.info = function () { 329 | for (var a = i(l), c = {size: a.length, caches: {}}, e = 0; e < a.length; e++) { 330 | var f = a[e]; 331 | c.caches[f] = l[f].info() 332 | } 333 | return c.cacheDefaults = b.extend({}, d), c 334 | }, k.get = function (a) { 335 | if (!b.isString(a))throw new Error("$angularCacheFactory.get(cacheId): cacheId: must be a string!"); 336 | return l[a] 337 | }, k.keySet = function () { 338 | return h(l) 339 | }, k.keys = function () { 340 | return i(l) 341 | }, k.removeAll = function () { 342 | for (var a = i(l), b = 0; b < a.length; b++)l[a[b]].destroy() 343 | }, k.clearAll = function () { 344 | for (var a = i(l), b = 0; b < a.length; b++)l[a[b]].removeAll() 345 | }, k.enableAll = function () { 346 | for (var a = i(l), b = 0; b < a.length; b++)l[a[b]].setOptions({disabled: !1}) 347 | }, k.disableAll = function () { 348 | for (var a = i(l), b = 0; b < a.length; b++)l[a[b]].setOptions({disabled: !0}) 349 | }, k 350 | }] 351 | } 352 | 353 | b.module("jmdobry.binary-heap", []).provider("BinaryHeap", d), b.module("jmdobry.angular-cache", ["ng", "jmdobry.binary-heap"]).provider("$angularCacheFactory", e) 354 | }(window, window.angular); 355 | -------------------------------------------------------------------------------- /dist/css/style.css: -------------------------------------------------------------------------------- 1 | .list-horizontal,.list-plain,ul{list-style:none}.list-horizontal,body,p,ul{margin:0;padding:0}#employeeLeftIcons a,.icon-utility-call,a{text-decoration:none}.reports,.reports li,body,p,ul{padding:0}*,:after,:before{box-sizing:border-box}body{font-family:SalesforceSans;background:#fff;color:#999;font-smoothing:antialiased;-webkit-tap-highlight-color:transparent;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.icon-utility-call,a{color:#293f54}.list-horizontal>li,.list-horizontal>li>a{display:inline-block;zoom:1}.shadow{box-shadow:0 2px 0 rgba(0,0,0,.1)}.hide{display:none!important}.show{display:block!important}.fadeIt{opacity:.01}.unfadeIt{opacity:1}.showFlex{display:-webkit-flex!important;display:flex!important}.capitalize{text-transform:capitalize}.overflowScrolling-touch{-webkit-overflow-scrolling:touch}.overflowScrolling-default{-webkit-overflow-scrolling:auto}.main-title,.name,.title{overflow:hidden;margin:0 auto}@font-face{font-family:SalesforceSans;src:url(https://www2.sfdcstatic.com/system/shared/common/assets/fonts/SalesforceSans/SalesforceSans-Regular.woff) format('woff');font-weight:300;font-style:normal}.employee-image,.report-image{height:100px;width:100px;background-size:100px;position:relative;z-index:3;margin:0 auto;border:4px solid #fff;border-radius:50%;display:inline-block;background-position:center;background-color:#FFF;background-repeat:no-repeat}.report-image{border-width:1px}.employee-image{border:2px solid #8D3EEA}#swipePad{height:100px;width:100%;margin:-26px auto 2px;z-index:10}.report-image{height:30px;width:30px;background-size:30px;margin:0}#employeeDetail{text-align:center;position:fixed;bottom:0;left:0;width:100%;z-index:5;height:200px}#employeeStrip{width:100%;height:46px;margin:0;background:#f5f5f5;border-width:1px 0;position:absolute;font-size:18pt;top:0}#employeeStrip a,#employeeStrip i{width:44px;height:44px;color:#293f54;display:inline-block;line-height:44px}#employeeEmail,.name,.reports>li a,.title{display:block}#employeeLeftIcons,#employeeRightIcons{margin:0 10px}#employeeLeftIcons{float:left}#employeeRightIcons{float:right}#peersIcons{width:185px;margin:0 auto}#peersIcons i{color:#ccc;font-size:14pt}.name,.title{color:#2A94D6;z-index:999;white-space:nowrap;width:200px;text-overflow:ellipsis}.title{color:#999;font-size:.8em}.main-title{max-height:40px;width:180px;height:40px;font-size:.8em}.reports{background:#fff;list-style-type:none;position:relative;margin:0;top:-270px}.reports li,.reports>li a{position:absolute;width:30px}.reports li{margin:0 0 0 -15px;border:0;height:170px;-webkit-transform-origin:15px bottom;transform-origin:15px bottom}.reports li span{transition:-webkit-transform .4s ease;transition:transform .4s ease;display:block;margin:0}.reports>li a{height:30px;overflow:visible;text-align:center;border-radius:50%}.reports>li:nth-of-type(1){-webkit-transform:rotate(-108deg);transform:rotate(-108deg)}.reports>li:nth-of-type(2){-webkit-transform:rotate(-126deg);transform:rotate(-126deg)}.reports>li:nth-of-type(3){-webkit-transform:rotate(-144deg);transform:rotate(-144deg)}.reports>li:nth-of-type(4){-webkit-transform:rotate(-162deg);transform:rotate(-162deg)}.reports>li:nth-of-type(5){-webkit-transform:rotate(-180deg);transform:rotate(-180deg)}.reports>li:nth-of-type(6){-webkit-transform:rotate(-198deg);transform:rotate(-198deg)}.reports>li:nth-of-type(7){-webkit-transform:rotate(-216deg);transform:rotate(-216deg)}.reports>li:nth-of-type(8){-webkit-transform:rotate(-234deg);transform:rotate(-234deg)}.reports>li:nth-of-type(9){-webkit-transform:rotate(-252deg);transform:rotate(-252deg)}.reports.expand li:nth-of-type(9) a ::before{content:'+';margin-right:2px}.reports.expand li:nth-of-type(1) a span{-webkit-transform:rotate(108deg);transform:rotate(108deg)}.reports.expand li:nth-of-type(2) a span{-webkit-transform:rotate(126deg);transform:rotate(126deg);-webkit-animation:spin2-expand .6s ease-out 1 backwards;animation:spin2-expand .6s ease-out 1 backwards}.reports.expand li:nth-of-type(3) a span{-webkit-transform:rotate(144deg);transform:rotate(144deg);-webkit-animation:spin3-expand .6s ease-out 1 backwards;animation:spin3-expand .6s ease-out 1 backwards}.reports.expand li:nth-of-type(4) a span{-webkit-transform:rotate(162deg);transform:rotate(162deg);-webkit-animation:spin4-expand .6s ease-out 1 backwards;animation:spin4-expand .6s ease-out 1 backwards}.reports.expand li:nth-of-type(5) a span{-webkit-transform:rotate(180deg);transform:rotate(180deg);-webkit-animation:spin5-expand .6s ease-out 1 backwards;animation:spin5-expand .6s ease-out 1 backwards}.reports.expand li:nth-of-type(6) a span{-webkit-transform:rotate(198deg);transform:rotate(198deg);-webkit-animation:spin6-expand .6s ease-out 1 backwards;animation:spin6-expand .6s ease-out 1 backwards}.reports.expand li:nth-of-type(7) a span{-webkit-transform:rotate(216deg);transform:rotate(216deg);-webkit-animation:spin7-expand .6s ease-out 1 backwards;animation:spin7-expand .6s ease-out 1 backwards}.reports.expand li:nth-of-type(8) a span{-webkit-transform:rotate(234deg);transform:rotate(234deg);-webkit-animation:spin8-expand .6s ease-out 1 backwards;animation:spin8-expand .6s ease-out 1 backwards}.reports.expand li:nth-of-type(9) a span{-webkit-transform:rotate(252deg);transform:rotate(252deg);-webkit-animation:spin9-expand .6s ease-out 1 backwards;animation:spin9-expand .6s ease-out 1 backwards}.reports li a{top:165px;-webkit-animation:contract .35s ease-out 1 backwards;animation:contract .35s ease-out 1 backwards}.reports.expand li a{top:10px;-webkit-animation:expand .6s ease 1 backwards;animation:expand .6s ease 1 backwards}.reports li:nth-of-type(1) a{-webkit-animation-delay:.32s;animation-delay:.32s}.reports li:nth-of-type(2) a{-webkit-animation-delay:.28s;animation-delay:.28s}.reports li:nth-of-type(3) a{-webkit-animation-delay:.24s;animation-delay:.24s}.reports li:nth-of-type(4) a{-webkit-animation-delay:.2s;animation-delay:.2s}.reports li:nth-of-type(5) a{-webkit-animation-delay:.16s;animation-delay:.16s}.reports li:nth-of-type(6) a{-webkit-animation-delay:.12s;animation-delay:.12s}.reports li:nth-of-type(7) a{-webkit-animation-delay:.08s;animation-delay:.08s}.reports li:nth-of-type(8) a{-webkit-animation-delay:.04s;animation-delay:.04s}.reports li:nth-of-type(9) a,.reports.expand li:nth-of-type(1) a{-webkit-animation-delay:0s;animation-delay:0s}.reports.expand li:nth-of-type(2) a{-webkit-animation-delay:.04s;animation-delay:.04s}.reports.expand li:nth-of-type(3) a{-webkit-animation-delay:.08s;animation-delay:.08s}.reports.expand li:nth-of-type(4) a{-webkit-animation-delay:.12s;animation-delay:.12s}.reports.expand li:nth-of-type(5) a{-webkit-animation-delay:.16s;animation-delay:.16s}.reports.expand li:nth-of-type(6) a{-webkit-animation-delay:.2s;animation-delay:.2s}.reports.expand li:nth-of-type(7) a{-webkit-animation-delay:.24s;animation-delay:.24s}.reports.expand li:nth-of-type(8) a{-webkit-animation-delay:.28s;animation-delay:.28s}.reports.expand li:nth-of-type(9) a{line-height:22px;text-align:center;background:rgba(42,148,214,.5);border:4px solid rgba(255,255,255,.4);color:#fff;font-size:.7em;-webkit-animation-delay:.32s;animation-delay:.32s}#reportsListView{width:100%;height:100%;z-index:5;top:150px;left:0;position:fixed;overflow-y:scroll;-webkit-overflow-scrolling:touch}.tog,.toggle{position:absolute;left:10px;text-align:left}#reportsListView ul{margin-bottom:170px}#reportsListView li{text-align:left;cursor:pointer;clear:both;border-top:1px solid #dadee2;height:45px;padding:0 10px}#reportsListView li :first-child{border-top:none}#reportsListView li i{float:left;margin:12px 12px 8px 2px}#reportsListView li div{float:left;margin:10px 10px 10px 5px}#reportsListView .name,#reportsListView .title{width:240px}.tog{bottom:10px}.toggle{top:70px}.employeeFocusListView{top:40px!important;position:fixed!important;width:100%!important}#managerDetail{background-color:#FFF;text-align:center;width:100%;overflow-y:scroll;height:325px;height:calc(100% - 180px);position:absolute;-webkit-transform:translate3D(0,0,-1px) rotateX(180deg);transform:translate3D(0,0,-1px) rotateX(180deg)}.manager-image{height:60px;width:60px;background-size:60px;border:4px solid #fff;border-radius:50%;display:inline-block;background-position:center;background-repeat:no-repeat}.reporting-structure{-webkit-transform:translate3D(0,0,-1px) rotateX(180deg);transform:translate3D(0,0,-1px) rotateX(180deg);position:absolute;width:100%;padding-bottom:50px}.reporting-structure li{margin:30px auto;position:relative}.reporting-structure li::before,.reporting-structure li:nth-last-child(1)::after{-webkit-animation:fadeIn 5s 0 1 ease normal;animation:fadeIn 5s 0 1 ease normal;content:'';position:absolute;top:-62px;left:50%;width:2px;height:70px;background:#eee;z-index:-1}#searchResultWrapper,#searchResultWrapper_row{width:100%;height:100%}.reporting-structure li:nth-last-child(1)::after{top:62px}.reporting-structure li:first-child::before{display:none}.reporting-structure li:nth-last-child(1) i,.reporting-structure li:nth-last-child(1) p{-webkit-animation-delay:0s;animation-delay:0s}.reporting-structure li:nth-last-child(2) i,.reporting-structure li:nth-last-child(2) p{-webkit-animation-delay:.04s;animation-delay:.04s}.reporting-structure li:nth-last-child(3) i,.reporting-structure li:nth-last-child(3) p{-webkit-animation-delay:.08s;animation-delay:.08s}.reporting-structure li:nth-last-child(4) i,.reporting-structure li:nth-last-child(4) p{-webkit-animation-delay:.12s;animation-delay:.12s}.reporting-structure li:nth-last-child(5) i,.reporting-structure li:nth-last-child(5) p{-webkit-animation-delay:.16s;animation-delay:.16s}.reporting-structure li:nth-last-child(6) i,.reporting-structure li:nth-last-child(6) p{-webkit-animation-delay:.2s;animation-delay:.2s}.reporting-structure li:nth-last-child(7) i,.reporting-structure li:nth-last-child(7) p{-webkit-animation-delay:.24s;animation-delay:.24s}.reporting-structure li:nth-last-child(8) i,.reporting-structure li:nth-last-child(8) p{-webkit-animation-delay:.28s;animation-delay:.28s}.reporting-structure li:nth-last-child(9) i,.reporting-structure li:nth-last-child(9) p{-webkit-animation-delay:.32s;animation-delay:.32s}.reporting-structure li:nth-last-child(10) i,.reporting-structure li:nth-last-child(10) p{-webkit-animation-delay:.36s;animation-delay:.36s}.reporting-structure li:nth-last-child(11) i,.reporting-structure li:nth-last-child(11) p{-webkit-animation-delay:.4s;animation-delay:.4s}.reporting-structure li:nth-last-child(12) i,.reporting-structure li:nth-last-child(12) p{-webkit-animation-delay:.44s;animation-delay:.44s}.reporting-structure li:nth-last-child(13) i,.reporting-structure li:nth-last-child(13) p{-webkit-animation-delay:.48s;animation-delay:.48s}.reporting-structure li:nth-last-child(14) i,.reporting-structure li:nth-last-child(14) p{-webkit-animation-delay:.52s;animation-delay:.52s}.reporting-structure li:nth-last-child(15) i,.reporting-structure li:nth-last-child(15) p{-webkit-animation-delay:.56s;animation-delay:.56s}.reporting-structure li:nth-last-child(16) i,.reporting-structure li:nth-last-child(16) p{-webkit-animation-delay:.6s;animation-delay:.6s}.reporting-structure li:nth-last-child(17) i,.reporting-structure li:nth-last-child(17) p{-webkit-animation-delay:.64s;animation-delay:.64s}.reporting-structure li:nth-last-child(18) i,.reporting-structure li:nth-last-child(18) p{-webkit-animation-delay:.68s;animation-delay:.68s}.reporting-structure li:nth-last-child(19) i,.reporting-structure li:nth-last-child(19) p{-webkit-animation-delay:.72s;animation-delay:.72s}.reporting-structure li:nth-last-child(20) i,.reporting-structure li:nth-last-child(20) p{-webkit-animation-delay:.76s;animation-delay:.76s}#searchResultWrapper{z-index:14;position:absolute;top:48px;left:0;display:-webkit-flex;-webkit-align-items:center;align-items:center;-webkit-justify-content:center;justify-content:center}#searchResultWrapper ul{height:100%;padding:5px 0;background-color:#fff}#searchResultWrapper ul li{cursor:pointer;padding:0;margin:10px}#searchBoxWrapper{position:absolute;top:5px;left:50%;z-index:15;width:86%;margin-left:-47%}#searchBox{width:100%;height:36px;padding:6px 10px;border-radius:5px;border:1px solid #85929F;font-size:16px}#searchToggle{padding:0;position:fixed;right:0;top:0;z-index:999;height:44px;width:44px;background:0;border:0;outline:0}.icon-utility-search{color:#999}.icon-utility-close{color:#fff}.icon-utility-close,.icon-utility-search{font-size:15px}#closeSelector{margin:0;padding:0;z-index:15;position:absolute;top:-44px;right:-4px}#expiredTokenOverlay,#phoneSelector{z-index:15;position:absolute;height:100%;width:100%;padding:0;margin:0;display:-webkit-flex;display:flex;-webkit-align-items:center;align-items:center;-webkit-justify-content:center;justify-content:center}#expiredTokenMessage{width:100%;padding:10px;text-align:center}#selectorRow{width:260px;border-radius:5px;background-color:#fff;padding:14px;position:relative}.selection li{border:1px solid #dfe0e1;color:#5c7995;font-size:16px;font-weight:700;width:100%;padding:7px 0;margin:0 auto 14px;text-align:center;cursor:pointer;border-radius:5px}.selection li:last-child{margin:0!important}#overlay{opacity:.5;background:#344A5F;width:100%;height:100%;z-index:10;top:0;left:0;position:fixed}.animated{-webkit-animation-duration:1s;animation-duration:1s;-webkit-animation-fill-mode:both;animation-fill-mode:both}.bounceIn{-webkit-animation:bounceIn .35s;animation:bounceIn .35s;opacity:1}.fadeIn{-webkit-animation-name:fadeIn;animation-name:fadeIn}.bounceInUp{-webkit-animation-name:bounceInUp;animation-name:bounceInUp}@-webkit-keyframes spin1-expand{0%{-webkit-transform:rotate(108deg);transform:rotate(108deg)}100%,60%{-webkit-transform:rotate(-252deg);transform:rotate(-252deg)}}@keyframes spin1-expand{0%{-webkit-transform:rotate(108deg);transform:rotate(108deg)}100%,60%{-webkit-transform:rotate(-252deg);transform:rotate(-252deg)}}@-webkit-keyframes spin2-expand{0%{-webkit-transform:rotate(126deg);transform:rotate(126deg)}100%,60%{-webkit-transform:rotate(-234deg);transform:rotate(-234deg)}}@keyframes spin2-expand{0%{-webkit-transform:rotate(126deg);transform:rotate(126deg)}100%,60%{-webkit-transform:rotate(-234deg);transform:rotate(-234deg)}}@-webkit-keyframes spin3-expand{0%{-webkit-transform:rotate(144deg);transform:rotate(144deg)}100%,60%{-webkit-transform:rotate(-216deg);transform:rotate(-216deg)}}@keyframes spin3-expand{0%{-webkit-transform:rotate(144deg);transform:rotate(144deg)}100%,60%{-webkit-transform:rotate(-216deg);transform:rotate(-216deg)}}@-webkit-keyframes spin4-expand{0%{-webkit-transform:rotate(162deg);transform:rotate(162deg)}100%,60%{-webkit-transform:rotate(-198deg);transform:rotate(-198deg)}}@keyframes spin4-expand{0%{-webkit-transform:rotate(162deg);transform:rotate(162deg)}100%,60%{-webkit-transform:rotate(-198deg);transform:rotate(-198deg)}}@-webkit-keyframes spin5-expand{0%{-webkit-transform:rotate(180deg);transform:rotate(180deg)}100%,60%{-webkit-transform:rotate(-180deg);transform:rotate(-180deg)}}@keyframes spin5-expand{0%{-webkit-transform:rotate(180deg);transform:rotate(180deg)}100%,60%{-webkit-transform:rotate(-180deg);transform:rotate(-180deg)}}@-webkit-keyframes spin6-expand{0%{-webkit-transform:rotate(198deg);transform:rotate(198deg)}100%,60%{-webkit-transform:rotate(-162deg);transform:rotate(-162deg)}}@keyframes spin6-expand{0%{-webkit-transform:rotate(198deg);transform:rotate(198deg)}100%,60%{-webkit-transform:rotate(-162deg);transform:rotate(-162deg)}}@-webkit-keyframes spin7-expand{0%{-webkit-transform:rotate(216eg);transform:rotate(216eg)}100%,60%{-webkit-transform:rotate(-144deg);transform:rotate(-144deg)}}@keyframes spin7-expand{0%{-webkit-transform:rotate(216eg);transform:rotate(216eg)}100%,60%{-webkit-transform:rotate(-144deg);transform:rotate(-144deg)}}@-webkit-keyframes spin8-expand{0%{-webkit-transform:rotate(234deg);transform:rotate(234deg)}100%,60%{-webkit-transform:rotate(-126deg);transform:rotate(-126deg)}}@keyframes spin8-expand{0%{-webkit-transform:rotate(234deg);transform:rotate(234deg)}100%,60%{-webkit-transform:rotate(-126deg);transform:rotate(-126deg)}}@-webkit-keyframes spin9-expand{0%{-webkit-transform:rotate(252deg);transform:rotate(252deg)}100%,60%{-webkit-transform:rotate(-108deg);transform:rotate(-108deg)}}@keyframes spin9-expand{0%{-webkit-transform:rotate(252deg);transform:rotate(252deg)}100%,60%{-webkit-transform:rotate(-108deg);transform:rotate(-108deg)}}@-webkit-keyframes expand{0%{top:165px}50%{top:-10px}70%{top:15px}100%{top:10px}}@keyframes expand{0%{top:165px}50%{top:-10px}70%{top:15px}100%{top:10px}}@-webkit-keyframes contract{0%{top:10px}40%{top:-25px}100%{top:165px}}@keyframes contract{0%{top:10px}40%{top:-25px}100%{top:165px}}@-webkit-keyframes bounceIn{0%,100%{-webkit-transform:scale(1);transform:scale(1)}60%{-webkit-transform:scale(1.3);transform:scale(1.3)}}@keyframes bounceIn{0%,100%{-webkit-transform:scale(1);transform:scale(1)}60%{-webkit-transform:scale(1.3);transform:scale(1.3)}}@-webkit-keyframes bounceInDown{0%{opacity:0;-webkit-transform:translateY(-2000px);transform:translateY(-2000px)}60%{opacity:1;-webkit-transform:translateY(30px);transform:translateY(30px)}80%{-webkit-transform:translateY(-10px);transform:translateY(-10px)}100%{-webkit-transform:translateY(0);transform:translateY(0)}}@keyframes bounceInDown{0%{opacity:0;-webkit-transform:translateY(-2000px);transform:translateY(-2000px)}60%{opacity:1;-webkit-transform:translateY(30px);transform:translateY(30px)}80%{-webkit-transform:translateY(-10px);transform:translateY(-10px)}100%{-webkit-transform:translateY(0);transform:translateY(0)}}@-webkit-keyframes fadeIn{0%{opacity:0}100%{opacity:1}}@keyframes fadeIn{0%{opacity:0}100%{opacity:1}}@-webkit-keyframes fadeInDown{0%{opacity:0;-webkit-transform:translateY(-20px);transform:translateY(-20px)}100%{opacity:1;-webkit-transform:translateY(0);transform:translateY(0)}}@keyframes fadeInDown{0%{opacity:0;-webkit-transform:translateY(-20px);transform:translateY(-20px)}100%{opacity:1;-webkit-transform:translateY(0);transform:translateY(0)}}@-webkit-keyframes bounceInUp{0%{opacity:0;-webkit-transform:translateY(2000px);transform:translateY(2000px)}60%{opacity:1;-webkit-transform:translateY(-30px);transform:translateY(-30px)}80%{-webkit-transform:translateY(10px);transform:translateY(10px)}100%{-webkit-transform:translateY(0);transform:translateY(0)}}@keyframes bounceInUp{0%{opacity:0;-webkit-transform:translateY(2000px);transform:translateY(2000px)}60%{opacity:1;-webkit-transform:translateY(-30px);transform:translateY(-30px)}80%{-webkit-transform:translateY(10px);transform:translateY(10px)}100%{-webkit-transform:translateY(0);transform:translateY(0)}}@-webkit-keyframes bounceOut{0%{-webkit-transform:scale(1);transform:scale(1)}25%{-webkit-transform:scale(.95);transform:scale(.95)}50%{opacity:1;-webkit-transform:scale(1.1);transform:scale(1.1)}100%{opacity:0;-webkit-transform:scale(.3);transform:scale(.3)}}@keyframes bounceOut{0%{-webkit-transform:scale(1);transform:scale(1)}25%{-webkit-transform:scale(.95);transform:scale(.95)}50%{opacity:1;-webkit-transform:scale(1.1);transform:scale(1.1)}100%{opacity:0;-webkit-transform:scale(.3);transform:scale(.3)}}@-webkit-keyframes bounceOutDown{0%{-webkit-transform:translateY(0);transform:translateY(0)}20%{opacity:1;-webkit-transform:translateY(-20px);transform:translateY(-20px)}100%{opacity:0;-webkit-transform:translateY(2000px);transform:translateY(2000px)}}@keyframes bounceOutDown{0%{-webkit-transform:translateY(0);transform:translateY(0)}20%{opacity:1;-webkit-transform:translateY(-20px);transform:translateY(-20px)}100%{opacity:0;-webkit-transform:translateY(2000px);transform:translateY(2000px)}}@-webkit-keyframes fadeInUp{0%{opacity:0;-webkit-transform:translateY(20px);transform:translateY(20px)}100%{opacity:1;-webkit-transform:translateY(0);transform:translateY(0)}}@keyframes fadeInUp{0%{opacity:0;-webkit-transform:translateY(20px);transform:translateY(20px)}100%{opacity:1;-webkit-transform:translateY(0);transform:translateY(0)}}@-webkit-keyframes fadeInUpBig{0%{opacity:0;-webkit-transform:translateY(2000px);transform:translateY(2000px)}100%{opacity:1;-webkit-transform:translateY(0);transform:translateY(0)}}@keyframes fadeInUpBig{0%{opacity:0;-webkit-transform:translateY(2000px);transform:translateY(2000px)}100%{opacity:1;-webkit-transform:translateY(0);transform:translateY(0)}}@-webkit-keyframes fadeOut{0%{opacity:1}100%{opacity:0}}@keyframes fadeOut{0%{opacity:1}100%{opacity:0}}@-webkit-keyframes fadeOutDown{0%{opacity:1;-webkit-transform:translateY(0);transform:translateY(0)}100%{opacity:0;-webkit-transform:translateY(20px);transform:translateY(20px)}}@keyframes fadeOutDown{0%{opacity:1;-webkit-transform:translateY(0);transform:translateY(0)}100%{opacity:0;-webkit-transform:translateY(20px);transform:translateY(20px)}}@-webkit-keyframes fadeOutDownBig{0%{opacity:1;-webkit-transform:translateY(0);transform:translateY(0)}100%{opacity:0;-webkit-transform:translateY(2000px);transform:translateY(2000px)}}@keyframes fadeOutDownBig{0%{opacity:1;-webkit-transform:translateY(0);transform:translateY(0)}100%{opacity:0;-webkit-transform:translateY(2000px);transform:translateY(2000px)}}@-webkit-keyframes flipInX{0%{-webkit-transform:perspective(400px) rotateX(90deg);transform:perspective(400px) rotateX(90deg);opacity:0}40%{-webkit-transform:perspective(400px) rotateX(-10deg);transform:perspective(400px) rotateX(-10deg)}70%{-webkit-transform:perspective(400px) rotateX(10deg);transform:perspective(400px) rotateX(10deg)}100%{-webkit-transform:perspective(400px) rotateX(0);transform:perspective(400px) rotateX(0);opacity:1}}@keyframes flipInX{0%{-webkit-transform:perspective(400px) rotateX(90deg);transform:perspective(400px) rotateX(90deg);opacity:0}40%{-webkit-transform:perspective(400px) rotateX(-10deg);transform:perspective(400px) rotateX(-10deg)}70%{-webkit-transform:perspective(400px) rotateX(10deg);transform:perspective(400px) rotateX(10deg)}100%{-webkit-transform:perspective(400px) rotateX(0);transform:perspective(400px) rotateX(0);opacity:1}}@-webkit-keyframes flipInY{0%{-webkit-transform:perspective(400px) rotateY(90deg);transform:perspective(400px) rotateY(90deg);opacity:0}40%{-webkit-transform:perspective(400px) rotateY(-10deg);transform:perspective(400px) rotateY(-10deg)}70%{-webkit-transform:perspective(400px) rotateY(10deg);transform:perspective(400px) rotateY(10deg)}100%{-webkit-transform:perspective(400px) rotateY(0);transform:perspective(400px) rotateY(0);opacity:1}}@keyframes flipInY{0%{-webkit-transform:perspective(400px) rotateY(90deg);transform:perspective(400px) rotateY(90deg);opacity:0}40%{-webkit-transform:perspective(400px) rotateY(-10deg);transform:perspective(400px) rotateY(-10deg)}70%{-webkit-transform:perspective(400px) rotateY(10deg);transform:perspective(400px) rotateY(10deg)}100%{-webkit-transform:perspective(400px) rotateY(0);transform:perspective(400px) rotateY(0);opacity:1}}@-webkit-keyframes rubberBand{0%{-webkit-transform:scale(1);transform:scale(1);opacity:0}30%{-webkit-transform:scaleX(1.25) scaleY(.75);transform:scaleX(1.25) scaleY(.75)}40%{-webkit-transform:scaleX(.75) scaleY(1.25);transform:scaleX(.75) scaleY(1.25)}60%{-webkit-transform:scaleX(1.15) scaleY(.85);transform:scaleX(1.15) scaleY(.85)}100%{-webkit-transform:scale(1);transform:scale(1);opacity:1}}@keyframes rubberBand{0%{-webkit-transform:scale(1);transform:scale(1);opacity:0}30%{-webkit-transform:scaleX(1.25) scaleY(.75);transform:scaleX(1.25) scaleY(.75)}40%{-webkit-transform:scaleX(.75) scaleY(1.25);transform:scaleX(.75) scaleY(1.25)}60%{-webkit-transform:scaleX(1.15) scaleY(.85);transform:scaleX(1.15) scaleY(.85)}100%{-webkit-transform:scale(1);transform:scale(1);opacity:1}}@-webkit-keyframes tada{0%{-webkit-transform:scale(1);transform:scale(1)}10%,20%{-webkit-transform:scale(.9) rotate(-3deg);transform:scale(.9) rotate(-3deg)}30%,50%,70%,90%{-webkit-transform:scale(1.1) rotate(3deg);transform:scale(1.1) rotate(3deg)}40%,60%,80%{-webkit-transform:scale(1.1) rotate(-3deg);transform:scale(1.1) rotate(-3deg)}100%{-webkit-transform:scale(1) rotate(0);transform:scale(1) rotate(0)}}@keyframes tada{0%{-webkit-transform:scale(1);transform:scale(1)}10%,20%{-webkit-transform:scale(.9) rotate(-3deg);transform:scale(.9) rotate(-3deg)}30%,50%,70%,90%{-webkit-transform:scale(1.1) rotate(3deg);transform:scale(1.1) rotate(3deg)}40%,60%,80%{-webkit-transform:scale(1.1) rotate(-3deg);transform:scale(1.1) rotate(-3deg)}100%{-webkit-transform:scale(1) rotate(0);transform:scale(1) rotate(0)}}@-webkit-keyframes wobble{0%,100%{-webkit-transform:translateX(0);transform:translateX(0)}15%{-webkit-transform:translateX(-25%) rotate(-5deg);transform:translateX(-25%) rotate(-5deg)}30%{-webkit-transform:translateX(20%) rotate(3deg);transform:translateX(20%) rotate(3deg)}45%{-webkit-transform:translateX(-15%) rotate(-3deg);transform:translateX(-15%) rotate(-3deg)}60%{-webkit-transform:translateX(10%) rotate(2deg);transform:translateX(10%) rotate(2deg)}75%{-webkit-transform:translateX(-5%) rotate(-1deg);transform:translateX(-5%) rotate(-1deg)}}@keyframes wobble{0%,100%{-webkit-transform:translateX(0);transform:translateX(0)}15%{-webkit-transform:translateX(-25%) rotate(-5deg);transform:translateX(-25%) rotate(-5deg)}30%{-webkit-transform:translateX(20%) rotate(3deg);transform:translateX(20%) rotate(3deg)}45%{-webkit-transform:translateX(-15%) rotate(-3deg);transform:translateX(-15%) rotate(-3deg)}60%{-webkit-transform:translateX(10%) rotate(2deg);transform:translateX(10%) rotate(2deg)}75%{-webkit-transform:translateX(-5%) rotate(-1deg);transform:translateX(-5%) rotate(-1deg)}}@media only screen and (min-device-width:768px) and (max-device-width:1024px) and (orientation:portrait) and (-webkit-min-device-pixel-ratio:2){#reportsListView div{width:90%}#reportsListView .name,#reportsListView .title{width:initial;margin:0}}@media only screen and (min-device-width:768px) and (max-device-width:1024px) and (orientation:landscape) and (-webkit-min-device-pixel-ratio:2){#reportsListView div{width:90%}#reportsListView .name,#reportsListView .title{width:initial;margin:0}}@media only screen and (min-device-width:768px) and (max-device-width:1024px) and (orientation:portrait){#reportsListView div{width:90%}#reportsListView .name,#reportsListView .title{width:initial;margin:0}}@media only screen and (min-device-width:768px) and (max-device-width:1024px) and (orientation:landscape){#reportsListView div{width:90%}#reportsListView .name,#reportsListView .title{width:initial;margin:0}} -------------------------------------------------------------------------------- /dist/compiled/style.522e5d67.css: -------------------------------------------------------------------------------- 1 | .list-horizontal,.list-plain,ul{list-style:none}.list-horizontal,body,p,ul{margin:0;padding:0}#employeeLeftIcons a,.icon-utility-call,a{text-decoration:none}.reports,.reports li,body,p,ul{padding:0}*,:after,:before{box-sizing:border-box}body{font-family:SalesforceSans;background:#fff;color:#999;font-smoothing:antialiased;-webkit-tap-highlight-color:transparent;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.icon-utility-call,a{color:#293f54}.list-horizontal>li,.list-horizontal>li>a{display:inline-block;zoom:1}.shadow{box-shadow:0 2px 0 rgba(0,0,0,.1)}.hide{display:none!important}.show{display:block!important}.fadeIt{opacity:.01}.unfadeIt{opacity:1}.showFlex{display:-webkit-flex!important;display:flex!important}.capitalize{text-transform:capitalize}.overflowScrolling-touch{-webkit-overflow-scrolling:touch}.overflowScrolling-default{-webkit-overflow-scrolling:auto}.main-title,.name,.title{overflow:hidden;margin:0 auto}@font-face{font-family:SalesforceSans;src:url(https://www2.sfdcstatic.com/system/shared/common/assets/fonts/SalesforceSans/SalesforceSans-Regular.woff) format('woff');font-weight:300;font-style:normal}.employee-image,.report-image{height:100px;width:100px;background-size:100px;position:relative;z-index:3;margin:0 auto;border:4px solid #fff;border-radius:50%;display:inline-block;background-position:center;background-color:#FFF;background-repeat:no-repeat}.report-image{border-width:1px}.employee-image{border:2px solid #8D3EEA}#swipePad{height:100px;width:100%;margin:-26px auto 2px;z-index:10}.report-image{height:30px;width:30px;background-size:30px;margin:0}#employeeDetail{text-align:center;position:fixed;bottom:0;left:0;width:100%;z-index:5;height:200px}#employeeStrip{width:100%;height:46px;margin:0;background:#f5f5f5;border-width:1px 0;position:absolute;font-size:18pt;top:0}#employeeStrip a,#employeeStrip i{width:44px;height:44px;color:#293f54;display:inline-block;line-height:44px}#employeeEmail,.name,.reports>li a,.title{display:block}#employeeLeftIcons,#employeeRightIcons{margin:0 10px}#employeeLeftIcons{float:left}#employeeRightIcons{float:right}#peersIcons{width:185px;margin:0 auto}#peersIcons i{color:#ccc;font-size:14pt}.name,.title{color:#2A94D6;z-index:999;white-space:nowrap;width:200px;text-overflow:ellipsis}.title{color:#999;font-size:.8em}.main-title{max-height:40px;width:180px;height:40px;font-size:.8em}.reports{background:#fff;list-style-type:none;position:relative;margin:0;top:-270px}.reports li,.reports>li a{position:absolute;width:30px}.reports li{margin:0 0 0 -15px;border:0;height:170px;-webkit-transform-origin:15px bottom;transform-origin:15px bottom}.reports li span{transition:-webkit-transform .4s ease;transition:transform .4s ease;display:block;margin:0}.reports>li a{height:30px;overflow:visible;text-align:center;border-radius:50%}.reports>li:nth-of-type(1){-webkit-transform:rotate(-108deg);transform:rotate(-108deg)}.reports>li:nth-of-type(2){-webkit-transform:rotate(-126deg);transform:rotate(-126deg)}.reports>li:nth-of-type(3){-webkit-transform:rotate(-144deg);transform:rotate(-144deg)}.reports>li:nth-of-type(4){-webkit-transform:rotate(-162deg);transform:rotate(-162deg)}.reports>li:nth-of-type(5){-webkit-transform:rotate(-180deg);transform:rotate(-180deg)}.reports>li:nth-of-type(6){-webkit-transform:rotate(-198deg);transform:rotate(-198deg)}.reports>li:nth-of-type(7){-webkit-transform:rotate(-216deg);transform:rotate(-216deg)}.reports>li:nth-of-type(8){-webkit-transform:rotate(-234deg);transform:rotate(-234deg)}.reports>li:nth-of-type(9){-webkit-transform:rotate(-252deg);transform:rotate(-252deg)}.reports.expand li:nth-of-type(9) a ::before{content:'+';margin-right:2px}.reports.expand li:nth-of-type(1) a span{-webkit-transform:rotate(108deg);transform:rotate(108deg)}.reports.expand li:nth-of-type(2) a span{-webkit-transform:rotate(126deg);transform:rotate(126deg);-webkit-animation:spin2-expand .6s ease-out 1 backwards;animation:spin2-expand .6s ease-out 1 backwards}.reports.expand li:nth-of-type(3) a span{-webkit-transform:rotate(144deg);transform:rotate(144deg);-webkit-animation:spin3-expand .6s ease-out 1 backwards;animation:spin3-expand .6s ease-out 1 backwards}.reports.expand li:nth-of-type(4) a span{-webkit-transform:rotate(162deg);transform:rotate(162deg);-webkit-animation:spin4-expand .6s ease-out 1 backwards;animation:spin4-expand .6s ease-out 1 backwards}.reports.expand li:nth-of-type(5) a span{-webkit-transform:rotate(180deg);transform:rotate(180deg);-webkit-animation:spin5-expand .6s ease-out 1 backwards;animation:spin5-expand .6s ease-out 1 backwards}.reports.expand li:nth-of-type(6) a span{-webkit-transform:rotate(198deg);transform:rotate(198deg);-webkit-animation:spin6-expand .6s ease-out 1 backwards;animation:spin6-expand .6s ease-out 1 backwards}.reports.expand li:nth-of-type(7) a span{-webkit-transform:rotate(216deg);transform:rotate(216deg);-webkit-animation:spin7-expand .6s ease-out 1 backwards;animation:spin7-expand .6s ease-out 1 backwards}.reports.expand li:nth-of-type(8) a span{-webkit-transform:rotate(234deg);transform:rotate(234deg);-webkit-animation:spin8-expand .6s ease-out 1 backwards;animation:spin8-expand .6s ease-out 1 backwards}.reports.expand li:nth-of-type(9) a span{-webkit-transform:rotate(252deg);transform:rotate(252deg);-webkit-animation:spin9-expand .6s ease-out 1 backwards;animation:spin9-expand .6s ease-out 1 backwards}.reports li a{top:165px;-webkit-animation:contract .35s ease-out 1 backwards;animation:contract .35s ease-out 1 backwards}.reports.expand li a{top:10px;-webkit-animation:expand .6s ease 1 backwards;animation:expand .6s ease 1 backwards}.reports li:nth-of-type(1) a{-webkit-animation-delay:.32s;animation-delay:.32s}.reports li:nth-of-type(2) a{-webkit-animation-delay:.28s;animation-delay:.28s}.reports li:nth-of-type(3) a{-webkit-animation-delay:.24s;animation-delay:.24s}.reports li:nth-of-type(4) a{-webkit-animation-delay:.2s;animation-delay:.2s}.reports li:nth-of-type(5) a{-webkit-animation-delay:.16s;animation-delay:.16s}.reports li:nth-of-type(6) a{-webkit-animation-delay:.12s;animation-delay:.12s}.reports li:nth-of-type(7) a{-webkit-animation-delay:.08s;animation-delay:.08s}.reports li:nth-of-type(8) a{-webkit-animation-delay:.04s;animation-delay:.04s}.reports li:nth-of-type(9) a,.reports.expand li:nth-of-type(1) a{-webkit-animation-delay:0s;animation-delay:0s}.reports.expand li:nth-of-type(2) a{-webkit-animation-delay:.04s;animation-delay:.04s}.reports.expand li:nth-of-type(3) a{-webkit-animation-delay:.08s;animation-delay:.08s}.reports.expand li:nth-of-type(4) a{-webkit-animation-delay:.12s;animation-delay:.12s}.reports.expand li:nth-of-type(5) a{-webkit-animation-delay:.16s;animation-delay:.16s}.reports.expand li:nth-of-type(6) a{-webkit-animation-delay:.2s;animation-delay:.2s}.reports.expand li:nth-of-type(7) a{-webkit-animation-delay:.24s;animation-delay:.24s}.reports.expand li:nth-of-type(8) a{-webkit-animation-delay:.28s;animation-delay:.28s}.reports.expand li:nth-of-type(9) a{line-height:22px;text-align:center;background:rgba(42,148,214,.5);border:4px solid rgba(255,255,255,.4);color:#fff;font-size:.7em;-webkit-animation-delay:.32s;animation-delay:.32s}#reportsListView{width:100%;height:100%;z-index:5;top:150px;left:0;position:fixed;overflow-y:scroll;-webkit-overflow-scrolling:touch}.tog,.toggle{position:absolute;left:10px;text-align:left}#reportsListView ul{margin-bottom:170px}#reportsListView li{text-align:left;cursor:pointer;clear:both;border-top:1px solid #dadee2;height:45px;padding:0 10px}#reportsListView li :first-child{border-top:none}#reportsListView li i{float:left;margin:12px 12px 8px 2px}#reportsListView li div{float:left;margin:10px 10px 10px 5px}#reportsListView .name,#reportsListView .title{width:240px}.tog{bottom:10px}.toggle{top:70px}.employeeFocusListView{top:40px!important;position:fixed!important;width:100%!important}#managerDetail{background-color:#FFF;text-align:center;width:100%;overflow-y:scroll;height:325px;height:calc(100% - 180px);position:absolute;-webkit-transform:translate3D(0,0,-1px) rotateX(180deg);transform:translate3D(0,0,-1px) rotateX(180deg)}.manager-image{height:60px;width:60px;background-size:60px;border:4px solid #fff;border-radius:50%;display:inline-block;background-position:center;background-repeat:no-repeat}.reporting-structure{-webkit-transform:translate3D(0,0,-1px) rotateX(180deg);transform:translate3D(0,0,-1px) rotateX(180deg);position:absolute;width:100%;padding-bottom:50px}.reporting-structure li{margin:30px auto;position:relative}.reporting-structure li::before,.reporting-structure li:nth-last-child(1)::after{-webkit-animation:fadeIn 5s 0 1 ease normal;animation:fadeIn 5s 0 1 ease normal;content:'';position:absolute;top:-62px;left:50%;width:2px;height:70px;background:#eee;z-index:-1}#searchResultWrapper,#searchResultWrapper_row{width:100%;height:100%}.reporting-structure li:nth-last-child(1)::after{top:62px}.reporting-structure li:first-child::before{display:none}.reporting-structure li:nth-last-child(1) i,.reporting-structure li:nth-last-child(1) p{-webkit-animation-delay:0s;animation-delay:0s}.reporting-structure li:nth-last-child(2) i,.reporting-structure li:nth-last-child(2) p{-webkit-animation-delay:.04s;animation-delay:.04s}.reporting-structure li:nth-last-child(3) i,.reporting-structure li:nth-last-child(3) p{-webkit-animation-delay:.08s;animation-delay:.08s}.reporting-structure li:nth-last-child(4) i,.reporting-structure li:nth-last-child(4) p{-webkit-animation-delay:.12s;animation-delay:.12s}.reporting-structure li:nth-last-child(5) i,.reporting-structure li:nth-last-child(5) p{-webkit-animation-delay:.16s;animation-delay:.16s}.reporting-structure li:nth-last-child(6) i,.reporting-structure li:nth-last-child(6) p{-webkit-animation-delay:.2s;animation-delay:.2s}.reporting-structure li:nth-last-child(7) i,.reporting-structure li:nth-last-child(7) p{-webkit-animation-delay:.24s;animation-delay:.24s}.reporting-structure li:nth-last-child(8) i,.reporting-structure li:nth-last-child(8) p{-webkit-animation-delay:.28s;animation-delay:.28s}.reporting-structure li:nth-last-child(9) i,.reporting-structure li:nth-last-child(9) p{-webkit-animation-delay:.32s;animation-delay:.32s}.reporting-structure li:nth-last-child(10) i,.reporting-structure li:nth-last-child(10) p{-webkit-animation-delay:.36s;animation-delay:.36s}.reporting-structure li:nth-last-child(11) i,.reporting-structure li:nth-last-child(11) p{-webkit-animation-delay:.4s;animation-delay:.4s}.reporting-structure li:nth-last-child(12) i,.reporting-structure li:nth-last-child(12) p{-webkit-animation-delay:.44s;animation-delay:.44s}.reporting-structure li:nth-last-child(13) i,.reporting-structure li:nth-last-child(13) p{-webkit-animation-delay:.48s;animation-delay:.48s}.reporting-structure li:nth-last-child(14) i,.reporting-structure li:nth-last-child(14) p{-webkit-animation-delay:.52s;animation-delay:.52s}.reporting-structure li:nth-last-child(15) i,.reporting-structure li:nth-last-child(15) p{-webkit-animation-delay:.56s;animation-delay:.56s}.reporting-structure li:nth-last-child(16) i,.reporting-structure li:nth-last-child(16) p{-webkit-animation-delay:.6s;animation-delay:.6s}.reporting-structure li:nth-last-child(17) i,.reporting-structure li:nth-last-child(17) p{-webkit-animation-delay:.64s;animation-delay:.64s}.reporting-structure li:nth-last-child(18) i,.reporting-structure li:nth-last-child(18) p{-webkit-animation-delay:.68s;animation-delay:.68s}.reporting-structure li:nth-last-child(19) i,.reporting-structure li:nth-last-child(19) p{-webkit-animation-delay:.72s;animation-delay:.72s}.reporting-structure li:nth-last-child(20) i,.reporting-structure li:nth-last-child(20) p{-webkit-animation-delay:.76s;animation-delay:.76s}#searchResultWrapper{z-index:14;position:absolute;top:48px;left:0;display:-webkit-flex;-webkit-align-items:center;align-items:center;-webkit-justify-content:center;justify-content:center}#searchResultWrapper ul{height:100%;padding:5px 0;background-color:#fff}#searchResultWrapper ul li{cursor:pointer;padding:0;margin:10px}#searchBoxWrapper{position:absolute;top:5px;left:50%;z-index:15;width:86%;margin-left:-47%}#searchBox{width:100%;height:36px;padding:6px 10px;border-radius:5px;border:1px solid #85929F;font-size:16px}#searchToggle{padding:0;position:fixed;right:0;top:0;z-index:999;height:44px;width:44px;background:0;border:0;outline:0}.icon-utility-search{color:#999}.icon-utility-close{color:#fff}.icon-utility-close,.icon-utility-search{font-size:15px}#closeSelector{margin:0;padding:0;z-index:15;position:absolute;top:-44px;right:-4px}#expiredTokenOverlay,#phoneSelector{z-index:15;position:absolute;height:100%;width:100%;padding:0;margin:0;display:-webkit-flex;display:flex;-webkit-align-items:center;align-items:center;-webkit-justify-content:center;justify-content:center}#expiredTokenMessage{width:100%;padding:10px;text-align:center}#selectorRow{width:260px;border-radius:5px;background-color:#fff;padding:14px;position:relative}.selection li{border:1px solid #dfe0e1;color:#5c7995;font-size:16px;font-weight:700;width:100%;padding:7px 0;margin:0 auto 14px;text-align:center;cursor:pointer;border-radius:5px}.selection li:last-child{margin:0!important}#overlay{opacity:.5;background:#344A5F;width:100%;height:100%;z-index:10;top:0;left:0;position:fixed}.animated{-webkit-animation-duration:1s;animation-duration:1s;-webkit-animation-fill-mode:both;animation-fill-mode:both}.bounceIn{-webkit-animation:bounceIn .35s;animation:bounceIn .35s;opacity:1}.fadeIn{-webkit-animation-name:fadeIn;animation-name:fadeIn}.bounceInUp{-webkit-animation-name:bounceInUp;animation-name:bounceInUp}@-webkit-keyframes spin1-expand{0%{-webkit-transform:rotate(108deg);transform:rotate(108deg)}100%,60%{-webkit-transform:rotate(-252deg);transform:rotate(-252deg)}}@keyframes spin1-expand{0%{-webkit-transform:rotate(108deg);transform:rotate(108deg)}100%,60%{-webkit-transform:rotate(-252deg);transform:rotate(-252deg)}}@-webkit-keyframes spin2-expand{0%{-webkit-transform:rotate(126deg);transform:rotate(126deg)}100%,60%{-webkit-transform:rotate(-234deg);transform:rotate(-234deg)}}@keyframes spin2-expand{0%{-webkit-transform:rotate(126deg);transform:rotate(126deg)}100%,60%{-webkit-transform:rotate(-234deg);transform:rotate(-234deg)}}@-webkit-keyframes spin3-expand{0%{-webkit-transform:rotate(144deg);transform:rotate(144deg)}100%,60%{-webkit-transform:rotate(-216deg);transform:rotate(-216deg)}}@keyframes spin3-expand{0%{-webkit-transform:rotate(144deg);transform:rotate(144deg)}100%,60%{-webkit-transform:rotate(-216deg);transform:rotate(-216deg)}}@-webkit-keyframes spin4-expand{0%{-webkit-transform:rotate(162deg);transform:rotate(162deg)}100%,60%{-webkit-transform:rotate(-198deg);transform:rotate(-198deg)}}@keyframes spin4-expand{0%{-webkit-transform:rotate(162deg);transform:rotate(162deg)}100%,60%{-webkit-transform:rotate(-198deg);transform:rotate(-198deg)}}@-webkit-keyframes spin5-expand{0%{-webkit-transform:rotate(180deg);transform:rotate(180deg)}100%,60%{-webkit-transform:rotate(-180deg);transform:rotate(-180deg)}}@keyframes spin5-expand{0%{-webkit-transform:rotate(180deg);transform:rotate(180deg)}100%,60%{-webkit-transform:rotate(-180deg);transform:rotate(-180deg)}}@-webkit-keyframes spin6-expand{0%{-webkit-transform:rotate(198deg);transform:rotate(198deg)}100%,60%{-webkit-transform:rotate(-162deg);transform:rotate(-162deg)}}@keyframes spin6-expand{0%{-webkit-transform:rotate(198deg);transform:rotate(198deg)}100%,60%{-webkit-transform:rotate(-162deg);transform:rotate(-162deg)}}@-webkit-keyframes spin7-expand{0%{-webkit-transform:rotate(216eg);transform:rotate(216eg)}100%,60%{-webkit-transform:rotate(-144deg);transform:rotate(-144deg)}}@keyframes spin7-expand{0%{-webkit-transform:rotate(216eg);transform:rotate(216eg)}100%,60%{-webkit-transform:rotate(-144deg);transform:rotate(-144deg)}}@-webkit-keyframes spin8-expand{0%{-webkit-transform:rotate(234deg);transform:rotate(234deg)}100%,60%{-webkit-transform:rotate(-126deg);transform:rotate(-126deg)}}@keyframes spin8-expand{0%{-webkit-transform:rotate(234deg);transform:rotate(234deg)}100%,60%{-webkit-transform:rotate(-126deg);transform:rotate(-126deg)}}@-webkit-keyframes spin9-expand{0%{-webkit-transform:rotate(252deg);transform:rotate(252deg)}100%,60%{-webkit-transform:rotate(-108deg);transform:rotate(-108deg)}}@keyframes spin9-expand{0%{-webkit-transform:rotate(252deg);transform:rotate(252deg)}100%,60%{-webkit-transform:rotate(-108deg);transform:rotate(-108deg)}}@-webkit-keyframes expand{0%{top:165px}50%{top:-10px}70%{top:15px}100%{top:10px}}@keyframes expand{0%{top:165px}50%{top:-10px}70%{top:15px}100%{top:10px}}@-webkit-keyframes contract{0%{top:10px}40%{top:-25px}100%{top:165px}}@keyframes contract{0%{top:10px}40%{top:-25px}100%{top:165px}}@-webkit-keyframes bounceIn{0%,100%{-webkit-transform:scale(1);transform:scale(1)}60%{-webkit-transform:scale(1.3);transform:scale(1.3)}}@keyframes bounceIn{0%,100%{-webkit-transform:scale(1);transform:scale(1)}60%{-webkit-transform:scale(1.3);transform:scale(1.3)}}@-webkit-keyframes bounceInDown{0%{opacity:0;-webkit-transform:translateY(-2000px);transform:translateY(-2000px)}60%{opacity:1;-webkit-transform:translateY(30px);transform:translateY(30px)}80%{-webkit-transform:translateY(-10px);transform:translateY(-10px)}100%{-webkit-transform:translateY(0);transform:translateY(0)}}@keyframes bounceInDown{0%{opacity:0;-webkit-transform:translateY(-2000px);transform:translateY(-2000px)}60%{opacity:1;-webkit-transform:translateY(30px);transform:translateY(30px)}80%{-webkit-transform:translateY(-10px);transform:translateY(-10px)}100%{-webkit-transform:translateY(0);transform:translateY(0)}}@-webkit-keyframes fadeIn{0%{opacity:0}100%{opacity:1}}@keyframes fadeIn{0%{opacity:0}100%{opacity:1}}@-webkit-keyframes fadeInDown{0%{opacity:0;-webkit-transform:translateY(-20px);transform:translateY(-20px)}100%{opacity:1;-webkit-transform:translateY(0);transform:translateY(0)}}@keyframes fadeInDown{0%{opacity:0;-webkit-transform:translateY(-20px);transform:translateY(-20px)}100%{opacity:1;-webkit-transform:translateY(0);transform:translateY(0)}}@-webkit-keyframes bounceInUp{0%{opacity:0;-webkit-transform:translateY(2000px);transform:translateY(2000px)}60%{opacity:1;-webkit-transform:translateY(-30px);transform:translateY(-30px)}80%{-webkit-transform:translateY(10px);transform:translateY(10px)}100%{-webkit-transform:translateY(0);transform:translateY(0)}}@keyframes bounceInUp{0%{opacity:0;-webkit-transform:translateY(2000px);transform:translateY(2000px)}60%{opacity:1;-webkit-transform:translateY(-30px);transform:translateY(-30px)}80%{-webkit-transform:translateY(10px);transform:translateY(10px)}100%{-webkit-transform:translateY(0);transform:translateY(0)}}@-webkit-keyframes bounceOut{0%{-webkit-transform:scale(1);transform:scale(1)}25%{-webkit-transform:scale(.95);transform:scale(.95)}50%{opacity:1;-webkit-transform:scale(1.1);transform:scale(1.1)}100%{opacity:0;-webkit-transform:scale(.3);transform:scale(.3)}}@keyframes bounceOut{0%{-webkit-transform:scale(1);transform:scale(1)}25%{-webkit-transform:scale(.95);transform:scale(.95)}50%{opacity:1;-webkit-transform:scale(1.1);transform:scale(1.1)}100%{opacity:0;-webkit-transform:scale(.3);transform:scale(.3)}}@-webkit-keyframes bounceOutDown{0%{-webkit-transform:translateY(0);transform:translateY(0)}20%{opacity:1;-webkit-transform:translateY(-20px);transform:translateY(-20px)}100%{opacity:0;-webkit-transform:translateY(2000px);transform:translateY(2000px)}}@keyframes bounceOutDown{0%{-webkit-transform:translateY(0);transform:translateY(0)}20%{opacity:1;-webkit-transform:translateY(-20px);transform:translateY(-20px)}100%{opacity:0;-webkit-transform:translateY(2000px);transform:translateY(2000px)}}@-webkit-keyframes fadeInUp{0%{opacity:0;-webkit-transform:translateY(20px);transform:translateY(20px)}100%{opacity:1;-webkit-transform:translateY(0);transform:translateY(0)}}@keyframes fadeInUp{0%{opacity:0;-webkit-transform:translateY(20px);transform:translateY(20px)}100%{opacity:1;-webkit-transform:translateY(0);transform:translateY(0)}}@-webkit-keyframes fadeInUpBig{0%{opacity:0;-webkit-transform:translateY(2000px);transform:translateY(2000px)}100%{opacity:1;-webkit-transform:translateY(0);transform:translateY(0)}}@keyframes fadeInUpBig{0%{opacity:0;-webkit-transform:translateY(2000px);transform:translateY(2000px)}100%{opacity:1;-webkit-transform:translateY(0);transform:translateY(0)}}@-webkit-keyframes fadeOut{0%{opacity:1}100%{opacity:0}}@keyframes fadeOut{0%{opacity:1}100%{opacity:0}}@-webkit-keyframes fadeOutDown{0%{opacity:1;-webkit-transform:translateY(0);transform:translateY(0)}100%{opacity:0;-webkit-transform:translateY(20px);transform:translateY(20px)}}@keyframes fadeOutDown{0%{opacity:1;-webkit-transform:translateY(0);transform:translateY(0)}100%{opacity:0;-webkit-transform:translateY(20px);transform:translateY(20px)}}@-webkit-keyframes fadeOutDownBig{0%{opacity:1;-webkit-transform:translateY(0);transform:translateY(0)}100%{opacity:0;-webkit-transform:translateY(2000px);transform:translateY(2000px)}}@keyframes fadeOutDownBig{0%{opacity:1;-webkit-transform:translateY(0);transform:translateY(0)}100%{opacity:0;-webkit-transform:translateY(2000px);transform:translateY(2000px)}}@-webkit-keyframes flipInX{0%{-webkit-transform:perspective(400px) rotateX(90deg);transform:perspective(400px) rotateX(90deg);opacity:0}40%{-webkit-transform:perspective(400px) rotateX(-10deg);transform:perspective(400px) rotateX(-10deg)}70%{-webkit-transform:perspective(400px) rotateX(10deg);transform:perspective(400px) rotateX(10deg)}100%{-webkit-transform:perspective(400px) rotateX(0);transform:perspective(400px) rotateX(0);opacity:1}}@keyframes flipInX{0%{-webkit-transform:perspective(400px) rotateX(90deg);transform:perspective(400px) rotateX(90deg);opacity:0}40%{-webkit-transform:perspective(400px) rotateX(-10deg);transform:perspective(400px) rotateX(-10deg)}70%{-webkit-transform:perspective(400px) rotateX(10deg);transform:perspective(400px) rotateX(10deg)}100%{-webkit-transform:perspective(400px) rotateX(0);transform:perspective(400px) rotateX(0);opacity:1}}@-webkit-keyframes flipInY{0%{-webkit-transform:perspective(400px) rotateY(90deg);transform:perspective(400px) rotateY(90deg);opacity:0}40%{-webkit-transform:perspective(400px) rotateY(-10deg);transform:perspective(400px) rotateY(-10deg)}70%{-webkit-transform:perspective(400px) rotateY(10deg);transform:perspective(400px) rotateY(10deg)}100%{-webkit-transform:perspective(400px) rotateY(0);transform:perspective(400px) rotateY(0);opacity:1}}@keyframes flipInY{0%{-webkit-transform:perspective(400px) rotateY(90deg);transform:perspective(400px) rotateY(90deg);opacity:0}40%{-webkit-transform:perspective(400px) rotateY(-10deg);transform:perspective(400px) rotateY(-10deg)}70%{-webkit-transform:perspective(400px) rotateY(10deg);transform:perspective(400px) rotateY(10deg)}100%{-webkit-transform:perspective(400px) rotateY(0);transform:perspective(400px) rotateY(0);opacity:1}}@-webkit-keyframes rubberBand{0%{-webkit-transform:scale(1);transform:scale(1);opacity:0}30%{-webkit-transform:scaleX(1.25) scaleY(.75);transform:scaleX(1.25) scaleY(.75)}40%{-webkit-transform:scaleX(.75) scaleY(1.25);transform:scaleX(.75) scaleY(1.25)}60%{-webkit-transform:scaleX(1.15) scaleY(.85);transform:scaleX(1.15) scaleY(.85)}100%{-webkit-transform:scale(1);transform:scale(1);opacity:1}}@keyframes rubberBand{0%{-webkit-transform:scale(1);transform:scale(1);opacity:0}30%{-webkit-transform:scaleX(1.25) scaleY(.75);transform:scaleX(1.25) scaleY(.75)}40%{-webkit-transform:scaleX(.75) scaleY(1.25);transform:scaleX(.75) scaleY(1.25)}60%{-webkit-transform:scaleX(1.15) scaleY(.85);transform:scaleX(1.15) scaleY(.85)}100%{-webkit-transform:scale(1);transform:scale(1);opacity:1}}@-webkit-keyframes tada{0%{-webkit-transform:scale(1);transform:scale(1)}10%,20%{-webkit-transform:scale(.9) rotate(-3deg);transform:scale(.9) rotate(-3deg)}30%,50%,70%,90%{-webkit-transform:scale(1.1) rotate(3deg);transform:scale(1.1) rotate(3deg)}40%,60%,80%{-webkit-transform:scale(1.1) rotate(-3deg);transform:scale(1.1) rotate(-3deg)}100%{-webkit-transform:scale(1) rotate(0);transform:scale(1) rotate(0)}}@keyframes tada{0%{-webkit-transform:scale(1);transform:scale(1)}10%,20%{-webkit-transform:scale(.9) rotate(-3deg);transform:scale(.9) rotate(-3deg)}30%,50%,70%,90%{-webkit-transform:scale(1.1) rotate(3deg);transform:scale(1.1) rotate(3deg)}40%,60%,80%{-webkit-transform:scale(1.1) rotate(-3deg);transform:scale(1.1) rotate(-3deg)}100%{-webkit-transform:scale(1) rotate(0);transform:scale(1) rotate(0)}}@-webkit-keyframes wobble{0%,100%{-webkit-transform:translateX(0);transform:translateX(0)}15%{-webkit-transform:translateX(-25%) rotate(-5deg);transform:translateX(-25%) rotate(-5deg)}30%{-webkit-transform:translateX(20%) rotate(3deg);transform:translateX(20%) rotate(3deg)}45%{-webkit-transform:translateX(-15%) rotate(-3deg);transform:translateX(-15%) rotate(-3deg)}60%{-webkit-transform:translateX(10%) rotate(2deg);transform:translateX(10%) rotate(2deg)}75%{-webkit-transform:translateX(-5%) rotate(-1deg);transform:translateX(-5%) rotate(-1deg)}}@keyframes wobble{0%,100%{-webkit-transform:translateX(0);transform:translateX(0)}15%{-webkit-transform:translateX(-25%) rotate(-5deg);transform:translateX(-25%) rotate(-5deg)}30%{-webkit-transform:translateX(20%) rotate(3deg);transform:translateX(20%) rotate(3deg)}45%{-webkit-transform:translateX(-15%) rotate(-3deg);transform:translateX(-15%) rotate(-3deg)}60%{-webkit-transform:translateX(10%) rotate(2deg);transform:translateX(10%) rotate(2deg)}75%{-webkit-transform:translateX(-5%) rotate(-1deg);transform:translateX(-5%) rotate(-1deg)}}@media only screen and (min-device-width:768px) and (max-device-width:1024px) and (orientation:portrait) and (-webkit-min-device-pixel-ratio:2){#reportsListView div{width:90%}#reportsListView .name,#reportsListView .title{width:initial;margin:0}}@media only screen and (min-device-width:768px) and (max-device-width:1024px) and (orientation:landscape) and (-webkit-min-device-pixel-ratio:2){#reportsListView div{width:90%}#reportsListView .name,#reportsListView .title{width:initial;margin:0}}@media only screen and (min-device-width:768px) and (max-device-width:1024px) and (orientation:portrait){#reportsListView div{width:90%}#reportsListView .name,#reportsListView .title{width:initial;margin:0}}@media only screen and (min-device-width:768px) and (max-device-width:1024px) and (orientation:landscape){#reportsListView div{width:90%}#reportsListView .name,#reportsListView .title{width:initial;margin:0}} --------------------------------------------------------------------------------