- 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: Iec29baecfa75f3c818b9deb945625a1bf977ca88mr9.5.2
parent
180cc90857
commit
01032cad60
@ -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
|
||||
}
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in new issue