├── .idea ├── .name ├── Asterisk-nodejs-panel.iml ├── encodings.xml ├── libraries │ └── sass_stdlib.xml ├── misc.xml ├── modules.xml ├── scopes │ └── scope_settings.xml ├── vcs.xml └── workspace.xml ├── helpers ├── array.js ├── executor.js ├── math.js └── time.js ├── index.html ├── libraries └── mysql.js ├── models ├── agent.js ├── client.js └── queue.js ├── modules ├── app.js ├── browser_ban.js ├── pbx_bugs_solver.js ├── refetcher.js └── reseter.js ├── package.json ├── public ├── images │ ├── arrow_posts_left.png │ ├── arrow_posts_right.png │ ├── asc.gif │ ├── asc_light.gif │ ├── bg_time_head.gif │ ├── desc.gif │ ├── desc_light.gif │ ├── dots_devider_v.gif │ ├── glyphicons-halflings-white.png │ ├── glyphicons-halflings.png │ ├── misc │ │ ├── Thumbs.db │ │ ├── accenture-logo.png │ │ ├── button-gloss.png │ │ ├── button-overlay.png │ │ ├── carbon_fibre_v2.png │ │ ├── custom-form-sprites.png │ │ ├── input-bg.png │ │ ├── modal-gloss.png │ │ └── table-sorter.png │ ├── orbit │ │ ├── bullets.jpg │ │ ├── left-arrow.png │ │ ├── loading.gif │ │ ├── mask-black.png │ │ ├── pause-black.png │ │ ├── right-arrow.png │ │ ├── rotator-black.png │ │ └── timer-black.png │ ├── trashcan.png │ └── widget_grad.png ├── javascripts │ ├── client_stat.js │ ├── foundation.js │ ├── handlebars.js │ ├── highcharts.js │ ├── jquery-1.8.0.min.js │ ├── jquery-1.8.1.min.js │ ├── jquery-ui-1.8.17.custom.min.js │ ├── jquery-ui-1.8.23.custom.min.js │ ├── jquery.stopwatch.js │ ├── jquery.tipTip.minified.js │ └── script.js └── stylesheets │ ├── PIE.htc │ ├── app.css │ ├── foundation.css │ ├── ie.css │ ├── images │ ├── ui-bg_flat_30_cccccc_40x100.png │ ├── ui-bg_flat_50_5c5c5c_40x100.png │ ├── ui-bg_glass_20_555555_1x400.png │ ├── ui-bg_glass_40_0078a3_1x400.png │ ├── ui-bg_glass_40_ffc73d_1x400.png │ ├── ui-bg_gloss-wave_25_333333_500x100.png │ ├── ui-bg_highlight-soft_80_eeeeee_1x100.png │ ├── ui-bg_inset-soft_25_000000_1x100.png │ ├── ui-bg_inset-soft_30_f58400_1x100.png │ ├── ui-icons_222222_256x240.png │ ├── ui-icons_4b8e0b_256x240.png │ ├── ui-icons_a83300_256x240.png │ ├── ui-icons_cccccc_256x240.png │ └── ui-icons_ffffff_256x240.png │ ├── jquery-ui-1.8.23.custom.css │ ├── normalize.css │ ├── style.css │ └── tipTip.css ├── readme.md ├── routes └── index.js ├── server.js └── views ├── index.html ├── index.jade └── layout.jade /.idea/.name: -------------------------------------------------------------------------------- 1 | Asterisk-nodejs-panel -------------------------------------------------------------------------------- /.idea/Asterisk-nodejs-panel.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /.idea/encodings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/libraries/sass_stdlib.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | 347 | jar:file:\C:\Program Files\JetBrains\PhpStorm 5.0.1\lib\webide.jar!\resources\html5-schema\html5.rnc 348 | 349 | 350 | 351 | 352 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /.idea/scopes/scope_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/workspace.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 13 | 14 | 15 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 60 | 61 | 70 | 71 | 72 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 110 | 111 | 112 | 113 | 116 | 117 | 120 | 121 | 122 | 123 | 126 | 127 | 130 | 131 | 134 | 135 | 136 | 137 | 140 | 141 | 144 | 145 | 148 | 149 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 1359033107654 179 | 1359033107654 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 212 | 213 | 224 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | -------------------------------------------------------------------------------- /helpers/array.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This function will delete an element from the array 3 | * 4 | * @param myArray haystack 5 | * @param element needle 6 | */ 7 | exports.deleteFromArray = function (myArray, element) { 8 | var position = myArray.indexOf(element); 9 | myArray.splice(position, 1); 10 | }; 11 | /** 12 | * This function will delete from Array of Objects given the property, the needle and... the haystack. You guessed! 13 | * 14 | * @param myArray haystack 15 | * @param searchTerm needle 16 | * @param property property to match 17 | */ 18 | exports.deleteFromArrayOfObjects = function (myArray, searchTerm, property) { 19 | var position = this.arrayObjectIndexOf(myArray, searchTerm, property); 20 | if (position !== -1) { 21 | myArray.splice(position, 1); 22 | } 23 | }; 24 | /** 25 | * This functions looks for an Object which have the property === searchterm 26 | * 27 | * @param myArray haystack 28 | * @param searchTerm needle 29 | * @param property property to match 30 | * @return {Number} position or -1 in case it's not found 31 | */ 32 | exports.arrayObjectIndexOf = function(myArray, searchTerm, property) { 33 | for(var i = 0, len = myArray.length; i < len; i++) { 34 | if (myArray[i][property] === searchTerm) return i; 35 | } 36 | return -1; 37 | }; 38 | /** 39 | * This function will delete the element to delete from an array of objects 40 | * 41 | * @param to_delete needle 42 | * @param target_array haystack 43 | * @param property property to match 44 | * 45 | * TODO: Params should be rearranged to match the previous orders 46 | */ 47 | exports.deleteSeveralFromArrayOfObjects = function(to_delete, target_array, property){ 48 | for (var j = 0, length = to_delete.length; j < length; j++){ 49 | this.deleteFromArrayOfObjects(target_array, to_delete[j],property); 50 | } 51 | }; -------------------------------------------------------------------------------- /helpers/executor.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This module encapsulates the execution of commands in bash 3 | */ 4 | module.exports = function Executor() { 5 | var sys = require('sys'), 6 | exec = require('child_process').exec; 7 | 8 | /** 9 | * Private function that just logs the output to console 10 | * 11 | * @param error if any 12 | * @param stdout from execution 13 | * @param stderr from execution 14 | */ 15 | function handleOutput ( error , stdout , stderr ) { 16 | console.log( stdout ); 17 | console.log('End of execution'); 18 | } 19 | /** 20 | * This is the only function visible and is in charge of actually, executing the command 21 | * 22 | * @param command to execute 23 | * @param cb callback to call on finish, if no callback is defined @handleOutput will be used instead 24 | */ 25 | this.execute = function ( command , cb) { 26 | console.log('Executing "%s" in bash', command); 27 | exec (command , cb || handleOutput); 28 | }; 29 | }; -------------------------------------------------------------------------------- /helpers/math.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Function to fix a number to some number of decimals 3 | * 4 | * @param number to fix 5 | * @param n of decimals 6 | * @return {Number} 7 | */ 8 | exports.fixedTo = function (number, n) { 9 | var k = Math.pow(10, n+1); 10 | return (Math.round(number * k) / k); 11 | }; -------------------------------------------------------------------------------- /helpers/time.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This function will calculate the difference in milliseconds between now and Date supplied 3 | * 4 | * @param date 5 | * @return {*} if Date is not a Date it will return null, otherwise, { Number } with difference 6 | */ 7 | exports.calculateTimeSince = function(date){ 8 | var now = new Date(); 9 | 10 | var difference; 11 | if (date){ 12 | difference = now.getTime() - date.getTime(); 13 | 14 | if (difference < 0) { difference = 1; } 15 | } 16 | else{ 17 | difference = null; 18 | } 19 | 20 | return difference; 21 | }; 22 | /** 23 | * Function that checks if date supplied meets the SLA 24 | * 25 | * @param date to check 26 | * @param objective to achieve 27 | * @return {Boolean} 28 | */ 29 | exports.meetSLA = function(date, objective) { 30 | var now = new Date(), 31 | difference = (now - date) / 1000; 32 | 33 | return difference <= objective; 34 | }; 35 | /** 36 | * Function that checks if the difference between two dates is lesser than the objective 37 | * 38 | * @param date_start 39 | * @param date_end 40 | * @param objective 41 | * @return {Boolean} 42 | */ 43 | exports.meetSLABefore = function(date_start, date_end, objective) { 44 | var difference = (date_end - date_start) / 1000; 45 | 46 | return difference <= objective; 47 | }; 48 | /** 49 | * Function that checks if the difference between two dates is greater than the objective 50 | * 51 | * @param date_start 52 | * @param date_end 53 | * @param objective 54 | * @return {Boolean} 55 | */ 56 | exports.meetSLAAfter = function(date_start, date_end, objective) { 57 | var difference = (date_end - date_start) / 1000; 58 | 59 | return difference >= objective; 60 | }; 61 | /** 62 | * This function parses a MySQL datetime string and returns a Date Object 63 | * 64 | * @timestamp has to be in the following format YYYY-MM-DD H:I:S 65 | */ 66 | exports.mysqlTimestampToDate = function(timestamp){ 67 | var regex=/^([0-9]{2,4})-([0-1][0-9])-([0-3][0-9]) (?:([0-2][0-9]):([0-5][0-9]):([0-5][0-9]))?$/; 68 | var parts=timestamp.replace(regex,"$1 $2 $3 $4 $5 $6").split(' '); 69 | return new Date(parts[0],parts[1]-1,parts[2],parts[3],parts[4],parts[5]); 70 | }; 71 | /** 72 | * Function that remove Hour, Minute and Second data from a Date 73 | * 74 | * @param date 75 | */ 76 | exports.setAbsoluteDay = function (date){ 77 | date.setHours(0); 78 | date.setMinutes(0); 79 | date.setSeconds(0); 80 | }; -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | Pannel Flaix 13 | 14 | 15 | 16 | 17 | 20 | 21 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 |
35 |
36 |
37 | 38 |
39 |
40 | 41 |
42 |
43 |
44 | 45 | 46 |
47 |
48 |
49 | 50 |
51 |
52 |
53 |
54 |
55 | Occupation 56 |
57 |
58 |
59 |
60 | 0% 61 |
62 |
63 |
64 |
65 |
66 |
67 | Awaiting 68 |
69 |
70 |
71 |
72 | 0 73 |
74 |
75 |
76 |
77 |
78 |
79 | Talking 80 |
81 |
82 |
83 |
84 | 0 85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 | 110 | 122 | 127 | 128 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | -------------------------------------------------------------------------------- /libraries/mysql.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module that encapsulates the mysql module 3 | */ 4 | mysql = require('mysql'); 5 | 6 | /** 7 | * Function that reconnects on connection lost by binding a function that fires up on error 8 | * 9 | * @param connection 10 | * @url https://github.com/felixge/node-mysql/issues/239 11 | */ 12 | function handleDisconnect(connection) { 13 | connection.on('error', function(err) { 14 | if (!err.fatal) { 15 | return; 16 | } 17 | 18 | if (err.code !== 'PROTOCOL_CONNECTION_LOST') { 19 | throw err; 20 | } 21 | 22 | console.log('Re-connecting lost connection: ' + err.stack); 23 | 24 | connection = mysql.createConnection(connection.config); 25 | handleDisconnect(connection); 26 | connection.connect(); 27 | }); 28 | } 29 | 30 | var mysql_conf = { 31 | host: 'HOST_IP', 32 | user: 'HOST_USER', 33 | password: 'HOST_PASS', 34 | database: 'HOST_DB' 35 | }; 36 | var client = mysql.createConnection(mysql_conf); 37 | 38 | handleDisconnect(client); 39 | 40 | module.exports.client = client; 41 | /** 42 | * Function that will perform a Query to the database 43 | * 44 | * @param query to perform 45 | * @param callback to call when data is ready. 46 | */ 47 | module.exports.doQuery = function(query , callback){ 48 | console.log ('Executing MySQL Query >> %s', query); 49 | client.query(query, 50 | function (err, results, fields){ 51 | if (err) 52 | { 53 | throw err; 54 | } 55 | callback(results); 56 | } 57 | ); 58 | }; -------------------------------------------------------------------------------- /models/agent.js: -------------------------------------------------------------------------------- 1 | var arrayHelper = require('../helpers/array.js'); 2 | /** 3 | * AGENTs model 4 | * 5 | * @param data object *array* with information needed to create the agent: 6 | * @codAgente agent code 7 | * @nombre agent name 8 | * @apellido1 first last name 9 | * @apellido2 second last name (if applicable) 10 | * @estado agent status in Asterisk 11 | * @param io socket.io object to broadcast 12 | * @constructor 13 | */ 14 | function Agent(data, io){ 15 | this.statuses = [ 16 | { 17 | id : 0, 18 | name : 'Disconnected', 19 | start_timer : false, 20 | is_call : false 21 | }, 22 | { 23 | id : 1, 24 | name : 'Available', 25 | start_timer : false, 26 | is_call : false 27 | }, 28 | { 29 | id : 2, 30 | name : 'Meeting', 31 | start_timer : true, 32 | is_call : false 33 | }, 34 | { 35 | id : 3, 36 | name : 'Administrative', 37 | start_timer : true, 38 | is_call : false 39 | }, 40 | { 41 | id : 4, 42 | name : 'Incoming', 43 | start_timer : true, 44 | is_call : true 45 | }, 46 | { 47 | id : 5, 48 | name : 'Outgoing', 49 | start_timer : true, 50 | is_call : true 51 | }, 52 | { 53 | id : 6, 54 | name : 'Resting', 55 | start_timer : true, 56 | is_call : false 57 | }, 58 | { 59 | id : 7, 60 | name : 'Glory time', 61 | start_timer : true, 62 | is_call : false 63 | } 64 | ]; 65 | 66 | this.prevStatus = {}; 67 | 68 | this.codAgente = data[0].codAgente; 69 | this.nombre = data[0].nombre; 70 | this.apellido1 = data[0].apellido1; 71 | this.apellido2 = data[0].apellido2; 72 | this.currentStatusTime = null; 73 | this.currentStatusTimeDiff = null; 74 | this.currentCallTimeDiff = null; 75 | this.currentCallTime = null; 76 | this.currentTalkingQueue = null; 77 | this.queues = this.getQueues(data); 78 | io.sockets.emit('logAgent', this); 79 | 80 | this.arrayHelper = arrayHelper; 81 | this.timeHelper = require('../helpers/time.js'); 82 | this.status = (this.arrayHelper.arrayObjectIndexOf(this.statuses, data[0].estado, 'name') !== -1) ? 83 | this.statuses[this.arrayHelper.arrayObjectIndexOf(this.statuses, data[0].estado, 'name')] : 84 | this.statuses[0]; 85 | } 86 | /** 87 | * This functions calculate current times of the current status and / or the current call 88 | */ 89 | Agent.prototype.calculateTimes = function(){ 90 | if (this.currentStatusTime !== null){ 91 | this.currentStatusTimeDiff = this.timeHelper.calculateTimeSince(this.currentStatusTime); 92 | } 93 | 94 | if (this.currentCallTime !== null){ 95 | this.currentCallTimeDiff = this.timeHelper.calculateTimeSince(this.currentCallTime); 96 | } 97 | }; 98 | /** 99 | * This function change the status of the agent. 100 | * 101 | * @param data object with needed data 102 | * @status : The new status (integer) should be a value between 1 and 5 103 | * @queue : The current queue where the agent is talking 104 | */ 105 | Agent.prototype.changeStatus = function(data){ 106 | this.prevStatus = this.status; 107 | 108 | if (this.statuses[data.status] !== undefined){ 109 | if (!this.statuses[data.status].start_timer) 110 | { 111 | this.currentStatusTime = null; 112 | this.currentStatusTimeDiff = null; 113 | this.currentCallTime = null; 114 | this.currentCallTimeDiff = null; 115 | this.currentTalkingQueue = null; 116 | } 117 | else 118 | { 119 | if (!this.statuses[data.status].is_call) 120 | {// That's it, not talking by phone 121 | this.currentStatusTime = new Date; 122 | this.currentTalkingQueue = null; 123 | } 124 | else 125 | { 126 | this.currentCallTime = new Date; 127 | this.currentTalkingQueue = data.queue || ''; 128 | } 129 | } 130 | 131 | this.status = this.statuses[data.status]; 132 | data.io.sockets.emit('changeEvent', {agent: this.codAgente, status: this.status.id, queue: this.currentTalkingQueue}); 133 | } 134 | }; 135 | /** 136 | * This function ends the current call 137 | * 138 | * @socket : The socket object in which will send info once the change has been made 139 | */ 140 | Agent.prototype.endCall = function (socket){ 141 | if (!this.prevStatus.is_call) 142 | {// Administrative time or unavailable 143 | this.changeStatus({ 144 | status : this.prevStatus.id || this.status.id, 145 | io : socket 146 | }); 147 | } 148 | else 149 | { 150 | this.changeStatus({ 151 | status: 1, 152 | io: socket 153 | }); 154 | } 155 | this.currentTalkingQueue = null; 156 | }; 157 | /** 158 | * This functions will store the agent's queues (and his/her priority) in a property. 159 | * 160 | * @queues : The queues 161 | */ 162 | Agent.prototype.getQueues = function (queues){ 163 | var array = []; 164 | for (var i=0, len = queues.length; i < len; i ++){ 165 | array.push({ 166 | name: queues[i].cola.replace('Cola',''), 167 | priority: queues[i].prioridad 168 | }); 169 | } 170 | return array; 171 | }; 172 | /** 173 | * This function is called whenever the agent starts or stops ringing. Will broadcast by socket 174 | * 175 | * @param data object with needed data 176 | * @action : Start or Stop 177 | */ 178 | Agent.prototype.manageRinging = function(data){ 179 | console.log('Agent [%s] %s ringing.', this.codAgente, data.action === 'start' ? 'started' : 'stopped'); 180 | data.io.sockets.emit('agentRinging', {agent: this.codAgente, action: data.action}); 181 | }; 182 | /** 183 | * Utility functions related to this model. Those are exported. 184 | * 185 | * @type {Object} 186 | */ 187 | var utils = { 188 | /** 189 | * This function is in charge of fetching agents from Database. First, will fetch and store agents. 190 | * Then, will retrieve their statuses and finally will call the callback with the results 191 | * 192 | * @param database to extract data 193 | * @param io to emit by socket 194 | * @param stored_agents currently stored agents (if any) 195 | * @param callback to be called once it finishes 196 | */ 197 | fetchAgents : function(database, io, stored_agents, callback){ 198 | var agents = []; 199 | 200 | utils.getAgents(database, function(results){ 201 | agents = utils.storeAgentsFromDB(io, stored_agents, results); 202 | utils.loadStatusTimes(database,function(data){ 203 | agents = utils.storeStatusTimes(agents, data); 204 | callback.apply(undefined,[agents]); 205 | }); 206 | }); 207 | }, 208 | 209 | /** 210 | * Function that get Agents from the database 211 | * 212 | * @param database 213 | * @param callback 214 | */ 215 | getAgents : function(database, callback){ 216 | var query = 'SELECT agentes.nombre AS nombre, ' + 217 | 'agentes.apellido1 AS apellido1, ' + 218 | 'agentes.apellido2 AS apellido2, ' + 219 | 'agentes.codAgente AS codAgente, ' + 220 | 'agentes.estado AS estado, ' + 221 | 'relcolaext.cola AS cola, ' + 222 | 'relcolaext.prioridad AS prioridad ' + 223 | 'FROM agentes ' + 224 | 'LEFT JOIN relcolaext ON relcolaext.codAgente = agentes.codAgente ' + 225 | 'LEFT JOIN colas ON colas.nombre = relcolaext.cola ' + 226 | 'WHERE agentes.visible_panel = 1 ' + 227 | 'AND agentes.activo = 1 ' + 228 | 'AND colas.panel = 1'; 229 | 230 | database.doQuery(query, callback); 231 | }, 232 | /** 233 | * This function will store all results from getAgents. It will try to keep current agents if there are any 234 | * updating their info (if applicable), inserting new ones and deleting. 235 | * 236 | * This function could be called anytime in the lifecycle of the app. 237 | * 238 | * @see getAgents 239 | * @param io to emit by socket 240 | * @param stored_agents contains the current stored agents 241 | * @param results that comes from the Database resultset 242 | */ 243 | storeAgentsFromDB : function (io, stored_agents, results) { 244 | var agents = stored_agents || []; 245 | 246 | for (var i = 0; i < results.length; i++) 247 | { 248 | var currentPosition = arrayHelper.arrayObjectIndexOf( 249 | agents, 250 | results[i].codAgente, 251 | 'codAgente' 252 | ); 253 | var agent_rows = utils.getAgentRows(results,results[i].codAgente); 254 | 255 | if (currentPosition === -1) 256 | { // It's not already connected 257 | agents.push( new Agent( 258 | agent_rows, 259 | io 260 | )); 261 | console.log('Agent %s %s [%s] was loaded on server startup', 262 | results[i].nombre, results[i].apellido1, results[i].codAgente); 263 | } 264 | else 265 | { 266 | var data = results[i], 267 | agent = agents[currentPosition]; 268 | 269 | agent.codAgente = data.codAgente; 270 | agent.nombre = data.nombre; 271 | agent.apellido1 = data.apellido1; 272 | agent.apellido2 = data.apellido2; 273 | agent.queues = agent.getQueues(agent_rows); 274 | 275 | if (!agent.status.is_call){ 276 | agent.status = (arrayHelper.arrayObjectIndexOf(agent.statuses, data.estado, 'name') !== -1) ? 277 | agent.statuses[arrayHelper.arrayObjectIndexOf(agent.statuses, data.estado, 'name')] : 278 | agent.statuses[0]; 279 | } 280 | } 281 | } 282 | var to_delete = []; 283 | 284 | for (var j = 0, length = agents.length; j < length; j ++) { 285 | var agent_aux = agents[j]; 286 | var current_position = arrayHelper.arrayObjectIndexOf(results, agent_aux.codAgente,'codAgente'); 287 | if (current_position === -1) { 288 | to_delete.push(agent_aux.codAgente); 289 | } 290 | } 291 | arrayHelper.deleteSeveralFromArrayOfObjects(to_delete, agents,'codAgente'); 292 | 293 | return agents; 294 | }, 295 | 296 | /** 297 | * This function will load the last status from all agents and when the change occurred 298 | * 299 | * @param database to query for data 300 | * @param callback to be called 301 | */ 302 | loadStatusTimes : function(database, callback){ 303 | var query = 'SELECT a.* ' + 304 | 'FROM eventos_centralita a ' + 305 | 'LEFT JOIN eventos_centralita b ' + 306 | 'ON b.codAgente = a.codAgente ' + 307 | 'AND b.fechaHora > a.fechaHora ' + 308 | 'WHERE b.idEvent IS NULL GROUP BY a.codAgente'; 309 | 310 | database.doQuery(query, callback); 311 | }, 312 | /** 313 | * This function will update each agent and will set the currenStatusTime property so it matches with the one we 314 | * have in database. 315 | * 316 | * @see loadStatusTimes 317 | * @param agents current agents 318 | * @param results that comes from the database 319 | */ 320 | storeStatusTimes : function(agents, results){ 321 | for (var i = 0, length = results.length; i < length; i++) { 322 | var agent_pos = arrayHelper.arrayObjectIndexOf(agents, results[i].codAgente, 'codAgente'); 323 | 324 | if (agent_pos !== -1 && agents[agent_pos].status.id > 1){ 325 | agents[agent_pos].currentStatusTime = results[i].fechaHora; 326 | } 327 | } 328 | return agents; 329 | }, 330 | /** 331 | * This function will get all rows for some agent 332 | * 333 | * @param array that contains all the rows for all agents 334 | * @param codAgent the agent code we are looking for 335 | * @return {Array} of rows 336 | */ 337 | getAgentRows : function (array, codAgent) { 338 | var agent_rows = []; 339 | 340 | // We get all results from that agent 341 | for (var j= 0; j < array.length; j++) 342 | { 343 | if (array[j].codAgente === codAgent) { 344 | agent_rows.push(array[j]); 345 | } 346 | } 347 | 348 | return agent_rows; 349 | }, 350 | /** 351 | * This function get the status of the agents, this function is the one that's called once a 352 | * client connects to the pannel and retrieve all necessary information that needs to be displayed 353 | * 354 | * @param agents 355 | * @return {Array} 356 | */ 357 | getStatus : function (agents){ 358 | var status = []; 359 | 360 | for (var i = 0, length = agents.length; i < length; i ++){ 361 | agents[i].calculateTimes(); 362 | 363 | status.push({ 364 | codAgente : agents[i].codAgente, 365 | status_id : agents[i].status.id, 366 | nombre : agents[i].nombre, 367 | apellido1 : agents[i].apellido1, 368 | apellido2 : agents[i].apellido2, 369 | currentCallTimeDiff : agents[i].currentCallTimeDiff, 370 | currentStatusTimeDiff : agents[i].currentStatusTimeDiff, 371 | currentTalkingQueue : agents[i].currentTalkingQueue, 372 | queues : agents[i].queues 373 | }); 374 | } 375 | 376 | return status; 377 | }, 378 | /** 379 | * This function is in charge of creating a new Agent 380 | * 381 | * @param results 382 | * @param io 383 | * @return {Agent} 384 | */ 385 | createAgent : function(results, io){ 386 | return new Agent(results,io); 387 | }, 388 | /** 389 | * Get an Agent object from it's agent's code 390 | * 391 | * @param agents 392 | * @param code 393 | * @return {*} 394 | */ 395 | getAgentFromCode : function (agents, code){ 396 | var agent, 397 | agent_position = arrayHelper.arrayObjectIndexOf(agents, code , 'codAgente'); 398 | 399 | if (agent_position !== -1){ agent = agents[agent_position]; } 400 | 401 | return agent; 402 | } 403 | }; 404 | 405 | exports.model = Agent; 406 | exports.utils = utils; -------------------------------------------------------------------------------- /models/client.js: -------------------------------------------------------------------------------- 1 | var arrayHelper = require('../helpers/array.js'), 2 | timeHelper = require('../helpers/time.js'); 3 | 4 | function Client ( data , io , database ) { 5 | var self = this; 6 | 7 | this.name = data.name; 8 | this.database = database; 9 | this.perc_abandoned = data.perc_abandoned; 10 | this.perc_answered = data.perc_answered; 11 | this.sec_abandoned = data.sec_abandoned; 12 | this.sec_answered = data.sec_answered; 13 | this.timeHelper = require('../helpers/time.js'); 14 | this.mathHelper = require('../helpers/math.js'); 15 | 16 | this.total_calls = 0; 17 | this.offered_calls = 0; 18 | this.total_abandoned = 0; 19 | this.total_answered = 0; 20 | this.failed_calls = 0; 21 | this.abandoned_after_SLA = 0; 22 | this.answered_before_SLA = 0; 23 | this.total_response_time = 0; 24 | this.total_abandon_time = 0; 25 | 26 | this.stats_by_hour = []; 27 | this.stats_by_hour.length = 23; 28 | 29 | this.io = io; 30 | this.socket = self.io.of('/' + this.name); 31 | 32 | self.socket.on('connection', function(socket){ 33 | console.log('Someone wants to see some %s stats...', self.name); 34 | self.sendStatus(socket.id); 35 | }); 36 | } 37 | Client.prototype.storeCall = function(now, call_date, abandoned){ 38 | var type = abandoned ? 'abandoned' : 'answered', 39 | meet_sla = false; 40 | 41 | now = (!now) ? new Date() : now; 42 | 43 | if (abandoned){ 44 | this.total_abandon_time += parseInt(((now - call_date) /1000).toFixed(0),10); 45 | this.total_abandoned++; 46 | 47 | if (timeHelper.meetSLAAfter(call_date, now, this.sec_abandoned)){ 48 | this.abandoned_after_SLA++; 49 | meet_sla = true; 50 | } 51 | } 52 | else { 53 | this.total_response_time += parseInt(((now - call_date) /1000).toFixed(0),10); 54 | this.total_answered++; 55 | 56 | if (timeHelper.meetSLABefore(call_date, now, this.sec_answered)){ 57 | this.answered_before_SLA++; 58 | meet_sla = true; 59 | } 60 | } 61 | 62 | utils.storeCall(this.stats_by_hour, now, call_date, type, meet_sla); 63 | }; 64 | Client.prototype.getStatus = function(){ 65 | return { 66 | total_calls : this.total_calls, 67 | total_offered_calls : this.offered_calls, 68 | total_abandoned : this.total_abandoned, 69 | total_answered : this.total_answered, 70 | failed_calls : this.failed_calls, 71 | abandoned_after_SLA : this.abandoned_after_SLA, 72 | answered_before_SLA : this.answered_before_SLA, 73 | average_response_time : this.mathHelper.fixedTo((this.total_response_time / this.total_answered),2), 74 | per_hour : this.stats_by_hour 75 | }; 76 | }; 77 | Client.prototype.sendStatus = function (socketid) { 78 | var status = this.getStatus(); 79 | 80 | if (socketid) { 81 | this.socket.socket(socketid).emit('clientStatus', status); 82 | } 83 | else { 84 | this.socket.emit('clientStatus', status) 85 | } 86 | }; 87 | Client.prototype.resetData = function () { 88 | this.total_calls = 0; 89 | this.total_offered_calls = 0; 90 | this.total_abandoned = 0; 91 | this.total_answered = 0; 92 | this.failed_calls = 0; 93 | this.abandoned_after_SLA = 0; 94 | this.answered_before_SLA = 0; 95 | this.average_response_time = 0; 96 | 97 | this.sendStatus(); 98 | }; 99 | Client.prototype.loadStats = function(start_date, end_date, callback){ 100 | var self = this; 101 | var query = 'SELECT ' + 102 | 'llamadas.uniqueid AS unique_id, ' + 103 | 'llamadas.tipo AS type, ' + 104 | 'clientes.nombre AS client, ' + 105 | 'colas.nombre AS queue, ' + 106 | 'IF(ISNULL(llamadas.fechaInicioCola), llamadas.fecha, llamadas.fechaInicioCola) AS start_date, ' + 107 | 'llamadas.fechaAnswered AS answered_date, ' + 108 | 'llamadas.fechaHungup AS hungup_date, ' + 109 | 'llamadas.agente AS agent, ' + 110 | 'IF(ISNULL(fechaAnswered), TIMESTAMPDIFF(SECOND,IF(ISNULL(llamadas.fechaInicioCola), llamadas.fecha, llamadas.fechaInicioCola),fechaHungup),TIMESTAMPDIFF(SECOND,IF(ISNULL(llamadas.fechaInicioCola), llamadas.fecha, llamadas.fechaInicioCola),fechaAnswered)) AS time_in_queue, '+ 111 | 'llamadas.status AS status ' + 112 | 'FROM llamadas ' + 113 | 'LEFT JOIN colas ON colas.id = llamadas.cola ' + 114 | 'LEFT JOIN numeros_cabecera ON numeros_cabecera.id = colas.numero ' + 115 | 'LEFT JOIN clientes ON clientes.idCliente = numeros_cabecera.cliente ' + 116 | 'WHERE llamadas.fecha >= ? ' + 117 | 'AND llamadas.fecha <= ? ' + 118 | 'AND clientes.nombre = ? ' + 119 | 'AND llamadas.tipo = \'Incoming\' ' + 120 | 'ORDER BY llamadas.fecha;'; 121 | 122 | console.log('Executing query >> %s', query); 123 | 124 | this.database.client.query( query , [start_date, end_date, this.name], function(err,results){ 125 | callback(self.returnStats.call(self,results)); 126 | }); 127 | }; 128 | Client.prototype.returnStats = function(results) { 129 | var stats = { 130 | name : this.name, 131 | sec_abandoned : this.sec_abandoned, 132 | sec_answered : this.sec_answered, 133 | perc_abandoned : this.perc_abandoned, 134 | perc_answered : this.perc_answered, 135 | real_time : false, 136 | total_calls : 0, 137 | total_offered_calls : 0, 138 | total_response_time : 0, 139 | average_response_time : 0, 140 | failed_calls : 0, 141 | total_abandoned : 0, 142 | abandoned_after_SLA : 0, 143 | total_answered : 0, 144 | answered_before_SLA : 0 145 | }; 146 | stats.per_hour = []; 147 | 148 | for (var i = 0, length = results.length; i < length; i++){ 149 | if (results[i].type === 'Incoming'){ 150 | var call_date = results[i].start_date, 151 | answered_date = results[i].answered_date, 152 | hungup_date = results[i].hungup_date, 153 | status = results[i].status, 154 | type = ( 155 | (status.indexOf('bandoned') === -1) && 156 | (status.indexOf('Voicemail') === -1) 157 | ) ? 'answered' : 'abandoned', 158 | meets_sla; 159 | 160 | stats.total_calls++; 161 | 162 | if (status !== 'Abandoned in message' && status !== 'Out of schedule') { 163 | stats.total_offered_calls++; 164 | stats['total_' + type]++; 165 | 166 | if (type === 'abandoned'){ 167 | meets_sla = this.timeHelper.meetSLAAfter( 168 | call_date, 169 | hungup_date, 170 | this['sec_' + type] 171 | ); 172 | if (meets_sla){ 173 | stats.abandoned_after_SLA++; 174 | } 175 | 176 | if (hungup_date === null || hungup_date.getTime() !== hungup_date.getTime()){ 177 | console.log('Error abandoned: '); 178 | console.log(results[i]); 179 | } 180 | else { 181 | utils.storeCall(stats.per_hour, hungup_date, call_date, type, meets_sla); 182 | } 183 | } 184 | else { 185 | var time_in_queue = parseInt(results[i].time_in_queue,10); 186 | if (!isNaN(time_in_queue)){ 187 | stats.total_response_time += time_in_queue; 188 | } 189 | 190 | meets_sla = this.timeHelper.meetSLABefore( 191 | call_date, 192 | answered_date, 193 | this['sec_' + type] 194 | ); 195 | if (meets_sla){ 196 | stats.answered_before_SLA++; 197 | } 198 | 199 | if (answered_date === null || answered_date.getTime() !== answered_date.getTime()){ 200 | console.log('Error answered: '); 201 | console.log(results[i]); 202 | } 203 | else { 204 | utils.storeCall(stats.per_hour, answered_date, call_date, type, meets_sla); 205 | } 206 | } 207 | } 208 | else { 209 | stats.failed_calls++; 210 | } 211 | } 212 | } 213 | stats.average_response_time = this.mathHelper.fixedTo((stats.total_response_time / stats.total_answered),2); 214 | 215 | return stats; 216 | }; 217 | 218 | var utils = { 219 | fetchClients : function(database, io, stored_clients, callback){ 220 | var clients = []; 221 | 222 | utils.getClients(database, function(results) { 223 | clients = utils.storeClientsFromDB(io, stored_clients, database, results); 224 | callback.apply(undefined,[clients]); 225 | }); 226 | }, 227 | /** 228 | * This function will get all clients for stats purpouses 229 | * 230 | * @param callback to be called when it finishes 231 | */ 232 | getClients : function(database, callback) { 233 | var query = 'SELECT nombre AS name, ' + 234 | 'porcAbandoned AS perc_abandoned, ' + 235 | 'secAbandoned AS sec_abandoned, ' + 236 | 'porcAnswered AS perc_answered, ' + 237 | 'secAnswered AS sec_answered ' + 238 | 'FROM clientes'; 239 | database.doQuery(query, callback); 240 | }, 241 | 242 | /** 243 | * This function will store all results from getClients 244 | * 245 | * @see getClients 246 | */ 247 | storeClientsFromDB : function(io, stored_clients, database, results) { 248 | var prefetched = (stored_clients.length !== 0), 249 | clients = stored_clients || []; 250 | 251 | for (var i = 0; i < results.length; i++) { 252 | var result_set = results[i], 253 | current_position = arrayHelper.arrayObjectIndexOf( 254 | clients, 255 | result_set.name, 256 | 'name' 257 | ); 258 | 259 | if (current_position === -1){ 260 | var client = new Client(result_set, io , database); 261 | 262 | clients.push( 263 | client 264 | ); 265 | } 266 | else { 267 | clients[current_position].perc_abandoned = result_set.perc_abandoned; 268 | clients[current_position].perc_answered = result_set.perc_answered; 269 | clients[current_position].sec_abandoned = result_set.sec_abandoned; 270 | clients[current_position].sec_answered = result_set.sec_answered; 271 | } 272 | 273 | console.log('Client %s was loaded on server startup', result_set.name); 274 | } 275 | 276 | // If there was some clients already loaded and his function is called, it means that the panel 277 | // is being refetched so we'll get rid of those which aren't in our result 278 | if (prefetched){ 279 | var to_delete = []; 280 | for (var j= 0, length = clients.length; j < length; j++){ 281 | var position = arrayHelper.arrayObjectIndexOf( 282 | results, 283 | clients[j].name, 284 | 'name' 285 | ); 286 | 287 | if (position === -1) { 288 | to_delete.push(clients[j].name); 289 | } 290 | } 291 | 292 | arrayHelper.deleteSeveralFromArrayOfObjects(to_delete, clients, 'name'); 293 | } 294 | 295 | return clients; 296 | }, 297 | 298 | /** 299 | * This is a helper method that gets a client object by it's name 300 | * 301 | * @param name you are looking for 302 | * @return {Client} the object or undefined 303 | */ 304 | getClientFromName : function (clients, name){ 305 | var client, 306 | client_position = arrayHelper.arrayObjectIndexOf(clients, name , 'name'); 307 | 308 | if (client_position !== -1){ client = clients[client_position]; } 309 | 310 | return client; 311 | }, 312 | 313 | storeCall : function (storage, now, call_date, type, meet_sla) { 314 | var hour = now.getHours(); 315 | 316 | if (storage[hour] === undefined){ 317 | storage[hour] = { 318 | total : 0, 319 | answered : 0, 320 | abandoned : 0, 321 | answered_sla : 0, 322 | abandoned_sla : 0, 323 | answered_time : 0, 324 | abandoned_time : 0 325 | }; 326 | } 327 | 328 | storage[hour].total++; 329 | storage[hour][type]++; 330 | storage[hour][type + '_time'] += parseInt(((now - call_date) /1000).toFixed(0),10); 331 | if (meet_sla){ storage[hour][type + '_sla']++; } 332 | } 333 | }; 334 | exports.model = Client; 335 | exports.utils = utils; -------------------------------------------------------------------------------- /models/queue.js: -------------------------------------------------------------------------------- 1 | var arrayHelper = require('../helpers/array.js'), 2 | timeHelper = require('../helpers/time.js'); 3 | 4 | function Queue(data){ 5 | // Stats 6 | this.total_calls = 0; 7 | this.total_abandoned = 0; 8 | this.total_abandoned_before_sla = 0; 9 | this.total_answered = 0; 10 | this.total_answered_before_sla = 0; 11 | 12 | this.name = data.name; 13 | this.color = data.color; 14 | this.client_name = data.client; 15 | this.client_obj = data.client_obj; 16 | 17 | // Initializing and data fullfillment 18 | this.calls = []; 19 | this.last_call_time = null; 20 | this.last_call_time_diff = null; 21 | } 22 | 23 | var utils = { 24 | fetchQueues : function(database, io, stored_queues, clients, getClientFromName, callback){ 25 | var queues = []; 26 | 27 | utils.getQueues(database,function(results){ 28 | queues = utils.storeQueuesFromDB(stored_queues, clients, getClientFromName, results); 29 | callback.apply(undefined,[queues]); 30 | }); 31 | }, 32 | getQueues : function(database, callback){ 33 | var query = "SELECT " + 34 | "colas.nombre AS name, " + 35 | "colas.color AS color, " + 36 | "clientes.nombre AS client " + 37 | "FROM colas " + 38 | "INNER JOIN numeros_cabecera cabecera ON colas.numero = cabecera.id " + 39 | "INNER JOIN clientes ON cabecera.cliente = clientes.idCliente " + 40 | "WHERE colas.panel = 1 " + 41 | "ORDER BY nombreCola ASC"; 42 | 43 | database.doQuery(query, callback); 44 | }, 45 | storeQueuesFromDB : function(stored_queues, clients, getClientFromName, results){ 46 | // This part runs through the current queue list and update as needed. If the queue isn't found on results 47 | // it will be deleted 48 | var queues = stored_queues || [], 49 | to_delete = []; 50 | 51 | for (var i = 0, len = queues.length; i < len; i ++){ 52 | var queuePosition = arrayHelper.arrayObjectIndexOf(results, queues[i].name, 'name'); 53 | 54 | if (queuePosition === -1) { // If we don't find it in the results may be hidden or deleted 55 | to_delete.push(queues[i].name); 56 | } 57 | else{ 58 | if (queues[i].color !== results[queuePosition].color){ 59 | queues[i].color = results[queuePosition].color; 60 | } 61 | } 62 | } 63 | 64 | // Deleting 65 | arrayHelper.deleteSeveralFromArrayOfObjects(to_delete, queues, 'name'); 66 | 67 | // This part will do the other part, adding queues that aren't present. 68 | for (var j = 0, length = results.length; j < length; j++){ 69 | 70 | var selfQueuePosition = arrayHelper.arrayObjectIndexOf( 71 | queues, 72 | results[j].name, 73 | 'name' 74 | ); 75 | 76 | if (selfQueuePosition === -1){//We have to insert 77 | console.log('New queue detected: %s', results[j].name); 78 | 79 | queues.push( 80 | new Queue( 81 | { 82 | name: results[j].name, 83 | color: results[j].color, 84 | client_name: results[j].client, 85 | client_obj : getClientFromName(clients, results[j].client) 86 | } 87 | ) 88 | ); 89 | } 90 | } 91 | 92 | return queues; 93 | }, 94 | 95 | dispatchCall : function(data){ 96 | var queue = data.queue, 97 | client = queue.client_obj; 98 | 99 | if (queue !== undefined){ 100 | var calls_length = queue.calls.length; 101 | 102 | if (data.type === 'in'){ 103 | // Pushing the call to the queue 104 | queue.calls.push({ 105 | uniqueid : data.uniqueid, 106 | date: new Date() 107 | }); 108 | 109 | // If there is only one call in the queue, the last time of a call is right now! 110 | if (queue.calls.length === 1) { 111 | queue.last_call_time = new Date(); 112 | } 113 | } 114 | else 115 | { 116 | // If it's in the queue 117 | var call_position = arrayHelper.arrayObjectIndexOf(queue.calls, data.uniqueid, 'uniqueid'); 118 | 119 | if (call_position !== -1) { 120 | var call = queue.calls[call_position], 121 | now = new Date(); 122 | 123 | // Client stats stuff 124 | client.storeCall(now, call.date, data.abandoned); 125 | client.sendStatus(); 126 | 127 | arrayHelper.deleteFromArrayOfObjects(queue.calls, data.uniqueid, 'uniqueid'); 128 | 129 | if (queue.calls.length === 0) { 130 | queue.last_call_time = null; 131 | } 132 | else { 133 | queue.last_call_time = queue.calls[0].date; 134 | } 135 | } 136 | else { 137 | console.log('Sorry, we couldn\'t find the call [%s] within %s.', data.uniqueid, queue.name); 138 | } 139 | } 140 | //Emiting by socket 141 | data.io.sockets.emit('callInOrOutQueue', { 142 | type: (data.abandoned === false) ? data.type : 'abandoned', 143 | queue: queue.name, 144 | calls: queue.calls.length, 145 | timeSince: timeHelper.calculateTimeSince(queue.last_call_time) 146 | }); 147 | } 148 | }, 149 | getStatus : function (queues){ 150 | var status = []; 151 | 152 | for (var i = 0, length = queues.length; i < length; i++){ 153 | status.push({ 154 | color : queues[i].color, 155 | last_call_time_diff : timeHelper.calculateTimeSince(queues[i].last_call_time), 156 | name : queues[i].name, 157 | num_calls : queues[i].calls.length 158 | }); 159 | } 160 | 161 | return status; 162 | }, 163 | getQueueFromName : function(queues, queue_name){ 164 | var queuePosition = arrayHelper.arrayObjectIndexOf(queues, queue_name, 'name'); 165 | return (queuePosition !== -1) ? queues[queuePosition] : undefined; 166 | }, 167 | resetData : function(queues) { 168 | for (var i = 0, length = queues.length; i < length; i++){ 169 | queues[i].total_calls = 0; 170 | queues[i].total_abandoned = 0; 171 | queues[i].total_abandoned_before_sla = 0; 172 | queues[i].total_answered = 0; 173 | queues[i].total_answered_before_sla = 0; 174 | } 175 | } 176 | }; 177 | 178 | exports.model = Queue; 179 | exports.utils = utils; -------------------------------------------------------------------------------- /modules/app.js: -------------------------------------------------------------------------------- 1 | module.exports = function App(database, io, async){ 2 | var Agent = require('../models/agent.js'), 3 | Client = require('../models/client.js'), 4 | Queue = require('../models/queue.js'), 5 | arrayHelper = require('../helpers/array.js'), 6 | timeHelper = require('../helpers/time.js'), 7 | Refetcher = require('../modules/refetcher.js'), 8 | Reseter = require('../modules/reseter.js'), 9 | Executor = require('../helpers/executor.js'); 10 | 11 | // Initializing data 12 | this.calls = 0; 13 | this.talking = 0; 14 | this.awaiting = 0; 15 | 16 | this.connected_clients = []; 17 | this.agents = []; 18 | this.clients = []; 19 | this.queues = []; 20 | 21 | this.agent_utils = Agent.utils; 22 | this.client_utils = Client.utils; 23 | this.queue_utils = Queue.utils; 24 | 25 | this.database = database; 26 | this.io = io; 27 | this.async = require('async'); 28 | this.refetcher = new Refetcher(this, database, io); 29 | this.reseter = new Reseter(this); 30 | this.executor = new Executor(); 31 | 32 | this.abandoned_status = [ 33 | 'Out of schedule', 34 | 'Abandoned in ring', 35 | 'Abandoned in message', 36 | 'Abandoned in queue', 37 | 'Voicemail' 38 | ]; 39 | 40 | var self = this; 41 | 42 | // Getting data from the database 43 | this.init = function(callback){ 44 | async.parallel([ 45 | self.getAgents, 46 | self.getClients 47 | ], function(){ 48 | // We need Agents and Clients before we can load Queues 49 | self.getQueues(function(){ 50 | callback.apply(self,[null]); 51 | }); 52 | }); 53 | }; 54 | 55 | /** 56 | * This function get all visible agents and their queues and then store them 57 | * 58 | * @param callback to be called when it finishes 59 | */ 60 | this.getAgents = function(callback){ 61 | Agent.utils.fetchAgents( 62 | self.database, 63 | self.io, 64 | self.agents, 65 | function(agents){ 66 | self.agents = agents; 67 | callback.apply(self,[null]); 68 | } 69 | ); 70 | }; 71 | /** 72 | * This function will get all clients for stats purpouses 73 | * 74 | * @param callback to be called when it finishes 75 | */ 76 | this.getClients = function(callback){ 77 | Client.utils.fetchClients( 78 | self.database, 79 | self.io, 80 | self.clients, 81 | function(clients){ 82 | self.clients = clients; 83 | callback.apply(self,[null]); 84 | } 85 | ); 86 | }; 87 | /** 88 | * This function get all queues and then store them 89 | * 90 | * @param callback to be called when it finishes 91 | */ 92 | this.getQueues = function(callback){ 93 | Queue.utils.fetchQueues( 94 | self.database, 95 | self.io, 96 | self.queues, 97 | self.clients, 98 | Client.utils.getClientFromName, 99 | function(queues){ 100 | self.queues = queues; 101 | callback.apply(self,[null]); 102 | } 103 | ); 104 | }; 105 | /** 106 | * This function will update the primary occupation with the one supplied and will inform all sockets about that 107 | */ 108 | this.updatePrimary = function (){ 109 | if (self.calls < 0) { 110 | self.calls = 0; 111 | } 112 | 113 | if (self.awaiting < 0) { 114 | self.awaiting = 0; 115 | } 116 | 117 | if (self.talking < 0) { 118 | self.talking = 0; 119 | } 120 | 121 | io.sockets.emit('updatePrimary',{ 122 | calls: self.calls, 123 | talking: self.talking, 124 | awaiting: self.awaiting 125 | }); 126 | }; 127 | /** 128 | * This is the function that is called once someone connects to the socket. It gets all data from all sources and 129 | * send back to the socket. 130 | * 131 | * @param socketid that just connected 132 | */ 133 | this.sendCurrentStatus = function (socketid, client_ip){ 134 | var status = {}; 135 | 136 | status.agents = Agent.utils.getStatus(self.agents); 137 | status.queues = Queue.utils.getStatus(self.queues); 138 | 139 | status.calls = self.calls; 140 | status.awaiting = self.awaiting; 141 | status.talking = self.talking; 142 | 143 | self.isAdminUser(client_ip, false, function (is_admin) { 144 | status.is_admin = is_admin; 145 | io.sockets.socket(socketid).emit('currentStatus', status); 146 | }); 147 | }; 148 | /** 149 | * This is the function that is called before sending status to a connected client to determine if this 150 | * user has admin role which enables some advanced functions 151 | * 152 | * @param client_ip is the client IP which is get through request's header 153 | * @param enforced this param avoid query madness because this only will be true the first time the client visits 154 | * @param callback which will be called with the result. 155 | */ 156 | this.isAdminUser = function (client_ip, enforced, callback) { 157 | var client_position = arrayHelper.arrayObjectIndexOf(self.connected_clients, client_ip, 'ip'), 158 | client = self.connected_clients[client_position]; 159 | 160 | if (client && client.is_admin){ 161 | callback(true); 162 | } 163 | else { 164 | if (enforced){ 165 | var query = 'SELECT agentes.grupo, agentes.usuario FROM agentes ' + 166 | 'LEFT JOIN acl_rel_grupos_permisos rel ON agentes.grupo = rel.grupo ' + 167 | 'WHERE agentes.usuario = (SELECT usuario FROM eventos_equipos WHERE ip = \'' + 168 | client_ip + 169 | '\' AND fecha >= CURDATE() ORDER BY fecha DESC LIMIT 1) ' + 170 | 'AND rel.permiso = 6'; 171 | 172 | self.database.doQuery(query, function(results){ 173 | callback(results.length !== 0); 174 | }); 175 | } 176 | else { 177 | callback(false); 178 | } 179 | } 180 | }; 181 | /** 182 | * This funcion will get all rows for some agent 183 | * 184 | * @param array that contains all the rows for all agents 185 | * @param codAgent the agent code we are looking for 186 | * @return {Array} of rows 187 | */ 188 | this.getAgentRows = function (array, codAgent) { 189 | var agent_rows = []; 190 | 191 | // We get all results from that agent 192 | for (var j= 0; j < array.length; j++) 193 | { 194 | if (array[j].codAgente === codAgent) 195 | { 196 | agent_rows.push(array[j]); 197 | } 198 | } 199 | 200 | return agent_rows; 201 | }; 202 | /** 203 | * This function will send a call to an agent. 204 | * 205 | * @param data object that contains all needed properties. 206 | * @from The queue you want to transfer the call from 207 | * @to The agent you want to transfer the call to 208 | */ 209 | this.sendCallToAgent = function (data){ 210 | var queue = Queue.utils.getQueueFromName(self.queues, data.from); 211 | 212 | if (queue){ 213 | var agent_position = arrayHelper.arrayObjectIndexOf(self.agents, data.to, 'codAgente'); 214 | if (agent_position !== -1) { 215 | if (queue.calls.length !== 0){ 216 | // If there is any call to be transfered 217 | var call_id = queue.calls[0].uniqueid, 218 | agent_code = data.to, 219 | command = 'ssh root@170.251.100.9 "/usr/local/bin/transferencia-flaix.sh ' + call_id+ 220 | ' '+agent_code+' '+ 221 | queue.name +'"'; 222 | self.executor.execute(command, function( error , stdout , stderr ){ 223 | console.log( stdout ); 224 | console.log('End of execution'); 225 | if (stdout.indexOf('failed') !== -1){ 226 | console.log('Transfering %s failed!', call_id); 227 | } 228 | }); 229 | } 230 | 231 | } 232 | } 233 | }; 234 | /** 235 | * This function force the unlog of an agent by running a script that calls Asterisk and tell it to unlog he/she 236 | * 237 | * @param data object that contains all needed properties. 238 | * @agent agent that will be unlogged 239 | * 240 | * TODO: This is not ideal, the pannel should connect to Asterisk directly 241 | */ 242 | this.forceUnlogAgent = function(data){ 243 | var agent_position = arrayHelper.arrayObjectIndexOf(self.agents, data.agent, 'codAgente'); 244 | 245 | if (agent_position !== -1) { 246 | self.executor.execute( 247 | 'ssh root@170.251.100.9 "/usr/local/bin/deslogeoforzado.sh ' + data.agent + '"' 248 | ); 249 | } 250 | }; 251 | /** 252 | * This function will load the stats for today. This will act in case you restart the app so the stats keep working 253 | */ 254 | this.loadTodayStats = function(){ 255 | console.log('Let\'s start loading today stats'); 256 | var query = 'SELECT ' + 257 | 'llamadas.uniqueid AS unique_id, ' + 258 | 'llamadas.tipo AS type, ' + 259 | 'clientes.nombre AS client, ' + 260 | 'colas.nombre AS queue, ' + 261 | 'IF(ISNULL(llamadas.fechaInicioCola), fecha , fechaInicioCola) AS start_date, ' + 262 | 'llamadas.fechaAnswered AS answered_date, ' + 263 | 'llamadas.fechaHungup AS hungup_date, ' + 264 | 'IF(ISNULL(fechaAnswered), TIMESTAMPDIFF(SECOND,fechaInicioCola,fechaHungup),TIMESTAMPDIFF(SECOND,fechaInicioCola,fechaAnswered)) AS time_in_queue, '+ 265 | 'llamadas.agente AS agent, ' + 266 | 'llamadas.status AS status ' + 267 | 'FROM llamadas ' + 268 | 'LEFT JOIN colas ON colas.id = llamadas.cola ' + 269 | 'LEFT JOIN numeros_cabecera ON numeros_cabecera.id = colas.numero ' + 270 | 'LEFT JOIN clientes ON clientes.idCliente = numeros_cabecera.cliente ' + 271 | 'WHERE DATE(llamadas.fecha) = CURDATE() ' + 272 | 'ORDER BY llamadas.fecha;'; 273 | 274 | console.log ('Executing MySQL Query >> %s', query); 275 | // TODO: It should be used the encapsulated version of the Database instead of this 276 | 277 | self.database.client.query(query, 278 | function (err, results, fields){ 279 | if (err) 280 | { 281 | throw err; 282 | } 283 | if (results.length > 0) 284 | { 285 | for (var i = 0; i < results.length; i++) 286 | { 287 | var client = Client.utils.getClientFromName(self.clients, results[i].client), 288 | call_queue = Queue.utils.getQueueFromName(self.queues,results[i].queue); 289 | 290 | var agent_position = arrayHelper.arrayObjectIndexOf( 291 | self.agents, 292 | results[i].agent, 293 | 'codAgente' 294 | ); 295 | 296 | var call_agent = (agent_position !== -1) ? self.agents[agent_position] : undefined; 297 | 298 | if (client !== undefined){ 299 | var call_date = results[i].start_date, 300 | answered_date = results[i].answered_date, 301 | hungup_date = results[i].hungup_date, 302 | status = results[i].status, 303 | type = (self.abandoned_status.indexOf(status) === -1) ? 'answered' : 'abandoned'; 304 | 305 | if (results[i].type === 'Incoming'){ 306 | client.total_calls++; 307 | 308 | if (status !== 'Abandoned in message' && status !== 'Out of schedule') { 309 | client.offered_calls++; 310 | 311 | if (type === 'abandoned') { 312 | client.storeCall(hungup_date,call_date,true); 313 | } 314 | else { 315 | client.storeCall(answered_date, call_date,false); 316 | } 317 | } 318 | else { 319 | client.failed_calls++; 320 | } 321 | } 322 | 323 | if (type === 'answered' && hungup_date === null && results[i].agent === null){ 324 | if (call_queue) { 325 | console.log('Call %s assigned to the queue %s in startup.', 326 | results[i].unique_id, 327 | call_queue.name 328 | ); 329 | 330 | self.awaiting++; 331 | self.calls++; 332 | call_queue.calls.push({ 333 | uniqueid : results[i].unique_id, 334 | date : call_date 335 | }); 336 | } 337 | else { 338 | console.log('This call - %s - cannot be allocated', 339 | results[i].unique_id 340 | ); 341 | } 342 | } 343 | else { 344 | if (type === 'answered' && hungup_date === null){ 345 | if (call_agent !== undefined) { 346 | console.log('Call %s assigned to the agent %s in startup.', 347 | results[i].unique_id, 348 | call_agent.codAgente 349 | ); 350 | 351 | if (results[i].type === 'Incoming'){ 352 | self.talking++; 353 | } 354 | 355 | self.calls++; 356 | call_agent.changeStatus({ 357 | start_timer : true, 358 | status : (results[i].type == 'Incoming') ? 4 : 5, 359 | io : self.io, 360 | is_call : true, 361 | queue : (call_queue) ? call_queue.name : null 362 | }); 363 | 364 | call_agent.currentCallTime = answered_date; 365 | } 366 | else { 367 | console.log('This call - %s - cannot be allocated', 368 | results[i].unique_id 369 | ); 370 | } 371 | } 372 | } 373 | } 374 | } 375 | } 376 | } 377 | ); 378 | }; 379 | /** 380 | * This will start the reset process... 381 | */ 382 | this.startReset = function (){ 383 | self.reseter.reset(); 384 | }; 385 | /** 386 | * This function will store clients data on connection 387 | * 388 | * @param client_ip the client ip 389 | * @param admin if it's admin or it's not 390 | */ 391 | this.storeConnectedClient = function(client_ip, admin) { 392 | var client_position = arrayHelper.arrayObjectIndexOf(self.connected_clients, client_ip, 'ip'); 393 | if(client_position === -1){ 394 | var query = 'SELECT usuario FROM eventos_equipos WHERE ip = \'' + 395 | client_ip + 396 | '\' AND fecha >= CURDATE() ORDER BY fecha DESC LIMIT 1'; 397 | 398 | self.database.doQuery(query, function(results){ 399 | var name = undefined; 400 | 401 | if (results.length !== 0){ 402 | name = results[0].usuario; 403 | } 404 | 405 | self.connected_clients.push({ 406 | user_name : name, 407 | is_admin : admin, 408 | ip : client_ip 409 | }); 410 | }); 411 | } 412 | else { 413 | self.connected_clients.push({ 414 | user_name : self.connected_clients[client_position].name, 415 | is_admin : admin, 416 | ip : client_ip 417 | }); 418 | self.connected_clients[client_position].is_admin = admin; 419 | } 420 | }; 421 | /** 422 | * This function will delete the client data using it's IP to find it 423 | * 424 | * @param client_ip 425 | */ 426 | this.deleteConnectedClient = function(client_ip) { 427 | arrayHelper.deleteFromArrayOfObjects(self.connected_clients, client_ip,'ip'); 428 | }; 429 | }; -------------------------------------------------------------------------------- /modules/browser_ban.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This module is a middleware for express that is executed before any request is processed. In the banned list we can 3 | * add any portion of the Agent String. Currently only Internet Explorer is banned. 4 | */ 5 | var banned = [ 6 | 'MSIE' 7 | ]; 8 | 9 | var enabled = true; 10 | 11 | module.exports = function(enabled) { 12 | enabled = (enabled === 'on'); 13 | 14 | return function(req, res, next) { 15 | if (req.headers['user-agent'] !== undefined && 16 | req.headers['user-agent'].indexOf(banned) !== -1 && 17 | req.headers['user-agent'].indexOf('Trident/6.0') === -1) { 18 | console.log(req.headers['user-agent']); 19 | res.end('Browser not compatible'); 20 | } 21 | else { next(); } 22 | } 23 | }; -------------------------------------------------------------------------------- /modules/pbx_bugs_solver.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This module is a middleware for express that is executed before any request is processed. 3 | * 4 | * Ideally, this shouldn't be needed but as we are hacking the Dialplan so much, I need to ignore some 5 | * requests to avoid unnecessary work and/or errors. 6 | */ 7 | var keywords = [ 8 | 'ignorame' 9 | ]; 10 | 11 | var enabled = true; 12 | var keywords_length = keywords.length; 13 | 14 | module.exports = function(enabled) { 15 | enabled = (enabled === 'on'); 16 | /** 17 | * This function, which is the only exported, will avoid any requests that contains a word stored in the 18 | * keywords array. 19 | */ 20 | return function(req, res, next) { 21 | var found = false; 22 | for (i = 0; i < keywords_length; i++){ 23 | if (req.url.indexOf(keywords[i]) !== -1){ 24 | found = true; 25 | break; 26 | } 27 | } 28 | if (found){ 29 | res.end('Ignoring...'); 30 | } 31 | else { next(); } 32 | } 33 | }; -------------------------------------------------------------------------------- /modules/refetcher.js: -------------------------------------------------------------------------------- 1 | module.exports = function Refetcher(app, mysql_client, io) { 2 | /** 3 | * This is the function that calls all reseters from different modules 4 | * 5 | * @param necessary indicates if the panel should be restarted. 6 | */ 7 | this.perform = function (){ 8 | var self = this; 9 | 10 | console.log('Starting reload'); 11 | 12 | app.init(function(){ 13 | console.log('All done!'); 14 | setTimeout(function(){ 15 | io.sockets.emit('reload', {}); 16 | }, 5000); 17 | }); 18 | }; 19 | }; -------------------------------------------------------------------------------- /modules/reseter.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This function Resets stats for all stuff. 3 | * 4 | * @param app that holds the objects /modules/app.js 5 | */ 6 | module.exports = function Reseter(app){ 7 | /** 8 | * This starts all reset processes 9 | */ 10 | this.reset = function(){ 11 | this.resetClients(); 12 | }; 13 | /** 14 | * This resets client's stats. 15 | */ 16 | this.resetClients = function(){ 17 | for (var i = 0, length = app.clients.length; i < length; i++){ 18 | app.clients[i].resetData(); 19 | } 20 | }; 21 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Panel Flaix", 3 | "version": "0.5", 4 | "private": true, 5 | "author" : "Antonio Laguna Matías ", 6 | "description" : "A real-time interface to show calls, agents and queues from Asterisk", 7 | "scripts": { 8 | "start": "NODE_ENV=production forever start server.js", 9 | "test": "NODE_ENV=development node server.js" 10 | }, 11 | "dependencies": { 12 | "async" : "0.1.X", 13 | "cron" : "1.X", 14 | "express": "3.0.X", 15 | "forever" : "0.X.X", 16 | "socket.io" : "0.9.X", 17 | "mysql" : "2.0.X" 18 | }, 19 | "repository": { 20 | "type": "git", 21 | "url": "https://github.com/Belelros/JavaScript-Operator-Panel.git" 22 | }, 23 | "license": "MIT", 24 | "engines": { 25 | "node": ">=0.6" 26 | } 27 | } -------------------------------------------------------------------------------- /public/images/arrow_posts_left.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Antonio-Laguna/Asterisk-nodejs-panel/c4c49284199ad2c5159f585922d08fb025631b6e/public/images/arrow_posts_left.png -------------------------------------------------------------------------------- /public/images/arrow_posts_right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Antonio-Laguna/Asterisk-nodejs-panel/c4c49284199ad2c5159f585922d08fb025631b6e/public/images/arrow_posts_right.png -------------------------------------------------------------------------------- /public/images/asc.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Antonio-Laguna/Asterisk-nodejs-panel/c4c49284199ad2c5159f585922d08fb025631b6e/public/images/asc.gif -------------------------------------------------------------------------------- /public/images/asc_light.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Antonio-Laguna/Asterisk-nodejs-panel/c4c49284199ad2c5159f585922d08fb025631b6e/public/images/asc_light.gif -------------------------------------------------------------------------------- /public/images/bg_time_head.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Antonio-Laguna/Asterisk-nodejs-panel/c4c49284199ad2c5159f585922d08fb025631b6e/public/images/bg_time_head.gif -------------------------------------------------------------------------------- /public/images/desc.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Antonio-Laguna/Asterisk-nodejs-panel/c4c49284199ad2c5159f585922d08fb025631b6e/public/images/desc.gif -------------------------------------------------------------------------------- /public/images/desc_light.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Antonio-Laguna/Asterisk-nodejs-panel/c4c49284199ad2c5159f585922d08fb025631b6e/public/images/desc_light.gif -------------------------------------------------------------------------------- /public/images/dots_devider_v.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Antonio-Laguna/Asterisk-nodejs-panel/c4c49284199ad2c5159f585922d08fb025631b6e/public/images/dots_devider_v.gif -------------------------------------------------------------------------------- /public/images/glyphicons-halflings-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Antonio-Laguna/Asterisk-nodejs-panel/c4c49284199ad2c5159f585922d08fb025631b6e/public/images/glyphicons-halflings-white.png -------------------------------------------------------------------------------- /public/images/glyphicons-halflings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Antonio-Laguna/Asterisk-nodejs-panel/c4c49284199ad2c5159f585922d08fb025631b6e/public/images/glyphicons-halflings.png -------------------------------------------------------------------------------- /public/images/misc/Thumbs.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Antonio-Laguna/Asterisk-nodejs-panel/c4c49284199ad2c5159f585922d08fb025631b6e/public/images/misc/Thumbs.db -------------------------------------------------------------------------------- /public/images/misc/accenture-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Antonio-Laguna/Asterisk-nodejs-panel/c4c49284199ad2c5159f585922d08fb025631b6e/public/images/misc/accenture-logo.png -------------------------------------------------------------------------------- /public/images/misc/button-gloss.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Antonio-Laguna/Asterisk-nodejs-panel/c4c49284199ad2c5159f585922d08fb025631b6e/public/images/misc/button-gloss.png -------------------------------------------------------------------------------- /public/images/misc/button-overlay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Antonio-Laguna/Asterisk-nodejs-panel/c4c49284199ad2c5159f585922d08fb025631b6e/public/images/misc/button-overlay.png -------------------------------------------------------------------------------- /public/images/misc/carbon_fibre_v2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Antonio-Laguna/Asterisk-nodejs-panel/c4c49284199ad2c5159f585922d08fb025631b6e/public/images/misc/carbon_fibre_v2.png -------------------------------------------------------------------------------- /public/images/misc/custom-form-sprites.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Antonio-Laguna/Asterisk-nodejs-panel/c4c49284199ad2c5159f585922d08fb025631b6e/public/images/misc/custom-form-sprites.png -------------------------------------------------------------------------------- /public/images/misc/input-bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Antonio-Laguna/Asterisk-nodejs-panel/c4c49284199ad2c5159f585922d08fb025631b6e/public/images/misc/input-bg.png -------------------------------------------------------------------------------- /public/images/misc/modal-gloss.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Antonio-Laguna/Asterisk-nodejs-panel/c4c49284199ad2c5159f585922d08fb025631b6e/public/images/misc/modal-gloss.png -------------------------------------------------------------------------------- /public/images/misc/table-sorter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Antonio-Laguna/Asterisk-nodejs-panel/c4c49284199ad2c5159f585922d08fb025631b6e/public/images/misc/table-sorter.png -------------------------------------------------------------------------------- /public/images/orbit/bullets.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Antonio-Laguna/Asterisk-nodejs-panel/c4c49284199ad2c5159f585922d08fb025631b6e/public/images/orbit/bullets.jpg -------------------------------------------------------------------------------- /public/images/orbit/left-arrow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Antonio-Laguna/Asterisk-nodejs-panel/c4c49284199ad2c5159f585922d08fb025631b6e/public/images/orbit/left-arrow.png -------------------------------------------------------------------------------- /public/images/orbit/loading.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Antonio-Laguna/Asterisk-nodejs-panel/c4c49284199ad2c5159f585922d08fb025631b6e/public/images/orbit/loading.gif -------------------------------------------------------------------------------- /public/images/orbit/mask-black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Antonio-Laguna/Asterisk-nodejs-panel/c4c49284199ad2c5159f585922d08fb025631b6e/public/images/orbit/mask-black.png -------------------------------------------------------------------------------- /public/images/orbit/pause-black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Antonio-Laguna/Asterisk-nodejs-panel/c4c49284199ad2c5159f585922d08fb025631b6e/public/images/orbit/pause-black.png -------------------------------------------------------------------------------- /public/images/orbit/right-arrow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Antonio-Laguna/Asterisk-nodejs-panel/c4c49284199ad2c5159f585922d08fb025631b6e/public/images/orbit/right-arrow.png -------------------------------------------------------------------------------- /public/images/orbit/rotator-black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Antonio-Laguna/Asterisk-nodejs-panel/c4c49284199ad2c5159f585922d08fb025631b6e/public/images/orbit/rotator-black.png -------------------------------------------------------------------------------- /public/images/orbit/timer-black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Antonio-Laguna/Asterisk-nodejs-panel/c4c49284199ad2c5159f585922d08fb025631b6e/public/images/orbit/timer-black.png -------------------------------------------------------------------------------- /public/images/trashcan.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Antonio-Laguna/Asterisk-nodejs-panel/c4c49284199ad2c5159f585922d08fb025631b6e/public/images/trashcan.png -------------------------------------------------------------------------------- /public/images/widget_grad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Antonio-Laguna/Asterisk-nodejs-panel/c4c49284199ad2c5159f585922d08fb025631b6e/public/images/widget_grad.png -------------------------------------------------------------------------------- /public/javascripts/client_stat.js: -------------------------------------------------------------------------------- 1 | (function($){ 2 | function pad2(number) { 3 | return (number < 10 ? '0' : '') + number 4 | } 5 | function fixedTo (number, n) { 6 | var k = Math.pow(10, n+1); 7 | return (Math.round(number * k) / k); 8 | } 9 | 10 | function formatSeconds (seconds){ 11 | seconds = Number(seconds); 12 | 13 | var h = Math.floor(seconds / 3600) || 0; 14 | var m = Math.floor(seconds % 3600 / 60) || 0; 15 | var s = Math.floor(seconds % 3600 % 60) || 0; 16 | 17 | return ((m > 0 ? (h > 0 && m < 10 ? "00" : "") + pad2(m) + ":" : "00:") + (s < 10 ? "0" : "") + s); 18 | } 19 | 20 | var Stats = { 21 | inbound_calls : 0, 22 | failed_calls : 0, 23 | offered_calls : 0, 24 | abandoned_calls : 0, 25 | basic_route : '/stats/clients/', 26 | abandoned_sla : 0, 27 | abandoned_perc : 0, 28 | abandoned_sla_perc : 0, 29 | answered_calls : 0, 30 | answered_perc : 0, 31 | answered_sla : 0, 32 | answered_sla_perc : 0, 33 | average_response_time : 0, 34 | colors : Highcharts.getOptions().colors, 35 | categories : ['Abandoned','Answered'], 36 | per_hour : [], 37 | chart : undefined, 38 | 39 | init : function(config){ 40 | this.config = config; 41 | 42 | // Global var! 43 | if (real_time){ 44 | this.connectSocket(); 45 | } 46 | else { 47 | this.receiveData(loaded_stats); 48 | } 49 | this.bindEvents(); 50 | }, 51 | connectSocket : function(){ 52 | this.socket = io.connect(this.config.socket_address); 53 | }, 54 | bindEvents : function(){ 55 | var self = this; 56 | 57 | if (self.socket) { 58 | self.socket.on('clientStatus',self.receiveData); 59 | window.onbeforeunload = function() { 60 | return "Refreshing is unnecesary since data is fetched in real time."; 61 | } 62 | } 63 | self.config.$open_tab.on('click',self.preventEvent); 64 | self.config.$tab.toggle(self.showTab,self.hideTab); 65 | 66 | self.config. $from.datepicker({ 67 | changeMonth: true, 68 | maxDate : 0, 69 | onSelect: function( selectedDate ) { 70 | self.config.$to.datepicker( "option", "minDate", selectedDate ); 71 | } 72 | }); 73 | self.config.$to.datepicker({ 74 | defaultDate: "+1w", 75 | maxDate : 0, 76 | changeMonth: true, 77 | onSelect: function( selectedDate ) { 78 | self.config.$from.datepicker( "option", "maxDate", selectedDate ); 79 | } 80 | }); 81 | 82 | self.config.$datetime.on('click',self.seeStatsForDate); 83 | self.config.$realtime.on('click', self.seeRealtime); 84 | 85 | }, 86 | seeRealtime : function(e){ 87 | Stats.preventEvent(e); 88 | location.href = Stats.basic_route + window.client_name; 89 | }, 90 | seeStatsForDate : function(e){ 91 | Stats.preventEvent(e); 92 | var from = Stats.config.$from.datepicker("getDate"), 93 | to = Stats.config.$to.datepicker("getDate"), 94 | dates = []; 95 | 96 | if (from === null){ 97 | alert('Plase, select the dates correctly in order to get stats from those dates'); 98 | } 99 | else { 100 | if (from) { dates.push(Stats.ISODateString(from)); } 101 | if (to) { dates.push(Stats.ISODateString(to)); } 102 | 103 | location.href = Stats.basic_route + window.client_name + '/' + dates.join('/to/'); 104 | } 105 | }, 106 | preventEvent : function(e){ 107 | e.preventDefault(); 108 | }, 109 | showTab : function(){ 110 | Stats.config.$tab 111 | .stop() 112 | .animate({ 113 | right: "400px" 114 | },500, function(){ 115 | Stats.config.$inner_tab.addClass('expanded'); 116 | }); 117 | Stats.config.$panel 118 | .stop() 119 | .animate({ 120 | width: "400px", 121 | opacity: 0.8 122 | }, 500, function(){ 123 | Stats.config.$content.fadeIn('slow'); 124 | }); 125 | }, 126 | hideTab : function() { 127 | Stats.config.$content.fadeOut('slow', function() { 128 | Stats.config.$tab 129 | .stop() 130 | .animate({ 131 | right: "0" 132 | },500, function(){ 133 | Stats.config.$inner_tab.removeClass(); 134 | }); 135 | Stats.config.$panel 136 | .stop() 137 | .animate({ 138 | width: "0", 139 | opacity: 0.1 140 | }, 500); 141 | }); 142 | }, 143 | receiveData : function (data){ 144 | Stats.storeData(data); 145 | Stats.updateWeb(); 146 | Stats.tableHourUpdate(); 147 | if (Stats.chart === undefined){ 148 | Stats.chartInit(); 149 | } 150 | else{ 151 | Stats.chartUpdate(); 152 | } 153 | }, 154 | getChartData : function(){ 155 | var data = [{ 156 | y: parseFloat(Stats.abandoned_perc,10), 157 | color: Stats.colors[1], 158 | drilldown: { 159 | name: 'Abandoned', 160 | categories: ['Abandoned in SLA', 'Abandoned out of SLA'], 161 | data: [ 162 | parseFloat(parseFloat(Stats.abandoned_sla_perc,10).toFixed(1),10), 163 | parseFloat(parseFloat((100 - Stats.abandoned_sla_perc),10).toFixed(1),10) 164 | ], 165 | color: Stats.colors[0] 166 | } 167 | }, { 168 | y: parseFloat(Stats.answered_perc,10), 169 | color: Stats.colors[2], 170 | drilldown: { 171 | name: 'Answered', 172 | categories: ['Answered in SLA', 'Answered out of SLA'], 173 | data: [ 174 | parseFloat(parseFloat(Stats.answered_sla_perc,10).toFixed(1),10), 175 | parseFloat(parseFloat((100 - Stats.answered_sla_perc),10).toFixed(1),10) 176 | ], 177 | color: Stats.colors[2] 178 | } 179 | }]; 180 | 181 | var total_data = []; 182 | var sla_data = []; 183 | for (var i = 0; i < data.length; i++) { 184 | total_data.push({ 185 | name: Stats.categories[i], 186 | y: data[i].y, 187 | color: data[i].color 188 | }); 189 | for (var j = 0; j < data[i].drilldown.data.length; j++) { 190 | var brightness = 0.2 - (j / data[i].drilldown.data.length) / 5 ; 191 | sla_data.push({ 192 | name: data[i].drilldown.categories[j], 193 | y: data[i].drilldown.data[j], 194 | color: Highcharts.Color(data[i].color).brighten(brightness).get() 195 | }); 196 | } 197 | } 198 | return { 199 | total : total_data, 200 | sla : sla_data 201 | }; 202 | }, 203 | chartInit : function(){ 204 | var data = Stats.getChartData(); 205 | 206 | Stats.chart = chart = new Highcharts.Chart({ 207 | chart: { 208 | renderTo: 'graph-calls', 209 | backgroundColor : '#707275', 210 | type: 'pie' 211 | }, 212 | title: { 213 | text: '' 214 | }, 215 | yAxis: { 216 | title: { 217 | text: '' 218 | } 219 | }, 220 | plotOptions: { 221 | pie: { 222 | shadow: false 223 | } 224 | }, 225 | tooltip: { 226 | formatter: function() { 227 | return ''+ this.point.name +': '+ this.y +' %'; 228 | } 229 | }, 230 | series: [{ 231 | name: 'Total', 232 | data: data.total, 233 | size: '60%', 234 | dataLabels: { 235 | formatter: function() { 236 | return this.y > 5 ? this.point.name : null; 237 | }, 238 | color: 'white', 239 | distance: -30 240 | } 241 | }, { 242 | name: 'Sla', 243 | data: data.sla, 244 | innerSize: '60%', 245 | dataLabels: { 246 | formatter: function() { 247 | // display only if larger than 1 248 | return this.y > 1 ? ''+ this.point.name +': '+ this.y +'%' : null; 249 | }, 250 | color: 'white' 251 | } 252 | }] 253 | }); 254 | }, 255 | chartUpdate : function(){ 256 | var data = Stats.getChartData(); 257 | 258 | Stats.chart.series[0].setData(data.total,true); 259 | Stats.chart.series[1].setData(data.sla,true); 260 | }, 261 | tableHourUpdate : function(){ 262 | var html = ''; 263 | for (var i = 0, length = this.per_hour.length; i < length; i++){ 264 | var service_level = this.per_hour[i].service_level === 'NaN' ? '-' : this.per_hour[i].service_level; 265 | html += '' + 266 | ''+ this.per_hour[i].hour_range +'' + 267 | ''+ this.per_hour[i].answered +'' + 268 | ''+ this.per_hour[i].abandoned +'' + 269 | ''+ service_level +' %' + 270 | ''+ this.per_hour[i].abandon_rate +' %' + 271 | ''+ this.per_hour[i].answered_time +'' + 272 | ''+ this.per_hour[i].abandoned_time +'' + 273 | ''; 274 | } 275 | Stats.config.$stats_table.html(html); 276 | }, 277 | storeData : function(data){ 278 | this.inbound_calls = data.total_calls; 279 | this.failed_calls = data.failed_calls; 280 | this.offered_calls = data.total_offered_calls; 281 | this.abandoned_calls = data.total_abandoned; 282 | this.answered_calls = data.total_answered; 283 | this.abandoned_sla = data.abandoned_after_SLA; 284 | this.answered_sla = data.answered_before_SLA; 285 | this.average_response_time = data.average_response_time; 286 | this.abandoned_sla_perc = Stats.calculatePercent('abandoned'); 287 | this.answered_sla_perc = Stats.calculatePercent('answered'); 288 | this.parseHourStats(data.per_hour); 289 | }, 290 | parseHourStats : function (per_hour){ 291 | this.per_hour = []; 292 | 293 | if (per_hour) { 294 | for (var i = 0, len = 24; i < len; i++){ 295 | if (per_hour[i]){ 296 | var hour_range = [[pad2(i),'00'].join(':'), [pad2(i+1),'00'].join(':')].join(' - '), 297 | total_calls = per_hour[i].abandoned + per_hour[i].answered, 298 | abandoned_time = formatSeconds(per_hour[i].abandoned_time / per_hour[i].abandoned), 299 | answered_time = formatSeconds(per_hour[i].answered_time / per_hour[i].answered); 300 | 301 | this.per_hour.push({ 302 | hour_range : hour_range, 303 | abandoned : per_hour[i].abandoned, 304 | abandoned_time : abandoned_time, 305 | answered : per_hour[i].answered, 306 | answered_time : answered_time, 307 | service_level : ((per_hour[i].answered_sla * 100) / per_hour[i].answered).toFixed(1), 308 | abandon_rate : ((per_hour[i].abandoned * 100) / total_calls).toFixed(1) 309 | }); 310 | } 311 | } 312 | } 313 | }, 314 | updateWeb : function() { 315 | Stats.config.sla_abandoned.text(parseFloat(Stats.abandoned_sla_perc,10).toFixed(1) + ' %'); 316 | Stats.config.sla_abandoned.removeClass(); 317 | Stats.config.sla_abandoned.addClass('number ' + 318 | Stats.determineColor(Stats.abandoned_sla_perc, Stats.config.perc_abandoned, true) 319 | ); 320 | 321 | Stats.config.sla_answered.text(parseFloat(Stats.answered_sla_perc,10).toFixed(1) + ' %'); 322 | Stats.config.sla_answered.removeClass(); 323 | Stats.config.sla_answered.addClass('number ' + 324 | Stats.determineColor(Stats.answered_sla_perc, Stats.config.perc_answered) 325 | ); 326 | 327 | Stats.config.average_response_time_row 328 | .text(Stats.average_response_time || '0') 329 | .removeClass() 330 | .addClass('number ' + Stats.determineColor(Stats.average_response_time, Stats.config.sec_answered, true)); 331 | 332 | Stats.config.inbound_calls_row.text(Stats.inbound_calls); 333 | Stats.config.failed_calls_row.text(Stats.failed_calls); 334 | Stats.config.kpi_abandoned_row.text(Stats.abandoned_perc || '0' + ' %'); 335 | 336 | Stats.config.offered_calls_row.text(Stats.offered_calls); 337 | 338 | Stats.config.abandoned_calls_row.text(Stats.abandoned_calls); 339 | Stats.config.answered_calls_row.text(Stats.answered_calls); 340 | Stats.config.abandoned_sla_row.text(Stats.abandoned_sla); 341 | Stats.config.answered_sla_row.text(Stats.answered_sla); 342 | }, 343 | logData : function(data){ 344 | console.log(data); 345 | }, 346 | ISODateString : function (d) { 347 | function pad(n){ 348 | return n < 10 ? '0'+ n : n 349 | } 350 | return d.getUTCFullYear()+'-' 351 | + pad(d.getMonth()+1)+'-' 352 | + pad(d.getDate())+'T' 353 | + pad(d.getHours())+':' 354 | + pad(d.getMinutes())+':' 355 | + pad(d.getSeconds())+'Z' 356 | }, 357 | calculatePercent : function(which) { 358 | var total, amount; 359 | 360 | if (which === 'abandoned'){ 361 | total = Stats.offered_calls; 362 | amount = Stats[which + '_sla']; 363 | Stats[which + '_perc'] = ((Stats.abandoned_calls * 100) / Stats.offered_calls).toFixed(1); 364 | } 365 | else { 366 | total = Stats[which + '_calls']; 367 | amount = Stats[which + '_sla']; 368 | Stats[which + '_perc'] = ((total * 100) / Stats.offered_calls).toFixed(1); 369 | } 370 | 371 | 372 | var result = (amount * 100) / total; 373 | return isNaN(result) ? 100 : result.toFixed(1); 374 | }, 375 | determineColor : function (amount, target, lesser) { 376 | var color = ''; 377 | 378 | if (lesser){ 379 | if (amount > target) { 380 | color = 'red' 381 | } 382 | else { 383 | if (amount >= (target -2)){ 384 | color = 'orange'; 385 | } 386 | else { 387 | color = 'green'; 388 | } 389 | } 390 | } 391 | else { 392 | if (amount < target) { 393 | color = 'red' 394 | } 395 | else { 396 | if (amount <= (target -2)){ 397 | color = 'orange'; 398 | } 399 | else { 400 | color = 'green'; 401 | } 402 | } 403 | } 404 | 405 | return color; 406 | } 407 | }; 408 | 409 | Stats.init({ 410 | socket_address : 'http://170.251.100.90:8080/' + client_name, 411 | perc_abandoned : parseInt(perc_abandoned, 10), 412 | perc_answered : parseInt(perc_answered, 10), 413 | sec_answered : parseInt(sec_answered,10), 414 | offered_calls_row : $('td#offered-calls'), 415 | inbound_calls_row : $('td#inbound-calls'), 416 | failed_calls_row : $('td#failed-calls'), 417 | abandoned_calls_row : $('td#abandoned-calls'), 418 | abandoned_sla_row : $('td#abandoned-sla'), 419 | answered_calls_row : $('td#answered-calls'), 420 | answered_sla_row : $('td#answered-sla'), 421 | average_response_time_row : $('td#average-response'), 422 | sla_abandoned : $('td#sla-abandoned'), 423 | sla_answered : $('td#sla-answering'), 424 | kpi_abandoned_row : $('td#kpi-abandoned'), 425 | $content : $(".content").hide(), 426 | $open_tab : $('a#open-tab'), 427 | $tab : $('#tab'), 428 | $inner_tab : $('#inner_tab'), 429 | $panel : $('#panel'), 430 | $from : $('input#from'), 431 | $to : $('input#to'), 432 | $datetime : $('button#datetime'), 433 | $realtime : $('button#realtime'), 434 | $stats_table : $('table#stats-table').find('tbody') 435 | }); 436 | })(jQuery); -------------------------------------------------------------------------------- /public/javascripts/jquery-ui-1.8.17.custom.min.js: -------------------------------------------------------------------------------- 1 | /* 2 | * jQuery UI Effects 1.8.17 3 | * 4 | * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about) 5 | * Dual licensed under the MIT or GPL Version 2 licenses. 6 | * http://jquery.org/license 7 | * 8 | * http://docs.jquery.com/UI/Effects/ 9 | */jQuery.effects||function(a,b){function l(b){if(!b||typeof b=="number"||a.fx.speeds[b])return!0;if(typeof b=="string"&&!a.effects[b])return!0;return!1}function k(b,c,d,e){typeof b=="object"&&(e=c,d=null,c=b,b=c.effect),a.isFunction(c)&&(e=c,d=null,c={});if(typeof c=="number"||a.fx.speeds[c])e=d,d=c,c={};a.isFunction(d)&&(e=d,d=null),c=c||{},d=d||c.duration,d=a.fx.off?0:typeof d=="number"?d:d in a.fx.speeds?a.fx.speeds[d]:a.fx.speeds._default,e=e||c.complete;return[b,c,d,e]}function j(a,b){var c={_:0},d;for(d in b)a[d]!=b[d]&&(c[d]=b[d]);return c}function i(b){var c,d;for(c in b)d=b[c],(d==null||a.isFunction(d)||c in g||/scrollbar/.test(c)||!/color/i.test(c)&&isNaN(parseFloat(d)))&&delete b[c];return b}function h(){var a=document.defaultView?document.defaultView.getComputedStyle(this,null):this.currentStyle,b={},c,d;if(a&&a.length&&a[0]&&a[a[0]]){var e=a.length;while(e--)c=a[e],typeof a[c]=="string"&&(d=c.replace(/\-(\w)/g,function(a,b){return b.toUpperCase()}),b[d]=a[c])}else for(c in a)typeof a[c]=="string"&&(b[c]=a[c]);return b}function d(b,d){var e;do{e=a.curCSS(b,d);if(e!=""&&e!="transparent"||a.nodeName(b,"body"))break;d="backgroundColor"}while(b=b.parentNode);return c(e)}function c(b){var c;if(b&&b.constructor==Array&&b.length==3)return b;if(c=/rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/.exec(b))return[parseInt(c[1],10),parseInt(c[2],10),parseInt(c[3],10)];if(c=/rgb\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*\)/.exec(b))return[parseFloat(c[1])*2.55,parseFloat(c[2])*2.55,parseFloat(c[3])*2.55];if(c=/#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/.exec(b))return[parseInt(c[1],16),parseInt(c[2],16),parseInt(c[3],16)];if(c=/#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])/.exec(b))return[parseInt(c[1]+c[1],16),parseInt(c[2]+c[2],16),parseInt(c[3]+c[3],16)];if(c=/rgba\(0, 0, 0, 0\)/.exec(b))return e.transparent;return e[a.trim(b).toLowerCase()]}a.effects={},a.each(["backgroundColor","borderBottomColor","borderLeftColor","borderRightColor","borderTopColor","borderColor","color","outlineColor"],function(b,e){a.fx.step[e]=function(a){a.colorInit||(a.start=d(a.elem,e),a.end=c(a.end),a.colorInit=!0),a.elem.style[e]="rgb("+Math.max(Math.min(parseInt(a.pos*(a.end[0]-a.start[0])+a.start[0],10),255),0)+","+Math.max(Math.min(parseInt(a.pos*(a.end[1]-a.start[1])+a.start[1],10),255),0)+","+Math.max(Math.min(parseInt(a.pos*(a.end[2]-a.start[2])+a.start[2],10),255),0)+")"}});var e={aqua:[0,255,255],azure:[240,255,255],beige:[245,245,220],black:[0,0,0],blue:[0,0,255],brown:[165,42,42],cyan:[0,255,255],darkblue:[0,0,139],darkcyan:[0,139,139],darkgrey:[169,169,169],darkgreen:[0,100,0],darkkhaki:[189,183,107],darkmagenta:[139,0,139],darkolivegreen:[85,107,47],darkorange:[255,140,0],darkorchid:[153,50,204],darkred:[139,0,0],darksalmon:[233,150,122],darkviolet:[148,0,211],fuchsia:[255,0,255],gold:[255,215,0],green:[0,128,0],indigo:[75,0,130],khaki:[240,230,140],lightblue:[173,216,230],lightcyan:[224,255,255],lightgreen:[144,238,144],lightgrey:[211,211,211],lightpink:[255,182,193],lightyellow:[255,255,224],lime:[0,255,0],magenta:[255,0,255],maroon:[128,0,0],navy:[0,0,128],olive:[128,128,0],orange:[255,165,0],pink:[255,192,203],purple:[128,0,128],violet:[128,0,128],red:[255,0,0],silver:[192,192,192],white:[255,255,255],yellow:[255,255,0],transparent:[255,255,255]},f=["add","remove","toggle"],g={border:1,borderBottom:1,borderColor:1,borderLeft:1,borderRight:1,borderTop:1,borderWidth:1,margin:1,padding:1};a.effects.animateClass=function(b,c,d,e){a.isFunction(d)&&(e=d,d=null);return this.queue(function(){var g=a(this),k=g.attr("style")||" ",l=i(h.call(this)),m,n=g.attr("class");a.each(f,function(a,c){b[c]&&g[c+"Class"](b[c])}),m=i(h.call(this)),g.attr("class",n),g.animate(j(l,m),{queue:!1,duration:c,easing:d,complete:function(){a.each(f,function(a,c){b[c]&&g[c+"Class"](b[c])}),typeof g.attr("style")=="object"?(g.attr("style").cssText="",g.attr("style").cssText=k):g.attr("style",k),e&&e.apply(this,arguments),a.dequeue(this)}})})},a.fn.extend({_addClass:a.fn.addClass,addClass:function(b,c,d,e){return c?a.effects.animateClass.apply(this,[{add:b},c,d,e]):this._addClass(b)},_removeClass:a.fn.removeClass,removeClass:function(b,c,d,e){return c?a.effects.animateClass.apply(this,[{remove:b},c,d,e]):this._removeClass(b)},_toggleClass:a.fn.toggleClass,toggleClass:function(c,d,e,f,g){return typeof d=="boolean"||d===b?e?a.effects.animateClass.apply(this,[d?{add:c}:{remove:c},e,f,g]):this._toggleClass(c,d):a.effects.animateClass.apply(this,[{toggle:c},d,e,f])},switchClass:function(b,c,d,e,f){return a.effects.animateClass.apply(this,[{add:c,remove:b},d,e,f])}}),a.extend(a.effects,{version:"1.8.17",save:function(a,b){for(var c=0;c").addClass("ui-effects-wrapper").css({fontSize:"100%",background:"transparent",border:"none",margin:0,padding:0}),e=document.activeElement;b.wrap(d),(b[0]===e||a.contains(b[0],e))&&a(e).focus(),d=b.parent(),b.css("position")=="static"?(d.css({position:"relative"}),b.css({position:"relative"})):(a.extend(c,{position:b.css("position"),zIndex:b.css("z-index")}),a.each(["top","left","bottom","right"],function(a,d){c[d]=b.css(d),isNaN(parseInt(c[d],10))&&(c[d]="auto")}),b.css({position:"relative",top:0,left:0,right:"auto",bottom:"auto"}));return d.css(c).show()},removeWrapper:function(b){var c,d=document.activeElement;if(b.parent().is(".ui-effects-wrapper")){c=b.parent().replaceWith(b),(b[0]===d||a.contains(b[0],d))&&a(d).focus();return c}return b},setTransition:function(b,c,d,e){e=e||{},a.each(c,function(a,c){unit=b.cssUnit(c),unit[0]>0&&(e[c]=unit[0]*d+unit[1])});return e}}),a.fn.extend({effect:function(b,c,d,e){var f=k.apply(this,arguments),g={options:f[1],duration:f[2],callback:f[3]},h=g.options.mode,i=a.effects[b];if(a.fx.off||!i)return h?this[h](g.duration,g.callback):this.each(function(){g.callback&&g.callback.call(this)});return i.call(this,g)},_show:a.fn.show,show:function(a){if(l(a))return this._show.apply(this,arguments);var b=k.apply(this,arguments);b[1].mode="show";return this.effect.apply(this,b)},_hide:a.fn.hide,hide:function(a){if(l(a))return this._hide.apply(this,arguments);var b=k.apply(this,arguments);b[1].mode="hide";return this.effect.apply(this,b)},__toggle:a.fn.toggle,toggle:function(b){if(l(b)||typeof b=="boolean"||a.isFunction(b))return this.__toggle.apply(this,arguments);var c=k.apply(this,arguments);c[1].mode="toggle";return this.effect.apply(this,c)},cssUnit:function(b){var c=this.css(b),d=[];a.each(["em","px","%","pt"],function(a,b){c.indexOf(b)>0&&(d=[parseFloat(c),b])});return d}}),a.easing.jswing=a.easing.swing,a.extend(a.easing,{def:"easeOutQuad",swing:function(b,c,d,e,f){return a.easing[a.easing.def](b,c,d,e,f)},easeInQuad:function(a,b,c,d,e){return d*(b/=e)*b+c},easeOutQuad:function(a,b,c,d,e){return-d*(b/=e)*(b-2)+c},easeInOutQuad:function(a,b,c,d,e){if((b/=e/2)<1)return d/2*b*b+c;return-d/2*(--b*(b-2)-1)+c},easeInCubic:function(a,b,c,d,e){return d*(b/=e)*b*b+c},easeOutCubic:function(a,b,c,d,e){return d*((b=b/e-1)*b*b+1)+c},easeInOutCubic:function(a,b,c,d,e){if((b/=e/2)<1)return d/2*b*b*b+c;return d/2*((b-=2)*b*b+2)+c},easeInQuart:function(a,b,c,d,e){return d*(b/=e)*b*b*b+c},easeOutQuart:function(a,b,c,d,e){return-d*((b=b/e-1)*b*b*b-1)+c},easeInOutQuart:function(a,b,c,d,e){if((b/=e/2)<1)return d/2*b*b*b*b+c;return-d/2*((b-=2)*b*b*b-2)+c},easeInQuint:function(a,b,c,d,e){return d*(b/=e)*b*b*b*b+c},easeOutQuint:function(a,b,c,d,e){return d*((b=b/e-1)*b*b*b*b+1)+c},easeInOutQuint:function(a,b,c,d,e){if((b/=e/2)<1)return d/2*b*b*b*b*b+c;return d/2*((b-=2)*b*b*b*b+2)+c},easeInSine:function(a,b,c,d,e){return-d*Math.cos(b/e*(Math.PI/2))+d+c},easeOutSine:function(a,b,c,d,e){return d*Math.sin(b/e*(Math.PI/2))+c},easeInOutSine:function(a,b,c,d,e){return-d/2*(Math.cos(Math.PI*b/e)-1)+c},easeInExpo:function(a,b,c,d,e){return b==0?c:d*Math.pow(2,10*(b/e-1))+c},easeOutExpo:function(a,b,c,d,e){return b==e?c+d:d*(-Math.pow(2,-10*b/e)+1)+c},easeInOutExpo:function(a,b,c,d,e){if(b==0)return c;if(b==e)return c+d;if((b/=e/2)<1)return d/2*Math.pow(2,10*(b-1))+c;return d/2*(-Math.pow(2,-10*--b)+2)+c},easeInCirc:function(a,b,c,d,e){return-d*(Math.sqrt(1-(b/=e)*b)-1)+c},easeOutCirc:function(a,b,c,d,e){return d*Math.sqrt(1-(b=b/e-1)*b)+c},easeInOutCirc:function(a,b,c,d,e){if((b/=e/2)<1)return-d/2*(Math.sqrt(1-b*b)-1)+c;return d/2*(Math.sqrt(1-(b-=2)*b)+1)+c},easeInElastic:function(a,b,c,d,e){var f=1.70158,g=0,h=d;if(b==0)return c;if((b/=e)==1)return c+d;g||(g=e*.3);if(h');var tiptip_content=$('
');var tiptip_arrow=$('
');$("body").append(tiptip_holder.html(tiptip_content).prepend(tiptip_arrow.html('
')))}else{var tiptip_holder=$("#tiptip_holder");var tiptip_content=$("#tiptip_content");var tiptip_arrow=$("#tiptip_arrow")}return this.each(function(){var org_elem=$(this);if(opts.content){var org_title=opts.content}else{var org_title=org_elem.attr(opts.attribute)}if(org_title!=""){if(!opts.content){org_elem.removeAttr(opts.attribute)}var timeout=false;if(opts.activation=="hover"){org_elem.hover(function(){active_tiptip()},function(){if(!opts.keepAlive){deactive_tiptip()}});if(opts.keepAlive){tiptip_holder.hover(function(){},function(){deactive_tiptip()})}}else if(opts.activation=="focus"){org_elem.focus(function(){active_tiptip()}).blur(function(){deactive_tiptip()})}else if(opts.activation=="click"){org_elem.click(function(){active_tiptip();return false}).hover(function(){},function(){if(!opts.keepAlive){deactive_tiptip()}});if(opts.keepAlive){tiptip_holder.hover(function(){},function(){deactive_tiptip()})}}function active_tiptip(){opts.enter.call(this);tiptip_content.html(org_title);tiptip_holder.hide().removeAttr("class").css("margin","0");tiptip_arrow.removeAttr("style");var top=parseInt(org_elem.offset()['top']);var left=parseInt(org_elem.offset()['left']);var org_width=parseInt(org_elem.outerWidth());var org_height=parseInt(org_elem.outerHeight());var tip_w=tiptip_holder.outerWidth();var tip_h=tiptip_holder.outerHeight();var w_compare=Math.round((org_width-tip_w)/2);var h_compare=Math.round((org_height-tip_h)/2);var marg_left=Math.round(left+w_compare);var marg_top=Math.round(top+org_height+opts.edgeOffset);var t_class="";var arrow_top="";var arrow_left=Math.round(tip_w-12)/2;if(opts.defaultPosition=="bottom"){t_class="_bottom"}else if(opts.defaultPosition=="top"){t_class="_top"}else if(opts.defaultPosition=="left"){t_class="_left"}else if(opts.defaultPosition=="right"){t_class="_right"}var right_compare=(w_compare+left)parseInt($(window).width());if((right_compare&&w_compare<0)||(t_class=="_right"&&!left_compare)||(t_class=="_left"&&left<(tip_w+opts.edgeOffset+5))){t_class="_right";arrow_top=Math.round(tip_h-13)/2;arrow_left=-12;marg_left=Math.round(left+org_width+opts.edgeOffset);marg_top=Math.round(top+h_compare)}else if((left_compare&&w_compare<0)||(t_class=="_left"&&!right_compare)){t_class="_left";arrow_top=Math.round(tip_h-13)/2;arrow_left=Math.round(tip_w);marg_left=Math.round(left-(tip_w+opts.edgeOffset+5));marg_top=Math.round(top+h_compare)}var top_compare=(top+org_height+opts.edgeOffset+tip_h+8)>parseInt($(window).height()+$(window).scrollTop());var bottom_compare=((top+org_height)-(opts.edgeOffset+tip_h+8))<0;if(top_compare||(t_class=="_bottom"&&top_compare)||(t_class=="_top"&&!bottom_compare)){if(t_class=="_top"||t_class=="_bottom"){t_class="_top"}else{t_class=t_class+"_top"}arrow_top=tip_h;marg_top=Math.round(top-(tip_h+5+opts.edgeOffset))}else if(bottom_compare|(t_class=="_top"&&bottom_compare)||(t_class=="_bottom"&&!top_compare)){if(t_class=="_top"||t_class=="_bottom"){t_class="_bottom"}else{t_class=t_class+"_bottom"}arrow_top=-12;marg_top=Math.round(top+org_height+opts.edgeOffset)}if(t_class=="_right_top"||t_class=="_left_top"){marg_top=marg_top+5}else if(t_class=="_right_bottom"||t_class=="_left_bottom"){marg_top=marg_top-5}if(t_class=="_left_top"||t_class=="_left_bottom"){marg_left=marg_left+5}tiptip_arrow.css({"margin-left":arrow_left+"px","margin-top":arrow_top+"px"});tiptip_holder.css({"margin-left":marg_left+"px","margin-top":marg_top+"px"}).attr("class","tip"+t_class);if(timeout){clearTimeout(timeout)}timeout=setTimeout(function(){tiptip_holder.stop(true,true).fadeIn(opts.fadeIn)},opts.delay)}function deactive_tiptip(){opts.exit.call(this);if(timeout){clearTimeout(timeout)}tiptip_holder.fadeOut(opts.fadeOut)}}})}})(jQuery); -------------------------------------------------------------------------------- /public/stylesheets/ie.css: -------------------------------------------------------------------------------- 1 | /* Foundation v2.1.4 http://foundation.zurb.com */ 2 | /* This is for all IE specfific style less than IE9. We hate IE. */ 3 | 4 | div.panel { border: 1px solid #ccc; } 5 | .lt-ie8 .nav-bar li.has-flyout a { padding-right: 20px; } 6 | .lt-ie8 .nav-bar li.has-flyout a:after { border-top: none; } 7 | -------------------------------------------------------------------------------- /public/stylesheets/images/ui-bg_flat_30_cccccc_40x100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Antonio-Laguna/Asterisk-nodejs-panel/c4c49284199ad2c5159f585922d08fb025631b6e/public/stylesheets/images/ui-bg_flat_30_cccccc_40x100.png -------------------------------------------------------------------------------- /public/stylesheets/images/ui-bg_flat_50_5c5c5c_40x100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Antonio-Laguna/Asterisk-nodejs-panel/c4c49284199ad2c5159f585922d08fb025631b6e/public/stylesheets/images/ui-bg_flat_50_5c5c5c_40x100.png -------------------------------------------------------------------------------- /public/stylesheets/images/ui-bg_glass_20_555555_1x400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Antonio-Laguna/Asterisk-nodejs-panel/c4c49284199ad2c5159f585922d08fb025631b6e/public/stylesheets/images/ui-bg_glass_20_555555_1x400.png -------------------------------------------------------------------------------- /public/stylesheets/images/ui-bg_glass_40_0078a3_1x400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Antonio-Laguna/Asterisk-nodejs-panel/c4c49284199ad2c5159f585922d08fb025631b6e/public/stylesheets/images/ui-bg_glass_40_0078a3_1x400.png -------------------------------------------------------------------------------- /public/stylesheets/images/ui-bg_glass_40_ffc73d_1x400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Antonio-Laguna/Asterisk-nodejs-panel/c4c49284199ad2c5159f585922d08fb025631b6e/public/stylesheets/images/ui-bg_glass_40_ffc73d_1x400.png -------------------------------------------------------------------------------- /public/stylesheets/images/ui-bg_gloss-wave_25_333333_500x100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Antonio-Laguna/Asterisk-nodejs-panel/c4c49284199ad2c5159f585922d08fb025631b6e/public/stylesheets/images/ui-bg_gloss-wave_25_333333_500x100.png -------------------------------------------------------------------------------- /public/stylesheets/images/ui-bg_highlight-soft_80_eeeeee_1x100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Antonio-Laguna/Asterisk-nodejs-panel/c4c49284199ad2c5159f585922d08fb025631b6e/public/stylesheets/images/ui-bg_highlight-soft_80_eeeeee_1x100.png -------------------------------------------------------------------------------- /public/stylesheets/images/ui-bg_inset-soft_25_000000_1x100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Antonio-Laguna/Asterisk-nodejs-panel/c4c49284199ad2c5159f585922d08fb025631b6e/public/stylesheets/images/ui-bg_inset-soft_25_000000_1x100.png -------------------------------------------------------------------------------- /public/stylesheets/images/ui-bg_inset-soft_30_f58400_1x100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Antonio-Laguna/Asterisk-nodejs-panel/c4c49284199ad2c5159f585922d08fb025631b6e/public/stylesheets/images/ui-bg_inset-soft_30_f58400_1x100.png -------------------------------------------------------------------------------- /public/stylesheets/images/ui-icons_222222_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Antonio-Laguna/Asterisk-nodejs-panel/c4c49284199ad2c5159f585922d08fb025631b6e/public/stylesheets/images/ui-icons_222222_256x240.png -------------------------------------------------------------------------------- /public/stylesheets/images/ui-icons_4b8e0b_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Antonio-Laguna/Asterisk-nodejs-panel/c4c49284199ad2c5159f585922d08fb025631b6e/public/stylesheets/images/ui-icons_4b8e0b_256x240.png -------------------------------------------------------------------------------- /public/stylesheets/images/ui-icons_a83300_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Antonio-Laguna/Asterisk-nodejs-panel/c4c49284199ad2c5159f585922d08fb025631b6e/public/stylesheets/images/ui-icons_a83300_256x240.png -------------------------------------------------------------------------------- /public/stylesheets/images/ui-icons_cccccc_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Antonio-Laguna/Asterisk-nodejs-panel/c4c49284199ad2c5159f585922d08fb025631b6e/public/stylesheets/images/ui-icons_cccccc_256x240.png -------------------------------------------------------------------------------- /public/stylesheets/images/ui-icons_ffffff_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Antonio-Laguna/Asterisk-nodejs-panel/c4c49284199ad2c5159f585922d08fb025631b6e/public/stylesheets/images/ui-icons_ffffff_256x240.png -------------------------------------------------------------------------------- /public/stylesheets/normalize.css: -------------------------------------------------------------------------------- 1 | /*! normalize.css v2.0.1 | MIT License | git.io/normalize */ 2 | 3 | /* ========================================================================== 4 | HTML5 display definitions 5 | ========================================================================== */ 6 | 7 | /* 8 | * Corrects `block` display not defined in IE 8/9. 9 | */ 10 | 11 | article, 12 | aside, 13 | details, 14 | figcaption, 15 | figure, 16 | footer, 17 | header, 18 | hgroup, 19 | nav, 20 | section, 21 | summary { 22 | display: block; 23 | } 24 | 25 | /* 26 | * Corrects `inline-block` display not defined in IE 8/9. 27 | */ 28 | 29 | audio, 30 | canvas, 31 | video { 32 | display: inline-block; 33 | } 34 | 35 | /* 36 | * Prevents modern browsers from displaying `audio` without controls. 37 | * Remove excess height in iOS 5 devices. 38 | */ 39 | 40 | audio:not([controls]) { 41 | display: none; 42 | height: 0; 43 | } 44 | 45 | /* 46 | * Addresses styling for `hidden` attribute not present in IE 8/9. 47 | */ 48 | 49 | [hidden] { 50 | display: none; 51 | } 52 | 53 | /* ========================================================================== 54 | Base 55 | ========================================================================== */ 56 | 57 | /* 58 | * 1. Sets default font family to sans-serif. 59 | * 2. Prevents iOS text size adjust after orientation change, without disabling 60 | * user zoom. 61 | */ 62 | 63 | html { 64 | font-family: sans-serif; /* 1 */ 65 | -webkit-text-size-adjust: 100%; /* 2 */ 66 | -ms-text-size-adjust: 100%; /* 2 */ 67 | } 68 | 69 | /* 70 | * Removes default margin. 71 | */ 72 | 73 | body { 74 | margin: 0; 75 | } 76 | 77 | /* ========================================================================== 78 | Links 79 | ========================================================================== */ 80 | 81 | /* 82 | * Addresses `outline` inconsistency between Chrome and other browsers. 83 | */ 84 | 85 | a:focus { 86 | outline: thin dotted; 87 | } 88 | 89 | /* 90 | * Improves readability when focused and also mouse hovered in all browsers. 91 | */ 92 | 93 | a:active, 94 | a:hover { 95 | outline: 0; 96 | } 97 | 98 | /* ========================================================================== 99 | Typography 100 | ========================================================================== */ 101 | 102 | /* 103 | * Addresses `h1` font sizes within `section` and `article` in Firefox 4+, 104 | * Safari 5, and Chrome. 105 | */ 106 | 107 | h1 { 108 | font-size: 2em; 109 | } 110 | 111 | /* 112 | * Addresses styling not present in IE 8/9, Safari 5, and Chrome. 113 | */ 114 | 115 | abbr[title] { 116 | border-bottom: 1px dotted; 117 | } 118 | 119 | /* 120 | * Addresses style set to `bolder` in Firefox 4+, Safari 5, and Chrome. 121 | */ 122 | 123 | b, 124 | strong { 125 | font-weight: bold; 126 | } 127 | 128 | /* 129 | * Addresses styling not present in Safari 5 and Chrome. 130 | */ 131 | 132 | dfn { 133 | font-style: italic; 134 | } 135 | 136 | /* 137 | * Addresses styling not present in IE 8/9. 138 | */ 139 | 140 | mark { 141 | background: #ff0; 142 | color: #000; 143 | } 144 | 145 | 146 | /* 147 | * Corrects font family set oddly in Safari 5 and Chrome. 148 | */ 149 | 150 | code, 151 | kbd, 152 | pre, 153 | samp { 154 | font-family: monospace, serif; 155 | font-size: 1em; 156 | } 157 | 158 | /* 159 | * Improves readability of pre-formatted text in all browsers. 160 | */ 161 | 162 | pre { 163 | white-space: pre; 164 | white-space: pre-wrap; 165 | word-wrap: break-word; 166 | } 167 | 168 | /* 169 | * Sets consistent quote types. 170 | */ 171 | 172 | q { 173 | quotes: "\201C" "\201D" "\2018" "\2019"; 174 | } 175 | 176 | /* 177 | * Addresses inconsistent and variable font size in all browsers. 178 | */ 179 | 180 | small { 181 | font-size: 80%; 182 | } 183 | 184 | /* 185 | * Prevents `sub` and `sup` affecting `line-height` in all browsers. 186 | */ 187 | 188 | sub, 189 | sup { 190 | font-size: 75%; 191 | line-height: 0; 192 | position: relative; 193 | vertical-align: baseline; 194 | } 195 | 196 | sup { 197 | top: -0.5em; 198 | } 199 | 200 | sub { 201 | bottom: -0.25em; 202 | } 203 | 204 | /* ========================================================================== 205 | Embedded content 206 | ========================================================================== */ 207 | 208 | /* 209 | * Removes border when inside `a` element in IE 8/9. 210 | */ 211 | 212 | img { 213 | border: 0; 214 | } 215 | 216 | /* 217 | * Corrects overflow displayed oddly in IE 9. 218 | */ 219 | 220 | svg:not(:root) { 221 | overflow: hidden; 222 | } 223 | 224 | /* ========================================================================== 225 | Figures 226 | ========================================================================== */ 227 | 228 | /* 229 | * Addresses margin not present in IE 8/9 and Safari 5. 230 | */ 231 | 232 | figure { 233 | margin: 0; 234 | } 235 | 236 | /* ========================================================================== 237 | Forms 238 | ========================================================================== */ 239 | 240 | /* 241 | * Define consistent border, margin, and padding. 242 | */ 243 | 244 | fieldset { 245 | border: 1px solid #c0c0c0; 246 | margin: 0 2px; 247 | padding: 0.35em 0.625em 0.75em; 248 | } 249 | 250 | /* 251 | * 1. Corrects color not being inherited in IE 8/9. 252 | * 2. Remove padding so people aren't caught out if they zero out fieldsets. 253 | */ 254 | 255 | legend { 256 | border: 0; /* 1 */ 257 | padding: 0; /* 2 */ 258 | } 259 | 260 | /* 261 | * 1. Corrects font family not being inherited in all browsers. 262 | * 2. Corrects font size not being inherited in all browsers. 263 | * 3. Addresses margins set differently in Firefox 4+, Safari 5, and Chrome 264 | */ 265 | 266 | button, 267 | input, 268 | select, 269 | textarea { 270 | font-family: inherit; /* 1 */ 271 | font-size: 100%; /* 2 */ 272 | margin: 0; /* 3 */ 273 | } 274 | 275 | /* 276 | * Addresses Firefox 4+ setting `line-height` on `input` using `!important` in 277 | * the UA stylesheet. 278 | */ 279 | 280 | button, 281 | input { 282 | line-height: normal; 283 | } 284 | 285 | /* 286 | * 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio` 287 | * and `video` controls. 288 | * 2. Corrects inability to style clickable `input` types in iOS. 289 | * 3. Improves usability and consistency of cursor style between image-type 290 | * `input` and others. 291 | */ 292 | 293 | button, 294 | html input[type="button"], /* 1 */ 295 | input[type="reset"], 296 | input[type="submit"] { 297 | -webkit-appearance: button; /* 2 */ 298 | cursor: pointer; /* 3 */ 299 | } 300 | 301 | /* 302 | * Re-set default cursor for disabled elements. 303 | */ 304 | 305 | button[disabled], 306 | input[disabled] { 307 | cursor: default; 308 | } 309 | 310 | /* 311 | * 1. Addresses box sizing set to `content-box` in IE 8/9. 312 | * 2. Removes excess padding in IE 8/9. 313 | */ 314 | 315 | input[type="checkbox"], 316 | input[type="radio"] { 317 | box-sizing: border-box; /* 1 */ 318 | padding: 0; /* 2 */ 319 | } 320 | 321 | /* 322 | * 1. Addresses `appearance` set to `searchfield` in Safari 5 and Chrome. 323 | * 2. Addresses `box-sizing` set to `border-box` in Safari 5 and Chrome 324 | * (include `-moz` to future-proof). 325 | */ 326 | 327 | input[type="search"] { 328 | -webkit-appearance: textfield; /* 1 */ 329 | -moz-box-sizing: content-box; 330 | -webkit-box-sizing: content-box; /* 2 */ 331 | box-sizing: content-box; 332 | } 333 | 334 | /* 335 | * Removes inner padding and search cancel button in Safari 5 and Chrome 336 | * on OS X. 337 | */ 338 | 339 | input[type="search"]::-webkit-search-cancel-button, 340 | input[type="search"]::-webkit-search-decoration { 341 | -webkit-appearance: none; 342 | } 343 | 344 | /* 345 | * Removes inner padding and border in Firefox 4+. 346 | */ 347 | 348 | button::-moz-focus-inner, 349 | input::-moz-focus-inner { 350 | border: 0; 351 | padding: 0; 352 | } 353 | 354 | /* 355 | * 1. Removes default vertical scrollbar in IE 8/9. 356 | * 2. Improves readability and alignment in all browsers. 357 | */ 358 | 359 | textarea { 360 | overflow: auto; /* 1 */ 361 | vertical-align: top; /* 2 */ 362 | } 363 | 364 | /* ========================================================================== 365 | Tables 366 | ========================================================================== */ 367 | 368 | /* 369 | * Remove most spacing between table cells. 370 | */ 371 | 372 | table { 373 | border-collapse: collapse; 374 | border-spacing: 0; 375 | } 376 | -------------------------------------------------------------------------------- /public/stylesheets/style.css: -------------------------------------------------------------------------------- 1 | html { 2 | background: #111314; 3 | height: 100%; 4 | } 5 | body { 6 | padding-top: 10px; 7 | height: 0; 8 | min-height: 0; 9 | } 10 | /* Colors */ 11 | .green { color: #77AB13; } 12 | .red { color: #AE432E; } 13 | .orange { color: #B5712E; } 14 | 15 | #dashboard-container { 16 | width: 960px; 17 | margin:0 auto; 18 | } 19 | .widget { 20 | background: #232526 url(/public/images/widget_grad.png) no-repeat 100% 0; 21 | box-shadow: 2px 0 3px #0d0d0d, -2px 0 3px #0d0d0d, 0 3px 3px #0d0d0d, 0 -2px 3px #0d0d0d; 22 | -moz-box-shadow: 2px 0 3px #0d0d0d, -2px 0 3px #0d0d0d, 0 3px 3px #0d0d0d, 0 -2px 3px #0d0d0d; 23 | -webkit-box-shadow: 2px 0 3px #0d0d0d, -2px 0 3px #0d0d0d, 0 3px 3px #0d0d0d, 0 -2px 3px #0d0d0d; 24 | font: 18px Arial; 25 | color: #707275; 26 | text-align: center; 27 | } 28 | .cabecera { 29 | padding: 10px; 30 | height: 70px; 31 | margin-bottom: 2%; 32 | } 33 | .cabecera img { 34 | display: block; 35 | margin: 0 auto; 36 | height: 50%; 37 | margin-bottom: 18px 38 | } 39 | .cabecera h3 { 40 | margin: 5px 0; 41 | } 42 | .widget > header { 43 | border-bottom:1px solid #151617; 44 | border-top:1px solid #323434; 45 | height:17px; 46 | overflow:hidden; 47 | padding:10px 15px 11px; 48 | line-height:100%; 49 | } 50 | .widget > header > section { 51 | float: left; 52 | color: #707275; 53 | font-size: 18px; 54 | text-transform: uppercase; 55 | text-shadow: 0 -1px 1px black; 56 | } 57 | .widget > section { 58 | border-top: 1px solid #303334; 59 | border-bottom: 1px solid #151617; 60 | padding: 0 15px; 61 | color: #707275; 62 | font-size: 12px; 63 | } 64 | .widget-body { 65 | overflow: hidden; 66 | } 67 | .sla .widget-body { 68 | min-height: 215px; 69 | } 70 | .widget-inner { 71 | padding-top: 20px; 72 | } 73 | .stats-hour { 74 | width: 100%; 75 | } 76 | .stats-hour th { 77 | text-align: center; 78 | } 79 | 80 | .tabla { 81 | float: left; 82 | margin-right: 2%; 83 | margin-bottom: 2%; 84 | } 85 | .stats { 86 | width: 30%; 87 | } 88 | .tabla td { 89 | text-align: center; 90 | } 91 | .slas-kpis { 92 | width: 68%; 93 | margin-right: 0; 94 | } 95 | .slas-kpis table { 96 | font-size: 1.2em; 97 | } 98 | .slas-kpis td, .slas-kpis th { 99 | line-height: 44px; 100 | } 101 | .sla { 102 | width: 40%; 103 | float: left; 104 | margin-bottom: 2%; 105 | } 106 | .graph { 107 | width: 100%; 108 | float: left; 109 | margin-bottom: 20px; 110 | } 111 | .graph .widget-body { 112 | padding-bottom: 15px; 113 | } 114 | .graph .widget-inner { 115 | height: 300px; 116 | } 117 | 118 | table { 119 | width: 100%; 120 | margin-bottom: 18px; 121 | color:#CCC; 122 | } 123 | table caption { 124 | color:#FFF; 125 | } 126 | table th, 127 | table td { 128 | padding: 8px; 129 | line-height: 18px; 130 | text-align: left; 131 | vertical-align: top; 132 | border-top: 1px solid #232323; 133 | } 134 | table th { 135 | font-weight: bold; 136 | } 137 | table thead th { 138 | background:#111; 139 | color:#E5E5E5; 140 | vertical-align: bottom; 141 | } 142 | table colgroup + thead tr:first-child th, 143 | table colgroup + thead tr:first-child td, 144 | table thead:first-child tr:first-child th, 145 | table thead:first-child tr:first-child td { 146 | border-top: 0; 147 | } 148 | table tbody + tbody { 149 | border-top: 2px solid #232323; 150 | } 151 | table.table-condensed th, 152 | table.table-condensed td { 153 | padding: 4px 5px; 154 | } 155 | table.table-bordered { 156 | border: 1px solid #232323; 157 | border-left: 0; 158 | border-collapse: separate; 159 | *border-collapse: collapsed; 160 | -webkit-border-radius: 4px; 161 | -moz-border-radius: 4px; 162 | border-radius: 4px; 163 | } 164 | table.table-bordered th, 165 | table.table-bordered td { 166 | border-left: 1px solid #232323; 167 | } 168 | table.table-bordered thead:first-child tr:first-child th, 169 | table.table-bordered tbody:first-child tr:first-child th, 170 | table.table-bordered tbody:first-child tr:first-child td { 171 | border-top: 0; 172 | } 173 | table.table-bordered thead:first-child tr:first-child th:first-child, 174 | table.table-bordered tbody:first-child tr:first-child td:first-child { 175 | -webkit-border-radius: 4px 0 0 0; 176 | -moz-border-radius: 4px 0 0 0; 177 | border-radius: 4px 0 0 0; 178 | } 179 | table.table-bordered thead:first-child tr:first-child th:last-child, 180 | table.table-bordered tbody:first-child tr:first-child td:last-child { 181 | -webkit-border-radius: 0 4px 0 0; 182 | -moz-border-radius: 0 4px 0 0; 183 | border-radius: 0 4px 0 0; 184 | } 185 | table.table-bordered thead:last-child tr:last-child th:first-child, 186 | table.table-bordered tbody:last-child tr:last-child td:first-child { 187 | -webkit-border-radius: 0 0 0 4px; 188 | -moz-border-radius: 0 0 0 4px; 189 | border-radius: 0 0 0 4px; 190 | } 191 | table.table-bordered thead:last-child tr:last-child th:last-child, 192 | table.table-bordered tbody:last-child tr:last-child td:last-child { 193 | -webkit-border-radius: 0 0 4px 0; 194 | -moz-border-radius: 0 0 4px 0; 195 | border-radius: 0 0 4px 0; 196 | } 197 | table.table-striped tbody tr:nth-child(odd) td, 198 | table.table-striped tbody tr:nth-child(odd) th { 199 | background-color: #393939; 200 | } 201 | table tbody tr td, 202 | table tbody tr th, 203 | table tbody tr:hover td, 204 | table tbody tr:hover th { 205 | background-color: #333; 206 | } 207 | .sla-row { 208 | overflow: hidden; 209 | padding: 15px; 210 | } 211 | .number { 212 | font-size: 40px; 213 | } 214 | .sla .texto { 215 | float: right; 216 | font-size: 25px; 217 | text-transform: uppercase; 218 | padding: 15px 20px 0 0; 219 | } 220 | 221 | /**** TAB FOR SELECTING DATES ****/ 222 | #tab { 223 | width:40px; 224 | height:50px; 225 | position:fixed; 226 | right:0; 227 | top:25px; 228 | display:block; 229 | cursor:pointer; 230 | background-color:#232526; 231 | border-radius: 10px 0 0 10px; 232 | padding: 5px 0 5px 5px; 233 | } 234 | #inner_tab { 235 | border-radius: 5px 0 0 5px; 236 | width: 100%; 237 | height: 100%; 238 | background: #CCC url('/public/images/arrow_posts_left.png') no-repeat center center; 239 | } 240 | #inner_tab:hover { 241 | background-position: 5px 13px; 242 | } 243 | .expanded { 244 | background: #CCC url('/public/images/arrow_posts_right.png') no-repeat center center !important; 245 | } 246 | .expanded:hover { 247 | background-position: 11px 13px !important; 248 | } 249 | #panel { 250 | position:fixed; 251 | right:0; 252 | top:25px; 253 | height:250px; 254 | width:0; 255 | font: 18px Arial; 256 | color: #707275; 257 | text-align: center; 258 | border-radius: 0 0 0 15px; 259 | } 260 | #panel h3 { 261 | margin: 0; 262 | margin-bottom: 15px; 263 | text-transform: uppercase; 264 | } 265 | 266 | #panel .content { 267 | width:320px; 268 | margin: 5px auto; 269 | } 270 | 271 | #panel p{margin:.5em 20px;} 272 | #panel label{display:block;} 273 | #panel input, #panel textarea{ 274 | width:272px; 275 | border:1px solid #111; 276 | background:#282828; 277 | padding:5px 3px; 278 | color:#fff; 279 | } 280 | #panel textarea{ 281 | height:125px; 282 | overflow:auto; 283 | } 284 | #panel p.submit{ 285 | text-align:right; 286 | } 287 | #panel button{ 288 | position: relative; 289 | cursor: pointer; 290 | font: bold 12px/normal 'Segoe UI', Arial, sans-serif; 291 | color: #333; 292 | text-decoration: none; 293 | text-shadow: 1px 1px rgba(255,255,255,0.5); 294 | border: 1px solid rgba(0,0,0,.1); 295 | padding: 5px 10px 6px; 296 | -webkit-border-radius: 3px; 297 | -moz-border-radius: 3px; 298 | border-radius: 3px; 299 | background-color: #91BD09 300 | background-image: -moz-linear-gradient(rgba(255,255,255,0.2), rgba(0,0,0,0.2)); 301 | background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, rgba(255,255,255,0.2)), color-stop(100%, rgba(0,0,0,0.2))); 302 | background-image: -webkit-linear-gradient(rgba(255,255,255,0.2), rgba(0,0,0,0.2)); 303 | background-image: -o-linear-gradient(rgba(255,255,255,0.2), rgba(0,0,0,0.2)); 304 | background-image: linear-gradient(rgba(255,255,255,0.2), rgba(0,0,0,0.2)); 305 | -webkit-box-shadow: inset 0px 1px rgba(255,255,255,0.6), 0px 0px 3px 0px rgba(0,0,0,.2); 306 | -moz-box-shadow: inset 0px 1px rgba(255,255,255,0.6), 0px 0px 3px 0px rgba(0,0,0,.2); 307 | box-shadow: inset 0px 1px rgba(255,255,255,0.6), 0px 0px 4px 0px rgba(0,0,0,.2); 308 | -webkit-user-select: none; 309 | -moz-user-select: none; 310 | margin-right: 10px; 311 | } 312 | 313 | #panel button::-moz-focus-inner { 314 | margin: 0; 315 | padding: 0; 316 | border: 0; 317 | } 318 | 319 | #panel button:hover, #panel button:active { 320 | text-decoration: none; 321 | background-color: #A0CF0C 322 | } 323 | 324 | #panel button:active { 325 | top: 1px; 326 | margin-bottom: 1px; 327 | border-bottom-width: 1px; 328 | background-color: #F9F9F9; 329 | background-image: -moz-linear-gradient(rgba(0,0,0,0.2), rgba(255,255,255,0.2)); 330 | background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, rgba(0,0,0,0.2)), color-stop(100%, rgba(255,255,255,0.2))); 331 | background-image: -webkit-linear-gradient(rgba(0,0,0,0.2), rgba(255,255,255,0.2)); 332 | background-image: -o-linear-gradient(rgba(0,0,0,0.2), rgba(255,255,255,0.2)); 333 | background-image: linear-gradient(rgba(0,0,0,0.2), rgba(255,255,255,0.2)); 334 | } -------------------------------------------------------------------------------- /public/stylesheets/tipTip.css: -------------------------------------------------------------------------------- 1 | /* TipTip CSS - Version 1.2 */ 2 | 3 | #tiptip_holder { 4 | display: none; 5 | position: absolute; 6 | top: 0; 7 | left: 0; 8 | z-index: 99999; 9 | } 10 | 11 | #tiptip_holder.tip_top { 12 | padding-bottom: 5px; 13 | } 14 | 15 | #tiptip_holder.tip_bottom { 16 | padding-top: 5px; 17 | } 18 | 19 | #tiptip_holder.tip_right { 20 | padding-left: 5px; 21 | } 22 | 23 | #tiptip_holder.tip_left { 24 | padding-right: 5px; 25 | } 26 | 27 | #tiptip_content { 28 | font-size: 11px; 29 | color: #fff; 30 | text-shadow: 0 0 2px #000; 31 | padding: 4px 8px; 32 | border: 1px solid rgba(255,255,255,0.25); 33 | background-color: rgb(25,25,25); 34 | background-color: rgba(25,25,25,0.92); 35 | background-image: -webkit-gradient(linear, 0% 0%, 0% 100%, from(transparent), to(#000)); 36 | border-radius: 3px; 37 | -webkit-border-radius: 3px; 38 | -moz-border-radius: 3px; 39 | box-shadow: 0 0 3px #555; 40 | -webkit-box-shadow: 0 0 3px #555; 41 | -moz-box-shadow: 0 0 3px #555; 42 | } 43 | 44 | #tiptip_arrow, #tiptip_arrow_inner { 45 | position: absolute; 46 | border-color: transparent; 47 | border-style: solid; 48 | border-width: 6px; 49 | height: 0; 50 | width: 0; 51 | } 52 | 53 | #tiptip_holder.tip_top #tiptip_arrow { 54 | border-top-color: #fff; 55 | border-top-color: rgba(255,255,255,0.35); 56 | } 57 | 58 | #tiptip_holder.tip_bottom #tiptip_arrow { 59 | border-bottom-color: #fff; 60 | border-bottom-color: rgba(255,255,255,0.35); 61 | } 62 | 63 | #tiptip_holder.tip_right #tiptip_arrow { 64 | border-right-color: #fff; 65 | border-right-color: rgba(255,255,255,0.35); 66 | } 67 | 68 | #tiptip_holder.tip_left #tiptip_arrow { 69 | border-left-color: #fff; 70 | border-left-color: rgba(255,255,255,0.35); 71 | } 72 | 73 | #tiptip_holder.tip_top #tiptip_arrow_inner { 74 | margin-top: -7px; 75 | margin-left: -6px; 76 | border-top-color: rgb(25,25,25); 77 | border-top-color: rgba(25,25,25,0.92); 78 | } 79 | 80 | #tiptip_holder.tip_bottom #tiptip_arrow_inner { 81 | margin-top: -5px; 82 | margin-left: -6px; 83 | border-bottom-color: rgb(25,25,25); 84 | border-bottom-color: rgba(25,25,25,0.92); 85 | } 86 | 87 | #tiptip_holder.tip_right #tiptip_arrow_inner { 88 | margin-top: -6px; 89 | margin-left: -5px; 90 | border-right-color: rgb(25,25,25); 91 | border-right-color: rgba(25,25,25,0.92); 92 | } 93 | 94 | #tiptip_holder.tip_left #tiptip_arrow_inner { 95 | margin-top: -6px; 96 | margin-left: -7px; 97 | border-left-color: rgb(25,25,25); 98 | border-left-color: rgba(25,25,25,0.92); 99 | } 100 | 101 | /* Webkit Hacks */ 102 | @media screen and (-webkit-min-device-pixel-ratio:0) { 103 | #tiptip_content { 104 | padding: 4px 8px 5px 8px; 105 | background-color: rgba(45,45,45,0.88); 106 | } 107 | #tiptip_holder.tip_bottom #tiptip_arrow_inner { 108 | border-bottom-color: rgba(45,45,45,0.88); 109 | } 110 | #tiptip_holder.tip_top #tiptip_arrow_inner { 111 | border-top-color: rgba(20,20,20,0.92); 112 | } 113 | } -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | JavaScript Operator Panel [JOP] 2 | ================================ 3 | 4 | As opposed to FOP [http://www.asternic.org / http://www.fop2.com/], JOP is meant to be a HTML/JS solution to see the 5 | current Asterisk status in a Pannel. 6 | 7 | It's built on top of node.js, socket.io and express.js 8 | 9 | It has even live pannels per-client to show stats of calls in the current day or to select from another day. 10 | 11 | *Note*: As our Asterisk build and dialplan are heavily customized, we are using curls from dialplan and routes through 12 | express to perform actions but you could connect to the AMI and be able to capture events. 13 | If you are interested, you could use some other Node - Asterisk modules 14 | 15 | I would love to work with some Asterisk dev/admin to bring this as something you could deploy within your Asterisk basic 16 | installation. -------------------------------------------------------------------------------- /routes/index.js: -------------------------------------------------------------------------------- 1 | module.exports = function Routes (app, database, io){ 2 | var self = this, 3 | arrayHelper = require('../helpers/array.js'), 4 | timeHelper = require('../helpers/time.js'); 5 | 6 | this.index = function(req, res){ 7 | var ip = req.ip; 8 | 9 | if (req.cookies.isadmin !== undefined){ 10 | // Revalidate cookie 11 | res.cookie('isadmin', true, {maxAge:90000}); 12 | app.storeConnectedClient(ip,true); 13 | res.sendfile('./index.html'); 14 | } 15 | else { 16 | app.isAdminUser(ip, true, function(is_admin){ 17 | if (is_admin){ 18 | res.cookie('isadmin', true, {maxAge:90000}); 19 | } 20 | app.storeConnectedClient(ip,is_admin); 21 | res.sendfile('./index.html'); 22 | }); 23 | } 24 | }; 25 | 26 | /** 27 | * Function that will be called when an agent transitions from unavailable to available 28 | */ 29 | this.logAgent = function (req, res){ 30 | var agent_position = arrayHelper.arrayObjectIndexOf(app.agents, req.params.agentCode,'codAgente'); 31 | 32 | // We have it but it's not logged 33 | if (agent_position !== -1) { 34 | if (app.agents[agent_position].status.id === 0) { // It's unlogged 35 | app.agents[agent_position].changeStatus({ 36 | status : 1, //Available 37 | io: io 38 | }); 39 | } 40 | res.send(200); 41 | } 42 | else { // We don't even know about him/her 43 | var query = 'SELECT agentes.nombre as nombre,'+ 44 | ' agentes.apellido1 as apellido1,'+ 45 | ' agentes.apellido2 as apellido2,'+ 46 | ' agentes.codAgente as codAgente,'+ 47 | ' agentes.estado as estado,'+ 48 | ' relcolaext.cola as cola,'+ 49 | ' relcolaext.prioridad as prioridad'+ 50 | ' FROM agentes, relcolaext'+ 51 | ' WHERE relcolaext.codAgente = agentes.codAgente'+ 52 | ' AND agentes.codAgente = "' + req.params.agentCode + '"'; 53 | console.log ('Executing MySQL Query >> %s', query); 54 | 55 | database.client.query(query, 56 | function (err, results, fields){ 57 | if (err){ 58 | res.send(500); 59 | throw err; 60 | } 61 | 62 | if (results[0]){ 63 | if (arrayHelper.arrayObjectIndexOf(app.agents, req.params.agentCode,'codAgente') === -1){ // It's not already connected 64 | // If we have a result, we push it into the agents array 65 | app.agents.push( app.agent_utils.createAgent( 66 | results, 67 | io 68 | )); 69 | console.log('Agent has logged %s %s [%s]', results[0].nombre, results[0].apellido1, req.params.agentCode); 70 | res.send(200); 71 | } 72 | else { 73 | res.send(500,'Agent were already added'); 74 | } 75 | } 76 | else{ 77 | console.log("Someone has tried to log into the system with the extension %s, but it wasn't able to do that. Maybe the extension is not registered or it's not related to any queues.", req.params.agentCode); 78 | } 79 | } 80 | ); 81 | } 82 | }; 83 | /* 84 | * Function that will be called upon agent disconnection 85 | */ 86 | this.unLogAgent = function (req, res) { 87 | var agent_position = arrayHelper.arrayObjectIndexOf(app.agents, req.params.agentCode,'codAgente'); 88 | if (agent_position !== -1){ 89 | app.agents[agent_position].changeStatus({ 90 | status : 0, //Disconnected 91 | io: io 92 | }); 93 | console.log('Unlogged agent [%s]', req.params.agentCode); 94 | } 95 | 96 | res.send(200); 97 | }; 98 | /* 99 | * Function that will be called when an agent's phone start/stop ringing 100 | */ 101 | this.agentRing = function (req, res) { 102 | var agent_position = arrayHelper.arrayObjectIndexOf(app.agents, req.params.agentCode,'codAgente'); 103 | 104 | if (agent_position !== -1){ 105 | app.agents[agent_position].manageRinging({ 106 | action : req.params.action, 107 | io: io 108 | }); 109 | } 110 | 111 | res.send(200); 112 | }; 113 | /** 114 | * Function that change the status of an agent. This function *should* only be called if the new status it's not a call 115 | */ 116 | this.changeStatusNoCall = function (req, res) { 117 | var newStatus = self.determineStatusByReq(req.url), 118 | agentPosition = arrayHelper.arrayObjectIndexOf(app.agents,req.params.agentCode,'codAgente'); 119 | 120 | // Changing the agent status and emiting by socket 121 | if (agentPosition !== -1){ 122 | app.agents[agentPosition].changeStatus({ 123 | status : newStatus.status_code, 124 | io : io 125 | }); 126 | } 127 | 128 | res.send(200); 129 | }; 130 | 131 | this.dispatchCallInQueue = function (req, res){ 132 | var isIncoming = (req.url.indexOf('/call/') !== -1), 133 | type = '', 134 | queue_name = req.params.queue, 135 | queue = app.queue_utils.getQueueFromName(app.queues, queue_name), 136 | unique_id = req.params.uniqueid, 137 | client = (queue !== undefined) ? queue.client_obj : null, 138 | call_position = -1; 139 | 140 | if (queue){ 141 | call_position = arrayHelper.arrayObjectIndexOf(queue.calls, unique_id, 'uniqueid'); 142 | } 143 | 144 | // If the call isn't in the queue yet 145 | if (isIncoming && call_position === -1) { 146 | console.log ('Incoming call [%s] to queue %s at %s', unique_id, queue_name, new Date()); 147 | 148 | // Stats stuff 149 | if (client) { 150 | client.total_calls++; 151 | client.offered_calls++; 152 | client.sendStatus(); 153 | } 154 | 155 | type = 'in'; 156 | } 157 | else 158 | { 159 | console.log ('Call answered by [%s] in %s at %s', req.params.agentCode, queue_name, new Date()); 160 | 161 | var pos_agent = arrayHelper.arrayObjectIndexOf(app.agents,req.params.agentCode,'codAgente'); 162 | 163 | if (pos_agent !== -1){ 164 | app.agents[pos_agent].changeStatus({status:4, io: io, queue: queue_name}); 165 | app.talking = self.calculateTalking(); 166 | app.updatePrimary(); 167 | } 168 | type = 'out'; 169 | } 170 | 171 | // Valid if queue position is found 172 | // Then if is incoming call, will be assured that the call isn't already registered 173 | // or it's not incoming 174 | if (queue !== undefined && ((isIncoming && call_position === -1) || (!isIncoming))){ 175 | app.queue_utils.dispatchCall({ 176 | uniqueid: unique_id, 177 | queue: queue, 178 | type: type, 179 | abandoned : false, 180 | io: io 181 | }); 182 | } 183 | else { 184 | console.log('The call [%s] wasn\'t added to %s because it was already there!', unique_id, queue_name); 185 | } 186 | 187 | res.send(200); 188 | }; 189 | /** 190 | * Function that will be called when an agent is performing a call 191 | */ 192 | this.externalCall = function (req, res){ 193 | console.log ('Outgoing call from %s', req.params.agentCode); 194 | 195 | var pos_agent = arrayHelper.arrayObjectIndexOf(app.agents,req.params.agentCode,'codAgente'); 196 | 197 | if (pos_agent !== -1){ 198 | app.agents[pos_agent].changeStatus({status: 5, io: io}); 199 | } 200 | 201 | res.send(200); 202 | }; 203 | /* 204 | * This function will be called when a call hangs. It may be in 'agente' in 'cola' or in 'message' 205 | */ 206 | this.hangCall = function (req, res){ 207 | console.log ('Call finished at %s', req.params.type); 208 | var queue; 209 | 210 | switch (req.params.type) 211 | { 212 | case 'agente': 213 | var pos_agent = arrayHelper.arrayObjectIndexOf(app.agents, req.params.agentOrQueue, 'codAgente'); 214 | 215 | if (pos_agent !== -1){ 216 | // If it's an incoming call 217 | app.agents[pos_agent].endCall(io); 218 | app.talking = self.calculateTalking(); 219 | app.updatePrimary(); 220 | } 221 | else { 222 | console.log('Call couldn\'t be ended because agent %s, cannot be found',req.params.agentOrQueue); 223 | } 224 | 225 | break; 226 | case 'cola' : 227 | queue = app.queue_utils.getQueueFromName(app.queues, req.params.agentOrQueue); 228 | if (queue !== undefined) { 229 | console.log('Call [%s] is about to be dispatched from %s', req.params.uniqueid, req.params.agentOrQueue); 230 | 231 | app.queue_utils.dispatchCall( 232 | { 233 | queue: queue, 234 | uniqueid: req.params.uniqueid, 235 | type: 'out', 236 | abandoned: true, 237 | io: io 238 | } 239 | ); 240 | } 241 | else { 242 | console.log('Call [%s] couldn\'t be dispatched from %s because queue wasn\'t found', 243 | req.params.uniqueid, req.params.agentOrQueue 244 | ); 245 | } 246 | break; 247 | case 'message' : 248 | queue = app.queue_utils.getQueueFromName(app.queues, req.params.agentOrQueue); 249 | 250 | if (queue !== undefined){ 251 | var client = queue.client_obj; 252 | 253 | if (client){ 254 | client.failed_calls++; 255 | client.total_calls++; 256 | client.sendStatus(); 257 | } 258 | } 259 | break; 260 | } 261 | 262 | res.send(200); 263 | }; 264 | /* 265 | * Whenever a call is transferred this function will be called from server.js 266 | */ 267 | this.transferCall = function (req, res){ 268 | console.log('Transfering [%s] from %s to %s', req.params.uniqueid, req.params.agent_from, req.params.agent_to); 269 | 270 | var pos_agent_from = arrayHelper.arrayObjectIndexOf(app.agents, req.params.agent_from, 'codAgente'), 271 | pos_agent_to = arrayHelper.arrayObjectIndexOf(app.agents, req.params.agent_to, 'codAgente'); 272 | 273 | // If we get the agent whom is transferring, we end the call there 274 | if (pos_agent_from !== -1) { 275 | app.agents[pos_agent_from].endCall(io); 276 | } 277 | else { 278 | console.log('We couldnt end the call at %s because we couldnt find it', req.params.agent_from) 279 | } 280 | 281 | // If we get the agent to whom the call is going to be transferred, we create a new call to him 282 | if (pos_agent_to !== -1) { 283 | app.agents[pos_agent_to].changeStatus({status:4, io: io, queue: req.params.queue_name}); 284 | } 285 | else { 286 | console.log('We couldnt send the call at %s because we couldnt find it', req.params.agent_to) 287 | } 288 | 289 | res.send(200); 290 | }; 291 | /* 292 | * This function is called whenever someone tries to see some stats from a client 293 | */ 294 | this.getClientStats = function ( req, res ) { 295 | var client = app.client_utils.getClientFromName(app.clients, req.params.client_name); 296 | if (client){ 297 | if (req.params.from_date === undefined){ 298 | // Real time request 299 | var client_data = { 300 | name : client.name, 301 | real_time : true, 302 | range_date : false, 303 | total_offered_calls : 0, 304 | total_calls : 0, 305 | total_abandoned : 0, 306 | abandoned_after_SLA : 0, 307 | total_answered : 0, 308 | answered_before_SLA : 0, 309 | average_response_time : 0, 310 | failed_calls : 0, 311 | perc_abandoned : client.perc_abandoned, 312 | perc_answered : client.perc_answered, 313 | per_hour : null, 314 | sec_abandoned : client.sec_abandoned, 315 | sec_answered : client.sec_answered 316 | }; 317 | 318 | if (req.url.indexOf('json') !== -1){ 319 | var status = client.getStatus(); 320 | status.client_name = client.name; 321 | status.per_hour = client.stats_by_hour; 322 | res.json(status); 323 | } 324 | else { 325 | res.render('index', client_data); 326 | } 327 | } 328 | else { 329 | var start_date = new Date(req.params.from_date), 330 | end_date, original_end_date; 331 | 332 | timeHelper.setAbsoluteDay(start_date); 333 | 334 | if (req.params.to_date !== undefined) { 335 | end_date = new Date(req.params.to_date); 336 | original_end_date = new Date(req.params.to_date); 337 | timeHelper.setAbsoluteDay(end_date); 338 | end_date.setDate(end_date.getDate() +1); 339 | } 340 | else { 341 | end_date = new Date(req.params.from_date); 342 | original_end_date = new Date(req.params.from_date); 343 | timeHelper.setAbsoluteDay(end_date); 344 | end_date.setDate(start_date.getDate() + 1); 345 | } 346 | 347 | if ((end_date - start_date) > 0){ 348 | app.async.parallel([function(callback){ 349 | client.loadStats(start_date, end_date, callback); 350 | }], function(data){ 351 | data.start_date = start_date; 352 | data.end_date = original_end_date; 353 | data.range_date = (end_date - start_date !== 86400000); 354 | 355 | res.render('index',data); 356 | }); 357 | } 358 | else { 359 | res.end('Dates are incorrect. IE, Start date is lower than End date'); 360 | } 361 | } 362 | } 363 | else { 364 | res.send(404, 'Sorry, we cannot find that!'); 365 | } 366 | }; 367 | /* 368 | * This function is called to update the current calls and calls in queue in the pannel 369 | */ 370 | this.updateCalls = function (req, res){ 371 | app.calls = parseInt(req.params.total_calls ,10); 372 | app.awaiting = parseInt(req.params.calls_in_queue ,10); 373 | app.updatePrimary(); 374 | 375 | res.send(200); 376 | }; 377 | /* 378 | * This function performs a reload of the data that the pannel has 379 | */ 380 | this.reload = function ( req, res ) { 381 | app.refetcher.perform(true); 382 | res.send(200, 'Done'); 383 | }; 384 | /* 385 | * This functions output by console a debug trace for a queue 386 | */ 387 | this.debugQueue = function (req, res) { 388 | var queue = app.queue_utils.getQueueFromName(app.queues, req.params.queue_name); 389 | 390 | if (queue){ 391 | console.log('Debug trace for [%s]',queue.name); 392 | console.log('------------------------------'); 393 | console.log(queue.calls); 394 | console.log('------------------------------'); 395 | } 396 | 397 | res.send(200, 'Done'); 398 | }; 399 | 400 | this.debugAgent = function(req, res) { 401 | var agent_position = arrayHelper.arrayObjectIndexOf(app.agents, req.params.agent_code, 'codAgente'), 402 | agent = (agent_position !== -1) ? app.agents[agent_position] : undefined; 403 | 404 | if (agent){ 405 | console.log('Debug trace for [%s]', agent.nombre); 406 | console.log('------------------------------'); 407 | console.log(agent); 408 | console.log('------------------------------'); 409 | } 410 | }; 411 | 412 | this.panelStats = function(req,res){ 413 | console.log(app.connected_clients); 414 | res.send(200,'Done'); 415 | }; 416 | 417 | /* 418 | * This function controlls the sim availability that's shown on the Pannel 419 | */ 420 | this.simAvailability = function (req, res) { 421 | io.sockets.emit('simAvailability', 422 | { 423 | sim: req.params.sim_number , 424 | available : req.params.available.toLowerCase() === 'available' 425 | } 426 | ); 427 | res.send(200, 'Done'); 428 | }; 429 | /** 430 | * This function calculates the number of people that it's currently talking 431 | * it just counts the people attending an Incoming call. 432 | * 433 | * @return {Number} of people talking 434 | */ 435 | this.calculateTalking = function(){ 436 | var talking = 0; 437 | for (var i = 0, length = app.agents.length; i < length; i++){ 438 | if (app.agents[i].status.id === 4){ 439 | talking++; 440 | } 441 | } 442 | return talking; 443 | }; 444 | 445 | /** 446 | * This functions determine the status contained in a string. The status code should match with those in 447 | * agent.js 448 | * 449 | * @request : The string to look within 450 | * 451 | * @return an object with the code and the string as it's properties 452 | */ 453 | this.determineStatusByReq = function (request) { 454 | var searches = [ 455 | { 456 | status_code : 1, 457 | string : '/available/' 458 | }, 459 | { 460 | status_code : 2, 461 | string : '/meeting/' 462 | }, 463 | { 464 | status_code : 3, 465 | string : '/administrative/' 466 | }, 467 | { 468 | status_code : 6, 469 | string : '/resting/' 470 | }, 471 | { 472 | status_code : 7, 473 | string : '/glorytime/' 474 | } 475 | ]; 476 | 477 | for (var i = 0; i < searches.length; i++){ 478 | if (request.indexOf(searches[i].string) !== -1) { return searches[i] } 479 | } 480 | 481 | // We should never reach here! 482 | return { 483 | status_code: 0, 484 | string : '' 485 | }; 486 | }; 487 | 488 | this.getAgentStatus = function (req, res) { 489 | var agent_code = req.params.agent_code, 490 | is_json = req.url.indexOf('json') !== -1; 491 | 492 | var result = is_json ? [] : ''; 493 | 494 | if (agent_code !== 'all'){ 495 | var agent = app.agent_utils.getAgentFromCode(app.agents,agent_code); 496 | if (agent) { 497 | if (is_json){ 498 | result.push(agent.status.name); 499 | } 500 | else { 501 | result = agent.status.name; 502 | } 503 | } 504 | } 505 | else { 506 | for (var i = 0, length = app.agents.length; i < length; i++){ 507 | if (is_json){ 508 | result.push({ 509 | codAgente : app.agents[i].codAgente, 510 | status : app.agents[i].status.name 511 | }); 512 | } 513 | else{ 514 | result += [app.agents[i].codAgente, app.agents[i].status.name].join(' ') + '\n'; 515 | } 516 | } 517 | } 518 | 519 | if (is_json){ res.json(result); } 520 | else { res.send(200, result); } 521 | }; 522 | }; -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies. 3 | */ 4 | var express = require('express'), 5 | server = express(), 6 | Routes = require('./routes'), 7 | App = require('./modules/app.js'), 8 | browser_ban = require('./modules/browser_ban.js'), 9 | pbx_bugs_solver = require('./modules/pbx_bugs_solver.js'), 10 | async = require('async'), 11 | database = require('./libraries/mysql.js'), 12 | socketio = require('socket.io'), 13 | CronJob = require('cron').CronJob, 14 | mysql = require('mysql'); 15 | 16 | var io = socketio.listen(server.listen(8080)); 17 | 18 | io.configure('production', function(){ 19 | console.log("***** Server in production mode *********"); 20 | io.enable('browser client minification'); // send minified client 21 | io.enable('browser client etag'); // apply etag caching logic based on version number 22 | io.enable('browser client gzip'); // gzip the file 23 | io.set('log level', 1); // reduce logging 24 | io.set('transports', [ // enable all transports (optional if you want flashsocket) 25 | 'websocket', 'flashsocket', 'htmlfile', 'xhr-polling', 'jsonp-polling' 26 | ]); 27 | }); 28 | io.configure('development', function(){ 29 | io.set('transports', ['websocket']); 30 | }); 31 | 32 | // Undefined max listeners! 33 | process.setMaxListeners(0); 34 | 35 | var app = new App(database, io, async), 36 | routes = new Routes(app, database, io); 37 | 38 | // CronJob which restarts stats for all data 39 | var cron_job = new CronJob('00 00 00 * * *', app.startReset,null,true); 40 | 41 | app.init(app.loadTodayStats); 42 | 43 | /* Server configuration */ 44 | server.configure(function(){ 45 | server.use(express.favicon()); 46 | server.use(express.logger('dev')); 47 | server.set('views', __dirname + '/views'); 48 | server.set('view engine', 'jade'); 49 | server.use(express.bodyParser()); 50 | server.use(express.cookieParser()); 51 | server.use(express.methodOverride()); 52 | server.use(express.static(__dirname + '/public')); 53 | server.use("/public", express.static(__dirname + '/public')); 54 | server.use(browser_ban()); 55 | server.use(pbx_bugs_solver()); 56 | server.use(server.router); 57 | }); 58 | 59 | server.configure('development', function(){ 60 | server.use(express.errorHandler()); 61 | }); 62 | 63 | /**** Routes definition *****/ 64 | server.get('/', routes.index); 65 | 66 | //region Agents management 67 | /* 68 | ########################## 69 | ### Agents management ### 70 | ########################## 71 | */ 72 | /** 73 | * When an agent logs into the phone this function will be called. 74 | * @agentCode : The Agent Code or Extension. 75 | * 76 | * URL Example: /logAgent/1010 77 | */ 78 | server.get('/logAgent/:agentCode', routes.logAgent); 79 | 80 | /** 81 | * When an agent unlog it's phone this function will be called. 82 | * @agentCode : The Agent Code or Extension. 83 | * 84 | * URL Example: /unLogAgent/1010 85 | */ 86 | server.get('/unLogAgent/:agentCode', routes.unLogAgent); 87 | 88 | /** 89 | * When an agent enters in administrative time, this function will be called. 90 | * 91 | * @agentCode : The Agent Code or Extension. 92 | * 93 | * URL Example: /administrative/1010 94 | */ 95 | server.get('/administrative/:agentCode', routes.changeStatusNoCall); 96 | /** 97 | * When an agent enters in resting time, this function will be called. 98 | * 99 | * @agentCode : The Agent Code or Extension. 100 | * 101 | * URL Example: /resting/1010 102 | */ 103 | server.get('/resting/:agentCode', routes.changeStatusNoCall); 104 | /** 105 | * When an agent disable administrative time or unavailable mode, this function will be called. 106 | * 107 | * @agentCode : The Agent Code or Extension. 108 | * 109 | * URL Example: /available/1010 110 | */ 111 | server.get('/available/:agentCode', routes.changeStatusNoCall); 112 | /** 113 | * When an agent put him/herself into unavailable, this function will be called. 114 | * 115 | * @agentCode : The Agent Code or Extension. 116 | * 117 | * URL Example: /meeting/1010 118 | */ 119 | server.get('/meeting/:agentCode', routes.changeStatusNoCall); 120 | /** 121 | * When an agent's phone starts or stops ringing, this function will be called 122 | * 123 | * @action : Whether the phone 'start' or 'stop' ringing 124 | * @agentCode : The Agent Code or Extension. 125 | * 126 | * URL Example: /ringing/start/1010 127 | */ 128 | server.get('/ringing/:action/:agentCode', routes.agentRing); 129 | /** 130 | * When an agent enters glorytime, this function will be called 131 | * 132 | * @agentCode : The Agent Code or Extension. 133 | * 134 | * URL Example: /glorytime/1010 135 | */ 136 | server.get('/glorytime/:agentCode', routes.changeStatusNoCall); 137 | //endregion 138 | //region Calls management 139 | /* 140 | ########################## 141 | #### Calls management #### 142 | ########################## 143 | */ 144 | /** 145 | * When a call enters in a queue, this function will be called 146 | * 147 | * @queue : The queue that is receiving the call. 148 | * @uniqueid : The uniqueid of the call. 149 | * 150 | * URL Example: /call/APPLUSCola/1234567890 151 | */ 152 | server.get('/call/:queue/:uniqueid', routes.dispatchCallInQueue); 153 | /** 154 | * When a call is answered by an agent, this function will be called. 155 | * 156 | * @queue : The queue that is receiving the call. 157 | * @uniqueid : The uniqueid of the call. 158 | * @agentCode : The agent whom is answering the call. 159 | * 160 | * URL Example: /answerCall/APPLUSCola/1234567890/1010 161 | */ 162 | server.get('/answerCall/:queue/:uniqueid/:agentCode', routes.dispatchCallInQueue); 163 | /** 164 | * When an agent performs a call, this function will be called. 165 | * 166 | * @agentCode : The agent whom is performing the call. 167 | * @queue : The queue that is receiving the call. 168 | * 169 | * URL Example: /externalCall/1010/APPLUSCola 170 | */ 171 | server.get('/externalCall/:agentCode/:queue', routes.externalCall); 172 | /** 173 | * When a call hangs, this function will be called. 174 | * 175 | * @type : This indicates where the call were terminated. Could have this values: 176 | * * 'agente' : If it ends in an agent 177 | * * 'cola' : If it ends in a queue 178 | * @uniqueid : The uniqueid of the call 179 | * @agentOrQueue : The AgentCode or the Queue Name where the call terminated. 180 | * 181 | * URL Example 1: /hangCall/cola/1234567890/APPLUSCola 182 | * URL Example 2: /hangCall/agente/1234567890/1010 183 | */ 184 | server.get('/hangCall/:type/:uniqueid/:agentOrQueue', routes.hangCall); 185 | /** 186 | * When a is tranferred from an agent to another one, this function will be called 187 | * 188 | * @uniqueid : The uniqueid of the call 189 | * @agent_from : The agent whom is transferring the call 190 | * @agent_to : The agent whom is receiving the call 191 | * @queue_name : The queue in which is the call that's going to be transferred 192 | */ 193 | server.get('/transferCall/:uniqueid/from/:agent_from/to/:agent_to/at/:queue_name', routes.transferCall); 194 | /** 195 | * Simple function that receives the total calls and the calls in queue from Asterisk 196 | * 197 | * @total_calls : Total calls in the system to calculate the primary occupation 198 | * @calls_in_queue : Current calls in queue 199 | */ 200 | server.get('/updateCalls/:total_calls/:calls_in_queue', routes.updateCalls); 201 | //endregion 202 | //region Stats management 203 | /** 204 | * This will try to find a client and will redirect to the stats page of the client 205 | * 206 | * @clientName : The name of the client 207 | * 208 | * URL Example: /stats/clients/Applus 209 | */ 210 | server.get('/stats/clients/:client_name', routes.getClientStats); 211 | /** 212 | * This will return the actual stats for the client in JSON format 213 | * 214 | * @clientName : The name of the client 215 | * 216 | * URL Example: /stats/json/clients/Applus 217 | */ 218 | server.get('/stats/json/clients/:client_name/', routes.getClientStats); 219 | /** 220 | * This will try to find a client and will redirect to the stats page of the client using a given day 221 | * 222 | * @clientName : The name of the client 223 | * @from_date : The day which we want o see the stats 224 | * 225 | * URL Example: /stats/clients/Applus 226 | */ 227 | server.get('/stats/clients/:client_name/:from_date', routes.getClientStats); 228 | /** 229 | * This will try to find a client and will redirect to the stats page of the client given a range date 230 | * it WILL validate that from_date is minor than to_date 231 | * 232 | * @clientName : The name of the client 233 | * @from_date 234 | * 235 | * URL Example: /stats/clients/Applus 236 | */ 237 | server.get('/stats/clients/:client_name/:from_date/to/:to_date', routes.getClientStats); 238 | /** 239 | * This function returns the current status of an agent. `all` can be used to fetch all agents statuses. 240 | * 241 | * @agent_code : The extension of the agent to request or, `all` 242 | * 243 | * URL Example: /stats/clients/Applus 244 | */ 245 | server.get('/status/agents/:agent_code', routes.getAgentStatus); 246 | /** 247 | * This function returns the current status of an agent. `all` can be used to fetch all agents statuses in JSON. 248 | * 249 | * @agent_code : The extension of the agent to request or, `all` 250 | * 251 | * URL Example: /stats/clients/Applus 252 | */ 253 | server.get('/status/agents/:agent_code/json', routes.getAgentStatus); 254 | //endregion 255 | //region Debugging functions 256 | /** 257 | * Simple function that logs the status of a queue. Usefull to see what calls are currently enqueued. 258 | */ 259 | server.get('/debug/queue/:queue_name', routes.debugQueue); 260 | /** 261 | * Simple function that logs the status of an agent. 262 | */ 263 | server.get('/debug/agent/:agent_code', routes.debugAgent); 264 | /** 265 | * Simple function that logs some usage info of the pannel 266 | */ 267 | server.get('/debug/stats/', routes.panelStats); 268 | //endregion 269 | //region Utility functions 270 | /** 271 | * This route will reload all data from all sources and will send a signal to all panels so they refresh 272 | */ 273 | server.get('/reload', routes.reload); 274 | /** 275 | * Function that stablish a mobile sim available or busy 276 | */ 277 | server.get('/sim/:sim_number/:available', routes.simAvailability); 278 | //endregion 279 | 280 | // On connection action 281 | io.sockets.on('connection', function (socket) { 282 | // If someone new comes, it will notified of the current status of the application 283 | var endpoint = socket.manager.handshaken[socket.id].address; 284 | console.log('Someone (%s) connected to the pannel', endpoint.address); 285 | 286 | app.sendCurrentStatus(socket.id, endpoint.address); 287 | 288 | // Binding to socket events 289 | socket.on('sendCall', app.sendCallToAgent); 290 | socket.on('forceUnlog', app.forceUnlogAgent); 291 | socket.on('disconnect', function() { 292 | app.deleteConnectedClient(socket.manager.handshaken[socket.id].address.address); 293 | }); 294 | }); 295 | 296 | // Reload all current pannels after 5 seconds 297 | // If the server reload because an error this will tell all connected clients to reload 298 | setTimeout(function(){ 299 | io.sockets.emit('reload', {}); 300 | }, 5000); -------------------------------------------------------------------------------- /views/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Panel Flaix 10 | 11 | 12 | 13 | 16 | 17 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 |
32 |
33 |
34 | ; 35 |
36 |
37 | 38 | Iconos de los clientes 39 | 40 |
41 |
42 |
43 | 44 | 45 |
46 |
47 |
48 | 49 |
50 |
51 | 63 |
64 |
65 |
66 | Ocupación del primario 67 |
68 |
69 |
70 |
71 | 0% 72 |
73 |
74 |
75 | 76 |
77 |
78 |
79 | 88 | 105 | 110 | 111 | 112 | -------------------------------------------------------------------------------- /views/index.jade: -------------------------------------------------------------------------------- 1 | extends layout 2 | 3 | block body 4 | #panel.widget 5 | .content 6 | h3 Date selection 7 | p 8 | label(for='from') From 9 | input#from(type='text', name='from', size='30') 10 | p 11 | label(for='to') To 12 | input#to(type='text', name='to', size='30') 13 | button#datetime Stats for date 14 | button#realtime Today's stats 15 | a#open-tab(href='#') 16 | #tab 17 | #inner_tab 18 | 19 | block content 20 | .widget.cabecera 21 | img(src='/images/misc/accenture-logo.png') 22 | if (real_time) 23 | h3 Realtime #{name}'s stats 24 | else 25 | if (range_date) 26 | h3 #{name}'s stats from #{start_date.toDateString()} to #{end_date.toDateString()} 27 | else 28 | h3 #{name}'s stats for #{start_date.toDateString()} 29 | .widget.tabla.stats 30 | header 31 | section Stats area 32 | section.widget-body 33 | .widget-inner 34 | table.table-striped 35 | tbody 36 | tr 37 | th Inbound calls 38 | td#inbound-calls 39 | tr 40 | th Failed calls 41 | td#failed-calls 42 | tr 43 | th Offered calls 44 | td#offered-calls 45 | tr 46 | th Abandoned calls 47 | td#abandoned-calls 48 | tr 49 | th Abandoned after #{sec_abandoned} seconds 50 | td#abandoned-sla 51 | tr 52 | th Answered calls 53 | td#answered-calls 54 | tr 55 | th Answered before #{sec_answered} seconds 56 | td#answered-sla 57 | .widget.tabla.slas-kpis 58 | header 59 | section SLAs & KPIs Area 60 | section.widget-body 61 | .widget-inner 62 | table.table-striped 63 | tbody 64 | tr 65 | th % calls answered in target time SLA (Target => #{perc_answered}%, #{sec_answered} secs) 66 | td#sla-answering 67 | tr 68 | th Average response time SLA (Target < #{sec_answered} sec) 69 | td#average-response 70 | tr 71 | th Abandoned calls SLA (Target <= #{perc_abandoned}%, #{sec_abandoned} secs) 72 | td#sla-abandoned 73 | tr 74 | th Abandoned calls KPI 75 | td#kpi-abandoned.number.orange 76 | .widget.graph.first 77 | header 78 | section Graph area 79 | section.widget-body 80 | #graph-calls.widget-inner 81 | 82 | .widget.tabla.stats-hour 83 | header 84 | section Stats per hour 85 | section.widget-body 86 | .widget-inner 87 | table#stats-table.table-striped 88 | thead 89 | th Hour 90 | th Answered 91 | th Abandoned 92 | th Service Level 93 | th Abandon rate 94 | th Average answer time 95 | th Average abandon time 96 | tbody 97 | 98 | script 99 | var client_name = '#{name}'; 100 | var real_time = #{real_time}; 101 | var range_date = #{range_date}; 102 | var perc_abandoned = '#{perc_abandoned}'; 103 | var sec_answered = '#{sec_answered}'; 104 | var perc_answered = '#{perc_answered}'; 105 | if (!real_time) 106 | var loaded_stats = { 107 | total_calls : #{total_calls}, 108 | total_offered_calls : #{total_offered_calls}, 109 | average_response_time : #{average_response_time}, 110 | failed_calls : #{failed_calls}, 111 | total_abandoned : #{total_abandoned}, 112 | abandoned_after_SLA : #{abandoned_after_SLA}, 113 | total_answered : #{total_answered}, 114 | answered_before_SLA : #{answered_before_SLA}, 115 | per_hour : !{JSON.stringify(per_hour)} 116 | }; -------------------------------------------------------------------------------- /views/layout.jade: -------------------------------------------------------------------------------- 1 | doctype 5 2 | html 3 | head 4 | title Client stats for #{name} 5 | link(rel='stylesheet', href='/stylesheets/normalize.css') 6 | link(rel='stylesheet', href='/stylesheets/style.css') 7 | link(rel='stylesheet', href='/stylesheets/jquery-ui-1.8.23.custom.css') 8 | script(src='/socket.io/socket.io.js') 9 | body 10 | block body 11 | #dashboard-container 12 | block content 13 | script(src='/javascripts/jquery-1.8.1.min.js', type='text/javascript') 14 | script(src='/javascripts/jquery-ui-1.8.23.custom.min.js', type='text/javascript') 15 | script(src='/javascripts/highcharts.js', type='text/javascript') 16 | script(src='/javascripts/client_stat.js', type='text/javascript') --------------------------------------------------------------------------------