chore: Import SLA helpers from utils (#9252)
chore: Add SLA helper from utils
This commit is contained in:
@@ -49,7 +49,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { evaluateSLAStatus } from '../helpers/SLAHelper';
|
import { evaluateSLAStatus } from '@chatwoot/utils';
|
||||||
import SLAPopoverCard from './SLAPopoverCard.vue';
|
import SLAPopoverCard from './SLAPopoverCard.vue';
|
||||||
|
|
||||||
const REFRESH_INTERVAL = 60000;
|
const REFRESH_INTERVAL = 60000;
|
||||||
@@ -137,7 +137,10 @@ export default {
|
|||||||
}, REFRESH_INTERVAL);
|
}, REFRESH_INTERVAL);
|
||||||
},
|
},
|
||||||
updateSlaStatus() {
|
updateSlaStatus() {
|
||||||
this.slaStatus = evaluateSLAStatus(this.appliedSLA, this.chat);
|
this.slaStatus = evaluateSLAStatus({
|
||||||
|
appliedSla: this.appliedSLA,
|
||||||
|
chat: this.chat,
|
||||||
|
});
|
||||||
},
|
},
|
||||||
openSlaPopover() {
|
openSlaPopover() {
|
||||||
if (!this.showExtendedInfo) return;
|
if (!this.showExtendedInfo) return;
|
||||||
|
|||||||
@@ -1,117 +0,0 @@
|
|||||||
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 };
|
|
||||||
};
|
|
||||||
@@ -1,150 +0,0 @@
|
|||||||
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,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -32,7 +32,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@braid/vue-formulate": "^2.5.2",
|
"@braid/vue-formulate": "^2.5.2",
|
||||||
"@chatwoot/prosemirror-schema": "1.0.5",
|
"@chatwoot/prosemirror-schema": "1.0.5",
|
||||||
"@chatwoot/utils": "^0.0.23",
|
"@chatwoot/utils": "^0.0.24",
|
||||||
"@hcaptcha/vue-hcaptcha": "^0.3.2",
|
"@hcaptcha/vue-hcaptcha": "^0.3.2",
|
||||||
"@june-so/analytics-next": "^2.0.0",
|
"@june-so/analytics-next": "^2.0.0",
|
||||||
"@radix-ui/colors": "^1.0.1",
|
"@radix-ui/colors": "^1.0.1",
|
||||||
|
|||||||
@@ -3177,10 +3177,10 @@
|
|||||||
prosemirror-utils "^0.9.6"
|
prosemirror-utils "^0.9.6"
|
||||||
prosemirror-view "^1.17.2"
|
prosemirror-view "^1.17.2"
|
||||||
|
|
||||||
"@chatwoot/utils@^0.0.23":
|
"@chatwoot/utils@^0.0.24":
|
||||||
version "0.0.23"
|
version "0.0.24"
|
||||||
resolved "https://registry.yarnpkg.com/@chatwoot/utils/-/utils-0.0.23.tgz#e961fd87ef9ee19c442bfcedac5fe0be2ef37726"
|
resolved "https://registry.yarnpkg.com/@chatwoot/utils/-/utils-0.0.24.tgz#584e26b7fba2a8b23c049cc555497d0f35a5aebd"
|
||||||
integrity sha512-BQ7DprXr7FIkSbHdDc1WonwH0rt/+B+WaaLaXNMjCcxJgkX/gZ8QMltruOfRp/S8cFyd9JfFQhNF0T9lz1OMvA==
|
integrity sha512-TEPA1LxrCfEVFYZvBj3bckn2sEljStriiZttF6lu3j1rdNn2tysqK9IwYcRBHS4nyfleRMG4kSFrqD653t3YfA==
|
||||||
dependencies:
|
dependencies:
|
||||||
date-fns "^2.29.1"
|
date-fns "^2.29.1"
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user