diff --git a/.eslintrc.js b/.eslintrc.js index 414fba3d..a822147d 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -5,11 +5,12 @@ module.exports = { sourceType: 'module' }, env: { - browser: true + browser: true, + { "es6": true } }, // https://github.com/feross/standard/blob/master/RULES.md#javascript-standard-style extends: [ - 'standard' + 'eslint:recommended' ], // required to lint *.vue files plugins: [ @@ -34,6 +35,7 @@ module.exports = { 'import/export': 2, // allow debugger during development 'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0, - 'brace-style': [2, 'stroustrup', { 'allowSingleLine': true }] + 'brace-style': [2, 'stroustrup', { 'allowSingleLine': true }], + "no-console": 0 } } diff --git a/TESTDATA.md b/TESTDATA.md new file mode 100644 index 00000000..fd0afeb9 --- /dev/null +++ b/TESTDATA.md @@ -0,0 +1,141 @@ +# CONVERSATIONS MODULE + +IMPORTANT: This document is meant for internal use at Sipwise, as it requires a vagrant-ngcp environment to be able to generate the test data. + +Here are details on how to create test data for the /api/conversations endpoint that the conversations module is consuming. + +## VOICEMAIL + +1. Go to Settings > Subscribers > Details > Preferences, and under Call Forwards create a call forward destination under "Call Forward Unconditional" to voicemail +1. Initiate a call from another subscriber to the subscriber you just set call forward for, and leave a voicemail + +See [calls section](#calls) for information on how to initiate calls. + +## SMS + +1. SSH in to the vagrant box, become root, and edit config.yml, set sms: enable: yes, and enter sms credentials for all affected fields: +[https://wiki.sipwise.com/wiki/index.php/SMS](https://wiki.sipwise.com/wiki/index.php/SMS) +1. ngcpcfg apply 'commit msg' +1. Go to Settings > Subscribers, find subscriber you want to use, and click "Details" +1. Copy one of the numbers from wiki (link in step 1 above) and add to E164 Number field +1. Send sms from a mobile phone to E164 number +1. To troubleshoot you can check log: +`tail -f /var/log/kannel/sms*` + +For outgoing you need to use the api (also with E 164 number set to sms number from wiki). According to gjungwirth pretty simple with POST request containing the destination number and the content.  + +## FAX +1. Go to Settings > Subscribers, find subscriber you want to use as caller, and click "Details" +1. Under "Master Data" click edit, and enter subscribers number also in the E164 field +1. Repeat the two steps above, this time for the callee +1. SSH in to the vagrant box, become root, and create a text file to use for sending fax: +`echo 'test test' > filetosend.txt` +1. Use the ngcp-fax tool to send a fax: +`ngcp-fax --caller 43993003 --callee 43993002 --file filetosend.txt` +1. To troubleshoot you can check log: +`tail -f /var/log/ngcp/faxserver.log` + +## CALLS + +Can be created by calling from one subscriber to another using the ngcp-csc-ui call (rtc:engine) feature. + +Another option is to use a soft phone client such as linphone for desktop and mobile. Simply enter the subscriber username and password, and in the domain field enter the ip of the vagrant box you're using. + +## XMPP CHAT + +Should be created using an xmpp client authenticating as subscribers, but have not been able to verify this + +## MOCK DATA + +You can also use mock data in src/api/conversations.js, replacing the resolve() with this: + +` +resolve( + { + "_embedded": { + "ngcp:conversations": [ + { + "_links": { + }, + "call_id": "SZ8e64JkCq", + "call_type": "call", + "callee": "43993006", + "caller": "43993007", + "direction": "out", + "duration": "0:00:08.592", + "id": 3, + "rating_status": "ok", + "start_time": "2017-11-07 14:19:00.526", + "status": "ok", + "type": "call" + }, + { + "_links": { + }, + "callee": "43993006", + "caller": "43993007", + "filename": "2cfb4c14-1958-472e-87a4-731ce5233514.tif", + "id": 1, + "pages": 0, + "start_time": "2017-11-07 10:46:11", + "status": "FAILED", + "subscriber_id": 235, + "type": "fax" + }, + { + "_links": { + }, + "callee": "43993006", + "caller": "43993007", + "filename": "264ba55c-e23c-4d41-8493-f096bb98add9.tif", + "id": 3, + "pages": 0, + "start_time": "2017-11-07 10:49:23", + "status": "FAILED", + "subscriber_id": 235, + "type": "fax" + }, + { + "_links": { + }, + "call_type": "cfu", + "callee": "43993007", + "caller": "vmu43993006@voicebox.local", + "direction": "out", + "duration": "0:00:06.854", + "id": 4, + "rating_status": "ok", + "start_time": "2017-11-07 15:21:55", + "status": "ok", + "type": "call" + }, + ] + }, + "_links": { + "curies": { + "href": "http://purl.org/sipwise/ngcp-api/#rel-{rel}", + "name": "ngcp", + "templated": true + }, + "ngcp:conversations": [ + { + "href": "/api/conversations/4?type=call" + }, + { + "href": "/api/conversations/1?type=fax" + }, + { + "href": "/api/conversations/4?type=fax" + } + ], + "profile": { + "href": "http://purl.org/sipwise/ngcp-api/" + }, + "self": { + "href": "/api/conversations/?page=1&rows=10" + } + }, + "total_count": 4 + } +); +` diff --git a/src/api/conversations.js b/src/api/conversations.js new file mode 100644 index 00000000..dce2dcd1 --- /dev/null +++ b/src/api/conversations.js @@ -0,0 +1,13 @@ + +import Vue from 'vue'; +import { getJsonBody } from './utils' + +export function getConversations(id) { + return new Promise((resolve, reject) => { + Vue.http.get('/api/conversations/?subscriber_id=' + id).then((result)=>{ + resolve(getJsonBody(result.body)._embedded['ngcp:conversations']); + }).catch((err)=>{ + reject(err); + }); + }); +} diff --git a/src/components/card/CscCollapsible.vue b/src/components/card/CscCollapsible.vue new file mode 100644 index 00000000..81c4e5d2 --- /dev/null +++ b/src/components/card/CscCollapsible.vue @@ -0,0 +1,37 @@ + + + + + diff --git a/src/components/pages/CallBlocking/CscCallBlocking.vue b/src/components/pages/CallBlocking/CscCallBlocking.vue index 624f9870..d89eb9ae 100644 --- a/src/components/pages/CallBlocking/CscCallBlocking.vue +++ b/src/components/pages/CallBlocking/CscCallBlocking.vue @@ -5,9 +5,9 @@
- {{ $t('pages.callBlocking' + suffix + '.addNumberButton') }} + {{ $t('pages.callBlocking' + suffix + '.addNumberButton') }}
@@ -17,6 +17,7 @@ {{ $t('buttons.save') }}
+
@@ -71,9 +72,9 @@ }, mounted() { this.listLoading = true; - this.$store.dispatch('callBlocking/load' + this.suffix).then(()=>{ + this.$store.dispatch('callBlocking/load' + this.suffix).then(() => { this.listLoading = false; - }).catch((err)=>{ + }).catch((err) => { this.listLoading = false; }); }, @@ -98,10 +99,10 @@ CscPage }, computed: { - numbers (){ + numbers() { return this.$store.state.callBlocking[this.pageName + 'List']; }, - enabled () { + enabled() { return this.$store.state.callBlocking[this.pageName + 'Enabled']; }, toggleButtonLabel() { @@ -118,7 +119,7 @@ return this.$i18n.t('pages.callBlocking' + this.suffix + '.toggleDisabledToast'); } }, - suffix () { + suffix() { return _.upperFirst(this.pageName); } }, @@ -195,7 +196,7 @@ ] }); }, - toggle (enabled) { + toggle(enabled) { this.$store.dispatch('callBlocking/toggle' + this.suffix, enabled).then(()=>{ showToast(this.toggleToastMessage); }).catch((err)=>{ @@ -208,12 +209,12 @@ diff --git a/src/components/pages/Conversations.vue b/src/components/pages/Conversations.vue index ed75cbb3..d7b31f27 100644 --- a/src/components/pages/Conversations.vue +++ b/src/components/pages/Conversations.vue @@ -1,18 +1,88 @@ - diff --git a/src/filters/date.js b/src/filters/date.js new file mode 100644 index 00000000..3395533d --- /dev/null +++ b/src/filters/date.js @@ -0,0 +1,7 @@ +import { date } from 'quasar' +const { formatDate } = date + +export default function(value) { + var timeStamp = new Date(value); + return `${formatDate(timeStamp, 'MMMM D, YYYY')} at ${formatDate(timeStamp, 'h:mm a')}`; +} diff --git a/src/filters/index.js b/src/filters/index.js index 813a6f85..ae1159be 100644 --- a/src/filters/index.js +++ b/src/filters/index.js @@ -1,5 +1,7 @@ import Vue from 'vue'; import NumberFilter from './number' +import DateFilter from './date' Vue.filter('number', NumberFilter); +Vue.filter('readableDate', DateFilter); diff --git a/src/i18n.js b/src/i18n.js index 5ea22ea5..caff41e8 100644 --- a/src/i18n.js +++ b/src/i18n.js @@ -1,5 +1,4 @@ - import Vue from 'vue' import VueI18n from 'vue-i18n' diff --git a/src/index.html b/src/index.html index 4e7bfd2c..8aa90e20 100644 --- a/src/index.html +++ b/src/index.html @@ -5,9 +5,8 @@ - - Quasar App - + Sipwise Customer Self Care +
diff --git a/src/locales/en.json b/src/locales/en.json index e4b31301..967a95a2 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -81,6 +81,18 @@ "addInputError": "Input a valid number or subscriber name", "addNumberButton": "Add number" }, + "conversations": { + "title": "Conversations", + "labels": { + "successful": "Successful", + "unsuccessful": "Unsuccessful", + "from": "from", + "to": "to" + }, + "buttons": { + "call": "CALL" + } + }, "reminder": { "title": "Reminder", "timeLabel": "Time of the day", diff --git a/src/main.js b/src/main.js index 1bdec8ed..e428ae85 100644 --- a/src/main.js +++ b/src/main.js @@ -19,6 +19,7 @@ import { i18n, locales } from './i18n' import router from './router' import { sync } from 'vuex-router-sync' import { RtcEngineCall } from './plugins/call' +import filter from './filters' Vue.use(VueResource); @@ -31,7 +32,6 @@ if (__THEME === 'mat') { require('quasar-extras/roboto-font') } import 'quasar-extras/material-icons' -// import 'quasar-extras/ionicons' import 'quasar-extras/fontawesome' import 'quasar-extras/animate' diff --git a/src/statics/favicon.ico b/src/statics/favicon.ico new file mode 100644 index 00000000..3af0982e Binary files /dev/null and b/src/statics/favicon.ico differ diff --git a/src/statics/quasar-logo.png b/src/statics/quasar-logo.png deleted file mode 100644 index 590e8ce5..00000000 Binary files a/src/statics/quasar-logo.png and /dev/null differ diff --git a/src/store/conversations.js b/src/store/conversations.js new file mode 100644 index 00000000..c5a30b0f --- /dev/null +++ b/src/store/conversations.js @@ -0,0 +1,41 @@ + +'use strict'; + +import _ from 'lodash'; +import { getConversations } from '../api/conversations'; + +export default { + namespaced: true, + state: { + conversations: [ + ] + }, + mutations: { + loadConversations(state, options) { + let list = []; + _.forEach(options, function(item) { + delete item._links; + if (item.type == 'call') { + item.type = item.call_type != 'call' ? 'call forward' + : item.type; + }; + list.push(item); + }) + state.conversations = list; + } + }, + actions: { + loadConversations(context) { + return new Promise((resolve, reject)=>{ + getConversations(localStorage.getItem('subscriberId')) + .then((result)=>{ + context.commit('loadConversations', result); + resolve(); + }) + .catch((err)=>{ + reject(err); + }); + }); + } + } +}; diff --git a/src/store/index.js b/src/store/index.js index 43999416..79b42921 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -8,6 +8,7 @@ import PbxGroupsModule from './pbx-groups' import CallBlockingModule from './call-blocking' import ReminderModule from './reminder' import CallModule from './call' +import ConversationsModule from './conversations' Vue.use(Vuex); @@ -17,6 +18,7 @@ export const store = new Vuex.Store({ pbxGroups: PbxGroupsModule, callBlocking: CallBlockingModule, reminder: ReminderModule, - call: CallModule + call: CallModule, + conversations: ConversationsModule } }); diff --git a/t/api/conversations.js b/t/api/conversations.js new file mode 100644 index 00000000..70c64643 --- /dev/null +++ b/t/api/conversations.js @@ -0,0 +1,72 @@ + +'use strict'; + +import Vue from 'vue'; +import VueResource from 'vue-resource'; +import { getConversations } from '../../src/api/conversations'; +import { assert } from 'chai'; + +Vue.use(VueResource); + +describe('Conversations', function(){ + + const subscriberId = 123; + + it('should get all data regarding conversations', function(done){ + + let innerData = [{ + "_links" : { + "collection" : { + "href" : "/api/conversations/" + }, + "curies" : { + "href" : "http://purl.org/sipwise/ngcp-api/#rel-{rel}", + "name" : "ngcp", + "templated" : true + }, + "ngcp:calls" : { + "href" : "/api/calls/5" + }, + "ngcp:conversations" : { + "href" : "/api/conversations/5?type=call" + }, + "profile" : { + "href" : "http://purl.org/sipwise/ngcp-api/" + }, + "self" : { + "href" : "/api/conversations/5?type=call" + } + }, + "call_id" : "cT1miqD5Nw", + "call_type" : "cfu", + "callee" : "vmu43993006@voicebox.local", + "caller" : "43993006", + "direction" : "out", + "duration" : "0:00:19.672", + "id" : 5, + "rating_status" : "ok", + "start_time" : "2017-11-10 08:51:10.452", + "status" : "ok", + "type" : "call" + }]; + let data = { + "_embedded": { + "ngcp:conversations": innerData + } + }; + + Vue.http.interceptors = []; + Vue.http.interceptors.unshift((request, next)=>{ + next(request.respondWith(JSON.stringify(data), { + status: 200 + })); + }); + getConversations(subscriberId).then((result)=>{ + assert.deepEqual(result, innerData); + done(); + }).catch((err)=>{ + done(err); + }); + }); + +}); diff --git a/t/api/subscriber.js b/t/api/subscriber.js index 5f4e8b9a..02d44b92 100644 --- a/t/api/subscriber.js +++ b/t/api/subscriber.js @@ -1,3 +1,4 @@ + 'use strict'; import Vue from 'vue'; @@ -7,9 +8,10 @@ import { assert } from 'chai'; Vue.use(VueResource); - describe('Subscriber', function(){ + const subscriberId = 123; + it('should get all subscriber preferences', function(done) { Vue.http.interceptors = []; Vue.http.interceptors.unshift((request, next)=>{ @@ -20,7 +22,7 @@ describe('Subscriber', function(){ status: 200 })); }); - getPreferences('123').then((result)=>{ + getPreferences(subscriberId).then((result)=>{ assert.property(result, 'block_in_mode'); assert.isFalse(result.block_in_mode); assert.property(result, 'clir'); @@ -40,7 +42,7 @@ describe('Subscriber', function(){ status: 403 })); }); - getPreferences('123').then(()=>{ + getPreferences(subscriberId).then(()=>{ done(new Error('Test failed')); }).catch((err)=>{ assert.equal(err.status, 403); diff --git a/t/store/conversations.js b/t/store/conversations.js new file mode 100644 index 00000000..ac738626 --- /dev/null +++ b/t/store/conversations.js @@ -0,0 +1,44 @@ + +'use strict'; + +import ConversationsModule from '../../src/store/conversations'; +import { assert } from 'chai'; + +describe('Conversations', function(){ + + it('should load conversations', function(){ + let state = { + conversations: [ + ] + }; + let data = [ + { + "_links": { + }, + "call_type": "cfu", + "caller": "43993010", + "type": "call" + }, + { + "_links": { + }, + "caller": "43993011", + "type": "fax" + } + ]; + ConversationsModule.mutations.loadConversations(state, data); + assert.deepEqual(state.conversations, [ + { + "call_type": "cfu", + "caller": "43993010", + "type": "call forward" + }, + { + "caller": "43993011", + "type": "fax" + } + ]); + + }); + +});