Compare commits
28
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
81d97b3727 | ||
|
|
013558e5af | ||
|
|
33d833497e | ||
|
|
7937605794 | ||
|
|
2874df2c00 | ||
|
|
dcb1de642e | ||
|
|
1b527c63c1 | ||
|
|
fcd7460ca1 | ||
|
|
3d7d68c1bd | ||
|
|
975ce54fd7 | ||
|
|
de982f2c9f | ||
|
|
1935d45a42 | ||
|
|
31e2356b29 | ||
|
|
cf1a8c4b17 | ||
|
|
89cac2dec7 | ||
|
|
088795f0ad | ||
|
|
790077bd8f | ||
|
|
596bebea5c | ||
|
|
7f1aff54ee | ||
|
|
13a1ccd85c | ||
|
|
3853f860db | ||
|
|
8bc7ebb890 | ||
|
|
49b2815e96 | ||
|
|
5a9a9e3b53 | ||
|
|
e243eb619a | ||
|
|
d4d5734a04 | ||
|
|
5abe802198 | ||
|
|
5ab9efc464 |
@@ -0,0 +1,73 @@
|
|||||||
|
/* global axios */
|
||||||
|
import ApiClient from './ApiClient';
|
||||||
|
|
||||||
|
class SLAReportsAPI extends ApiClient {
|
||||||
|
constructor() {
|
||||||
|
super('applied_slas', { accountScoped: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
get({
|
||||||
|
from,
|
||||||
|
to,
|
||||||
|
assigned_agent_id,
|
||||||
|
inbox_id,
|
||||||
|
team_id,
|
||||||
|
sla_policy_id,
|
||||||
|
page,
|
||||||
|
} = {}) {
|
||||||
|
return axios.get(this.url, {
|
||||||
|
params: {
|
||||||
|
since: from,
|
||||||
|
until: to,
|
||||||
|
assigned_agent_id,
|
||||||
|
inbox_id,
|
||||||
|
team_id,
|
||||||
|
sla_policy_id,
|
||||||
|
page,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
download({
|
||||||
|
from,
|
||||||
|
to,
|
||||||
|
assigned_agent_id,
|
||||||
|
inbox_id,
|
||||||
|
team_id,
|
||||||
|
sla_policy_id,
|
||||||
|
} = {}) {
|
||||||
|
return axios.get(`${this.url}/download`, {
|
||||||
|
params: {
|
||||||
|
since: from,
|
||||||
|
until: to,
|
||||||
|
assigned_agent_id,
|
||||||
|
inbox_id,
|
||||||
|
team_id,
|
||||||
|
sla_policy_id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getMetrics({
|
||||||
|
from,
|
||||||
|
to,
|
||||||
|
assigned_agent_id,
|
||||||
|
inbox_id,
|
||||||
|
team_id,
|
||||||
|
sla_policy_id,
|
||||||
|
} = {}) {
|
||||||
|
// no ratings for metrics
|
||||||
|
return axios.get(`${this.url}/metrics`, {
|
||||||
|
params: {
|
||||||
|
since: from,
|
||||||
|
until: to,
|
||||||
|
assigned_agent_id,
|
||||||
|
inbox_id,
|
||||||
|
team_id,
|
||||||
|
sla_policy_id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new SLAReportsAPI();
|
||||||
@@ -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,13 @@ 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,
|
||||||
|
toState: frontendURL(`accounts/${accountId}/reports/sla`),
|
||||||
|
toStateName: 'sla_reports',
|
||||||
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
+5
@@ -45,6 +45,11 @@ export default {
|
|||||||
type: Number,
|
type: Number,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
conversationLabels: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -505,5 +505,26 @@
|
|||||||
"THURSDAY": "Thursday",
|
"THURSDAY": "Thursday",
|
||||||
"FRIDAY": "Friday",
|
"FRIDAY": "Friday",
|
||||||
"SATURDAY": "Saturday"
|
"SATURDAY": "Saturday"
|
||||||
|
},
|
||||||
|
"SLA_REPORTS": {
|
||||||
|
"HEADER": "SLA Reports",
|
||||||
|
"NO_RECORDS": "There are no SLA reports available.",
|
||||||
|
"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."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"TABLE": {
|
||||||
|
"HEADER": {
|
||||||
|
"POLICY_BREACHED": "Policy breached",
|
||||||
|
"CONVERSATION": "Conversation",
|
||||||
|
"AGENT": "Agent"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -239,6 +239,7 @@
|
|||||||
"REPORTS_LABEL": "Labels",
|
"REPORTS_LABEL": "Labels",
|
||||||
"REPORTS_INBOX": "Inbox",
|
"REPORTS_INBOX": "Inbox",
|
||||||
"REPORTS_TEAM": "Team",
|
"REPORTS_TEAM": "Team",
|
||||||
|
"REPORTS_SLA": "SLA",
|
||||||
"SET_AVAILABILITY_TITLE": "Set yourself as",
|
"SET_AVAILABILITY_TITLE": "Set yourself as",
|
||||||
"SLA": "SLA",
|
"SLA": "SLA",
|
||||||
"BETA": "Beta",
|
"BETA": "Beta",
|
||||||
|
|||||||
@@ -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,57 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex flex-col flex-1 gap-6 p-4 overflow-auto font-inter">
|
||||||
|
<!-- <SLA-filters /> -->
|
||||||
|
<SLAMetrics
|
||||||
|
:hit-rate="slaMetrics.hitRate"
|
||||||
|
:no-of-breaches="slaMetrics.numberOfSLABreaches"
|
||||||
|
:is-loading="uiFlags.isLoading"
|
||||||
|
/>
|
||||||
|
<SLATable
|
||||||
|
:sla-reports="slaReports"
|
||||||
|
:current-page="Number(slaMeta.currentPage)"
|
||||||
|
:total-count="Number(slaMeta.count)"
|
||||||
|
@page-change="onPageChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import { mapGetters } from 'vuex';
|
||||||
|
import SLAMetrics from './components/SLA/SLAMetrics.vue';
|
||||||
|
import SLATable from './components/SLA/SLATable.vue';
|
||||||
|
// import SLAFilters from './components/SLA/SLAFilters.vue';
|
||||||
|
export default {
|
||||||
|
name: 'SLAReports',
|
||||||
|
components: {
|
||||||
|
SLAMetrics,
|
||||||
|
SLATable,
|
||||||
|
// SLAFilters,
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
pageNumber: 1,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapGetters({
|
||||||
|
slaReports: 'slaReports/getAll',
|
||||||
|
slaMetrics: 'slaReports/getMetrics',
|
||||||
|
slaMeta: 'slaReports/getMeta',
|
||||||
|
uiFlags: 'slaReports/getUIFlags',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.$store.dispatch('slaReports/getMetrics');
|
||||||
|
this.fetchSLAReports();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
fetchSLAReports({ pageNumber } = {}) {
|
||||||
|
this.$store.dispatch('slaReports/get', {
|
||||||
|
page: pageNumber || this.pageNumber,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onPageChange(pageNumber) {
|
||||||
|
this.fetchSLAReports({ pageNumber });
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
<template>
|
||||||
|
<div class="avatar-container" :style="style" aria-hidden="true">
|
||||||
|
<slot>{{ userInitial }}</slot>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'Avatar',
|
||||||
|
props: {
|
||||||
|
username: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
type: Number,
|
||||||
|
default: 40,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
style() {
|
||||||
|
return {
|
||||||
|
fontSize: `${Math.floor(this.size / 2.5)}px`,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
userInitial() {
|
||||||
|
const parts = this.username.split(/[ -]/);
|
||||||
|
let initials = parts.reduce((acc, curr) => acc + curr.charAt(0), '');
|
||||||
|
|
||||||
|
if (initials.length > 2 && initials.search(/[A-Z]/) !== -1) {
|
||||||
|
initials = initials.replace(/[a-z]+/g, '');
|
||||||
|
}
|
||||||
|
initials = initials.substring(0, 2).toUpperCase();
|
||||||
|
|
||||||
|
return initials;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
@tailwind components;
|
||||||
|
@layer components {
|
||||||
|
.avatar-color {
|
||||||
|
background-image: linear-gradient(to top, #c2e1ff 0%, #d6ebff 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark-avatar-color {
|
||||||
|
background-image: linear-gradient(to top, #135899 0%, #135899 100%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.avatar-container {
|
||||||
|
@apply flex leading-[100%] font-medium items-center justify-center text-center cursor-default avatar-color dark:dark-avatar-color text-woot-600 dark:text-woot-200;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
+66
@@ -0,0 +1,66 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="menu text-slate-700 dark:text-slate-200 font-medium text-sm h-11 flex items-center hover:bg-slate-50 dark:hover:bg-slate-100"
|
||||||
|
role="button"
|
||||||
|
@click.stop="$emit('click')"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
v-if="variant === 'label' && option.color"
|
||||||
|
class="label-pill"
|
||||||
|
:style="{ backgroundColor: option.color }"
|
||||||
|
/>
|
||||||
|
<thumbnail
|
||||||
|
v-if="variant === 'agent'"
|
||||||
|
:username="option.label"
|
||||||
|
:src="option.thumbnail"
|
||||||
|
:status="option.status"
|
||||||
|
size="20px"
|
||||||
|
class="agent-thumbnail"
|
||||||
|
/>
|
||||||
|
<p class="menu-label overflow-hidden whitespace-nowrap text-ellipsis">
|
||||||
|
{{ option.label }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
Thumbnail,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
option: {
|
||||||
|
type: Object,
|
||||||
|
default: () => {},
|
||||||
|
},
|
||||||
|
variant: {
|
||||||
|
type: String,
|
||||||
|
default: 'default',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.menu {
|
||||||
|
width: calc(var(--space-mega) * 2);
|
||||||
|
@apply flex items-center flex-nowrap p-1 rounded-sm overflow-hidden cursor-pointer;
|
||||||
|
|
||||||
|
.menu-label {
|
||||||
|
@apply my-0 mx-2 text-xs flex-shrink-0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// &:hover {
|
||||||
|
// @apply bg-woot-500 dark:bg-woot-500 text-white dark:text-slate-50;
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-thumbnail {
|
||||||
|
margin-top: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label-pill {
|
||||||
|
@apply w-4 h-4 rounded-full border border-slate-50 border-solid dark:border-slate-900 flex-shrink-0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
+122
@@ -0,0 +1,122 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="bg-white dark:bg-slate- rounded-xl border-slate-100 dark:border-slate-900 border-solid border"
|
||||||
|
>
|
||||||
|
<menu-item
|
||||||
|
:option="unreadOption"
|
||||||
|
variant="icon"
|
||||||
|
@click="$emit('mark-as-unread')"
|
||||||
|
/>
|
||||||
|
<menu-item-with-submenu :option="priorityConfig">
|
||||||
|
<menu-item
|
||||||
|
v-for="(option, i) in priorityConfig.options"
|
||||||
|
:key="i"
|
||||||
|
:option="option"
|
||||||
|
/>
|
||||||
|
</menu-item-with-submenu>
|
||||||
|
<template v-for="option in statusMenuConfig">
|
||||||
|
<menu-item
|
||||||
|
v-if="show(option.key)"
|
||||||
|
:key="option.key"
|
||||||
|
:option="option"
|
||||||
|
variant="icon"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<menu-item
|
||||||
|
v-if="show(snoozeOption.key)"
|
||||||
|
:option="snoozeOption"
|
||||||
|
variant="icon"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<menu-item-with-submenu :option="priorityConfig">
|
||||||
|
<menu-item
|
||||||
|
v-for="(option, i) in priorityConfig.options"
|
||||||
|
:key="i"
|
||||||
|
:option="option"
|
||||||
|
/>
|
||||||
|
</menu-item-with-submenu>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import MenuItem from './MenuItem.vue';
|
||||||
|
import MenuItemWithSubmenu from './menuItemWithSubmenu.vue';
|
||||||
|
import wootConstants from 'dashboard/constants/globals';
|
||||||
|
import agentMixin from 'dashboard/mixins/agentMixin';
|
||||||
|
import { mapGetters } from 'vuex';
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
MenuItem,
|
||||||
|
MenuItemWithSubmenu,
|
||||||
|
},
|
||||||
|
mixins: [agentMixin],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
STATUS_TYPE: wootConstants.STATUS_TYPE,
|
||||||
|
unreadOption: {
|
||||||
|
label: this.$t('CONVERSATION.CARD_CONTEXT_MENU.MARK_AS_UNREAD'),
|
||||||
|
icon: 'mail',
|
||||||
|
},
|
||||||
|
statusMenuConfig: [
|
||||||
|
{
|
||||||
|
key: wootConstants.STATUS_TYPE.RESOLVED,
|
||||||
|
label: this.$t('CONVERSATION.CARD_CONTEXT_MENU.RESOLVED'),
|
||||||
|
icon: 'checkmark',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: wootConstants.STATUS_TYPE.PENDING,
|
||||||
|
label: this.$t('CONVERSATION.CARD_CONTEXT_MENU.PENDING'),
|
||||||
|
icon: 'book-clock',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: wootConstants.STATUS_TYPE.OPEN,
|
||||||
|
label: this.$t('CONVERSATION.CARD_CONTEXT_MENU.REOPEN'),
|
||||||
|
icon: 'arrow-redo',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
snoozeOption: {
|
||||||
|
key: wootConstants.STATUS_TYPE.SNOOZED,
|
||||||
|
label: this.$t('CONVERSATION.CARD_CONTEXT_MENU.SNOOZE.TITLE'),
|
||||||
|
icon: 'snooze',
|
||||||
|
},
|
||||||
|
priorityConfig: {
|
||||||
|
key: 'priority',
|
||||||
|
label: this.$t('CONVERSATION.PRIORITY.TITLE'),
|
||||||
|
icon: 'warning',
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
label: this.$t('CONVERSATION.PRIORITY.OPTIONS.NONE'),
|
||||||
|
key: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: this.$t('CONVERSATION.PRIORITY.OPTIONS.URGENT'),
|
||||||
|
key: 'urgent',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: this.$t('CONVERSATION.PRIORITY.OPTIONS.HIGH'),
|
||||||
|
key: 'high',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: this.$t('CONVERSATION.PRIORITY.OPTIONS.MEDIUM'),
|
||||||
|
key: 'medium',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: this.$t('CONVERSATION.PRIORITY.OPTIONS.LOW'),
|
||||||
|
key: 'low',
|
||||||
|
},
|
||||||
|
].filter(item => item.key !== this.priority),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapGetters({}),
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
show(key) {
|
||||||
|
// If the conversation status is same as the action, then don't display the option
|
||||||
|
// i.e.: Don't show an option to resolve if the conversation is already resolved.
|
||||||
|
return this.status !== key;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
+50
@@ -0,0 +1,50 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<woot-button
|
||||||
|
color-scheme="secondary"
|
||||||
|
icon="plus-sign"
|
||||||
|
class="rounded-xl"
|
||||||
|
@click="openContextMenu"
|
||||||
|
>
|
||||||
|
Add filter
|
||||||
|
</woot-button>
|
||||||
|
<woot-context-menu
|
||||||
|
v-if="showContextMenu"
|
||||||
|
ref="menu"
|
||||||
|
:x="contextMenu.x"
|
||||||
|
:y="contextMenu.y"
|
||||||
|
@close="closeContextMenu"
|
||||||
|
>
|
||||||
|
<SLA-context-menu />
|
||||||
|
</woot-context-menu>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import SLAContextMenu from './SLAContextMenu.vue';
|
||||||
|
export default {
|
||||||
|
name: 'SLAFilters',
|
||||||
|
components: {
|
||||||
|
SLAContextMenu,
|
||||||
|
},
|
||||||
|
props: {},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
showContextMenu: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {},
|
||||||
|
methods: {
|
||||||
|
openContextMenu() {
|
||||||
|
this.showContextMenu = true;
|
||||||
|
this.contextMenu = {
|
||||||
|
x: null,
|
||||||
|
y: null,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
closeContextMenu() {
|
||||||
|
this.showContextMenu = false;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
+46
@@ -0,0 +1,46 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex flex-col gap-2 items-start justify-center min-w-[10rem]">
|
||||||
|
<span
|
||||||
|
class="inline-flex items-center gap-1 text-sm text-slate-700 dark:text-slate-200 font-medium"
|
||||||
|
>
|
||||||
|
{{ label }}
|
||||||
|
<fluent-icon
|
||||||
|
v-tooltip.right="toolTip"
|
||||||
|
size="14"
|
||||||
|
icon="information"
|
||||||
|
type="outline"
|
||||||
|
class="flex-shrink-0 text-sm font-normal flex sm:font-medium text-slate-500 dark:text-slate-500"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
<div
|
||||||
|
v-if="isLoading"
|
||||||
|
class="w-12 h-6 mb-0.5 rounded-md bg-slate-50 animate-pulse"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<span v-else class="text-2xl font-medium text-slate-900 dark:text-slate-25">
|
||||||
|
{{ value }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
label: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
value: {
|
||||||
|
type: [String, Number],
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
toolTip: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
isLoading: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
+40
@@ -0,0 +1,40 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="flex sm:flex-row flex-col w-full gap-4 sm:gap-14 bg-white dark:bg-slate-900 rounded-xl border border-slate-75 dark:border-slate-700/50 px-6 py-4"
|
||||||
|
>
|
||||||
|
<SLAMetricCard
|
||||||
|
:label="$t('SLA_REPORTS.METRICS.HIT_RATE.LABEL')"
|
||||||
|
:value="hitRate"
|
||||||
|
:tool-tip="$t('SLA_REPORTS.METRICS.HIT_RATE.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_BREACHES.LABEL')"
|
||||||
|
:value="noOfBreaches"
|
||||||
|
:tool-tip="$t('SLA_REPORTS.METRICS.NO_OF_BREACHES.TOOLTIP')"
|
||||||
|
:is-loading="isLoading"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import SLAMetricCard from './SLAMetricCard.vue';
|
||||||
|
defineProps({
|
||||||
|
hitRate: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
noOfBreaches: {
|
||||||
|
type: Number,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
isLoading: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
+66
@@ -0,0 +1,66 @@
|
|||||||
|
<script setup>
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import UserAvatarWithName from 'dashboard/components/widgets/UserAvatarWithName.vue';
|
||||||
|
import CardLabels from 'dashboard/components/widgets/conversation/conversationCardComponents/CardLabels.vue';
|
||||||
|
const props = defineProps({
|
||||||
|
slaName: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
conversationId: {
|
||||||
|
type: Number,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
conversation: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const assignee = computed(() => props.conversation.assignee);
|
||||||
|
</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 border-slate-75 dark:border-slate-800/50 dark:bg-slate-900"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="flex items-center capitalize py-2 px-0 text-sm tracking-[0.5] text-slate-700 dark:text-slate-100 text-left rtl:text-right col-span-2"
|
||||||
|
>
|
||||||
|
{{ slaName }}
|
||||||
|
</div>
|
||||||
|
<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-100">
|
||||||
|
{{ `#${conversationId} ` }}
|
||||||
|
</span>
|
||||||
|
<span class="text-slate-600 dark:text-slate-100">with </span>
|
||||||
|
<span class="text-slate-700 dark:text-slate-100 capitalize truncate">{{
|
||||||
|
conversation.contact.name
|
||||||
|
}}</span>
|
||||||
|
<card-labels
|
||||||
|
class="w-[80%]"
|
||||||
|
:conversation-id="conversationId"
|
||||||
|
:conversation-labels="conversation.labels"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2 col-span-2">
|
||||||
|
<user-avatar-with-name v-if="assignee" :user="assignee" />
|
||||||
|
<span
|
||||||
|
v-else
|
||||||
|
class="text-slate-600 dark:text-slate-200 capitalize truncate"
|
||||||
|
>
|
||||||
|
---
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<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"
|
||||||
|
>
|
||||||
|
<woot-button color-scheme="secondary" variant="link">
|
||||||
|
View details
|
||||||
|
</woot-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
+99
@@ -0,0 +1,99 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
class="min-w-full border-t border-x rounded-xl border-slate-75 dark:border-slate-800/50"
|
||||||
|
>
|
||||||
|
<!-- Header -->
|
||||||
|
<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"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="flex items-center font-medium text-xs py-2 px-0 tracking-[10%] text-slate-700 dark:text-slate-100 text-left rtl:text-right uppercase col-span-2"
|
||||||
|
>
|
||||||
|
{{ $t('SLA_REPORTS.TABLE.HEADER.POLICY_BREACHED') }}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="flex items-center col-span-6 px-0 py-2 text-xs font-medium text-left uppercase text-slate-700 dark:text-slate-100 rtl:text-right"
|
||||||
|
>
|
||||||
|
{{ $t('SLA_REPORTS.TABLE.HEADER.CONVERSATION') }}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="flex items-center px-0 text-xs font-medium text-left uppercase col-span-2py-2 text-slate-700 dark:text-slate-100 rtl:text-right"
|
||||||
|
>
|
||||||
|
{{ $t('SLA_REPORTS.TABLE.HEADER.AGENT') }}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="col-span-2 px-0 py-2 text-xs font-medium text-left uppercase text-slate-700 dark:text-slate-100 rtl:text-right"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-show="!slaReports.length"
|
||||||
|
class="flex items-center justify-center h-32 bg-white dark:bg-slate-900"
|
||||||
|
>
|
||||||
|
{{ $t('SLA_REPORTS.NO_RECORDS') }}
|
||||||
|
</div>
|
||||||
|
<SLA-report-item
|
||||||
|
v-for="sla in slaReports"
|
||||||
|
:key="sla.id"
|
||||||
|
:sla-name="sla.sla_policy.name"
|
||||||
|
:conversation="sla.conversation"
|
||||||
|
:conversation-id="sla.conversation_id"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<table-footer
|
||||||
|
v-if="shouldShowFooter"
|
||||||
|
:current-page="currentPage"
|
||||||
|
:total-count="totalCount"
|
||||||
|
:page-size="pageSize"
|
||||||
|
class="dark:bg-slate-900 sticky bottom-0 border-none"
|
||||||
|
@page-change="onPageChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import TableFooter from 'dashboard/components/widgets/TableFooter.vue';
|
||||||
|
import SLAReportItem from './SLAReportItem.vue';
|
||||||
|
export default {
|
||||||
|
name: 'SLATable',
|
||||||
|
components: {
|
||||||
|
SLAReportItem,
|
||||||
|
TableFooter,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
slaReports: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
totalCount: {
|
||||||
|
type: Number,
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
|
currentPage: {
|
||||||
|
type: Number,
|
||||||
|
default: 1,
|
||||||
|
},
|
||||||
|
pageSize: {
|
||||||
|
type: Number,
|
||||||
|
default: 25,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
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>
|
||||||
+95
@@ -0,0 +1,95 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
:style="{ height: size, width: size }"
|
||||||
|
class="rounded-full flex items-center justify-center bg-slate-100 dark:bg-slate-800 text-slate-700 dark:text-slate-100"
|
||||||
|
:title="userName"
|
||||||
|
>
|
||||||
|
<slot>
|
||||||
|
<img
|
||||||
|
v-if="shouldShowImage"
|
||||||
|
:src="src"
|
||||||
|
draggable="false"
|
||||||
|
@load="onImgLoad"
|
||||||
|
@error="onImgError"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
:style="{ height: size, width: size }"
|
||||||
|
class="flex justify-center items-center rounded-full"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<span :style="{ fontSize: fontSize }" class="font-semibold">{{
|
||||||
|
userInitial
|
||||||
|
}}</span>
|
||||||
|
</div>
|
||||||
|
</slot>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import { removeEmoji } from 'shared/helpers/emoji';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
src: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
type: String,
|
||||||
|
default: '24px',
|
||||||
|
},
|
||||||
|
userName: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
hasImageLoaded: false,
|
||||||
|
imgError: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
userNameWithoutEmoji() {
|
||||||
|
return removeEmoji(this.userName);
|
||||||
|
},
|
||||||
|
showStatusIndicator() {
|
||||||
|
if (this.shouldShowStatusAlways) return true;
|
||||||
|
return this.status === 'online' || this.status === 'busy';
|
||||||
|
},
|
||||||
|
avatarSize() {
|
||||||
|
return Number(this.size.replace(/\D+/g, ''));
|
||||||
|
},
|
||||||
|
shouldShowImage() {
|
||||||
|
if (!this.src) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (this.hasImageLoaded) {
|
||||||
|
return !this.imgError;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
userInitial() {
|
||||||
|
return this.userName.charAt(0).toUpperCase();
|
||||||
|
},
|
||||||
|
fontSize() {
|
||||||
|
return `${Math.floor(this.avatarSize / 2.5)}px`;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
src(value, oldValue) {
|
||||||
|
if (value !== oldValue && this.imgError) {
|
||||||
|
this.imgError = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
onImgError() {
|
||||||
|
this.imgError = true;
|
||||||
|
},
|
||||||
|
onImgLoad() {
|
||||||
|
this.hasImageLoaded = true;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
+43
@@ -0,0 +1,43 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="text-slate-800 dark:text-slate-100 menu-with-submenu min-width-calc w-full p-1 flex items-center h-7 rounded-md relative bg-white dark:bg-slate-700 justify-between hover:bg-woot-75 cursor-pointer dark:hover:bg-slate-800"
|
||||||
|
:class="!subMenuAvailable ? 'opacity-50 cursor-not-allowed' : ''"
|
||||||
|
>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<p class="my-0 mx-2 text-xs">{{ option.label }}</p>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="subMenuAvailable"
|
||||||
|
class="submenu bg-white dark:bg-slate-700 p-1 shadow-lg rounded-md absolute left-full top-0 hidden min-h-min max-h-[15rem] overflow-y-auto overflow-x-hidden cursor-pointer"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
option: {
|
||||||
|
type: Object,
|
||||||
|
default: () => {},
|
||||||
|
},
|
||||||
|
subMenuAvailable: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.menu-with-submenu {
|
||||||
|
min-width: calc(6.25rem * 2);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
.submenu {
|
||||||
|
@apply block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -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,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ import teams from './modules/teams';
|
|||||||
import userNotificationSettings from './modules/userNotificationSettings';
|
import userNotificationSettings from './modules/userNotificationSettings';
|
||||||
import webhooks from './modules/webhooks';
|
import webhooks from './modules/webhooks';
|
||||||
import draftMessages from './modules/draftMessages';
|
import draftMessages from './modules/draftMessages';
|
||||||
|
import SLAReports from './modules/SLAReports';
|
||||||
|
|
||||||
import LogRocket from 'logrocket';
|
import LogRocket from 'logrocket';
|
||||||
import createPlugin from 'logrocket-vuex';
|
import createPlugin from 'logrocket-vuex';
|
||||||
@@ -111,6 +112,7 @@ export default new Vuex.Store({
|
|||||||
webhooks,
|
webhooks,
|
||||||
draftMessages,
|
draftMessages,
|
||||||
sla,
|
sla,
|
||||||
|
slaReports: SLAReports,
|
||||||
},
|
},
|
||||||
plugins,
|
plugins,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,99 @@
|
|||||||
|
import * as MutationHelpers from 'shared/helpers/vuex/mutationHelpers';
|
||||||
|
import types from '../mutation-types';
|
||||||
|
import SLAReportsAPI from '../../api/slaReports';
|
||||||
|
|
||||||
|
export const state = {
|
||||||
|
records: [],
|
||||||
|
metrics: {
|
||||||
|
numberOfSLABreaches: 0,
|
||||||
|
hitRate: '0%',
|
||||||
|
},
|
||||||
|
uiFlags: {
|
||||||
|
isFetching: false,
|
||||||
|
isFetchingMetrics: false,
|
||||||
|
},
|
||||||
|
meta: {
|
||||||
|
count: 0,
|
||||||
|
currentPage: 1,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getters = {
|
||||||
|
getAll(_state) {
|
||||||
|
return _state.records;
|
||||||
|
},
|
||||||
|
getMeta(_state) {
|
||||||
|
return _state.meta;
|
||||||
|
},
|
||||||
|
getMetrics(_state) {
|
||||||
|
return _state.metrics;
|
||||||
|
},
|
||||||
|
getUIFlags(_state) {
|
||||||
|
return _state.uiFlags;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const actions = {
|
||||||
|
get: async function getResponses({ commit }, params) {
|
||||||
|
commit(types.SET_SLA_REPORTS_UI_FLAG, { isFetching: true });
|
||||||
|
try {
|
||||||
|
const response = await SLAReportsAPI.get(params);
|
||||||
|
const { payload, meta } = response.data;
|
||||||
|
|
||||||
|
commit(types.SET_SLA_REPORTS, payload);
|
||||||
|
commit(types.SET_SLA_REPORTS_META, meta);
|
||||||
|
} catch (error) {
|
||||||
|
// Ignore error
|
||||||
|
} finally {
|
||||||
|
commit(types.SET_SLA_REPORTS_UI_FLAG, { isFetching: false });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getMetrics: async function getMetrics({ commit }, params) {
|
||||||
|
commit(types.SET_SLA_REPORTS_UI_FLAG, { isFetchingMetrics: true });
|
||||||
|
try {
|
||||||
|
const response = await SLAReportsAPI.getMetrics(params);
|
||||||
|
commit(types.SET_SLA_REPORTS_METRICS, response.data);
|
||||||
|
} catch (error) {
|
||||||
|
// Ignore error
|
||||||
|
} finally {
|
||||||
|
commit(types.SET_SLA_REPORTS_UI_FLAG, { isFetchingMetrics: false });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const mutations = {
|
||||||
|
[types.SET_SLA_REPORTS_UI_FLAG](_state, data) {
|
||||||
|
_state.uiFlags = {
|
||||||
|
..._state.uiFlags,
|
||||||
|
...data,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
[types.SET_SLA_REPORTS]: MutationHelpers.set,
|
||||||
|
[types.SET_SLA_REPORTS_METRICS](
|
||||||
|
_state,
|
||||||
|
{ number_of_sla_breaches: numberOfSLABreaches, hit_rate: hitRate }
|
||||||
|
) {
|
||||||
|
_state.metrics = {
|
||||||
|
numberOfSLABreaches,
|
||||||
|
hitRate,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
[types.SET_SLA_REPORTS_META](
|
||||||
|
_state,
|
||||||
|
{ total_applied_slas: totalAppliedSLAs, current_page: currentPage }
|
||||||
|
) {
|
||||||
|
_state.meta = {
|
||||||
|
count: totalAppliedSLAs,
|
||||||
|
currentPage,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {
|
||||||
|
namespaced: true,
|
||||||
|
state,
|
||||||
|
getters,
|
||||||
|
actions,
|
||||||
|
mutations,
|
||||||
|
};
|
||||||
@@ -309,4 +309,10 @@ export default {
|
|||||||
ADD_SLA: 'ADD_SLA',
|
ADD_SLA: 'ADD_SLA',
|
||||||
EDIT_SLA: 'EDIT_SLA',
|
EDIT_SLA: 'EDIT_SLA',
|
||||||
DELETE_SLA: 'DELETE_SLA',
|
DELETE_SLA: 'DELETE_SLA',
|
||||||
|
|
||||||
|
// SLA Reports
|
||||||
|
SET_SLA_REPORTS_UI_FLAG: 'SET_SLA_REPORTS_UI_FLAG',
|
||||||
|
SET_SLA_REPORTS: 'SET_SLA_REPORTS',
|
||||||
|
SET_SLA_REPORTS_METRICS: 'SET_SLA_REPORTS_METRICS',
|
||||||
|
SET_SLA_REPORTS_META: 'SET_SLA_REPORTS_META',
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -33,6 +33,11 @@ class NotificationListener < BaseListener
|
|||||||
return if event.data[:notifiable_assignee_change].blank?
|
return if event.data[:notifiable_assignee_change].blank?
|
||||||
return if conversation.pending?
|
return if conversation.pending?
|
||||||
|
|
||||||
|
if assignee.nil?
|
||||||
|
Rail.logger.error "[NotificationListener] Assignee is nil, skipping notification creation for #{conversation.id}"
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
NotificationBuilder.new(
|
NotificationBuilder.new(
|
||||||
notification_type: 'conversation_assignment',
|
notification_type: 'conversation_assignment',
|
||||||
user: assignee,
|
user: assignee,
|
||||||
|
|||||||
@@ -6,12 +6,14 @@ 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
|
||||||
|
@total_applied_slas = total_applied_slas
|
||||||
|
@applied_slas = @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
|
||||||
@@ -52,7 +54,7 @@ class Api::V1::Accounts::AppliedSlasController < Api::V1::Accounts::EnterpriseAc
|
|||||||
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).includes(:sla_policy)
|
||||||
@applied_slas = initial_query
|
@applied_slas = initial_query
|
||||||
.filter_by_date_range(range)
|
.filter_by_date_range(range)
|
||||||
.filter_by_inbox_id(params[:inbox_id])
|
.filter_by_inbox_id(params[:inbox_id])
|
||||||
@@ -62,10 +64,6 @@ class Api::V1::Accounts::AppliedSlasController < Api::V1::Accounts::EnterpriseAc
|
|||||||
.filter_by_assigned_agent_id(params[:assigned_agent_id])
|
.filter_by_assigned_agent_id(params[:assigned_agent_id])
|
||||||
end
|
end
|
||||||
|
|
||||||
def paginate_slas
|
|
||||||
@applied_slas = @applied_slas.page(@current_page).per(RESULTS_PER_PAGE)
|
|
||||||
end
|
|
||||||
|
|
||||||
def set_current_page
|
def set_current_page
|
||||||
@current_page = params[:page] || 1
|
@current_page = params[:page] || 1
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -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]) }
|
||||||
private
|
private
|
||||||
|
|
||||||
def ensure_account_id
|
def ensure_account_id
|
||||||
|
|||||||
@@ -1,14 +1,28 @@
|
|||||||
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.id applied_sla.id
|
||||||
json.conversation_id applied_sla.conversation_id
|
json.sla_policy_id applied_sla.sla_policy_id
|
||||||
json.sla_status applied_sla.sla_status
|
json.conversation_id applied_sla.conversation_id
|
||||||
json.created_at applied_sla.created_at
|
json.sla_status applied_sla.sla_status
|
||||||
json.updated_at applied_sla.updated_at
|
json.created_at applied_sla.created_at
|
||||||
json.conversation do
|
json.updated_at applied_sla.updated_at
|
||||||
json.partial! 'api/v1/models/conversation', conversation: applied_sla.conversation
|
json.conversation do
|
||||||
end
|
json.contact do
|
||||||
json.sla_events applied_sla.sla_events do |sla_event|
|
json.name applied_sla.conversation.contact.name
|
||||||
json.partial! 'api/v1/models/sla_event', formats: [:json], sla_event: sla_event
|
end
|
||||||
|
json.labels applied_sla.conversation.cached_label_list
|
||||||
|
json.assignee applied_sla.conversation.assignee
|
||||||
|
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
|
||||||
|
json.sla_policy do
|
||||||
|
json.partial! 'api/v1/models/sla_policy', sla_policy: applied_sla.sla_policy
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
json.meta do
|
||||||
|
json.total_applied_slas @total_applied_slas
|
||||||
|
json.current_page @current_page
|
||||||
|
end
|
||||||
|
|||||||
Reference in New Issue
Block a user