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