Files
chatwoot/app/javascript/dashboard/helper/slaHelper.js
T

180 lines
5.0 KiB
JavaScript

import { useCamelCase } from 'dashboard/composables/useTransformKeys';
/**
* Formats seconds into a human-readable time string
* @param {number} seconds - The time in seconds (can be negative for overdue)
* @returns {string} Formatted time string like "2h 30m" or "1d 4h"
*/
const formatSLATime = seconds => {
const absSeconds = Math.abs(seconds);
const units = {
y: 31536000,
mo: 2592000,
d: 86400,
h: 3600,
m: 60,
};
if (absSeconds < 60) {
return '1m';
}
const parts = [];
let remaining = absSeconds;
Object.entries(units).forEach(([unit, value]) => {
if (parts.length >= 2) return;
const count = Math.floor(remaining / value);
if (count > 0) {
parts.push(`${count}${unit}`);
remaining -= count * value;
}
});
return parts.join(' ');
};
const toUnixTimestamp = value => {
if (!value || typeof value === 'number') return value;
const numericValue = Number(value);
if (!Number.isNaN(numericValue)) return numericValue;
const parsedTimestamp = Date.parse(value);
return Number.isNaN(parsedTimestamp)
? value
: Math.floor(parsedTimestamp / 1000);
};
const isSLACompleted = (sla, conversation) => {
const terminalStatuses = ['hit', 'missed'];
return Boolean(
sla.slaCompletedAt ||
terminalStatuses.includes(sla.slaStatus) ||
conversation.status === 'resolved'
);
};
export const shouldRefreshSLAStatus = ({ appliedSla, chat }) => {
if (!appliedSla || !chat) return false;
return !isSLACompleted(useCamelCase(appliedSla), useCamelCase(chat));
};
/**
* Evaluates SLA status using backend-computed due times
* @param {Object} params - Parameters object
* @param {Object} params.appliedSla - The applied SLA with due_at timestamps
* @param {Object} params.chat - The conversation object
* @param {Array} params.slaEvents - Recorded SLA miss events for this conversation
* @returns {Object} SLA status with type, threshold, icon, and isSlaMissed
*/
export const evaluateSLAStatus = ({ appliedSla, chat, slaEvents = [] }) => {
const emptyStatus = { type: '', threshold: '', icon: '', isSlaMissed: false };
if (!appliedSla || !chat) {
return emptyStatus;
}
const sla = useCamelCase(appliedSla);
const conversation = useCamelCase(chat);
const events = useCamelCase(slaEvents || []);
const currentTime = Math.floor(Date.now() / 1000);
const completionTime = toUnixTimestamp(sla.slaCompletedAt);
const isCompleted = isSLACompleted(sla, conversation);
const evaluationTime = completionTime || (isCompleted ? null : currentTime);
const slaStatuses = [];
const dueAtByType = {
FRT: sla.slaFrtDueAt,
RT: sla.slaRtDueAt,
};
const slaTypes = ['FRT', 'NRT', 'RT'];
events.forEach(event => {
const type = event.eventType?.toUpperCase();
if (!slaTypes.includes(type)) return;
const missedAt =
type === 'NRT' ? event.createdAt : dueAtByType[type] || event.createdAt;
if (!missedAt) return;
slaStatuses.push({
type,
threshold: evaluationTime ? missedAt - evaluationTime : null,
icon: 'flame',
isSlaMissed: true,
});
});
if (!isCompleted) {
const firstReplyCreatedAt = toUnixTimestamp(
conversation.firstReplyCreatedAt
);
const shouldCheckFirstResponse =
!firstReplyCreatedAt || firstReplyCreatedAt > sla.slaFrtDueAt;
// Check FRT - until first reply is made on time
if (sla.slaFrtDueAt && shouldCheckFirstResponse) {
const threshold = sla.slaFrtDueAt - currentTime;
slaStatuses.push({
type: 'FRT',
threshold,
icon: threshold <= 0 ? 'flame' : 'alarm',
isSlaMissed: threshold <= 0,
});
}
// Check NRT - only if first reply made and waiting for response
if (sla.slaNrtDueAt && firstReplyCreatedAt && conversation.waitingSince) {
const threshold = sla.slaNrtDueAt - currentTime;
slaStatuses.push({
type: 'NRT',
threshold,
icon: threshold <= 0 ? 'flame' : 'alarm',
isSlaMissed: threshold <= 0,
});
}
// Check RT - only if conversation is unresolved
if (sla.slaRtDueAt) {
const threshold = sla.slaRtDueAt - currentTime;
slaStatuses.push({
type: 'RT',
threshold,
icon: threshold <= 0 ? 'flame' : 'alarm',
isSlaMissed: threshold <= 0,
});
}
}
if (slaStatuses.length === 0) {
return emptyStatus;
}
// Show existing breaches before upcoming deadlines, then pick the closest timer.
slaStatuses.sort((a, b) => {
if (a.isSlaMissed !== b.isSlaMissed) {
return a.isSlaMissed ? -1 : 1;
}
if (a.threshold === null || b.threshold === null) {
if (a.threshold === b.threshold) return 0;
return a.threshold === null ? -1 : 1;
}
return Math.abs(a.threshold) - Math.abs(b.threshold);
});
const mostUrgent = slaStatuses[0];
return {
type: mostUrgent.type,
threshold:
mostUrgent.threshold === null ? '' : formatSLATime(mostUrgent.threshold),
icon: mostUrgent.icon,
isSlaMissed: mostUrgent.isSlaMissed,
};
};