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',
|
'label_reports',
|
||||||
'inbox_reports',
|
'inbox_reports',
|
||||||
'team_reports',
|
'team_reports',
|
||||||
|
'sla_reports',
|
||||||
],
|
],
|
||||||
menuItems: [
|
menuItems: [
|
||||||
{
|
{
|
||||||
@@ -71,6 +72,14 @@ const reports = accountId => ({
|
|||||||
toState: frontendURL(`accounts/${accountId}/reports/teams`),
|
toState: frontendURL(`accounts/${accountId}/reports/teams`),
|
||||||
toStateName: 'team_reports',
|
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,
|
type: Number,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
conversationLabels: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -508,15 +508,29 @@
|
|||||||
},
|
},
|
||||||
"SLA_REPORTS": {
|
"SLA_REPORTS": {
|
||||||
"HEADER": "SLA Reports",
|
"HEADER": "SLA Reports",
|
||||||
|
"NO_RECORDS": "SLA applied conversations are not available.",
|
||||||
|
"LOADING": "Loading SLA data...",
|
||||||
"METRICS": {
|
"METRICS": {
|
||||||
"HIT_RATE": {
|
"HIT_RATE": {
|
||||||
"LABEL": "Hit Rate",
|
"LABEL": "Hit Rate",
|
||||||
"TOOLTIP": "Percentage of SLAs created were completed successfully"
|
"TOOLTIP": "Percentage of SLAs created were completed successfully"
|
||||||
},
|
},
|
||||||
"NO_OF_BREACHES": {
|
"NO_OF_MISSES": {
|
||||||
"LABEL": "Number of Breaches",
|
"LABEL": "Number of Misses",
|
||||||
"TOOLTIP": "The total SLA breaches in a certain period."
|
"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",
|
"CAMPAIGNS": "Campaigns",
|
||||||
"ONGOING": "Ongoing",
|
"ONGOING": "Ongoing",
|
||||||
"ONE_OFF": "One off",
|
"ONE_OFF": "One off",
|
||||||
|
"REPORTS_SLA": "SLA",
|
||||||
"REPORTS_BOT": "Bot",
|
"REPORTS_BOT": "Bot",
|
||||||
"REPORTS_AGENT": "Agents",
|
"REPORTS_AGENT": "Agents",
|
||||||
"REPORTS_LABEL": "Labels",
|
"REPORTS_LABEL": "Labels",
|
||||||
|
|||||||
@@ -4,6 +4,10 @@ export default {
|
|||||||
computed: {
|
computed: {
|
||||||
...mapGetters({ accountLabels: 'labels/getLabels' }),
|
...mapGetters({ accountLabels: 'labels/getLabels' }),
|
||||||
savedLabels() {
|
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'](
|
return this.$store.getters['conversationLabels/getConversationLabels'](
|
||||||
this.conversationId
|
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>
|
<template>
|
||||||
<div class="flex flex-col md:flex-row justify-between mb-4">
|
<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" />
|
<reports-filters-date-range @on-range-change="onDateRangeChange" />
|
||||||
<woot-date-range-picker
|
<woot-date-range-picker
|
||||||
v-if="isDateRangeSelected"
|
v-if="isDateRangeSelected"
|
||||||
|
|||||||
@@ -13,9 +13,18 @@
|
|||||||
class="w-full sm:w-px h-full border border-slate-75 dark:border-slate-700/50"
|
class="w-full sm:w-px h-full border border-slate-75 dark:border-slate-700/50"
|
||||||
/>
|
/>
|
||||||
<SLAMetricCard
|
<SLAMetricCard
|
||||||
:label="$t('SLA_REPORTS.METRICS.NO_OF_BREACHES.LABEL')"
|
:label="$t('SLA_REPORTS.METRICS.NO_OF_MISSES.LABEL')"
|
||||||
:value="noOfBreaches"
|
: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"
|
:is-loading="isLoading"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -32,6 +41,10 @@ defineProps({
|
|||||||
type: Number,
|
type: Number,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
noOfConversations: {
|
||||||
|
type: Number,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
isLoading: {
|
isLoading: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
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 CsatResponses = () => import('./CsatResponses.vue');
|
||||||
const BotReports = () => import('./BotReports.vue');
|
const BotReports = () => import('./BotReports.vue');
|
||||||
const LiveReports = () => import('./LiveReports.vue');
|
const LiveReports = () => import('./LiveReports.vue');
|
||||||
|
const SLAReports = () => import('./SLAReports.vue');
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
routes: [
|
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 = {
|
export const state = {
|
||||||
records: [],
|
records: [],
|
||||||
metrics: {
|
metrics: {
|
||||||
numberOfSLABreaches: 0,
|
numberOfConversations: 0,
|
||||||
|
numberOfSLAMisses: 0,
|
||||||
hitRate: '0%',
|
hitRate: '0%',
|
||||||
},
|
},
|
||||||
uiFlags: {
|
uiFlags: {
|
||||||
@@ -72,19 +73,21 @@ export const mutations = {
|
|||||||
[types.SET_SLA_REPORTS]: MutationHelpers.set,
|
[types.SET_SLA_REPORTS]: MutationHelpers.set,
|
||||||
[types.SET_SLA_REPORTS_METRICS](
|
[types.SET_SLA_REPORTS_METRICS](
|
||||||
_state,
|
_state,
|
||||||
{ number_of_sla_breaches: numberOfSLABreaches, hit_rate: hitRate }
|
{
|
||||||
|
number_of_sla_misses: numberOfSLAMisses,
|
||||||
|
hit_rate: hitRate,
|
||||||
|
total_applied_slas: numberOfConversations,
|
||||||
|
}
|
||||||
) {
|
) {
|
||||||
_state.metrics = {
|
_state.metrics = {
|
||||||
numberOfSLABreaches,
|
numberOfSLAMisses,
|
||||||
hitRate,
|
hitRate,
|
||||||
|
numberOfConversations,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
[types.SET_SLA_REPORTS_META](
|
[types.SET_SLA_REPORTS_META](_state, { count, current_page: currentPage }) {
|
||||||
_state,
|
|
||||||
{ total_applied_slas: totalAppliedSLAs, current_page: currentPage }
|
|
||||||
) {
|
|
||||||
_state.meta = {
|
_state.meta = {
|
||||||
count: totalAppliedSLAs,
|
count,
|
||||||
currentPage,
|
currentPage,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -21,4 +21,33 @@ describe('#getters', () => {
|
|||||||
isFetchingMetrics: false,
|
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', () => {
|
it('set metrics', () => {
|
||||||
const state = { metrics: {} };
|
const state = { metrics: {} };
|
||||||
mutations[types.SET_SLA_REPORTS_METRICS](state, {
|
mutations[types.SET_SLA_REPORTS_METRICS](state, {
|
||||||
number_of_sla_breaches: 1,
|
number_of_sla_misses: 1,
|
||||||
hit_rate: '100%',
|
hit_rate: '100%',
|
||||||
|
total_applied_slas: 1,
|
||||||
});
|
});
|
||||||
expect(state.metrics).toEqual({
|
expect(state.metrics).toEqual({
|
||||||
numberOfSLABreaches: 1,
|
numberOfSLAMisses: 1,
|
||||||
hitRate: '100%',
|
hitRate: '100%',
|
||||||
|
numberOfConversations: 1,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -37,7 +39,7 @@ describe('#mutations', () => {
|
|||||||
it('set meta', () => {
|
it('set meta', () => {
|
||||||
const state = { meta: {} };
|
const state = { meta: {} };
|
||||||
mutations[types.SET_SLA_REPORTS_META](state, {
|
mutations[types.SET_SLA_REPORTS_META](state, {
|
||||||
total_applied_slas: 1,
|
count: 1,
|
||||||
current_page: 1,
|
current_page: 1,
|
||||||
});
|
});
|
||||||
expect(state.meta).toEqual({
|
expect(state.meta).toEqual({
|
||||||
|
|||||||
@@ -6,22 +6,23 @@ class Api::V1::Accounts::AppliedSlasController < Api::V1::Accounts::EnterpriseAc
|
|||||||
|
|
||||||
before_action :set_applied_slas, only: [:index, :metrics, :download]
|
before_action :set_applied_slas, only: [:index, :metrics, :download]
|
||||||
before_action :set_current_page, only: [:index]
|
before_action :set_current_page, only: [:index]
|
||||||
before_action :paginate_slas, only: [:index]
|
|
||||||
before_action :check_admin_authorization?
|
before_action :check_admin_authorization?
|
||||||
|
|
||||||
sort_on :created_at, type: :datetime
|
sort_on :created_at, type: :datetime
|
||||||
|
|
||||||
def index; end
|
def index
|
||||||
|
@count = number_of_sla_misses
|
||||||
|
@applied_slas = @missed_applied_slas.page(@current_page).per(RESULTS_PER_PAGE)
|
||||||
|
end
|
||||||
|
|
||||||
def metrics
|
def metrics
|
||||||
@total_applied_slas = total_applied_slas
|
@total_applied_slas = total_applied_slas
|
||||||
@number_of_sla_breaches = number_of_sla_breaches
|
@number_of_sla_misses = number_of_sla_misses
|
||||||
@hit_rate = hit_rate
|
@hit_rate = hit_rate
|
||||||
end
|
end
|
||||||
|
|
||||||
def download
|
def download
|
||||||
@breached_slas = breached_slas
|
@missed_applied_slas = missed_applied_slas
|
||||||
|
|
||||||
response.headers['Content-Type'] = 'text/csv'
|
response.headers['Content-Type'] = 'text/csv'
|
||||||
response.headers['Content-Disposition'] = 'attachment; filename=breached_conversation.csv'
|
response.headers['Content-Disposition'] = 'attachment; filename=breached_conversation.csv'
|
||||||
render layout: false, formats: [:csv]
|
render layout: false, formats: [:csv]
|
||||||
@@ -29,41 +30,38 @@ class Api::V1::Accounts::AppliedSlasController < Api::V1::Accounts::EnterpriseAc
|
|||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def breached_slas
|
|
||||||
@applied_slas.includes(:sla_policy).joins(:conversation)
|
|
||||||
.where.not(conversations: { status: :resolved })
|
|
||||||
.where(applied_slas: { sla_status: :missed })
|
|
||||||
end
|
|
||||||
|
|
||||||
def total_applied_slas
|
def total_applied_slas
|
||||||
@total_applied_slas ||= @applied_slas.count
|
@total_applied_slas ||= @applied_slas.count
|
||||||
end
|
end
|
||||||
|
|
||||||
def number_of_sla_breaches
|
def number_of_sla_misses
|
||||||
@number_of_sla_breaches ||= @applied_slas.missed.count
|
@number_of_sla_misses ||= missed_applied_slas.count
|
||||||
end
|
end
|
||||||
|
|
||||||
def hit_rate
|
def hit_rate
|
||||||
number_of_sla_breaches.zero? ? '100%' : "#{hit_rate_percentage}%"
|
number_of_sla_misses.zero? ? '100%' : "#{hit_rate_percentage}%"
|
||||||
end
|
end
|
||||||
|
|
||||||
def hit_rate_percentage
|
def hit_rate_percentage
|
||||||
((total_applied_slas - number_of_sla_breaches) / total_applied_slas.to_f * 100).round(2)
|
((total_applied_slas - number_of_sla_misses) / total_applied_slas.to_f * 100).round(2)
|
||||||
end
|
end
|
||||||
|
|
||||||
def set_applied_slas
|
def set_applied_slas
|
||||||
initial_query = Current.account.applied_slas.includes(:conversation)
|
initial_query = Current.account.applied_slas.includes(:conversation)
|
||||||
@applied_slas = initial_query
|
@applied_slas = apply_filters(initial_query)
|
||||||
.filter_by_date_range(range)
|
|
||||||
.filter_by_inbox_id(params[:inbox_id])
|
|
||||||
.filter_by_team_id(params[:team_id])
|
|
||||||
.filter_by_sla_policy_id(params[:sla_policy_id])
|
|
||||||
.filter_by_label_list(params[:label_list])
|
|
||||||
.filter_by_assigned_agent_id(params[:assigned_agent_id])
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def paginate_slas
|
def apply_filters(query)
|
||||||
@applied_slas = @applied_slas.page(@current_page).per(RESULTS_PER_PAGE)
|
query.filter_by_date_range(range)
|
||||||
|
.filter_by_inbox_id(params[:inbox_id])
|
||||||
|
.filter_by_team_id(params[:team_id])
|
||||||
|
.filter_by_sla_policy_id(params[:sla_policy_id])
|
||||||
|
.filter_by_label_list(params[:label_list])
|
||||||
|
.filter_by_assigned_agent_id(params[:assigned_agent_id])
|
||||||
|
end
|
||||||
|
|
||||||
|
def missed_applied_slas
|
||||||
|
@missed_applied_slas ||= @applied_slas.missed
|
||||||
end
|
end
|
||||||
|
|
||||||
def set_current_page
|
def set_current_page
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ class AppliedSla < ApplicationRecord
|
|||||||
joins(:conversation).where(conversations: { assigned_agent_id: assigned_agent_id })
|
joins(:conversation).where(conversations: { assigned_agent_id: assigned_agent_id })
|
||||||
end
|
end
|
||||||
}
|
}
|
||||||
scope :missed, -> { where(sla_status: :missed) }
|
scope :missed, -> { where(sla_status: %i[missed active_with_misses]) }
|
||||||
|
|
||||||
after_update_commit :push_conversation_event
|
after_update_commit :push_conversation_event
|
||||||
|
|
||||||
|
|||||||
@@ -10,8 +10,8 @@
|
|||||||
] %>
|
] %>
|
||||||
<%= CSV.generate_line headers %>
|
<%= CSV.generate_line headers %>
|
||||||
|
|
||||||
<% @breached_slas.each do |sla| %>
|
<% @missed_applied_slas.each do |sla| %>
|
||||||
<% breached_events = sla.sla_events.map(&:event_type).join(', ') %>
|
<% missed_events = sla.sla_events.map(&:event_type).join(', ') %>
|
||||||
<% conversation = sla.conversation %>
|
<% conversation = sla.conversation %>
|
||||||
<%= CSV.generate_line([
|
<%= CSV.generate_line([
|
||||||
conversation.display_id,
|
conversation.display_id,
|
||||||
@@ -21,6 +21,6 @@
|
|||||||
conversation.inbox&.name,
|
conversation.inbox&.name,
|
||||||
conversation.cached_label_list,
|
conversation.cached_label_list,
|
||||||
app_account_conversation_url(account_id: conversation.account_id, id: conversation.display_id),
|
app_account_conversation_url(account_id: conversation.account_id, id: conversation.display_id),
|
||||||
breached_events
|
missed_events
|
||||||
]) %>
|
]) %>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|||||||
@@ -1,14 +1,22 @@
|
|||||||
json.array! @applied_slas do |applied_sla|
|
json.payload do
|
||||||
json.id applied_sla.id
|
json.array! @applied_slas do |applied_sla|
|
||||||
json.sla_policy_id applied_sla.sla_policy_id
|
json.applied_sla applied_sla.push_event_data
|
||||||
json.conversation_id applied_sla.conversation_id
|
json.conversation do
|
||||||
json.sla_status applied_sla.sla_status
|
conversation = applied_sla.conversation
|
||||||
json.created_at applied_sla.created_at
|
json.id conversation.id
|
||||||
json.updated_at applied_sla.updated_at
|
json.contact do
|
||||||
json.conversation do
|
json.name conversation.contact.name if conversation.contact
|
||||||
json.partial! 'api/v1/models/conversation', conversation: applied_sla.conversation
|
end
|
||||||
end
|
json.labels conversation.cached_label_list
|
||||||
json.sla_events applied_sla.sla_events do |sla_event|
|
json.assignee conversation.assignee.push_event_data if conversation.assignee
|
||||||
json.partial! 'api/v1/models/sla_event', formats: [:json], sla_event: sla_event
|
end
|
||||||
|
json.sla_events applied_sla.sla_events do |sla_event|
|
||||||
|
json.partial! 'api/v1/models/sla_event', formats: [:json], sla_event: sla_event
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
json.meta do
|
||||||
|
json.count @count
|
||||||
|
json.current_page @current_page
|
||||||
|
end
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
json.total_applied_slas @total_applied_slas
|
json.total_applied_slas @total_applied_slas
|
||||||
json.number_of_sla_breaches @number_of_sla_breaches
|
json.number_of_sla_misses @number_of_sla_misses
|
||||||
json.hit_rate @hit_rate
|
json.hit_rate @hit_rate
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
"size-limit": [
|
"size-limit": [
|
||||||
{
|
{
|
||||||
"path": "public/packs/js/widget-*.js",
|
"path": "public/packs/js/widget-*.js",
|
||||||
"limit": "280 KB"
|
"limit": "281 KB"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "public/packs/js/sdk.js",
|
"path": "public/packs/js/sdk.js",
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ RSpec.describe 'Applied SLAs API', type: :request do
|
|||||||
body = JSON.parse(response.body)
|
body = JSON.parse(response.body)
|
||||||
|
|
||||||
expect(body).to include('total_applied_slas' => 1)
|
expect(body).to include('total_applied_slas' => 1)
|
||||||
expect(body).to include('number_of_sla_breaches' => 1)
|
expect(body).to include('number_of_sla_misses' => 1)
|
||||||
expect(body).to include('hit_rate' => '0.0%')
|
expect(body).to include('hit_rate' => '0.0%')
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -48,7 +48,7 @@ RSpec.describe 'Applied SLAs API', type: :request do
|
|||||||
body = JSON.parse(response.body)
|
body = JSON.parse(response.body)
|
||||||
|
|
||||||
expect(body).to include('total_applied_slas' => 1)
|
expect(body).to include('total_applied_slas' => 1)
|
||||||
expect(body).to include('number_of_sla_breaches' => 0)
|
expect(body).to include('number_of_sla_misses' => 0)
|
||||||
expect(body).to include('hit_rate' => '100%')
|
expect(body).to include('hit_rate' => '100%')
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -64,7 +64,7 @@ RSpec.describe 'Applied SLAs API', type: :request do
|
|||||||
body = JSON.parse(response.body)
|
body = JSON.parse(response.body)
|
||||||
|
|
||||||
expect(body).to include('total_applied_slas' => 3)
|
expect(body).to include('total_applied_slas' => 3)
|
||||||
expect(body).to include('number_of_sla_breaches' => 1)
|
expect(body).to include('number_of_sla_misses' => 1)
|
||||||
expect(body).to include('hit_rate' => '66.67%')
|
expect(body).to include('hit_rate' => '66.67%')
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -80,7 +80,7 @@ RSpec.describe 'Applied SLAs API', type: :request do
|
|||||||
body = JSON.parse(response.body)
|
body = JSON.parse(response.body)
|
||||||
|
|
||||||
expect(body).to include('total_applied_slas' => 2)
|
expect(body).to include('total_applied_slas' => 2)
|
||||||
expect(body).to include('number_of_sla_breaches' => 1)
|
expect(body).to include('number_of_sla_misses' => 1)
|
||||||
expect(body).to include('hit_rate' => '50.0%')
|
expect(body).to include('hit_rate' => '50.0%')
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -98,7 +98,7 @@ RSpec.describe 'Applied SLAs API', type: :request do
|
|||||||
body = JSON.parse(response.body)
|
body = JSON.parse(response.body)
|
||||||
|
|
||||||
expect(body).to include('total_applied_slas' => 2)
|
expect(body).to include('total_applied_slas' => 2)
|
||||||
expect(body).to include('number_of_sla_breaches' => 1)
|
expect(body).to include('number_of_sla_misses' => 1)
|
||||||
expect(body).to include('hit_rate' => '50.0%')
|
expect(body).to include('hit_rate' => '50.0%')
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -128,7 +128,7 @@ RSpec.describe 'Applied SLAs API', type: :request do
|
|||||||
|
|
||||||
csv_data = CSV.parse(response.body)
|
csv_data = CSV.parse(response.body)
|
||||||
csv_data.reject! { |row| row.all?(&:nil?) }
|
csv_data.reject! { |row| row.all?(&:nil?) }
|
||||||
expect(csv_data.size).to eq(2)
|
expect(csv_data.size).to eq(3)
|
||||||
expect(csv_data[1][0].to_i).to eq(conversation1.display_id)
|
expect(csv_data[1][0].to_i).to eq(conversation1.display_id)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -145,21 +145,20 @@ RSpec.describe 'Applied SLAs API', type: :request do
|
|||||||
context 'when it is an authenticated user' do
|
context 'when it is an authenticated user' do
|
||||||
it 'returns the applied slas' do
|
it 'returns the applied slas' do
|
||||||
create(:applied_sla, sla_policy: sla_policy1, conversation: conversation1)
|
create(:applied_sla, sla_policy: sla_policy1, conversation: conversation1)
|
||||||
create(:applied_sla, sla_policy: sla_policy1, conversation: conversation2)
|
create(:applied_sla, sla_policy: sla_policy1, conversation: conversation2, sla_status: 'missed')
|
||||||
get "/api/v1/accounts/#{account.id}/applied_slas",
|
get "/api/v1/accounts/#{account.id}/applied_slas",
|
||||||
headers: administrator.create_new_auth_token
|
headers: administrator.create_new_auth_token
|
||||||
expect(response).to have_http_status(:success)
|
expect(response).to have_http_status(:success)
|
||||||
body = JSON.parse(response.body)
|
body = JSON.parse(response.body)
|
||||||
|
expect(body['payload'].size).to eq(1)
|
||||||
expect(body.size).to eq(2)
|
expect(body['payload'].first).to include('applied_sla')
|
||||||
expect(body.first).to include('id')
|
expect(body['payload'].first['conversation']['id']).to eq(conversation2.id)
|
||||||
expect(body.first).to include('sla_policy_id' => sla_policy1.id)
|
expect(body['meta']).to include('count' => 1)
|
||||||
expect(body.first).to include('conversation_id' => conversation1.id)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'filters applied slas based on a date range' do
|
it 'filters applied slas based on a date range' do
|
||||||
create(:applied_sla, sla_policy: sla_policy1, conversation: conversation1, created_at: 10.days.ago)
|
create(:applied_sla, sla_policy: sla_policy1, conversation: conversation1, created_at: 10.days.ago, sla_status: 'missed')
|
||||||
create(:applied_sla, sla_policy: sla_policy1, conversation: conversation2, created_at: 3.days.ago)
|
create(:applied_sla, sla_policy: sla_policy1, conversation: conversation2, created_at: 3.days.ago, sla_status: 'missed')
|
||||||
|
|
||||||
get "/api/v1/accounts/#{account.id}/applied_slas",
|
get "/api/v1/accounts/#{account.id}/applied_slas",
|
||||||
params: { since: 5.days.ago.to_time.to_i.to_s, until: Time.zone.today.to_time.to_i.to_s },
|
params: { since: 5.days.ago.to_time.to_i.to_s, until: Time.zone.today.to_time.to_i.to_s },
|
||||||
@@ -167,13 +166,13 @@ RSpec.describe 'Applied SLAs API', type: :request do
|
|||||||
expect(response).to have_http_status(:success)
|
expect(response).to have_http_status(:success)
|
||||||
body = JSON.parse(response.body)
|
body = JSON.parse(response.body)
|
||||||
|
|
||||||
expect(body.size).to eq(1)
|
expect(body['payload'].size).to eq(1)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'filters applied slas based on a date range and agent ids' do
|
it 'filters applied slas based on a date range and agent ids' do
|
||||||
create(:applied_sla, sla_policy: sla_policy1, conversation: conversation1, created_at: 10.days.ago)
|
create(:applied_sla, sla_policy: sla_policy1, conversation: conversation1, created_at: 10.days.ago)
|
||||||
create(:applied_sla, sla_policy: sla_policy1, conversation: conversation3, created_at: 3.days.ago)
|
create(:applied_sla, sla_policy: sla_policy1, conversation: conversation3, created_at: 3.days.ago, sla_status: 'missed')
|
||||||
create(:applied_sla, sla_policy: sla_policy1, conversation: conversation2, created_at: 3.days.ago)
|
create(:applied_sla, sla_policy: sla_policy1, conversation: conversation2, created_at: 3.days.ago, sla_status: 'active_with_misses')
|
||||||
|
|
||||||
get "/api/v1/accounts/#{account.id}/applied_slas",
|
get "/api/v1/accounts/#{account.id}/applied_slas",
|
||||||
params: { agent_ids: [agent2.id] },
|
params: { agent_ids: [agent2.id] },
|
||||||
@@ -181,13 +180,13 @@ RSpec.describe 'Applied SLAs API', type: :request do
|
|||||||
expect(response).to have_http_status(:success)
|
expect(response).to have_http_status(:success)
|
||||||
body = JSON.parse(response.body)
|
body = JSON.parse(response.body)
|
||||||
|
|
||||||
expect(body.size).to eq(3)
|
expect(body['payload'].size).to eq(2)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'filters applied slas based on sla policy ids' do
|
it 'filters applied slas based on sla policy ids' do
|
||||||
create(:applied_sla, sla_policy: sla_policy1, conversation: conversation1)
|
create(:applied_sla, sla_policy: sla_policy1, conversation: conversation1, sla_status: 'missed')
|
||||||
create(:applied_sla, sla_policy: sla_policy1, conversation: conversation2)
|
create(:applied_sla, sla_policy: sla_policy1, conversation: conversation2)
|
||||||
create(:applied_sla, sla_policy: sla_policy2, conversation: conversation2)
|
create(:applied_sla, sla_policy: sla_policy2, conversation: conversation2, sla_status: 'active_with_misses')
|
||||||
|
|
||||||
get "/api/v1/accounts/#{account.id}/applied_slas",
|
get "/api/v1/accounts/#{account.id}/applied_slas",
|
||||||
params: { sla_policy_id: sla_policy1.id },
|
params: { sla_policy_id: sla_policy1.id },
|
||||||
@@ -195,15 +194,15 @@ RSpec.describe 'Applied SLAs API', type: :request do
|
|||||||
expect(response).to have_http_status(:success)
|
expect(response).to have_http_status(:success)
|
||||||
body = JSON.parse(response.body)
|
body = JSON.parse(response.body)
|
||||||
|
|
||||||
expect(body.size).to eq(2)
|
expect(body['payload'].size).to eq(1)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'filters applied slas based on labels' do
|
it 'filters applied slas based on labels' do
|
||||||
conversation2.update_labels('label1')
|
conversation2.update_labels('label1')
|
||||||
conversation3.update_labels('label1')
|
conversation3.update_labels('label1')
|
||||||
create(:applied_sla, sla_policy: sla_policy1, conversation: conversation1, created_at: 10.days.ago)
|
create(:applied_sla, sla_policy: sla_policy1, conversation: conversation1, created_at: 10.days.ago, sla_status: 'active_with_misses')
|
||||||
create(:applied_sla, sla_policy: sla_policy1, conversation: conversation2, created_at: 3.days.ago)
|
create(:applied_sla, sla_policy: sla_policy1, conversation: conversation2, created_at: 3.days.ago, sla_status: 'missed')
|
||||||
create(:applied_sla, sla_policy: sla_policy1, conversation: conversation3, created_at: 3.days.ago)
|
create(:applied_sla, sla_policy: sla_policy1, conversation: conversation3, created_at: 3.days.ago, sla_status: 'missed')
|
||||||
|
|
||||||
get "/api/v1/accounts/#{account.id}/applied_slas",
|
get "/api/v1/accounts/#{account.id}/applied_slas",
|
||||||
params: { label_list: ['label1'] },
|
params: { label_list: ['label1'] },
|
||||||
@@ -211,7 +210,7 @@ RSpec.describe 'Applied SLAs API', type: :request do
|
|||||||
expect(response).to have_http_status(:success)
|
expect(response).to have_http_status(:success)
|
||||||
body = JSON.parse(response.body)
|
body = JSON.parse(response.body)
|
||||||
|
|
||||||
expect(body.size).to eq(2)
|
expect(body['payload'].size).to eq(2)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
Reference in New Issue
Block a user