diff --git a/i18n/en-US.json b/i18n/en-US.json index 385d3c3e..23b3fd25 100644 --- a/i18n/en-US.json +++ b/i18n/en-US.json @@ -656,7 +656,11 @@ "5": "Autoprovision one of our supported devices. The UI comes complete with pre-populated manufacturers and devices. Once complete it will automatically be assigned to the specific user.", "6": "Manage what features Users can access. Turn on/off and manage any of the specific User Features." } - } + }, + + "__comment": "UI-1888: ", + "__version": "4.0", + "confirmMobileUnAssignment": "You are about to un-assign a Mobile device. Be aware the phone number associated with this device ({{variable}}) will also be un-assigned from this user. Are you sure?" }, "strategy": { @@ -797,7 +801,10 @@ "directory": "Directory", "__comment": "UI-1742: Can now select a callflow created in Advanced Callflows in Main Number", "__version": "3.22", - "advancedCallflows": "Advanced Callflows" + "advancedCallflows": "Advanced Callflows", + "__comment": "UI-1886: Can now select a media in virtual receptionist", + "__version": "3.23", + "media": "Media" }, "confirmMessages": { "deleteHoliday": "This holiday will be permanently deleted. Continue?", @@ -970,23 +977,23 @@ "__comment": "UI-1220: Adding a Main VMBox automatically when they first load the SmartPBX if they don't have one", "__version": "3.19", "mainVMBoxName": "Main Voicemail Box", - - "__comment": "UI-1604: Walkthrough dashboard", - "__version": "3.21", - "walkthrough": { - "steps": { - "1": "Review your entire office setup including your users, devices, numbers, main number and conference number", - "2": "Create and manage most of your services in the Users tab. Create new users, purchase port, and assign numbers, add an autoprovision devices, and manage User Features.", - "3": "Once you have created enough users, create user groups. This is extremely useful if you have users that work in the same department, such as a sales team.", - "4": "Create a main number for your business. Manage call handling with Virtual Receptionist, and route calls depending on Office Hours and Holidays" - } - }, "__comment": "UI-1210: Display the Main Faxbox number on the dashboard", "__version": "3.22", "faxingNumberLabel": "Faxbox Number", "__comment": "UI-1197: Display company directory on the dashboard", "__version": "3.22", "directoryLabel": "Company Directory Users:", + + "__comment": "UI-1851: Update walkthrough dashboard text", + "__version": "3.23", + "walkthrough": { + "steps": { + "1": "Welcome to SmartPBX, the easiest way to set up your office phone system! This is the main dashboard. From here you can see your users, devices, numbers all in one place!", + "2": "Setting up Users is a breeze in SmartPBX. Create and manage most of your services in the Users tab. Create new users, purchase port, and assign numbers, add an autoprovision devices, and manage User Features.", + "3": "Once you have created enough users, create user groups. This is extremely useful if you have users that work in the same department, such as a sales team.", + "4": "Main Number services are highly important for your business! Manage call handling with Virtual Receptionist, and route calls depending on Office Hours and Holidays." + } + }, "__comment": "UI-1856: On the dashboard, hide cnam related actions when the feature is disabled on the account", "__version": "3.23", "missingCnamE911Message": "Please setup your Company Caller ID and e911 on a Main Number.", @@ -1121,7 +1128,7 @@ "recipients": { "menuTitle": "Recipients", "sectionTitle": "Recipients", - "header": "This panel lets you manage who will be notified when someones leave a message on this voicemail box. Click on \"Add New Recipient\" to add an e-mail address to the list", + "header": "This panel lets you manage who will be notified when someone leaves a message in this voicemail box. Click on \"Add New Recipient\" to add an e-mail address to the list", "create": "Add New Recipient" }, "__comment": "UI-975: Adding the prepend feature to numbers", diff --git a/i18n/fr-FR.json b/i18n/fr-FR.json index 310316e9..65f36164 100644 --- a/i18n/fr-FR.json +++ b/i18n/fr-FR.json @@ -605,7 +605,9 @@ "5": "Ajouter des téléphones à vos utilisateurs via cette case.", "6": "Gérez les différentes fonctionnalités disponibles pour vos utilisateurs via ce menu." } - } + }, + + "confirmMobileUnAssignment": "Vous allez retirer un téléphone mobile. Le numéro lié à ce téléphone ({{variable}}) sera aussi retiré de cet utilisateur. Voulez-vous continuer ?" }, "strategy": { diff --git a/submodules/devices/devices.css b/submodules/devices/devices.css index 5703af91..ca5a183f 100644 --- a/submodules/devices/devices.css +++ b/submodules/devices/devices.css @@ -223,28 +223,28 @@ overflow: visible; } -.edit-device { +.voip-edit-device-popup .edit-device { width: 700px; } -.edit-device .actions { +.voip-edit-device-popup .edit-device .actions { border-top: 1px solid #CCCCCC; height: 30px; line-height: 30px; padding: 15px; } -.edit-device .actions button { +.voip-edit-device-popup .edit-device .actions button { margin-left: 10px; } -.edit-device .title-bar { +.voip-edit-device-popup .edit-device .title-bar { border-bottom: 1px solid #ccc; margin-bottom: 20px; padding: 15px 0; } -.edit-device .title-bar .device-title > div { +.voip-edit-device-popup .edit-device .title-bar .device-title > div { display: inline-block; float: left; font-size: 18px; @@ -253,27 +253,27 @@ margin-left: 40px; } -.edit-device .title-bar .device-title .device-icon { +.voip-edit-device-popup .edit-device .title-bar .device-title .device-icon { height: 75px; line-height: 75px; } -.edit-device .title-bar .device-title .device-icon > i { +.voip-edit-device-popup .edit-device .title-bar .device-title .device-icon > i { font-size: 48px; vertical-align: middle; } -.edit-device .title-bar .device-title .device-image { +.voip-edit-device-popup .edit-device .title-bar .device-title .device-image { border-radius: 2px; border: 1px solid #CCC; } -.edit-device .title-bar .nav-pills { +.voip-edit-device-popup .edit-device .title-bar .nav-pills { margin-top: 24px; margin-right: 15px; } -.edit-device .content .tabs-section .title { +.voip-edit-device-popup .edit-device .content .tabs-section .title { color: #22a5ff; font-size: 24px; margin-bottom: 30px; @@ -281,57 +281,61 @@ text-shadow: 1px 0 1px #CCCCCC; } -.edit-device .content .tabs-section .helper { +.voip-edit-device-popup .edit-device .content .tabs-section .helper { position: relative; margin: 0 40px 15px 40px; } -.edit-device .content .tabs-section .helper i { +.voip-edit-device-popup .edit-device .content .tabs-section .helper i { position: absolute; top: 0; left: -25px; } -.edit-device .content .tabs-section .number-address { +.voip-edit-device-popup .edit-device .content .tabs-section .number-address { display: none; } -.edit-device .help-box.red-box { - margin: 10px 25px +.voip-edit-device-popup .edit-device .help-box.red-box { + margin: 10px 25px; } -.edit-device .content #form_device .displayed-realm { +.voip-edit-device-popup .edit-device .content #form_device .controls .text { + padding-top: 5px; +} + +.voip-edit-device-popup .edit-device .content #form_device .displayed-realm { line-height: 30px; font-weight: bold; } -.edit-device .restrictions-container { +.voip-edit-device-popup .edit-device .restrictions-container { position: relative; } -.edit-device .restriction-matcher-div { +.voip-edit-device-popup .edit-device .restriction-matcher-div { margin-right: 40px; float: right; } -.edit-device .restriction-matcher-input { +.voip-edit-device-popup .edit-device .restriction-matcher-input { width: 120px; margin-right: 10px; margin-left: 5px; } -.edit-device .restriction-matcher-sign { +.voip-edit-device-popup .edit-device .restriction-matcher-sign { margin-left: 20px; width: 25px; vertical-align: middle; display: none; } -.edit-device .restriction-list { +.voip-edit-device-popup .edit-device .restriction-list { display: inline-block; } -.edit-device .restriction-message { +.voip-edit-device-popup .edit-device .restriction-message { width: 275px; margin: 0 0 0 30px !important; padding: 10px; @@ -339,71 +343,71 @@ display: none; } -.edit-device .disabled-restrictions-info { +.voip-edit-device-popup .edit-device .disabled-restrictions-info { width: 270px; position: absolute; bottom: 0px; right: 40px; } -.edit-device .restriction-list .restriction-line.disabled .control-label, -.edit-device .restriction-list .restriction-line.disabled .control-label > i { +.voip-edit-device-popup .edit-device .restriction-list .restriction-line.disabled .control-label, +.voip-edit-device-popup .edit-device .restriction-list .restriction-line.disabled .control-label > i { cursor: default; color: #aaa; } -.edit-device .feature-key-value { +.voip-edit-device-popup .edit-device .feature-key-value { display: none; margin-left: 20px; } -.edit-device .feature-key-value.active { +.voip-edit-device-popup .edit-device .feature-key-value.active { display: inline-block; } -.edit-device .feature-key-value label { +.voip-edit-device-popup .edit-device .feature-key-value label { display: inline-block; margin-right: 10px; } /* Codecs selector */ -.edit-device .content .codec-selector .box-selector { +.voip-edit-device-popup .edit-device .content .codec-selector .box-selector { float: left; width: 300px; } -.edit-device .content .codec-selector .box-selector:first-child { +.voip-edit-device-popup .edit-device .content .codec-selector .box-selector:first-child { margin-left: 30px; margin-right: 40px; } -.edit-device .content .codec-selector .box-selector ul { +.voip-edit-device-popup .edit-device .content .codec-selector .box-selector ul { height: 328px; overflow: auto; } -.edit-device .content #video_codec_selector .box-selector ul { +.voip-edit-device-popup .edit-device .content #video_codec_selector .box-selector ul { min-height: 123px; } /* RTP Special CSS */ -.edit-device .content .rtp-line > * { +.voip-edit-device-popup .edit-device .content .rtp-line > * { float: left; font-size: 14px; margin-right: 10px; } -.edit-device .form-horizontal .control-label.checkbox { +.voip-edit-device-popup .edit-device .form-horizontal .control-label.checkbox { width: inherit; margin-left: 35px; padding-top: 3px; } /* Restart Button */ -.edit-device #restart_device { +.voip-edit-device-popup .edit-device #restart_device { margin-left: 10px; } /* Special checkbox case */ -.edit-device .form-horizontal .control-label.checkbox-basic { +.voip-edit-device-popup .edit-device .form-horizontal .control-label.checkbox-basic { margin-left: 160px; } \ No newline at end of file diff --git a/submodules/devices/devices.js b/submodules/devices/devices.js index 2240b14c..088f891f 100644 --- a/submodules/devices/devices.js +++ b/submodules/devices/devices.js @@ -394,19 +394,21 @@ define(function(require){ } }); - templateDevice.find('#delete_device').on('click', function() { - var deviceId = $(this).parents('.edit-device').data('id'); + if (type !== 'mobile') { + templateDevice.find('#delete_device').on('click', function() { + var deviceId = $(this).parents('.edit-device').data('id'); - monster.ui.confirm(self.i18n.active().devices.confirmDeleteDevice, function() { - self.devicesDeleteDevice(deviceId, function(device) { - popup.dialog('close').remove(); + monster.ui.confirm(self.i18n.active().devices.confirmDeleteDevice, function() { + self.devicesDeleteDevice(deviceId, function(device) { + popup.dialog('close').remove(); - toastr.success(monster.template(self, '!' + self.i18n.active().devices.deletedDevice, { deviceName: device.name })); + toastr.success(monster.template(self, '!' + self.i18n.active().devices.deletedDevice, { deviceName: device.name })); - callbackDelete && callbackDelete(device); + callbackDelete && callbackDelete(device); + }); }); }); - }); + } templateDevice.find('.actions .cancel-link').on('click', function() { popup.dialog('close').remove(); @@ -507,7 +509,7 @@ define(function(require){ var popup = monster.ui.dialog(templateDevice, { position: ['center', 20], title: popupTitle, - dialogClass: 'overflow-visible' + dialogClass: 'voip-edit-device-popup overflow-visible' }); }, @@ -811,7 +813,7 @@ define(function(require){ mapIconClass = { cellphone: 'fa fa-phone', smartphone: 'icon-telicon-mobile-phone', - landline: 'icon-telicon-home-phone', + landline: 'icon-telicon-home', mobile: 'icon-telicon-sprint-phone', softphone: 'icon-telicon-soft-phone', sip_device: 'icon-telicon-voip-phone', diff --git a/submodules/myOffice/myOffice.js b/submodules/myOffice/myOffice.js index dd1e3c44..d0e96f54 100644 --- a/submodules/myOffice/myOffice.js +++ b/submodules/myOffice/myOffice.js @@ -10,7 +10,8 @@ define(function(require){ subscribe: { 'voip.myOffice.render': 'myOfficeRender', - 'myaccount.closed': 'myOfficeMyAccountClosed' + 'auth.continueTrial': 'myOfficeWalkthroughRender', + 'myaccount.closed': 'myOfficeAfterMyaccountClosed' }, chartColors: [ @@ -117,10 +118,37 @@ define(function(require){ .empty() .append(template); + self.myOfficeCheckWalkthrough(); + callback && callback(); }); }, + // we check if we have to display the walkthrough: + // first make sure it's not a trial, then + // only show it if we've already shown the walkthrough in myaccount + myOfficeCheckWalkthrough: function() { + var self = this; + + if(!monster.apps.auth.currentAccount.hasOwnProperty('trial_time_left')) { + monster.pub('myaccount.hasToShowWalkthrough', function(response) { + if(response === false) { + self.myOfficeWalkthroughRender(); + } + }); + } + }, + + myOfficeAfterMyaccountClosed: function() { + var self = this; + + // If it's not a trial, we show the Walkthrough the first time + // because if it's a trial, myOfficeWalkthroughRender will be called by another event + if(!monster.apps.auth.currentAccount.hasOwnProperty('trial_time_left')) { + self.myOfficeWalkthroughRender(); + } + }, + myOfficeCreateMainVMBoxIfMissing: function(callback) { var self = this; @@ -952,7 +980,7 @@ define(function(require){ loadNumberDetails(callerIdNumberSelect.val()); }, - myOfficeMyAccountClosed: function() { + myOfficeWalkthroughRender: function() { var self = this; if(self.isActive()) { diff --git a/submodules/strategy/strategy.js b/submodules/strategy/strategy.js index ea22b3d4..e26be0b2 100644 --- a/submodules/strategy/strategy.js +++ b/submodules/strategy/strategy.js @@ -367,7 +367,6 @@ define(function(require){ }); }); - self.strategyNumbersBindEvents(strategyNumbersContainer, strategyData); self.strategyConfNumBindEvents(strategyConfNumContainer, strategyData); self.strategyFaxingNumBindEvents(strategyFaxingNumContainer, strategyData); @@ -571,20 +570,33 @@ define(function(require){ holidayData = { id: val.id, name: val.name, - month: val.month + fromMonth: val.month }; - if("ordinal" in val) { - holidayType = "advanced"; + if(val.hasOwnProperty('ordinal')) { + holidayType = 'advanced'; holidayData.ordinal = val.ordinal; holidayData.wday = val.wdays[0]; - } else { - holidayData.fromDay = val.days[0]; - if(val.days.length > 1) { - holidayType = "range"; - holidayData.toDay = val.days[val.days.length-1]; - } else { - holidayType = "single"; + } + else { + if(val.hasOwnProperty('viewData')) { + holidayType = 'range'; + holidayData.fromDay = val.viewData.fromDay; + holidayData.fromMonth = val.viewData.fromMonth; + holidayData.toDay = val.viewData.toDay; + holidayData.toMonth = val.viewData.toMonth; + holidayData.set = true; + } + else { + holidayData.fromDay = val.days[0]; + if(val.days.length > 1) { + holidayType = 'range'; + holidayData.toDay = val.days[val.days.length-1]; + holidayData.toMonth = val.month; + } + else { + holidayType = 'single'; + } } } @@ -628,7 +640,6 @@ define(function(require){ tabMessage: self.i18n.active().strategy.calls.callTabsMessages[callflowName] }; - if (strategyData.callflows[callflowName].flow.hasOwnProperty("is_main_number_cf")) { tabData.callOption.callEntityId = strategyData.callflows[callflowName].flow.data.id; tabData.callOption.type = "advanced-callflow"; @@ -1396,7 +1407,8 @@ define(function(require){ container.on('click', '.delete-holiday', function(e) { var holidaysElement = $(this).parents('.holidays-element'), - id = holidaysElement.data('id'); + id = holidaysElement.data('id'), + type = holidaysElement.data('type'); if(id) { monster.ui.confirm(self.i18n.active().strategy.confirmMessages.deleteHoliday, function() { @@ -1406,23 +1418,22 @@ define(function(require){ self.strategyRebuildMainCallflowRuleArray(strategyData); self.strategyUpdateCallflow(mainCallflow, function(updatedCallflow) { strategyData.callflows["MainCallflow"] = updatedCallflow; - self.callApi({ - resource: 'temporalRule.delete', - data: { - accountId: self.accountId, - ruleId: id - }, - success: function(data, status) { - delete strategyData.temporalRules.holidays[data.data.name]; - holidaysElement.remove(); - } - }); + var afterDelete = function(data) { + delete strategyData.temporalRules.holidays[data.name]; + holidaysElement.remove(); + }; + + if(type === 'set') { + self.strategyDeleteRuleSetAndRules(id, afterDelete); + } + else { + self.strategyDeleteHoliday(id, afterDelete); + } }); }) } else { holidaysElement.remove(); } - }); container.on('click', '.save-button', function(e) { @@ -1435,76 +1446,52 @@ define(function(require){ if(holidaysEnabled) { $.each(container.find('.holidays-element'), function() { - var $this = $(this), - name = $this.find('.name').val().trim(), - month = $this.find('.month :selected').val(), - fromDay = $this.find('.day.from :selected').val(), - toDay = $this.find('.day.to :selected').val(), - ordinal = $this.find('.ordinal :selected').val(), - wday = $this.find('.wday :selected').val(), - id = $this.data('id'), - holidayRule = { - cycle: "yearly", - interval: 1, - month: parseInt(month), - type: "main_holidays" - }; + var holidayRule = self.strategyBuildHolidayRule($(this), holidayRulesRequests); - if(!name || Object.keys(holidayRulesRequests).indexOf(name) >= 0) { + if(!holidayRule) { invalidData = true; return false; } - holidayRule.name = name; - if(fromDay) { - var firstDay = parseInt(fromDay); - holidayRule.days = [firstDay]; - if(toDay) { - var lastDay = parseInt(toDay); - for(var day = firstDay+1; day <= lastDay; day++) { - holidayRule.days.push(day); - } - } - } else { - holidayRule.ordinal = ordinal - holidayRule.wdays = [wday] - } - - if(id) { - holidayRulesRequests[name] = function(callback) { - self.callApi({ - resource: 'temporalRule.update', - data: { - accountId: self.accountId, - ruleId: id, - data: holidayRule - }, - success: function(data, status) { - callback(null, data.data); - } + holidayRulesRequests[holidayRule.name] = function(callback) { + // ghetto strategyBuildHoliday builds a complete different object for a range, so we check if one of the different key is in there, if yes, this is a range spanning multiple months + if(holidayRule.hasOwnProperty('isRange')) { + self.strategyBuildMultiMonthRangeHoliday(holidayRule, function(data) { + data.viewData = holidayRule; + callback && callback(null, data); }); } - } else { - holidayRulesRequests[name] = function(callback) { - self.callApi({ - resource: 'temporalRule.create', - data: { - accountId: self.accountId, - data: holidayRule - }, - success: function(data, status) { - callback(null, data.data); - } + else { + self.strategyCleanUpdateHoliday(holidayRule, function(data) { + callback && callback(null, data); }); } - } - + }; }); if(invalidData) { monster.ui.alert(self.i18n.active().strategy.alertMessages.uniqueHoliday) } else { monster.parallel(holidayRulesRequests, function(err, results) { + // First extract all ids from the new holidayList + var existingHolidaysCallflowsIds = [], + newHolidayCallflowsIds = _.pluck(holidayRulesRequests, 'id'); + + // Find all IDs of existing Callflows in the Main Callflow that are linking to the Main Holidays + _.each(mainCallflow.flow.children, function(directChild, id) { + if(id !== '_' && directChild.data.id === strategyData.callflows["MainHolidays"].id) { + existingHolidaysCallflowsIds.push(id); + } + }); + + // Now see if any of these existing IDs that are no longer in the list of holidays + // If we find orphans, remove them from the main callflow + _.each(existingHolidaysCallflowsIds, function(id) { + if(newHolidayCallflowsIds.indexOf(id) < 0) { + delete mainCallflow.flow.children[id]; + } + }); + _.each(results, function(val, key) { mainCallflow.flow.children[val.id] = { children: {}, @@ -1526,21 +1513,21 @@ define(function(require){ }); } } else { - monster.ui.confirm(self.i18n.active().strategy.confirmMessages.disableHolidays, function() { _.each(strategyData.temporalRules.holidays, function(val, key) { holidayRulesRequests[key] = function(callback) { - self.callApi({ - resource: 'temporalRule.delete', - data: { - accountId: self.accountId, - ruleId: val.id - }, - success: function(data, status) { + if(val.hasOwnProperty('temporal_rules')) { + self.strategyDeleteRuleSetAndRules(val.id, function() { delete mainCallflow.flow.children[val.id]; - callback(null, data.data); - } - }); + callback(null, {}); + }); + } + else { + self.strategyDeleteHoliday(val.id, function() { + delete mainCallflow.flow.children[val.id]; + callback(null, {}); + }); + } } }); @@ -1559,6 +1546,373 @@ define(function(require){ }); }, + strategyCleanUpdateHoliday: function(data, callback) { + var self = this, + updateHoliday = function() { + delete data.extra; + self.strategyUpdateHoliday(data, function(data) { + callback && callback(data); + }); + }; + + if(data.extra.oldType === 'set') { + self.strategyDeleteRuleSetAndRules(data.id, function() { + delete data.id; + updateHoliday(); + }); + } + else { + updateHoliday(); + } + }, + + strategyBuildMultiMonthRangeHoliday: function(data, globalCallback) { + var self = this, + fromDay = parseInt(data.fromDay), + fromMonth = parseInt(data.fromMonth), + toDay = parseInt(data.toDay), + toMonth = parseInt(data.toMonth), + name = data.name, + getMonthRule = function(name, pMonth, pStartDay, pEndDay) { + var month = parseInt(pMonth), + fromDay = pStartDay || 1, + toDay = pEndDay || 31, + days = []; + + for(var day = fromDay; day <= toDay; day++) { + days.push(day); + } + + return { + name: name + '_' + month, + cycle: 'yearly', + days: days, + interval: 1, + month: month + }; + }, + rulesToCreate = [ ], + ruleSet = { + name: name, + temporal_rules: [], + type: 'main_holidays' + }, + parallelRequests = {}, + junkName = name + '_' + monster.util.randomString(6); + + if(fromMonth !== toMonth) { + rulesToCreate.push(getMonthRule(junkName, fromMonth, fromDay, 31)); + + var firstMonthLoop = fromMonth === 12 ? 1 : fromMonth + 1; + + for(var loopMonth = firstMonthLoop; (loopMonth !== toMonth && (loopMonth - 12) !== toMonth); loopMonth++) { + if(loopMonth === 13) { loopMonth = 1 }; + rulesToCreate.push(getMonthRule(junkName, loopMonth, 1, 31)); + } + + rulesToCreate.push(getMonthRule(junkName, toMonth, 1, toDay)); + } + else { + rulesToCreate.push(getMonthRule(junkName, fromMonth, fromDay, toDay)); + } + + _.each(rulesToCreate, function(rule) { + parallelRequests[rule.name] = function(callback) { + self.strategyUpdateHoliday(rule, function(data) { + callback && callback(null, data); + }); + }; + }); + + var createCleanSet = function() { + // Create All Rules, and then Create Rule Set. + monster.parallel(parallelRequests, function(err, results) { + _.each(rulesToCreate, function(rule) { + ruleSet.temporal_rules.push(results[rule.name].id); + }); + + self.strategyCreateRuleSet(ruleSet, function(data) { + globalCallback(data); + }); + }); + }; + + if(data.hasOwnProperty('id')) { + if(data.extra.oldType === 'rule') { + self.strategyDeleteHoliday(data.id, function() { + createCleanSet(); + }); + } + else { + self.strategyDeleteRuleSetAndRules(data.id, function() { + createCleanSet(); + }); + } + } + else { + createCleanSet(); + } + }, + + strategyGetDetailRuleSet: function(id, globalCallback) { + var self = this; + + self.strategyGetRuleSet(id, function(set) { + var parallelRequests = {}; + + _.each(set.temporal_rules, function(ruleId) { + parallelRequests[ruleId] = function(callback) { + self.strategyGetRule(ruleId, function(data) { + if(data.hasOwnProperty('message') && data.message === 'bad identifier') { + data = {}; + } + callback && callback(null, data); + }); + } + }); + + monster.parallel(parallelRequests, function(err, results) { + var ruleDetails = [], + listRules = [], + viewData = {}; + + _.each(set.temporal_rules, function(ruleId) { + if(!_.isEmpty(results[ruleId])) { + listRules.push(ruleId); + ruleDetails.push(results[ruleId]); + } + }); + + if(ruleDetails.length) { + viewData.fromDay = ruleDetails[0].days[0]; + viewData.toDay = ruleDetails[ruleDetails.length-1].days[ruleDetails[ruleDetails.length-1].days.length-1]; + viewData.fromMonth = ruleDetails[0].month; + viewData.toMonth = ruleDetails[ruleDetails.length-1].month; + } + + // If list of actual existing rules isn't the same as the ones in the set, we'll update the set and remove the reference to non-existing rules. + if(!_.isEqual(listRules, set.temporal_rules)) { + // If there is at least one valid rule in the set + if(listRules.length > 0) { + set.temporal_rules = listRules; + // We just want to update the list of rules + self.strategyUpdateRuleSet(set, function(data) { + data.viewData = viewData; + + globalCallback && globalCallback(data); + }); + } + // Otherwise we delete the set + else { + self.strategyDeleteRuleSet(set.id, function() { + globalCallback && globalCallback({}); + }); + } + } + else { + set.viewData = viewData; + + globalCallback && globalCallback(set); + } + }); + }); + + }, + + strategyGetRuleSet: function(id, callback) { + var self = this; + + self.callApi({ + resource: 'temporalSet.get', + data: { + accountId: self.accountId, + setId: id + }, + success: function(data, status) { + callback(data.data); + } + }); + }, + + strategyUpdateRuleSet: function(data, callback) { + var self = this; + + self.callApi({ + resource: 'temporalSet.update', + data: { + accountId: self.accountId, + setId: data.id, + data: data + }, + success: function(data, status) { + callback(data.data); + } + }); + }, + + strategyCreateRuleSet: function(data, callback) { + var self = this; + + self.callApi({ + resource: 'temporalSet.create', + data: { + accountId: self.accountId, + data: data + }, + success: function(data, status) { + callback(data.data); + } + }); + }, + + strategyDeleteRuleSetAndRules: function(id, globalCallback) { + var self = this; + + self.strategyGetRuleSet(id, function(data) { + var parallelRequests = {}; + + _.each(data.temporal_rules, function(id) { + parallelRequests[id] = function(callback) { + self.strategyDeleteHoliday(id, function() { + callback && callback(null, {}); + }); + }; + }); + + monster.parallel(parallelRequests, function(err, results) { + self.strategyDeleteRuleSet(id, function(data) { + globalCallback && globalCallback(data); + }); + }); + }); + }, + + strategyDeleteRuleSet: function(id, callback) { + var self = this; + + self.callApi({ + resource: 'temporalSet.delete', + data: { + accountId: self.accountId, + setId: id + }, + success: function(data, status) { + callback && callback(data.data); + } + }); + }, + + strategyBuildHolidayRule: function(container, rules) { + var self = this, + $this = $(container), + name = $this.find('.name').val().trim(), + month = parseInt($this.find('.month.from :selected').val()), + toMonth = parseInt($this.find('.month.to :selected').val()), + fromDay = parseInt($this.find('.day.from :selected').val()), + toDay = parseInt($this.find('.day.to :selected').val()), + ordinal = $this.find('.ordinal :selected').val(), + wday = $this.find('.wday :selected').val(), + id = $this.data('id'), + type = $this.data('type'), + holidayRule = {}; + + if(!name || Object.keys(rules).indexOf(name) >= 0) { + holidayRule = false; + } + else if(toMonth && month !== toMonth) { + holidayRule = { + isRange: true, + name: name, + fromDay: fromDay, + fromMonth: month, + toDay: toDay, + toMonth: toMonth + }; + } + else { + holidayRule = { + name: name, + cycle: 'yearly', + interval: 1, + month: month, + type: 'main_holidays' + }; + + if(fromDay) { + var firstDay = fromDay; + holidayRule.days = [firstDay]; + if(toDay) { + for(var day = firstDay+1; day <= toDay; day++) { + holidayRule.days.push(day); + } + } + } + else { + holidayRule.ordinal = ordinal + holidayRule.wdays = [wday] + } + } + + if(id) { + holidayRule.id = id; + } + + holidayRule.extra = { + oldType: type + }; + + return holidayRule; + }, + + strategyUpdateHoliday: function(data, callback) { + var self = this; + + if(data.id) { + self.callApi({ + resource: 'temporalRule.update', + data: { + accountId: self.accountId, + ruleId: data.id, + data: data + }, + success: function(data, status) { + callback(data.data); + } + }); + } else { + self.callApi({ + resource: 'temporalRule.create', + data: { + accountId: self.accountId, + data: data + }, + success: function(data, status) { + callback(data.data); + } + }); + } + }, + + strategyDeleteHoliday: function(id, callback) { + var self = this; + + self.callApi({ + resource: 'temporalRule.delete', + data: { + accountId: self.accountId, + ruleId: id, + generateError: false + }, + success: function(data, status) { + callback(data.data); + }, + // Sometimes we'll try to delete a time of day which no longer exist, but still need to execute the callback + error: function(data, status) { + callback(data.data); + } + }); + }, + strategyCallsBindEvents: function(container, strategyData) { var self = this; @@ -1627,6 +1981,7 @@ define(function(require){ case 'user': case 'device': case 'callflow': + case 'media': flowElement.data.id = selectedEntity.val(); break; case 'ring_group': @@ -2130,19 +2485,15 @@ define(function(require){ } switch(entityType) { - case 'directory': - case 'user': - case 'device': - case 'voicemail': - case 'callflow': - menuElements[number].data.id = entityId; - break; case 'ring_group': menuElements[number].data.endpoints = [{ endpoint_type: "group", id: entityId }]; break; + default: + menuElements[number].data.id = entityId; + break; } }); @@ -2176,27 +2527,27 @@ define(function(require){ _.each(entities, function(value, key) { var group = { - groupName: self.i18n.active().strategy.callEntities[key], - groupType: key, - entities: $.map(value, function(entity) { - var name = entity.name; - - if(!name) { - if(entity.hasOwnProperty('first_name')) { - name = entity.first_name + ' ' + entity.last_name; - } - else if (entity.hasOwnProperty('numbers')) { - name = entity.numbers.toString(); - } + groupName: self.i18n.active().strategy.callEntities[key], + groupType: key, + entities: $.map(value, function(entity) { + var name = entity.name; + + if(!name) { + if(entity.hasOwnProperty('first_name')) { + name = entity.first_name + ' ' + entity.last_name; + } + else if (entity.hasOwnProperty('numbers')) { + name = entity.numbers.toString(); } + } - return { - id: entity.id, - name: name, - module: entity.module || key - }; - }) - }; + return { + id: entity.id, + name: name, + module: entity.module || key + }; + }) + }; switch(group.groupType) { case 'directory': @@ -2211,6 +2562,9 @@ define(function(require){ case 'ring_group': group.groupIcon = 'fa fa-users'; break; + case 'media': + group.groupIcon = 'fa fa-music'; + break; case 'voicemail': group.groupIcon = 'icon-telicon-voicemail'; break; @@ -2350,7 +2704,7 @@ define(function(require){ flow: { children: {}, data: {}, - module: "faxing" + module: "faxbox" } } }, @@ -2563,59 +2917,93 @@ define(function(require){ }, filters); }, - strategyGetTemporalRules: function(callback) { + strategyGetAllRules: function(globalCallback) { var self = this; + + monster.parallel({ + 'rules': function(localCallback) { + self.callApi({ + resource: 'temporalRule.list', + data: { + accountId: self.accountId, + filters: { 'has_key':'type' } + }, + success: function(data, status) { + localCallback && localCallback(null, data.data); + } + }); + }, + 'sets': function(localCallback) { + self.callApi({ + resource: 'temporalSet.list', + data: { + accountId: self.accountId, + filters: { + has_key:'type', + filter_type: 'main_holidays' + } + }, + success: function(data, status) { + var parallelRequests = {}; + + _.each(data.data, function(set) { + parallelRequests[set.id] = function(callback) { + self.strategyGetDetailRuleSet(set.id, function(data) { + callback && callback(null, data); + }); + }; + }); + + monster.parallel(parallelRequests, function(err, results) { + localCallback && localCallback(null, results); + }); + } + }); + } + }, + function(err, results) { + globalCallback && globalCallback(results); + } + ); + }, + + strategyGetRule: function(id, callback) { + var self = this; + self.callApi({ - resource: 'temporalRule.list', + resource: 'temporalRule.get', data: { accountId: self.accountId, - filters: { 'has_key':'type' } + ruleId: id, + generateError: false }, success: function(data, status) { - var parallelRequests = {}; + callback && callback(data.data); + }, + error: function(data, status) { + callback && callback(data.data); + } + }); + }, - _.each(data.data, function(val, key) { - parallelRequests[val.name] = function(callback) { - self.callApi({ - resource: 'temporalRule.get', - data: { - accountId: self.accountId, - ruleId: val.id - }, - success: function(data, status) { - callback(null, data.data); - } - }); - } - }); + strategyGetTemporalRules: function(callback) { + var self = this; - _.each(self.weekdayLabels, function(val) { - if(!(val in parallelRequests)) { - parallelRequests[val] = function(callback) { - self.callApi({ - resource: 'temporalRule.create', - data: { - accountId: self.accountId, - data: { - cycle: "weekly", - interval: 1, - name: val, - type: "main_weekdays", - time_window_start: 32400, // 9:00AM - time_window_stop: 61200, // 5:00PM - wdays: [val.substring(4).toLowerCase()] - } - }, - success: function(data, status) { - callback(null, data.data); - } - }); - } - } - }); + self.strategyGetAllRules(function(data) { + var parallelRequests = {}; + + _.each(data.rules, function(val, key) { + parallelRequests[val.name] = function(callback) { + self.strategyGetRule(val.id, function(data) { + callback(null, data); + }); + } + }); - if(!("MainLunchHours" in parallelRequests)) { - parallelRequests["MainLunchHours"] = function(callback) { + // Always check that the necessary time rules exist, or re-create them + _.each(self.weekdayLabels, function(val) { + if(!(val in parallelRequests)) { + parallelRequests[val] = function(callback) { self.callApi({ resource: 'temporalRule.create', data: { @@ -2623,11 +3011,11 @@ define(function(require){ data: { cycle: "weekly", interval: 1, - name: "MainLunchHours", - type: "main_lunchbreak", - time_window_start: 43200, - time_window_stop: 46800, - wdays: self.weekdays + name: val, + type: "main_weekdays", + time_window_start: 32400, // 9:00AM + time_window_stop: 61200, // 5:00PM + wdays: [val.substring(4).toLowerCase()] } }, success: function(data, status) { @@ -2636,31 +3024,60 @@ define(function(require){ }); } } + }); - monster.parallel(parallelRequests, function(err, results) { - var temporalRules = { - weekdays: {}, - lunchbreak: {}, - holidays: {} - }; - - _.each(results, function(val, key) { - switch(val.type) { - case "main_weekdays": - temporalRules.weekdays[key] = val - break; - case "main_lunchbreak": - temporalRules.lunchbreak = val; - break; - case "main_holidays": - temporalRules.holidays[key] = val; - break; + if(!("MainLunchHours" in parallelRequests)) { + parallelRequests["MainLunchHours"] = function(callback) { + self.callApi({ + resource: 'temporalRule.create', + data: { + accountId: self.accountId, + data: { + cycle: "weekly", + interval: 1, + name: "MainLunchHours", + type: "main_lunchbreak", + time_window_start: 43200, + time_window_stop: 46800, + wdays: self.weekdays + } + }, + success: function(data, status) { + callback(null, data.data); } }); + } + } + + monster.parallel(parallelRequests, function(err, results) { + var temporalRules = { + weekdays: {}, + lunchbreak: {}, + holidays: {} + }; - callback(temporalRules); + _.each(results, function(val, key) { + switch(val.type) { + case "main_weekdays": + temporalRules.weekdays[key] = val + break; + case "main_lunchbreak": + temporalRules.lunchbreak = val; + break; + case "main_holidays": + temporalRules.holidays[key] = val; + break; + } }); - } + + _.each(data.sets, function(set) { + if(!_.isEmpty(set)) { + temporalRules.holidays[set.name] = set; + } + }); + + callback(temporalRules); + }); }); }, @@ -2682,6 +3099,11 @@ define(function(require){ } }); }, + media: function (callback) { + self.strategyListMedia(function (media) { + callback(null, media); + }); + }, userCallflows: function(_callback) { self.callApi({ resource: 'callflow.list', @@ -2767,6 +3189,7 @@ define(function(require){ var callEntities = { device: results.devices, user: $.extend(true, [], results.users), + media: results.media, userCallflows: [], ring_group: [], userGroups: $.map(results.userGroups, function(val) { @@ -2778,6 +3201,10 @@ define(function(require){ advancedCallflows: results.advancedCallflows }; + _.each(callEntities.media, function(media) { + media.module = 'media'; + }); + _.each(callEntities.device, function(device) { device.module = 'device'; }); @@ -2967,6 +3394,20 @@ define(function(require){ }); }, + strategyListMedia: function(callback) { + var self = this; + + self.callApi({ + resource: 'media.list', + data: { + accountId: self.accountId + }, + success: function(data, status) { + callback && callback(data.data); + } + }); + }, + _strategyOnCurrentAccountUpdated: function(accountData) { var self = this; $('#strategy_custom_hours_timezone').text(timezone.formatTimezone(accountData.timezone)); diff --git a/submodules/users/users.css b/submodules/users/users.css index 3a20922f..f7c6bf27 100644 --- a/submodules/users/users.css +++ b/submodules/users/users.css @@ -435,6 +435,10 @@ } /* Detail Numbers */ +#users_container .detail-numbers .list-wrapper .item-row .number-container { + min-width: 237px; +} + #users_container .detail-numbers .list-wrapper .item-row .features .tooltip-inner { white-space:pre-wrap; } diff --git a/submodules/users/users.js b/submodules/users/users.js index 8cbadbab..d781bf1d 100644 --- a/submodules/users/users.js +++ b/submodules/users/users.js @@ -17,7 +17,7 @@ define(function(require){ deviceIcons: { 'cellphone': 'fa fa-phone', 'smartphone': 'icon-telicon-mobile-phone', - 'landline': 'icon-telicon-home-phone', + 'landline': 'icon-telicon-home', 'mobile': 'icon-telicon-sprint-phone', 'softphone': 'icon-telicon-soft-phone', 'sip_device': 'icon-telicon-voip-phone', @@ -326,6 +326,20 @@ define(function(require){ mapUsers[user.id] = self.usersFormatUserData(user); }); + // Inject MDNs into the numbers' indicator so they are displayed like Kazoo numbers + _.each(data.devices, function(device, idx) { + if (device.device_type === 'mobile'&& device.hasOwnProperty('owner_id')) { + var user = mapUsers[device.owner_id]; + + if (user.extra.phoneNumber === '') { + user.extra.phoneNumber = device.mobile.mdn; + } + else { + user.extra.additionalNumbers++; + } + } + }); + _.each(data.callflows, function(callflow) { if(callflow.type !== 'faxing') { var userId = callflow.owner_id; @@ -391,13 +405,19 @@ define(function(require){ } } else { - mapUsers[userId].extra.devices.push({ - id: device.id, - name: device.name + ' (' + device.device_type.replace('_', ' ') + ')', - type: device.device_type, - registered: isRegistered, - icon: self.deviceIcons[device.device_type] - }); + var deviceDataToTemplate = { + id: device.id, + name: device.name + ' (' + device.device_type.replace('_', ' ') + ')', + type: device.device_type, + registered: isRegistered, + icon: self.deviceIcons[device.device_type] + }; + + if (device.device_type === 'mobile') { + deviceDataToTemplate.mobile = device.mobile; + } + + mapUsers[userId].extra.devices.push(deviceDataToTemplate); } } }); @@ -1002,15 +1022,33 @@ define(function(require){ }); template.on('click', '.detail-devices .list-assigned-items .remove-device', function() { - var row = $(this).parents('.item-row'); + var row = $(this).parents('.item-row'), + userId = template.find('.grid-row.active').data('id'), + deviceId = row.data('id'), + userData = _.find(data.users, function(user, idx) { return user.id === userId; }), + deviceData = _.find(userData.extra.devices, function(device, idx) { return device.id === deviceId; }), + removeDevice = function () { + if(row.hasClass('assigned')) { + unassignedDevices[row.data('id')] = true; + } + row.remove(); + var rows = template.find('.detail-devices .list-assigned-items .item-row'); + if(rows.is(':visible') === false) { + template.find('.detail-devices .list-assigned-items .empty-row').show(); + } + }; - if(row.hasClass('assigned')) { - unassignedDevices[row.data('id')] = true; + if (deviceData.type === 'mobile') { + monster.ui.confirm( + self.i18n.active().users.confirmMobileUnAssignment.replace( + '{{variable}}', + monster.util.formatPhoneNumber(deviceData.mobile.mdn) + ), + removeDevice + ); } - row.remove(); - var rows = template.find('.detail-devices .list-assigned-items .item-row'); - if(rows.is(':visible') === false) { - template.find('.detail-devices .list-assigned-items .empty-row').show(); + else { + removeDevice(); } }); @@ -1019,17 +1057,19 @@ define(function(require){ var $this = $(this), row = $this.parents('.item-row'); - extraSpareNumbers.push(row.data('id')); + if (row.data('type') !== 'mobile') { + extraSpareNumbers.push(row.data('id')); - row.slideUp(function() { - row.remove(); + row.slideUp(function() { + row.remove(); - if ( !template.find('.list-assigned-items .item-row').is(':visible') ) { - template.find('.list-assigned-items .empty-row').slideDown(); - } + if ( !template.find('.list-assigned-items .item-row').is(':visible') ) { + template.find('.list-assigned-items .empty-row').slideDown(); + } - template.find('.spare-link').removeClass('disabled'); - }); + template.find('.spare-link').removeClass('disabled'); + }); + } }); template.on('click', '.actions .spare-link:not(.disabled)', function(e) { @@ -2044,7 +2084,10 @@ define(function(require){ id: currentUser.id, timeout: 20 }; + } + flow.module = callflowNode.module; + flow.data = callflowNode.data; // In next 5 lines, look for user/group node, and replace it with the new data; var flow = userCallflow.flow; @@ -2574,10 +2617,9 @@ define(function(require){ }); }, - usersGetNumbersData: function(userId, callback) { - var self = this; - - monster.parallel({ + usersGetNumbersData: function(userId, callback, loadNumbersView) { + var self = this, + parallelRequests = { user: function(callbackParallel) { self.usersGetUser(userId, function(user) { callbackParallel && callbackParallel(null, user); @@ -2623,13 +2665,20 @@ define(function(require){ self.usersListNumbers(function(listNumbers) { callbackParallel && callbackParallel(null, listNumbers); }); - } - }, - function(err, results) { - callback && callback(results); } - ); + + if (loadNumbersView) { + parallelRequests.devices = function(callbackParallel) { + self.usersListDeviceUser(userId, function (listDevices) { + callbackParallel && callbackParallel(null, listDevices); + }); + }; + } + + monster.parallel(parallelRequests, function(err, results) { + callback && callback(results); + }); }, usersGetNumbersTemplate: function(userId, callback) { @@ -2644,7 +2693,7 @@ define(function(require){ callback && callback(template, results); }); - }); + }, true); }, usersGetDevicesTemplate: function(userId, callback) { var self = this; @@ -2702,6 +2751,21 @@ define(function(require){ user: data.user || {} }; + if (data.hasOwnProperty('devices') && data.devices.length) { + _.each(data.devices, function(device, idx) { + if (device.device_type === 'mobile') { + data.numbers.numbers[device.mobile.mdn] = { + assigned_to: response.user.id, + features: [ 'mobile' ], + isLocal: false, + phoneNumber: device.mobile.mdn, + state: 'in_service', + used_by: 'mobile' + }; + } + }); + } + monster.pub('common.numbers.getListFeatures', function(features) { if('numbers' in data.numbers) { _.each(data.numbers.numbers, function(number, k) { @@ -2721,6 +2785,9 @@ define(function(require){ response.countSpare++; response.unassignedNumbers[k] = number; } + else if (number.used_by === 'mobile') { + response.assignedNumbers.push(number); + } }); } @@ -3226,6 +3293,23 @@ define(function(require){ }); }, + usersSearchMobileCallflowsByNumber: function (userId, phoneNumber, callback) { + var self = this; + + self.callApi({ + resource: 'callflow.searchByNumber', + data: { + accountId: self.accountId, + value: encodeURIComponent('+1' + phoneNumber), + filter_owner_id: userId, + filter_type: 'mobile' + }, + success: function(data) { + callback(data.data[0]); + } + }); + }, + usersGetMainDirectory: function(callback) { var self = this; @@ -3712,16 +3796,62 @@ define(function(require){ usersUpdateDevices: function(data, userId, callbackAfterUpdate) { var self = this, updateDevices = function(userCallflow) { - var listFnParallel = []; + var listFnParallel = [], + updateDeviceRequest = function (newDataDevice, callback) { + self.usersUpdateDevice(newDataDevice, function (updatedDataDevice) { + callback(null, updatedDataDevice); + }); + } _.each(data.new, function(deviceId) { listFnParallel.push(function(callback) { self.usersGetDevice(deviceId, function(data) { data.owner_id = userId; - self.usersUpdateDevice(data, function(data) { - callback(null, data); - }); + if (data.device_type === "mobile") { + self.usersSearchMobileCallflowsByNumber(userId, data.mobile.mdn, function (listCallflowData) { + self.callApi({ + resource: 'callflow.get', + data: { + accountId: self.accountId, + callflowId: listCallflowData.id + }, + success: function(rawCallflowData, status) { + var callflowData = rawCallflowData.data; + + if (userCallflow) { + $.extend(true, callflowData, { + owner_id: userId, + flow: { + module: 'callflow', + data: { + id: userCallflow.id + } + } + }); + } + else { + $.extend(true, callflowData, { + owner_id: userId, + flow: { + module: 'device', + data: { + id: deviceId + } + } + }); + } + + self.usersUpdateCallflow(callflowData, function () { + updateDeviceRequest(data, callback); + }); + } + }); + }); + } + else { + updateDeviceRequest(data, callback); + } }); }); }); @@ -3731,9 +3861,37 @@ define(function(require){ self.usersGetDevice(deviceId, function(data) { delete data.owner_id; - self.usersUpdateDevice(data, function(data) { - callback(null, data); - }); + if (data.device_type === 'mobile') { + self.usersSearchMobileCallflowsByNumber(userId, data.mobile.mdn, function (listCallflowData) { + self.callApi({ + resource: 'callflow.get', + data: { + accountId: self.accountId, + callflowId: listCallflowData.id + }, + success: function(rawCallflowData, status) { + var callflowData = rawCallflowData.data; + + delete callflowData.owner_id; + $.extend(true, callflowData, { + flow: { + module: 'device', + data: { + id: deviceId + } + } + }); + + self.usersUpdateCallflow(callflowData, function () { + updateDeviceRequest(data, callback); + }); + } + }); + }); + } + else { + updateDeviceRequest(data, callback); + } }); }); }); diff --git a/views/app.html b/views/app.html index 54135f70..a67883e0 100644 --- a/views/app.html +++ b/views/app.html @@ -31,7 +31,7 @@
- + {{ i18n.menuTitles.mainNumber }}
diff --git a/views/devices-landline.html b/views/devices-landline.html index 6f7273d4..5171c924 100644 --- a/views/devices-landline.html +++ b/views/devices-landline.html @@ -2,7 +2,7 @@
- +
{{#if id}} diff --git a/views/devices-layout.html b/views/devices-layout.html index 85fcb30f..1c1f0009 100644 --- a/views/devices-layout.html +++ b/views/devices-layout.html @@ -14,9 +14,8 @@
  • {{ i18n.devices.types.sip_device }}
  • {{ i18n.devices.types.cellphone }}
  • -
  • {{ i18n.devices.types.mobile }}
  • {{ i18n.devices.types.softphone }}
  • -
  • {{ i18n.devices.types.landline }}
  • +
  • {{ i18n.devices.types.landline }}
  • {{ i18n.devices.types.fax }}
  • {{ i18n.devices.types.ata }}
  • {{ i18n.devices.types.sip_uri }}
  • diff --git a/views/devices-mobile.html b/views/devices-mobile.html index deb78c52..b10e6b3b 100644 --- a/views/devices-mobile.html +++ b/views/devices-mobile.html @@ -21,13 +21,18 @@ @@ -39,28 +44,28 @@
    - +
    {{name}}
    - +
    {{mobile.mdn}}
    - +
    {{sip.username}}
    - +
    {{sip.password}}
    @@ -193,10 +198,6 @@
    - {{#if id}} - {{ i18n.devices.deleteDevice }} - {{/if}} -
    {{ i18n.cancel }}