Browse Source

initial commit for recording app

master
Emmanuel Balogun 3 years ago
commit
c0fc19bdc9
13 changed files with 1230 additions and 0 deletions
  1. +19
    -0
      README.md
  2. +564
    -0
      app.js
  3. +66
    -0
      i18n/de-DE.json
  4. +68
    -0
      i18n/en-US.json
  5. +31
    -0
      metadata/app.json
  6. BIN
      metadata/icon/recordings.png
  7. BIN
      metadata/screenshots/recordings1.png
  8. BIN
      metadata/screenshots/recordings2.png
  9. +298
    -0
      style/app.css
  10. +4
    -0
      views/cell-recording-player.html
  11. +115
    -0
      views/received-recordings.html
  12. +4
    -0
      views/recordings-CDRDialog.html
  13. +61
    -0
      views/recordings-rows.html

+ 19
- 0
README.md View File

@ -0,0 +1,19 @@
# Monster UI Recordings
Allows you to effectively work with call recordings for v4.3
Requires [Monster UI v.4.3](https://github.com/2600hz/monster-ui)
Ensure you've storage setup (Amazon S3, Google Drive, etc.)
Also, ensure you've enabled `cb_recordings` module
#### Installation instructions:
1. Copy the accounts app to your apps directory
2. Register the recording app
```bash
# sup crossbar_maintenance init_app PATH_TO_RECORDING_DIRECTORY API_ROOT
# The Kazoo user should be able to read files from recordings app directory
sup crossbar_maintenance init_app /var/www/html/monster-ui/apps/recordings https://site.com:8443/v2/
```
3. Activate recording app in the Monster UI App Store ( `/#/apps/appstore` )

+ 564
- 0
app.js View File

@ -0,0 +1,564 @@
define(function(require) {
var $ = require('jquery'),
_ = require('lodash'),
monster = require('monster');
var app = {
name: 'recordings',
css: [ 'app' ],
i18n: {
'de-DE': { customCss: false },
'en-US': { customCss: false }
},
appFlags: {
recordings: {
maxRange: 31,
defaultRange: 1,
minPhoneNumberLength: 7
}
},
requests: {
'recordings.user.get': {
'verb': 'GET',
'url': 'accounts/{accountId}/users/{userId}/recordings?{filters}'
},
'recordings.delete': {
'verb': 'DELETE',
'url': 'accounts/{accountId}/recordings/{recordingId}'
}
},
subscribe: {},
load: function(callback) {
var self = this;
self.initApp(function() {
callback && callback(self);
});
},
initApp: function(callback) {
var self = this;
monster.pub('auth.initApp', {
app: self,
callback: callback
});
},
render: function(container) {
var self = this;
var menus = [
{
tabs: [
{
text: self.i18n.active().recordings.menuTitles.receivedRECs,
callback: self.renderReceivedRECs
}
]
}
];
monster.ui.generateAppLayout(self, {
menus: menus
});
},
renderReceivedRECs: function(pArgs) {
var self = this,
args = pArgs || {},
parent = args.container || $('#recordings_app_container .app-content-wrapper');
self.listRECBoxes(function(recboxes) {
var dataTemplate = {
recboxes: recboxes,
count: recboxes.length
},
template = $(self.getTemplate({
name: 'received-recordings',
data: dataTemplate
}));
self.recordingsInitDatePicker(parent, template);
self.bindReceivedRECs(template);
parent
.fadeOut(function() {
$(this)
.empty()
.append(template)
.fadeIn();
});
});
},
recordingsInitDatePicker: function(parent, template) {
var self = this,
dates = monster.util.getDefaultRangeDates(self.appFlags.recordings.defaultRange),
fromDate = dates.from,
toDate = dates.to;
var optionsDatePicker = {
container: template,
range: self.appFlags.recordings.maxRange
};
monster.ui.initRangeDatepicker(optionsDatePicker);
template.find('#startDate').datepicker('setDate', fromDate);
template.find('#endDate').datepicker('setDate', toDate);
template.find('.apply-filter').on('click', function(e) {
var recboxId = template.find('#select_recbox').val();
self.displayRECList(parent, recboxId);
});
template.find('.toggle-filter').on('click', function() {
template.find('.filter-by-date').toggleClass('active');
});
},
bindReceivedRECs: function(template) {
var self = this,
$selectRECBox = template.find('.select-recbox');
monster.ui.tooltips(template);
monster.ui.footable(template.find('.footable'));
monster.ui.chosen($selectRECBox, {
placeholder_text_single: self.i18n.active().recordings.receivedRECs.actionBar.selectREC.none
});
$selectRECBox.on('change', function() {
var recboxId = $(this).val();
// We update the select-recbox from the listing recordings when we click on a recbox in the welcome page
template.find('.select-recbox').val(recboxId).trigger('chosen:updated');
self.displayRECList(template, recboxId);
});
template.find('#refresh_recordings').on('click', function() {
var recboxId = $selectRECBox.val();
if (recboxId !== 'none') {
self.displayRECList(template, recboxId);
}
});
template.find('.mark-as-link').on('click', function() {
var folder = $(this).data('type'),
recboxId = $selectRECBox.val(),
$recordings = template.find('.select-recording:checked'),
recordings = [];
$recordings.each(function() {
recordings.push($(this).data('media-id'));
});
template.find('.data-state')
.hide();
template.find('.loading-state')
.show();
});
template.find('.delete-recordings').on('click', function() {
var recboxId = $selectRECBox.val(),
$recordings = template.find('.select-recording:checked'),
recordings = [];
$recordings.each(function() {
recordings.push($(this).data('media-id'));
});
template.find('.data-state')
.hide();
template.find('.loading-state')
.show();
self.bulkRemoveRecordings(recboxId, recordings, function() {
self.displayRECList(template, recboxId);
});
});
template.on('click', '.play-rec', function(e) {
var $row = $(this).parents('.recording-row'),
$activeRows = template.find('.recording-row.active');
if (!$row.hasClass('active') && $activeRows.length !== 0) {
return;
}
e.stopPropagation();
var recboxId = template.find('#select_recbox').val(),
mediaId = $row.data('media-id');
template.find('table').addClass('highlighted');
$row.addClass('active');
self.playRecording(template, recboxId, mediaId);
});
template.on('click', '.details-rec', function() {
var $row = $(this).parents('.recording-row'),
callId = $row.data('call-id');
self.getCDR(callId, function(cdr) {
var template = $(self.getTemplate({
name: 'recordings-CDRDialog'
}));
monster.ui.renderJSON(cdr, template.find('#jsoneditor'));
monster.ui.dialog(template, { title: self.i18n.active().recordings.receivedRECs.CDRPopup.title });
}, function() {
monster.ui.alert(self.i18n.active().recordings.receivedRECs.noCDR);
});
});
var afterSelect = function() {
if (template.find('.select-recording:checked').length) {
template.find('.hidable').removeClass('hidden');
template.find('.main-select-recording').prop('checked', true);
} else {
template.find('.hidable').addClass('hidden');
template.find('.main-select-recording').prop('checked', false);
}
};
template.on('change', '.select-recording', function() {
afterSelect();
});
template.find('.main-select-recording').on('click', function() {
var $this = $(this),
isChecked = $this.prop('checked');
template.find('.select-recording').prop('checked', isChecked);
afterSelect();
});
template.find('.select-some-recordings').on('click', function() {
var $this = $(this),
type = $this.data('type');
template.find('.select-recording').prop('checked', false);
if (type !== 'none') {
if (type === 'all') {
template.find('.select-recording').prop('checked', true);
} else if (['new', 'saved', 'deleted'].indexOf(type) >= 0) {
template.find('.recording-row[data-folder="' + type + '"] .select-recording').prop('checked', true);
}
}
afterSelect();
});
template.on('click', '.select-line', function() {
if (template.find('table').hasClass('highlighted')) {
return;
}
var cb = $(this).parents('.recording-row').find('.select-recording');
cb.prop('checked', !cb.prop('checked'));
afterSelect();
});
},
removeOpacityLayer: function(template, $activeRows) {
$activeRows.find('.recording-player').remove();
$activeRows.find('.duration, .actions').show();
$activeRows.removeClass('active');
template.find('table').removeClass('highlighted');
},
formatRECURI: function(recboxId, mediaId) {
var self = this;
return self.apiUrl + 'accounts/' + self.accountId + '/recordings/' + mediaId + '?accept=audio/mpeg&auth_token=' + self.getAuthToken();
},
playRecording: function(template, recboxId, mediaId) {
var self = this,
$row = template.find('.recording-row[data-media-id="' + mediaId + '"]');
template.find('table').addClass('highlighted');
$row.addClass('active');
$row.find('.duration, .actions').hide();
var uri = self.formatRECURI(recboxId, mediaId),
dataTemplate = {
uri: uri
},
templateCell = $(self.getTemplate({
name: 'cell-recording-player',
data: dataTemplate
}));
$row.append(templateCell);
var closePlayerOnClickOutside = function(e) {
if ($(e.target).closest('.recording-player').length) {
return;
}
e.stopPropagation();
closePlayer();
},
closePlayer = function() {
$(document).off('click', closePlayerOnClickOutside);
self.removeOpacityLayer(template, $row);
};
$(document).on('click', closePlayerOnClickOutside);
templateCell.find('.close-player').on('click', closePlayer);
// Autoplay in JS. For some reason in HTML, we can't pause the stream properly for the first play.
templateCell.find('audio').get(0).play();
},
recordingsGetRows: function(filters, recboxId, callback) {
var self = this;
self.newGetRECBoxMessages(filters, recboxId, function(data) {
var formattedData = self.formatRecordingsData(data.data, recboxId),
dataTemplate = {
recordings: formattedData.recordings
},
$rows = $(self.getTemplate({
name: 'recordings-rows',
data: dataTemplate
}));
callback && callback($rows, data, formattedData);
});
},
displayRECList: function(container, recboxId) {
var self = this,
fromDate = container.find('input.filter-from').datepicker('getDate'),
toDate = container.find('input.filter-to').datepicker('getDate'),
filterByDate = container.find('.filter-by-date').hasClass('active');
container.removeClass('empty');
//container.find('.counts-wrapper').hide();
container.find('.count-wrapper[data-type="new"] .count-text').html('?');
container.find('.count-wrapper[data-type="total"] .count-text').html('?');
// Gives a better feedback to the user if we empty it as we click... showing the user something is happening.
container.find('.data-state')
.hide();
container.find('.loading-state')
.show();
container.find('.hidable').addClass('hidden');
container.find('.main-select-recording').prop('checked', false);
monster.ui.footable(container.find('.recordings-table .footable'), {
getData: function(filters, callback) {
if (filterByDate) {
filters = $.extend(true, filters, {
created_from: monster.util.dateToBeginningOfGregorianDay(fromDate),
created_to: monster.util.dateToEndOfGregorianDay(toDate)
});
}
// we do this to keep context
self.recordingsGetRows(filters, recboxId, function($rows, data, formattedData) {
container.find('.count-wrapper[data-type="new"] .count-text').html(formattedData.counts.newRecordings);
container.find('.count-wrapper[data-type="total"] .count-text').html(formattedData.counts.totalRecordings);
callback && callback($rows, data);
});
},
afterInitialized: function() {
container.find('.data-state')
.show();
container.find('.loading-state')
.hide();
},
backendPagination: {
enabled: false
}
});
},
formatRecordingsData: function(recordings, recboxId) {
var self = this,
tryFormatPhoneNumber = function(value) {
var minPhoneNumberLength = self.appFlags.recordings.minPhoneNumberLength,
prefixedPhoneNumber,
formattedPhoneNumber;
if (_.size(value) < minPhoneNumberLength) {
return {
isPhoneNumber: false,
value: value,
userFormat: value
};
}
prefixedPhoneNumber = _.head(value) === '+'
? value
: /^\d+$/.test(value) // Prepend '+' if there are only numbers
? '+' + value
: value;
formattedPhoneNumber = monster.util.getFormatPhoneNumber(prefixedPhoneNumber);
return {
isPhoneNumber: formattedPhoneNumber.isValid,
value: formattedPhoneNumber.isValid
? formattedPhoneNumber.e164Number
: value,
userFormat: formattedPhoneNumber.isValid
? formattedPhoneNumber.userFormat
: value
};
},
formattedRecordings = _.map(recordings, function(rec) {
var to = rec.to.substr(0, rec.to.indexOf('@')),
from = rec.from.substr(0, rec.from.indexOf('@')),
callerIDName = _.get(rec, 'caller_id_name', ''),
formattedTo = tryFormatPhoneNumber(to),
formattedFrom = tryFormatPhoneNumber(from),
formattedCallerIDName = tryFormatPhoneNumber(callerIDName);
return _.merge({
formatted: {
to: formattedTo,
from: formattedFrom,
callerIDName: formattedCallerIDName,
duration: monster.util.friendlyTimer(rec.duration_ms / 1000),
uri: self.formatRECURI(recboxId, rec.id),
callId: monster.util.getModbID(rec.call_id, rec.start),
mediaId: rec.id,
showCallerIDName: formattedCallerIDName.value !== formattedFrom.value
},
direction: rec.direction,
timestamp: rec.start
}, rec);
}),
formattedData = {
recordings: formattedRecordings,
counts: {
newRecordings: _.sumBy(recordings, function(rec) {
return _
.chain(rec)
.get('folder')
.isEqual('new')
.toInteger()
.value();
}),
totalRecordings: recordings.length
}
};
return formattedData;
},
getCDR: function(callId, callback, error) {
var self = this;
self.callApi({
resource: 'cdrs.get',
data: {
accountId: self.accountId,
cdrId: callId,
generateError: false
},
success: function(data) {
callback && callback(data.data);
},
error: function(data, status, globalHandler) {
if (data && data.error === '404') {
error && error({});
} else {
globalHandler(data, { generateError: true });
}
}
});
},
getRECBox: function(recboxId, callback) {
var self = this;
monster.request({
resource: 'recordings.user.get',
data: {
accountId: self.accountId,
userId: recboxId
},
success: function(data) {
callback && callback(data.data);
}
});
},
newGetRECBoxMessages: function(filters, recboxId, callback) {
var self = this;
monster.request({
resource: 'recordings.user.get',
data: {
accountId: self.accountId,
userId: recboxId,
filters: filters
},
success: function(data) {
callback && callback(data);
}
});
},
bulkRemoveRecordings: function(recboxId, recordings, callback) {
var self = this;
$.each(recordings, function(i, recordingId){
monster.request({
resource: 'recordings.delete',
data: {
accountId: self.accountId,
recordingId: recordingId
},
success: function(data) {
callback && callback(data.data);
}
});
})
},
listRECBoxes: function(callback) {
var self = this;
self.callApi({
resource: 'user.list',
data: {
accountId: self.accountId,
filters: {
paginate: false
}
},
success: function(data) {
callback && callback(data.data);
}
});
}
};
return app;
});

+ 66
- 0
i18n/de-DE.json View File

@ -0,0 +1,66 @@
{
"recordings": {
"menuTitles": {
"receivedRECs": "Empfangene Sprachnachrichten",
"storage": "Speicher"
},
"receivedRECs": {
"actionBar": {
"to": "An",
"delete": "Löschen",
"tooltips": {
"refresh": "Aktualisieren",
"delete": "Dadurch werden die ausgewählten Spachnachrichten aus der Datenbank entfernt und können danach auch nicht mehr abgehört werden.",
"moveTo": "Verschieben nach",
"select": "Auswählen",
"markAs": "Markieren als"
},
"markAsDeleted": "Als gelöscht markieren",
"markAsListened": "Als gehört markieren",
"new": "Neu:",
"selectREC": {
"none": "Einen Anrufbeantworter auswählen"
},
"select": {
"deleted": "Gelöscht",
"listened": "Gehört",
"all": "Alle auf Seite",
"none": "Keine",
"new": "Neu"
},
"currentlyViewing": "Derzeit angezeigt",
"markAsNew": "Als neu markieren",
"from": "Von",
"total": "Insgesamt:"
},
"status": {
"deleted": "Gelöscht",
"new": "Neu",
"saved": "Gehört"
},
"table": {
"columns": {
"from": "Von",
"duration": "Dauer",
"name": "Name",
"targetNumber": "Zielnummer",
"status": "Status",
"callId": "Anruf-ID",
"received": "Empfangen"
},
"emptyRow": "Im ausgewählten Anrufbeantworter sind keine Nachrichten gespeichert"
},
"empty": {
"headline1": "Es gibt",
"headline2": "Anrufbeantworter für dieses Konto",
"subHeadline": "Wählen Sie einen Anrufbeantworter aus"
},
"filterByDate": "Nach Datum filtern",
"CDRPopup": {
"title": "EVN-Details"
},
"noCDR": "Wir haben keinen zugehörigen EVN-Datensatz zu dieser Nachricht gefunden. Wenn es sich um eine alte Nachricht handelte"
},
"title": "Voicemail-Manager"
}
}

+ 68
- 0
i18n/en-US.json View File

@ -0,0 +1,68 @@
{
"recordings": {
"title": "Recordings Manager",
"menuTitles": {
"receivedRECs": "Received Recordings",
"storage": "Storage"
},
"receivedRECs": {
"filterByDate": "Filter by Dates",
"table": {
"columns": {
"status": "Status",
"direction": "Direction",
"callId": "Call ID",
"received": "Received",
"from": "From",
"targetNumber": "Target Number",
"duration": "Duration",
"name": "Name"
},
"emptyRow": "There are no recordings stored for the selected User"
},
"empty": {
"headline1": "There are",
"headline2": "users recognized on this account",
"subHeadline": "Select a user to manage the recordings it contains."
},
"actionBar": {
"select": {
"all": "All on page",
"new": "New",
"listened": "Listened",
"deleted": "Deleted",
"none": "None"
},
"selectREC": {
"none": "Select a User"
},
"from": "From",
"to": "To",
"markAsNew": "Mark as New",
"markAsListened": "Mark as Listened",
"markAsDeleted": "Mark as Deleted",
"tooltips": {
"markAs": "Delete",
"refresh": "Refresh",
"select": "Select",
"moveTo": "Move To",
"delete": "This will remove the selected recordings from the database."
},
"delete": "Delete",
"currentlyViewing": "Currently Viewing",
"new": "New:",
"total": "Total:"
},
"status": {
"saved": "Listened",
"deleted": "Deleted",
"new": "New"
},
"direction": "Direction",
"noCDR": "We didn't find any corresponding CDR to this recording. It is possible that the logs have been deleted from the system if this is an old recording.",
"CDRPopup": {
"title": "CDR Details"
}
}
}
}

+ 31
- 0
metadata/app.json View File

@ -0,0 +1,31 @@
{
"name": "recordings",
"i18n": {
"en-US": {
"label": "Recordings",
"description": "The Recordings app allows you to effectively work with call recordings"
},
"de-De": {
"label": "Recordings",
"description": "The Recordings app allows you to effectively work with call recordings"
}
},
"tags": [
"reseller"
],
"icon": "recordings.png",
"api_url": "",
"author": "Baloeng",
"version": "1.0",
"license": "-",
"price": 0,
"screenshots": [
"recordings1.png",
"recordings2.png"
],
"urls": {
"documentation": "{documentation_url}",
"howto": "{howto_video_url}"
},
"pvt_type": "app"
}

BIN
metadata/icon/recordings.png View File

Before After
Width: 512  |  Height: 512  |  Size: 4.7 KiB

BIN
metadata/screenshots/recordings1.png View File

Before After
Width: 2154  |  Height: 964  |  Size: 127 KiB

BIN
metadata/screenshots/recordings2.png View File

Before After
Width: 2194  |  Height: 964  |  Size: 76 KiB

+ 298
- 0
style/app.css View File

@ -0,0 +1,298 @@
.received-recordings-container.empty .empty-state {
display: block
}
.received-recordings-container .empty-state {
display: none;
text-align: center
}
.received-recordings-container .empty-state .headline {
font-size: 22px;
margin-top: 35px
}
.received-recordings-container .empty-state .sub-headline {
color: #606069;
font-size: 16px;
margin-top: 13px
}
.received-recordings-container .empty-state .count {
font-weight: 700;
margin-left: 5px;
margin-right: 5px
}
.received-recordings-container .empty-state .recboxes-list {
margin-top: 30px;
width: 450px;
text-align: left
}
.received-recordings-container .empty-state .chosen-drop {
width: 450px
}
.received-recordings-container .empty-state .chosen-container>a,
.received-recordings-container .main-header .chosen-container>a {
height: 40px;
line-height: 40px;
width: 450px
}
.received-recordings-container .main-header {
margin-bottom: 25px
}
.received-recordings-container .main-header .select-header {
color: #606069;
margin-bottom: 5px
}
.received-recordings-container .main-header .chosen-container>a,
.received-recordings-container .main-header .chosen-drop,
.received-recordings-container .main-header .recbox-selector,
.received-recordings-container .main-header .recboxes-list {
width: 300px
}
.received-recordings-container .main-header>* {
display: inline-block;
margin-right: 25px
}
.received-recordings-container .main-header #refresh_recordings {
margin-top: 35px
}
.received-recordings-container .main-header .recbox-selector .chosen-container .chosen-single span {
font-size: 18px
}
.received-recordings-container .main-header .counts-wrapper {
background: #fff;
border: 1px solid #c0c0c9;
border-radius: 2px;
margin-right: 25px;
margin-top: 24px
}
.received-recordings-container .main-header .counts-wrapper .count-wrapper {
color: #606069;
display: inline-block;
height: 26px;
padding: 5px 10px;
text-align: center;
min-width: 65px
}
.received-recordings-container .main-header .counts-wrapper .count-wrapper .count-text {
font-size: 24px;
color: #333;
padding-left: 7px
}
.received-recordings-container .main-header .counts-wrapper .count-wrapper[data-type=new] {
border-top: 3px solid #33db24
}
.received-recordings-container .main-header .counts-wrapper .count-wrapper[data-type=new] .count-text {
color: #33db24
}
.received-recordings-container .main-header .counts-wrapper .count-wrapper:not(:last-child) {
border-right: 1px solid #c0c0c9
}
.received-recordings-container .data-state {
display: none
}
.received-recordings-container .loading-state {
display: none;
background: #fff none repeat scroll 0 0;
border: 1px dashed #aaa;
font-size: 60px;
padding: 50px;
text-align: center;
position: relative;
top: 50px
}
.received-recordings-container .recboxes-list {
display: block;
margin: auto;
width: 400px
}
.received-recordings-container.empty .filters.basic-actions>*,
.received-recordings-container.empty .filters.search,
.received-recordings-container.empty .main-header {
display: none
}
.received-recordings-container .filters.basic-actions {
display: inline-block
}
.received-recordings-container:not(.empty) .action-bar {
display: block
}
.received-recordings-container .action-bar {
display: none
}
.received-recordings-container .action-bar .filters>:first-child {
margin-left: 0
}
.received-recordings-container .action-bar .margin-left {
margin-left: 10px
}
.received-recordings-container .action-bar .hidable {
display: inline-block
}
.received-recordings-container .action-bar .hidable.hidden {
display: none
}
.received-recordings-container .action-bar .move-to-wrapper {
margin-left: 10px
}
.received-recordings-container .recordings-table table {
margin-top: 0
}
.received-recordings-container .recordings-table tbody tr>td:first-child {
position: relative
}
.received-recordings-container .recordings-table tbody tr>td .disable-cell-content {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 10;
display: none
}
.received-recordings-container .recordings-table .highlighted tbody tr>td .disable-cell-content {
display: block
}
.received-recordings-container .recordings-table tbody tr>td:first-child .monster-checkbox {
margin-right: 10px;
margin-top: 8px
}
.received-recordings-container .recordings-table table.highlighted tbody tr {
opacity: .3
}
.received-recordings-container .recordings-table table.highlighted tbody tr.active {
opacity: 1
}
.received-recordings-container .recordings-table .select-cell {
min-width: 20px !important
}
.received-recordings-container .recordings-table .status {
text-transform: uppercase
}
.received-recordings-container .recordings-table .select-line {
cursor: pointer
}
.received-recordings-container .recordings-table tr .bottom-line {
color: #707079;
font-size: 12px
}
.received-recordings-container .recordings-table td.no-padding {
padding: 0
}
.received-recordings-container .dropdown-menu.vmbox-target {
height: 300px;
overflow-y: auto;
overflow-x: hidden
}
.received-recordings-container .recordings-table .actions {
width: 128px
}
.received-recordings-container .recordings-table .recording-player {
display: table-cell;
width: 175px
}
.received-recordings-container .recordings-table .close-player {
cursor: pointer
}
.received-recordings-container .recordings-table .recording-player audio {
width: 170px;
float: right
}
.received-recordings-container .recordings-table .recording-player .close-player {
border: 1px solid #aaa;
border-radius: 0 4px 4px 0;
color: #888;
float: right;
font-weight: 700;
margin: 0;
padding: 3px 12px
}
.received-recordings-container .filter-by-date .date-ranges>* {
margin: 0 10px 0 0;
vertical-align: middle
}
.received-recordings-container .filter-by-date .date-ranges>span {
margin-right: 5px
}
.received-recordings-container .filter-by-date .date-ranges input.date-filter {
height: 24px;
width: 90px
}
.received-recordings-container .filter-by-date .date-ranges i.fa-calendar {
margin-left: -30px;
margin-right: 20px
}
.received-recordings-container .filter-by-date {
float: right;
line-height: 30px;
margin-left: 30px
}
.received-recordings-container .filter-by-date.active .expand-dates {
float: right
}
.received-recordings-container .filter-by-date .date-ranges,
.received-recordings-container .filter-by-date.active .expand-dates {
display: none
}
.received-recordings-container .filter-by-date.active .date-ranges {
display: block
}
#recordings_cdr_details_dialog {
width: 750px;
margin: 15px
}

+ 4
- 0
views/cell-recording-player.html View File

@ -0,0 +1,4 @@
<td colspan="2" class="recording-player">
<span class="close-player">{{ i18n.close }}</span>
<audio controls src="{{uri}}"></audio>
</td>

+ 115
- 0
views/received-recordings.html View File

@ -0,0 +1,115 @@
<div class="received-recordings-container empty">
<div class="main-header clearfix">
<div class="recording-selection-wrapper pull-left">
<div class="filters recbox-selector">
<div class="select-header">
{{ i18n.recordings.receivedRECs.actionBar.currentlyViewing }}
</div>
<select class="select-recbox" id="select_recbox">
<option value="none"></option>
{{#each recboxes}}
<option value="{{id}}">{{first_name}} {{last_name}}</option>
{{/each}}
</select>
</div>
</div>
<div class="counts-wrapper pull-left">
<div class="count-wrapper" data-type="total">
{{ i18n.recordings.receivedRECs.actionBar.total }}
<span class="count-text"></span>
</div>
</div>
<a id="refresh_recordings" class="monster-link pull-left" href="javascript:void(0);" data-toggle="tooltip" data-placement="top" data-original-title="{{ i18n.recordings.receivedRECs.actionBar.tooltips.refresh }}"><i class="fa fa-refresh"></i></a>
</div>
<div class="monster-table-wrapper-spaced">
<div class="action-bar monster-table-header">
<div class="filters basic-actions pull-left">
<div class="select-recordings-wrapper monster-select-dropdown-neutral pull-left" data-toggle="tooltip" data-placement="top" data-original-title="{{ i18n.recordings.receivedRECs.actionBar.tooltips.select }}">
<li class="dropdown">
<a data-toggle="dropdown" class="dropdown-toggle" href="javascript:void(0);">
{{#monsterCheckbox}}
<input class="main-select-recording" type="checkbox"/>
{{/monsterCheckbox}}
<i class="fa fa-caret-down"></i>
</a>
<ul class="dropdown-menu">
<li><a href="javascript:void(0);" class="select-some-recordings" data-type="all">{{ i18n.recordings.receivedRECs.actionBar.select.all }}</a></li>
<li><a href="javascript:void(0);" class="select-some-recordings" data-type="none">{{ i18n.recordings.receivedRECs.actionBar.select.none }}</a></li>
</ul>
</li>
</div>
<div class="filters selected-actions pull-left margin-left">
<div class="mark-as-wrapper hidable hidden monster-select-dropdown-neutral pull-left" data-toggle="tooltip" data-placement="top" data-original-title="{{ i18n.recordings.receivedRECs.actionBar.tooltips.markAs }}">
<li class="dropdown">
<a data-toggle="dropdown" class="dropdown-toggle" href="javascript:void(0);">
<i class="icon-telicon-prioritize"></i>
<i class="fa fa-caret-down"></i>
</a>
<ul class="dropdown-menu">
<li><a href="javascript:void(0);" class="delete-recordings"><i class="fa fa-trash"></i>{{ i18n.recordings.receivedRECs.actionBar.delete }}</a></li>
</ul>
</li>
</div>
</div>
</div>
<div class="filter-by-date">
<div class="expand-dates">
<a class="monster-link blue toggle-filter">{{ i18n.recordings.receivedRECs.filterByDate }}</a>
</div>
<div class="date-ranges">
<span>{{i18n.startDate}}</span>
<input id="startDate" type="text" class="date-filter filter-from">
<i class="fa fa-calendar"></i>
<span>{{i18n.endDate}}</span>
<input id="endDate" type="text" class="date-filter filter-to">
<i class="fa fa-calendar"></i>
<button type="button" class="apply-filter monster-button-neutral monster-button-fit">{{i18n.filter}}</button>
<a class="monster-link blue toggle-filter">{{i18n.cancel}}</a>
</div>
</div>
</div>
<div class="content">
<div class="empty-state">
<div class="headline">{{ i18n.recordings.receivedRECs.empty.headline1 }}<span class="count">{{count}}</span>{{ i18n.recordings.receivedRECs.empty.headline2 }}</div>
<div class="sub-headline">{{ i18n.recordings.receivedRECs.empty.subHeadline }}</div>
<div class="recboxes-list">
<select class="select-recbox" id="select_recbox_empty">
<option value="none"></option>
{{#each recboxes}}
<option class="box-row" data-id="{{id}}" value="{{id}}">{{first_name}} {{last_name}}</option>
{{/each}}
</select>
</div>
</div>
<div class="data-state">
<div class="recordings-table">
<table class="monster-table footable" id="recordings_table">
<thead>
<tr>
<th class="select-cell" data-type="html" data-sortable="false"></th>
<th data-type="html">{{ i18n.recordings.receivedRECs.table.columns.direction }}</th>
<th data-type="html" data-sorted="true" data-direction="DESC">{{ i18n.recordings.receivedRECs.table.columns.received }}</th>
<th data-type="html">{{ i18n.recordings.receivedRECs.table.columns.from }}</th>
<th data-type="html">{{ i18n.recordings.receivedRECs.table.columns.targetNumber }}</th>
<th data-breakpoints="xs">{{ i18n.recordings.receivedRECs.table.columns.duration }}</th>
<th data-filterable="false" data-type="html" data-sortable="false"></th>
</tr>
</thead>
<tbody>
</tbody>
</table>
</div>
</div>
<div class="loading-state">
<i class="fa fa-spin fa-spinner monster-primary-color"></i>
</div>
</div>
</div>
</div>

+ 4
- 0
views/recordings-CDRDialog.html View File

@ -0,0 +1,4 @@
<div id="recordings_cdr_details_dialog">
<div id="jsoneditor">
</div>
</div>

+ 61
- 0
views/recordings-rows.html View File

@ -0,0 +1,61 @@
{{#each recordings}}
<tr class="recording-row" data-media-id="{{formatted.mediaId}}" data-call-id="{{formatted.callId}}" data-timestamp="{{timestamp}}" data-folder="{{folder}}">
<td style="width:20px;" class="select-cell" data-filter-value="{{formatted.callId}}">
{{#monsterCheckbox}}
<input class="select-recording" type="checkbox" data-media-id="{{formatted.mediaId}}"/>
{{/monsterCheckbox}}
<div class="disable-cell-content"></div>
</td>
<td class="status select-line" data-folder="{{folder}}" data-sort-value="{{folder}}">
{{direction}}
</td>
<!--<td class="select-line">{{call_id}}</td>-->
<td class="select-line no-padding" data-filter-value="{{timestamp}} {{toFriendlyDate timestamp}}" data-sort-value="{{timestamp}}">
<div class="top-line">{{toFriendlyDate timestamp 'date'}}</div>
<div class="bottom-line">{{toFriendlyDate timestamp 'time'}}</div>
</td>
{{#if formatted.showCallerIDName}}
<td class="select-line no-padding" data-filter-value="{{from}} {{formatted.from.value}} {{formatted.callerIDName.value}} {{formatted.from.userFormat}} {{formatted.callerIDName.userFormat}}" data-sort-value="{{formatted.from.value}} {{formatted.callerIDName.value}}" >
<div class="top-line">
{{#if formatted.callerIDName.isPhoneNumber}}
{{#monsterNumberWrapper formatted.callerIDName.value}}{{/monsterNumberWrapper}}
{{else}}
{{formatted.callerIDName.value}}
{{/if}}
</div>
<div class="bottom-line">
{{#if formatted.from.isPhoneNumber}}
{{#monsterNumberWrapper formatted.from.value}}{{/monsterNumberWrapper}}
{{else}}
{{formatted.from.value}}
{{/if}}
</div>
</td>
{{else}}
<td class="select-line" data-filter-value="{{from}} {{formatted.from.value}} {{formatted.callerIDName.value}} {{formatted.from.userFormat}} {{formatted.callerIDName.userFormat}}" data-sort-value="{{formatted.from.value}} {{formatted.callerIDName.value}}">
{{#if formatted.from.isPhoneNumber}}
{{#monsterNumberWrapper formatted.from.value}}{{/monsterNumberWrapper}}
{{else}}
{{formatted.from.value}}
{{/if}}
</td>
{{/if}}
<td class="select-line" data-filter-value="{{to}} {{formatted.to.value}} {{formatted.to.userFormat}}" data-sort-value="{{formatted.to.value}}">
{{#if formatted.to.isPhoneNumber}}
{{#monsterNumberWrapper formatted.to.value}}{{/monsterNumberWrapper}}
{{else}}
{{formatted.to.value}}
{{/if}}
</td>
<td class="duration select-line">
{{formatted.duration}}
</td>
<td class="actions">
<i class="fa fa-play-circle action-item play-rec"></i>
<a class="action-item" href="{{formatted.uri}}" target="_blank">
<i class="fa fa-download download-rec"></i>
</a>
<i class="fa fa-list action-item details-rec"></i>
</td>
</tr>
{{/each}}

Loading…
Cancel
Save