diff --git a/src/api/call-forward.js b/src/api/call-forward.js index 19dd2e98..6eae6eb6 100644 --- a/src/api/call-forward.js +++ b/src/api/call-forward.js @@ -82,3 +82,96 @@ export function getDestinationsets(id) { }); }); } + +export function loadAlwaysEverybodyDestinations(subscriberId) { + return new Promise((resolve, reject)=>{ + Promise.resolve().then(()=>{ + return getMappings(subscriberId); + }).then((mappings)=>{ + let cfuPromises = []; + let cfnaPromises = []; + let cfbPromises = []; + if(_.has(mappings, 'cfu') && _.isArray(mappings.cfu) && mappings.cfu.length > 0) { + mappings.cfu.forEach((cfuMapping)=>{ + if (cfuMapping.timeset_id === null && cfuMapping.sourceset_id === null) { + cfuPromises.push(getDestinationsetById(cfuMapping.destinationset_id)); + } + }); + } + if(_.has(mappings, 'cfna') && _.isArray(mappings.cfna) && mappings.cfna.length > 0) { + mappings.cfna.forEach((cfnaMapping)=>{ + if (cfnaMapping.timeset_id === null && cfnaMapping.sourceset_id === null) { + cfnaPromises.push(getDestinationsetById(cfnaMapping.destinationset_id)); + } + }); + } + if(_.has(mappings, 'cfb') && _.isArray(mappings.cfb) && mappings.cfb.length > 0) { + mappings.cfb.forEach((cfbMapping)=>{ + if (cfbMapping.timeset_id === null && cfbMapping.sourceset_id === null) { + cfbPromises.push(getDestinationsetById(cfbMapping.destinationset_id)); + } + }); + } + return Promise.all([ + Promise.all(cfuPromises), + Promise.all(cfnaPromises), + Promise.all(cfbPromises) + ]); + }).then((res)=>{ + resolve({ + online: res[0], + offline: res[1], + busy: res[2] + }); + }).catch((err)=>{ + reject(err); + }); + }); +} + +export function getDestinationsetById(id) { + return new Promise((resolve, reject)=>{ + Vue.http.get('/api/cfdestinationsets/' + id).then((res)=>{ + let destinationset = getJsonBody(res.body); + delete destinationset['_links']; + resolve(destinationset); + }).catch((err)=>{ + reject(err); + }); + }); +} + +export function deleteDestinationFromDestinationset(options) { + let headers = { + 'Content-Type': 'application/json-patch+json' + }; + return new Promise((resolve, reject) => { + Vue.http.patch('/api/cfdestinationsets/' + options.id, [{ + op: 'replace', + path: '/destinations', + value: options.data + }], { headers: headers }).then(result => { + if (options.deleteDestinationset) { + deleteDestinationsetById(options.id).then((res) => { + resolve(res); + }).catch((err) => { + console.log(err); + }); + } else { + resolve(result); + }; + }).catch(err => { + reject(err); + }); + }); +} + +export function deleteDestinationsetById(id) { + return new Promise((resolve, reject) => { + Vue.http.delete('/api/cfdestinationsets/' + id).then(result => { + resolve(result); + }).catch(err => { + reject(err); + }); + }); +} diff --git a/src/components/pages/CallForward/AfterHours.vue b/src/components/pages/CallForward/AfterHours.vue index 2a25291a..6d46e694 100644 --- a/src/components/pages/CallForward/AfterHours.vue +++ b/src/components/pages/CallForward/AfterHours.vue @@ -1,5 +1,6 @@ <template> - <csc-page title="After Hours"></csc-page> + <csc-page :title="$t('pages.callForward.titles.afterHours')"> + </csc-page> </template> <script> diff --git a/src/components/pages/CallForward/Always.vue b/src/components/pages/CallForward/Always.vue index fabc2636..5604cadf 100644 --- a/src/components/pages/CallForward/Always.vue +++ b/src/components/pages/CallForward/Always.vue @@ -1,19 +1,48 @@ <template> - <csc-page title="Always"></csc-page> + <csc-page :title="$t('pages.callForward.titles.always')"> + <q-card class="dest-card"> + <csc-destinations :title="$t('pages.callForward.whenOnline')" + :group="destinations.online" + icon="signal_wifi_4_bar"> + </csc-destinations> + <csc-destinations :title="$t('pages.callForward.whenBusy')" + :group="destinations.busy" + icon="record_voice_over"> + </csc-destinations> + <csc-destinations :title="$t('pages.callForward.whenOffline')" + :group="destinations.offline" + icon="signal_wifi_off"> + </csc-destinations> + </q-card> + </csc-page> </template> <script> - import CscPage from '../../CscPage' + import CscPage from '../../CscPage' + import CscDestinations from './CscDestinations' + import { QCard } from 'quasar-framework' export default { + mounted() { + this.$store.dispatch('callForward/loadAlwaysEverybodyDestinations'); + }, data () { return { } }, components: { - CscPage + QCard, + CscPage, + CscDestinations + }, + methods: { + }, + computed: { + destinations() { + return this.$store.state.callForward.alwaysEverybodyDestinations; + } } } </script> -<style> +<style lang="stylus"> </style> diff --git a/src/components/pages/CallForward/CompanyHours.vue b/src/components/pages/CallForward/CompanyHours.vue index 9e344a7b..9418e281 100644 --- a/src/components/pages/CallForward/CompanyHours.vue +++ b/src/components/pages/CallForward/CompanyHours.vue @@ -1,5 +1,6 @@ <template> - <csc-page title="Company Hours"></csc-page> + <csc-page :title="$t('pages.callForward.titles.companyHours')"> + </csc-page> </template> <script> diff --git a/src/components/pages/CallForward/CscDestination.vue b/src/components/pages/CallForward/CscDestination.vue new file mode 100644 index 00000000..b304617f --- /dev/null +++ b/src/components/pages/CallForward/CscDestination.vue @@ -0,0 +1,95 @@ +<template> + <div> + <q-item highlight v-for="(destination, index) in destinations"> + <q-item-main> + <div class="dest-row"> + <span v-if="index == 0"> {{ $t('pages.callForward.firstRing') }} </span> + <span v-else-if="index > 0"> {{ $t('pages.callForward.thenRing') }} </span> + <span class="dest-values"> {{ destination.destination | numberFormat }} </span> + <span v-if="isNumber(destination.destination)"> + <span> {{ $t('pages.callForward.for') }} </span> + <span class="dest-values">{{ destination.timeout }}</span> + <span> {{ $t('pages.callForward.secs') }} </span> + </span> + </div> + </q-item-main> + <q-item-side right> + <q-btn color="negative" flat icon="delete" @click="deleteDestination(index)">{{ $t('buttons.remove') }}</q-btn> + </q-item-side> + </q-item> + </div> +</template> + +<script> + import numberFormat from '../../../filters/number-format' + import _ from 'lodash' + import { showToast } from '../../../helpers/ui' + import { QItem, QItemSide, Dialog, Toast, QBtn, QItemMain } from 'quasar-framework' + export default { + name: 'csc-destination', + props: [ + 'destinations', + 'destinationsetId' + ], + components: { + QItem, + QItemMain, + QItemSide, + Dialog, + Toast, + QBtn + }, + computed: { + }, + methods: { + isNumber(destination) { + let dest = destination.split(/:|@/); + return !isNaN(dest[1]); + }, + deleteDestination(index) { + let clonedDestinations = _.cloneDeep(this.destinations); + let indexInt = parseInt(index); + let store = this.$store; + let removeDestination = numberFormat(this.destinations[index].destination); + let self = this; + let isLastDestination = this.destinations.length === 1; + clonedDestinations.splice(indexInt, 1); + Dialog.create({ + title: self.$t('pages.callForward.removeDialogTitle'), + message: self.$t('pages.callForward.removeDialogText', { + destination: removeDestination + }), + buttons: [ + self.$t('buttons.cancel'), + { + label: self.$t('buttons.remove'), + color: 'negative', + handler () { + store.dispatch('callForward/deleteDestinationFromDestinationset', { + id: self.destinationsetId, + data: clonedDestinations, + deleteDestinationset: isLastDestination }).then((result) => { + store.dispatch('callForward/loadAlwaysEverybodyDestinations'); + showToast(self.$t('pages.callForward.removeSuccessMessage', { + destination: removeDestination + })); + }).catch((err) => { + showToast(self.$t('pages.callForward.removeErrorMessage')); + }); + } + } + ] + }); + } + } + } +</script> + +<style lang="stylus"> +@import '~variables' +.dest-row + display inline-block + width 90% + .dest-values + font-weight 500 +</style> diff --git a/src/components/pages/CallForward/CscDestinations.vue b/src/components/pages/CallForward/CscDestinations.vue new file mode 100644 index 00000000..c4fdd107 --- /dev/null +++ b/src/components/pages/CallForward/CscDestinations.vue @@ -0,0 +1,55 @@ +<template> + <div> + <q-card-title class="dest-title"> + <q-icon :name="icon" class="dest-icon" /> + {{ title }} + </q-card-title> + <q-card-main> + <q-list no-border> + <div v-if="group.length === 0"> + <q-item> + <div class="dest-row"> + <span> {{ $t('pages.callForward.forwardToNowhere') }} </span> + </div> + </q-item> + </div> + <div v-else v-for="destinationset in group"> + <csc-destination :destinations="destinationset.destinations" :destinationset-id="destinationset.id"> + </csc-destination> + </div> + </q-list> + </q-card-main> + </div> +</template> + +<script> + import CscDestination from './CscDestination' + import { QCardTitle, QCardMain, QList, + QItem, QIcon } from 'quasar-framework' + export default { + name: 'csc-destinations', + props: [ + 'title', + 'icon', + 'group' + ], + components: { + QCardTitle, + QCardMain, + QList, + QItem, + QIcon, + CscDestination + } + } +</script> + +<style lang="stylus"> +@import '~variables' +.dest-row + inline-block +.dest-title + color $primary +.dest-icon + margin-right 5px +</style> diff --git a/src/filters/number-format.js b/src/filters/number-format.js index 3792bc06..eef12be7 100644 --- a/src/filters/number-format.js +++ b/src/filters/number-format.js @@ -1,12 +1,16 @@ - import url from 'url'; import { PhoneNumberUtil, PhoneNumberFormat } from 'google-libphonenumber'; +import { format } from 'quasar-framework' +const { capitalize } = format var phoneUtil = PhoneNumberUtil.getInstance(); export default function(number) { try { let phoneNumber = url.parse(number, true).auth.split(':')[0]; + if (isNaN(phoneNumber)) { + phoneNumber = normalizeDestination(url.parse(number, true)); + } return normalizeNumber(phoneNumber); } catch(err1) { return normalizeNumber(number); @@ -35,3 +39,15 @@ export function rawNumber(number) { } return ''; } + +export function normalizeDestination(destination) { + let normalizedDestination; + if (destination.host == 'app.local') { + normalizedDestination = destination.auth; + } else if (destination.host == 'voicebox.local') { + normalizedDestination = 'Voicemail'; + } else { + normalizedDestination = capitalize(destination.host.split('.')[0]); + } + return normalizedDestination; +} diff --git a/src/locales/en.json b/src/locales/en.json index 908dc788..568ad6a4 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -114,6 +114,26 @@ }, "timeUpdatedMsg": "Time updated!", "recurrenceUpdatedMsg": "Recurrence updated!" + }, + "callForward": { + "titles": { + "always": "Always", + "companyHours": "Company Hours", + "afterHours": "After Hours" + }, + "whenOnline": "When I am online ...", + "whenBusy": "When I am busy ...", + "whenOffline": "When I am offline ...", + "forwardTo": "forward to", + "firstRing": "first ring", + "thenRing": "then ring", + "for": "for", + "secs": "secs", + "forwardToNowhere": "forward to nowhere", + "removeDialogTitle": "Remove call forward destination", + "removeDialogText": "You are about to remove the destination {destination}", + "removeSuccessMessage": "Removed destination {destination}", + "removeErrorMessage": "An error occured while trying to delete the destination. Please try again." } }, "call": { diff --git a/src/store/call-forward.js b/src/store/call-forward.js index a77d7a60..6eaf8ef0 100644 --- a/src/store/call-forward.js +++ b/src/store/call-forward.js @@ -3,7 +3,9 @@ import _ from 'lodash'; import { getSourcesets, getDestinationsets, getTimesets, - getMappings } from '../api/call-forward'; + getMappings, loadAlwaysEverybodyDestinations, + deleteDestinationFromDestinationset, + deleteDestinationsetById } from '../api/call-forward'; export default { namespaced: true, @@ -11,7 +13,12 @@ export default { mappings: null, sourcesets: null, timesets: null, - destinationsets: null + destinationsets: null, + alwaysEverybodyDestinations: { + online: [], + busy: [], + offline: [] + } }, mutations: { loadMappings(state, result) { @@ -25,6 +32,9 @@ export default { }, loadDestinationsets(state, result) { state.destinationsets = result; + }, + loadAlwaysEverybodyDestinations(state, result) { + state.alwaysEverybodyDestinations = result; } }, actions: { @@ -53,7 +63,7 @@ export default { getTimesets(localStorage.getItem('subscriberId')) .then(result => { context.commit('loadTimesets', result); - }).catch(err => { + }).catch((err) => { reject(err); }); }); @@ -67,6 +77,33 @@ export default { reject(err); }); }); + }, + loadAlwaysEverybodyDestinations(context) { + return new Promise((resolve, reject)=>{ + loadAlwaysEverybodyDestinations(localStorage.getItem('subscriberId')).then((result)=>{ + context.commit('loadAlwaysEverybodyDestinations', result); + }) + }); + }, + deleteDestinationFromDestinationset(context, options) { + return new Promise((resolve, reject) => { + deleteDestinationFromDestinationset(options) + .then((result) => { + resolve(result); + }).catch((err) => { + reject(err); + }); + }); + }, + deleteDestinationsetById(context, id) { + return new Promise((resolve, reject) => { + deleteDestinationsetById(id) + .then((result) => { + resolve(result); + }).catch((err) => { + reject(err); + }); + }); } } }; diff --git a/t/api/call-forward.js b/t/api/call-forward.js index b52d0f8d..547c90e1 100644 --- a/t/api/call-forward.js +++ b/t/api/call-forward.js @@ -4,7 +4,8 @@ import Vue from 'vue'; import VueResource from 'vue-resource'; import { getMappings, getSourcesets, getTimesets, - getDestinationsets } from '../../src/api/call-forward'; + getDestinationsets, getDestinationsetById, + deleteDestinationFromDestinationset } from '../../src/api/call-forward'; import { assert } from 'chai'; Vue.use(VueResource); @@ -229,4 +230,133 @@ describe('CallForward', function(){ }); }); + it('should get all call forward destinationset by id', function(done){ + + let data = { + "_links": { + "collection": { + "href": "/api/cfdestinationsets/" + }, + "curies": { + "href": "http://purl.org/sipwise/ngcp-api/#rel-{rel}", + "name": "ngcp", + "templated": true + }, + "ngcp:subscribers": { + "href": "/api/subscribers/309" + }, + "profile": { + "href": "http://purl.org/sipwise/ngcp-api/" + }, + "self": { + "href": "/api/cfdestinationsets/3" + } + }, + "destinations": [ + { + "announcement_id": null, + "destination": "sip:3333@192.168.178.23", + "priority": 1, + "simple_destination": "3333", + "timeout": 60 + }, + { + "announcement_id": null, + "destination": "sip:3333@192.168.178.23", + "priority": 1, + "simple_destination": "3333", + "timeout": 90 + }, + { + "announcement_id": null, + "destination": "sip:vmu04ee2ae6-aa11-4cee-82fe-5ac57c11174e@voicebox.local", + "priority": 1, + "timeout": 300 + }, + { + "announcement_id": null, + "destination": "sip:04ee2ae6-aa11-4cee-82fe-5ac57c11174e@fax2mail.local", + "priority": 1, + "timeout": 300 + } + ], + "id": 3, + "name": "t2" + }; + + let responseData = { + "destinations": [ + { + "announcement_id": null, + "destination": "sip:3333@192.168.178.23", + "priority": 1, + "simple_destination": "3333", + "timeout": 60 + }, + { + "announcement_id": null, + "destination": "sip:3333@192.168.178.23", + "priority": 1, + "simple_destination": "3333", + "timeout": 90 + }, + { + "announcement_id": null, + "destination": "sip:vmu04ee2ae6-aa11-4cee-82fe-5ac57c11174e@voicebox.local", + "priority": 1, + "timeout": 300 + }, + { + "announcement_id": null, + "destination": "sip:04ee2ae6-aa11-4cee-82fe-5ac57c11174e@fax2mail.local", + "priority": 1, + "timeout": 300 + } + ], + "id": 3, + "name": "t2" + }; + + Vue.http.interceptors = []; + Vue.http.interceptors.unshift((request, next)=>{ + next(request.respondWith(JSON.stringify(data), { + status: 200 + })); + }); + getDestinationsetById('3').then((result)=>{ + assert.deepEqual(result, responseData); + done(); + }).catch((err)=>{ + done(err); + }); + }); + + it('should delete destination from call forward destinationset', function(done){ + + let options = { + id: 3, + data: { + "announcement_id": null, + "destination": "sip:3333@192.168.178.23", + "priority": 1, + "simple_destination": "3333", + "timeout": 60 + }, + deleteDestinationset: false + }; + + Vue.http.interceptors = []; + Vue.http.interceptors.unshift((request, next)=>{ + next(request.respondWith(JSON.stringify({}), { + status: 204 + })); + }); + deleteDestinationFromDestinationset(options).then((result)=>{ + assert.isOk(result); + done(); + }).catch((err)=>{ + done(err); + }); + }); + }); diff --git a/t/store/call-forward.js b/t/store/call-forward.js index e69de29b..514a0809 100644 --- a/t/store/call-forward.js +++ b/t/store/call-forward.js @@ -0,0 +1,40 @@ + +'use strict'; + +import CallForwardModule from '../../src/store/call-forward'; +import { assert } from 'chai'; + +describe('CallForward', function(){ + + it('should load always everybody destinations', function(){ + let state = { + alwaysEverybodyDestinations: [ + ] + }; + let data = { + busy: [], + offline: [{ + destinations: [{ + "announcement_id": null, + "destination": "sip:3333@192.168.178.23", + "priority": 1, + "simple_destination": "3333", + "timeout": 60 + }, + { + "announcement_id": null, + "destination": "sip:2222@192.168.178.23", + "priority": 1, + "simple_destination": "2222", + "timeout": 300 + }], + id: 3, + name: "csc_destinationset_1" + }], + online: [] + }; + CallForwardModule.mutations.loadAlwaysEverybodyDestinations(state, data); + assert.deepEqual(state.alwaysEverybodyDestinations, data); + }); + +});