diff --git a/package.json b/package.json
index 4d0b6e25..abb859f0 100644
--- a/package.json
+++ b/package.json
@@ -38,6 +38,7 @@
"decode-uri-component": "^0.4.0",
"eventsource": "^2.0.2",
"file-saver": "^2.0.2",
+ "content-disposition": "^0.5.4",
"jest-junit": "^11.1.0",
"jssip": "3.8.2",
"jwt-decode": "^2.2.0",
@@ -46,6 +47,7 @@
"lodash": "4.17.21",
"moment": "^2.29.4",
"npm": "^9.0.0",
+ "path": "0.12.7",
"qrcode": "1.5.0",
"quasar": "2",
"stream": "npm:stream-browserify",
diff --git a/src/api/common.js b/src/api/common.js
index d07b6a52..b09c3d40 100644
--- a/src/api/common.js
+++ b/src/api/common.js
@@ -9,7 +9,7 @@ import {
hasJwt
} from 'src/auth'
import { getCurrentLangAsV1Format } from 'src/i18n'
-
+import saveAs from 'file-saver'
export const LIST_DEFAULT_PAGE = 1
export const LIST_DEFAULT_ROWS = 24
export const LIST_ALL_ROWS = 1000
@@ -397,3 +397,41 @@ export function getAsBlob (options) {
})
})
}
+export async function apiGet (options = {
+ path: undefined,
+ resource: undefined,
+ resourceId: undefined,
+ config: {}
+}) {
+ let path = options.path
+ if (options.resource && options.resourceId) {
+ path = 'api/' + options.resource + '/' + options.resourceId
+ } else if (options.resource) {
+ path = 'api/' + options.resource + '/'
+ }
+ return httpApi.get(path, options.config).catch(handleResponseError)
+}
+export async function apiPost (options = {
+ resource: undefined,
+ data: undefined,
+ config: {}
+}) {
+ let path = options.path
+ if (options.resource) {
+ path = options.resource + '/'
+ }
+ return httpApi.post(path, options.data, _.merge({
+ headers: {
+ Prefer: 'return=representation'
+ }
+ }, options.config)).catch(handleResponseError)
+}
+export async function apiDownloadFile ({ apiGetOptions, defaultFileName, defaultContentType }) {
+ const res = await apiGet(apiGetOptions)
+ const fileName = defaultFileName
+ saveAs(new Blob([res.data], { type: res.headers['content-type'] || defaultContentType }), fileName)
+}
+export async function apiUploadCsv (options) {
+ const res = await apiPost(options)
+ return res
+}
diff --git a/src/api/subscriber.js b/src/api/subscriber.js
index 51dbccc6..b152cb65 100644
--- a/src/api/subscriber.js
+++ b/src/api/subscriber.js
@@ -12,7 +12,8 @@ import {
patchRemove,
patchReplaceFull,
patchAddFull,
- httpApi
+ httpApi,
+ apiUploadCsv
} from './common'
import {
@@ -62,15 +63,20 @@ export async function setPreferencePhonebook (id, field, value) {
try {
await replacePreferencePhonebook(id, field, value)
} catch (err) {
- const errCode = err.status + ''
- if (errCode === '422') {
- // eslint-disable-next-line no-useless-catch
- try {
- await addPreferencePhonebook(id, field, value)
- } catch (innerErr) {
- throw innerErr
- }
- } else {
+ if (err) {
+ throw err
+ }
+ }
+ }
+}
+export async function setPreferencePhonebookCustomer (id, field, value) {
+ if (value === undefined || value === null || value === '' || (Array.isArray(value) && !value.length)) {
+ await removePreferencePhonebookCustomer(id, field)
+ } else {
+ try {
+ await replacePreferencePhonebookCustomer(id, field, value)
+ } catch (err) {
+ if (err) {
throw err
}
}
@@ -111,23 +117,16 @@ export async function removePreferencePhonebook (id, field) {
fieldPath: field
})
}
-export function addPreference (id, field, value) {
- return new Promise((resolve, reject) => {
- patchAdd({
- path: 'api/subscriberpreferences/' + id,
- fieldPath: field,
- value: value
- }).then(() => {
- resolve()
- }).catch((err) => {
- reject(err)
- })
+export async function removePreferencePhonebookCustomer (id, field) {
+ return await patchRemove({
+ path: 'api/customerphonebookentries/' + id,
+ fieldPath: field
})
}
-export function addPreferencePhonebook (id, field, value) {
+export function addPreference (id, field, value) {
return new Promise((resolve, reject) => {
patchAdd({
- path: 'api/subscriberphonebookentries/' + id,
+ path: 'api/subscriberpreferences/' + id,
fieldPath: field,
value: value
}).then(() => {
@@ -177,6 +176,19 @@ export function replacePreferencePhonebook (id, field, value) {
})
})
}
+export function replacePreferencePhonebookCustomer (id, field, value) {
+ return new Promise((resolve, reject) => {
+ patchReplace({
+ path: 'api/customerphonebookentries/' + id,
+ fieldPath: field,
+ value: value
+ }).then(() => {
+ resolve()
+ }).catch((err) => {
+ reject(err)
+ })
+ })
+}
export function prependItemToArrayPreference (id, field, value) {
return new Promise((resolve, reject) => {
Promise.resolve().then(() => {
@@ -766,6 +778,24 @@ export async function getSubscriberPhonebook (options) {
})
return list
}
+export async function getCustomerPhonebook (options) {
+ let all = false
+ if (options.rows === 0) {
+ delete options.rows
+ delete options.page
+ all = true
+ }
+ if (!options.order_by) {
+ delete options.order_by
+ delete options.order_by_direction
+ }
+ const list = await getList({
+ resource: 'customerphonebookentries',
+ all,
+ params: options
+ })
+ return list
+}
export async function createPhonebook (data) {
const payLoad = {
name: data.name,
@@ -774,15 +804,41 @@ export async function createPhonebook (data) {
}
return await httpApi.post('api/subscriberphonebookentries/', payLoad)
}
+export async function createCustomerPhonebook (data) {
+ const payLoad = {
+ name: data.name,
+ number: data.number
+ }
+ return await httpApi.post('api/customerphonebookentries/', payLoad)
+}
+export async function uploadCsv (context, formData) {
+ const config = {
+ headers: {
+ 'Content-Type': 'text/csv'
+ }
+ }
+ const purgeExistingValue = formData?.purge_existing ? '1' : '0'
+ await apiUploadCsv({
+ path: 'api/customerphonebookentries' + '/?purge_existing=' + purgeExistingValue + '&customer_id=' + formData.customer_id,
+ data: formData.file,
+ config
+ })
+}
export function setValueShared (id, value) {
return setPreferencePhonebook(id, 'shared', value)
}
export function setValueName (id, value) {
return setPreferencePhonebook(id, 'name', value)
}
+export function setValueNameCustomer (id, value) {
+ return setPreferencePhonebookCustomer(id, 'name', value)
+}
export function setValueNumber (id, value) {
return setPreferencePhonebook(id, 'number', value)
}
+export function setValueNumberCustomer (id, value) {
+ return setPreferencePhonebookCustomer(id, 'number', value)
+}
export async function getRecordingStream (fileId) {
return await getAsBlob({
path: 'api/callrecordingfiles/' + fileId
diff --git a/src/components/CscMainMenuTop.vue b/src/components/CscMainMenuTop.vue
index d95ae85b..7f213bb0 100644
--- a/src/components/CscMainMenuTop.vue
+++ b/src/components/CscMainMenuTop.vue
@@ -210,6 +210,12 @@ export default {
icon: 'dialpad',
label: this.$t('Auto Attendant'),
visible: this.hasSubscriberProfileAttribute(PROFILE_ATTRIBUTE_MAP.auto_attendant)
+ },
+ {
+ to: '/user/pbx-configuration/customer-phonebook',
+ icon: 'person',
+ label: this.$t('Customer Phonebook'),
+ visible: true
}
]
},
diff --git a/src/pages/CscPageCustomerPhonebook.vue b/src/pages/CscPageCustomerPhonebook.vue
new file mode 100644
index 00000000..ce43f75e
--- /dev/null
+++ b/src/pages/CscPageCustomerPhonebook.vue
@@ -0,0 +1,228 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ $t('Refresh') }}
+
+
+
+
+
+
+
+
+
+ |
+
+
+
+
+
+
+
diff --git a/src/pages/CscPageCustomerPhonebookAdd.vue b/src/pages/CscPageCustomerPhonebookAdd.vue
new file mode 100644
index 00000000..219c5faf
--- /dev/null
+++ b/src/pages/CscPageCustomerPhonebookAdd.vue
@@ -0,0 +1,138 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/pages/CscPageCustomerPhonebookDetails.vue b/src/pages/CscPageCustomerPhonebookDetails.vue
new file mode 100644
index 00000000..8d350136
--- /dev/null
+++ b/src/pages/CscPageCustomerPhonebookDetails.vue
@@ -0,0 +1,145 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/pages/CscPageCustomerPhonebookUpload.vue b/src/pages/CscPageCustomerPhonebookUpload.vue
new file mode 100644
index 00000000..0b582d85
--- /dev/null
+++ b/src/pages/CscPageCustomerPhonebookUpload.vue
@@ -0,0 +1,117 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/router/routes.js b/src/router/routes.js
index 41e32f02..00e0ec28 100644
--- a/src/router/routes.js
+++ b/src/router/routes.js
@@ -15,6 +15,7 @@ import CscPageSpeedDial from 'src/pages/CscPageSpeedDial'
import CscPagePbxGroups from 'src/pages/CscPagePbxGroups'
import CscPagePbxGroupDetails from 'src/pages/CscPagePbxGroupDetails'
import CscPagePbxSeats from 'src/pages/CscPagePbxSeats'
+import CscPageCustomerPhonebook from 'src/pages/CscPageCustomerPhonebook'
import CscPagePbxSeatDetails from 'src/pages/CscPagePbxSeatDetails'
import CscPagePbxDevices from 'src/pages/CscPagePbxDevices'
import CscPagePbxCallQueues from 'src/pages/CscPagePbxCallQueues'
@@ -36,7 +37,10 @@ import CscPagePbxSettingsMsConfigs from 'pages/CscPagePbxSettingsMsConfigs'
import CscPagePbxSettingsCallQueues from 'pages/CscPagePbxSettingsCallQueues'
import CscPagePbxSoundSetDetails from 'src/pages/CscPagePbxSoundSetDetails'
import CscPageSubscriberPhonebookDetails from 'src/pages/CscPageSubscriberPhonebookDetails'
+import CscPageCustomerPhonebookDetails from 'src/pages/CscPageCustomerPhonebookDetails'
import CscPageSubscriberPhonebookAdd from 'src/pages/CscPageSubscriberPhonebookAdd'
+import CscPageCustomerPhonebookAdd from 'src/pages/CscPageCustomerPhonebookAdd'
+import CscPageCustomerPhonebookUpload from 'src/pages/CscPageCustomerPhonebookUpload'
import CscPagePbxStatisticsCdr from 'src/pages/CscPagePbxStatisticsCdr'
import { i18n } from 'src/boot/i18n'
@@ -251,6 +255,45 @@ const routes = [
}
}
},
+ {
+ path: 'pbx-configuration/customer-phonebook',
+ component: CscPageCustomerPhonebook,
+ meta: {
+ get title () {
+ return i18n.global.tc('PBX Configuration')
+ },
+ get subtitle () {
+ return i18n.global.tc('Customer Phonebook')
+ }
+ }
+ },
+ {
+ path: 'pbx-configuration/customer-phonebook/create',
+ component: CscPageCustomerPhonebookAdd,
+ meta: {
+ get title () {
+ return i18n.global.tc('Add Phonebook')
+ }
+ }
+ },
+ {
+ path: 'pbx-configuration/customer-phonebook/upload',
+ component: CscPageCustomerPhonebookUpload,
+ meta: {
+ get title () {
+ return i18n.global.tc('Upload CSV')
+ }
+ }
+ },
+ {
+ path: 'pbx-configuration/customer-phonebook/:id',
+ component: CscPageCustomerPhonebookDetails,
+ meta: {
+ get title () {
+ return i18n.global.tc('Customer Phonebook')
+ }
+ }
+ },
{
path: 'pbx-configuration/seat/:id',
component: CscPagePbxSeatDetails,
diff --git a/src/store/user.js b/src/store/user.js
index 201eff45..c254ce74 100644
--- a/src/store/user.js
+++ b/src/store/user.js
@@ -15,12 +15,17 @@ import {
getBrandingLogo,
getSubscriberRegistrations,
getSubscriberPhonebook,
+ getCustomerPhonebook,
getSubscriberProfile,
setValueShared,
setValueName,
+ setValueNameCustomer,
+ setValueNumberCustomer,
setValueNumber,
changeSIPPassword,
createPhonebook,
+ createCustomerPhonebook,
+ uploadCsv,
getNcosLevels,
getNcosSet,
getPreferences,
@@ -37,7 +42,8 @@ import { setLocal } from 'src/storage'
import { getSipInstanceId } from 'src/helpers/call-utils'
import { PROFILE_ATTRIBUTE_MAP } from 'src/constants'
import {
- httpApi
+ httpApi,
+ apiDownloadFile
} from 'src/api/common'
export default {
@@ -68,6 +74,7 @@ export default {
defaultBranding: {},
subscriberRegistrations: [],
subscriberPhonebook: [],
+ customerPhonebook: [],
phonebookMap: {},
platformInfo: null,
qrCode: null,
@@ -289,6 +296,9 @@ export default {
setSubscriberPhonebook (state, value) {
state.subscriberPhonebook = value
},
+ setCustomerPhonebook (state, value) {
+ state.customerPhonebook = value
+ },
setProfile (state, value) {
state.profile = value
},
@@ -433,12 +443,45 @@ export default {
throw err
}
},
+ async loadCustomerPhonebook ({ commit, dispatch, state, rootGetters }, options) {
+ try {
+ const list = await getCustomerPhonebook({
+ ...options
+ })
+ commit('setCustomerPhonebook', list.items)
+ return list.totalCount
+ } catch (err) {
+ commit('setCustomerPhonebook', [])
+ throw err
+ }
+ },
+ async ajaxDownloadPhonebookCSV ({ commit }, customerId = 0) {
+ const apiGetOptions = {
+ resource: 'customerphonebookentries',
+ config: {
+ headers: {
+ Accept: 'text/csv'
+ },
+ params: {
+ customer_id: customerId
+ }
+ }
+ }
+ await apiDownloadFile({
+ apiGetOptions,
+ defaultFileName: 'customer_phonebook_entries.csv',
+ defaultContentType: 'text/csv'
+ })
+ },
async removeSubscriberRegistration (context, row) {
await httpApi.delete('api/subscriberregistrations/' + row.id)
},
async removeSubscriberPhonebook (context, row) {
await httpApi.delete('api/subscriberphonebookentries/' + row.id)
},
+ async removeCustomerPhonebook (context, row) {
+ await httpApi.delete('api/customerphonebookentries/' + row.id)
+ },
async getNcosLevelsSubscriber () {
const ncosLevel = []
const list = await getNcosLevels()
@@ -478,6 +521,10 @@ export default {
const list = await httpApi.get('api/subscriberphonebookentries/' + id)
return list
},
+ async getPhonebookCustomerDetails (context, id) {
+ const list = await httpApi.get('api/customerphonebookentries/' + id)
+ return list
+ },
async getValueShared (context, options) {
await setValueShared(options.phonebookId, options.shared)
},
@@ -488,12 +535,24 @@ export default {
async getValueName (context, options) {
await setValueName(options.phonebookId, options.name)
},
+ async getValueNameCustomer (context, options) {
+ await setValueNameCustomer(options.phonebookId, options.name)
+ },
async getValueNumber (context, options) {
await setValueNumber(options.phonebookId, options.number)
},
+ async getValueNumberCustomer (context, options) {
+ await setValueNumberCustomer(options.phonebookId, options.number)
+ },
async createPhonebookSubscriber (context, data) {
await createPhonebook(data)
},
+ async createPhonebookCustomer (context, data) {
+ await createCustomerPhonebook(data)
+ },
+ async uploadPhonebookCustomer (context, data) {
+ await uploadCsv(context, data)
+ },
async fetchAuthToken ({ commit, state, getters }, expiringTime = 300) {
const subscriber = state.subscriber
const expireDate = date.addToDate(new Date(), { seconds: expiringTime })
diff --git a/yarn.lock b/yarn.lock
index 9b55a6c0..25f6523d 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -4198,7 +4198,7 @@ consolidate@0.16.0:
dependencies:
bluebird "^3.7.2"
-content-disposition@0.5.4, content-disposition@^0.5.2:
+content-disposition@0.5.4, content-disposition@^0.5.2, content-disposition@^0.5.4:
version "0.5.4"
resolved "https://npm-registry.sipwise.com/content-disposition/-/content-disposition-0.5.4.tgz#8b82b4efac82512a02bb0b1dcec9d2c5e8eb5bfe"
integrity sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==
@@ -9801,6 +9801,14 @@ path-type@^4.0.0:
resolved "https://npm-registry.sipwise.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b"
integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==
+path@0.12.7:
+ version "0.12.7"
+ resolved "https://npm-registry.sipwise.com/path/-/path-0.12.7.tgz#d4dc2a506c4ce2197eb481ebfcd5b36c0140b10f"
+ integrity sha512-aXXC6s+1w7otVF9UletFkFcDsJeO7lSZBPUQhtb5O0xJe8LtYhj/GxldoL09bBj9+ZmE2hNoHqQSFMN5fikh4Q==
+ dependencies:
+ process "^0.11.1"
+ util "^0.10.3"
+
pathval@^1.1.1:
version "1.1.1"
resolved "https://npm-registry.sipwise.com/pathval/-/pathval-1.1.1.tgz#8534e77a77ce7ac5a2512ea21e0fdb8fcf6c3d8d"
@@ -10216,7 +10224,7 @@ process-nextick-args@~2.0.0:
resolved "https://npm-registry.sipwise.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2"
integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==
-process@^0.11.10:
+process@^0.11.1, process@^0.11.10:
version "0.11.10"
resolved "https://npm-registry.sipwise.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182"
integrity sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==
@@ -12207,6 +12215,13 @@ util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1:
resolved "https://npm-registry.sipwise.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==
+util@^0.10.3:
+ version "0.10.4"
+ resolved "https://npm-registry.sipwise.com/util/-/util-0.10.4.tgz#3aa0125bfe668a4672de58857d3ace27ecb76901"
+ integrity sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A==
+ dependencies:
+ inherits "2.0.3"
+
utila@~0.4:
version "0.4.0"
resolved "https://npm-registry.sipwise.com/utila/-/utila-0.4.0.tgz#8a16a05d445657a3aea5eecc5b12a4fa5379772c"