278 lines
8.7 KiB
TypeScript
278 lines
8.7 KiB
TypeScript
/**
|
|
* SLA Tracker Utility
|
|
* Handles real-time SLA tracking with working hours (excludes weekends, non-working hours, holidays)
|
|
* Configuration is fetched from backend via configService
|
|
*/
|
|
|
|
import { configService } from '@/services/configService';
|
|
|
|
// Default working hours (fallback if config not loaded)
|
|
let WORK_START_HOUR = 9;
|
|
let WORK_END_HOUR = 18;
|
|
let WORK_START_DAY = 1;
|
|
let WORK_END_DAY = 5;
|
|
let configLoaded = false;
|
|
|
|
// Lazy initialization of configuration
|
|
async function ensureConfigLoaded() {
|
|
if (configLoaded) return;
|
|
|
|
try {
|
|
const config = await configService.getConfig();
|
|
WORK_START_HOUR = config.workingHours.START_HOUR;
|
|
WORK_END_HOUR = config.workingHours.END_HOUR;
|
|
WORK_START_DAY = config.workingHours.START_DAY;
|
|
WORK_END_DAY = config.workingHours.END_DAY;
|
|
configLoaded = true;
|
|
console.log('[SLA Tracker] ✅ Loaded working hours from backend:', { WORK_START_HOUR, WORK_END_HOUR });
|
|
} catch (error) {
|
|
console.warn('[SLA Tracker] ⚠️ Using default working hours (9 AM - 6 PM)');
|
|
}
|
|
}
|
|
|
|
// Initialize config on first import (non-blocking)
|
|
ensureConfigLoaded().catch(() => {});
|
|
|
|
/**
|
|
* Check if current time is within working hours
|
|
* @param date - Date to check
|
|
* @param priority - Priority type ('express' includes weekends, 'standard' excludes weekends)
|
|
*/
|
|
export function isWorkingTime(date: Date = new Date(), priority: string = 'standard'): boolean {
|
|
const day = date.getDay(); // 0 = Sunday, 6 = Saturday
|
|
const hour = date.getHours();
|
|
|
|
// For standard priority: exclude weekends
|
|
// For express priority: include weekends (calendar days)
|
|
if (priority === 'standard') {
|
|
if (day < WORK_START_DAY || day > WORK_END_DAY) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Working hours check (applies to both priorities)
|
|
if (hour < WORK_START_HOUR || hour >= WORK_END_HOUR) {
|
|
return false;
|
|
}
|
|
|
|
// TODO: Add holiday check if holiday API is available
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Get next working time from a given date
|
|
* @param date - Current date
|
|
* @param priority - Priority type ('express' includes weekends, 'standard' excludes weekends)
|
|
*/
|
|
export function getNextWorkingTime(date: Date = new Date(), priority: string = 'standard'): Date {
|
|
const result = new Date(date);
|
|
|
|
// If already in working time, return as is
|
|
if (isWorkingTime(result, priority)) {
|
|
return result;
|
|
}
|
|
|
|
// For standard priority: skip weekends
|
|
if (priority === 'standard') {
|
|
const day = result.getDay();
|
|
if (day === 0) { // Sunday
|
|
result.setDate(result.getDate() + 1);
|
|
result.setHours(WORK_START_HOUR, 0, 0, 0);
|
|
return result;
|
|
}
|
|
if (day === 6) { // Saturday
|
|
result.setDate(result.getDate() + 2);
|
|
result.setHours(WORK_START_HOUR, 0, 0, 0);
|
|
return result;
|
|
}
|
|
}
|
|
|
|
// If before work hours, move to work start
|
|
if (result.getHours() < WORK_START_HOUR) {
|
|
result.setHours(WORK_START_HOUR, 0, 0, 0);
|
|
return result;
|
|
}
|
|
|
|
// If after work hours, move to next day work start
|
|
if (result.getHours() >= WORK_END_HOUR) {
|
|
result.setDate(result.getDate() + 1);
|
|
result.setHours(WORK_START_HOUR, 0, 0, 0);
|
|
// Check if next day is weekend (only for standard priority)
|
|
return getNextWorkingTime(result, priority);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Calculate elapsed working hours between two dates with minute precision
|
|
* @param startDate - Start date
|
|
* @param endDate - End date (defaults to now)
|
|
* @param priority - Priority type ('express' includes weekends, 'standard' excludes weekends)
|
|
*/
|
|
export function calculateElapsedWorkingHours(startDate: Date, endDate: Date = new Date(), priority: string = 'standard'): number {
|
|
let current = new Date(startDate);
|
|
const end = new Date(endDate);
|
|
let elapsedMinutes = 0;
|
|
|
|
// Move minute by minute and count only working minutes
|
|
while (current < end) {
|
|
if (isWorkingTime(current, priority)) {
|
|
elapsedMinutes++;
|
|
}
|
|
current.setMinutes(current.getMinutes() + 1);
|
|
|
|
// Safety: stop if calculating more than 1 year
|
|
const hoursSoFar = elapsedMinutes / 60;
|
|
if (hoursSoFar > 8760) break;
|
|
}
|
|
|
|
// Convert minutes to hours (with decimal precision)
|
|
return elapsedMinutes / 60;
|
|
}
|
|
|
|
/**
|
|
* Calculate remaining working hours to deadline
|
|
* @param deadline - Deadline date
|
|
* @param fromDate - Start date (defaults to now)
|
|
* @param priority - Priority type ('express' includes weekends, 'standard' excludes weekends)
|
|
*/
|
|
export function calculateRemainingWorkingHours(deadline: Date, fromDate: Date = new Date(), priority: string = 'standard'): number {
|
|
const deadlineTime = new Date(deadline).getTime();
|
|
const currentTime = new Date(fromDate).getTime();
|
|
|
|
// If deadline has passed
|
|
if (deadlineTime <= currentTime) {
|
|
return 0;
|
|
}
|
|
|
|
// Calculate remaining working hours
|
|
return calculateElapsedWorkingHours(fromDate, deadline, priority);
|
|
}
|
|
|
|
/**
|
|
* Calculate SLA progress percentage
|
|
* @param startDate - Start date
|
|
* @param deadline - Deadline date
|
|
* @param currentDate - Current date (defaults to now)
|
|
* @param priority - Priority type ('express' includes weekends, 'standard' excludes weekends)
|
|
*/
|
|
export function calculateSLAProgress(startDate: Date, deadline: Date, currentDate: Date = new Date(), priority: string = 'standard'): number {
|
|
const totalHours = calculateElapsedWorkingHours(startDate, deadline, priority);
|
|
const elapsedHours = calculateElapsedWorkingHours(startDate, currentDate, priority);
|
|
|
|
if (totalHours === 0) return 0;
|
|
|
|
const progress = (elapsedHours / totalHours) * 100;
|
|
return Math.min(Math.max(progress, 0), 100); // Clamp between 0 and 100
|
|
}
|
|
|
|
/**
|
|
* Get SLA status information
|
|
*/
|
|
export interface SLAStatus {
|
|
isWorkingTime: boolean;
|
|
progress: number;
|
|
elapsedHours: number;
|
|
remainingHours: number;
|
|
totalHours: number;
|
|
isPaused: boolean;
|
|
nextWorkingTime?: Date;
|
|
statusText: string;
|
|
}
|
|
|
|
export function getSLAStatus(startDate: string | Date, deadline: string | Date, priority: string = 'standard'): SLAStatus {
|
|
const start = new Date(startDate);
|
|
const end = new Date(deadline);
|
|
const now = new Date();
|
|
|
|
const isWorking = isWorkingTime(now, priority);
|
|
const elapsedHours = calculateElapsedWorkingHours(start, now, priority);
|
|
const totalHours = calculateElapsedWorkingHours(start, end, priority);
|
|
const remainingHours = Math.max(0, totalHours - elapsedHours);
|
|
const progress = calculateSLAProgress(start, end, now, priority);
|
|
|
|
let statusText = '';
|
|
if (!isWorking) {
|
|
statusText = priority === 'express'
|
|
? 'SLA tracking paused (outside working hours)'
|
|
: 'SLA tracking paused (outside working hours/days)';
|
|
} else if (remainingHours === 0) {
|
|
statusText = 'SLA deadline reached';
|
|
} else if (progress >= 100) {
|
|
statusText = 'SLA breached';
|
|
} else if (progress >= 75) {
|
|
statusText = 'SLA critical';
|
|
} else if (progress >= 50) {
|
|
statusText = 'SLA warning';
|
|
} else {
|
|
statusText = 'On track';
|
|
}
|
|
|
|
return {
|
|
isWorkingTime: isWorking,
|
|
progress,
|
|
elapsedHours,
|
|
remainingHours,
|
|
totalHours,
|
|
isPaused: !isWorking,
|
|
nextWorkingTime: !isWorking ? getNextWorkingTime(now, priority) : undefined,
|
|
statusText
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Format working hours for display
|
|
*/
|
|
export function formatWorkingHours(hours: number): string {
|
|
if (hours === 0) return '0h';
|
|
if (hours < 0) return '0h';
|
|
|
|
const totalMinutes = Math.round(hours * 60);
|
|
const days = Math.floor(totalMinutes / (8 * 60)); // 8 working hours per day
|
|
const remainingMinutes = totalMinutes % (8 * 60);
|
|
const remainingHours = Math.floor(remainingMinutes / 60);
|
|
const minutes = remainingMinutes % 60;
|
|
|
|
if (days > 0 && remainingHours > 0 && minutes > 0) {
|
|
return `${days}d ${remainingHours}h ${minutes}m`;
|
|
} else if (days > 0 && remainingHours > 0) {
|
|
return `${days}d ${remainingHours}h`;
|
|
} else if (days > 0) {
|
|
return `${days}d`;
|
|
} else if (remainingHours > 0 && minutes > 0) {
|
|
return `${remainingHours}h ${minutes}m`;
|
|
} else if (remainingHours > 0) {
|
|
return `${remainingHours}h`;
|
|
} else {
|
|
return `${minutes}m`;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get time until next working period
|
|
* @param priority - Priority type ('express' includes weekends, 'standard' excludes weekends)
|
|
*/
|
|
export function getTimeUntilNextWorking(priority: string = 'standard'): string {
|
|
if (isWorkingTime(new Date(), priority)) {
|
|
return 'In working hours';
|
|
}
|
|
|
|
const now = new Date();
|
|
const next = getNextWorkingTime(now, priority);
|
|
const diff = next.getTime() - now.getTime();
|
|
|
|
const hours = Math.floor(diff / (1000 * 60 * 60));
|
|
const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60));
|
|
|
|
if (hours > 24) {
|
|
const days = Math.floor(hours / 24);
|
|
return `Resumes in ${days}d ${hours % 24}h`;
|
|
} else if (hours > 0) {
|
|
return `Resumes in ${hours}h ${minutes}m`;
|
|
} else {
|
|
return `Resumes in ${minutes}m`;
|
|
}
|
|
}
|