feat: SLA report filter (#9218)
Co-authored-by: Shivam Mishra <scm.mymail@gmail.com> Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
This commit is contained in:
@@ -13,6 +13,7 @@ class SLAReportsAPI extends ApiClient {
|
|||||||
inbox_id,
|
inbox_id,
|
||||||
team_id,
|
team_id,
|
||||||
sla_policy_id,
|
sla_policy_id,
|
||||||
|
label_list,
|
||||||
page,
|
page,
|
||||||
} = {}) {
|
} = {}) {
|
||||||
return axios.get(this.url, {
|
return axios.get(this.url, {
|
||||||
@@ -23,6 +24,7 @@ class SLAReportsAPI extends ApiClient {
|
|||||||
inbox_id,
|
inbox_id,
|
||||||
team_id,
|
team_id,
|
||||||
sla_policy_id,
|
sla_policy_id,
|
||||||
|
label_list,
|
||||||
page,
|
page,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -54,6 +56,7 @@ class SLAReportsAPI extends ApiClient {
|
|||||||
assigned_agent_id,
|
assigned_agent_id,
|
||||||
inbox_id,
|
inbox_id,
|
||||||
team_id,
|
team_id,
|
||||||
|
label_list,
|
||||||
sla_policy_id,
|
sla_policy_id,
|
||||||
} = {}) {
|
} = {}) {
|
||||||
return axios.get(`${this.url}/metrics`, {
|
return axios.get(`${this.url}/metrics`, {
|
||||||
@@ -62,6 +65,7 @@ class SLAReportsAPI extends ApiClient {
|
|||||||
until: to,
|
until: to,
|
||||||
assigned_agent_id,
|
assigned_agent_id,
|
||||||
inbox_id,
|
inbox_id,
|
||||||
|
label_list,
|
||||||
team_id,
|
team_id,
|
||||||
sla_policy_id,
|
sla_policy_id,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ describe('#SLAReports API', () => {
|
|||||||
inbox_id: 1,
|
inbox_id: 1,
|
||||||
team_id: 1,
|
team_id: 1,
|
||||||
sla_policy_id: 1,
|
sla_policy_id: 1,
|
||||||
|
label_list: ['label1'],
|
||||||
});
|
});
|
||||||
expect(axiosMock.get).toHaveBeenCalledWith('/api/v1/applied_slas', {
|
expect(axiosMock.get).toHaveBeenCalledWith('/api/v1/applied_slas', {
|
||||||
params: {
|
params: {
|
||||||
@@ -45,6 +46,7 @@ describe('#SLAReports API', () => {
|
|||||||
inbox_id: 1,
|
inbox_id: 1,
|
||||||
team_id: 1,
|
team_id: 1,
|
||||||
sla_policy_id: 1,
|
sla_policy_id: 1,
|
||||||
|
label_list: ['label1'],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -56,6 +58,7 @@ describe('#SLAReports API', () => {
|
|||||||
inbox_id: 1,
|
inbox_id: 1,
|
||||||
team_id: 1,
|
team_id: 1,
|
||||||
sla_policy_id: 1,
|
sla_policy_id: 1,
|
||||||
|
label_list: ['label1'],
|
||||||
});
|
});
|
||||||
expect(axiosMock.get).toHaveBeenCalledWith(
|
expect(axiosMock.get).toHaveBeenCalledWith(
|
||||||
'/api/v1/applied_slas/metrics',
|
'/api/v1/applied_slas/metrics',
|
||||||
@@ -67,6 +70,7 @@ describe('#SLAReports API', () => {
|
|||||||
inbox_id: 1,
|
inbox_id: 1,
|
||||||
team_id: 1,
|
team_id: 1,
|
||||||
sla_policy_id: 1,
|
sla_policy_id: 1,
|
||||||
|
label_list: ['label1'],
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,6 +1,13 @@
|
|||||||
@import '~vue2-datepicker/scss/index';
|
@import '~vue2-datepicker/scss/index';
|
||||||
|
|
||||||
.date-picker {
|
.date-picker {
|
||||||
|
// To be removed one SLA reports date picker is created
|
||||||
|
&.small {
|
||||||
|
.mx-input {
|
||||||
|
@apply h-8 text-sm;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&.no-margin {
|
&.no-margin {
|
||||||
.mx-input {
|
.mx-input {
|
||||||
@apply mb-0;
|
@apply mb-0;
|
||||||
|
|||||||
@@ -153,6 +153,29 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.multiselect-wrap--small {
|
.multiselect-wrap--small {
|
||||||
|
// To be removed one SLA reports date picker is created
|
||||||
|
&.tiny {
|
||||||
|
.multiselect.no-margin {
|
||||||
|
@apply min-h-[32px];
|
||||||
|
}
|
||||||
|
|
||||||
|
.multiselect__select {
|
||||||
|
@apply min-h-[32px] h-8;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
@apply top-[60%];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.multiselect__tags {
|
||||||
|
@apply min-h-[32px] max-h-[32px];
|
||||||
|
|
||||||
|
.multiselect__single {
|
||||||
|
@apply pt-1 pb-1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.multiselect__tags,
|
.multiselect__tags,
|
||||||
.multiselect__input,
|
.multiselect__input,
|
||||||
.multiselect {
|
.multiselect {
|
||||||
|
|||||||
@@ -82,7 +82,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed, defineEmits } from 'vue';
|
import { computed } from 'vue';
|
||||||
|
|
||||||
// Props
|
// Props
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
|
|||||||
@@ -138,7 +138,11 @@
|
|||||||
"groupBy": "Year"
|
"groupBy": "Year"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"BUSINESS_HOURS": "Business Hours"
|
"BUSINESS_HOURS": "Business Hours",
|
||||||
|
"FILTER_ACTIONS": {
|
||||||
|
"CLEAR_FILTER": "Clear filter",
|
||||||
|
"EMPTY_LIST": "No results found"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"AGENT_REPORTS": {
|
"AGENT_REPORTS": {
|
||||||
"HEADER": "Agents Overview",
|
"HEADER": "Agents Overview",
|
||||||
@@ -512,6 +516,26 @@
|
|||||||
"LOADING": "Loading SLA data...",
|
"LOADING": "Loading SLA data...",
|
||||||
"DOWNLOAD_SLA_REPORTS": "Download SLA reports",
|
"DOWNLOAD_SLA_REPORTS": "Download SLA reports",
|
||||||
"DOWNLOAD_FAILED": "Failed to download SLA Reports",
|
"DOWNLOAD_FAILED": "Failed to download SLA Reports",
|
||||||
|
"DROPDOWN": {
|
||||||
|
"ADD_FIlTER": "Add filter",
|
||||||
|
"CLEAR_ALL": "Clear all",
|
||||||
|
"CLEAR_FILTER": "Clear filter",
|
||||||
|
"EMPTY_LIST": "No results found",
|
||||||
|
"NO_FILTER": "No filters available",
|
||||||
|
"SEARCH": "Search filter",
|
||||||
|
"INPUT_PLACEHOLDER": {
|
||||||
|
"SLA": "SLA name",
|
||||||
|
"AGENTS": "Agent name",
|
||||||
|
"INBOXES": "Inbox name",
|
||||||
|
"LABELS": "Label name",
|
||||||
|
"TEAMS": "Team name"
|
||||||
|
},
|
||||||
|
"SLA": "SLA Policy",
|
||||||
|
"INBOXES": "Inbox",
|
||||||
|
"AGENTS": "Agent",
|
||||||
|
"LABELS": "Label",
|
||||||
|
"TEAMS": "Team"
|
||||||
|
},
|
||||||
"METRICS": {
|
"METRICS": {
|
||||||
"HIT_RATE": {
|
"HIT_RATE": {
|
||||||
"LABEL": "Hit Rate",
|
"LABEL": "Hit Rate",
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col flex-1 px-4 pt-4 overflow-auto">
|
<div class="flex flex-col flex-1 gap-6 px-4 pt-4 overflow-auto">
|
||||||
<SLAReportFilters @filter-change="onFilterChange" />
|
<SLAReportFilters @filter-change="onFilterChange" />
|
||||||
<woot-button
|
<woot-button
|
||||||
color-scheme="success"
|
color-scheme="success"
|
||||||
@@ -44,8 +44,15 @@ export default {
|
|||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
pageNumber: 1,
|
pageNumber: 1,
|
||||||
from: 0,
|
activeFilter: {
|
||||||
to: 0,
|
from: 0,
|
||||||
|
to: 0,
|
||||||
|
assigned_agent_id: null,
|
||||||
|
inbox_id: null,
|
||||||
|
team_id: null,
|
||||||
|
sla_policy_id: null,
|
||||||
|
label_list: null,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@@ -57,6 +64,11 @@ export default {
|
|||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
|
this.$store.dispatch('agents/get');
|
||||||
|
this.$store.dispatch('inboxes/get');
|
||||||
|
this.$store.dispatch('teams/get');
|
||||||
|
this.$store.dispatch('labels/get');
|
||||||
|
this.$store.dispatch('sla/get');
|
||||||
this.fetchSLAMetrics();
|
this.fetchSLAMetrics();
|
||||||
this.fetchSLAReports();
|
this.fetchSLAReports();
|
||||||
},
|
},
|
||||||
@@ -64,22 +76,17 @@ export default {
|
|||||||
fetchSLAReports({ pageNumber } = {}) {
|
fetchSLAReports({ pageNumber } = {}) {
|
||||||
this.$store.dispatch('slaReports/get', {
|
this.$store.dispatch('slaReports/get', {
|
||||||
page: pageNumber || this.pageNumber,
|
page: pageNumber || this.pageNumber,
|
||||||
from: this.from,
|
...this.activeFilter,
|
||||||
to: this.to,
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
fetchSLAMetrics() {
|
fetchSLAMetrics() {
|
||||||
this.$store.dispatch('slaReports/getMetrics', {
|
this.$store.dispatch('slaReports/getMetrics', this.activeFilter);
|
||||||
from: this.from,
|
|
||||||
to: this.to,
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
onPageChange(pageNumber) {
|
onPageChange(pageNumber) {
|
||||||
this.fetchSLAReports({ pageNumber });
|
this.fetchSLAReports({ pageNumber });
|
||||||
},
|
},
|
||||||
onFilterChange({ from, to }) {
|
onFilterChange(params) {
|
||||||
this.from = from;
|
this.activeFilter = params;
|
||||||
this.to = to;
|
|
||||||
this.fetchSLAReports();
|
this.fetchSLAReports();
|
||||||
this.fetchSLAMetrics();
|
this.fetchSLAMetrics();
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,73 @@
|
|||||||
|
<script setup>
|
||||||
|
import FilterButton from './FilterButton.vue';
|
||||||
|
import FilterListDropdown from './FilterListDropdown.vue';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
name: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
id: {
|
||||||
|
type: Number,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
type: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
activeFilterType: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
showMenu: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
placeholder: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
enableSearch: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits([
|
||||||
|
'toggleDropdown',
|
||||||
|
'removeFilter',
|
||||||
|
'addFilter',
|
||||||
|
'closeDropdown',
|
||||||
|
]);
|
||||||
|
const toggleDropdown = () => emit('toggleDropdown', props.type);
|
||||||
|
const removeFilter = () => emit('removeFilter', props.type);
|
||||||
|
const addFilter = item => emit('addFilter', item);
|
||||||
|
const closeDropdown = () => emit('closeDropdown');
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<filter-button
|
||||||
|
right-icon="chevron-down"
|
||||||
|
:button-text="name"
|
||||||
|
class="bg-slate-50 dark:bg-slate-800 hover:bg-slate-75 dark:hover:bg-slate-800"
|
||||||
|
@click="toggleDropdown"
|
||||||
|
>
|
||||||
|
<template v-if="showMenu && activeFilterType === type" #dropdown>
|
||||||
|
<filter-list-dropdown
|
||||||
|
v-if="options"
|
||||||
|
v-on-clickaway="closeDropdown"
|
||||||
|
:list-items="options"
|
||||||
|
:active-filter-id="id"
|
||||||
|
:input-placeholder="placeholder"
|
||||||
|
:enable-search="enableSearch"
|
||||||
|
class="flex flex-col w-[240px] overflow-y-auto left-0 md:left-auto md:right-0 top-10"
|
||||||
|
@click="addFilter"
|
||||||
|
@removeFilter="removeFilter"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</filter-button>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
<script setup>
|
||||||
|
import FilterButton from './FilterButton.vue';
|
||||||
|
import FilterListDropdown from './FilterListDropdown.vue';
|
||||||
|
import FilterListItemButton from './FilterListItemButton.vue';
|
||||||
|
import FilterDropdownEmptyState from './FilterDropdownEmptyState.vue';
|
||||||
|
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
name: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
menuOption: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
showMenu: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
placeholderI18nKey: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
enableSearch: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
emptyStateMessage: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const hoveredItemId = ref(null);
|
||||||
|
|
||||||
|
const showSubMenu = id => {
|
||||||
|
hoveredItemId.value = id;
|
||||||
|
};
|
||||||
|
|
||||||
|
const hideSubMenu = () => {
|
||||||
|
hoveredItemId.value = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const isHovered = id => hoveredItemId.value === id;
|
||||||
|
|
||||||
|
const emit = defineEmits(['toggleDropdown', 'addFilter', 'closeDropdown']);
|
||||||
|
const toggleDropdown = () => emit('toggleDropdown');
|
||||||
|
const addFilter = item => {
|
||||||
|
emit('addFilter', item);
|
||||||
|
hideSubMenu();
|
||||||
|
};
|
||||||
|
const closeDropdown = () => {
|
||||||
|
hideSubMenu();
|
||||||
|
emit('closeDropdown');
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<filter-button :button-text="name" left-icon="filter" @click="toggleDropdown">
|
||||||
|
<!-- Dropdown with search and sub-dropdown -->
|
||||||
|
<template v-if="showMenu" #dropdown>
|
||||||
|
<filter-list-dropdown
|
||||||
|
v-on-clickaway="closeDropdown"
|
||||||
|
class="left-0 md:right-0 top-10"
|
||||||
|
>
|
||||||
|
<template #listItem>
|
||||||
|
<filter-dropdown-empty-state
|
||||||
|
v-if="!menuOption.length"
|
||||||
|
:message="emptyStateMessage"
|
||||||
|
/>
|
||||||
|
<filter-list-item-button
|
||||||
|
v-for="item in menuOption"
|
||||||
|
:key="item.id"
|
||||||
|
:button-text="item.name"
|
||||||
|
@mouseenter="showSubMenu(item.id)"
|
||||||
|
@mouseleave="hideSubMenu"
|
||||||
|
@focus="showSubMenu(item.id)"
|
||||||
|
>
|
||||||
|
<!-- Submenu with search and clear button -->
|
||||||
|
<template v-if="item.options && isHovered(item.id)" #dropdown>
|
||||||
|
<filter-list-dropdown
|
||||||
|
:list-items="item.options"
|
||||||
|
:input-placeholder="
|
||||||
|
$t(`${placeholderI18nKey}.${item.type.toUpperCase()}`)
|
||||||
|
"
|
||||||
|
:enable-search="enableSearch"
|
||||||
|
class="flex flex-col w-[216px] overflow-y-auto top-0 left-36"
|
||||||
|
@click="addFilter"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</filter-list-item-button>
|
||||||
|
</template>
|
||||||
|
</filter-list-dropdown>
|
||||||
|
</template>
|
||||||
|
</filter-button>
|
||||||
|
</template>
|
||||||
@@ -4,23 +4,44 @@ defineProps({
|
|||||||
type: String,
|
type: String,
|
||||||
default: '',
|
default: '',
|
||||||
},
|
},
|
||||||
|
rightIcon: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
leftIcon: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<button
|
<button
|
||||||
class="inline-flex relative items-center p-1.5 w-fit h-8 gap-1.5 bg-white dark:bg-slate-900 rounded-lg hover:bg-slate-50 dark:hover:bg-slate-800 active:bg-slate-75 dark:active:bg-slate-800"
|
class="inline-flex relative items-center p-1.5 w-fit h-8 gap-1.5 rounded-lg hover:bg-slate-50 dark:hover:bg-slate-800 active:bg-slate-75 dark:active:bg-slate-800"
|
||||||
@click="$emit('click')"
|
@click="$emit('click')"
|
||||||
>
|
>
|
||||||
<slot name="leftIcon" />
|
<slot name="leftIcon">
|
||||||
|
<fluent-icon
|
||||||
|
v-if="leftIcon"
|
||||||
|
:icon="leftIcon"
|
||||||
|
size="18"
|
||||||
|
class="flex-shrink-0 text-slate-900 dark:text-slate-50"
|
||||||
|
/>
|
||||||
|
</slot>
|
||||||
<span
|
<span
|
||||||
v-if="buttonText"
|
v-if="buttonText"
|
||||||
class="text-sm font-medium text-slate-900 dark:text-slate-50"
|
class="text-sm font-medium truncate text-slate-900 dark:text-slate-50"
|
||||||
>
|
>
|
||||||
{{ buttonText }}
|
{{ buttonText }}
|
||||||
</span>
|
</span>
|
||||||
<slot name="rightIcon" />
|
<slot name="rightIcon">
|
||||||
<div v-if="$slots.dropdown" class="absolute right-0 top-10" @click.stop>
|
<fluent-icon
|
||||||
<slot name="dropdown" />
|
v-if="rightIcon"
|
||||||
</div>
|
:icon="rightIcon"
|
||||||
|
size="18"
|
||||||
|
class="flex-shrink-0 text-slate-900 dark:text-slate-50"
|
||||||
|
/>
|
||||||
|
</slot>
|
||||||
|
|
||||||
|
<slot name="dropdown" />
|
||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,9 +1,5 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
defineProps({
|
defineProps({
|
||||||
buttonText: {
|
|
||||||
type: String,
|
|
||||||
default: '',
|
|
||||||
},
|
|
||||||
inputValue: {
|
inputValue: {
|
||||||
type: String,
|
type: String,
|
||||||
default: '',
|
default: '',
|
||||||
@@ -18,7 +14,7 @@ defineProps({
|
|||||||
<div
|
<div
|
||||||
class="flex items-center justify-between h-10 min-h-[40px] sticky top-0 bg-white z-10 dark:bg-slate-800 gap-2 px-3 border-b rounded-t-xl border-slate-50 dark:border-slate-700"
|
class="flex items-center justify-between h-10 min-h-[40px] sticky top-0 bg-white z-10 dark:bg-slate-800 gap-2 px-3 border-b rounded-t-xl border-slate-50 dark:border-slate-700"
|
||||||
>
|
>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center w-full gap-2">
|
||||||
<fluent-icon
|
<fluent-icon
|
||||||
icon="search"
|
icon="search"
|
||||||
size="18"
|
size="18"
|
||||||
@@ -32,14 +28,16 @@ defineProps({
|
|||||||
@input="$emit('input', $event.target.value)"
|
@input="$emit('input', $event.target.value)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- Clear filter button -->
|
||||||
<woot-button
|
<woot-button
|
||||||
|
v-if="!inputValue"
|
||||||
size="small"
|
size="small"
|
||||||
variant="clear"
|
variant="clear"
|
||||||
color-scheme="primary"
|
color-scheme="primary"
|
||||||
class="!px-1 !py-1.5"
|
class="!px-1 !py-1.5"
|
||||||
@click="$emit('click')"
|
@click="$emit('click')"
|
||||||
>
|
>
|
||||||
{{ buttonText }}
|
{{ $t('REPORT.FILTER_ACTIONS.CLEAR_FILTER') }}
|
||||||
</woot-button>
|
</woot-button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -14,18 +14,14 @@ const props = defineProps({
|
|||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
inputButtonText: {
|
|
||||||
type: String,
|
|
||||||
default: '',
|
|
||||||
},
|
|
||||||
emptyListMessage: {
|
|
||||||
type: String,
|
|
||||||
default: '',
|
|
||||||
},
|
|
||||||
inputPlaceholder: {
|
inputPlaceholder: {
|
||||||
type: String,
|
type: String,
|
||||||
default: '',
|
default: '',
|
||||||
},
|
},
|
||||||
|
activeFilterId: {
|
||||||
|
type: Number,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const searchTerm = ref('');
|
const searchTerm = ref('');
|
||||||
@@ -42,29 +38,35 @@ const filteredListItems = computed(() => {
|
|||||||
const isDropdownListEmpty = computed(() => {
|
const isDropdownListEmpty = computed(() => {
|
||||||
return !filteredListItems.value.length;
|
return !filteredListItems.value.length;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const isFilterActive = id => {
|
||||||
|
if (!props.activeFilterId) return false;
|
||||||
|
return id === props.activeFilterId;
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="z-20 w-40 bg-white border shadow dark:bg-slate-800 rounded-xl border-slate-50 dark:border-slate-700/50 max-h-72"
|
class="absolute z-20 w-40 bg-white border shadow dark:bg-slate-800 rounded-xl border-slate-50 dark:border-slate-700/50 max-h-[400px]"
|
||||||
|
@click.stop
|
||||||
>
|
>
|
||||||
<slot name="search">
|
<slot name="search">
|
||||||
<filter-dropdown-search
|
<filter-dropdown-search
|
||||||
v-if="enableSearch && listItems.length"
|
v-if="enableSearch && listItems.length"
|
||||||
:button-text="inputButtonText"
|
|
||||||
:input-value="searchTerm"
|
:input-value="searchTerm"
|
||||||
:input-placeholder="inputPlaceholder"
|
:input-placeholder="inputPlaceholder"
|
||||||
@input="onSearch"
|
@input="onSearch"
|
||||||
@click="onSearch('')"
|
@click="$emit('removeFilter')"
|
||||||
/>
|
/>
|
||||||
</slot>
|
</slot>
|
||||||
<slot name="listItem">
|
<slot name="listItem">
|
||||||
<filter-dropdown-empty-state
|
<filter-dropdown-empty-state
|
||||||
v-if="isDropdownListEmpty"
|
v-if="isDropdownListEmpty"
|
||||||
:message="emptyListMessage"
|
:message="$t('REPORT.FILTER_ACTIONS.EMPTY_LIST')"
|
||||||
/>
|
/>
|
||||||
<filter-list-item-button
|
<filter-list-item-button
|
||||||
v-for="item in filteredListItems"
|
v-for="item in filteredListItems"
|
||||||
:key="item.id"
|
:key="item.id"
|
||||||
|
:is-active="isFilterActive(item.id)"
|
||||||
:button-text="item.name"
|
:button-text="item.name"
|
||||||
@click="$emit('click', item)"
|
@click="$emit('click', item)"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ defineProps({
|
|||||||
<template>
|
<template>
|
||||||
<button
|
<button
|
||||||
class="relative inline-flex items-center justify-start w-full p-3 border-0 rounded-none first:rounded-t-xl last:rounded-b-xl h-11 hover:bg-slate-50 dark:hover:bg-slate-700 active:bg-slate-75 dark:active:bg-slate-800"
|
class="relative inline-flex items-center justify-start w-full p-3 border-0 rounded-none first:rounded-t-xl last:rounded-b-xl h-11 hover:bg-slate-50 dark:hover:bg-slate-700 active:bg-slate-75 dark:active:bg-slate-800"
|
||||||
@click="$emit('click')"
|
@click.stop="$emit('click')"
|
||||||
@mouseenter="$emit('mouseenter')"
|
@mouseenter="$emit('mouseenter')"
|
||||||
@mouseleave="$emit('mouseleave')"
|
@mouseleave="$emit('mouseleave')"
|
||||||
@focus="$emit('focus')"
|
@focus="$emit('focus')"
|
||||||
|
|||||||
@@ -0,0 +1,210 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="flex flex-col flex-wrap items-start gap-2 md:items-center md:flex-nowrap md:flex-row"
|
||||||
|
>
|
||||||
|
<!-- Active filters section -->
|
||||||
|
<div v-if="hasActiveFilters" class="flex flex-wrap gap-2 md:flex-nowrap">
|
||||||
|
<active-filter-chip
|
||||||
|
v-for="filter in activeFilters"
|
||||||
|
v-bind="filter"
|
||||||
|
:key="filter.type"
|
||||||
|
:placeholder="
|
||||||
|
$t(
|
||||||
|
`SLA_REPORTS.DROPDOWN.INPUT_PLACEHOLDER.${filter.type.toUpperCase()}`
|
||||||
|
)
|
||||||
|
"
|
||||||
|
:active-filter-type="activeFilterType"
|
||||||
|
:show-menu="showSubDropdownMenu"
|
||||||
|
enable-search
|
||||||
|
@toggleDropdown="openActiveFilterDropdown"
|
||||||
|
@closeDropdown="closeActiveFilterDropdown"
|
||||||
|
@addFilter="addFilter"
|
||||||
|
@removeFilter="removeFilter"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<!-- Dividing line between Active filters and Add filter button -->
|
||||||
|
<div
|
||||||
|
v-if="hasActiveFilters && !isAllFilterSelected"
|
||||||
|
class="w-full h-px border md:w-px md:h-5 border-slate-75 dark:border-slate-800"
|
||||||
|
/>
|
||||||
|
<!-- Add filter and clear filter button -->
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<add-filter-chip
|
||||||
|
v-if="!isAllFilterSelected"
|
||||||
|
placeholder-i18n-key="SLA_REPORTS.DROPDOWN.INPUT_PLACEHOLDER"
|
||||||
|
:name="$t('SLA_REPORTS.DROPDOWN.ADD_FIlTER')"
|
||||||
|
:menu-option="filterListMenuItems"
|
||||||
|
:show-menu="showDropdownMenu"
|
||||||
|
:empty-state-message="$t('SLA_REPORTS.DROPDOWN.NO_FILTER')"
|
||||||
|
@toggleDropdown="showDropdown"
|
||||||
|
@closeDropdown="closeDropdown"
|
||||||
|
@addFilter="addFilter"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Dividing line between Add filter and Clear all filter button -->
|
||||||
|
<div
|
||||||
|
v-if="hasActiveFilters"
|
||||||
|
class="w-px h-5 border border-slate-75 dark:border-slate-800"
|
||||||
|
/>
|
||||||
|
<!-- Clear all filter button -->
|
||||||
|
<filter-button
|
||||||
|
v-if="hasActiveFilters"
|
||||||
|
:button-text="$t('SLA_REPORTS.DROPDOWN.CLEAR_ALL')"
|
||||||
|
@click="clearAllFilters"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { mapGetters } from 'vuex';
|
||||||
|
import {
|
||||||
|
buildFilterList,
|
||||||
|
getActiveFilter,
|
||||||
|
getFilterType,
|
||||||
|
} from './helpers/SLAFilterHelpers';
|
||||||
|
import FilterButton from '../Filters/v3/FilterButton.vue';
|
||||||
|
import ActiveFilterChip from '../Filters/v3/ActiveFilterChip.vue';
|
||||||
|
import AddFilterChip from '../Filters/v3/AddFilterChip.vue';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
FilterButton,
|
||||||
|
ActiveFilterChip,
|
||||||
|
AddFilterChip,
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
showDropdownMenu: false,
|
||||||
|
showSubDropdownMenu: false,
|
||||||
|
activeFilterType: '',
|
||||||
|
appliedFilters: {
|
||||||
|
assigned_agent_id: null,
|
||||||
|
inbox_id: null,
|
||||||
|
team_id: null,
|
||||||
|
sla_policy_id: null,
|
||||||
|
label_list: null,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapGetters({
|
||||||
|
agents: 'agents/getAgents',
|
||||||
|
inboxes: 'inboxes/getInboxes',
|
||||||
|
teams: 'teams/getTeams',
|
||||||
|
labels: 'labels/getLabels',
|
||||||
|
sla: 'sla/getSLA',
|
||||||
|
}),
|
||||||
|
filterListMenuItems() {
|
||||||
|
const filterTypes = [
|
||||||
|
{ id: '1', name: this.$t('SLA_REPORTS.DROPDOWN.SLA'), type: 'sla' },
|
||||||
|
{
|
||||||
|
id: '2',
|
||||||
|
name: this.$t('SLA_REPORTS.DROPDOWN.INBOXES'),
|
||||||
|
type: 'inboxes',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '3',
|
||||||
|
name: this.$t('SLA_REPORTS.DROPDOWN.AGENTS'),
|
||||||
|
type: 'agents',
|
||||||
|
},
|
||||||
|
{ id: '4', name: this.$t('SLA_REPORTS.DROPDOWN.TEAMS'), type: 'teams' },
|
||||||
|
{
|
||||||
|
id: '5',
|
||||||
|
name: this.$t('SLA_REPORTS.DROPDOWN.LABELS'),
|
||||||
|
type: 'labels',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
// Filter out the active filters from the filter list
|
||||||
|
// We only want to show the filters that are not already applied
|
||||||
|
// In the add filter dropdown
|
||||||
|
const activeFilters = Object.keys(this.appliedFilters).filter(
|
||||||
|
key => this.appliedFilters[key]
|
||||||
|
);
|
||||||
|
const activeFilterTypes = activeFilters.map(key =>
|
||||||
|
getFilterType(key, 'keyToType')
|
||||||
|
);
|
||||||
|
return filterTypes
|
||||||
|
.filter(({ type }) => !activeFilterTypes.includes(type))
|
||||||
|
.map(({ id, name, type }) => ({
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
type,
|
||||||
|
options: buildFilterList(this[type], type),
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
activeFilters() {
|
||||||
|
// Get the active filters from the applied filters
|
||||||
|
// and return the filter name, type and options
|
||||||
|
const activeKey = Object.keys(this.appliedFilters).filter(
|
||||||
|
key => this.appliedFilters[key]
|
||||||
|
);
|
||||||
|
return activeKey.map(key => {
|
||||||
|
const filterType = getFilterType(key, 'keyToType');
|
||||||
|
const item = getActiveFilter(
|
||||||
|
this[filterType],
|
||||||
|
filterType,
|
||||||
|
this.appliedFilters[key]
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
id: item.id,
|
||||||
|
name: filterType === 'labels' ? item.title : item.name,
|
||||||
|
type: filterType,
|
||||||
|
options: buildFilterList(this[filterType], filterType),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
hasActiveFilters() {
|
||||||
|
return Object.values(this.appliedFilters).some(value => value !== null);
|
||||||
|
},
|
||||||
|
isAllFilterSelected() {
|
||||||
|
return !this.filterListMenuItems.length;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
addFilter(item) {
|
||||||
|
const { type, id, name } = item;
|
||||||
|
const filterKey = getFilterType(type, 'typeToKey');
|
||||||
|
this.appliedFilters[filterKey] = type === 'labels' ? name : id;
|
||||||
|
this.$emit('filter-change', this.appliedFilters);
|
||||||
|
this.resetDropdown();
|
||||||
|
},
|
||||||
|
removeFilter(type) {
|
||||||
|
const filterKey = getFilterType(type, 'typeToKey');
|
||||||
|
this.appliedFilters[filterKey] = null;
|
||||||
|
this.$emit('filter-change', this.appliedFilters);
|
||||||
|
},
|
||||||
|
clearAllFilters() {
|
||||||
|
this.appliedFilters = {
|
||||||
|
assigned_agent_id: null,
|
||||||
|
inbox_id: null,
|
||||||
|
team_id: null,
|
||||||
|
sla_policy_id: null,
|
||||||
|
label_list: null,
|
||||||
|
};
|
||||||
|
this.$emit('filter-change', this.appliedFilters);
|
||||||
|
this.resetDropdown();
|
||||||
|
},
|
||||||
|
showDropdown() {
|
||||||
|
this.showSubDropdownMenu = false;
|
||||||
|
this.showDropdownMenu = !this.showDropdownMenu;
|
||||||
|
},
|
||||||
|
closeDropdown() {
|
||||||
|
this.showDropdownMenu = false;
|
||||||
|
},
|
||||||
|
openActiveFilterDropdown(filterType) {
|
||||||
|
this.closeDropdown();
|
||||||
|
this.activeFilterType = filterType;
|
||||||
|
this.showSubDropdownMenu = !this.showSubDropdownMenu;
|
||||||
|
},
|
||||||
|
closeActiveFilterDropdown() {
|
||||||
|
this.activeFilterType = '';
|
||||||
|
this.showSubDropdownMenu = false;
|
||||||
|
},
|
||||||
|
resetDropdown() {
|
||||||
|
this.closeDropdown();
|
||||||
|
this.closeActiveFilterDropdown();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
@@ -1,22 +1,25 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col md:flex-row justify-between mb-4">
|
<div class="flex flex-col flex-wrap w-full gap-3 md:flex-row">
|
||||||
<div class="md:grid flex flex-col filter-container gap-3 w-full">
|
<reports-filters-date-range
|
||||||
<reports-filters-date-range @on-range-change="onDateRangeChange" />
|
class="sm:min-w-[200px] tiny h-8"
|
||||||
<woot-date-range-picker
|
@on-range-change="onDateRangeChange"
|
||||||
v-if="isDateRangeSelected"
|
/>
|
||||||
show-range
|
<woot-date-range-picker
|
||||||
class="no-margin auto-width"
|
v-if="isDateRangeSelected"
|
||||||
:value="customDateRange"
|
show-range
|
||||||
:confirm-text="$t('REPORT.CUSTOM_DATE_RANGE.CONFIRM')"
|
class="no-margin auto-width sm:min-w-[240px] small h-8"
|
||||||
:placeholder="$t('REPORT.CUSTOM_DATE_RANGE.PLACEHOLDER')"
|
:value="customDateRange"
|
||||||
@change="onCustomDateRangeChange"
|
:confirm-text="$t('REPORT.CUSTOM_DATE_RANGE.CONFIRM')"
|
||||||
/>
|
:placeholder="$t('REPORT.CUSTOM_DATE_RANGE.PLACEHOLDER')"
|
||||||
</div>
|
@change="onCustomDateRangeChange"
|
||||||
|
/>
|
||||||
|
<SLA-filter @filter-change="emitFilterChange" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
import WootDateRangePicker from 'dashboard/components/ui/DateRangePicker.vue';
|
import WootDateRangePicker from 'dashboard/components/ui/DateRangePicker.vue';
|
||||||
import ReportsFiltersDateRange from '../Filters/DateRange.vue';
|
import ReportsFiltersDateRange from '../Filters/DateRange.vue';
|
||||||
|
import SLAFilter from '../SLA/SLAFilter.vue';
|
||||||
import subDays from 'date-fns/subDays';
|
import subDays from 'date-fns/subDays';
|
||||||
import { DATE_RANGE_OPTIONS } from '../../constants';
|
import { DATE_RANGE_OPTIONS } from '../../constants';
|
||||||
import { getUnixStartOfDay, getUnixEndOfDay } from 'helpers/DateHelper';
|
import { getUnixStartOfDay, getUnixEndOfDay } from 'helpers/DateHelper';
|
||||||
@@ -25,6 +28,7 @@ export default {
|
|||||||
components: {
|
components: {
|
||||||
WootDateRangePicker,
|
WootDateRangePicker,
|
||||||
ReportsFiltersDateRange,
|
ReportsFiltersDateRange,
|
||||||
|
SLAFilter,
|
||||||
},
|
},
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
@@ -70,8 +74,13 @@ export default {
|
|||||||
this.$emit('filter-change', {
|
this.$emit('filter-change', {
|
||||||
from,
|
from,
|
||||||
to,
|
to,
|
||||||
|
...this.selectedGroupByFilter,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
emitFilterChange(params) {
|
||||||
|
this.selectedGroupByFilter = params;
|
||||||
|
this.emitChange();
|
||||||
|
},
|
||||||
onDateRangeChange(selectedRange) {
|
onDateRangeChange(selectedRange) {
|
||||||
this.selectedDateRange = selectedRange;
|
this.selectedDateRange = selectedRange;
|
||||||
this.emitChange();
|
this.emitChange();
|
||||||
@@ -83,9 +92,3 @@ export default {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.filter-container {
|
|
||||||
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
export const buildFilterList = (items, type) =>
|
||||||
|
// Build the filter list for the dropdown
|
||||||
|
items.map(item => ({
|
||||||
|
id: item.id,
|
||||||
|
name: type === 'labels' ? item.title : item.name,
|
||||||
|
type,
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const getActiveFilter = (filters, type, key) => {
|
||||||
|
// Method is used to get the active filter from the filter list
|
||||||
|
return filters.find(filterItem =>
|
||||||
|
type === 'labels'
|
||||||
|
? filterItem.title === key
|
||||||
|
: filterItem.id.toString() === key.toString()
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getFilterType = (input, direction) => {
|
||||||
|
// Method is used to map the filter key to the filter type
|
||||||
|
const filterMap = {
|
||||||
|
keyToType: {
|
||||||
|
assigned_agent_id: 'agents',
|
||||||
|
inbox_id: 'inboxes',
|
||||||
|
team_id: 'teams',
|
||||||
|
sla_policy_id: 'sla',
|
||||||
|
label_list: 'labels',
|
||||||
|
},
|
||||||
|
typeToKey: {
|
||||||
|
agents: 'assigned_agent_id',
|
||||||
|
inboxes: 'inbox_id',
|
||||||
|
teams: 'team_id',
|
||||||
|
sla: 'sla_policy_id',
|
||||||
|
labels: 'label_list',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return filterMap[direction][input];
|
||||||
|
};
|
||||||
@@ -31,6 +31,7 @@ import VueDOMPurifyHTML from 'vue-dompurify-html';
|
|||||||
import { domPurifyConfig } from '../shared/helpers/HTMLSanitizer';
|
import { domPurifyConfig } from '../shared/helpers/HTMLSanitizer';
|
||||||
import AnalyticsPlugin from '../dashboard/helper/AnalyticsHelper/plugin';
|
import AnalyticsPlugin from '../dashboard/helper/AnalyticsHelper/plugin';
|
||||||
import resizeDirective from '../dashboard/helper/directives/resize.js';
|
import resizeDirective from '../dashboard/helper/directives/resize.js';
|
||||||
|
import { directive as onClickaway } from 'vue-clickaway';
|
||||||
|
|
||||||
Vue.config.env = process.env;
|
Vue.config.env = process.env;
|
||||||
|
|
||||||
@@ -80,6 +81,7 @@ Vue.component('woot-wizard', WootWizard);
|
|||||||
Vue.component('fluent-icon', FluentIcon);
|
Vue.component('fluent-icon', FluentIcon);
|
||||||
|
|
||||||
Vue.directive('resize', resizeDirective);
|
Vue.directive('resize', resizeDirective);
|
||||||
|
Vue.directive('on-clickaway', onClickaway);
|
||||||
const i18nConfig = new VueI18n({
|
const i18nConfig = new VueI18n({
|
||||||
locale: 'en',
|
locale: 'en',
|
||||||
messages: i18n,
|
messages: i18n,
|
||||||
|
|||||||
@@ -30,14 +30,14 @@ class AppliedSla < ApplicationRecord
|
|||||||
enum sla_status: { active: 0, hit: 1, missed: 2, active_with_misses: 3 }
|
enum sla_status: { active: 0, hit: 1, missed: 2, active_with_misses: 3 }
|
||||||
|
|
||||||
scope :filter_by_date_range, ->(range) { where(created_at: range) if range.present? }
|
scope :filter_by_date_range, ->(range) { where(created_at: range) if range.present? }
|
||||||
scope :filter_by_inbox_id, ->(inbox_id) { where(inbox_id: inbox_id) if inbox_id.present? }
|
scope :filter_by_inbox_id, ->(inbox_id) { joins(:conversation).where(conversations: { inbox_id: inbox_id }) if inbox_id.present? }
|
||||||
scope :filter_by_team_id, ->(team_id) { where(team_id: team_id) if team_id.present? }
|
scope :filter_by_team_id, ->(team_id) { joins(:conversation).where(conversations: { team_id: team_id }) if team_id.present? }
|
||||||
scope :filter_by_sla_policy_id, ->(sla_policy_id) { where(sla_policy_id: sla_policy_id) if sla_policy_id.present? }
|
scope :filter_by_sla_policy_id, ->(sla_policy_id) { where(sla_policy_id: sla_policy_id) if sla_policy_id.present? }
|
||||||
scope :filter_by_label_list, ->(label_list) { joins(:conversation).where(conversations: { cached_label_list: label_list }) if label_list.present? }
|
scope :filter_by_label_list, lambda { |label_list|
|
||||||
|
joins(:conversation).where('conversations.cached_label_list LIKE ?', "%#{label_list}%") if label_list.present?
|
||||||
|
}
|
||||||
scope :filter_by_assigned_agent_id, lambda { |assigned_agent_id|
|
scope :filter_by_assigned_agent_id, lambda { |assigned_agent_id|
|
||||||
if assigned_agent_id.present?
|
joins(:conversation).where(conversations: { assignee_id: assigned_agent_id }) if assigned_agent_id.present?
|
||||||
joins(:conversation).where(conversations: { assigned_agent_id: assigned_agent_id })
|
|
||||||
end
|
|
||||||
}
|
}
|
||||||
scope :missed, -> { where(sla_status: %i[missed active_with_misses]) }
|
scope :missed, -> { where(sla_status: %i[missed active_with_misses]) }
|
||||||
|
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ RSpec.describe 'Applied SLAs API', type: :request do
|
|||||||
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)
|
||||||
|
|
||||||
get "/api/v1/accounts/#{account.id}/applied_slas/metrics",
|
get "/api/v1/accounts/#{account.id}/applied_slas/metrics",
|
||||||
params: { label_list: ['label1'] },
|
params: { label_list: 'label1' },
|
||||||
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)
|
||||||
@@ -205,7 +205,7 @@ RSpec.describe 'Applied SLAs API', type: :request do
|
|||||||
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: 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' },
|
||||||
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)
|
||||||
|
|||||||
Reference in New Issue
Block a user