define(function(require) { var $ = require('jquery'), _ = require('lodash'), monster = require('monster'); var app = { requests: { 'provisioner.ui.getModel': { 'apiRoot': monster.config.api.provisioner, 'url': 'ui/{brand}/{family}/{model}', 'verb': 'GET', generateError: false }, 'provisioner.devices.unlock': { 'apiRoot': monster.config.api.provisioner, 'url': 'locks/{accountId}/{macAddress}', 'verb': 'DELETE' } }, subscribe: { 'voip.devices.render': 'devicesRender', 'voip.devices.renderAdd': 'devicesRenderAdd', 'voip.devices.editDevice': 'devicesRenderEdit' }, appFlags: { devices: { iconClassesByDeviceTypes: { application: 'icon-telicon-apps', ata: 'icon-telicon-ata', cellphone: 'fa fa-phone', fax: 'icon-telicon-fax', landline: 'icon-telicon-home', mobile: 'icon-telicon-sprint-phone', sip_device: 'icon-telicon-voip-phone', sip_uri: 'icon-telicon-voip-phone', smartphone: 'icon-telicon-mobile-phone', softphone: 'icon-telicon-soft-phone' }, /** * Lists device types allowed to be added by devicesRenderAdd. * The order is important and controls the list rendered in DOM. * @type {Array} */ addableDeviceTypes: [ 'sip_device', 'cellphone', 'smartphone', 'softphone', 'landline', 'fax', 'ata', 'sip_uri' ], /** * Lists device types allowed to be edited by devicesRenderEdit. * @type {Array} */ editableDeviceTypes: [ 'ata', 'cellphone', 'fax', 'landline', 'mobile', 'sip_device', 'sip_uri', 'smartphone', 'softphone' ], provisionerConfigFlags: monster.config.whitelabel.provisioner } }, /* Users */ /* args: parent and deviceId */ devicesRender: function(pArgs) { var self = this, args = pArgs || {}, parent = args.parent || $('.right-content'), _deviceId = args.deviceId || '', callback = args.callback; self.devicesGetData(function(data) { var dataTemplate = self.devicesFormatListData(data), template = $(self.getTemplate({ name: 'layout', data: dataTemplate, submodule: 'devices' })), templateDevice; _.each(dataTemplate.devices, function(device) { templateDevice = $(self.getTemplate({ name: 'row', data: device, submodule: 'devices' })); template.find('.devices-rows').append(templateDevice); }); self.devicesBindEvents(template, parent, dataTemplate); parent .empty() .append(template); if (_deviceId) { var row = parent.find('.grid-row[data-id=' + _deviceId + ']'); monster.ui.highlight(row, { endColor: '#FCFCFC' }); } if (dataTemplate.devices.length === 0) { parent.find('.no-devices-row').css('display', 'block'); } else { parent.find('.no-devices-row').css('display', 'none'); } callback && callback(); }); }, devicesBindEvents: function(template, parent, data) { var self = this; setTimeout(function() { template.find('.search-query').focus(); }); template.find('.devices-header .search-query').on('keyup', function() { var searchString = $(this).val().toLowerCase(), rows = template.find('.devices-rows .grid-row:not(.title)'), emptySearch = template.find('.devices-rows .empty-search-row'); _.each(rows, function(pRow) { var row = $(pRow); row.data('search').toLowerCase().indexOf(searchString) < 0 ? row.hide() : row.show(); }); if (rows.size() > 0) { rows.is(':visible') ? emptySearch.hide() : emptySearch.show(); } }); template.find('.switch-state').on('change', function() { var toggle = $(this), row = toggle.parents('.grid-row'), deviceId = row.data('id'), enable = toggle.prop('checked'); self.devicesGetDevice(deviceId, function(dataDevice) { dataDevice.enabled = enable; self.devicesUpdateDevice(dataDevice, function(dataDevice) { row.find('.type').removeClass('unregistered registered disabled'); var classStatus = 'disabled'; if (dataDevice.enabled === true) { classStatus = 'unregistered'; _.each(data.devices, function(device) { if (device.id === dataDevice.id) { if (device.registered === true) { classStatus = 'registered'; } return false; } }); } row.find('.type').addClass(classStatus); //We could display a success message but that could spam the user so for now we don't display anything }, function() { toggle.prop('checked', !enable); }); }, function() { toggle.prop('checked', !enable); }); }); template.find('.settings').on('click', function() { var $this = $(this), action = $this.data('action'), dataDevice = { id: $this.parents('.grid-row').data('id'), isRegistered: $this.parents('.grid-row').data('registered') === true }; if (action === 'edit') { self.devicesRenderEdit({ data: dataDevice, callbackSave: function(dataDevice) { self.devicesRender({ deviceId: dataDevice.id }); } }); } else if (action === 'delete') { monster.ui.confirm(self.i18n.active().devices.confirmDeleteDevice, function() { self.devicesHelperDeleteDevice(dataDevice.id, function() { self.devicesRender(); }); }); } }); template.find('.create-device').on('click', function() { var type = $(this).data('type'); self.devicesRenderAdd({ type: type, callback: function(device) { self.devicesRender({ deviceId: device.id }); } }); }); }, /** * @param {Object} data * @return {Array} */ getKeyTypes: function(data) { return _.filter([ 'combo_keys', 'feature_keys' ], function(type) { return _.get(data, [type, 'iterate'], 0) > 0; }); }, /** * @param {Object} args * @param {Object} args.data * @param {Boolean} [args.allowAssign] * @param {Function} [args.callbackSave] * @param {Function} [args.callbackDelete] */ devicesRenderEdit: function(args) { var self = this, data = args.data, allowAssign = _.get(args, 'allowAssign'), callbackSave = args.callbackSave, callbackDelete = args.callbackDelete || function(device) { self.devicesRender(); }; self.devicesGetEditData(data, function(dataDevice) { self.devicesRenderDevice({ data: dataDevice, allowAssign: allowAssign, callbackSave: callbackSave, callbackDelete: callbackDelete }); }); }, /** * @param {Object} args * @param {Boolean} [args.allowAssign] * @param {String} args.type * @param {Function} args.callback */ devicesRenderAdd: function(args) { var self = this, allowAssign = _.get(args, 'allowAssign'), type = args.type, callback = args.callback, data = { device_type: type }; if (type === 'sip_device' && monster.config.api.provisioner) { monster.pub('common.chooseModel.render', { callback: function(dataModel, callbackCommonSuccess) { self.callApi({ resource: 'device.create', data: { accountId: self.accountId, data: dataModel }, success: function(data, status) { callback(data.data); callbackCommonSuccess && callbackCommonSuccess(); } }); }, callbackMissingBrand: function() { self.devicesRenderEdit({ allowAssign: allowAssign, data: data, callbackSave: function(dataDevice) { callback && callback(dataDevice); } }); } }); } else { self.devicesRenderEdit({ allowAssign: allowAssign, data: data, callbackSave: function(dataDevice) { callback && callback(dataDevice); } }); } }, /** * @param {Object} args.data * @param {Boolean} [args.allowAssign=true] * @param {Function} [args.callbackSave] * @param {Function} [args.callbackDelete] */ devicesRenderDevice: function(args) { var self = this, data = _.get(args, 'data'), isAssignAllowed = !!_.get(args, 'allowAssign', true), callbackSave = _.get(args, 'callbackSave'), callbackDelete = _.get(args, 'callbackDelete'), mode = data.id ? 'edit' : 'add', type = data.device_type, popupTitle = mode === 'edit' ? self.getTemplate({ name: '!' + self.i18n.active().devices[type].editTitle, data: { name: data.name } }) : self.i18n.active().devices[type].addTitle, templateDevice = $(self.getTemplate({ name: 'devices-' + type, data: $.extend(true, {}, data, { isProvisionerConfigured: monster.config.api.hasOwnProperty('provisioner'), showEmergencyCallerId: monster.util.isNumberFeatureEnabled('e911') }), submodule: 'devices' })), deviceForm = templateDevice.find('#form_device'), assignTemplate = $(self.getTemplate({ name: 'assign-to', data: data, submodule: 'devices' })); if (isAssignAllowed) { deviceForm.find('.tabs-section[data-section="basic"]').append(assignTemplate); } if (data.extra.hasOwnProperty('provision') && data.extra.provision.hasOwnProperty('keys')) { _.each(data.extra.provision.keys, function(value) { var section = '.tabs-section[data-section="' + value.type + '"] '; _.each(value.data, function(val, key) { if (val) { var groupSelector = '.control-group[data-id="' + key + '"] ', valueSelector = '.feature-key-value[data-type~="' + val.type + '"]'; templateDevice .find(section.concat(groupSelector, valueSelector)) .addClass('active'); templateDevice.find(valueSelector + ' [name="provision.keys.' + value.id + '[' + key + '].value.value"]') .val(_.get(val, 'value.value')); templateDevice.find(valueSelector + ' [name="provision.keys.' + value.id + '[' + key + '].value.label"]') .val(_.get(val, 'value.label')); } }); }); templateDevice.find('.keys').each(function() { var $this = $(this), itemUpdated = false, $itemUnder, $itemBefore, $itemAfter, $siblings; $this.sortable({ items: '.control-group', placeholder: 'control-group placeholder', update: function(e, ui) { ui.item.addClass('moved'); itemUpdated = true; }, start: function(e, ui) { $itemBefore = ui.item.prevAll('.control-group:not(.placeholder):first'); $itemAfter = ui.item.nextAll('.control-group:not(.placeholder):first'); $siblings = ui.item.siblings('.control-group'); }, stop: function(e, ui) { // Swap if (!_.isEmpty($itemUnder) && !$itemUnder.hasClass('placeholder')) { $itemUnder.addClass('moved'); if (itemUpdated) { // The dragged item was updated, so we only need to swap the other item if (!_.isEmpty($itemBefore) && !$itemUnder.is($itemBefore)) { $itemUnder.remove().insertAfter($itemBefore); } else if (!_.isEmpty($itemAfter) && !$itemUnder.is($itemAfter)) { $itemUnder.remove().insertBefore($itemAfter); } } else { // Special case: the dragged item is over a sibling next to it, // but it did not triggered an update event, because the // placeholder was still at the same original position of the item ui.item.addClass('moved'); if (!$itemUnder.is($itemBefore)) { $itemUnder.insertBefore(ui.item); } else if (!$itemUnder.is($itemAfter)) { $itemUnder.insertAfter(ui.item); } } } // Update items $this .find('.feature-key-index') .each(function(idx, el) { $(el).text(idx + 1); }); if ($this.data('section') === 'comboKeys') { $this .find('.control-group') .first() .addClass('warning') .siblings('.control-group.warning') .removeClass('warning'); } // Cleanup if (!_.isEmpty($itemUnder)) { $itemUnder.removeClass('selected'); $itemUnder = null; } itemUpdated = false; }, sort: _.debounce(function(e, ui) { var $newItemUnder = $siblings.filter(function(idx, elem) { var itemPosition = ui.position, $elem = $(elem), elemPosition = $elem.position(); return itemPosition.left >= elemPosition.left && itemPosition.left <= elemPosition.left + $elem.width() && itemPosition.top >= elemPosition.top && itemPosition.top <= elemPosition.top + $elem.height(); }); if ($newItemUnder.is($itemUnder)) { return; } if (!_.isEmpty($itemUnder)) { $itemUnder.removeClass('selected'); } $newItemUnder.addClass('selected'); $itemUnder = $newItemUnder; }, 50) }); }); templateDevice .find('.feature-key-index') .each(function(idx, el) { $(el) .text(parseInt($(el).text(), 10) + 1); }); } if (data.extra.hasE911Numbers) { var currentNumber; if (data.caller_id && data.caller_id.emergency && data.caller_id.emergency.number) { currentNumber = data.caller_id.emergency.number; self.devicesGetE911NumberAddress(data.caller_id.emergency.number, function(address) { templateDevice .find('.number-address') .show() .find('p') .html(address); }); } monster.pub('common.numberSelector.render', { container: templateDevice.find('.emergency-number'), inputName: 'caller_id.emergency.number', number: currentNumber, customNumbers: data.extra.e911Numbers, noBuy: true, noExtension: true, labels: { empty: self.i18n.active().devices.popupSettings.callerId.notSet, remove: self.i18n.active().devices.popupSettings.callerId.useDefault, spare: self.i18n.active().devices.popupSettings.callerId.selectNumber, hideNumber: true } }); } monster.ui.validate(deviceForm, { rules: { 'name': { required: true }, 'mac_address': { required: true, mac: true }, 'mobile.mdn': { number: true }, 'sip.username': { required: true }, 'sip.password': { required: true }, 'call_forward.number': { required: true } }, ignore: '' // Do not ignore hidden fields }); if ($.inArray(type, ['sip_device', 'smartphone', 'mobile', 'softphone', 'fax', 'ata']) > -1) { var audioCodecs = monster.ui.codecSelector('audio', templateDevice.find('#audio_codec_selector'), data.media.audio.codecs); } if ($.inArray(type, ['sip_device', 'smartphone', 'mobile', 'softphone']) > -1) { var videoCodecs = monster.ui.codecSelector('video', templateDevice.find('#video_codec_selector'), data.media.video.codecs); } monster.ui.tabs(templateDevice); monster.ui.protectField(templateDevice.find('#sip_password'), templateDevice); monster.ui.tooltips(templateDevice); monster.ui.mask(templateDevice.find('#mac_address'), 'macAddress'); monster.ui.mask(templateDevice.find('[name="call_forward.number"]'), 'phoneNumber'); monster.ui.chosen(templateDevice.find('.chosen-feature-key-user'), { width: 'inherit' }); if (!(data.media.encryption.enforce_security)) { templateDevice.find('#rtp_method').hide(); } templateDevice.find('#secure_rtp').on('change', function() { templateDevice.find('#rtp_method').toggle(); }); templateDevice.find('#restart_device').on('click', function() { if (!$(this).hasClass('disabled')) { self.devicesRestart(data.id, function() { monster.ui.toast({ type: 'success', message: self.i18n.active().devices.popupSettings.miscellaneous.restart.success }); }); } }); templateDevice.find('#unlock_device').on('click', function() { self.devicesUnlock(data.mac_address.replace(/:/g, ''), function() { monster.ui.toast({ type: 'success', message: self.i18n.active().devices.popupSettings.miscellaneous.unlock.success }); }); }); templateDevice.find('.actions .save').on('click', function() { if (monster.ui.valid(deviceForm)) { templateDevice.find('.feature-key-value:not(.active)').remove(); var $this = $(this), hasToRestart = !!$this.data('extra'), dataToSave = self.devicesMergeData(data, templateDevice, audioCodecs, videoCodecs); if ($this.hasClass('disabled')) { return; } $this.prop('disabled', 'disabled'); self.devicesSaveDevice(data.owner_id, dataToSave, function(data) { if (hasToRestart) { self.devicesRestart(data.id, function() { monster.ui.toast({ type: 'success', message: self.i18n.active().devices.popupSettings.miscellaneous.restart.success }); }); } popup.dialog('close').remove(); callbackSave && callbackSave(data); }, function() { $this.prop('disabled', false); }); } else { templateDevice.find('.tabs-selector[data-section="basic"]').click(); } }); if (type !== 'mobile') { templateDevice.find('#delete_device').on('click', function() { var deviceId = $(this).parents('.edit-device').data('id'); self.devicesHelperDeleteDevice(deviceId, function(device) { popup.dialog('close').remove(); callbackDelete && callbackDelete(device); }); }); } templateDevice.find('.actions .cancel-link').on('click', function() { popup.dialog('close').remove(); }); templateDevice.on('change', '.caller-id-select', function() { var selectedNumber = this.value; var divAddress = templateDevice.find('.number-address'); divAddress.find('p').empty(); if (selectedNumber !== '') { self.devicesGetE911NumberAddress(selectedNumber, function(address) { divAddress.find('p').html(address); }); divAddress.slideDown(); } else { divAddress.slideUp(); } }); templateDevice.find('.restrictions-switch').on('change', function() { templateDevice.find('.restriction-matcher-sign').hide(); templateDevice.find('.restriction-message').hide(); }); templateDevice.find('.restriction-matcher-button').on('click', function(e) { e.preventDefault(); var number = templateDevice.find('.restriction-matcher-input').val(); if (number) { self.callApi({ resource: 'numbers.matchClassifier', data: { accountId: self.accountId, phoneNumber: number }, success: function(data, status) { var matchedLine = templateDevice.find('.restriction-line[data-restriction="' + data.data.name + '"]'), matchedSign = matchedLine.find('.restriction-matcher-sign'), matchedMsg = templateDevice.find('.restriction-message'); templateDevice.find('.restriction-matcher-sign').hide(); if (matchedLine.find('.restrictions-switch').prop('checked')) { matchedSign .removeClass('monster-red fa-times') .addClass('monster-green fa-check') .css('display', 'inline-block'); matchedMsg .removeClass('red-box') .addClass('green-box') .css('display', 'inline-block') .empty() .text(self.getTemplate({ name: '!' + self.i18n.active().devices.popupSettings.restrictions.matcher.allowMessage, data: { phoneNumber: monster.util.formatPhoneNumber(number) } })); } else { matchedSign .removeClass('monster-green fa-check') .addClass('monster-red fa-times') .css('display', 'inline-block'); matchedMsg .removeClass('green-box') .addClass('red-box') .css('display', 'inline-block') .empty() .text(self.getTemplate({ name: '!' + self.i18n.active().devices.popupSettings.restrictions.matcher.denyMessage, data: { phoneNumber: monster.util.formatPhoneNumber(number) } })); } } }); } else { templateDevice.find('.restriction-matcher-sign').hide(); templateDevice.find('.restriction-message').hide(); } }); templateDevice.find('.feature-key-type').on('change', function() { var $this = $(this), type = $this.val(), $featureKeyValue = $this.closest('.feature-key-value'); $featureKeyValue.siblings('.feature-key-value.active[data-type]').removeClass('active'); $featureKeyValue.siblings('.feature-key-value[data-type~="' + type + '"]').addClass('active'); }); templateDevice.find('.tabs-section[data-section="featureKeys"] .type-info a').on('click', function() { var $this = $(this); setTimeout(function() { var action = ($this.hasClass('collapsed') ? 'show' : 'hide').concat('Info'); $this.find('.text').text(self.i18n.active().devices.popupSettings.keys.info.link[action]); }); }); var popup = monster.ui.dialog(templateDevice, { title: popupTitle, dialogClass: 'voip-edit-device-popup' }); }, devicesRestart: function(deviceId, callback) { var self = this; self.callApi({ resource: 'device.restart', data: { accountId: self.accountId, deviceId: deviceId }, success: function(data) { callback && callback(data.data); } }); }, devicesUnlock: function(macAddress, callback) { var self = this; monster.request({ resource: 'provisioner.devices.unlock', data: { accountId: self.accountId, macAddress: macAddress }, success: function(data, status) { callback && callback(); } }); }, devicesMergeData: function(originalData, template, audioCodecs, videoCodecs) { var self = this, hasCodecs = $.inArray(originalData.device_type, ['sip_device', 'landline', 'fax', 'ata', 'softphone', 'smartphone', 'mobile', 'sip_uri']) > -1, hasCallForward = $.inArray(originalData.device_type, ['landline', 'cellphone', 'smartphone']) > -1, hasRTP = $.inArray(originalData.device_type, ['sip_device', 'mobile', 'softphone']) > -1, formData = monster.ui.getFormData('form_device'), isValuePropertyEmpty = function(data, property) { return _ .chain(data) .get(['value', property]) .trim() .isEmpty() .value(); }; if ('mac_address' in formData) { formData.mac_address = monster.util.formatMacAddress(formData.mac_address); } if (hasCallForward) { formData.call_forward = $.extend(true, { enabled: true, require_keypress: true, keep_caller_id: true }, formData.call_forward); if (originalData.device_type === 'smartphone') { formData.call_forward.failover = true; } if (formData.hasOwnProperty('extra') && formData.extra.allowVMCellphone) { formData.call_forward.require_keypress = !formData.extra.allowVMCellphone; } } if (hasCodecs) { formData.media = $.extend(true, { audio: { codecs: [] }, video: { codecs: [] } }, formData.media); } if ('call_restriction' in formData) { _.each(formData.call_restriction, function(restriction, key) { if (key in originalData.extra.restrictions && originalData.extra.restrictions[key].disabled) { restriction.action = originalData.extra.restrictions[key].action; } else { restriction.action = restriction.action === true ? 'inherit' : 'deny'; } }); } if (_.has(formData, 'provision.keys')) { /** * form2object sends keys back as arrays even if the first key is 1 * they needs to be coerced into an object to match the datatype in originalData */ _.each(formData.provision.keys, function(value, key, list) { var keys = {}; _.each(list[key], function(val, idx) { if (val.type === 'none') { keys[idx] = null; } else { if (key === 'combo_keys' && val.type === 'parking') { val.value.value = _.parseInt(val.value.value, 10); } if (key !== 'combo_keys' || isValuePropertyEmpty(val, 'label')) { if (isValuePropertyEmpty(val, 'value')) { delete val.value; } else { val.value = val.value.value; } } keys[idx] = val; } }); if (_.isEmpty(keys)) { delete originalData.provision[key]; } else { originalData.provision[key] = keys; } }); delete formData.provision.keys; } var mergedData = $.extend(true, {}, originalData, formData); /* The extend doesn't override an array if the new array is empty, so we need to run these snippet after the merge */ if (hasRTP) { mergedData.media.encryption.methods = []; if (mergedData.media.encryption.enforce_security) { mergedData.media.encryption.methods.push(formData.extra.rtpMethod); } } if (mergedData.extra.hasOwnProperty('notify_unregister')) { mergedData.suppress_unregister_notifications = !mergedData.extra.notify_unregister; } if (hasCodecs) { if (audioCodecs) { mergedData.media.audio.codecs = audioCodecs.getSelectedItems(); } if (videoCodecs) { mergedData.media.video.codecs = videoCodecs.getSelectedItems(); } } // If the key is set to "auto" we remove the key, we don't support this anymore if (mergedData.hasOwnProperty('media') && mergedData.media.hasOwnProperty('fax_option') && mergedData.media.fax_option === 'auto') { delete mergedData.media.fax_option; } // The UI mistakenly created this key, so we clean it up if (mergedData.hasOwnProperty('media') && mergedData.media.hasOwnProperty('fax') && mergedData.media.fax.hasOwnProperty('option')) { delete mergedData.media.fax.option; } if (mergedData.hasOwnProperty('caller_id_options') && mergedData.caller_id_options.hasOwnProperty('outbound_privacy') && mergedData.caller_id_options.outbound_privacy === 'default') { delete mergedData.caller_id_options.outbound_privacy; if (_.isEmpty(mergedData.caller_id_options)) { delete mergedData.caller_id_options; } } if (mergedData.hasOwnProperty('caller_id') && mergedData.caller_id.hasOwnProperty('emergency') && mergedData.caller_id.emergency.hasOwnProperty('number') && mergedData.caller_id.emergency.number === '') { delete mergedData.caller_id.emergency.number; if (_.isEmpty(mergedData.caller_id.emergency)) { delete mergedData.caller_id.emergency; } } //if there is no owner, do not add one. if (mergedData.owner_id && mergedData.owner_id === 'none') { delete mergedData.owner_id; } /* Migration clean-up */ delete mergedData.media.secure_rtp; delete mergedData.extra; return mergedData; }, /** * @param {Object} data * @param {Object} data.device * @param {String} data.device.device_type * @param {Object} data.e911Numbers * @param {Object} data.accountLimits * @param {Object} data.listClassifiers * @param {Object} data.users * @param {Object} [data.template] * @param {Boolean} [dataList.isRegistered] * @return {Object} */ devicesFormatData: function(data, dataList) { var self = this, keyActionsMod = _.get( self.appFlags.devices.provisionerConfigFlags, ['brands', _.get(data.device, 'provision.endpoint_brand'), 'keyFunctions'], [] ), defaultLineKeys = _.get( self.appFlags.devices.provisionerConfigFlags, ['brands', _.get(data.device, 'provision.endpoint_brand'), 'lineKeys'] ), isClassifierDisabledByAccount = function isClassifierDisabledByAccount(classifier) { return _.get(data.accountLimits, ['call_restriction', classifier, 'action']) === 'deny'; }, deviceDefaults = { call_restriction: {}, device_type: 'sip_device', enabled: true, media: { audio: { codecs: ['PCMA', 'PCMU'] }, encryption: { enforce_security: false }, video: { codecs: [] } }, suppress_unregister_notifications: true }, callForwardSettings = { call_forward: { require_keypress: true, keep_caller_id: true }, contact_list: { exclude: true } }, sipSettings = { sip: { password: monster.util.randomString(12), realm: monster.apps.auth.currentAccount.realm, username: 'user_' + monster.util.randomString(10) } }, deviceDefaultsForType = _.get({ ata: _.merge({}, sipSettings), cellphone: _.merge({}, callForwardSettings), fax: _.merge({ media: { fax_option: 'false' }, outbound_flags: [ 'fax' ] }, sipSettings), landline: _.merge({}, callForwardSettings), mobile: _.merge({}, sipSettings), sip_device: _.merge({}, sipSettings), sip_uri: { sip: _.merge({ expire_seconds: 360, invite_format: 'route', method: 'password' }, _.pick(sipSettings.sip, [ 'password', 'username' ])) }, smartphone: _.merge({}, sipSettings, callForwardSettings), softphone: _.merge({}, sipSettings) }, data.device.device_type, {}), deviceOverrides = { provision: _ .chain(data.template) .thru(self.getKeyTypes) .map(function(type) { return { type: type, data: _ .chain(data.template) .get([type, 'iterate'], 0) .range() .map(function(index) { return _.get(data.device, ['provision', type, index], { type: 'none' }); }) .value() }; }) .keyBy('type') .mapValues('data') .value() }, deviceData = _.mergeWith( {}, deviceDefaults, deviceDefaultsForType, data.device, function(dest, src) { return _.every([dest, src], _.isArray) ? src : undefined; } ), mergedDevice = _.merge( {}, deviceData, deviceOverrides ); return _.merge({ extra: { allowVMCellphone: !_.get(mergedDevice, 'call_forward.require_keypress', true), availableCodecs: { audio: [], video: [] }, e911Numbers: data.e911Numbers, hasDisabledRestrictions: _.some(data.listClassifiers, function(metadata, classifier) { return isClassifierDisabledByAccount(classifier); }), hasE911Numbers: !_.isEmpty(data.e911Numbers), isRegistered: _.get(dataList, 'isRegistered', false), outboundPrivacy: _.map(self.appFlags.common.outboundPrivacy, function(strategy) { return { key: strategy, value: monster.util.tryI18n(self.i18n.active().commonMisc.outboundPrivacy.values, strategy) }; }), provision: { keys: _ .chain(data.template) .thru(self.getKeyTypes) .map(function(type) { var camelCasedType = _.camelCase(type), i18n = _.get(self.i18n.active().devices.popupSettings.keys, camelCasedType), entries = _.get(mergedDevice, ['provision', type], []), entriesCount = _.size(entries); return _.merge({ id: type, type: camelCasedType, lineKeys: defaultLineKeys || [1], actions: _ .chain([ 'presence', 'parking', 'personal_parking', 'speed_dial' ]) .concat( type === 'combo_keys' ? ['line'] : [] ) .filter(function(action) { return _.isEmpty(keyActionsMod) || _.includes(keyActionsMod, action); }) .concat(['none']) .map(function(action) { var i18n = self.i18n.active().devices.popupSettings.keys, hasDefaultLineKeys = !!defaultLineKeys, allowedDefaultLineKeyActions = ['none', 'line']; return _.merge({ id: action, info: _.get(i18n, ['info', 'types', action]), label: _.get(i18n, ['types', action]) }, type === 'combo_keys' && hasDefaultLineKeys && !_.includes(allowedDefaultLineKeyActions, action) ? { isActionRestringed: true } : {} ); }) // Sort alphabetically while keeping `none` as first item .sort(function(a, b) { return a.id === 'none' ? -1 : b.id === 'none' ? 1 : a.label.localeCompare(b.label, monster.config.whitelabel.language); }) .value(), data: _.map(entries, function(metadata, idx) { var value = _.get(metadata, 'value', {}); return _.merge({ keyNumber: idx + 1 }, metadata, _.isPlainObject(value) ? {} : { value: { value: _.toString(value) } } ); }) }, _.pick(i18n, [ 'menuTitle', 'sectionTitle', 'label' ]), _.has(i18n, 'range') ? { sectionTitle: self.getTemplate({ name: '!' + i18n.sectionTitle, data: { range: entriesCount > 1 ? self.getTemplate({ name: '!' + i18n.range, data: { min: 1, max: entriesCount } }) : '' } }) } : {}); }) .value(), parkingSpots: _.range(1, 11) }, restrictions: _.mapValues(data.listClassifiers, function(metadata, classifier) { var i18n = _.get(self.i18n.active().devices.classifiers, classifier); return { action: _.get(data.device, ['call_restriction', classifier, 'action'], 'inherit'), disabled: isClassifierDisabledByAccount(classifier), friendly_name: _.get(i18n, 'name', metadata.friendly_name), help: _.get(i18n, 'help') }; }), rtpMethod: _.get(mergedDevice, 'media.encryption.enforce_security', false) ? _.head(mergedDevice.media.encryption.methods) : '', selectedCodecs: { audio: [], video: [] }, users: _.sortBy(data.users, function(user) { return _ .chain(user) .thru(monster.util.getUserFullName) .toLower() .value(); }) } }, mergedDevice); }, /** * @param {Object} data * @param {Object} data.users * @param {Object} data.status * @param {Object} data.devices * @return {Object} */ devicesFormatListData: function(data) { var self = this, getIconClassForDeviceType = function getIconClassForDeviceType(type) { var knownType = _.has(self.appFlags.devices.iconClassesByDeviceTypes, type) ? type : 'sip_device'; return _.get(self.appFlags.devices.iconClassesByDeviceTypes, knownType); }, usersById = _.keyBy(data.users, 'id'), unassignedString = self.i18n.active().devices.unassignedDevice, registeredDevicesById = _.map(data.status, 'device_id'); return { countDevices: _.size(data.devices), devices: _ .chain(data.devices) .map(function(device) { var staticStatusClasses = ['unregistered', 'registered'], deviceType = device.device_type, isRegistered = _.includes(['sip_device', 'smartphone', 'softphone', 'fax', 'ata'], deviceType) ? _.includes(registeredDevicesById, device.id) : true, isEnabled = _.get(device, 'enabled', false), userName = _ .chain(usersById) .get(device.owner_id, { first_name: unassignedString, last_name: '' }) .thru(monster.util.getUserFullName) .value(); return _.merge({ // Display a device in black if it's disabled, otherwise, until we know whether it's registered or not, we set the color to red classStatus: isEnabled ? staticStatusClasses[_.toNumber(isRegistered)] : 'disabled', enabled: isEnabled, friendlyIconClass: getIconClassForDeviceType(deviceType), friendlyType: monster.util.tryI18n(self.i18n.active().devices.types, deviceType), isAssigned: _ .chain(device) .has('owner_id') .toString() .value(), isEditable: _.includes(self.appFlags.devices.editableDeviceTypes, deviceType), // Even though a device is registered, we don't count it as registered if it's disabled isRegistered: isEnabled && isRegistered, macAddress: device.mac_address, registered: isRegistered, sipUserName: device.userName, sortableUserName: userName.split(' ').reverse().join(' '), type: deviceType, userName: userName }, _.pick(device, [ 'id', 'name' ])); }) .sort(function(a, b) { // If owner is the same, order by device name if (a.userName === b.userName) { var aName = a.name.toLowerCase(), bName = b.name.toLowerCase(); return (aName > bName) ? 1 : (aName < bName) ? -1 : 0; } else { // Otherwise, push the unassigned devices to the bottom of the list, and show the assigned devices ordered by user name if (a.userName === unassignedString) { return 1; } else if (b.userName === unassignedString) { return -1; } else { var aSortName = a.sortableUserName.toLowerCase(), bSortName = b.sortableUserName.toLowerCase(); return (aSortName > bSortName) ? 1 : (aSortName < bSortName) ? -1 : 0; } } }) .value(), deviceTypesToAdd: _.map(self.appFlags.devices.addableDeviceTypes, function(type) { return { type: type, icon: _.get(self.appFlags.devices.iconClassesByDeviceTypes, type) }; }) }; }, devicesHelperDeleteDevice: function(deviceId, onSuccess) { var self = this; monster.waterfall([ function(waterfallCb) { monster.ui.confirm(self.i18n.active().devices.confirmDeleteDevice, function() { waterfallCb(null); }, function() { waterfallCb(true); }); }, function(waterfallCb) { self.devicesDeleteDevice(deviceId, function(device) { waterfallCb(null, device); }); } ], function(err, device) { if (err) { return; } monster.ui.toast({ type: 'success', message: self.getTemplate({ name: '!' + self.i18n.active().devices.deletedDevice, data: { deviceName: device.name } }) }); onSuccess && onSuccess(device); }); }, /* Utils */ devicesDeleteDevice: function(deviceId, callback) { var self = this; self.callApi({ resource: 'device.delete', data: { accountId: self.accountId, deviceId: deviceId, data: {} }, success: function(data) { callback(data.data); } }); }, devicesListClassifiers: function(callback) { var self = this; self.callApi({ resource: 'numbers.listClassifiers', data: { accountId: self.accountId }, success: function(data) { callback(data.data); } }); }, devicesGetE911Numbers: function(callback) { var self = this; self.callApi({ resource: 'numbers.list', data: { accountId: self.accountId, filters: { paginate: 'false' } }, success: function(data) { var e911Numbers = {}; _.each(data.data.numbers, function(val, key) { if (val.features.indexOf('e911') >= 0) { e911Numbers[key] = self.devicesFormatNumber(val); } }); callback(e911Numbers); } }); }, devicesFormatNumber: function(value) { var self = this; return value; }, devicesGetEditData: function(dataDevice, callback) { var self = this; monster.waterfall([ function(waterfallCb) { monster.parallel({ listClassifiers: function(callback) { self.devicesListClassifiers(function(dataClassifiers) { callback(null, dataClassifiers); }); }, device: function(callback) { if (!_.has(dataDevice, 'id')) { return callback(null, dataDevice); } self.devicesGetDevice(dataDevice.id, function(dataDevice) { callback(null, dataDevice); }); }, e911Numbers: function(callback) { self.devicesGetE911Numbers(function(e911Numbers) { callback(null, e911Numbers); }); }, accountLimits: function(callback) { self.callApi({ resource: 'limits.get', data: { accountId: self.accountId }, success: function(data, status) { callback(null, data.data); } }); }, users: function(callback) { self.devicesListUsers({ success: function(users, status) { callback(null, users); } }); } }, function(error, results) { waterfallCb(null, results); }); }, function(results, waterfallCb) { if (!_.has(results.device, 'provision')) { return waterfallCb(null, results); } self.devicesGetIterator(results.device.provision, function(template) { waterfallCb(null, _.merge({ template: template }, results)); }, function() { waterfallCb(null, results); }); } ], function(err, results) { var formattedData = self.devicesFormatData(results, dataDevice); callback && callback(formattedData); }); }, devicesGetDevice: function(deviceId, callbackSuccess, callbackError) { var self = this; self.callApi({ resource: 'device.get', data: { accountId: self.accountId, deviceId: deviceId }, success: function(data) { callbackSuccess && callbackSuccess(data.data); }, error: function(data) { callbackError && callbackError(data); } }); }, /** * @param {String|undefined} originalUserId * @param {Object} deviceData * @param {Function} callbackSuccess * @param {Function} [callbackError] */ devicesSaveDevice: function(originalUserId, deviceData, callbackSuccess, callbackError) { var self = this, isMobileDevice = deviceData.device_type === 'mobile', hasDifferentUserId = originalUserId !== deviceData.owner_id, shouldUpdateMobileCallflow = isMobileDevice && hasDifferentUserId, maybeUpdateMobileCallflowAssignment = function maybeUpdateMobileCallflowAssignment(shouldUpdateMobileCallflow, device, callback) { if (!shouldUpdateMobileCallflow) { return callback(null); } var userId = _.get(device, 'owner_id', null), userMainCallflowId = userId ? undefined : null; self.updateMobileCallflowAssignment(userId, userMainCallflowId, device, callback); }, saveDevice = function saveDevice(device, callback) { var method = _.has(device, 'id') ? 'devicesUpdateDevice' : 'devicesCreateDevice'; self[method](device, _.partial(callback, null), callback); }; /** * We perform both operations in parallel because, although app#updateMobileCallflowAssignment * requires an existing device to run, since it is not possible to create mobile devices * from smartpbx, that ID will always be present. */ monster.parallel({ _: _.partial(maybeUpdateMobileCallflowAssignment, shouldUpdateMobileCallflow, deviceData), device: _.partial(saveDevice, deviceData) }, function(err, results) { if (err) { return callbackError && callbackError(err); } callbackSuccess && callbackSuccess(results.device); }); }, /** * @param {Object} deviceData * @param {Function} callbackSuccess * @param {Function} [callbackError] */ devicesCreateDevice: function(deviceData, callbackSuccess, callbackError) { var self = this; self.callApi({ resource: 'device.create', data: { accountId: self.accountId, data: deviceData }, success: function(data) { callbackSuccess(data.data); }, error: function(data) { callbackError && callbackError(data); } }); }, /** * @param {Object} deviceData * @param {Function} callbackSuccess * @param {Function} [callbackError] */ devicesUpdateDevice: function(deviceData, callbackSuccess, callbackError) { var self = this; self.callApi({ resource: 'device.update', data: { accountId: self.accountId, data: deviceData, deviceId: deviceData.id }, success: function(data) { callbackSuccess && callbackSuccess(data.data); }, error: function(data) { callbackError && callbackError(data); } }); }, devicesGetData: function(callback) { var self = this; monster.parallel({ users: function(callback) { self.callApi({ resource: 'user.list', data: { accountId: self.accountId, filters: { paginate: 'false' } }, success: function(dataUsers) { callback && callback(null, dataUsers.data); } }); }, status: function(callback) { self.callApi({ resource: 'device.getStatus', data: { accountId: self.accountId, filters: { paginate: 'false' } }, success: function(dataStatus) { callback && callback(null, dataStatus.data); } }); }, devices: function(callback) { self.callApi({ resource: 'device.list', data: { accountId: self.accountId, filters: { paginate: 'false' } }, success: function(dataDevices) { callback(null, dataDevices.data); } }); } }, function(err, results) { callback && callback(results); }); }, devicesGetE911NumberAddress: function(number, callback) { var self = this; self.callApi({ resource: 'numbers.get', data: { accountId: self.accountId, phoneNumber: number }, success: function(_data, status) { var street_address = _data.data.e911.street_address, locality = _data.data.e911.locality, postal_code = _data.data.e911.postal_code, region = _data.data.e911.region; if (typeof _data.data.e911.extended_address !== 'undefined') { callback(street_address + ', ' + _data.data.e911.extended_address + '
' + locality + ', ' + region + ' ' + postal_code); } else { callback(street_address + ', ' + '
' + locality + ', ' + region + ' ' + postal_code); } } }); }, devicesGetIterator: function(args, callbackSuccess, callbackError) { var self = this; if (args.hasOwnProperty('endpoint_brand') && args.hasOwnProperty('endpoint_family') && args.hasOwnProperty('endpoint_model')) { monster.request({ resource: 'provisioner.ui.getModel', data: { brand: args.endpoint_brand, family: args.endpoint_family, model: args.endpoint_model }, success: function(data, status) { callbackSuccess && callbackSuccess(data.data.template); }, error: function(data, status) { callbackError && callbackError(); } }); } else { callbackError && callbackError(); } }, devicesListUsers: function(args) { var self = this; self.callApi({ resource: 'user.list', data: { accountId: self.accountId, filters: { paginate: 'false' } }, success: function(data, status) { args.hasOwnProperty('success') && args.success(data.data); }, error: function(data, status) { args.hasOwnProperty('error') && args.error(); } }); } }; return app; });