import dayjs, { Dayjs } from 'dayjs'; import { TAT_CONFIG, isTestMode } from '../config/tat.config'; const WORK_START_HOUR = TAT_CONFIG.WORK_START_HOUR; const WORK_END_HOUR = TAT_CONFIG.WORK_END_HOUR; // Cache for holidays to avoid repeated DB queries let holidaysCache: Set = new Set(); let holidaysCacheExpiry: Date | null = null; /** * Load holidays from database and cache them */ async function loadHolidaysCache(): Promise { try { // Reload cache every 6 hours if (holidaysCacheExpiry && new Date() < holidaysCacheExpiry) { return; } const { holidayService } = await import('../services/holiday.service'); const currentYear = new Date().getFullYear(); const startDate = `${currentYear}-01-01`; const endDate = `${currentYear + 1}-12-31`; // Include next year for year-end calculations const holidays = await holidayService.getHolidaysInRange(startDate, endDate); holidaysCache = new Set(holidays); holidaysCacheExpiry = dayjs().add(6, 'hour').toDate(); console.log(`[TAT Utils] Loaded ${holidays.length} holidays into cache`); } catch (error) { console.error('[TAT Utils] Error loading holidays cache:', error); // Continue without holidays if loading fails } } /** * Check if a date is a holiday (uses cache) */ function isHoliday(date: Dayjs): boolean { const dateStr = date.format('YYYY-MM-DD'); return holidaysCache.has(dateStr); } /** * Check if a given date is within working time * Working hours: Monday-Friday, 9 AM - 6 PM (configurable) * Excludes: Weekends (Sat/Sun) and holidays * In TEST MODE: All times are considered working time */ function isWorkingTime(date: Dayjs): boolean { // In test mode, treat all times as working time for faster testing if (isTestMode()) { return true; } const day = date.day(); // 0 = Sun, 6 = Sat const hour = date.hour(); // Check if weekend if (day < TAT_CONFIG.WORK_START_DAY || day > TAT_CONFIG.WORK_END_DAY) { return false; } // Check if outside working hours if (hour < WORK_START_HOUR || hour >= WORK_END_HOUR) { return false; } // Check if holiday if (isHoliday(date)) { return false; } return true; } /** * Add working hours to a start date * Skips weekends, non-working hours, and holidays (unless in test mode) * In TEST MODE: 1 hour = 1 minute for faster testing */ export async function addWorkingHours(start: Date | string, hoursToAdd: number): Promise { let current = dayjs(start); // In test mode, convert hours to minutes for faster testing if (isTestMode()) { return current.add(hoursToAdd, 'minute'); } // Load holidays cache if not loaded await loadHolidaysCache(); let remaining = hoursToAdd; while (remaining > 0) { current = current.add(1, 'hour'); if (isWorkingTime(current)) { remaining -= 1; } } return current; } /** * Synchronous version for backward compatibility (doesn't check holidays) * Use addWorkingHours() for holiday-aware calculations */ export function addWorkingHoursSync(start: Date | string, hoursToAdd: number): Dayjs { let current = dayjs(start); // In test mode, convert hours to minutes for faster testing if (isTestMode()) { return current.add(hoursToAdd, 'minute'); } let remaining = hoursToAdd; while (remaining > 0) { current = current.add(1, 'hour'); const day = current.day(); const hour = current.hour(); // Simple check without holidays if (day >= 1 && day <= 5 && hour >= WORK_START_HOUR && hour < WORK_END_HOUR) { remaining -= 1; } } return current; } /** * Initialize holidays cache (call on server startup) */ export async function initializeHolidaysCache(): Promise { await loadHolidaysCache(); } /** * Calculate TAT milestones (50%, 75%, 100%) * Returns Date objects for each milestone * Async version - honors holidays */ export async function calculateTatMilestones(start: Date | string, tatDurationHours: number) { const halfTime = await addWorkingHours(start, tatDurationHours * 0.5); const seventyFive = await addWorkingHours(start, tatDurationHours * 0.75); const full = await addWorkingHours(start, tatDurationHours); return { halfTime: halfTime.toDate(), seventyFive: seventyFive.toDate(), full: full.toDate() }; } /** * Synchronous version for backward compatibility (doesn't check holidays) */ export function calculateTatMilestonesSync(start: Date | string, tatDurationHours: number) { const halfTime = addWorkingHoursSync(start, tatDurationHours * 0.5); const seventyFive = addWorkingHoursSync(start, tatDurationHours * 0.75); const full = addWorkingHoursSync(start, tatDurationHours); return { halfTime: halfTime.toDate(), seventyFive: seventyFive.toDate(), full: full.toDate() }; } /** * Calculate delay in milliseconds from now to target date */ export function calculateDelay(targetDate: Date): number { const now = dayjs(); const target = dayjs(targetDate); const delay = target.diff(now, 'millisecond'); return delay > 0 ? delay : 0; // Return 0 if target is in the past }