| @ -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}} | |||