├── .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 |
4 |
5 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.idea/workspace.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 |
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 | 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 |
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 |
--------------------------------------------------------------------------------
/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 |
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 |
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')
--------------------------------------------------------------------------------