Re_Figma_Code/src/utils/slaTracker.ts

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`;
}
}