- 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