TT#100756 CallRecordings - As Customer, I want to filter CallRecordings by time range

- Integrate date range input in CallRecording list view
- Adjust data fetching part
- Specify UX flow

Change-Id: I666aaa397a09ec9a5c2c63e76485e7fc4a797a46
mr9.5.2
Carlo Venusino 5 years ago
parent cd6e98a14a
commit 6bc5155662

@ -0,0 +1,273 @@
<template>
<div>
<div
class="row justify-center full-width q-gutter-x-sm"
>
<div
class="col-xs-12 col-md-2"
>
<q-select
v-model="filterTypeModel"
dense
:options="filterTypeOptions"
:label="$t('Filter by')"
:disable="loading"
data-cy="filter-type"
/>
</div>
<div
v-if="filterType === 'timerange'"
class="row col-xs-12 col-md-4"
>
<q-input
v-model="dateStartFilter"
class="q-pr-sm col-6"
dense
:disable="loading || filterType === null"
:label="$t('Start time')"
>
<template v-slot:prepend>
<q-icon
name="event"
class="cursor-pointer"
>
<q-popup-proxy
transition-show="scale"
transition-hide="scale"
@hide="addFilter('startTime', dateStartFilter)"
>
<q-date
v-model="dateStartFilter"
mask="YYYY-MM-DD HH:mm"
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>
<template v-slot:append>
<q-icon
name="access_time"
class="cursor-pointer"
>
<q-popup-proxy
transition-show="scale"
transition-hide="scale"
@hide="addFilter('startTime', dateStartFilter)"
>
<q-time
v-model="dateStartFilter"
mask="YYYY-MM-DD HH:mm"
format24h
>
<div class="row items-center justify-end">
<q-btn
v-close-popup
:label="$t('Close')"
color="primary"
flat
/>
</div>
</q-time>
</q-popup-proxy>
</q-icon>
</template>
</q-input>
<q-input
v-model="dateEndFilter"
class="col-6"
dense
:disable="loading || filterType === null"
:label="$t('End time')"
@input="triggerFilter"
>
<template v-slot:prepend>
<q-icon
name="event"
class="cursor-pointer"
>
<q-popup-proxy
transition-show="scale"
transition-hide="scale"
@hide="addFilter('endTime', dateEndFilter)"
>
<q-date
v-model="dateEndFilter"
mask="YYYY-MM-DD HH:mm"
>
<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>
<template v-slot:append>
<q-icon
name="access_time"
class="cursor-pointer"
>
<q-popup-proxy
transition-show="scale"
transition-hide="scale"
@hide="addFilter('endTime', dateEndFilter)"
>
<q-time
v-model="dateEndFilter"
mask="YYYY-MM-DD HH:mm"
format24h
>
<div class="row items-center justify-end">
<q-btn
v-close-popup
:label="$t('Close')"
color="primary"
flat
/>
</div>
</q-time>
</q-popup-proxy>
</q-icon>
</template>
</q-input>
</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"
data-cy="filter-applied-item"
@remove="removeFilter(id)"
/>
</div>
</div>
</div>
</template>
<script>
import _ from 'lodash'
export default {
name: 'CscCallRecordingFilters',
props: {
loading: {
type: Boolean,
default: false
}
},
data () {
return {
filterTypeModel: null,
typedFilter: null,
dateStartFilter: null,
dateEndFilter: null,
filters: []
}
},
computed: {
filterType () {
return this.filterTypeModel && this.filterTypeModel.value
},
filterTypeOptions () {
return [
{
label: this.$t('Timerange'),
value: 'timerange'
}
]
},
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: {
triggerFilter (data) {
this.addFilter(this.filterTypeModel?.value, this.typedFilter)
},
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
}
}
}
</script>

@ -538,6 +538,7 @@
"Time is invalid": "", "Time is invalid": "",
"Time of the day": "Uhrzeit des Tages", "Time of the day": "Uhrzeit des Tages",
"Timeout": "Klingeldauer", "Timeout": "Klingeldauer",
"Timerange": "",
"To": "An", "To": "An",
"Today": "Heute", "Today": "Heute",
"Toggled attachment successfully": "Voicemail als Anhang geändert", "Toggled attachment successfully": "Voicemail als Anhang geändert",

@ -538,6 +538,7 @@
"Time is invalid": "Time is invalid", "Time is invalid": "Time is invalid",
"Time of the day": "Time of the day", "Time of the day": "Time of the day",
"Timeout": "Timeout", "Timeout": "Timeout",
"Timerange": "Timerange",
"To": "To", "To": "To",
"Today": "Today", "Today": "Today",
"Toggled attachment successfully": "Toggled attachment successfully", "Toggled attachment successfully": "Toggled attachment successfully",

@ -538,6 +538,7 @@
"Time is invalid": "", "Time is invalid": "",
"Time of the day": "Hora del día", "Time of the day": "Hora del día",
"Timeout": "Tiempo de espera", "Timeout": "Tiempo de espera",
"Timerange": "",
"To": "Para", "To": "Para",
"Today": "Hoy", "Today": "Hoy",
"Toggled attachment successfully": "Adjunto cambiado correctamente", "Toggled attachment successfully": "Adjunto cambiado correctamente",

@ -538,6 +538,7 @@
"Time is invalid": "", "Time is invalid": "",
"Time of the day": "Heure de la journée", "Time of the day": "Heure de la journée",
"Timeout": "Temporisation", "Timeout": "Temporisation",
"Timerange": "",
"To": "A", "To": "A",
"Today": "", "Today": "",
"Toggled attachment successfully": "Les pièces jointes ont été activées avec succès", "Toggled attachment successfully": "Les pièces jointes ont été activées avec succès",

@ -538,6 +538,7 @@
"Time is invalid": "", "Time is invalid": "",
"Time of the day": "Ora del giorno", "Time of the day": "Ora del giorno",
"Timeout": "Timeout", "Timeout": "Timeout",
"Timerange": "",
"To": "A", "To": "A",
"Today": "Oggi", "Today": "Oggi",
"Toggled attachment successfully": "Opzione allegato modificata con successo", "Toggled attachment successfully": "Opzione allegato modificata con successo",

@ -1,7 +1,37 @@
<template> <template>
<csc-page <csc-page-sticky
class="q-pa-lg" id="csc-page-call-recording"
> >
<template
v-slot: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
v-slot:toolbar
>
<csc-call-recording-filters
v-if="showFilters"
ref="filters"
class="q-mb-md q-pa-md"
@filter="filterEvent"
/>
</template>
<template> <template>
<div class="q-pa-md"> <div class="q-pa-md">
<q-table <q-table
@ -33,16 +63,7 @@
<q-tr <q-tr
:props="props" :props="props"
> >
<q-td auto-width> <q-td auto-width />
<q-btn
size="sm"
color="primary"
round
dense
:icon="isRowExpanded(props.row.id) ? 'remove' : 'add'"
@click="updateCollapseArray(props.row.id)"
/>
</q-td>
<q-td <q-td
v-for="col in props.cols" v-for="col in props.cols"
:key="col.name" :key="col.name"
@ -51,12 +72,20 @@
{{ col.value }} {{ col.value }}
</q-td> </q-td>
<q-td> <q-td>
<csc-more-menu> <q-btn
<csc-popup-menu-item-delete color="negative"
color="negative" icon="delete"
@click="confirmRowDeletion(props.row.id)" flat
/> @click="confirmRowDeletion(props.row.id)"
</csc-more-menu> />
<q-btn
size="md"
color="primary"
round
flat
:icon="isRowExpanded(props.row.id) ? 'expand_less' : 'expand_more'"
@click="updateCollapseArray(props.row.id)"
/>
<csc-confirmation-dialog <csc-confirmation-dialog
:key="props.row.id" :key="props.row.id"
:ref="'confirmDelete-'+props.row.id" :ref="'confirmDelete-'+props.row.id"
@ -83,35 +112,45 @@
:loading="$wait.is('csc-call-recordings')" :loading="$wait.is('csc-call-recordings')"
:hide-pagination="true" :hide-pagination="true"
row-key="name" row-key="name"
class="csc-item-odd"
> >
<template v-slot:header="props"> <template v-slot:loading>
<q-tr :props="props"> <q-inner-loading
showing
color="primary"
/>
</template>
<template v-slot:header="innerProps">
<q-tr :props="innerProps">
<q-th auto-width />
<q-th <q-th
v-for="col in props.cols" v-for="col in innerProps.cols"
:key="col.name" :key="col.name"
:props="props" :props="innerProps"
> >
{{ col.label }} {{ col.label }}
</q-th> </q-th>
<q-th auto-width /> <q-th auto-width />
</q-tr> </q-tr>
</template> </template>
<template v-slot:body="props"> <template v-slot:body="innerProps">
<q-tr :props="props"> <q-tr :props="innerProps">
<q-td auto-width />
<q-td <q-td
v-for="col in props.cols" v-for="col in innerProps.cols"
:key="col.name" :key="col.name"
:props="props" :props="innerProps"
> >
{{ col.value }} {{ col.value }}
</q-td> </q-td>
<q-td> <q-td>
<q-btn <q-btn
size="sm" size="md"
color="primary" color="primary"
icon="download" icon="download"
dense dense
@click="saveFile(props.row.id)" flat
@click="saveFile(innerProps.row.id)"
/> />
</q-td> </q-td>
</q-tr> </q-tr>
@ -123,29 +162,23 @@
</q-table> </q-table>
</div> </div>
</template> </template>
</csc-page> </csc-page-sticky>
</template> </template>
<script> <script>
import { import { mapGetters } from 'vuex'
mapGetters import { mapWaitingActions } from 'vue-wait'
} from 'vuex'
import {
mapWaitingActions
} from 'vue-wait'
import CscPage from 'components/CscPage'
import CscMoreMenu from 'components/CscMoreMenu'
import CscPopupMenuItemDelete from 'components/CscPopupMenuItemDelete'
import CscConfirmationDialog from 'components/CscConfirmationDialog'
import { showGlobalError, showToast } from 'src/helpers/ui'
import { saveAs } from 'file-saver' import { saveAs } from 'file-saver'
import { showGlobalError, showToast } from 'src/helpers/ui'
import CscConfirmationDialog from 'components/CscConfirmationDialog'
import CscPageSticky from 'components/CscPageSticky'
import CscCallRecordingFilters from 'components/pages/CallRecording/CscCallRecordingFilters'
export default { export default {
name: 'CscCallBlocking', name: 'CscCallBlocking',
components: { components: {
CscPage, CscConfirmationDialog,
CscMoreMenu, CscPageSticky,
CscPopupMenuItemDelete, CscCallRecordingFilters
CscConfirmationDialog
}, },
data () { data () {
return { return {
@ -175,36 +208,38 @@ export default {
label: '#', label: '#',
align: 'left', align: 'left',
field: row => row.id, field: row => row.id,
format: val => `${val}`, format: val => `${val}`
sortable: true
}, },
{ {
name: 'type', name: 'type',
required: true, required: true,
align: 'left', align: 'left',
label: this.$t('Type'), label: this.$t('Type'),
field: row => row.type, field: row => row.type
sortable: true
}, },
{ {
name: 'format', name: 'format',
required: true, required: true,
align: 'left', align: 'left',
label: this.$t('Format'), label: this.$t('Format'),
field: row => row.format, field: row => row.format
sortable: true
} }
], ],
data: [], data: [],
rowStatus: [],
pagination: { pagination: {
sortBy: 'id', sortBy: 'id',
descending: false, descending: false,
page: 1, page: 1,
rowsPerPage: 5, rowsPerPage: 5,
rowsNumber: 0 rowsNumber: 0
} },
rowStatus: [],
filter: {
startTime: null,
endTime: null
},
showFilters: false
} }
}, },
computed: { computed: {
@ -225,7 +260,8 @@ export default {
}, },
async mounted () { async mounted () {
await this.fetchPaginatedRecordings({ await this.fetchPaginatedRecordings({
pagination: this.pagination pagination: this.pagination,
filter: this.filter
}) })
}, },
methods: { methods: {
@ -237,15 +273,27 @@ export default {
}), }),
async fetchPaginatedRecordings (props) { async fetchPaginatedRecordings (props) {
const { page, rowsPerPage, sortBy, descending } = props.pagination const { page, rowsPerPage, sortBy, descending } = props.pagination
const { startTime, endTime } = this.filter
const count = await this.fetchRecordings({ const count = await this.fetchRecordings({
page: page, page: page,
rows: rowsPerPage, rows: rowsPerPage,
order_by: sortBy, order_by: sortBy,
order_by_direction: descending ? 'desc' : 'asc' order_by_direction: descending ? 'desc' : 'asc',
start_time: startTime,
end_time: endTime
}) })
this.pagination = { ...props.pagination } this.pagination = { ...props.pagination }
this.pagination.rowsNumber = count this.pagination.rowsNumber = count
}, },
openFilters () {
this.showFilters = true
},
closeFilters () {
if (this.$refs.filters) {
this.$refs.filters.removeFilters()
}
this.showFilters = false
},
confirmRowDeletion (rowId) { confirmRowDeletion (rowId) {
this.$refs['confirmDelete-' + rowId].open() this.$refs['confirmDelete-' + rowId].open()
}, },
@ -254,7 +302,8 @@ export default {
await this.deleteRecording(rowId) await this.deleteRecording(rowId)
showToast(this.$t('Recording successfully deleted')) showToast(this.$t('Recording successfully deleted'))
await this.fetchPaginatedRecordings({ await this.fetchPaginatedRecordings({
pagination: this.pagination pagination: this.pagination,
filter: this.filter
}) })
} catch (err) { } catch (err) {
showGlobalError(this.$t('Something went wrong. Please retry later')) showGlobalError(this.$t('Something went wrong. Please retry later'))
@ -275,6 +324,14 @@ export default {
async saveFile (fileId) { async saveFile (fileId) {
const file = await this.downloadRecording(fileId) const file = await this.downloadRecording(fileId)
saveAs(file, 'call-recording-' + fileId + '.wav') saveAs(file, 'call-recording-' + fileId + '.wav')
},
filterEvent (filter) {
this.$scrollTo(this.$parent.$el)
this.filter = filter
this.fetchPaginatedRecordings({
pagination: this.pagination,
filter: this.filter
})
} }
} }
} }

Loading…
Cancel
Save