| @ -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` ) | |||||
| @ -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; | |||||
| }); | |||||
| @ -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" | |||||
| } | |||||
| } | |||||
| @ -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" | |||||
| } | |||||
| } | |||||
| } | |||||
| } | |||||
| @ -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" | |||||
| } | |||||
| @ -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 | |||||
| } | |||||
| @ -0,0 +1,4 @@ | |||||
| <td colspan="2" class="recording-player"> | |||||
| <span class="close-player">{{ i18n.close }}</span> | |||||
| <audio controls src="{{uri}}"></audio> | |||||
| </td> | |||||
| @ -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> | |||||
| @ -0,0 +1,4 @@ | |||||
| <div id="recordings_cdr_details_dialog"> | |||||
| <div id="jsoneditor"> | |||||
| </div> | |||||
| </div> | |||||
| @ -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}} | |||||