feat: SLA reports view (#9189)
* feat: SLA report table * feat: Add SLA popover card * feat: Update popover position * feat: Add loader * Update SLACardLabel.vue * feat: Update column order * chore: fix conditions * Update SLATable.vue * chore: enable reports in ui * chore: Revamp report SLA apis * chore: revert download method * chore: improve the code * Update enterprise/app/views/api/v1/accounts/applied_slas/download.csv.erb Co-authored-by: Vishnu Narayanan <iamwishnu@gmail.com> * chore: style fixes * chore: fix specs * feat: Add number of conversations * chore: review comments * fix: translation * Update app/javascript/dashboard/i18n/locale/en/report.json Co-authored-by: Vishnu Narayanan <iamwishnu@gmail.com> * Update app/javascript/dashboard/i18n/locale/en/report.json Co-authored-by: Vishnu Narayanan <iamwishnu@gmail.com> * Update app/javascript/dashboard/i18n/locale/en/report.json Co-authored-by: Vishnu Narayanan <iamwishnu@gmail.com> * Update SLAReportItem.vue * Update report.json * Update package.json * chore: review comments * chore: remove unused translation * feat: Add TableHeaderCell component * chore: more review fixes * Update app/javascript/dashboard/components/widgets/TableHeaderCell.vue Co-authored-by: Shivam Mishra <scm.mymail@gmail.com> * Update TableHeaderCell.vue --------- Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> Co-authored-by: Vishnu Narayanan <iamwishnu@gmail.com> Co-authored-by: Shivam Mishra <scm.mymail@gmail.com>
This commit is contained in:
@@ -12,6 +12,7 @@ const reports = accountId => ({
|
||||
'label_reports',
|
||||
'inbox_reports',
|
||||
'team_reports',
|
||||
'sla_reports',
|
||||
],
|
||||
menuItems: [
|
||||
{
|
||||
@@ -71,6 +72,14 @@ const reports = accountId => ({
|
||||
toState: frontendURL(`accounts/${accountId}/reports/teams`),
|
||||
toStateName: 'team_reports',
|
||||
},
|
||||
{
|
||||
icon: 'document-list-clock',
|
||||
label: 'REPORTS_SLA',
|
||||
hasSubMenu: false,
|
||||
featureFlag: FEATURE_FLAGS.SLA,
|
||||
toState: frontendURL(`accounts/${accountId}/reports/sla`),
|
||||
toStateName: 'sla_reports',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
const props = defineProps({
|
||||
span: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
required: true,
|
||||
default: '',
|
||||
},
|
||||
});
|
||||
|
||||
const spanClass = computed(() => {
|
||||
if (props.span === 1) return 'col-span-1';
|
||||
if (props.span === 2) return 'col-span-2';
|
||||
if (props.span === 3) return 'col-span-3';
|
||||
if (props.span === 4) return 'col-span-4';
|
||||
if (props.span === 5) return 'col-span-5';
|
||||
if (props.span === 6) return 'col-span-6';
|
||||
if (props.span === 7) return 'col-span-7';
|
||||
if (props.span === 8) return 'col-span-8';
|
||||
if (props.span === 9) return 'col-span-9';
|
||||
if (props.span === 10) return 'col-span-10';
|
||||
|
||||
return 'col-span-1';
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<div
|
||||
class="flex items-center px-0 py-2 text-xs font-medium text-left uppercase text-slate-700 dark:text-slate-100 rtl:text-right"
|
||||
:class="spanClass"
|
||||
>
|
||||
<slot>
|
||||
{{ label }}
|
||||
</slot>
|
||||
</div>
|
||||
</template>
|
||||
@@ -46,6 +46,11 @@ export default {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
conversationLabels: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
||||
@@ -508,15 +508,29 @@
|
||||
},
|
||||
"SLA_REPORTS": {
|
||||
"HEADER": "SLA Reports",
|
||||
"NO_RECORDS": "SLA applied conversations are not available.",
|
||||
"LOADING": "Loading SLA data...",
|
||||
"METRICS": {
|
||||
"HIT_RATE": {
|
||||
"LABEL": "Hit Rate",
|
||||
"TOOLTIP": "Percentage of SLAs created were completed successfully"
|
||||
},
|
||||
"NO_OF_BREACHES": {
|
||||
"LABEL": "Number of Breaches",
|
||||
"TOOLTIP": "The total SLA breaches in a certain period."
|
||||
"NO_OF_MISSES": {
|
||||
"LABEL": "Number of Misses",
|
||||
"TOOLTIP": "Total SLA misses in a certain period"
|
||||
},
|
||||
"NO_OF_CONVERSATIONS": {
|
||||
"LABEL": "Number of Conversations",
|
||||
"TOOLTIP": "Total number of conversations with SLA"
|
||||
}
|
||||
},
|
||||
"TABLE": {
|
||||
"HEADER": {
|
||||
"POLICY": "Policy",
|
||||
"CONVERSATION": "Conversation",
|
||||
"AGENT": "Agent"
|
||||
},
|
||||
"VIEW_DETAILS": "View Details"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -240,6 +240,7 @@
|
||||
"CAMPAIGNS": "Campaigns",
|
||||
"ONGOING": "Ongoing",
|
||||
"ONE_OFF": "One off",
|
||||
"REPORTS_SLA": "SLA",
|
||||
"REPORTS_BOT": "Bot",
|
||||
"REPORTS_AGENT": "Agents",
|
||||
"REPORTS_LABEL": "Labels",
|
||||
|
||||
@@ -4,6 +4,10 @@ export default {
|
||||
computed: {
|
||||
...mapGetters({ accountLabels: 'labels/getLabels' }),
|
||||
savedLabels() {
|
||||
// If conversationLabels is passed as prop, use it
|
||||
if (this.conversationLabels)
|
||||
return this.conversationLabels.split(',').map(item => item.trim());
|
||||
// Otherwise, get labels from store
|
||||
return this.$store.getters['conversationLabels/getConversationLabels'](
|
||||
this.conversationId
|
||||
);
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
<template>
|
||||
<div class="flex flex-col flex-1 px-4 pt-4 overflow-auto">
|
||||
<SLAReportFilters @filter-change="onFilterChange" />
|
||||
<div class="flex flex-col gap-6">
|
||||
<SLAMetrics
|
||||
:hit-rate="slaMetrics.hitRate"
|
||||
:no-of-breaches="slaMetrics.numberOfSLAMisses"
|
||||
:no-of-conversations="slaMetrics.numberOfConversations"
|
||||
:is-loading="uiFlags.isFetchingMetrics"
|
||||
/>
|
||||
<SLATable
|
||||
:sla-reports="slaReports"
|
||||
:is-loading="uiFlags.isFetching"
|
||||
:current-page="Number(slaMeta.currentPage)"
|
||||
:total-count="Number(slaMeta.count)"
|
||||
@page-change="onPageChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import SLAMetrics from './components/SLA/SLAMetrics.vue';
|
||||
import SLATable from './components/SLA/SLATable.vue';
|
||||
import SLAReportFilters from './components/SLA/SLAReportFilters.vue';
|
||||
|
||||
export default {
|
||||
name: 'SLAReports',
|
||||
components: {
|
||||
SLAMetrics,
|
||||
SLATable,
|
||||
SLAReportFilters,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
pageNumber: 1,
|
||||
from: 0,
|
||||
to: 0,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
slaReports: 'slaReports/getAll',
|
||||
slaMetrics: 'slaReports/getMetrics',
|
||||
slaMeta: 'slaReports/getMeta',
|
||||
uiFlags: 'slaReports/getUIFlags',
|
||||
}),
|
||||
},
|
||||
mounted() {
|
||||
this.fetchSLAMetrics();
|
||||
this.fetchSLAReports();
|
||||
},
|
||||
methods: {
|
||||
fetchSLAReports({ pageNumber } = {}) {
|
||||
this.$store.dispatch('slaReports/get', {
|
||||
page: pageNumber || this.pageNumber,
|
||||
from: this.from,
|
||||
to: this.to,
|
||||
});
|
||||
},
|
||||
fetchSLAMetrics() {
|
||||
this.$store.dispatch('slaReports/getMetrics', {
|
||||
from: this.from,
|
||||
to: this.to,
|
||||
});
|
||||
},
|
||||
onPageChange(pageNumber) {
|
||||
this.fetchSLAReports({ pageNumber });
|
||||
},
|
||||
onFilterChange({ from, to }) {
|
||||
this.from = from;
|
||||
this.to = to;
|
||||
this.fetchSLAReports();
|
||||
this.fetchSLAMetrics();
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -1,6 +1,8 @@
|
||||
<template>
|
||||
<div class="flex flex-col md:flex-row justify-between mb-4">
|
||||
<div class="md:grid flex flex-col filter-container gap-3 w-full">
|
||||
<div
|
||||
class="md:grid flex-col gap-3 w-full grid grid-cols-[repeat(auto-fit,minmax(200px,1fr))] p-5"
|
||||
>
|
||||
<reports-filters-date-range @on-range-change="onDateRangeChange" />
|
||||
<woot-date-range-picker
|
||||
v-if="isDateRangeSelected"
|
||||
|
||||
@@ -13,9 +13,18 @@
|
||||
class="w-full sm:w-px h-full border border-slate-75 dark:border-slate-700/50"
|
||||
/>
|
||||
<SLAMetricCard
|
||||
:label="$t('SLA_REPORTS.METRICS.NO_OF_BREACHES.LABEL')"
|
||||
:label="$t('SLA_REPORTS.METRICS.NO_OF_MISSES.LABEL')"
|
||||
:value="noOfBreaches"
|
||||
:tool-tip="$t('SLA_REPORTS.METRICS.NO_OF_BREACHES.TOOLTIP')"
|
||||
:tool-tip="$t('SLA_REPORTS.METRICS.NO_OF_MISSES.TOOLTIP')"
|
||||
:is-loading="isLoading"
|
||||
/>
|
||||
<div
|
||||
class="w-full sm:w-px h-full border border-slate-75 dark:border-slate-700/50"
|
||||
/>
|
||||
<SLAMetricCard
|
||||
:label="$t('SLA_REPORTS.METRICS.NO_OF_CONVERSATIONS.LABEL')"
|
||||
:value="noOfConversations"
|
||||
:tool-tip="$t('SLA_REPORTS.METRICS.NO_OF_CONVERSATIONS.TOOLTIP')"
|
||||
:is-loading="isLoading"
|
||||
/>
|
||||
</div>
|
||||
@@ -32,6 +41,10 @@ defineProps({
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
noOfConversations: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
isLoading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
<template>
|
||||
<div class="flex flex-col md:flex-row justify-between mb-4">
|
||||
<div class="md:grid flex flex-col filter-container gap-3 w-full">
|
||||
<reports-filters-date-range @on-range-change="onDateRangeChange" />
|
||||
<woot-date-range-picker
|
||||
v-if="isDateRangeSelected"
|
||||
show-range
|
||||
class="no-margin auto-width"
|
||||
:value="customDateRange"
|
||||
:confirm-text="$t('REPORT.CUSTOM_DATE_RANGE.CONFIRM')"
|
||||
:placeholder="$t('REPORT.CUSTOM_DATE_RANGE.PLACEHOLDER')"
|
||||
@change="onCustomDateRangeChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import WootDateRangePicker from 'dashboard/components/ui/DateRangePicker.vue';
|
||||
import ReportsFiltersDateRange from '../Filters/DateRange.vue';
|
||||
import subDays from 'date-fns/subDays';
|
||||
import { DATE_RANGE_OPTIONS } from '../../constants';
|
||||
import { getUnixStartOfDay, getUnixEndOfDay } from 'helpers/DateHelper';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
WootDateRangePicker,
|
||||
ReportsFiltersDateRange,
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
selectedDateRange: DATE_RANGE_OPTIONS.LAST_7_DAYS,
|
||||
selectedGroupByFilter: null,
|
||||
customDateRange: [new Date(), new Date()],
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
isDateRangeSelected() {
|
||||
return (
|
||||
this.selectedDateRange.id === DATE_RANGE_OPTIONS.CUSTOM_DATE_RANGE.id
|
||||
);
|
||||
},
|
||||
to() {
|
||||
if (this.isDateRangeSelected) {
|
||||
return getUnixEndOfDay(this.customDateRange[1]);
|
||||
}
|
||||
return getUnixEndOfDay(new Date());
|
||||
},
|
||||
from() {
|
||||
if (this.isDateRangeSelected) {
|
||||
return getUnixStartOfDay(this.customDateRange[0]);
|
||||
}
|
||||
|
||||
const { offset } = this.selectedDateRange;
|
||||
const fromDate = subDays(new Date(), offset);
|
||||
return getUnixStartOfDay(fromDate);
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
businessHoursSelected() {
|
||||
this.emitChange();
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.emitChange();
|
||||
},
|
||||
methods: {
|
||||
emitChange() {
|
||||
const { from, to } = this;
|
||||
this.$emit('filter-change', {
|
||||
from,
|
||||
to,
|
||||
});
|
||||
},
|
||||
onDateRangeChange(selectedRange) {
|
||||
this.selectedDateRange = selectedRange;
|
||||
this.emitChange();
|
||||
},
|
||||
onCustomDateRangeChange(value) {
|
||||
this.customDateRange = value;
|
||||
this.emitChange();
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.filter-container {
|
||||
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,59 @@
|
||||
<script setup>
|
||||
import UserAvatarWithName from 'dashboard/components/widgets/UserAvatarWithName.vue';
|
||||
import CardLabels from 'dashboard/components/widgets/conversation/conversationCardComponents/CardLabels.vue';
|
||||
import SLAViewDetails from './SLAViewDetails.vue';
|
||||
defineProps({
|
||||
slaName: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
conversationId: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
conversation: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
slaEvents: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="grid content-center items-center h-16 grid-cols-12 gap-4 px-6 py-0 w-full bg-white border-b last:border-b-0 last:rounded-b-xl border-slate-75 dark:border-slate-800/50 dark:bg-slate-900"
|
||||
>
|
||||
<div
|
||||
class="flex items-center gap-2 col-span-6 px-0 py-2 text-sm tracking-[0.5] text-slate-700 dark:text-slate-100 rtl:text-right"
|
||||
>
|
||||
<span class="text-slate-700 dark:text-slate-200">
|
||||
{{ `#${conversationId} ` }}
|
||||
</span>
|
||||
<span class="text-slate-600 dark:text-slate-300">with </span>
|
||||
<span class="text-slate-700 dark:text-slate-200 capitalize truncate">{{
|
||||
conversation.contact.name
|
||||
}}</span>
|
||||
<card-labels
|
||||
class="w-[80%]"
|
||||
:conversation-id="conversationId"
|
||||
:conversation-labels="conversation.labels"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="flex items-center capitalize py-2 px-0 text-sm tracking-[0.5] text-slate-700 dark:text-slate-50 text-left rtl:text-right col-span-2"
|
||||
>
|
||||
{{ slaName }}
|
||||
</div>
|
||||
<div class="flex items-center gap-2 col-span-2">
|
||||
<user-avatar-with-name
|
||||
v-if="conversation.assignee"
|
||||
:user="conversation.assignee"
|
||||
/>
|
||||
<span v-else class="text-slate-600 dark:text-slate-200"> --- </span>
|
||||
</div>
|
||||
<SLAViewDetails :sla-events="slaEvents" />
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,112 @@
|
||||
<template>
|
||||
<div>
|
||||
<div
|
||||
class="min-w-full border rounded-xl border-slate-75 dark:border-slate-700/50"
|
||||
>
|
||||
<div
|
||||
class="grid content-center h-12 grid-cols-12 gap-4 px-6 py-0 border-b bg-slate-25 border-slate-75 dark:border-slate-800 rounded-t-xl dark:bg-slate-900"
|
||||
>
|
||||
<table-header-cell
|
||||
:span="6"
|
||||
:label="$t('SLA_REPORTS.TABLE.HEADER.CONVERSATION')"
|
||||
/>
|
||||
<table-header-cell
|
||||
:span="2"
|
||||
:label="$t('SLA_REPORTS.TABLE.HEADER.POLICY')"
|
||||
/>
|
||||
<table-header-cell
|
||||
:span="2"
|
||||
:label="$t('SLA_REPORTS.TABLE.HEADER.AGENT')"
|
||||
/>
|
||||
<table-header-cell :span="2" label="" />
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="isLoading"
|
||||
class="flex items-center justify-center h-32 bg-white dark:bg-slate-900"
|
||||
>
|
||||
<spinner />
|
||||
<span>{{ $t('SLA_REPORTS.LOADING') }}</span>
|
||||
</div>
|
||||
<div v-else-if="slaReports.length > 0">
|
||||
<SLA-report-item
|
||||
v-for="slaReport in slaReports"
|
||||
:key="slaReport.applied_sla.id"
|
||||
:sla-name="slaReport.applied_sla.sla_name"
|
||||
:conversation="slaReport.conversation"
|
||||
:conversation-id="slaReport.conversation.id"
|
||||
:sla-events="slaReport.sla_events"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="flex items-center justify-center h-32 bg-white dark:bg-slate-900"
|
||||
>
|
||||
{{ $t('SLA_REPORTS.NO_RECORDS') }}
|
||||
</div>
|
||||
</div>
|
||||
<table-footer
|
||||
v-if="shouldShowFooter"
|
||||
:current-page="currentPage"
|
||||
:total-count="totalCount"
|
||||
:page-size="pageSize"
|
||||
class="bg-slate-25 dark:bg-slate-900 sticky bottom-0 border-none mt-4"
|
||||
@page-change="onPageChange"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import TableFooter from 'dashboard/components/widgets/TableFooter.vue';
|
||||
import TableHeaderCell from 'dashboard/components/widgets/TableHeaderCell.vue';
|
||||
import SLAReportItem from './SLAReportItem.vue';
|
||||
import Spinner from 'shared/components/Spinner.vue';
|
||||
export default {
|
||||
name: 'SLATable',
|
||||
components: {
|
||||
SLAReportItem,
|
||||
TableFooter,
|
||||
Spinner,
|
||||
TableHeaderCell,
|
||||
},
|
||||
props: {
|
||||
slaReports: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
totalCount: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
currentPage: {
|
||||
type: Number,
|
||||
default: 1,
|
||||
},
|
||||
pageSize: {
|
||||
type: Number,
|
||||
default: 25,
|
||||
},
|
||||
isLoading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
pageNo: 1,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
shouldShowFooter() {
|
||||
return this.currentPage === 1
|
||||
? this.totalCount > this.pageSize
|
||||
: this.slaReports.length > 0;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onPageChange(page) {
|
||||
this.$emit('page-change', page);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,55 @@
|
||||
<template>
|
||||
<div v-on-clickaway="closeSlaEvents" class="label-wrap">
|
||||
<div
|
||||
class="flex items-center col-span-2 px-0 py-2 text-sm tracking-[0.5] text-slate-700 dark:text-slate-100 rtl:text-right"
|
||||
>
|
||||
<div class="relative">
|
||||
<woot-button
|
||||
color-scheme="secondary"
|
||||
variant="link"
|
||||
@click="openSlaEvents"
|
||||
>
|
||||
{{ $t('SLA_REPORTS.TABLE.VIEW_DETAILS') }}
|
||||
</woot-button>
|
||||
<SLA-popover-card
|
||||
v-if="showSlaPopoverCard"
|
||||
:all-missed-slas="slaEvents"
|
||||
class="right-0"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mixin as clickaway } from 'vue-clickaway';
|
||||
import SLAPopoverCard from 'dashboard/components/widgets/conversation/components/SLAPopoverCard.vue';
|
||||
export default {
|
||||
components: {
|
||||
SLAPopoverCard,
|
||||
},
|
||||
mixins: [clickaway],
|
||||
|
||||
props: {
|
||||
slaEvents: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
showSlaPopoverCard: false,
|
||||
};
|
||||
},
|
||||
|
||||
methods: {
|
||||
closeSlaEvents() {
|
||||
this.showSlaPopoverCard = false;
|
||||
},
|
||||
openSlaEvents() {
|
||||
this.showSlaPopoverCard = !this.showSlaPopoverCard;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -9,6 +9,7 @@ const TeamReports = () => import('./TeamReports.vue');
|
||||
const CsatResponses = () => import('./CsatResponses.vue');
|
||||
const BotReports = () => import('./BotReports.vue');
|
||||
const LiveReports = () => import('./LiveReports.vue');
|
||||
const SLAReports = () => import('./SLAReports.vue');
|
||||
|
||||
export default {
|
||||
routes: [
|
||||
@@ -151,5 +152,22 @@ export default {
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: frontendURL('accounts/:accountId/reports'),
|
||||
component: SettingsContent,
|
||||
props: {
|
||||
headerTitle: 'SLA_REPORTS.HEADER',
|
||||
icon: 'document-list-clock',
|
||||
keepAlive: false,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: 'sla',
|
||||
name: 'sla_reports',
|
||||
roles: ['administrator'],
|
||||
component: SLAReports,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@@ -5,7 +5,8 @@ import SLAReportsAPI from '../../api/slaReports';
|
||||
export const state = {
|
||||
records: [],
|
||||
metrics: {
|
||||
numberOfSLABreaches: 0,
|
||||
numberOfConversations: 0,
|
||||
numberOfSLAMisses: 0,
|
||||
hitRate: '0%',
|
||||
},
|
||||
uiFlags: {
|
||||
@@ -72,19 +73,21 @@ export const mutations = {
|
||||
[types.SET_SLA_REPORTS]: MutationHelpers.set,
|
||||
[types.SET_SLA_REPORTS_METRICS](
|
||||
_state,
|
||||
{ number_of_sla_breaches: numberOfSLABreaches, hit_rate: hitRate }
|
||||
{
|
||||
number_of_sla_misses: numberOfSLAMisses,
|
||||
hit_rate: hitRate,
|
||||
total_applied_slas: numberOfConversations,
|
||||
}
|
||||
) {
|
||||
_state.metrics = {
|
||||
numberOfSLABreaches,
|
||||
numberOfSLAMisses,
|
||||
hitRate,
|
||||
numberOfConversations,
|
||||
};
|
||||
},
|
||||
[types.SET_SLA_REPORTS_META](
|
||||
_state,
|
||||
{ total_applied_slas: totalAppliedSLAs, current_page: currentPage }
|
||||
) {
|
||||
[types.SET_SLA_REPORTS_META](_state, { count, current_page: currentPage }) {
|
||||
_state.meta = {
|
||||
count: totalAppliedSLAs,
|
||||
count,
|
||||
currentPage,
|
||||
};
|
||||
},
|
||||
|
||||
@@ -21,4 +21,33 @@ describe('#getters', () => {
|
||||
isFetchingMetrics: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('getMeta', () => {
|
||||
const state = {
|
||||
meta: {
|
||||
count: 0,
|
||||
currentPage: 1,
|
||||
},
|
||||
};
|
||||
expect(getters.getMeta(state)).toEqual({
|
||||
count: 0,
|
||||
currentPage: 1,
|
||||
});
|
||||
});
|
||||
|
||||
it('getMetrics', () => {
|
||||
const state = {
|
||||
metrics: {
|
||||
numberOfConversations: 27,
|
||||
numberOfSLAMisses: 25,
|
||||
hitRate: '7.41%',
|
||||
},
|
||||
};
|
||||
|
||||
expect(getters.getMetrics(state)).toEqual({
|
||||
numberOfConversations: 27,
|
||||
numberOfSLAMisses: 25,
|
||||
hitRate: '7.41%',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -23,12 +23,14 @@ describe('#mutations', () => {
|
||||
it('set metrics', () => {
|
||||
const state = { metrics: {} };
|
||||
mutations[types.SET_SLA_REPORTS_METRICS](state, {
|
||||
number_of_sla_breaches: 1,
|
||||
number_of_sla_misses: 1,
|
||||
hit_rate: '100%',
|
||||
total_applied_slas: 1,
|
||||
});
|
||||
expect(state.metrics).toEqual({
|
||||
numberOfSLABreaches: 1,
|
||||
numberOfSLAMisses: 1,
|
||||
hitRate: '100%',
|
||||
numberOfConversations: 1,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -37,7 +39,7 @@ describe('#mutations', () => {
|
||||
it('set meta', () => {
|
||||
const state = { meta: {} };
|
||||
mutations[types.SET_SLA_REPORTS_META](state, {
|
||||
total_applied_slas: 1,
|
||||
count: 1,
|
||||
current_page: 1,
|
||||
});
|
||||
expect(state.meta).toEqual({
|
||||
|
||||
Reference in New Issue
Block a user