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 @@ + + + + + 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"