commit c0fc19bdc95c4127e5040f92b8e163cb636dd709 Author: Emmanuel Balogun Date: Sat Jul 9 13:13:45 2022 +0100 initial commit for recording app diff --git a/README.md b/README.md new file mode 100755 index 0000000..3be8f28 --- /dev/null +++ b/README.md @@ -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` ) \ No newline at end of file diff --git a/app.js b/app.js new file mode 100755 index 0000000..ed6e333 --- /dev/null +++ b/app.js @@ -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; +}); diff --git a/i18n/de-DE.json b/i18n/de-DE.json new file mode 100755 index 0000000..f8f23b6 --- /dev/null +++ b/i18n/de-DE.json @@ -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" + } +} diff --git a/i18n/en-US.json b/i18n/en-US.json new file mode 100755 index 0000000..9218cbc --- /dev/null +++ b/i18n/en-US.json @@ -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" + } + } + } +} diff --git a/metadata/app.json b/metadata/app.json new file mode 100755 index 0000000..9424709 --- /dev/null +++ b/metadata/app.json @@ -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" +} \ No newline at end of file diff --git a/metadata/icon/recordings.png b/metadata/icon/recordings.png new file mode 100755 index 0000000..a106371 Binary files /dev/null and b/metadata/icon/recordings.png differ diff --git a/metadata/screenshots/recordings1.png b/metadata/screenshots/recordings1.png new file mode 100755 index 0000000..d624997 Binary files /dev/null and b/metadata/screenshots/recordings1.png differ diff --git a/metadata/screenshots/recordings2.png b/metadata/screenshots/recordings2.png new file mode 100755 index 0000000..0ecf83d Binary files /dev/null and b/metadata/screenshots/recordings2.png differ diff --git a/style/app.css b/style/app.css new file mode 100755 index 0000000..38a3101 --- /dev/null +++ b/style/app.css @@ -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 +} \ No newline at end of file diff --git a/views/cell-recording-player.html b/views/cell-recording-player.html new file mode 100755 index 0000000..bd4e79a --- /dev/null +++ b/views/cell-recording-player.html @@ -0,0 +1,4 @@ + + {{ i18n.close }} + + \ No newline at end of file diff --git a/views/received-recordings.html b/views/received-recordings.html new file mode 100755 index 0000000..192250d --- /dev/null +++ b/views/received-recordings.html @@ -0,0 +1,115 @@ +
+
+
+
+
+ {{ i18n.recordings.receivedRECs.actionBar.currentlyViewing }} +
+ +
+
+ +
+
+ {{ i18n.recordings.receivedRECs.actionBar.total }} + +
+
+ + +
+
+ + +
+
+
{{ i18n.recordings.receivedRECs.empty.headline1 }}{{count}}{{ i18n.recordings.receivedRECs.empty.headline2 }}
+
{{ i18n.recordings.receivedRECs.empty.subHeadline }}
+ +
+ +
+
+
+
+ + + + + + + + + + + + + + + +
{{ i18n.recordings.receivedRECs.table.columns.direction }}{{ i18n.recordings.receivedRECs.table.columns.received }}{{ i18n.recordings.receivedRECs.table.columns.from }}{{ i18n.recordings.receivedRECs.table.columns.targetNumber }}{{ i18n.recordings.receivedRECs.table.columns.duration }}
+
+
+
+ +
+
+
+
diff --git a/views/recordings-CDRDialog.html b/views/recordings-CDRDialog.html new file mode 100755 index 0000000..3aa8bab --- /dev/null +++ b/views/recordings-CDRDialog.html @@ -0,0 +1,4 @@ +
+
+
+
\ No newline at end of file diff --git a/views/recordings-rows.html b/views/recordings-rows.html new file mode 100755 index 0000000..e158c52 --- /dev/null +++ b/views/recordings-rows.html @@ -0,0 +1,61 @@ +{{#each recordings}} + + + {{#monsterCheckbox}} + + {{/monsterCheckbox}} +
+ + + {{direction}} + + + +
{{toFriendlyDate timestamp 'date'}}
+
{{toFriendlyDate timestamp 'time'}}
+ + {{#if formatted.showCallerIDName}} + +
+ {{#if formatted.callerIDName.isPhoneNumber}} + {{#monsterNumberWrapper formatted.callerIDName.value}}{{/monsterNumberWrapper}} + {{else}} + {{formatted.callerIDName.value}} + {{/if}} +
+
+ {{#if formatted.from.isPhoneNumber}} + {{#monsterNumberWrapper formatted.from.value}}{{/monsterNumberWrapper}} + {{else}} + {{formatted.from.value}} + {{/if}} +
+ + {{else}} + + {{#if formatted.from.isPhoneNumber}} + {{#monsterNumberWrapper formatted.from.value}}{{/monsterNumberWrapper}} + {{else}} + {{formatted.from.value}} + {{/if}} + + {{/if}} + + {{#if formatted.to.isPhoneNumber}} + {{#monsterNumberWrapper formatted.to.value}}{{/monsterNumberWrapper}} + {{else}} + {{formatted.to.value}} + {{/if}} + + + {{formatted.duration}} + + + + + + + + + +{{/each}}