From 01032cad602b84ae85c6f2b5309dddb27249744d Mon Sep 17 00:00:00 2001 From: Carlo Venusino <cvenusino@sipwise.com> Date: Tue, 13 Apr 2021 15:46:30 +0200 Subject: [PATCH] TT#117521 Auto-attendant - As a SubscriberAdmin, I want to configure Auto-attendant slots - TT#118754 Create new menu point under PBXConfiguration + new page + new route - TT#118755 Render a list of all slots of all Subscribers (Pilot, Seats, Groups) - TT#118756 Load Subscriber (Pilot, Seats, Groups) name for each slot in the list - TT#118757 Implement AddForm to be able to add a new AA-Slot - TT#118758 Implement Deletion for a AA-Slot including a confirmation Dialog - TT#118765 Implement EditForm to be able to edit the destination of an existing AA-Slot NOTES - In order to test the feature you need to create a Customer with Product = Cloud PBX Account, and at least one subscriber which you need to login into CSC. - There are a couple of components which have been created and not used due tue specs revision, but left in place for future possible utilisation: - Sorting by subscriber id and name has been removed as it is not supported by the endpoint src/components/form/CscSelectLazy.vue src/components/CscDataTableEditSelect.vue Change-Id: Iec29baecfa75f3c818b9deb945625a1bf977ca88 --- src/api/pbx-auto-attendants.js | 20 ++ src/components/CscDataTableEditInput.vue | 111 ++++++++ src/components/CscDataTableEditSelect.vue | 99 +++++++ src/components/CscMainMenuTop.vue | 6 + src/components/CscPopupMenuItem.vue | 2 +- src/components/form/CscSelectLazy.vue | 135 +++++++++ .../CscNewCallForwardDateRange.vue | 4 +- .../CscPbxAutoAttendantAddForm.vue | 149 ++++++++++ ...n.vue => CscPbxAutoAttendantSelection.vue} | 2 +- .../CscPbxAutoAttendantSlotsTable.vue | 261 ++++++++++++++++++ .../PbxConfiguration/CscPbxDeviceConfig.vue | 6 +- .../PbxConfiguration/CscPbxDeviceFilters.vue | 6 +- src/i18n/de.json | 20 +- src/i18n/en.json | 20 +- src/i18n/es.json | 20 +- src/i18n/fr.json | 20 +- src/i18n/it.json | 20 +- src/pages/CscPagePbxAutoAttendant.vue | 212 ++++++++++++++ src/router/routes.js | 9 + src/store/index.js | 4 +- src/store/pbx-auto-attendants.js | 115 ++++++++ 21 files changed, 1224 insertions(+), 17 deletions(-) create mode 100644 src/api/pbx-auto-attendants.js create mode 100644 src/components/CscDataTableEditInput.vue create mode 100644 src/components/CscDataTableEditSelect.vue create mode 100644 src/components/form/CscSelectLazy.vue create mode 100644 src/components/pages/PbxConfiguration/CscPbxAutoAttendantAddForm.vue rename src/components/pages/PbxConfiguration/{CscPbxAttendantSelection.vue => CscPbxAutoAttendantSelection.vue} (97%) create mode 100644 src/components/pages/PbxConfiguration/CscPbxAutoAttendantSlotsTable.vue create mode 100644 src/pages/CscPagePbxAutoAttendant.vue create mode 100644 src/store/pbx-auto-attendants.js diff --git a/src/api/pbx-auto-attendants.js b/src/api/pbx-auto-attendants.js new file mode 100644 index 00000000..d3d0738e --- /dev/null +++ b/src/api/pbx-auto-attendants.js @@ -0,0 +1,20 @@ +import Vue from 'vue' +import { patchReplaceFull } from 'src/api/common' + +export async function getAutoAttendants (options) { + const params = { ...options, ...{ expand: 1 } } + const res = await Vue.http.get('api/autoattendants/', { + params: params + }) + return res.body.total_count > 0 ? res.body : [] +} + +export async function editSubscriberSlots (options) { + const res = await patchReplaceFull({ + resource: 'autoattendants', + resourceId: options.subscriberId, + fieldPath: 'slots', + value: options.slots + }) + return res.slots +} diff --git a/src/components/CscDataTableEditInput.vue b/src/components/CscDataTableEditInput.vue new file mode 100644 index 00000000..2ad9718b --- /dev/null +++ b/src/components/CscDataTableEditInput.vue @@ -0,0 +1,111 @@ +<template> + <span + class="cursor-pointer" + > + <q-input + v-model="internalValue" + dense + hide-bottom-space + borderless + filled + :error="error" + :error-message="errorMessage" + :disable="$attrs.disable" + @keyup="save" + @clear="clear" + /> + </span> +</template> + +<script> +import { i18n } from 'src/boot/i18n' +export default { + name: 'CscDataTableEditInput', + props: { + column: { + type: Object, + required: true + }, + row: { + type: Object, + required: true + }, + value: { + type: [String, Number], + default: undefined + }, + saveLabel: { + type: String, + default: i18n.t('Save') + } + }, + data () { + return { + internalValue: this.value + } + }, + validations () { + const config = {} + if (this.column.componentValidations) { + config.internalValue = {} + this.column.componentValidations.forEach((validation) => { + config.internalValue[validation.name] = validation.validator + }) + } + return config + }, + computed: { + error () { + if (this.column.componentValidations) { + return this.$v.internalValue.$error + } else { + return false + } + }, + errorMessage () { + if (this.column.componentValidations) { + const validation = this.column.componentValidations.find(validation => + this.$v.internalValue[validation.name] === false + ) + if (validation) { + return validation.error + } else { + return undefined + } + } else { + return undefined + } + } + }, + watch: { + value (value) { + this.internalValue = value + } + }, + mounted () { + this.internalValue = this.value + this.$v.$reset() + }, + methods: { + validate () { + if (this.column.componentValidations) { + this.$v.$touch() + return !this.$v.$invalid + } else { + return true + } + }, + save () { + this.$v.$touch() + this.$emit('changed', { + column: this.column, + row: this.row, + value: this.internalValue + }) + }, + clear () { + this.$v.$reset() + } + } +} +</script> diff --git a/src/components/CscDataTableEditSelect.vue b/src/components/CscDataTableEditSelect.vue new file mode 100644 index 00000000..331497c9 --- /dev/null +++ b/src/components/CscDataTableEditSelect.vue @@ -0,0 +1,99 @@ +<template> + <span + class="cursor-pointer" + > + <template + v-if="value === '' || value === undefined || value === null" + > + <q-btn + icon="add" + dense + flat + size="sm" + :disable="$attrs.disable" + /> + </template> + {{ label }} + <q-popup-edit + v-model="internalValue" + buttons + :label-set="saveLabel" + @before-show="popupBeforeShowEvent" + @save="$emit('saved', { + column: column, + row: row, + value: internalValue + })" + > + <q-select + v-model="internalValue" + :options="filteredOptions || column.componentOptions" + :label="column.label" + emit-value + map-options + dense + autofocus + fill-input + :disable="$attrs.disable" + > + <template + v-if="column.componentIcon" + v-slot:prepend + > + <q-icon + :name="column.componentIcon" + /> + </template> + </q-select> + </q-popup-edit> + </span> +</template> + +<script> +import { i18n } from 'src/boot/i18n' +export default { + name: 'CscDataTableEditSelect', + props: { + column: { + type: Object, + required: true + }, + row: { + type: Object, + required: true + }, + value: { + type: [String, Number], + default: undefined + }, + filteredOptions: { + type: Array, + default: undefined + }, + saveLabel: { + type: String, + default: i18n.t('Save') + } + }, + data () { + return { + internalValue: this.value + } + }, + computed: { + label () { + const refOption = this.column.componentOptions.find(option => option.value === this.value) + if (refOption) { + return refOption.label + } else { + return this.value + } + } + }, + methods: { + popupBeforeShowEvent () { + this.internalValue = this.value + } + } +} +</script> diff --git a/src/components/CscMainMenuTop.vue b/src/components/CscMainMenuTop.vue index a6406782..d899a32b 100644 --- a/src/components/CscMainMenuTop.vue +++ b/src/components/CscMainMenuTop.vue @@ -179,6 +179,12 @@ export default { icon: 'arrow_forward', label: this.$t('Manager Secretary'), visible: true + }, + { + to: '/user/pbx-configuration/auto-attendant', + icon: 'dialpad', + label: this.$t('Auto-attendant'), + visible: true } ] }, diff --git a/src/components/CscPopupMenuItem.vue b/src/components/CscPopupMenuItem.vue index 2582cfb2..e5ca4f7f 100644 --- a/src/components/CscPopupMenuItem.vue +++ b/src/components/CscPopupMenuItem.vue @@ -35,7 +35,7 @@ export default { default: 'primary' }, label: { - type: String, + type: [String, Number], default: '' }, sublabel: { diff --git a/src/components/form/CscSelectLazy.vue b/src/components/form/CscSelectLazy.vue new file mode 100644 index 00000000..3a0b168b --- /dev/null +++ b/src/components/form/CscSelectLazy.vue @@ -0,0 +1,135 @@ +<template> + <q-select + ref="select" + :value="$attrs.value" + :options="filteredOptions" + emit-value + map-options + use-input + input-debounce="500" + :loading="$wait.is(waitIdentifier) || $attrs.loading" + v-bind="$attrs" + v-on="$listeners" + @filter="filter" + > + <template + v-slot:prepend + > + <slot + name="prepend" + /> + <q-icon + v-if="icon" + :name="icon" + /> + </template> + <template + v-slot:append + > + <slot + name="append" + /> + </template> + <template + v-slot:after + > + <slot + name="after" + /> + </template> + </q-select> +</template> + +<script> +import _ from 'lodash' +export default { + name: 'CscSelectLazy', + props: { + icon: { + type: String, + default: undefined + }, + storeGetter: { + type: String, + required: true + }, + storeAction: { + type: String, + required: true + }, + storeActionParams: { + type: Object, + default: null + }, + loadInitially: { + type: Boolean, + default: true + }, + filterCustomizationFunction: { + type: Function, + default: (filter) => filter + }, + initialOption: { + type: Object, + default: undefined + } + }, + data () { + return { + optionsWereUpdated: false + } + }, + computed: { + filteredOptions () { + let options = _.clone(this.$store.getters[this.storeGetter]) + if (options === undefined || options === null) { + options = [] + } + if (!this.optionsWereUpdated && this.initialOption && (options.length === 0 || options[0].disable === true)) { + options.splice(0, 1, this.initialOption) + } + if (options.length === 0) { + options.push({ + label: this.$t('No data found'), + disable: true + }) + } + return options + }, + waitIdentifier () { + return this.$vnode.tag + this.$vnode.componentInstance?._uid + } + }, + mounted () { + if (this.loadInitially) { + this.filter('') + } + }, + methods: { + async filter (filter, update, abort) { + this.$wait.start(this.waitIdentifier) + try { + const filterFinalised = this.filterCustomizationFunction(filter) + let options = filterFinalised + if (_.isObject(this.storeActionParams)) { + options = _.merge(this.storeActionParams, { + filter: filterFinalised + }) + } + await this.$store.dispatch(this.storeAction, options) + this.optionsWereUpdated = true + if (typeof update === 'function') { + update() + } + } catch (e) { + if (typeof abort === 'function') { + abort() + } + throw e + } finally { + this.$wait.end(this.waitIdentifier) + } + } + } +} +</script> diff --git a/src/components/pages/NewCallForward/CscNewCallForwardDateRange.vue b/src/components/pages/NewCallForward/CscNewCallForwardDateRange.vue index 56cd8e9e..11b2b217 100644 --- a/src/components/pages/NewCallForward/CscNewCallForwardDateRange.vue +++ b/src/components/pages/NewCallForward/CscNewCallForwardDateRange.vue @@ -391,13 +391,11 @@ export default { } </script> -<style lang="stylus" rel="stylesheet/stylus"> +<style lang="stylus" rel="stylesheet/stylus" scoped> .csc-cf-daterange-btn-cont margin-top 10px width 100% text-align center - .q-menu - min-width auto !important .q-datetime-days div:not(.q-datetime-day-active), .q-datetime-dark, .q-datetime-range .q-datetime-input .q-input-target, diff --git a/src/components/pages/PbxConfiguration/CscPbxAutoAttendantAddForm.vue b/src/components/pages/PbxConfiguration/CscPbxAutoAttendantAddForm.vue new file mode 100644 index 00000000..6937edf0 --- /dev/null +++ b/src/components/pages/PbxConfiguration/CscPbxAutoAttendantAddForm.vue @@ -0,0 +1,149 @@ +<template> + <div class="q-pa-xs"> + <div + class="csc-pbx-aa-form-cont" + > + <q-form + @submit="save" + :loading="$wait.is('csc-pbx-auto-attendant-form')" + class="csc-pbx-aa-form justify-center" + > + <div class="row"> + <csc-select-lazy + class="col-12" + icon="person" + :value="data.subscriberId" + :label="$t('Subscriber')" + clearable + store-getter="pbxAutoAttendants/subscribers" + store-action="pbxAutoAttendants/fetchSubscribers" + :load-initially="false" + v-bind="$attrs" + v-on="$listeners" + @input="setSubscriberId($event)" + /> + </div> + <div + v-for="(slotNumber, index) in slotsNumbers" + :key="index" + class="col-12" + > + <div + v-if="index % 2 === 0" + class="row" + > + <q-input + v-model="data.slots[index]" + :label="$t('Slot {number}', { + number: slotNumber + })" + dense + class="col-6 q-pa-xs" + /> + + <q-input + v-model="data.slots[index +1]" + :label="$t('Slot {number}', { + number: slotsNumbers[index +1] + })" + dense + class="col-6 q-pa-xs" + /> + </div> + </div> + <div + class="row justify-center" + > + <q-btn + flat + color="default" + icon="clear" + :disable="$wait.is('csc-pbx-auto-attendant-form')" + :label="$t('Cancel')" + @click="$emit('closeForm')" + /> + <q-btn + flat + color="primary" + icon="check" + :loading="$wait.is('csc-pbx-auto-attendant-form')" + :disabled="disableSave" + :label="$t('Save')" + @click="save" + /> + </div> + </q-form> + </div> + </div> +</template> + +<script> +import { mapGetters } from 'vuex' +import { required } from 'vuelidate/lib/validators' +import { mapWaitingActions } from 'vue-wait' +import { showToast } from 'src/helpers/ui' +import CscSelectLazy from 'components/form/CscSelectLazy' +export default { + name: 'CscPbxAutoAttendantAddForm', + components: { + CscSelectLazy + }, + data () { + return { + data: { + subscriberId: null, + slots: [] + }, + loading: false + } + }, + validations: { + data: { + subscriberId: { + required + } + } + }, + computed: { + ...mapGetters('pbxAutoAttendants', [ + 'slotsNumbers' + ]), + disableSave () { + return this.data.subscriberId === null || this.data.slots.length < 1 + } + }, + methods: { + ...mapWaitingActions('pbxAutoAttendants', { + updateSubscriberSlots: 'csc-pbx-auto-attendant-form' + }), + setSubscriberId (subscriberId) { + this.data.subscriberId = subscriberId + this.data.slots = [] + }, + async save () { + await this.updateSubscriberSlots({ + subscriberId: this.data.subscriberId, + slots: this.data.slots.map((slot, index) => { + if (slot) { + return { + slot: index, + destination: slot + } + } + }).filter(Boolean) + }) + showToast(this.$t('Slots successfully added')) + this.$emit('newSubscriberSaved') + this.$emit('closeForm') + } + } +} +</script> + +<style lang="stylus" rel="stylesheet/stylus" scoped> +.csc-pbx-aa-form-cont + width 100% +.csc-pbx-aa-form + width 50% + margin auto +</style> diff --git a/src/components/pages/PbxConfiguration/CscPbxAttendantSelection.vue b/src/components/pages/PbxConfiguration/CscPbxAutoAttendantSelection.vue similarity index 97% rename from src/components/pages/PbxConfiguration/CscPbxAttendantSelection.vue rename to src/components/pages/PbxConfiguration/CscPbxAutoAttendantSelection.vue index ebb82b9e..f96104e4 100644 --- a/src/components/pages/PbxConfiguration/CscPbxAttendantSelection.vue +++ b/src/components/pages/PbxConfiguration/CscPbxAutoAttendantSelection.vue @@ -36,7 +36,7 @@ <script> export default { - name: 'CscPbxAttendantSelection', + name: 'CscPbxAutoAttendantSelection', props: { showSelectedItemIcon: { type: Boolean, diff --git a/src/components/pages/PbxConfiguration/CscPbxAutoAttendantSlotsTable.vue b/src/components/pages/PbxConfiguration/CscPbxAutoAttendantSlotsTable.vue new file mode 100644 index 00000000..3abca339 --- /dev/null +++ b/src/components/pages/PbxConfiguration/CscPbxAutoAttendantSlotsTable.vue @@ -0,0 +1,261 @@ +<template> + <q-table + :data="data" + :columns="columns" + :loading="$wait.is('csc-pbx-autoattendant-slots-table')" + :pagination.sync="pagination" + :hide-pagination="true" + row-key="name" + class="csc-item-odd" + > + <template v-slot:loading> + <q-inner-loading + showing + color="primary" + /> + </template> + <template v-slot:header="props"> + <q-tr> + <q-th auto-width /> + <q-th + v-for="col in props.cols" + :key="col.name" + class="text-left" + > + {{ col.label }} + </q-th> + <q-th auto-width /> + <q-th auto-width /> + </q-tr> + <q-tr + v-for="(row, index) in unsavedSlots" + :key="index" + > + <q-td auto-width /> + <q-td> + {{ row.slot }} + </q-td> + <q-td> + <csc-data-table-edit-input + :column="{name:'destination', label: $t('Destination'), componentValidations: [getDestinationValidation()]}" + :row="{slot: row.slot, destination: row.destination}" + :value="row.destination" + :save-label="$t('Add')" + @changed="updateNewSlotDestination(index, $event.value)" + /> + </q-td> + <q-td> + <q-btn + icon="delete" + color="negative" + flat + dense + @click="resetNewSlot(index)" + /> + <q-btn + v-if="row.destination" + icon="check" + color="primary" + :label="$t('Save')" + flat + dense + @click="saveSlots" + /> + </q-td> + <q-td auto-width /> + </q-tr> + </template> + <template v-slot:body="props"> + <q-tr> + <q-td auto-width /> + <q-td + v-for="col in props.cols" + :key="col.name" + > + <div + v-if="col.name === 'slot'" + > + {{ col.value }} + </div> + <csc-data-table-edit-input + v-if="col.name === 'destination'" + :column="col" + :row="props.row" + :value="col.value" + @changed="editDestination(props.rowIndex, $event.value)" + /> + </q-td> + <q-td> + <q-btn + icon="delete" + color="negative" + flat + dense + @click="confirmRowDeletion(props.row.slot, props.rowIndex)" + /> + <q-btn + v-if="isRowDirty(props.rowIndex)" + icon="check" + color="primary" + :label="$t('Save')" + flat + dense + @click="saveSlots" + /> + </q-td> + <q-td auto-width /> + </q-tr> + </template> + </q-table> +</template> + +<script> +import _ from 'lodash' +import { mapWaitingActions } from 'vue-wait' +import { mapGetters } from 'vuex' +import { required } from 'vuelidate/lib/validators' +import { showGlobalError, showToast } from 'src/helpers/ui' +import CscDataTableEditInput from 'components/CscDataTableEditInput' +import CscRemoveDialog from 'components/CscRemoveDialog' + +export default { + name: 'CscPbxAutoAttendantSlotsTable', + components: { + CscDataTableEditInput + }, + props: { + data: { + type: Array, + default: undefined + }, + subscriberId: { + type: Number, + default: undefined + } + }, + data () { + return { + slots: [], + unsavedSlots: [], + dirtySlots: [], + columns: [ + { + name: 'slot', + align: 'left', + label: this.$t('Slot'), + field: row => row.slot, + componentOptions: this.slotsNumbers + }, + { + name: 'destination', + align: 'left', + label: this.$t('Destination'), + field: row => row.destination, + componentValidations: [this.getDestinationValidation()] + } + ], + pagination: { + page: 1, + rowsPerPage: 0 // 0 means all rows + } + } + }, + computed: { + ...mapGetters('pbxAutoAttendants', [ + 'slotsNumbers', + 'newSlots' + ]), + isRowDirty: (state) => (rowIndex) => { + return state.dirtySlots.includes(rowIndex) + } + }, + watch: { + data () { + this.initTable() + } + }, + mounted () { + this.initTable() + }, + methods: { + ...mapWaitingActions('pbxAutoAttendants', { + updateSubscriberSlots: 'csc-pbx-autoattendant-slots-table', + editNewSlot: 'csc-pbx-autoattendant-slots-table', + deleteNewSlot: 'csc-pbx-autoattendant-slots-table', + resetAllNewSlots: 'csc-pbx-autoattendant-slots-table' + }), + initTable () { + this.slots = _.cloneDeep(this.data) + this.unsavedSlots = this.newSlots.filter(slot => slot.subscriber_id === this.subscriberId)[0].slots + this.dirtySlots = [] + }, + updateNewSlotDestination (index, value) { + this.editNewSlot({ + subscriberId: this.subscriberId, + index: index, + destination: value + }) + }, + resetNewSlot (index) { + this.deleteNewSlot({ + subscriberId: this.subscriberId, + index: index + }) + }, + async editDestination (rowIndex, value) { + const destination = this.slots[rowIndex].destination + if (value !== null && value !== '' && destination !== value) { + this.slots[rowIndex].destination = value + this.dirtySlots.push(rowIndex) + } else { + this.dirtySlots = this.dirtySlots.filter(item => item !== rowIndex) + } + }, + confirmRowDeletion (slot, rowIndex) { + this.$q.dialog({ + component: CscRemoveDialog, + parent: this, + title: this.$t('Delete slot?'), + message: this.$t('You are about to delete slot {slot}', { slot: slot }) + }).onOk(() => { + this.deleteSlot(rowIndex) + }) + }, + async deleteSlot (rowIndex) { + this.slots = this.slots.filter((slot, index) => index !== rowIndex) + this.saveSlots() + }, + async saveSlots () { + for (const newSlot of this.unsavedSlots) { + if (!newSlot.destination) { + showGlobalError(this.$t('Please fill or remove the empty slots')) + return + } + } + await this.updateSubscriberSlots({ + subscriberId: this.subscriberId, + slots: [...this.unsavedSlots, ...this.slots] + }) + this.resetAllNewSlots(this.subscriberId) + showToast(this.$t('Slots saved successfully')) + }, + hasExistingSlotValue (index, fieldName) { + return this.slots[index] ? this.slots[index][fieldName] : false + }, + getDestinationValidation () { + return { + name: 'required', + validator: required, + error: this.$t('Destination must not be empty') + } + } + // This can be applied as format function in case we want + // the prevent the user to edit prefix/suffix (sip: and @domain) + // of the destinations + // + // extractDestination (value) { + // return value.match(/(?<=sip:)(.*?)(?=@)/)[0] + // } + } +} +</script> diff --git a/src/components/pages/PbxConfiguration/CscPbxDeviceConfig.vue b/src/components/pages/PbxConfiguration/CscPbxDeviceConfig.vue index 28bdb2b5..a3ac2359 100644 --- a/src/components/pages/PbxConfiguration/CscPbxDeviceConfig.vue +++ b/src/components/pages/PbxConfiguration/CscPbxDeviceConfig.vue @@ -51,7 +51,7 @@ </div> </div> </div> - <csc-pbx-attendant-selection + <csc-pbx-auto-attendant-selection :value="selectedKeySubscriber" :options="subscriberOptions" @input="keySubscriberChanged" @@ -100,18 +100,18 @@ <script> import _ from 'lodash' +import CscPbxAutoAttendantSelection from './CscPbxAutoAttendantSelection' import { Platform } from 'quasar' import { BoundingBox2D } from 'src/helpers/graphics' -import CscPbxAttendantSelection from './CscPbxAttendantSelection' export default { name: 'CscPbxDeviceConfig', components: { - CscPbxAttendantSelection + CscPbxAutoAttendantSelection }, props: { device: { diff --git a/src/components/pages/PbxConfiguration/CscPbxDeviceFilters.vue b/src/components/pages/PbxConfiguration/CscPbxDeviceFilters.vue index 40fc8341..236a820a 100644 --- a/src/components/pages/PbxConfiguration/CscPbxDeviceFilters.vue +++ b/src/components/pages/PbxConfiguration/CscPbxDeviceFilters.vue @@ -49,7 +49,7 @@ @opened="$emit('model-select-opened')" @input="triggerFilter" /> - <csc-pbx-attendant-selection + <csc-pbx-auto-attendant-selection v-if="filterType === 'display_name'" use-input dense @@ -88,14 +88,14 @@ <script> import _ from 'lodash' import CscPbxModelSelect from '../PbxConfiguration/CscPbxModelSelect' -import CscPbxAttendantSelection from '../PbxConfiguration/CscPbxAttendantSelection' +import CscPbxAutoAttendantSelection from './CscPbxAutoAttendantSelection' import { mapActions, mapState } from 'vuex' export default { name: 'CscPbxDeviceFilters', components: { CscPbxModelSelect, - CscPbxAttendantSelection + CscPbxAutoAttendantSelection }, props: { loading: { diff --git a/src/i18n/de.json b/src/i18n/de.json index 7f83dd34..c9f1f7ad 100644 --- a/src/i18n/de.json +++ b/src/i18n/de.json @@ -12,6 +12,7 @@ "404 Not Found": "404 Not Found", "A default subscriber sound set to be set before being able to select, in the Sound Sets page.": "Ein Standard-Soundset muss festgelegt werden, bevor ein Soundset ausgewählt werden kann.", "ACL": "", + "ADD SLOT": "", "Abort": "Abbrechen", "Accepted email address to allow mail2fax transmission.": "", "Active": "", @@ -23,6 +24,7 @@ "Add Group": "Gruppe hinzufügen", "Add Number": "Weiterleitung zu Rufnummern hinzuƒügen", "Add Seat": "Seat hinzufügen", + "Add Slots": "", "Add Sound Set": "Soundset hinzufügen", "Add Speed Dial": "Kurzwahl hinzufügen", "Add Time": "Zeitraum hinzufügen", @@ -34,6 +36,7 @@ "Add forwarding": "", "Add new": "Neue hinzufügen", "Add number": "Nummer hinzufügen", + "Add slot": "", "Add source": "Anrufer hinzufügen", "Add time": "", "Add time range": "", @@ -94,6 +97,7 @@ "Audio + Video": "Audio und Video", "Audio Call": "Audioanruf", "Audio Only": "Nur Audio", + "Auto-attendant": "", "Blacklist": "Blacklist", "Block Incoming": "Eingehende Anrufe blockieren", "Block Incoming/Outgoing": "Ein-/Ausgehende Anrufe blockieren", @@ -182,6 +186,7 @@ "Delete forwarding": "", "Delete from {groupName} forwarding": "", "Delete recording": "", + "Delete slot?": "", "Delete sourceset": "Anruferliste löschen", "Delete voicemail after email notification is delivered": "Voicemail löschen, wenn die E-Mail-Benachrichtigung gesendet wurde", "Delete {groupName} forwarding group": "", @@ -196,6 +201,7 @@ "Destination Email": "", "Destination Number": "Zielrufnummer", "Destination email to send the secret key renew notification to.": "", + "Destination must not be empty": "", "Destinations": "", "Devices": "Geräte", "Disable": "Deaktivieren", @@ -341,6 +347,7 @@ "No Voicemails found": "Keine Voicemails gefunden", "No call goes to primary number": "", "No call queues created yet": "Es wurden noch keine Anrufwarteschlangen erstellt.", + "No data found": "", "No data to save. Please provide at least one time range.": "", "No destinations created yet": "", "No devices created yet": "Noch keine Geräte angelegt", @@ -390,6 +397,9 @@ "Play sound in loop": "Sound in Schleife abspielen", "Playing in loop": "In Schleife abspielen", "Please add a destination to the group before adding conditions": "", + "Please fill all the empty destinations": "", + "Please fill all the fields": "", + "Please fill or remove the empty slots": "", "Please select a valid timerange": "", "Please select an option": "", "Primary Number": "Primär-Rufnummer", @@ -500,6 +510,12 @@ "Show filters": "Filter anzeigen", "Sign In": "Log-in", "Slot": "Kurzwahl", + "Slot added successfully": "", + "Slot edited successfully": "", + "Slot successfully deleted": "", + "Slot {number}": "", + "Slots saved successfully": "", + "Slots successfully added": "", "Something went wrong. Please retry later": "", "Sound Set": "Soundset", "Sound Sets": "Sound Sets", @@ -516,6 +532,7 @@ "Start time should be less than End time": "", "Station name": "Gerätename", "Su": "", + "Subscriber": "", "Subscriber Sign In": "Subscriber Log-in", "Sunday": "Sonntag", "Super": "Hoch", @@ -604,6 +621,7 @@ "You are about to delete {name} sourceset": "", "You are about to delete {name} timeset": "", "You are about to delete recording #{id}": "", + "You are about to delete slot {slot}": "", "You are about to delete time range \"{from} - {to}\"": "", "You are about to delete {destination} from {groupName} call forwarding": "", "You are about to delete {groupName} call forwarding group": "", @@ -697,4 +715,4 @@ "{field} must have at most {maxLength} letters": "{field} darf höchstens {maxLength} Buchstaben beinhalten", "{mode} of sources": "{mode} of sources", "{mode} own phone": "Eigene Rufnummer {mode}" -} \ No newline at end of file +} diff --git a/src/i18n/en.json b/src/i18n/en.json index 71e3d260..92323c8d 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -12,6 +12,7 @@ "404 Not Found": "404 Not Found", "A default subscriber sound set to be set before being able to select, in the Sound Sets page.": "A default subscriber sound set to be set before being able to select, in the Sound Sets page.", "ACL": "ACL", + "ADD SLOT": "ADD SLOT", "Abort": "Abort", "Accepted email address to allow mail2fax transmission.": "Accepted email address to allow mail2fax transmission.", "Active": "Active", @@ -23,6 +24,7 @@ "Add Group": "Add Group", "Add Number": "Add Number", "Add Seat": "Add Seat", + "Add Slots": "Add Slots", "Add Sound Set": "Add Sound Set", "Add Speed Dial": "Add Speed Dial", "Add Time": "Add Time", @@ -34,6 +36,7 @@ "Add forwarding": "Add forwarding", "Add new": "Add new", "Add number": "Add number", + "Add slot": "Add slot", "Add source": "Add source", "Add time": "Add time", "Add time range": "Add time range", @@ -94,6 +97,7 @@ "Audio + Video": "Audio + Video", "Audio Call": "Audio Call", "Audio Only": "Audio Only", + "Auto-attendant": "Auto-attendant", "Blacklist": "Blacklist", "Block Incoming": "Block Incoming", "Block Incoming/Outgoing": "Block Incoming/Outgoing", @@ -182,6 +186,7 @@ "Delete forwarding": "Delete forwarding", "Delete from {groupName} forwarding": "Delete from {groupName} forwarding", "Delete recording": "Delete recording", + "Delete slot?": "Delete slot?", "Delete sourceset": "Delete sourceset", "Delete voicemail after email notification is delivered": "Delete voicemail after email notification is delivered", "Delete {groupName} forwarding group": "Delete {groupName} forwarding group", @@ -196,6 +201,7 @@ "Destination Email": "Destination Email", "Destination Number": "Destination Number", "Destination email to send the secret key renew notification to.": "Destination email to send the secret key renew notification to.", + "Destination must not be empty": "Destination must not be empty", "Destinations": "Destinations", "Devices": "Devices", "Disable": "Disable", @@ -341,6 +347,7 @@ "No Voicemails found": "No Voicemails found", "No call goes to primary number": "No call goes to primary number", "No call queues created yet": "No call queues created yet", + "No data found": "No data found", "No data to save. Please provide at least one time range.": "No data to save. Please provide at least one time range.", "No destinations created yet": "No destinations created yet", "No devices created yet": "No devices created yet", @@ -390,6 +397,9 @@ "Play sound in loop": "Play sound in loop", "Playing in loop": "Playing in loop", "Please add a destination to the group before adding conditions": "Please add a destination to the group before adding conditions", + "Please fill all the empty destinations": "Please fill all the empty destinations", + "Please fill all the fields": "Please fill all the fields", + "Please fill or remove the empty slots": "Please fill or remove the empty slots", "Please select a valid timerange": "Please select a valid timerange", "Please select an option": "Please select an option", "Primary Number": "Primary Number", @@ -500,6 +510,12 @@ "Show filters": "Show filters", "Sign In": "Sign In", "Slot": "Slot", + "Slot added successfully": "Slot added successfully", + "Slot edited successfully": "Slot edited successfully", + "Slot successfully deleted": "Slot successfully deleted", + "Slot {number}": "Slot {number}", + "Slots saved successfully": "Slots saved successfully", + "Slots successfully added": "Slots successfully added", "Something went wrong. Please retry later": "Something went wrong. Please retry later", "Sound Set": "Sound Set", "Sound Sets": "Sound Sets", @@ -516,6 +532,7 @@ "Start time should be less than End time": "Start time should be less than End time", "Station name": "Station name", "Su": "Su", + "Subscriber": "Subscriber", "Subscriber Sign In": "Subscriber Sign In", "Sunday": "Sunday", "Super": "Super", @@ -604,6 +621,7 @@ "You are about to delete {name} sourceset": "You are about to delete {name} sourceset", "You are about to delete {name} timeset": "You are about to delete {name} timeset", "You are about to delete recording #{id}": "You are about to delete recording #{id}", + "You are about to delete slot {slot}": "You are about to delete slot {slot}", "You are about to delete time range \"{from} - {to}\"": "You are about to delete time range \"{from} - {to}\"", "You are about to delete {destination} from {groupName} call forwarding": "You are about to delete {destination} from {groupName} call forwarding", "You are about to delete {groupName} call forwarding group": "You are about to delete {groupName} call forwarding group", @@ -697,4 +715,4 @@ "{field} must have at most {maxLength} letters": "{field} must have at most {maxLength} letters", "{mode} of sources": "{mode} of sources", "{mode} own phone": "{mode} own phone" -} \ No newline at end of file +} diff --git a/src/i18n/es.json b/src/i18n/es.json index d20585a9..b65bdc52 100644 --- a/src/i18n/es.json +++ b/src/i18n/es.json @@ -12,6 +12,7 @@ "404 Not Found": "404 Not Encontrado", "A default subscriber sound set to be set before being able to select, in the Sound Sets page.": "Un conjunto de sonido de suscriptor predeterminado que se establecerá antes de poder seleccionar, en la página de Conjuntos de Sonido.", "ACL": "", + "ADD SLOT": "", "Abort": "Abortar", "Accepted email address to allow mail2fax transmission.": "", "Active": "", @@ -23,6 +24,7 @@ "Add Group": "Agregar grupo", "Add Number": "Agregar Número", "Add Seat": "Agregar asiento", + "Add Slots": "", "Add Sound Set": "Añadir Conjunto de Sonido", "Add Speed Dial": "Agregar marcado rápido", "Add Time": "Agregar tiempo", @@ -34,6 +36,7 @@ "Add forwarding": "", "Add new": "Añadir nuevo", "Add number": "Agregar número", + "Add slot": "", "Add source": "Agregar fuente", "Add time": "", "Add time range": "", @@ -94,6 +97,7 @@ "Audio + Video": "Audio + Video", "Audio Call": "Llamada de audio", "Audio Only": "Solo Audio", + "Auto-attendant": "", "Blacklist": "Lista Negra", "Block Incoming": "Bloquear Entrantes", "Block Incoming/Outgoing": "Bloquear Entrantes/Salientes", @@ -182,6 +186,7 @@ "Delete forwarding": "", "Delete from {groupName} forwarding": "", "Delete recording": "", + "Delete slot?": "", "Delete sourceset": "Eliminar el conjunto de fuentes", "Delete voicemail after email notification is delivered": "Eliminar el correo de voz después de enviar la notificación por correo electrónico", "Delete {groupName} forwarding group": "", @@ -196,6 +201,7 @@ "Destination Email": "", "Destination Number": "Número de destino", "Destination email to send the secret key renew notification to.": "", + "Destination must not be empty": "", "Destinations": "", "Devices": "Dispositivos", "Disable": "Desactivar", @@ -341,6 +347,7 @@ "No Voicemails found": "No se encontraron mensajes de voz", "No call goes to primary number": "", "No call queues created yet": "Aún no se han creado colas de llamadas", + "No data found": "", "No data to save. Please provide at least one time range.": "", "No destinations created yet": "", "No devices created yet": "Aún no se han creado dispositivos", @@ -390,6 +397,9 @@ "Play sound in loop": "Reproducir sonido en bucle", "Playing in loop": "Reproducir en bucle", "Please add a destination to the group before adding conditions": "", + "Please fill all the empty destinations": "", + "Please fill all the fields": "", + "Please fill or remove the empty slots": "", "Please select a valid timerange": "", "Please select an option": "", "Primary Number": "Número primario", @@ -500,6 +510,12 @@ "Show filters": "Mostrar filtros", "Sign In": "Iniciar sesión", "Slot": "Ranura", + "Slot added successfully": "", + "Slot edited successfully": "", + "Slot successfully deleted": "", + "Slot {number}": "", + "Slots saved successfully": "", + "Slots successfully added": "", "Something went wrong. Please retry later": "", "Sound Set": "Conjunto de sonido", "Sound Sets": "Conjuntos de Sonido", @@ -516,6 +532,7 @@ "Start time should be less than End time": "", "Station name": "Nombre de la estación", "Su": "", + "Subscriber": "", "Subscriber Sign In": "Iniciar sesión de suscriptor", "Sunday": "Domingo", "Super": "Super", @@ -604,6 +621,7 @@ "You are about to delete {name} sourceset": "", "You are about to delete {name} timeset": "", "You are about to delete recording #{id}": "", + "You are about to delete slot {slot}": "", "You are about to delete time range \"{from} - {to}\"": "", "You are about to delete {destination} from {groupName} call forwarding": "", "You are about to delete {groupName} call forwarding group": "", @@ -697,4 +715,4 @@ "{field} must have at most {maxLength} letters": "{field} debe tener como máximo {maxLength} letras", "{mode} of sources": "{mode} de fuentes", "{mode} own phone": "{mode} teléfono propio" -} \ No newline at end of file +} diff --git a/src/i18n/fr.json b/src/i18n/fr.json index c1d8c9bb..af6ec071 100644 --- a/src/i18n/fr.json +++ b/src/i18n/fr.json @@ -12,6 +12,7 @@ "404 Not Found": "Erreur 404", "A default subscriber sound set to be set before being able to select, in the Sound Sets page.": "", "ACL": "", + "ADD SLOT": "", "Abort": "Abandonner", "Accepted email address to allow mail2fax transmission.": "", "Active": "", @@ -23,6 +24,7 @@ "Add Group": "Ajouter un groupe", "Add Number": "Ajouter un numéro", "Add Seat": "Ajouter un siège", + "Add Slots": "", "Add Sound Set": "", "Add Speed Dial": "Ajouter un raccourci", "Add Time": "Ajouter une heure", @@ -34,6 +36,7 @@ "Add forwarding": "", "Add new": "En ajouter un nouveau", "Add number": "Ajouter un numéro", + "Add slot": "", "Add source": "Ajouter une source", "Add time": "", "Add time range": "", @@ -94,6 +97,7 @@ "Audio + Video": "Audio et vidéo", "Audio Call": "Appel audio", "Audio Only": "Audio seulement", + "Auto-attendant": "", "Blacklist": "Liste noire", "Block Incoming": "", "Block Incoming/Outgoing": "", @@ -182,6 +186,7 @@ "Delete forwarding": "", "Delete from {groupName} forwarding": "", "Delete recording": "", + "Delete slot?": "", "Delete sourceset": "Supprimer la liste de sources", "Delete voicemail after email notification is delivered": "Supprimer le message vocal une fois la notification e-mail délivrée", "Delete {groupName} forwarding group": "", @@ -196,6 +201,7 @@ "Destination Email": "", "Destination Number": "Numéro de destination", "Destination email to send the secret key renew notification to.": "", + "Destination must not be empty": "", "Destinations": "", "Devices": "Postes", "Disable": "Désactiver", @@ -341,6 +347,7 @@ "No Voicemails found": "Message vocaux introuvables", "No call goes to primary number": "", "No call queues created yet": "Aucune file d’attente de créée", + "No data found": "", "No data to save. Please provide at least one time range.": "", "No destinations created yet": "", "No devices created yet": "Aucun poste créé", @@ -390,6 +397,9 @@ "Play sound in loop": "", "Playing in loop": "", "Please add a destination to the group before adding conditions": "", + "Please fill all the empty destinations": "", + "Please fill all the fields": "", + "Please fill or remove the empty slots": "", "Please select a valid timerange": "", "Please select an option": "", "Primary Number": "Numéro principal", @@ -500,6 +510,12 @@ "Show filters": "Afficher les filtre", "Sign In": "Authentification", "Slot": "Emplacement", + "Slot added successfully": "", + "Slot edited successfully": "", + "Slot successfully deleted": "", + "Slot {number}": "", + "Slots saved successfully": "", + "Slots successfully added": "", "Something went wrong. Please retry later": "", "Sound Set": "", "Sound Sets": "", @@ -516,6 +532,7 @@ "Start time should be less than End time": "", "Station name": "Nom du poste", "Su": "", + "Subscriber": "", "Subscriber Sign In": "Authentification de l’abonné", "Sunday": "Dimanche", "Super": "Supérieur", @@ -604,6 +621,7 @@ "You are about to delete {name} sourceset": "", "You are about to delete {name} timeset": "", "You are about to delete recording #{id}": "", + "You are about to delete slot {slot}": "", "You are about to delete time range \"{from} - {to}\"": "", "You are about to delete {destination} from {groupName} call forwarding": "", "You are about to delete {groupName} call forwarding group": "", @@ -697,4 +715,4 @@ "{field} must have at most {maxLength} letters": "{field} doit avoir au maximum {maxLength} caractères", "{mode} of sources": "Sources {mode}", "{mode} own phone": "{mode} téléphone personnel" -} \ No newline at end of file +} diff --git a/src/i18n/it.json b/src/i18n/it.json index f67efb15..647d098a 100644 --- a/src/i18n/it.json +++ b/src/i18n/it.json @@ -12,6 +12,7 @@ "404 Not Found": "404 Not Found", "A default subscriber sound set to be set before being able to select, in the Sound Sets page.": "Un set di messaggi predefinito per gli utenti, disponibile prima che se ne possa selezionare uno nella pagina Set di Messaggi.", "ACL": "", + "ADD SLOT": "", "Abort": "Abbandona", "Accepted email address to allow mail2fax transmission.": "", "Active": "", @@ -23,6 +24,7 @@ "Add Group": "Aggiungi gruppo", "Add Number": "Aggiungi Numero", "Add Seat": "Aggiungi postazione", + "Add Slots": "", "Add Sound Set": "Aggiungi set messaggi", "Add Speed Dial": "Aggiungi Selezione Rapida", "Add Time": "Aggiungi orario", @@ -34,6 +36,7 @@ "Add forwarding": "", "Add new": "Aggiungi nuovo", "Add number": "Aggiungi numero", + "Add slot": "", "Add source": "Aggiungi ", "Add time": "", "Add time range": "", @@ -94,6 +97,7 @@ "Audio + Video": "Audio + Video", "Audio Call": "Chiamata Vocale", "Audio Only": "Solo Audio", + "Auto-attendant": "", "Blacklist": "Lista Nera", "Block Incoming": "Blocca Entranti", "Block Incoming/Outgoing": "Blocca Entranti/Uscenti", @@ -182,6 +186,7 @@ "Delete forwarding": "", "Delete from {groupName} forwarding": "", "Delete recording": "", + "Delete slot?": "", "Delete sourceset": "Cancella set pattern sorgente", "Delete voicemail after email notification is delivered": "Cancella il messaggio vocale dopo che l'email di notifica è stata inoltrata", "Delete {groupName} forwarding group": "", @@ -196,6 +201,7 @@ "Destination Email": "", "Destination Number": "Numero di Destinazione", "Destination email to send the secret key renew notification to.": "", + "Destination must not be empty": "", "Destinations": "", "Devices": "Dispositivi", "Disable": "Disabilita", @@ -341,6 +347,7 @@ "No Voicemails found": "Non è stato trovato nessun Messaggio Vocale", "No call goes to primary number": "", "No call queues created yet": "Nessuna coda creata", + "No data found": "", "No data to save. Please provide at least one time range.": "", "No destinations created yet": "", "No devices created yet": "Nessun dispositivo creato", @@ -390,6 +397,9 @@ "Play sound in loop": "Ripoduci il messaggio a ciclo continuo", "Playing in loop": "Ripoduci a ciclo continuo", "Please add a destination to the group before adding conditions": "", + "Please fill all the empty destinations": "", + "Please fill all the fields": "", + "Please fill or remove the empty slots": "", "Please select a valid timerange": "", "Please select an option": "", "Primary Number": "Numero Principale", @@ -500,6 +510,12 @@ "Show filters": "Mostra filtri", "Sign In": "Accedi", "Slot": "Posizione", + "Slot added successfully": "", + "Slot edited successfully": "", + "Slot successfully deleted": "", + "Slot {number}": "", + "Slots saved successfully": "", + "Slots successfully added": "", "Something went wrong. Please retry later": "", "Sound Set": "Set Messaggi", "Sound Sets": "Annunci", @@ -516,6 +532,7 @@ "Start time should be less than End time": "", "Station name": "Nome stazione", "Su": "", + "Subscriber": "", "Subscriber Sign In": "Accedi come utente", "Sunday": "Domenica", "Super": "Super", @@ -604,6 +621,7 @@ "You are about to delete {name} sourceset": "", "You are about to delete {name} timeset": "", "You are about to delete recording #{id}": "", + "You are about to delete slot {slot}": "", "You are about to delete time range \"{from} - {to}\"": "", "You are about to delete {destination} from {groupName} call forwarding": "", "You are about to delete {groupName} call forwarding group": "", @@ -697,4 +715,4 @@ "{field} must have at most {maxLength} letters": "Il campo {field} può contenere al massimo {maxLength} lettere", "{mode} of sources": "{mode} dei pattern sorgente", "{mode} own phone": "{mode} proprio telefono" -} \ No newline at end of file +} diff --git a/src/pages/CscPagePbxAutoAttendant.vue b/src/pages/CscPagePbxAutoAttendant.vue new file mode 100644 index 00000000..3c20af28 --- /dev/null +++ b/src/pages/CscPagePbxAutoAttendant.vue @@ -0,0 +1,212 @@ +<template> + <csc-page + class="q-pa-lg" + > + <div class="q-pa-md"> + <q-table + :data="data" + :columns="columns" + :loading="$wait.is('csc-pbx-auto-attendant')" + row-key="name" + flat + :pagination.sync="pagination" + @request="fetchWithPagination" + > + <template v-slot:header="props"> + <q-tr :props="props"> + <q-th auto-width /> + <q-th + v-for="col in props.cols" + :key="col.name" + :props="props" + class="table-header" + > + {{ col.label }} + </q-th> + <q-th auto-width /> + <q-th auto-width /> + </q-tr> + </template> + <template + v-slot:body="props" + > + <q-tr> + <q-td auto-width /> + <q-td + v-for="col in props.cols" + :key="col.name" + > + {{ col.value }} + </q-td> + <q-td auto-width> + <q-btn-dropdown + size="md" + color="primary" + :label="$t('Add slot')" + :disabled="getAvailableSlots(props.row.slots, props.row.subscriber_id).length === 0" + icon="add" + dropdown-icon=" " + flat + > + <q-list + v-for="availableSlot in getAvailableSlots(props.row.slots, props.row.subscriber_id)" + :key="availableSlot" + > + <csc-popup-menu-item + :label="availableSlot" + @click="addSlot(props.row.subscriber_id, availableSlot)" + /> + </q-list> + </q-btn-dropdown> + <q-btn + size="md" + color="primary" + round + flat + :icon="isRowExpanded(props.row.subscriber_id) ? 'expand_less' : 'expand_more'" + @click="updateCollapseArray(props.row.subscriber_id)" + /> + </q-td> + <q-td auto-width /> + </q-tr> + <q-tr + v-show="isRowExpanded(props.row.subscriber_id)" + no-hover + > + <q-td + colspan="100%" + class="table-cell" + > + <csc-pbx-auto-attendant-slots-table + :data="sortedSlots(props.row.slots)" + :subscriber-id="props.row.subscriber_id" + /> + </q-td> + </q-tr> + </template> + </q-table> + </div> + </csc-page> +</template> + +<script> +import _ from 'lodash' +import { mapGetters } from 'vuex' +import { mapWaitingActions } from 'vue-wait' +import { displayName } from 'src/filters/subscriber' +import CscPage from 'components/CscPage' +import CscPbxAutoAttendantSlotsTable from 'components/pages/PbxConfiguration/CscPbxAutoAttendantSlotsTable' +import CscPopupMenuItem from 'components/CscPopupMenuItem' +export default { + name: 'CscPagePbxAutoAttendant', + components: { + CscPage, + CscPopupMenuItem, + CscPbxAutoAttendantSlotsTable + }, + data () { + return { + data: [], + rowStatus: [], + columns: [ + { + name: 'Subscriber Id', + required: true, + label: this.$t('Id'), + align: 'left', + field: row => row.subscriber_id, + format: val => `${val}` + }, + { + name: 'Name', + required: true, + align: 'left', + label: this.$t('Name'), + field: row => displayName(row.subscriber) + } + ], + pagination: { + page: 1, + rowsPerPage: 5, + rowsNumber: 0 + } + } + }, + computed: { + ...mapGetters('pbxAutoAttendants', [ + 'slots', + 'slotsNumbers', + 'newSlots' + ]) + }, + watch: { + slots () { + this.data = this.slots + this.rowStatus = this.slots.map(slot => { + return { + subscriber_id: slot.subscriber_id, + expanded: false + } + }) + } + }, + mounted () { + this.fetchWithPagination({ + pagination: this.pagination + }) + }, + methods: { + ...mapWaitingActions('pbxAutoAttendants', { + fetchAutoAttendants: 'csc-pbx-auto-attendant', + createNewSlot: 'csc-pbx-auto-attendant' + }), + async fetchWithPagination (props) { + const { page, rowsPerPage } = props.pagination + const count = await this.fetchAutoAttendants({ + page: page, + rows: rowsPerPage + }) + this.pagination = { ...props.pagination } + this.pagination.rowsNumber = count + }, + async addSlot (subscriberId, slot) { + this.createNewSlot({ + subscriberId: subscriberId, + slot: slot + }) + this.expandRow(subscriberId) + }, + isRowExpanded (subscriberId) { + const rowStatus = this.rowStatus.filter(row => row.subscriber_id === subscriberId)[0] || null + return rowStatus && rowStatus.expanded + }, + updateCollapseArray (subscriberId) { + const rowStatus = this.rowStatus.filter(row => row.subscriber_id === subscriberId)[0] + rowStatus.expanded = !rowStatus.expanded + }, + getAvailableSlots (subscriberSlots, subscriberId) { + const subscriberSavedSlots = subscriberSlots.map(item => item.slot) + const subscriberNewSlots = this.newSlots.filter(item => item.subscriber_id === subscriberId) + subscriberSlots = subscriberNewSlots.length > 0 + ? [...subscriberSavedSlots, ...subscriberNewSlots[0].slots.map(item => item.slot)] + : subscriberSavedSlots + const availableSlots = this.slotsNumbers.filter(slot => !subscriberSlots.includes(slot)) + return availableSlots + }, + expandRow (subscriberId) { + const status = this.rowStatus.filter(row => row.subscriber_id === subscriberId)[0] + status.expanded = true + }, + sortedSlots (slots) { + const sorted = _.cloneDeep(slots) + return sorted.sort((a, b) => a.slot > b.slot ? 1 : -1) + } + } +} +</script> +<style lang="stylus" rel="stylesheet/stylus" scoped> +.table-header + font-size 15px +.table-cell + padding 0 +</style> diff --git a/src/router/routes.js b/src/router/routes.js index a1f77a6d..2e70deb0 100644 --- a/src/router/routes.js +++ b/src/router/routes.js @@ -22,6 +22,7 @@ import CscPagePbxDevices from 'src/pages/CscPagePbxDevices' import CscPagePbxCallQueues from 'src/pages/CscPagePbxCallQueues' import CscPagePbxSoundSets from 'src/pages/CscPagePbxSoundSets' import CscPagePbxMsConfigs from 'src/pages/CscPagePbxMsConfigs' +import CscPagePbxAutoAttendant from 'src/pages/CscPagePbxAutoAttendant' import CscPagePbxSettings from 'src/pages/CscPagePbxSettings' import CscPageVoicebox from 'src/pages/CscPageVoicebox' import CscPageFaxSettings from 'src/pages/CscPageFaxSettings' @@ -190,6 +191,14 @@ export default function routes (app) { subtitle: i18n.t('Manager Secretary') } }, + { + path: 'pbx-configuration/auto-attendant', + component: CscPagePbxAutoAttendant, + meta: { + title: i18n.t('PBX Configuration'), + subtitle: i18n.t('Auto-attendant') + } + }, { path: 'voicebox', component: CscPageVoicebox, diff --git a/src/store/index.js b/src/store/index.js index fe2043ac..f1bea2af 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -21,6 +21,7 @@ import PbxDevicesModule from './pbx-devices' import PbxCallQueuesModule from './pbx-callqueues' import PbxSoundSetsModule from './pbx-soundsets' import PbxMsConfigsModule from './pbx-ms-configs' +import PbxAutoAttendants from './pbx-auto-attendants' import ReminderModule from './reminder' import SpeedDialModule from './speed-dial' @@ -76,7 +77,8 @@ export default function (/* { ssrContext } */) { pbxCallQueues: PbxCallQueuesModule, pbxSoundSets: PbxSoundSetsModule, pbxMsConfigs: PbxMsConfigsModule, - callForwarding: CallForwardingModule + callForwarding: CallForwardingModule, + pbxAutoAttendants: PbxAutoAttendants }, state: { diff --git a/src/store/pbx-auto-attendants.js b/src/store/pbx-auto-attendants.js new file mode 100644 index 00000000..09b9b45a --- /dev/null +++ b/src/store/pbx-auto-attendants.js @@ -0,0 +1,115 @@ +import { getAutoAttendants, editSubscriberSlots } from '../api/pbx-auto-attendants' +import { getSubscribers } from '../api/subscriber' +import { displayName } from 'src/filters/subscriber' +export default { + namespaced: true, + state: { + slots: [], + newSlots: [], + subscribers: [], + slotsNumbers: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] + }, + getters: { + slots (state) { + return state.slots + }, + slotsNumbers (state) { + return state.slotsNumbers + }, + newSlots (state) { + return state.newSlots + }, + subscribers (state) { + return state.subscribers.map(subscriber => { + return { + label: displayName(subscriber), + value: subscriber.id + } + }) + } + }, + mutations: { + slots (state, data) { + state.slots = data + }, + newSlots (state, data) { + for (const slot of data) { + state.newSlots.push({ + subscriber_id: slot.subscriber_id, + slots: [] + }) + } + }, + subscriberSlots (state, data) { + const subscriberSlots = state.slots.filter(slot => slot.subscriber_id === data.subscriberId)[0] + subscriberSlots.slots = data.slots + }, + createNewSlot (state, data) { + const subscriberSlots = state.newSlots.filter(slot => slot.subscriber_id === data.subscriberId)[0] + subscriberSlots.slots.push({ + slot: data.slot, + destination: null + }) + }, + editNewSlot (state, data) { + const subscriberSlots = state.newSlots.filter(slot => slot.subscriber_id === data.subscriberId)[0] + subscriberSlots.slots[data.index].destination = data.destination + }, + deleteNewSlot (state, data) { + const subscriberSlots = state.newSlots.filter(slot => slot.subscriber_id === data.subscriberId)[0] + subscriberSlots.slots.splice(data.index, 1) + }, + resetNewSlots (state, subscriberId) { + const subscriberSlots = state.newSlots.filter(slot => slot.subscriber_id === subscriberId)[0] + subscriberSlots.slots.splice(0, subscriberSlots.slots.length) + }, + subscribers (state, subscribers) { + state.subscribers = subscribers + } + }, + actions: { + async fetchAutoAttendants (context, options) { + const autoAttendants = await getAutoAttendants(options) + context.commit('slots', autoAttendants._embedded['ngcp:autoattendants']) + context.commit('newSlots', autoAttendants._embedded['ngcp:autoattendants']) + return autoAttendants.total_count + }, + async fetchSubscribers (context, subscriberName) { + const subscribers = await getSubscribers({ + params: { + display_name: subscriberName || '*' + } + }) + context.commit('subscribers', subscribers.items) + }, + async updateSubscriberSlots (context, options) { + const slots = await editSubscriberSlots(options) + context.commit('subscriberSlots', { + subscriberId: options.subscriberId, + slots: slots + }) + }, + createNewSlot (context, options) { + context.commit('createNewSlot', { + subscriberId: options.subscriberId, + slot: options.slot + }) + }, + editNewSlot (context, options) { + context.commit('editNewSlot', { + subscriberId: options.subscriberId, + index: options.index, + destination: options.destination + }) + }, + deleteNewSlot (context, options) { + context.commit('deleteNewSlot', { + subscriberId: options.subscriberId, + index: options.index + }) + }, + resetAllNewSlots (context, subscriberId) { + context.commit('resetNewSlots', subscriberId) + } + } +}