mirror of
https://github.com/lingble/chatwoot.git
synced 2025-11-01 19:48:08 +00:00
feat: UI to show the SLA threshold in chat screen (#9146)
- UI will show the breach in the conversation list. - UI will show the breach in the conversation header. Fixes: https://linear.app/chatwoot/issue/CW-3146/update-the-ui-to-show-the-breach-in-the-conversation-list Fixes: https://linear.app/chatwoot/issue/CW-3144/ui-update-to-show-the-breachgoing-to-breach
This commit is contained in:
@@ -86,7 +86,11 @@
|
||||
{{ unreadCount > 9 ? '9+' : unreadCount }}
|
||||
</span>
|
||||
</div>
|
||||
<card-labels :conversation-id="chat.id" />
|
||||
<card-labels :conversation-id="chat.id" class="mt-0.5 mx-2 mb-0">
|
||||
<template v-if="hasSlaPolicyId" #before>
|
||||
<SLA-card-label :chat="chat" class="ltr:mr-1 rtl:ml-1" />
|
||||
</template>
|
||||
</card-labels>
|
||||
</div>
|
||||
<woot-context-menu
|
||||
v-if="showContextMenu"
|
||||
@@ -125,6 +129,7 @@ import alertMixin from 'shared/mixins/alertMixin';
|
||||
import TimeAgo from 'dashboard/components/ui/TimeAgo.vue';
|
||||
import CardLabels from './conversationCardComponents/CardLabels.vue';
|
||||
import PriorityMark from './PriorityMark.vue';
|
||||
import SLACardLabel from './components/SLACardLabel.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@@ -135,6 +140,7 @@ export default {
|
||||
TimeAgo,
|
||||
MessagePreview,
|
||||
PriorityMark,
|
||||
SLACardLabel,
|
||||
},
|
||||
|
||||
mixins: [inboxMixin, timeMixin, conversationMixin, alertMixin],
|
||||
@@ -252,6 +258,9 @@ export default {
|
||||
const stateInbox = this.inbox;
|
||||
return stateInbox.name || '';
|
||||
},
|
||||
hasSlaPolicyId() {
|
||||
return this.chat?.sla_policy_id;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onCardClick(e) {
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
<template>
|
||||
<div
|
||||
class="bg-white dark:bg-slate-900 flex justify-between items-center py-2 px-4 border-b border-slate-50 dark:border-slate-800/50 flex-col md:flex-row"
|
||||
class="flex flex-col items-center justify-between px-4 py-2 bg-white border-b dark:bg-slate-900 border-slate-50 dark:border-slate-800/50 md:flex-row"
|
||||
>
|
||||
<div
|
||||
class="flex-1 w-full min-w-0 flex flex-col items-center justify-center"
|
||||
class="flex flex-col items-center justify-center flex-1 w-full min-w-0"
|
||||
:class="isInboxView ? 'sm:flex-row' : 'md:flex-row'"
|
||||
>
|
||||
<div class="flex justify-start items-center min-w-0 w-fit max-w-full">
|
||||
<div class="flex items-center justify-start max-w-full min-w-0 w-fit">
|
||||
<back-button
|
||||
v-if="showBackButton"
|
||||
:back-url="backButtonUrl"
|
||||
@@ -19,10 +19,10 @@
|
||||
:status="currentContact.availability_status"
|
||||
/>
|
||||
<div
|
||||
class="items-start flex flex-col ml-2 rtl:ml-0 rtl:mr-2 min-w-0 w-fit overflow-hidden"
|
||||
class="flex flex-col items-start min-w-0 ml-2 overflow-hidden rtl:ml-0 rtl:mr-2 w-fit"
|
||||
>
|
||||
<div
|
||||
class="flex items-center flex-row gap-1 m-0 p-0 w-fit max-w-full"
|
||||
class="flex flex-row items-center max-w-full gap-1 p-0 m-0 w-fit"
|
||||
>
|
||||
<woot-button
|
||||
variant="link"
|
||||
@@ -31,7 +31,7 @@
|
||||
@click.prevent="$emit('contact-panel-toggle')"
|
||||
>
|
||||
<span
|
||||
class="text-base leading-tight font-medium text-slate-900 dark:text-slate-100"
|
||||
class="text-base font-medium leading-tight text-slate-900 dark:text-slate-100"
|
||||
>
|
||||
{{ currentContact.name }}
|
||||
</span>
|
||||
@@ -46,7 +46,7 @@
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="conversation--header--actions items-center flex text-xs gap-2 text-ellipsis overflow-hidden whitespace-nowrap"
|
||||
class="flex items-center gap-2 overflow-hidden text-xs conversation--header--actions text-ellipsis whitespace-nowrap"
|
||||
>
|
||||
<inbox-name v-if="hasMultipleInboxes" :inbox="inbox" />
|
||||
<span
|
||||
@@ -67,9 +67,10 @@
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="header-actions-wrap items-center flex flex-row flex-grow justify-end mt-3 lg:mt-0"
|
||||
class="flex flex-row items-center justify-end flex-grow gap-2 mt-3 header-actions-wrap lg:mt-0"
|
||||
:class="{ 'justify-end': isContactPanelOpen }"
|
||||
>
|
||||
<SLA-card-label v-if="hasSlaPolicyId" :chat="chat" show-extended-info />
|
||||
<more-actions :conversation-id="currentChat.id" />
|
||||
</div>
|
||||
</div>
|
||||
@@ -85,6 +86,7 @@ import inboxMixin from 'shared/mixins/inboxMixin';
|
||||
import InboxName from '../InboxName.vue';
|
||||
import MoreActions from './MoreActions.vue';
|
||||
import Thumbnail from '../Thumbnail.vue';
|
||||
import SLACardLabel from './components/SLACardLabel.vue';
|
||||
import wootConstants from 'dashboard/constants/globals';
|
||||
import { conversationListPageURL } from 'dashboard/helper/URLHelper';
|
||||
import { snoozedReopenTime } from 'dashboard/helper/snoozeHelpers';
|
||||
@@ -95,6 +97,7 @@ export default {
|
||||
InboxName,
|
||||
MoreActions,
|
||||
Thumbnail,
|
||||
SLACardLabel,
|
||||
},
|
||||
mixins: [inboxMixin, agentMixin, eventListenerMixins],
|
||||
props: {
|
||||
@@ -173,6 +176,9 @@ export default {
|
||||
hasMultipleInboxes() {
|
||||
return this.$store.getters['inboxes/getInboxes'].length > 1;
|
||||
},
|
||||
hasSlaPolicyId() {
|
||||
return this.chat?.sla_policy_id;
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
||||
@@ -1,47 +1,63 @@
|
||||
<template>
|
||||
<div
|
||||
class="flex items-center px-2 truncate border min-w-fit border-slate-75 dark:border-slate-700"
|
||||
:class="showExtendedInfo ? 'py-[5px] rounded-lg' : 'py-0.5 gap-1 rounded'"
|
||||
v-if="hasSlaThreshold"
|
||||
class="relative flex items-center border cursor-pointer min-w-fit border-slate-75 dark:border-slate-700"
|
||||
:class="showExtendedInfo ? 'rounded-lg' : 'rounded'"
|
||||
>
|
||||
<div
|
||||
class="flex items-center gap-1"
|
||||
:class="
|
||||
showExtendedInfo &&
|
||||
'ltr:pr-1.5 rtl:pl-1.5 ltr:border-r rtl:border-l border-solid border-slate-75 dark:border-slate-700'
|
||||
"
|
||||
class="flex items-center w-full truncate"
|
||||
:class="showExtendedInfo ? 'h-[26px] px-1.5' : 'h-5 px-2 gap-1'"
|
||||
@mouseover="openSlaPopover()"
|
||||
@mouseleave="closeSlaPopover()"
|
||||
>
|
||||
<fluent-icon
|
||||
size="14"
|
||||
:icon="slaStatus.icon"
|
||||
type="outline"
|
||||
:icon-lib="isSlaMissed ? 'lucide' : 'fluent'"
|
||||
class="flex-shrink-0"
|
||||
:class="slaTextStyles"
|
||||
/>
|
||||
<span
|
||||
v-if="showExtendedInfo"
|
||||
class="text-xs font-medium"
|
||||
:class="slaTextStyles"
|
||||
<div
|
||||
class="flex items-center gap-1"
|
||||
:class="
|
||||
showExtendedInfo &&
|
||||
'ltr:pr-1.5 rtl:pl-1.5 ltr:border-r rtl:border-l border-solid border-slate-75 dark:border-slate-700'
|
||||
"
|
||||
>
|
||||
{{ slaStatusText }}
|
||||
<fluent-icon
|
||||
size="14"
|
||||
:icon="slaStatus.icon"
|
||||
type="outline"
|
||||
:icon-lib="isSlaMissed ? 'lucide' : 'fluent'"
|
||||
class="flex-shrink-0"
|
||||
:class="slaTextStyles"
|
||||
/>
|
||||
<span
|
||||
v-if="showExtendedInfo"
|
||||
class="text-xs font-medium"
|
||||
:class="slaTextStyles"
|
||||
>
|
||||
{{ slaStatusText }}
|
||||
</span>
|
||||
</div>
|
||||
<span
|
||||
class="text-xs font-medium"
|
||||
:class="[slaTextStyles, showExtendedInfo && 'ltr:pl-1.5 rtl:pr-1.5']"
|
||||
>
|
||||
{{ slaStatus.threshold }}
|
||||
</span>
|
||||
</div>
|
||||
<span
|
||||
class="text-xs font-medium"
|
||||
:class="[slaTextStyles, showExtendedInfo && 'ltr:pl-1.5 rtl:pr-1.5']"
|
||||
>
|
||||
{{ slaStatus.threshold }}
|
||||
</span>
|
||||
<SLA-popover-card
|
||||
v-if="showSlaPopoverCard"
|
||||
:all-missed-slas="slaEvents"
|
||||
class="right-0 top-7"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import { evaluateSLAStatus } from '../helpers/SLAHelper';
|
||||
import SLAPopoverCard from './SLAPopoverCard.vue';
|
||||
|
||||
// const REFRESH_INTERVAL = 60000;
|
||||
const REFRESH_INTERVAL = 60000;
|
||||
|
||||
export default {
|
||||
components: {
|
||||
SLAPopoverCard,
|
||||
},
|
||||
props: {
|
||||
chat: {
|
||||
type: Object,
|
||||
@@ -55,19 +71,27 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
timer: null,
|
||||
slaStatus: {},
|
||||
showSlaPopover: false,
|
||||
slaStatus: {
|
||||
threshold: null,
|
||||
isSlaMissed: false,
|
||||
type: null,
|
||||
icon: null,
|
||||
},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
activeSLA: 'sla/getSLAById',
|
||||
}),
|
||||
slaPolicyId() {
|
||||
return this.chat?.sla_policy_id;
|
||||
},
|
||||
sla() {
|
||||
if (!this.slaPolicyId) return null;
|
||||
return this.activeSLA(this.slaPolicyId);
|
||||
appliedSLA() {
|
||||
return this.chat?.applied_sla;
|
||||
},
|
||||
slaEvents() {
|
||||
return this.chat?.sla_events;
|
||||
},
|
||||
hasSlaThreshold() {
|
||||
return this.slaStatus?.threshold;
|
||||
},
|
||||
isSlaMissed() {
|
||||
return this.slaStatus?.isSlaMissed;
|
||||
@@ -79,12 +103,17 @@ export default {
|
||||
},
|
||||
slaStatusText() {
|
||||
const upperCaseType = this.slaStatus?.type?.toUpperCase(); // FRT, NRT, or RT
|
||||
const statusKey = this.isSlaMissed ? 'BREACH' : 'DUE';
|
||||
const statusKey = this.isSlaMissed ? 'MISSED' : 'DUE';
|
||||
|
||||
return this.$t(`CONVERSATION.HEADER.SLA_STATUS.${upperCaseType}`, {
|
||||
status: this.$t(`CONVERSATION.HEADER.SLA_STATUS.${statusKey}`),
|
||||
});
|
||||
},
|
||||
showSlaPopoverCard() {
|
||||
return (
|
||||
this.showExtendedInfo && this.showSlaPopover && this.slaEvents.length
|
||||
);
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
chat() {
|
||||
@@ -93,10 +122,29 @@ export default {
|
||||
},
|
||||
mounted() {
|
||||
this.updateSlaStatus();
|
||||
this.createTimer();
|
||||
},
|
||||
beforeDestroy() {
|
||||
if (this.timer) {
|
||||
clearTimeout(this.timer);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
createTimer() {
|
||||
this.timer = setTimeout(() => {
|
||||
this.updateSlaStatus();
|
||||
this.createTimer();
|
||||
}, REFRESH_INTERVAL);
|
||||
},
|
||||
updateSlaStatus() {
|
||||
this.slaStatus = evaluateSLAStatus(this.sla, this.chat);
|
||||
this.slaStatus = evaluateSLAStatus(this.appliedSLA, this.chat);
|
||||
},
|
||||
openSlaPopover() {
|
||||
if (!this.showExtendedInfo) return;
|
||||
this.showSlaPopover = true;
|
||||
},
|
||||
closeSlaPopover() {
|
||||
this.showSlaPopover = false;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
<script setup>
|
||||
import { format, fromUnixTime } from 'date-fns';
|
||||
|
||||
defineProps({
|
||||
allMissedSlas: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
});
|
||||
|
||||
const formatDate = timestamp => format(fromUnixTime(timestamp), 'PP');
|
||||
|
||||
const upperCase = str => str.toUpperCase();
|
||||
</script>
|
||||
<template>
|
||||
<div
|
||||
class="absolute flex flex-col items-start bg-[#fdfdfd] dark:bg-slate-800 z-50 p-4 border border-solid border-slate-75 dark:border-slate-700 w-[384px] rounded-xl gap-4"
|
||||
>
|
||||
<div
|
||||
v-for="missedSLA in allMissedSlas"
|
||||
:key="missedSLA.id"
|
||||
class="flex items-center justify-between w-full"
|
||||
>
|
||||
<span
|
||||
class="text-sm font-normal tracking-[-0.6%] w-[140px] truncate text-slate-900 dark:text-slate-50"
|
||||
>
|
||||
{{
|
||||
$t(
|
||||
`CONVERSATION.HEADER.SLA_POPOVER.${upperCase(missedSLA.event_type)}`
|
||||
)
|
||||
}}
|
||||
</span>
|
||||
<span
|
||||
class="text-sm font-normal tracking-[-0.6%] text-slate-600 dark:text-slate-200"
|
||||
>
|
||||
{{ $t('CONVERSATION.HEADER.SLA_POPOVER.MISSED') }}
|
||||
</span>
|
||||
<span
|
||||
class="text-sm font-normal tracking-[-0.6%] text-slate-900 dark:text-slate-50"
|
||||
>
|
||||
{{ formatDate(missedSLA.created_at) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,13 +1,14 @@
|
||||
<template>
|
||||
<div
|
||||
v-show="activeLabels.length"
|
||||
v-if="activeLabels.length || $slots.before"
|
||||
ref="labelContainer"
|
||||
class="label-container mt-0.5 mx-2 mb-0"
|
||||
v-resize="computeVisibleLabelPosition"
|
||||
>
|
||||
<div
|
||||
class="labels-wrap flex items-end min-w-0 flex-shrink gap-y-1 flex-wrap"
|
||||
:class="{ expand: showAllLabels }"
|
||||
class="flex items-end flex-shrink min-w-0 gap-y-1"
|
||||
:class="{ 'h-auto overflow-visible flex-row flex-wrap': showAllLabels }"
|
||||
>
|
||||
<slot name="before" />
|
||||
<woot-label
|
||||
v-for="(label, index) in activeLabels"
|
||||
:key="label.id"
|
||||
@@ -26,7 +27,7 @@
|
||||
? $t('CONVERSATION.CARD.HIDE_LABELS')
|
||||
: $t('CONVERSATION.CARD.SHOW_LABELS')
|
||||
"
|
||||
class="show-more--button sticky flex-shrink-0 right-0 mr-6 rtl:rotate-180"
|
||||
class="sticky right-0 flex-shrink-0 mr-6 show-more--button rtl:rotate-180"
|
||||
color-scheme="secondary"
|
||||
variant="hollow"
|
||||
:icon="showAllLabels ? 'chevron-left' : 'chevron-right'"
|
||||
@@ -59,26 +60,34 @@ export default {
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
// the problem here is that there is a certain amount of delay between the conversation
|
||||
// card being mounted and the resize event eventually being triggered
|
||||
// This means we need to run the function immediately after the component is mounted
|
||||
// Happens especially when used in a virtual list.
|
||||
// We can make the first trigger, a standard part of the directive, in case
|
||||
// we face this issue again
|
||||
this.computeVisibleLabelPosition();
|
||||
},
|
||||
methods: {
|
||||
onShowLabels(e) {
|
||||
e.stopPropagation();
|
||||
this.showAllLabels = !this.showAllLabels;
|
||||
this.$nextTick(() => this.computeVisibleLabelPosition());
|
||||
},
|
||||
computeVisibleLabelPosition() {
|
||||
const beforeSlot = this.$slots.before ? 100 : 0;
|
||||
const labelContainer = this.$refs.labelContainer;
|
||||
const labels = this.$refs.labelContainer.querySelectorAll('.label');
|
||||
if (!labelContainer) return;
|
||||
|
||||
const labels = Array.from(labelContainer.querySelectorAll('.label'));
|
||||
let labelOffset = 0;
|
||||
this.showExpandLabelButton = false;
|
||||
|
||||
Array.from(labels).forEach((label, index) => {
|
||||
labels.forEach((label, index) => {
|
||||
labelOffset += label.offsetWidth + 8;
|
||||
|
||||
if (labelOffset < labelContainer.clientWidth - 16) {
|
||||
if (labelOffset < labelContainer.clientWidth - 16 - beforeSlot) {
|
||||
this.labelPosition = index;
|
||||
} else {
|
||||
this.showExpandLabelButton = true;
|
||||
this.showExpandLabelButton = labels.length > 1;
|
||||
}
|
||||
});
|
||||
},
|
||||
@@ -95,10 +104,6 @@ export default {
|
||||
}
|
||||
|
||||
.labels-wrap {
|
||||
&.expand {
|
||||
@apply h-auto overflow-visible flex-row flex-wrap;
|
||||
}
|
||||
|
||||
.secondary {
|
||||
@apply border border-solid border-slate-100 dark:border-slate-700;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
const calculateThreshold = (timeOffset, threshold) => {
|
||||
// Calculate the time left for the SLA to breach or the time since the SLA has missed
|
||||
if (threshold === null) return null;
|
||||
const currentTime = Math.floor(Date.now() / 1000);
|
||||
return timeOffset + threshold - currentTime;
|
||||
};
|
||||
|
||||
const findMostUrgentSLAStatus = SLAStatuses => {
|
||||
// Sort the SLAs based on the threshold and return the most urgent SLA
|
||||
SLAStatuses.sort(
|
||||
(sla1, sla2) => Math.abs(sla1.threshold) - Math.abs(sla2.threshold)
|
||||
);
|
||||
return SLAStatuses[0];
|
||||
};
|
||||
|
||||
const formatSLATime = seconds => {
|
||||
const units = {
|
||||
y: 31536000, // 60 * 60 * 24 * 365
|
||||
mo: 2592000, // 60 * 60 * 24 * 30
|
||||
d: 86400, // 60 * 60 * 24
|
||||
h: 3600, // 60 * 60
|
||||
m: 60,
|
||||
};
|
||||
|
||||
if (seconds < 60) {
|
||||
return '1m';
|
||||
}
|
||||
|
||||
// we will only show two parts, two max granularity's, h-m, y-d, d-h, m, but no seconds
|
||||
const parts = [];
|
||||
|
||||
Object.keys(units).forEach(unit => {
|
||||
const value = Math.floor(seconds / units[unit]);
|
||||
if (seconds < 60 && parts.length > 0) return;
|
||||
if (parts.length === 2) return;
|
||||
if (value > 0) {
|
||||
parts.push(value + unit);
|
||||
seconds -= value * units[unit];
|
||||
}
|
||||
});
|
||||
return parts.join(' ');
|
||||
};
|
||||
|
||||
const createSLAObject = (
|
||||
type,
|
||||
{
|
||||
sla_first_response_time_threshold: frtThreshold,
|
||||
sla_next_response_time_threshold: nrtThreshold,
|
||||
sla_resolution_time_threshold: rtThreshold,
|
||||
created_at: createdAt,
|
||||
} = {},
|
||||
{
|
||||
first_reply_created_at: firstReplyCreatedAt,
|
||||
waiting_since: waitingSince,
|
||||
status,
|
||||
} = {}
|
||||
) => {
|
||||
// Mapping of breach types to their logic
|
||||
const SLATypes = {
|
||||
FRT: {
|
||||
threshold: calculateThreshold(createdAt, frtThreshold),
|
||||
// Check FRT only if threshold is not null and first reply hasn't been made
|
||||
condition:
|
||||
frtThreshold !== null &&
|
||||
(!firstReplyCreatedAt || firstReplyCreatedAt === 0),
|
||||
},
|
||||
NRT: {
|
||||
threshold: calculateThreshold(waitingSince, nrtThreshold),
|
||||
// Check NRT only if threshold is not null, first reply has been made and we are waiting since
|
||||
condition:
|
||||
nrtThreshold !== null && !!firstReplyCreatedAt && !!waitingSince,
|
||||
},
|
||||
RT: {
|
||||
threshold: calculateThreshold(createdAt, rtThreshold),
|
||||
// Check RT only if the conversation is open and threshold is not null
|
||||
condition: status === 'open' && rtThreshold !== null,
|
||||
},
|
||||
};
|
||||
|
||||
const SLAStatus = SLATypes[type];
|
||||
return SLAStatus ? { ...SLAStatus, type } : null;
|
||||
};
|
||||
|
||||
const evaluateSLAConditions = (appliedSla, chat) => {
|
||||
// Filter out the SLA based on conditions and update the object with the breach status(icon, isSlaMissed)
|
||||
const SLATypes = ['FRT', 'NRT', 'RT'];
|
||||
return SLATypes.map(type => createSLAObject(type, appliedSla, chat))
|
||||
.filter(SLAStatus => SLAStatus && SLAStatus.condition)
|
||||
.map(SLAStatus => ({
|
||||
...SLAStatus,
|
||||
icon: SLAStatus.threshold <= 0 ? 'flame' : 'alarm',
|
||||
isSlaMissed: SLAStatus.threshold <= 0,
|
||||
}));
|
||||
};
|
||||
|
||||
export const evaluateSLAStatus = (appliedSla, chat) => {
|
||||
if (!appliedSla || !chat)
|
||||
return { type: '', threshold: '', icon: '', isSlaMissed: false };
|
||||
|
||||
// Filter out the SLA and create the object for each breach
|
||||
const SLAStatuses = evaluateSLAConditions(appliedSla, chat);
|
||||
|
||||
// Return the most urgent SLA which is latest to breach or has missed
|
||||
const mostUrgent = findMostUrgentSLAStatus(SLAStatuses);
|
||||
return mostUrgent
|
||||
? {
|
||||
type: mostUrgent.type,
|
||||
threshold: formatSLATime(
|
||||
mostUrgent.threshold <= 0
|
||||
? -mostUrgent.threshold
|
||||
: mostUrgent.threshold
|
||||
),
|
||||
icon: mostUrgent.icon,
|
||||
isSlaMissed: mostUrgent.isSlaMissed,
|
||||
}
|
||||
: { type: '', threshold: '', icon: '', isSlaMissed: false };
|
||||
};
|
||||
@@ -0,0 +1,150 @@
|
||||
import { evaluateSLAStatus } from '../SLAHelper';
|
||||
|
||||
beforeEach(() => {
|
||||
jest
|
||||
.spyOn(Date, 'now')
|
||||
.mockImplementation(() => new Date('2024-01-01T00:00:00Z').getTime());
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe('SLAHelper', () => {
|
||||
describe('evaluateSLAStatus', () => {
|
||||
it('returns an empty object when sla or chat is not present', () => {
|
||||
expect(evaluateSLAStatus(null, null)).toEqual({
|
||||
type: '',
|
||||
threshold: '',
|
||||
icon: '',
|
||||
isSlaMissed: false,
|
||||
});
|
||||
});
|
||||
|
||||
// Case when FRT SLA is missed
|
||||
it('correctly identifies a missed FRT SLA', () => {
|
||||
const appliedSla = {
|
||||
sla_first_response_time_threshold: 600,
|
||||
sla_next_response_time_threshold: 1200,
|
||||
sla_resolution_time_threshold: 1800,
|
||||
created_at: 1704066540,
|
||||
};
|
||||
const chatMissed = {
|
||||
first_reply_created_at: 0,
|
||||
waiting_since: 0,
|
||||
status: 'open',
|
||||
};
|
||||
expect(evaluateSLAStatus(appliedSla, chatMissed)).toEqual({
|
||||
type: 'FRT',
|
||||
threshold: '1m',
|
||||
icon: 'flame',
|
||||
isSlaMissed: true,
|
||||
});
|
||||
});
|
||||
|
||||
// Case when FRT SLA is not missed
|
||||
it('correctly identifies an FRT SLA not yet breached', () => {
|
||||
const appliedSla = {
|
||||
sla_first_response_time_threshold: 600,
|
||||
sla_next_response_time_threshold: 1200,
|
||||
sla_resolution_time_threshold: 1800,
|
||||
created_at: 1704066660,
|
||||
};
|
||||
const chatNotMissed = {
|
||||
first_reply_created_at: 0,
|
||||
waiting_since: 0,
|
||||
status: 'open',
|
||||
};
|
||||
expect(evaluateSLAStatus(appliedSla, chatNotMissed)).toEqual({
|
||||
type: 'FRT',
|
||||
threshold: '1m',
|
||||
icon: 'alarm',
|
||||
isSlaMissed: false,
|
||||
});
|
||||
});
|
||||
|
||||
// Case when NRT SLA is missed
|
||||
it('correctly identifies a missed NRT SLA', () => {
|
||||
const appliedSla = {
|
||||
sla_first_response_time_threshold: 600,
|
||||
sla_next_response_time_threshold: 1200,
|
||||
sla_resolution_time_threshold: 1800,
|
||||
created_at: 1704065200,
|
||||
};
|
||||
const chatMissed = {
|
||||
first_reply_created_at: 1704066200,
|
||||
waiting_since: 1704065940,
|
||||
status: 'open',
|
||||
};
|
||||
expect(evaluateSLAStatus(appliedSla, chatMissed)).toEqual({
|
||||
type: 'NRT',
|
||||
threshold: '1m',
|
||||
icon: 'flame',
|
||||
isSlaMissed: true,
|
||||
});
|
||||
});
|
||||
|
||||
// Case when NRT SLA is not missed
|
||||
it('correctly identifies an NRT SLA not yet breached', () => {
|
||||
const appliedSla = {
|
||||
sla_first_response_time_threshold: 600,
|
||||
sla_next_response_time_threshold: 1200,
|
||||
sla_resolution_time_threshold: 1800,
|
||||
created_at: 1704065200 - 2000,
|
||||
};
|
||||
const chatNotMissed = {
|
||||
first_reply_created_at: 1704066200,
|
||||
waiting_since: 1704066060,
|
||||
status: 'open',
|
||||
};
|
||||
expect(evaluateSLAStatus(appliedSla, chatNotMissed)).toEqual({
|
||||
type: 'NRT',
|
||||
threshold: '1m',
|
||||
icon: 'alarm',
|
||||
isSlaMissed: false,
|
||||
});
|
||||
});
|
||||
|
||||
// Case when RT SLA is missed
|
||||
it('correctly identifies a missed RT SLA', () => {
|
||||
const appliedSla = {
|
||||
sla_first_response_time_threshold: 600,
|
||||
sla_next_response_time_threshold: 1200,
|
||||
sla_resolution_time_threshold: 1800,
|
||||
created_at: 1704065340,
|
||||
};
|
||||
const chatMissed = {
|
||||
first_reply_created_at: 1704066200,
|
||||
waiting_since: 0,
|
||||
status: 'open',
|
||||
};
|
||||
expect(evaluateSLAStatus(appliedSla, chatMissed)).toEqual({
|
||||
type: 'RT',
|
||||
threshold: '1m',
|
||||
icon: 'flame',
|
||||
isSlaMissed: true,
|
||||
});
|
||||
});
|
||||
|
||||
// Case when RT SLA is not missed
|
||||
it('correctly identifies an RT SLA not yet breached', () => {
|
||||
const appliedSla = {
|
||||
sla_first_response_time_threshold: 600,
|
||||
sla_next_response_time_threshold: 1200,
|
||||
sla_resolution_time_threshold: 1800,
|
||||
created_at: 1704065460,
|
||||
};
|
||||
const chatNotMissed = {
|
||||
first_reply_created_at: 1704066200,
|
||||
waiting_since: 0,
|
||||
status: 'open',
|
||||
};
|
||||
expect(evaluateSLAStatus(appliedSla, chatNotMissed)).toEqual({
|
||||
type: 'RT',
|
||||
threshold: '1m',
|
||||
icon: 'alarm',
|
||||
isSlaMissed: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user