Browse Source

Queue Agents

master
Vladimir Barkasov 8 years ago
parent
commit
e433cb468d
6 changed files with 782 additions and 210 deletions
  1. +16
    -0
      README.md
  2. +327
    -44
      app.js
  3. +340
    -106
      style/app.scss
  4. +17
    -42
      views/settings.html
  5. +42
    -0
      views/settings_queue_agents.html
  6. +40
    -18
      views/settings_queue_form.html

+ 16
- 0
README.md View File

@ -41,3 +41,19 @@ Installation
'agents_livestats_loading': { 'verb': 'GET', 'url': 'accounts/{accountId}/agents/stats' }
},
```
3. Add files to `/src/js/vendor/datatables`
- buttons.bootstrap.min.js
- buttons.html5.min.js
- dataTables.bootstrap.min.js
- dataTables.buttons.min.js
- jquery.dataTables.min.js
4. Add file `jquery.dataTables.css` to `/src/css/vendor/jquery/`
5. Add next strings to `/src/js/main.js` after `paths: {`:
```javascript
'datatables.net': 'js/vendor/datatables/jquery.dataTables.min',
'datatables.net-bs': 'js/vendor/datatables/dataTables.bootstrap.min',
'datatables.net-buttons': 'js/vendor/datatables/dataTables.buttons.min',
'datatables.net-buttons-html5': 'js/vendor/datatables/buttons.html5.min',
'datatables.net-buttons-bootstrap': 'js/vendor/datatables/buttons.bootstrap.min',
```

+ 327
- 44
app.js View File

@ -4,6 +4,14 @@ define(function(require){
monster = require('monster'),
toastr = require('toastr');
require([
'datatables.net',
'datatables.net-bs',
'datatables.net-buttons',
'datatables.net-buttons-html5',
'datatables.net-buttons-bootstrap'
]);
var app = {
name: 'callcenter',
@ -23,6 +31,16 @@ var app = {
'es-ES': { customCss: false }
},
requests: {
/* move here all unstandart request
'google.getUser': {
apiRoot: 'http://api.google.com/',
url: 'users',
verb: 'PUT'
}*/
},
load: function(callback){
var self = this;
@ -39,6 +57,10 @@ var app = {
calls_in_progress: {}
},
vars: {
users: []
},
initApp: function(callback) {
var self = this;
@ -47,6 +69,14 @@ var app = {
callback: callback
});
self.initHandlebarsHelpers();
},
initHandlebarsHelpers: function() {
Handlebars.registerHelper('inc', function(value, options) {
return parseInt(value) + 1;
});
Handlebars.registerHelper('compare', function (lvalue, operator, rvalue, options) {
var operators, result;
@ -537,9 +567,7 @@ var app = {
$container.find('.js-open-cc-settings').on('click', function(e) {
e.preventDefault();
var html = $(monster.template(self, 'settings', {}));
$container.empty().append(html);
self.settingsInit($container);
self.settingsRender($container);
}).click(); // TODO: remove ".click()" after development
},
@ -581,7 +609,14 @@ var app = {
}, 4000);
},
settingsInit: function($container) {
settingsRender: function($container, callback) {
var self = this;
var html = $(monster.template(self, 'settings', {}));
$container.empty().append(html);
self.settingsInit($container, callback);
},
settingsInit: function($container, callback) {
var self = this;
var $queuesListBox = $container.find('#queues-list-container');
self.settingsQueuesListRender(null, $queuesListBox, function() {
@ -593,6 +628,13 @@ var app = {
});
self.settingsBindEvents($container);
// TODO: remove after development
$('#queues-list li:first-child a').click();
if(typeof(callback) === 'function') {
callback();
}
});
},
@ -625,7 +667,7 @@ var app = {
self.settingsQueueFormRender($parent);
},
settingsQueueFormRender: function($container, data, callback) {
settingsQueueFormRender: function($container, queueData, callback) {
var self = this;
var defaultQueueData = {
@ -638,7 +680,7 @@ var app = {
max_queue_size: '0'
};
data = $.extend(true, defaultQueueData, data || {});
queueData = $.extend(true, defaultQueueData, queueData || {});
/* var data = {
"data": {
"name": "test2",
@ -666,37 +708,224 @@ var app = {
"verb": "PUT"
};*/
self.callApi({
resource: 'media.list',
data: {
accountId: self.accountId
monster.parallel({
users: function(callback) {
self.callApi({
resource: 'user.list',
data: {
accountId: self.accountId
},
success: function (users) {
callback(null, users.data);
}
});
},
media: function(callback) {
self.callApi({
resource: 'media.list',
data: {
accountId: self.accountId
},
success: function(media, status) {
callback(null, media.data);
}
});
}
},
success: function(mediaData, status) {
console.log('mediaData:');
console.log(mediaData);
mediaData.data.unshift(
{
id: '',
name: 'Default' // TODO: i18n it
},
{
id: 'silence_stream://300000',
name: 'Silence' // TODO: i18n it
}
);
function(err, results) {
if (err) {
console.log('Error');
console.log(err);
} else {
var html = $(monster.template(self, 'settings_queue_form', {
data: data,
media_list: mediaData.data
}));
$container.empty().append(html);
// results.media
// results.users
results.media.unshift(
{
id: '',
name: 'Default' // TODO: i18n it
},
{
id: 'silence_stream://300000',
name: 'Silence' // TODO: i18n it
}
);
self.settingsQueueFormBindEvents($container);
var html = $(monster.template(self, 'settings_queue_form', {
data: queueData,
media_list: results.media
}));
if(typeof(callback) === 'function') {
callback();
self.vars.users = results.users;
$container.empty().append(html);
self.settingsQueueFormBindEvents($container);
self.settingsQueueAgentsPanelRender(results.users, queueData.agents, $container);
}
});
},
settingsQueueAgentsPanelRender: function(usersList, selectedAgentsIdList, $container) {
var self = this,
agentsList,
usersWithoutAgents;
agentsList = self.settingsQueueAgentsPanelExtractAgentsData(selectedAgentsIdList, usersList);
usersWithoutAgents = self.settingsQueueAgentsPanelGetUsersWithoutAgents(usersList, agentsList);
var template = $(monster.template(self, 'settings_queue_agents', {
agents: agentsList,
users: usersWithoutAgents
}));
var $parent = $container.find('#queue-agents-content');
$parent.html(template);
self.settingsQueueAgentsPanelInit($parent, function(){
self.settingsQueueAgentsPanelBindEvents($parent);
});
},
settingsQueueAgentsPanelExtractAgentsData: function(selectedAgentsIdList, usersList) {
// fill agents list
var agentsList = [];
for(var a=0, alen= selectedAgentsIdList.length; a<alen; a++) {
for(var u=0, ulen= usersList.length; u<ulen; u++) {
if(selectedAgentsIdList[a] === usersList[u].id) {
console.log('selectedAgentsIdList[a]:');
console.log(selectedAgentsIdList[a]);
console.log('usersList[u]:');
console.log(usersList[u]);
agentsList.push(usersList[u]);
break;
}
}
}
return agentsList;
},
settingsQueueAgentsPanelGetUsersWithoutAgents: function(users, agents) {
var usersWithoutAgents = [];
for(var u=0, ulen=users.length; u<ulen; u++) {
for(var a=0, alen= agents.length; a<alen; a++) {
if(agents[a].id === users[u].id) {
users[u].isAgent = true;
break;
}
}
}
for(u=0, ulen=users.length; u<ulen; u++) {
if(!users[u].isAgent) {
usersWithoutAgents.push(users[u]);
}
}
return usersWithoutAgents;
},
settingsQueueAgentsPanelBindEvents: function($parent) {
var self = this;
var handledClass= 'js-handled';
$parent.find('.js-remove-agent').not('.js-handled').on('click', function(e) {
e.preventDefault();
$(this).closest('li').remove();
self.settingsQueueAgentsPanelUpdateUserTable();
}).addClass(handledClass);
$parent.find('.js-add-agent').not('.js-handled').on('click', function(e) {
e.preventDefault();
var $userContainer = $(this).closest('tr');
var userId = $userContainer.data('user-id');
var $agentsList = $('#queue-agents-list');
var userName = $userContainer.find('.js-user-name').text();
if($agentsList.find('[data-user-id="' + userId + '"]').length > 0) {
// already exist
return;
}
var userItemHTML = ('' +
'<li data-user-id="{{id}}">' +
'<span class="item-name">{{name}}</span>' +
'<a href="#" class="js-remove-agent remove-agent-btn">' +
'<i class="fa fa-remove"></i>' +
'</a>' +
'</li>')
.replace('{{id}}', userId)
.replace('{{name}}', userName);
$agentsList.append(userItemHTML);
self.settingsQueueAgentsPanelBindEvents($agentsList);
self.settingsQueueAgentsPanelUpdateUserTable();
}).addClass(handledClass);
$parent.find('.js-edit-user').not('.js-handled').on('click', function(e) {
e.preventDefault();
// TODO: open popup with user settings for editing
$(this).addClass('js-handled');
}).addClass(handledClass);
},
settingsQueueAgentsPanelUpdateUserTable: function() {
// find exist agents
var self = this;
var agentsIdList = [];
var $agentsItems = $('#queue-agents-list').find('li');
// clear user's agent property
for(var u=0, ulen=self.vars.users.length; u<ulen; u++) {
self.vars.users[u].isAgent = false;
}
$agentsItems.each(function(i, el){
var userId = $(el).data('user-id');
if(userId) {
agentsIdList.push(userId);
}
});
console.log('self.vars.users:');
console.log(self.vars.users);
self.settingsQueueAgentsPanelRender(self.vars.users, agentsIdList, $('#queue-agents-wrapper'))
},
settingsQueueAgentsPanelInit: function($parent, callback) {
var table = $parent.find('#queue-users-table').DataTable({
bStateSave: false,
lengthMenu: [[10, 25, -1], [10, 25, 'All']],
order: [[ 1, 'asc' ]],
autoWidth: false,
columnDefs: [
{
targets: 0,
width: '5%'
},
{
targets : 'no-sort',
orderable: false
}
],
language: {
search: '',
searchPlaceholder: 'Search...'
},
dom: 'ftplB',
buttons: [],
initComplete: function(settings, json) {
if(typeof(callback) !== 'undefined') {
callback(settings, json);
}
}
});
@ -716,35 +945,89 @@ var app = {
$container.find('.js-save-queue').on('click', function(e) {
e.preventDefault();
var data = self.settingsQueueFormGetData($container.find('#queue-form'));
var data = self.serializeFormElements($container.find('#queue-form'));
var queueId = $(this).data('queue-id');
console.log('Queue form data:');
console.log(data);
self.settingsQueueSave(queueId, data, function(){
console.log('Queue saving complete!');
var agentsIdList = [];
$('#queue-agents-list').find('li').each(function(i, el) {
var userId = $(el).data('user-id');
if(userId) {
agentsIdList.push(userId);
}
});
self.settingsAgentsSave(queueId, agentsIdList, function() {
self.settingsQueueSave(queueId, data, function(){
console.log('Queue saving complete!');
});
});
});
$container.find('.js-cancel').on('click', function(e) {
$container.find('.js-delete-queue').on('click', function(e) {
e.preventDefault();
var queueId = $(this).data('queue-id');
// TODO: i18n it!
monster.ui.confirm('Are you sure to remove this queue?', function() {
self.settingsQueueRemove(queueId, function() {
self.settingsRender($('#monster_content'), function(){
// TODO: i18n it!
self.settingsShowMessage('Queue was successfully removed!', 'success')
});
});
});
});
},
settingsQueueFormGetData: function($form) {
settingsQueueRemove: function(queueId, callback) {
var self = this;
var sourceData = self.serializeForm($form);
console.log('Serialized Data:');
console.log(sourceData);
self.callApi({
resource: 'queues.queues_delete',
data: {
accountId: self.accountId,
queuesId: queueId
},
success: function(data, status) {
if(typeof(callback) === 'function') {
callback();
}
}
});
},
settingsAgentsSave: function(queueId, agentsIdList, callback) {
var self = this;
return sourceData;
self.callApi({
resource: 'agents.agents_update',
data: {
accountId: self.accountId,
queuesId: queueId,
data: agentsIdList
},
success: function(data, status) {
console.log('Agents were saved.');
console.log(data);
if(typeof(callback) === 'function') {
callback();
}
}
});
},
serializeForm: function($form) {
serializeFormElements: function($parent, selector) {
if(typeof(selector) === 'undefined') {
selector = '.js-to-serialize[name]';
}
var result = {};
$form.find('[name]').each(function(i, el){
$parent.find(selector).each(function(i, el){
var $el = $(el);
var name = $el.attr('name');


style/app.css → style/app.scss View File


+ 17
- 42
views/settings.html View File

@ -1,47 +1,22 @@
<div class="cc-buttons-panel">
<a href="" class="js-open-cc-dashboard btn">
<i class="fa fa-cog icon-small"></i> Dashboard
</a>
</div>
<div id="cc-settings" class="container-fluid">
<div class="row-fluid">
<div class="sidebar-list-panel span2 well">
<h4 class="sidebar-list-panel__header">
Queues
<a href="" class="js-cc-create-queue btn btn-round">
<i class="fa fa-plus icon-small"></i>
</a>
</h4>
<div id="cc-settings">
<div class="cc-buttons-panel">
<a href="" class="js-open-cc-dashboard btn">
<i class="fa fa-cog icon-small"></i> Dashboard
</a>
</div>
<div class="container-fluid">
<div class="row-fluid">
<div class="sidebar-list-panel span2 well">
<h4 class="sidebar-list-panel__header">
Queues
<a href="" class="js-cc-create-queue btn btn-round">
<i class="fa fa-plus icon-small"></i>
</a>
</h4>
<div id="queues-list-container"></div>
</div>
<div id="cc-settings-content" class="cc-settings-content span10">
<div id="accordion" class="panel-group">
<div class="panel panel-default">
<div class="panel-heading">
<h4 class="panel-title">
<a data-toggle="collapse" data-parent="#accordion" href="#collapseOne" >Queue settings</a>
</h4>
</div>
<div id="collapseOne" class="panel-collapse collapse">
<div class="panel-body">
<p>HTML stands for HyperText Markup Language. HTML is the main markup language for describing the structure of Web pages. <a href="https://www.tutorialrepublic.com/html-tutorial/" target="_blank">Learn more.</a></p>
</div>
</div>
</div>
<div class="panel panel-default">
<div class="panel-heading">
<h4 class="panel-title">
<a data-toggle="collapse" data-parent="#accordion" href="#collapseTwo">Agents</a>
</h4>
</div>
<div id="collapseTwo" class="panel-collapse collapse in">
<div class="panel-body">
<p>Bootstrap is a powerful front-end framework for faster and easier web development. It is a collection of CSS and HTML conventions. <a href="https://www.tutorialrepublic.com/twitter-bootstrap-tutorial/" target="_blank">Learn more.</a></p>
</div>
</div>
</div>
<div id="queues-list-container"></div>
</div>
<div id="cc-settings-content" class="cc-settings-content span10"></div>
</div>
</div>
</div>

+ 42
- 0
views/settings_queue_agents.html View File

@ -0,0 +1,42 @@
<div class="row-fluid">
<div class="span4">
<div class="agents-list-wrapper well-list">
<h3>Selected</h3>
<ul id="queue-agents-list">
{{#each this.agents}}
<li data-user-id="{{ id }}">
<span class="item-name">{{ first_name }} {{ last_name }}</span>
<a href="#" class="js-remove-agent remove-agent-btn">
<i class="fa fa-remove"></i>
</a>
</li>
{{/each}}
</ul>
</div>
</div>
<div class="span8">
<table id="queue-users-table" class="table table-condensed table-striped table-hover">
<thead>
<tr>
<th class="no-sort">Actions</th>
<th>User Name</th>
</tr>
</thead>
<tbody>
{{#each this.users}}
<tr data-user-id="{{ id }}">
<td class="actions-col">
<a href="#" class="mini-btn js-add-agent add-agent-btn">
<i class="fa fa-arrow-left"></i>
</a>
<a href="#" class="mini-btn js-edit-user edit-agent-btn">
<i class="fa fa-pencil-square-o"></i>
</a>
</td>
<td class="js-user-name">{{ first_name }} {{ last_name }}</td>
</tr>
{{/each}}
</tbody>
</table>
</div>
</div>

+ 40
- 18
views/settings_queue_form.html View File

@ -8,12 +8,27 @@
<div class="control-group">
<label class="control-label" for="queue-name">Name</label>
<div class="controls">
<input type="text" value="{{ data.name }}" name="name" data-toggle="tooltip"
<input class="js-to-serialize" type="text" value="{{ data.name }}" name="name" data-toggle="tooltip"
id="queue-name" title="Friendly name for this Queue">
</div>
</div>
<div id="accordion" class="accordion-light">
<div class="panel panel-default">
<div class="panel-heading">
<h4 class="panel-title">
<a data-toggle="collapse" data-parent=".accordion-light" href="#queue-agents-wrapper" class="collapsed">
<i class="fa fa-accordion-mark"></i>
<span>Agents</span>
</a>
</h4>
</div>
<div id="queue-agents-wrapper" class="panel-collapse collapse">
<div class="panel-body queue-agents-content" id="queue-agents-content"></div>
</div>
</div>
<div class="panel panel-default">
<div class="panel-heading">
<h4 class="panel-title">
@ -30,7 +45,8 @@
<div class="controls">
<div class="checkbox">
<label>
<input type="checkbox" value="" name="record_caller" {{#if data.record_caller }}checked="checked"{{/if}} >
<input class="js-to-serialize" type="checkbox" value="" name="record_caller"
{{#if data.record_caller }}checked="checked"{{/if}} >
<span data-toggle="tooltip" title="You can also change individual agent's recording settings under the Agent's section">Call Recording</span>
</label>
</div>
@ -40,7 +56,7 @@
<div class="control-group">
<label class="control-label" for="queue-music-on-hold">Music on Hold</label>
<div class="controls">
<select name="moh" id="queue-music-on-hold" data-toggle="tooltip" title="Select the media file that you want to be played by default when it's not set on a user or a device.">
<select class="js-to-serialize" name="moh" id="queue-music-on-hold" data-toggle="tooltip" title="Select the media file that you want to be played by default when it's not set on a user or a device.">
{{#each media_list}}
{{#compare @root.data.moh "===" id}}
<option id="{{ id }}" value="{{ id }}" selected="selected">{{ name }}</option>
@ -57,7 +73,7 @@
<div class="control-group">
<label class="control-label" for="queue-strategy">Strategy</label>
<div class="controls">
<select id="queue-strategy" name="strategy" data-toggle="tooltip" title="The queue strategy for connecting agents to caller">
<select class="js-to-serialize" id="queue-strategy" name="strategy" data-toggle="tooltip" title="The queue strategy for connecting agents to caller">
{{#compare data.strategy "===" "most_idle"}}
<option value="round_robin">Round Robin</option>
<option value="most_idle" selected="selected">Most Idle</option>
@ -72,7 +88,8 @@
<div class="control-group">
<label class="control-label" for="queue-call-recording-url">Call Recording URL</label>
<div class="controls">
<input id="queue-call-recording-url" type="text" value="{{ data.call_recording_url }}" name="call_recording_url"
<input id="queue-call-recording-url" class="js-to-serialize" type="text"
value="{{ data.call_recording_url }}" name="call_recording_url"
placeholder="http://xxx.yyy.com/call_recordings" data-toggle="tooltip"
title="URL pointing to a server that will host the recording of the calls processed by this queue.">
</div>
@ -81,7 +98,8 @@
<div class="control-group">
<label class="control-label" for="queue-agent-wrapup-time">Call Wrap-up Time (s)</label>
<div class="controls">
<input id="queue-agent-wrapup-time" type="text" name="agent_wrapup_time" placeholder="30"
<input class="js-to-serialize" id="queue-agent-wrapup-time" type="text"
name="agent_wrapup_time" placeholder="30"
value="{{ data.agent_wrapup_time }}" data-toggle="tooltip"
title="Automatic break time between calls for the agents in this queue.">
</div>
@ -90,8 +108,9 @@
<div class="control-group">
<label class="control-label" for="queue-max-queue-size">Max Number of Calls</label>
<div class="controls">
<input type="text" name="max_queue_size" value="{{ data.max_queue_size }}"
id="queue-max-queue-size" placeholder="10000" data-toggle="tooltip"
<input class="js-to-serialize" type="text" name="max_queue_size"
value="{{ data.max_queue_size }}" id="queue-max-queue-size" placeholder="10000"
data-toggle="tooltip"
title="How many callers are allowed to wait on hold in the queue (0 for no limit).">
</div>
</div>
@ -99,8 +118,9 @@
<div class="control-group">
<label class="control-label" for="queue-connection-timeout">Max Hold Time (s)</label>
<div class="controls">
<input type="text" name="connection_timeout" value="{{ data.connection_timeout }}"
id="queue-connection-timeout" placeholder="30" data-toggle="tooltip"
<input class="js-to-serialize" type="text" name="connection_timeout"
value="{{ data.connection_timeout }}" id="queue-connection-timeout" placeholder="30"
data-toggle="tooltip"
title="In seconds, how long to try to connect the caller before progressing past the queue callflow action (0 for no limit).">
</div>
</div>
@ -110,7 +130,8 @@
<div class="controls">
<div class="checkbox">
<label>
<input type="checkbox" name="enter_when_empty" {{#if data.enter_when_empty }}checked="checked"{{/if}}>
<input class="js-to-serialize" type="checkbox" name="enter_when_empty"
{{#if data.enter_when_empty }}checked="checked"{{/if}}>
Allows a caller to enter this queue when no agents are available.
</label>
</div>
@ -134,8 +155,9 @@
<div class="control-group">
<label class="control-label" for="queue-notifications-hangup">Notification on Hangup</label>
<div class="controls">
<input type="text" name="notifications.hangup" value="{{ data.notifications.hangup }}"
id="queue-notifications-hangup" placeholder="http://xxx.yyy/script_hangup.php" data-toggle="tooltip"
<input class="js-to-serialize" type="text" name="notifications.hangup"
value="{{ data.notifications.hangup }}" id="queue-notifications-hangup"
placeholder="http://xxx.yyy/script_hangup.php" data-toggle="tooltip"
title="URL for a callback when the call ends to tell the customer on their own servers that a call has ended.">
</div>
</div>
@ -143,8 +165,9 @@
<div class="control-group">
<label class="control-label" for="queue-notifications-pickup">Notification after Pickup</label>
<div class="controls">
<input type="text" name="notifications.pickup" value="{{ data.notifications.pickup }}"
id="queue-notifications-pickup" placeholder="http://xxx.yyy/script_pickup.php" data-toggle="tooltip"
<input class="js-to-serialize" type="text" name="notifications.pickup"
value="{{ data.notifications.pickup }}" id="queue-notifications-pickup"
placeholder="http://xxx.yyy/script_pickup.php" data-toggle="tooltip"
title="URL for a callback when the call is picked up to tell the customer on their own servers that a call has been picked up.">
</div>
</div>
@ -152,8 +175,8 @@
<div class="control-group">
<label class="control-label" for="queue-notifications-method">Method</label>
<div class="controls">
<select name="notifications.method" data-toggle="tooltip" id="queue-notifications-method"
title="What HTTP method to use">
<select class="js-to-serialize" name="notifications.method" data-toggle="tooltip"
id="queue-notifications-method" title="What HTTP method to use">
{{#compare data.notifications.method "===" "POST"}}
<option value="GET">GET</option>
<option value="POST" selected="selected">POST</option>
@ -176,6 +199,5 @@
{{else}}
<button class="btn btn-primary js-save-queue">Create</button>
{{/if}}
<button class="btn js-cancel">Cancel</button>
</div>
</form>

Loading…
Cancel
Save