MT#58289 add possibility to view the CDRs of the customer

Change-Id: I15e9d100233408fbfe82cbb916c7d2fb76a311f8
mr12.0
Hugo Zigha 2 years ago
parent 8e7c0ad2b2
commit 177f01d535

@ -17,15 +17,21 @@ export function getConversations (options) {
const from = _.get(options, 'from', '')
const to = _.get(options, 'to', '')
const direction = _.get(options, 'direction', '')
const subscriberId = _.get(options, 'subscriberId')
const noCount = _.get(options, 'no_count')
const params = {
subscriber_id: _.get(options, 'subscriberId'),
order_by: _.get(options, 'order_by', 'timestamp'),
order_by_direction: 'desc',
no_count: true,
tz: 'UTC',
page: _.get(options, 'page', 1),
rows: _.get(options, 'rows', LIST_DEFAULT_ROWS)
}
if (noCount !== null) {
params.no_count = noCount
}
if (subscriberId !== null) {
params.subscriber_id = subscriberId
}
if (type !== null) {
params.type = type
}

@ -149,6 +149,20 @@ export default {
label: this.$t('Fax Settings'),
visible: this.hasFaxCapability && this.hasSubscriberProfileAttribute(PROFILE_ATTRIBUTE_MAP.faxServer)
},
{
icon: 'fas fa-chart-line',
label: this.$t('PBX Statistics'),
visible: this.isPbxAdmin,
opened: this.isPbxConfiguration,
children: [
{
to: '/user/pbx-statistics/cdr',
icon: 'fas fa-table',
label: this.$t('Cdr'),
visible: true
}
]
},
{
icon: 'miscellaneous_services',
label: this.$t('PBX Configuration'),

@ -0,0 +1,313 @@
<template>
<div>
<div
class="row justify-center full-width q-gutter-x-sm"
>
<div
class="col-xs-12 col-md-3"
>
<q-select
v-model="filterTypeModel"
dense
:options="filterTypeOptions"
:label="$t('Filter by')"
data-cy="csc-cdr-filter"
:disable="loading"
/>
</div>
<div
v-if="filterType === 'timerange'"
class="row col-xs-12 col-md-6"
>
<q-input
v-model="dateStartFilter"
class="q-pr-sm col-6"
dense
:disable="loading || filterType === null"
:label="$t('From')"
data-cy="csc-cdr-filter-from"
>
<template #prepend>
<q-icon
name="event"
class="cursor-pointer"
@click="loadFormattedDateStart()"
>
<q-popup-proxy
transition-show="scale"
transition-hide="scale"
@hide="addFilter('startTime', dateStartFilter)"
>
<q-date
v-model="dateStartFilter"
mask="YYYY-MM-DD"
format24h
>
<div class="row items-center justify-end">
<q-btn
v-close-popup
:label="$t('Close')"
color="primary"
flat
/>
</div>
</q-date>
</q-popup-proxy>
</q-icon>
</template>
</q-input>
<q-input
v-model="dateEndFilter"
class="col-6"
dense
:disable="loading || filterType === null"
:label="$t('To')"
data-cy="csc-cdr-filter-to"
@input="triggerFilter"
>
<template #prepend>
<q-icon
name="event"
class="cursor-pointer"
@click="loadFormattedDateEnd()"
>
<q-popup-proxy
transition-show="scale"
transition-hide="scale"
@hide="addFilter('endTime', dateEndFilter)"
>
<q-date
v-model="dateEndFilter"
mask="YYYY-MM-DD"
>
<div class="row items-center justify-end">
<q-btn
v-close-popup
:label="$t('Close')"
color="primary"
flat
/>
</div>
</q-date>
</q-popup-proxy>
</q-icon>
</template>
</q-input>
</div>
<div
v-else-if="false"
class="row col-xs-12 col-md-3"
>
<q-select
v-model="filterDirection"
class="full-width"
dense
:options="filterDirectionOptions"
:label="$t('In/Out')"
data-cy="csc-cdr-filter"
:disable="loading"
@update:model-value="triggerDirectionFilter"
/>
</div>
<div
v-else-if="filterType === 'type'"
class="row col-xs-12 col-md-3"
>
<q-select
v-model="filterTypeField"
class="full-width"
dense
:options="filterTypeFieldOptions"
:label="$t('Type')"
data-cy="csc-cdr-filter"
:disable="loading"
@update:model-value="triggerTypeFieldFilter"
/>
</div>
</div>
<div
class="row justify-center full-width q-gutter-x-sm"
>
<div
class="col-xs-12 col-md-4"
>
<q-chip
v-for="({ filterInfo, id }) in filtersList"
:key="id"
:label="filterInfo"
:disable="false"
icon="filter_alt"
removable
dense
color="primary"
text-color="dark"
@remove="removeFilter(id)"
/>
</div>
</div>
</div>
</template>
<script>
import _ from 'lodash'
import { mapGetters } from 'vuex'
export default {
name: 'CscCdrFilters',
props: {
loading: {
type: Boolean,
default: false
}
},
emits: ['filter'],
data () {
return {
filterTypeModel: null,
filterDirection: null,
filterTypeField: null,
dateStartFilter: null,
dateEndFilter: null,
filters: []
}
},
computed: {
...mapGetters([
'getCurrentFormattedDateWithDash'
]),
filterType () {
return this.filterTypeModel && this.filterTypeModel.value
},
filterTypeOptions () {
return [
{
label: this.$t('Timerange'),
value: 'timerange'
},
/* {
label: this.$t('Direction'),
value: 'direction'
}, */
{
label: this.$t('Type'),
value: 'type'
}
]
},
filterDirectionOptions () {
return [
{
label: this.$t('In'),
value: 'in'
},
{
label: this.$t('Out'),
value: 'out'
}
]
},
filterTypeFieldOptions () {
return [
{
label: this.$t('Call'),
value: 'call'
},
{
label: this.$t('Voicemail'),
value: 'voicemail'
},
{
label: this.$t('Sms'),
value: 'sms'
},
{
label: this.$t('Fax'),
value: 'fax'
}
]
},
filtersList () {
return this.filters.map((filterItem) => {
const filterDisplayValue = filterItem.value
let filterName
switch (filterItem.name) {
case 'startTime':
filterName = this.$t('Start time')
break
case 'endTime' :
filterName = this.$t('End time')
break
default:
filterName = this.filterTypeOptions.find(option => option.value === filterItem.name).label
}
return {
id: filterItem.name,
filterInfo: filterName + ': ' + filterDisplayValue
}
})
}
},
watch: {
filterTypeModel () {
this.resetFilters()
}
},
methods: {
triggerDirectionFilter () {
this.addFilter(this.filterTypeModel?.value, this.filterDirection.value)
},
triggerTypeFieldFilter () {
this.addFilter(this.filterTypeModel?.value, this.filterTypeField.value)
},
removeFilter (name) {
this.filters = this.filters.filter(item => item.name !== name)
this.filter()
},
removeFilters () {
if (this.filters.length > 0) {
this.filters = []
this.filter()
}
},
addFilter (name, value) {
const valueTrimmed = _.trim(value)
if (valueTrimmed) {
this.resetFilters()
this.filters = this.filters.filter(item => item.name !== name)
const filter = {
name: name,
value: valueTrimmed
}
this.filters.push(filter)
this.filter()
}
},
filter () {
const params = {}
this.filters.forEach(filter => {
params[filter.name] = filter.value
})
this.$emit('filter', params)
},
resetFilters () {
this.typedFilter = null
this.dateStartFilter = null
this.dateEndFilter = null
this.filterDirection = null
this.filterTypeField = null
},
loadFormattedDateStart () {
const currentDate = this.getCurrentFormattedDateWithDash
if (!this.dateStartFilter) {
this.dateStartFilter = currentDate
}
},
loadFormattedDateEnd () {
const currentDate = this.getCurrentFormattedDateWithDash
if (!this.dateEndFilter) {
this.dateEndFilter = currentDate
}
}
}
}
</script>

@ -56,6 +56,7 @@
"Block outgoing": "Block outgoing",
"Busy Greeting": "Busy Greeting",
"Busy Lamp Field": "Busy Lamp Field",
"CDR": "CDR",
"CLI": "CLI",
"Call": "Call",
"Call Blocking": "Call Blocking",
@ -81,6 +82,7 @@
"Calls, Faxes, VoiceMails": "Calls, Faxes, VoiceMails",
"Cancel": "Cancel",
"Cancel Mode": "Cancel Mode",
"Cdr": "Cdr",
"Change Email": "Change Email",
"Change PIN": "Change PIN",
"Change Password": "Change Password",
@ -146,6 +148,7 @@
"Destination must not be empty": "Destination must not be empty",
"Destinations": "Destinations",
"Devices": "Devices",
"Direction": "Direction",
"Disable": "Disable",
"Display Name": "Display Name",
"Do not ring primary number": "Do not ring primary number",
@ -219,8 +222,10 @@
"If available": "If available",
"If busy": "If busy",
"If not available": "If not available",
"In": "In",
"In call with": "In call with",
"In call with {number}": "In call with {number}",
"In/Out": "In/Out",
"Incoming": "Incoming",
"Incoming call from": "Incoming call from",
"Incoming call from {number}": "Incoming call from {number}",
@ -308,8 +313,10 @@
"Only incoming calls from listed numbers are allowed": "Only incoming calls from listed numbers are allowed",
"Only once": "Only once",
"Only outgoing calls to listed numbers are allowed": "Only outgoing calls to listed numbers are allowed",
"Out": "Out",
"Outgoing": "Outgoing",
"PBX Configuration": "PBX Configuration",
"PBX Statistics": "PBX Statistics",
"PIN": "PIN",
"Page Header": "Page Header",
"Page not found": "Page not found",
@ -429,6 +436,7 @@
"Slot {number}": "Slot {number}",
"Slots saved successfully": "Slots saved successfully",
"Slots successfully added": "Slots successfully added",
"Sms": "Sms",
"Something went wrong. Please retry later": "Something went wrong. Please retry later",
"Sound Set": "Sound Set",
"Sound Sets": "Sound Sets",
@ -439,6 +447,7 @@
"Start time": "Start time",
"Start time should be less than End time": "Start time should be less than End time",
"Station name": "Station name",
"Status": "Status",
"Su": "Su",
"Subscriber": "Subscriber",
"Subscriber Phonebook": "Subscriber Phonebook",
@ -509,6 +518,7 @@
"Wrap Up Time": "Wrap Up Time",
"Wrap up time": "Wrap up time",
"Wrong username or password": "Wrong username or password",
"Xmpp": "Xmpp",
"Yesterday": "Yesterday",
"You are about to change your login password. After the password was changed successfully, you get automatically logged out to authenticate with the new password. ": "You are about to change your login password. After the password was changed successfully, you get automatically logged out to authenticate with the new password. ",
"You are about to delete recording #{id}": "You are about to delete recording #{id}",

@ -0,0 +1,221 @@
<!-- eslint-disable vue/no-v-model-argument -->
<template>
<csc-page-sticky
id="csc-page-pbx-statistics-cdr"
>
<template
#header
>
<q-btn
v-if="!showFilters"
icon="filter_alt"
color="primary"
flat
:label="$t('Filter')"
@click="openFilters"
/>
<q-btn
v-if="showFilters"
icon="clear"
color="negative"
flat
:label="$t('Close filters')"
@click="closeFilters"
/>
</template>
<template
#toolbar
>
<csc-cdr-filters
v-if="showFilters"
ref="filters"
:loading="$wait.is('loadConversations')"
class="q-mb-md q-pa-md"
@filter="filterEvent"
/>
</template>
<div>
<div class="q-pa-md">
<q-table
v-model:pagination="pagination"
class="no-shadow"
:columns="columns"
:rows="conversations"
:loading="$wait.is('loadConversations')"
row-key="id"
@request="fetchPaginatedConversations"
>
<template #loading>
<q-inner-loading
showing
color="primary"
>
<csc-spinner />
</q-inner-loading>
</template>
<template #top-left>
<q-btn
icon="refresh"
size="sm"
flat
@click="refresh"
>
{{ $t('Refresh') }}
</q-btn>
</template>
</q-table>
</div>
</div>
</csc-page-sticky>
</template>
<script>
import { mapState } from 'vuex'
import CscPageSticky from 'components/CscPageSticky'
import { mapWaitingActions } from 'vue-wait'
import CscSpinner from 'components/CscSpinner'
import { LIST_DEFAULT_ROWS } from 'src/api/common'
import CscCdrFilters from 'components/pages/PbxStatistics/CscCdrFilters'
import _ from 'lodash'
export default {
name: 'CscPagePbxStatisticsCdr',
components: {
CscSpinner,
CscPageSticky,
CscCdrFilters
},
data () {
return {
data: [],
pagination: {
sortBy: 'timestamp',
descending: true,
page: 1,
rowsPerPage: LIST_DEFAULT_ROWS,
rowsNumber: 0
},
showFilters: false,
filter: {
startTime: null,
endTime: null,
caller: null,
callee: null
}
}
},
computed: {
...mapState('conversations', [
'conversations'
]),
columns () {
return [
{
name: 'start_time',
required: true,
label: this.$t('Start time'),
align: 'left',
field: row => this.$filters.smartTime(row.start_time),
sortable: true
},
{
name: 'type',
required: true,
align: 'left',
label: this.$t('Type'),
field: row => _.capitalize(row.type),
sortable: true
},
{
name: 'caller',
required: true,
align: 'left',
label: this.$t('Caller'),
field: row => row.caller,
sortable: true
},
{
name: 'callee',
required: true,
align: 'left',
label: this.$t('Callee'),
field: row => row.callee,
sortable: true
},
{
name: 'direction',
required: true,
align: 'left',
label: this.$t('Direction'),
field: row => row.direction,
sortable: true
},
{
name: 'duration',
required: true,
align: 'left',
label: this.$t('Duration'),
field: row => row.duration,
sortable: true
},
{
name: 'status',
required: true,
align: 'left',
label: this.$t('Status'),
field: row => row.status,
sortable: true
}
]
}
},
async mounted () {
await this.refresh()
},
methods: {
...mapWaitingActions('conversations', {
loadConversations: 'loadConversations'
}),
async refresh () {
await this.fetchPaginatedConversations({
pagination: this.pagination
})
},
async fetchPaginatedConversations (props) {
const { page, rowsPerPage, sortBy, descending } = props.pagination
const { startTime, endTime, direction, type } = this.filter
const count = await this.loadConversations({
page,
rows: rowsPerPage,
order_by: sortBy,
order_by_direction: descending ? 'desc' : 'asc',
from: startTime,
to: endTime,
direction: direction,
type: type
})
this.pagination = { ...props.pagination }
this.pagination.rowsNumber = count
},
openFilters () {
this.showFilters = true
},
closeFilters () {
if (this.$refs.filters) {
this.$refs.filters.removeFilters()
}
this.showFilters = false
},
filterEvent (filter) {
this.$scrollTo(this.$parent.$el)
console.log(filter)
this.filter = filter
this.fetchPaginatedConversations({
pagination: this.pagination
})
}
}
}
</script>

@ -37,6 +37,7 @@ import CscPagePbxSettingsCallQueues from 'pages/CscPagePbxSettingsCallQueues'
import CscPagePbxSoundSetDetails from 'src/pages/CscPagePbxSoundSetDetails'
import CscPageSubscriberPhonebookDetails from 'src/pages/CscPageSubscriberPhonebookDetails'
import CscPageSubscriberPhonebookAdd from 'src/pages/CscPageSubscriberPhonebookAdd'
import CscPagePbxStatisticsCdr from 'src/pages/CscPagePbxStatisticsCdr'
import { i18n } from 'src/boot/i18n'
const getToken = (route) => {
@ -201,6 +202,18 @@ const routes = [
profileAttribute: PROFILE_ATTRIBUTE_MAP.speedDial
}
},
{
path: 'pbx-statistics/cdr',
component: CscPagePbxStatisticsCdr,
meta: {
get title () {
return i18n.global.tc('PBX Statistics')
},
get subtitle () {
return i18n.global.tc('CDR')
}
}
},
{
path: 'pbx-configuration/groups',
component: CscPagePbxGroups,

@ -26,6 +26,18 @@ const ReloadConfig = {
}
export default {
async loadConversations ({ commit, dispatch, state, rootGetters }, options) {
try {
const list = await getConversations({
...options
})
commit('setConversations', list.items)
return list.totalCount
} catch (err) {
commit('setConversations', [])
throw err
}
},
reloadItems (context, options) {
context.commit('reloadItemsRequesting')
const rows = context.state.currentPage * ROWS_PER_PAGE
@ -37,7 +49,8 @@ export default {
subscriberId: context.getters.getSubscriberId,
page: 1,
rows: rows,
type: options.type
type: options.type,
no_count: true
}).then((result) => {
const firstResultItemTimestamp = result.items[0]
? result.items[0].start_time
@ -95,7 +108,8 @@ export default {
type: options.type,
from: _.get(options, 'filter.from', ''),
to: _.get(options, 'filter.to', ''),
direction: _.get(options, 'filter.direction', '')
direction: _.get(options, 'filter.direction', ''),
no_count: true
})
context.commit('nextPageSucceeded', res)
} catch (err) {

@ -155,5 +155,8 @@ export default {
deletionFailed (state, err) {
state.deletionState = RequestState.failed
state.deletionError = err
},
setConversations (state, value) {
state.conversations = value
}
}

Loading…
Cancel
Save