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)
+        }
+    }
+}