- Fixed unused function parameters by prefixing with underscore - Removed unused imports and variables - Applied eslint --fix for automatic style fixes - Property shorthand - String template literals - Prefer const over let where appropriate - Spacing and formatting Reduces lint errors from 108+ to 78 (61 unused vars, 17 other issues) Related to CI lint failures in previous commit 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
296 lines
7.5 KiB
JavaScript
296 lines
7.5 KiB
JavaScript
/**
|
|
* SLATracking Model
|
|
* Tracks Service Level Agreement compliance for response times
|
|
*/
|
|
|
|
const { ObjectId } = require('mongodb');
|
|
const { getCollection } = require('../utils/db.util');
|
|
|
|
class SLATracking {
|
|
/**
|
|
* Create SLA tracking entry
|
|
*/
|
|
static async create(data) {
|
|
const collection = await getCollection('sla_tracking');
|
|
|
|
const sla = {
|
|
// What this SLA is tracking
|
|
item_type: data.item_type, // contact, media, case
|
|
item_id: new ObjectId(data.item_id),
|
|
|
|
// SLA targets (hours)
|
|
sla_target: data.sla_target || 24, // Default 24 hours
|
|
sla_priority: data.sla_priority || 'normal', // low, normal, high, urgent
|
|
|
|
// Timestamps
|
|
received_at: data.received_at || new Date(),
|
|
due_at: data.due_at || this.calculateDueDate(data.sla_target || 24),
|
|
first_response_at: data.first_response_at || null,
|
|
resolved_at: data.resolved_at || null,
|
|
|
|
// Status
|
|
status: data.status || 'pending', // pending, responded, resolved, breached
|
|
breach: data.breach || false,
|
|
|
|
// Response time metrics (hours)
|
|
response_time: data.response_time || null,
|
|
resolution_time: data.resolution_time || null,
|
|
|
|
// Escalation tracking
|
|
escalated: data.escalated || false,
|
|
escalated_at: data.escalated_at || null,
|
|
escalated_to: data.escalated_to ? new ObjectId(data.escalated_to) : null,
|
|
|
|
// Metadata
|
|
project: data.project || 'tractatus',
|
|
assigned_to: data.assigned_to ? new ObjectId(data.assigned_to) : null,
|
|
|
|
created_at: new Date(),
|
|
updated_at: new Date()
|
|
};
|
|
|
|
const result = await collection.insertOne(sla);
|
|
return { ...sla, _id: result.insertedId };
|
|
}
|
|
|
|
/**
|
|
* Calculate due date based on SLA hours
|
|
*/
|
|
static calculateDueDate(slaHours, fromDate = new Date()) {
|
|
return new Date(fromDate.getTime() + (slaHours * 60 * 60 * 1000));
|
|
}
|
|
|
|
/**
|
|
* Find SLA by item
|
|
*/
|
|
static async findByItem(itemType, itemId) {
|
|
const collection = await getCollection('sla_tracking');
|
|
return await collection.findOne({
|
|
item_type: itemType,
|
|
item_id: new ObjectId(itemId)
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Record first response
|
|
*/
|
|
static async recordResponse(itemType, itemId) {
|
|
const collection = await getCollection('sla_tracking');
|
|
|
|
const sla = await this.findByItem(itemType, itemId);
|
|
if (!sla || sla.first_response_at) {
|
|
return; // Already recorded
|
|
}
|
|
|
|
const responseTime = (new Date() - new Date(sla.received_at)) / (1000 * 60 * 60); // hours
|
|
const breach = responseTime > sla.sla_target;
|
|
|
|
await collection.updateOne(
|
|
{ _id: sla._id },
|
|
{
|
|
$set: {
|
|
first_response_at: new Date(),
|
|
response_time: responseTime,
|
|
status: 'responded',
|
|
breach,
|
|
updated_at: new Date()
|
|
}
|
|
}
|
|
);
|
|
|
|
return await collection.findOne({ _id: sla._id });
|
|
}
|
|
|
|
/**
|
|
* Record resolution
|
|
*/
|
|
static async recordResolution(itemType, itemId) {
|
|
const collection = await getCollection('sla_tracking');
|
|
|
|
const sla = await this.findByItem(itemType, itemId);
|
|
if (!sla || sla.resolved_at) {
|
|
return; // Already recorded
|
|
}
|
|
|
|
const resolutionTime = (new Date() - new Date(sla.received_at)) / (1000 * 60 * 60); // hours
|
|
|
|
await collection.updateOne(
|
|
{ _id: sla._id },
|
|
{
|
|
$set: {
|
|
resolved_at: new Date(),
|
|
resolution_time: resolutionTime,
|
|
status: 'resolved',
|
|
updated_at: new Date()
|
|
}
|
|
}
|
|
);
|
|
|
|
return await collection.findOne({ _id: sla._id });
|
|
}
|
|
|
|
/**
|
|
* Escalate SLA
|
|
*/
|
|
static async escalate(itemType, itemId, escalatedTo) {
|
|
const collection = await getCollection('sla_tracking');
|
|
|
|
const sla = await this.findByItem(itemType, itemId);
|
|
if (!sla) return;
|
|
|
|
await collection.updateOne(
|
|
{ _id: sla._id },
|
|
{
|
|
$set: {
|
|
escalated: true,
|
|
escalated_at: new Date(),
|
|
escalated_to: new ObjectId(escalatedTo),
|
|
updated_at: new Date()
|
|
}
|
|
}
|
|
);
|
|
|
|
return await collection.findOne({ _id: sla._id });
|
|
}
|
|
|
|
/**
|
|
* Get items approaching SLA breach (within 2 hours)
|
|
*/
|
|
static async getApproachingBreach(options = {}) {
|
|
const collection = await getCollection('sla_tracking');
|
|
const { project = null } = options;
|
|
|
|
const now = new Date();
|
|
const twoHoursFromNow = new Date(now.getTime() + (2 * 60 * 60 * 1000));
|
|
|
|
const query = {
|
|
status: 'pending',
|
|
breach: false,
|
|
due_at: {
|
|
$gte: now,
|
|
$lte: twoHoursFromNow
|
|
}
|
|
};
|
|
|
|
if (project) query.project = project;
|
|
|
|
return await collection
|
|
.find(query)
|
|
.sort({ due_at: 1 })
|
|
.toArray();
|
|
}
|
|
|
|
/**
|
|
* Get breached SLAs
|
|
*/
|
|
static async getBreached(options = {}) {
|
|
const collection = await getCollection('sla_tracking');
|
|
const { limit = 50, project = null } = options;
|
|
|
|
const query = { breach: true };
|
|
if (project) query.project = project;
|
|
|
|
return await collection
|
|
.find(query)
|
|
.sort({ due_at: 1 })
|
|
.limit(limit)
|
|
.toArray();
|
|
}
|
|
|
|
/**
|
|
* Get pending items (not yet responded)
|
|
*/
|
|
static async getPending(options = {}) {
|
|
const collection = await getCollection('sla_tracking');
|
|
const { limit = 50, project = null } = options;
|
|
|
|
const query = { status: 'pending' };
|
|
if (project) query.project = project;
|
|
|
|
return await collection
|
|
.find(query)
|
|
.sort({ due_at: 1 })
|
|
.limit(limit)
|
|
.toArray();
|
|
}
|
|
|
|
/**
|
|
* Check and update breached SLAs
|
|
*/
|
|
static async checkBreaches() {
|
|
const collection = await getCollection('sla_tracking');
|
|
|
|
const now = new Date();
|
|
|
|
// Find all pending items past their due date
|
|
const breached = await collection.find({
|
|
status: 'pending',
|
|
breach: false,
|
|
due_at: { $lt: now }
|
|
}).toArray();
|
|
|
|
if (breached.length > 0) {
|
|
// Mark as breached
|
|
await collection.updateMany(
|
|
{
|
|
status: 'pending',
|
|
breach: false,
|
|
due_at: { $lt: now }
|
|
},
|
|
{
|
|
$set: {
|
|
breach: true,
|
|
status: 'breached',
|
|
updated_at: new Date()
|
|
}
|
|
}
|
|
);
|
|
}
|
|
|
|
return breached.length;
|
|
}
|
|
|
|
/**
|
|
* Get SLA statistics
|
|
*/
|
|
static async getStats(filters = {}) {
|
|
const collection = await getCollection('sla_tracking');
|
|
|
|
const query = {};
|
|
if (filters.project) query.project = filters.project;
|
|
|
|
const [total, pending, responded, resolved, breached, avgResponseTime] = await Promise.all([
|
|
collection.countDocuments(query),
|
|
collection.countDocuments({ ...query, status: 'pending' }),
|
|
collection.countDocuments({ ...query, status: 'responded' }),
|
|
collection.countDocuments({ ...query, status: 'resolved' }),
|
|
collection.countDocuments({ ...query, breach: true }),
|
|
collection.aggregate([
|
|
{ $match: { ...query, response_time: { $exists: true, $ne: null } } },
|
|
{ $group: { _id: null, avg: { $avg: '$response_time' } } }
|
|
]).toArray()
|
|
]);
|
|
|
|
const complianceRate = total > 0 ? ((total - breached) / total) * 100 : 100;
|
|
|
|
return {
|
|
total,
|
|
pending,
|
|
responded,
|
|
resolved,
|
|
breached,
|
|
compliance_rate: Math.round(complianceRate * 100) / 100,
|
|
avg_response_time: avgResponseTime.length > 0 ? Math.round(avgResponseTime[0].avg * 100) / 100 : null
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Delete SLA record
|
|
*/
|
|
static async delete(id) {
|
|
const collection = await getCollection('sla_tracking');
|
|
return await collection.deleteOne({ _id: new ObjectId(id) });
|
|
}
|
|
}
|
|
|
|
module.exports = SLATracking;
|