tat pause rsume calclulation enhanced for accuracy

This commit is contained in:
laxmanhalaki 2025-11-29 16:15:24 +05:30
parent a4cf77eebf
commit 18620235d8
16 changed files with 261 additions and 161 deletions

View File

@ -1,2 +1,2 @@
import{a as t}from"./index-ZFZ_Bqjc.js";import"./radix-vendor-C2EbRL2a.js";import"./charts-vendor-Cji9-Yri.js";import"./utils-vendor-DHm03ykU.js";import"./ui-vendor-biwEPLZp.js";import"./socket-vendor-TjCxX7sJ.js";import"./redux-vendor-tbZCm13o.js";import"./router-vendor-1fSSvDCY.js";async function m(n){return(await t.post(`/conclusions/${n}/generate`)).data.data}async function d(n,o){return(await t.post(`/conclusions/${n}/finalize`,{finalRemark:o})).data.data}async function f(n){return(await t.get(`/conclusions/${n}`)).data.data}export{d as finalizeConclusion,m as generateConclusion,f as getConclusion}; import{a as t}from"./index-BJNKp6s1.js";import"./radix-vendor-C2EbRL2a.js";import"./charts-vendor-Cji9-Yri.js";import"./utils-vendor-DHm03ykU.js";import"./ui-vendor-biwEPLZp.js";import"./socket-vendor-TjCxX7sJ.js";import"./redux-vendor-tbZCm13o.js";import"./router-vendor-1fSSvDCY.js";async function m(n){return(await t.post(`/conclusions/${n}/generate`)).data.data}async function d(n,o){return(await t.post(`/conclusions/${n}/finalize`,{finalRemark:o})).data.data}async function f(n){return(await t.get(`/conclusions/${n}`)).data.data}export{d as finalizeConclusion,m as generateConclusion,f as getConclusion};
//# sourceMappingURL=conclusionApi-DZ8g8n5O.js.map //# sourceMappingURL=conclusionApi-CLFPR2m0.js.map

View File

@ -1 +1 @@
{"version":3,"file":"conclusionApi-DZ8g8n5O.js","sources":["../../src/services/conclusionApi.ts"],"sourcesContent":["import apiClient from './authApi';\r\n\r\nexport interface ConclusionRemark {\r\n conclusionId: string;\r\n requestId: string;\r\n aiGeneratedRemark: string | null;\r\n aiModelUsed: string | null;\r\n aiConfidenceScore: number | null;\r\n finalRemark: string | null;\r\n editedBy: string | null;\r\n isEdited: boolean;\r\n editCount: number;\r\n approvalSummary: any;\r\n documentSummary: any;\r\n keyDiscussionPoints: string[];\r\n generatedAt: string | null;\r\n finalizedAt: string | null;\r\n createdAt: string;\r\n updatedAt: string;\r\n}\r\n\r\n/**\r\n * Generate AI-powered conclusion remark\r\n */\r\nexport async function generateConclusion(requestId: string): Promise<{\r\n conclusionId: string;\r\n aiGeneratedRemark: string;\r\n keyDiscussionPoints: string[];\r\n confidence: number;\r\n generatedAt: string;\r\n}> {\r\n const response = await apiClient.post(`/conclusions/${requestId}/generate`);\r\n return response.data.data;\r\n}\r\n\r\n/**\r\n * Update conclusion remark (edit by initiator)\r\n */\r\nexport async function updateConclusion(requestId: string, finalRemark: string): Promise<ConclusionRemark> {\r\n const response = await apiClient.put(`/conclusions/${requestId}`, { finalRemark });\r\n return response.data.data;\r\n}\r\n\r\n/**\r\n * Finalize conclusion and close request\r\n */\r\nexport async function finalizeConclusion(requestId: string, finalRemark: string): Promise<{\r\n conclusionId: string;\r\n requestNumber: string;\r\n status: string;\r\n finalRemark: string;\r\n finalizedAt: string;\r\n}> {\r\n const response = await apiClient.post(`/conclusions/${requestId}/finalize`, { finalRemark });\r\n return response.data.data;\r\n}\r\n\r\n/**\r\n * Get conclusion for a request\r\n */\r\nexport async function getConclusion(requestId: string): Promise<ConclusionRemark> {\r\n const response = await apiClient.get(`/conclusions/${requestId}`);\r\n return response.data.data;\r\n}\r\n\r\n"],"names":["generateConclusion","requestId","apiClient","finalizeConclusion","finalRemark","getConclusion"],"mappings":"6RAwBA,eAAsBA,EAAmBC,EAMtC,CAED,OADiB,MAAMC,EAAU,KAAK,gBAAgBD,CAAS,WAAW,GAC1D,KAAK,IACvB,CAaA,eAAsBE,EAAmBF,EAAmBG,EAMzD,CAED,OADiB,MAAMF,EAAU,KAAK,gBAAgBD,CAAS,YAAa,CAAE,YAAAG,EAAa,GAC3E,KAAK,IACvB,CAKA,eAAsBC,EAAcJ,EAA8C,CAEhF,OADiB,MAAMC,EAAU,IAAI,gBAAgBD,CAAS,EAAE,GAChD,KAAK,IACvB"} {"version":3,"file":"conclusionApi-CLFPR2m0.js","sources":["../../src/services/conclusionApi.ts"],"sourcesContent":["import apiClient from './authApi';\r\n\r\nexport interface ConclusionRemark {\r\n conclusionId: string;\r\n requestId: string;\r\n aiGeneratedRemark: string | null;\r\n aiModelUsed: string | null;\r\n aiConfidenceScore: number | null;\r\n finalRemark: string | null;\r\n editedBy: string | null;\r\n isEdited: boolean;\r\n editCount: number;\r\n approvalSummary: any;\r\n documentSummary: any;\r\n keyDiscussionPoints: string[];\r\n generatedAt: string | null;\r\n finalizedAt: string | null;\r\n createdAt: string;\r\n updatedAt: string;\r\n}\r\n\r\n/**\r\n * Generate AI-powered conclusion remark\r\n */\r\nexport async function generateConclusion(requestId: string): Promise<{\r\n conclusionId: string;\r\n aiGeneratedRemark: string;\r\n keyDiscussionPoints: string[];\r\n confidence: number;\r\n generatedAt: string;\r\n}> {\r\n const response = await apiClient.post(`/conclusions/${requestId}/generate`);\r\n return response.data.data;\r\n}\r\n\r\n/**\r\n * Update conclusion remark (edit by initiator)\r\n */\r\nexport async function updateConclusion(requestId: string, finalRemark: string): Promise<ConclusionRemark> {\r\n const response = await apiClient.put(`/conclusions/${requestId}`, { finalRemark });\r\n return response.data.data;\r\n}\r\n\r\n/**\r\n * Finalize conclusion and close request\r\n */\r\nexport async function finalizeConclusion(requestId: string, finalRemark: string): Promise<{\r\n conclusionId: string;\r\n requestNumber: string;\r\n status: string;\r\n finalRemark: string;\r\n finalizedAt: string;\r\n}> {\r\n const response = await apiClient.post(`/conclusions/${requestId}/finalize`, { finalRemark });\r\n return response.data.data;\r\n}\r\n\r\n/**\r\n * Get conclusion for a request\r\n */\r\nexport async function getConclusion(requestId: string): Promise<ConclusionRemark> {\r\n const response = await apiClient.get(`/conclusions/${requestId}`);\r\n return response.data.data;\r\n}\r\n\r\n"],"names":["generateConclusion","requestId","apiClient","finalizeConclusion","finalRemark","getConclusion"],"mappings":"6RAwBA,eAAsBA,EAAmBC,EAMtC,CAED,OADiB,MAAMC,EAAU,KAAK,gBAAgBD,CAAS,WAAW,GAC1D,KAAK,IACvB,CAaA,eAAsBE,EAAmBF,EAAmBG,EAMzD,CAED,OADiB,MAAMF,EAAU,KAAK,gBAAgBD,CAAS,YAAa,CAAE,YAAAG,EAAa,GAC3E,KAAK,IACvB,CAKA,eAAsBC,EAAcJ,EAA8C,CAEhF,OADiB,MAAMC,EAAU,IAAI,gBAAgBD,CAAS,EAAE,GAChD,KAAK,IACvB"}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -52,7 +52,7 @@
transition: transform 0.2s ease; transition: transform 0.2s ease;
} }
</style> </style>
<script type="module" crossorigin src="/assets/index-ZFZ_Bqjc.js"></script> <script type="module" crossorigin src="/assets/index-BJNKp6s1.js"></script>
<link rel="modulepreload" crossorigin href="/assets/charts-vendor-Cji9-Yri.js"> <link rel="modulepreload" crossorigin href="/assets/charts-vendor-Cji9-Yri.js">
<link rel="modulepreload" crossorigin href="/assets/radix-vendor-C2EbRL2a.js"> <link rel="modulepreload" crossorigin href="/assets/radix-vendor-C2EbRL2a.js">
<link rel="modulepreload" crossorigin href="/assets/utils-vendor-DHm03ykU.js"> <link rel="modulepreload" crossorigin href="/assets/utils-vendor-DHm03ykU.js">
@ -60,7 +60,7 @@
<link rel="modulepreload" crossorigin href="/assets/socket-vendor-TjCxX7sJ.js"> <link rel="modulepreload" crossorigin href="/assets/socket-vendor-TjCxX7sJ.js">
<link rel="modulepreload" crossorigin href="/assets/redux-vendor-tbZCm13o.js"> <link rel="modulepreload" crossorigin href="/assets/redux-vendor-tbZCm13o.js">
<link rel="modulepreload" crossorigin href="/assets/router-vendor-1fSSvDCY.js"> <link rel="modulepreload" crossorigin href="/assets/router-vendor-1fSSvDCY.js">
<link rel="stylesheet" crossorigin href="/assets/index-CiGKtlR4.css"> <link rel="stylesheet" crossorigin href="/assets/index-DD2tGQ-m.css">
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>

View File

@ -19,7 +19,7 @@ export async function handlePauseResumeJob(job: Job): Promise<void> {
logger.warn(`[Pause Resume Processor] Unknown job type: ${type}`); logger.warn(`[Pause Resume Processor] Unknown job type: ${type}`);
} }
} catch (error: any) { } catch (error: any) {
logger.error(`[Pause Resume Processor] Failed to process job ${job.id}:`, error); logger.error(`[Pause Resume Processor] Failed to process job ${job.id}:`, error?.message || error);
throw error; // Re-throw to trigger retry mechanism throw error; // Re-throw to trigger retry mechanism
} }
} }

View File

@ -30,15 +30,31 @@ try {
}); });
pauseResumeWorker.on('failed', (job, err) => { pauseResumeWorker.on('failed', (job, err) => {
logger.error(`[Pause Resume Worker] Failed: ${job?.name} (${job?.id})`, err.message); logger.error(`[Pause Resume Worker] Failed: ${job?.name} (${job?.id})`, err?.message || err);
}); });
pauseResumeWorker.on('error', (err) => { pauseResumeWorker.on('error', (err) => {
logger.error('[Pause Resume Worker] Error:', err.message); // Connection errors are common if Redis is unavailable - log as warning
const errorCode = (err as any)?.code;
const isConnectionError = err?.message?.includes('connect') ||
err?.message?.includes('ECONNREFUSED') ||
err?.message?.includes('Redis') ||
errorCode === 'ECONNREFUSED';
if (isConnectionError) {
logger.warn('[Pause Resume Worker] Connection issue (Redis may be unavailable):', err?.message || errorCode || String(err));
} else {
// Log full error details for non-connection errors to diagnose issues
logger.error('[Pause Resume Worker] Error:', {
message: err?.message || 'Unknown error',
code: errorCode,
name: err?.name,
stack: err?.stack
});
}
}); });
} }
} catch (workerError: any) { } catch (workerError: any) {
logger.error('[Pause Resume Worker] Failed to create worker:', workerError); logger.error('[Pause Resume Worker] Failed to create worker:', workerError?.message || workerError);
pauseResumeWorker = null; pauseResumeWorker = null;
} }

View File

@ -66,7 +66,7 @@ export async function handleTatJob(job: Job<TatJobData>) {
} }
const tatHours = Number((approvalLevel as any).tatHours || 0); const tatHours = Number((approvalLevel as any).tatHours || 0);
const levelStartTime = (approvalLevel as any).levelStartTime || (approvalLevel as any).createdAt; const levelStartTime = (approvalLevel as any).levelStartTime || (approvalLevel as any).createdAt || (approvalLevel as any).tatStartTime;
const now = new Date(); const now = new Date();
// FIXED: Use proper working hours calculation instead of calendar hours // FIXED: Use proper working hours calculation instead of calendar hours
@ -74,15 +74,30 @@ export async function handleTatJob(job: Job<TatJobData>) {
const priority = ((workflow as any).priority || 'STANDARD').toString().toLowerCase(); const priority = ((workflow as any).priority || 'STANDARD').toString().toLowerCase();
// Pass pause information if available // Pass pause information if available
const pauseInfo = (approvalLevel as any).isPaused ? { // IMPORTANT: Check both currently paused AND previously paused/resumed levels
isPaused: (approvalLevel as any).isPaused, // For resumed levels, we need to include pauseElapsedHours and pauseResumeDate
// so the calculation includes pre-pause elapsed time
const isCurrentlyPaused = (approvalLevel as any).isPaused === true;
const wasResumed = !isCurrentlyPaused &&
(approvalLevel as any).pauseElapsedHours !== null &&
(approvalLevel as any).pauseElapsedHours !== undefined &&
(approvalLevel as any).pauseResumeDate !== null;
const pauseInfo = isCurrentlyPaused ? {
isPaused: true,
pausedAt: (approvalLevel as any).pausedAt, pausedAt: (approvalLevel as any).pausedAt,
pauseElapsedHours: (approvalLevel as any).pauseElapsedHours, pauseElapsedHours: (approvalLevel as any).pauseElapsedHours,
pauseResumeDate: (approvalLevel as any).pauseResumeDate pauseResumeDate: (approvalLevel as any).pauseResumeDate
} : wasResumed ? {
// Level was paused but has been resumed - include pre-pause elapsed hours
isPaused: false,
pausedAt: null,
pauseElapsedHours: Number((approvalLevel as any).pauseElapsedHours), // Pre-pause elapsed hours
pauseResumeDate: (approvalLevel as any).pauseResumeDate // Actual resume timestamp
} : undefined; } : undefined;
const elapsedHours = await calculateElapsedWorkingHours(levelStartTime, now, priority, pauseInfo); const elapsedHours = await calculateElapsedWorkingHours(levelStartTime, now, priority, pauseInfo);
const remainingHours = Math.max(0, tatHours - elapsedHours); let remainingHours = Math.max(0, tatHours - elapsedHours);
// Calculate expected completion time using proper working hours calculation // Calculate expected completion time using proper working hours calculation
// EXPRESS: includes weekends but only during working hours // EXPRESS: includes weekends but only during working hours
@ -137,6 +152,10 @@ export async function handleTatJob(job: Job<TatJobData>) {
message = `TAT breached for Request ${requestNumber}: ${title}. Immediate action required!`; message = `TAT breached for Request ${requestNumber}: ${title}. Immediate action required!`;
activityDetails = 'TAT deadline reached - Breach notification'; activityDetails = 'TAT deadline reached - Breach notification';
// When breached, ensure remaining hours is 0 (no rounding errors)
// If elapsedHours >= tatHours, remainingHours should be exactly 0
remainingHours = 0;
// Update TAT status in database with comprehensive tracking // Update TAT status in database with comprehensive tracking
await ApprovalLevel.update( await ApprovalLevel.update(
{ {
@ -191,7 +210,8 @@ export async function handleTatJob(job: Job<TatJobData>) {
type === 'threshold2' ? 'HIGH' : type === 'threshold2' ? 'HIGH' :
'MEDIUM'; 'MEDIUM';
// Send notification to approver // Send notification to approver (with error handling to prevent job failure)
try {
await notificationService.sendToUsers([approverId], { await notificationService.sendToUsers([approverId], {
title: type === 'breach' ? 'TAT Breach Alert' : 'TAT Reminder', title: type === 'breach' ? 'TAT Breach Alert' : 'TAT Reminder',
body: message, body: message,
@ -202,11 +222,18 @@ export async function handleTatJob(job: Job<TatJobData>) {
priority: notificationPriority, priority: notificationPriority,
actionRequired: type === 'breach' || type === 'threshold2' // Require action for critical alerts actionRequired: type === 'breach' || type === 'threshold2' // Require action for critical alerts
}); });
logger.info(`[TAT Processor] ✅ Notification sent to approver ${approverId} for ${type} (${threshold}%)`);
} catch (notificationError: any) {
logger.error(`[TAT Processor] ❌ Failed to send notification to approver ${approverId} for ${type}:`, notificationError?.message || notificationError);
// Don't fail the job - alert is already created, notification failure is non-critical
// The alert will still be visible in the UI even if push notification fails
}
// If breached, also notify the initiator (workflow creator) // If breached, also notify the initiator (workflow creator)
if (type === 'breach') { if (type === 'breach') {
const initiatorId = (workflow as any).initiatorId; const initiatorId = (workflow as any).initiatorId;
if (initiatorId && initiatorId !== approverId) { if (initiatorId && initiatorId !== approverId) {
try {
await notificationService.sendToUsers([initiatorId], { await notificationService.sendToUsers([initiatorId], {
title: 'TAT Breach - Request Delayed', title: 'TAT Breach - Request Delayed',
body: `Your request ${requestNumber}: "${title}" has exceeded its TAT. The approver has been notified.`, body: `Your request ${requestNumber}: "${title}" has exceeded its TAT. The approver has been notified.`,
@ -217,7 +244,11 @@ export async function handleTatJob(job: Job<TatJobData>) {
priority: 'HIGH', priority: 'HIGH',
actionRequired: false actionRequired: false
}); });
logger.info(`[TAT Processor] Breach notification sent to initiator ${initiatorId}`); logger.info(`[TAT Processor] ✅ Breach notification sent to initiator ${initiatorId}`);
} catch (initiatorNotifyError: any) {
logger.error(`[TAT Processor] ❌ Failed to send breach notification to initiator ${initiatorId}:`, initiatorNotifyError?.message || initiatorNotifyError);
// Don't fail the job - notification failure is non-critical
}
} }
} }

View File

@ -30,15 +30,23 @@ try {
}); });
tatWorker.on('failed', (job, err) => { tatWorker.on('failed', (job, err) => {
logger.error(`[TAT Worker] Failed: ${job?.name}`, err.message); logger.error(`[TAT Worker] Failed: ${job?.name} (${job?.id})`, err?.message || err);
}); });
tatWorker.on('error', (err) => { tatWorker.on('error', (err) => {
logger.error('[TAT Worker] Error:', err.message); // Connection errors are common if Redis is unavailable - log as warning
const isConnectionError = err?.message?.includes('connect') ||
err?.message?.includes('ECONNREFUSED') ||
err?.message?.includes('Redis');
if (isConnectionError) {
logger.warn('[TAT Worker] Connection issue (Redis may be unavailable):', err?.message || err);
} else {
logger.error('[TAT Worker] Error:', err?.message || err);
}
}); });
} }
} catch (workerError: any) { } catch (workerError: any) {
logger.error('[TAT Worker] Failed to create worker:', workerError); logger.error('[TAT Worker] Failed to create worker:', workerError?.message || workerError);
tatWorker = null; tatWorker = null;
} }

View File

@ -8,20 +8,18 @@ import logger from '@utils/logger';
*/ */
export async function seedDefaultConfigurations(): Promise<void> { export async function seedDefaultConfigurations(): Promise<void> {
try { try {
// Check if configurations already exist // Ensure pgcrypto extension is available for gen_random_uuid()
const count = await sequelize.query( try {
'SELECT COUNT(*) as count FROM admin_configurations', await sequelize.query('CREATE EXTENSION IF NOT EXISTS "pgcrypto"', { type: QueryTypes.RAW });
{ type: QueryTypes.SELECT } } catch (extError: any) {
); // Extension might already exist or user might not have permission - continue
logger.debug('[Config Seed] pgcrypto extension check:', extError?.message || 'already exists');
if (count && (count[0] as any).count > 0) {
// Table has data, skip seeding silently
return;
} }
logger.info('[Config Seed] Seeding default configurations...'); logger.info('[Config Seed] Seeding default configurations (duplicates will be skipped automatically)...');
// Insert default configurations // Insert default configurations with ON CONFLICT handling
// This allows re-running the seed without errors if configs already exist
await sequelize.query(` await sequelize.query(`
INSERT INTO admin_configurations ( INSERT INTO admin_configurations (
config_id, config_key, config_category, config_value, value_type, config_id, config_key, config_category, config_value, value_type,
@ -729,12 +727,25 @@ export async function seedDefaultConfigurations(): Promise<void> {
NOW(), NOW(),
NOW() NOW()
) )
ON CONFLICT (config_key) DO NOTHING
`, { type: QueryTypes.INSERT }); `, { type: QueryTypes.INSERT });
logger.info('[Config Seed] ✅ Default configurations seeded successfully (30 settings across 7 categories)'); // Verify how many were actually inserted
} catch (error) { const result = await sequelize.query(
logger.error('[Config Seed] Error seeding configurations:', error); 'SELECT COUNT(*) as count FROM admin_configurations',
{ type: QueryTypes.SELECT }
);
const totalCount = result && (result[0] as any).count ? (result[0] as any).count : 0;
logger.info(`[Config Seed] ✅ Configuration seeding complete. Total configurations: ${totalCount}`);
} catch (error: any) {
logger.error('[Config Seed] ❌ Error seeding configurations:', {
message: error?.message || String(error),
stack: error?.stack,
name: error?.name
});
// Don't throw - let server start even if seeding fails // Don't throw - let server start even if seeding fails
// User can manually run seed script if needed: npm run seed:config
} }
} }

View File

@ -82,8 +82,30 @@ export class PauseService {
// Calculate elapsed hours before pause // Calculate elapsed hours before pause
const priority = ((workflow as any).priority || 'STANDARD').toString().toLowerCase(); const priority = ((workflow as any).priority || 'STANDARD').toString().toLowerCase();
const levelStartTime = (level as any).levelStartTime || (level as any).tatStartTime || (level as any).createdAt;
const elapsedHours = await calculateElapsedWorkingHours(levelStartTime, now, priority); // Check if this level was previously paused and resumed
// If so, we need to account for the previous pauseElapsedHours
// IMPORTANT: Convert to number to avoid string concatenation (DB returns DECIMAL as string)
const previousPauseElapsedHours = Number((level as any).pauseElapsedHours || 0);
const previousResumeDate = (level as any).pauseResumeDate;
const originalTatStartTime = (level as any).pauseTatStartTime || (level as any).levelStartTime || (level as any).tatStartTime || (level as any).createdAt;
let elapsedHours: number;
let levelStartTimeForCalculation: Date;
if (previousPauseElapsedHours > 0 && previousResumeDate) {
// This is a second (or subsequent) pause
// Calculate: previous elapsed hours + time from resume to now
levelStartTimeForCalculation = previousResumeDate; // Start from last resume time
const timeSinceResume = await calculateElapsedWorkingHours(levelStartTimeForCalculation, now, priority);
elapsedHours = previousPauseElapsedHours + Number(timeSinceResume);
logger.info(`[Pause] Second pause detected - Previous elapsed: ${previousPauseElapsedHours}h, Since resume: ${timeSinceResume}h, Total: ${elapsedHours}h`);
} else {
// First pause - calculate from original start time
levelStartTimeForCalculation = originalTatStartTime;
elapsedHours = await calculateElapsedWorkingHours(levelStartTimeForCalculation, now, priority);
}
// Store TAT snapshot // Store TAT snapshot
const tatSnapshot = { const tatSnapshot = {
@ -95,7 +117,7 @@ export class PauseService {
? Math.min(100, Math.round((elapsedHours / Number((level as any).tatHours)) * 100)) ? Math.min(100, Math.round((elapsedHours / Number((level as any).tatHours)) * 100))
: 0), : 0),
pausedAt: now.toISOString(), pausedAt: now.toISOString(),
originalTatStartTime: levelStartTime originalTatStartTime: originalTatStartTime // Always use the original start time, not the resume time
}; };
// Update approval level with pause information // Update approval level with pause information
@ -105,7 +127,7 @@ export class PauseService {
pausedBy: userId, pausedBy: userId,
pauseReason: reason, pauseReason: reason,
pauseResumeDate: resumeDate, pauseResumeDate: resumeDate,
pauseTatStartTime: levelStartTime, pauseTatStartTime: originalTatStartTime, // Always preserve the original start time
pauseElapsedHours: elapsedHours, pauseElapsedHours: elapsedHours,
status: ApprovalStatus.PAUSED status: ApprovalStatus.PAUSED
}); });

View File

@ -31,7 +31,9 @@ export class TatSchedulerService {
} }
const now = startTime || new Date(); const now = startTime || new Date();
const isExpress = priority === Priority.EXPRESS; // Handle both enum and string (case-insensitive) priority values
const priorityStr = typeof priority === 'string' ? priority.toUpperCase() : priority;
const isExpress = priorityStr === Priority.EXPRESS || priorityStr === 'EXPRESS';
// Get current thresholds from database configuration // Get current thresholds from database configuration
const thresholds = await getTatThresholds(); const thresholds = await getTatThresholds();
@ -51,7 +53,6 @@ export class TatSchedulerService {
threshold1Time = t1.toDate(); threshold1Time = t1.toDate();
threshold2Time = t2.toDate(); threshold2Time = t2.toDate();
breachTime = tBreach.toDate(); breachTime = tBreach.toDate();
logger.info(`[TAT Scheduler] Using EXPRESS mode - all days, working hours only (9 AM - 6 PM)`);
} else { } else {
// STANDARD: Working days only (Mon-Fri), working hours (9 AM - 6 PM), excludes holidays // STANDARD: Working days only (Mon-Fri), working hours (9 AM - 6 PM), excludes holidays
const t1 = await addWorkingHours(now, tatDurationHours * (thresholds.first / 100)); const t1 = await addWorkingHours(now, tatDurationHours * (thresholds.first / 100));
@ -60,15 +61,9 @@ export class TatSchedulerService {
threshold1Time = t1.toDate(); threshold1Time = t1.toDate();
threshold2Time = t2.toDate(); threshold2Time = t2.toDate();
breachTime = tBreach.toDate(); breachTime = tBreach.toDate();
logger.info(`[TAT Scheduler] Using STANDARD mode - weekdays only, working hours (9 AM - 6 PM), excludes holidays`);
} }
logger.info(`[TAT Scheduler] Calculating TAT milestones for request ${requestId}, level ${levelId}`); logger.info(`[TAT Scheduler] Scheduling TAT jobs - Request: ${requestId}, Priority: ${priority}, TAT: ${tatDurationHours}h`);
logger.info(`[TAT Scheduler] Priority: ${priority}, TAT Hours: ${tatDurationHours}`);
logger.info(`[TAT Scheduler] Start: ${dayjs(now).format('YYYY-MM-DD HH:mm')}`);
logger.info(`[TAT Scheduler] Threshold 1 (${thresholds.first}%): ${dayjs(threshold1Time).format('YYYY-MM-DD HH:mm')}`);
logger.info(`[TAT Scheduler] Threshold 2 (${thresholds.second}%): ${dayjs(threshold2Time).format('YYYY-MM-DD HH:mm')}`);
logger.info(`[TAT Scheduler] Breach (100%): ${dayjs(breachTime).format('YYYY-MM-DD HH:mm')}`);
const jobs = [ const jobs = [
{ {
@ -142,11 +137,10 @@ export class TatSchedulerService {
} }
); );
logger.info(`[TAT Scheduler] Scheduled ${job.type} (${job.threshold}%)`);
jobIndex++; jobIndex++;
} }
logger.info(`[TAT Scheduler] TAT jobs scheduled for request ${requestId}`); logger.info(`[TAT Scheduler] TAT jobs scheduled for request ${requestId}`);
} catch (error) { } catch (error) {
logger.error(`[TAT Scheduler] Failed to schedule TAT jobs:`, error); logger.error(`[TAT Scheduler] Failed to schedule TAT jobs:`, error);
throw error; throw error;
@ -184,7 +178,9 @@ export class TatSchedulerService {
} }
const now = startTime; const now = startTime;
const isExpress = priority === Priority.EXPRESS; // Handle both enum and string (case-insensitive) priority values
const priorityStr = typeof priority === 'string' ? priority.toUpperCase() : priority;
const isExpress = priorityStr === Priority.EXPRESS || priorityStr === 'EXPRESS';
// Get current thresholds from database configuration // Get current thresholds from database configuration
const thresholds = await getTatThresholds(); const thresholds = await getTatThresholds();
@ -196,13 +192,7 @@ export class TatSchedulerService {
: 0; : 0;
const originalTatHours = elapsedHours + remainingTatHours; const originalTatHours = elapsedHours + remainingTatHours;
logger.info(`[TAT Scheduler] ========== RESUME TAT SCHEDULING ==========`); logger.info(`[TAT Scheduler] Resuming TAT scheduling - Request: ${requestId}, Remaining: ${(remainingTatHours * 60).toFixed(1)} min, Priority: ${isExpress ? 'EXPRESS' : 'STANDARD'}`);
logger.info(`[TAT Scheduler] Request: ${requestId}, Level: ${levelId}`);
logger.info(`[TAT Scheduler] Original TAT: ${(originalTatHours * 60).toFixed(1)} min (${originalTatHours.toFixed(2)} hrs)`);
logger.info(`[TAT Scheduler] Elapsed before pause: ${(elapsedHours * 60).toFixed(1)} min (${alertStatus.percentageUsedAtPause.toFixed(1)}%)`);
logger.info(`[TAT Scheduler] Remaining after resume: ${(remainingTatHours * 60).toFixed(1)} min`);
logger.info(`[TAT Scheduler] Thresholds configured - First: ${thresholds.first}%, Second: ${thresholds.second}%`);
logger.info(`[TAT Scheduler] Alerts already sent - ${thresholds.first}%: ${alertStatus.tat50AlertSent}, ${thresholds.second}%: ${alertStatus.tat75AlertSent}, Breach: ${alertStatus.tatBreached}`);
// Jobs to schedule - only include those that haven't been sent and haven't been passed // Jobs to schedule - only include those that haven't been sent and haven't been passed
const jobsToSchedule: Array<{ const jobsToSchedule: Array<{
@ -230,10 +220,7 @@ export class TatSchedulerService {
alreadyPassed: false, alreadyPassed: false,
hoursFromNow: hoursFromNow hoursFromNow: hoursFromNow
}); });
logger.info(`[TAT Scheduler] → ${thresholds.first}% alert: Schedule in ${(hoursFromNow * 60).toFixed(1)} min`);
} }
} else {
logger.info(`[TAT Scheduler] → ${thresholds.first}% alert: SKIP (${alertStatus.tat50AlertSent ? 'already sent' : 'already passed'})`);
} }
// Threshold 2 (e.g., 75%) // Threshold 2 (e.g., 75%)
@ -249,10 +236,7 @@ export class TatSchedulerService {
alreadyPassed: false, alreadyPassed: false,
hoursFromNow: hoursFromNow hoursFromNow: hoursFromNow
}); });
logger.info(`[TAT Scheduler] → ${thresholds.second}% alert: Schedule in ${(hoursFromNow * 60).toFixed(1)} min`);
} }
} else {
logger.info(`[TAT Scheduler] → ${thresholds.second}% alert: SKIP (${alertStatus.tat75AlertSent ? 'already sent' : 'already passed'})`);
} }
// Breach (100%) // Breach (100%)
@ -265,13 +249,10 @@ export class TatSchedulerService {
alreadyPassed: false, alreadyPassed: false,
hoursFromNow: remainingTatHours hoursFromNow: remainingTatHours
}); });
logger.info(`[TAT Scheduler] → 100% breach: Schedule in ${(remainingTatHours * 60).toFixed(1)} min`);
} else {
logger.info(`[TAT Scheduler] → 100% breach: SKIP (already sent)`);
} }
if (jobsToSchedule.length === 0) { if (jobsToSchedule.length === 0) {
logger.info(`[TAT Scheduler] No TAT jobs to schedule (all alerts already sent or thresholds already passed)`); logger.info(`[TAT Scheduler] No TAT jobs to schedule (all alerts already sent)`);
return; return;
} }
@ -314,10 +295,9 @@ export class TatSchedulerService {
} }
); );
logger.info(`[TAT Scheduler] ✓ Scheduled ${job.type} (${job.threshold}%) for ${dayjs(targetTime).format('YYYY-MM-DD HH:mm')} (in ${(job.hoursFromNow * 60).toFixed(1)} working minutes)`); logger.info(`[TAT Scheduler] ✓ Scheduled ${job.type} (${job.threshold}%) for ${dayjs(targetTime).format('YYYY-MM-DD HH:mm')}`);
} }
logger.info(`[TAT Scheduler] ========== RESUME SCHEDULING COMPLETE ==========`);
logger.info(`[TAT Scheduler] ✅ ${jobsToSchedule.length} TAT job(s) scheduled for request ${requestId}`); logger.info(`[TAT Scheduler] ✅ ${jobsToSchedule.length} TAT job(s) scheduled for request ${requestId}`);
} catch (error) { } catch (error) {
logger.error(`[TAT Scheduler] Failed to schedule TAT jobs on resume:`, error); logger.error(`[TAT Scheduler] Failed to schedule TAT jobs on resume:`, error);

View File

@ -223,7 +223,7 @@ export async function addWorkingHoursExpress(start: Date | string, hoursToAdd: n
return current.add(hoursToAdd, 'minute'); return current.add(hoursToAdd, 'minute');
} }
// Load configuration // Load configuration (but don't load holidays - EXPRESS works on holidays too)
await loadWorkingHoursCache(); await loadWorkingHoursCache();
const config = workingHoursCache || { const config = workingHoursCache || {
@ -234,11 +234,16 @@ export async function addWorkingHoursExpress(start: Date | string, hoursToAdd: n
}; };
// If start time is outside working hours, advance to work start hour (reset to clean hour) // If start time is outside working hours, advance to work start hour (reset to clean hour)
// IMPORTANT: For EXPRESS, we work on ALL days (weekends, holidays), so we don't skip them
const originalStart = current.format('YYYY-MM-DD HH:mm:ss'); const originalStart = current.format('YYYY-MM-DD HH:mm:ss');
const currentHour = current.hour(); const currentHour = current.hour();
const currentDay = current.day(); // 0 = Sunday, 6 = Saturday
if (currentHour < config.startHour) { if (currentHour < config.startHour) {
// Before work hours - jump to work start hour on the same day (even if weekend/holiday)
current = current.hour(config.startHour).minute(0).second(0).millisecond(0); current = current.hour(config.startHour).minute(0).second(0).millisecond(0);
} else if (currentHour >= config.endHour) { } else if (currentHour >= config.endHour) {
// After work hours - go to next day's work start hour (even if weekend/holiday)
current = current.add(1, 'day').hour(config.startHour).minute(0).second(0).millisecond(0); current = current.add(1, 'day').hour(config.startHour).minute(0).second(0).millisecond(0);
} }
@ -247,17 +252,30 @@ export async function addWorkingHoursExpress(start: Date | string, hoursToAdd: n
const fractionalHours = hoursToAdd - wholeHours; const fractionalHours = hoursToAdd - wholeHours;
let remaining = wholeHours; let remaining = wholeHours;
let hoursCounted = 0;
// Add whole hours // Add whole hours
while (remaining > 0) { // CRITICAL: For EXPRESS, count ALL days (weekends, holidays) - only check working hours (9 AM - 6 PM)
let iterations = 0;
const maxIterations = 10000; // Safety limit
while (remaining > 0 && iterations < maxIterations) {
current = current.add(1, 'hour'); current = current.add(1, 'hour');
const hour = current.hour(); const hour = current.hour();
// For express: count ALL days (including weekends/holidays) // For express: count ALL days (including weekends/holidays)
// But only during working hours (configured start - end hour) // But only during working hours (configured start - end hour)
// NO checks for day of week or holidays - EXPRESS works 7 days a week
if (hour >= config.startHour && hour < config.endHour) { if (hour >= config.startHour && hour < config.endHour) {
remaining -= 1; remaining -= 1;
} }
// If outside working hours, we just continue to next hour without counting
// This ensures we only count 9 AM - 6 PM on any day
iterations++;
}
if (iterations >= maxIterations) {
console.error(`[EXPRESS TAT] Safety break - exceeded ${maxIterations} iterations`);
} }
// Add fractional part (convert to minutes) // Add fractional part (convert to minutes)
@ -267,7 +285,7 @@ export async function addWorkingHoursExpress(start: Date | string, hoursToAdd: n
// Check if fractional addition pushed us past working hours // Check if fractional addition pushed us past working hours
if (current.hour() >= config.endHour) { if (current.hour() >= config.endHour) {
// Overflow to next day's working hours // Overflow to next day's working hours (even if weekend/holiday)
const excessMinutes = (current.hour() - config.endHour) * 60 + current.minute(); const excessMinutes = (current.hour() - config.endHour) * 60 + current.minute();
current = current.add(1, 'day').hour(config.startHour).minute(excessMinutes).second(0).millisecond(0); current = current.add(1, 'day').hour(config.startHour).minute(excessMinutes).second(0).millisecond(0);
} }
@ -577,12 +595,14 @@ export async function calculateElapsedWorkingHours(
// If was paused but now resumed, calculate from resume date // If was paused but now resumed, calculate from resume date
let actualStartDate = startDate; let actualStartDate = startDate;
let prePauseElapsed = 0; let prePauseElapsed = 0;
let resumeTime = null;
if (pauseInfo?.pauseResumeDate && pauseInfo.pauseElapsedHours !== undefined) { if (pauseInfo?.pauseResumeDate && pauseInfo.pauseElapsedHours !== undefined) {
// Was paused, now resumed // Was paused, now resumed
// Use elapsed hours at pause + time from resume to end // Use elapsed hours at pause + time from resume to end
prePauseElapsed = pauseInfo.pauseElapsedHours; prePauseElapsed = pauseInfo.pauseElapsedHours;
actualStartDate = pauseInfo.pauseResumeDate; actualStartDate = pauseInfo.pauseResumeDate;
resumeTime = pauseInfo.pauseResumeDate; // Store resume time for reference
} }
let start = dayjs(actualStartDate); let start = dayjs(actualStartDate);
@ -601,6 +621,16 @@ export async function calculateElapsedWorkingHours(
endDay: TAT_CONFIG.WORK_END_DAY endDay: TAT_CONFIG.WORK_END_DAY
}; };
// CRITICAL: For resumed levels, we must use the exact resume time as start
// Do NOT advance resume time to next working period - resume time is the actual moment TAT resumed
// Only advance if we're calculating from original start (not resumed)
const isResumedLevel = resumeTime !== null;
if (!isResumedLevel) {
// Only adjust start time if this is NOT a resumed level
// For resumed levels, use exact resume time (even if outside working hours)
// The working hours calculation below will handle skipping non-working periods
// CRITICAL FIX: If start time is outside working hours, advance to next working period // CRITICAL FIX: If start time is outside working hours, advance to next working period
// This ensures we only count elapsed time when TAT is actually running // This ensures we only count elapsed time when TAT is actually running
const originalStart = start.format('YYYY-MM-DD HH:mm:ss'); const originalStart = start.format('YYYY-MM-DD HH:mm:ss');
@ -637,6 +667,8 @@ export async function calculateElapsedWorkingHours(
start = start.add(1, 'day').hour(config.startHour).minute(0).second(0).millisecond(0); start = start.add(1, 'day').hour(config.startHour).minute(0).second(0).millisecond(0);
} }
} }
}
// For resumed levels, keep the exact resume time - the day-by-day calculation below will handle working hours correctly
if (end.isBefore(start)) { if (end.isBefore(start)) {
return 0; return 0;