tat pause resume job logic altered
This commit is contained in:
parent
755dd8fbbb
commit
a4cf77eebf
@ -1,2 +1,2 @@
|
|||||||
import{a as t}from"./index-BVxBNLM-.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-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};
|
||||||
//# sourceMappingURL=conclusionApi-U5wrbP17.js.map
|
//# sourceMappingURL=conclusionApi-DZ8g8n5O.js.map
|
||||||
@ -1 +1 @@
|
|||||||
{"version":3,"file":"conclusionApi-U5wrbP17.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-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"}
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
64
build/assets/index-ZFZ_Bqjc.js
Normal file
64
build/assets/index-ZFZ_Bqjc.js
Normal file
File diff suppressed because one or more lines are too long
1
build/assets/index-ZFZ_Bqjc.js.map
Normal file
1
build/assets/index-ZFZ_Bqjc.js.map
Normal file
File diff suppressed because one or more lines are too long
@ -52,7 +52,7 @@
|
|||||||
transition: transform 0.2s ease;
|
transition: transform 0.2s ease;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<script type="module" crossorigin src="/assets/index-BVxBNLM-.js"></script>
|
<script type="module" crossorigin src="/assets/index-ZFZ_Bqjc.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">
|
||||||
|
|||||||
@ -40,11 +40,26 @@ export class ApprovalService {
|
|||||||
const now = new Date();
|
const now = new Date();
|
||||||
|
|
||||||
// Calculate elapsed hours using working hours logic (with pause handling)
|
// Calculate elapsed hours using working hours logic (with pause handling)
|
||||||
const pauseInfo = (level as any).isPaused ? {
|
// Case 1: Level is currently paused (isPaused = true)
|
||||||
isPaused: (level as any).isPaused,
|
// Case 2: Level was paused and resumed (isPaused = false but pauseElapsedHours and pauseResumeDate exist)
|
||||||
|
const isPausedLevel = (level as any).isPaused;
|
||||||
|
const wasResumed = !isPausedLevel &&
|
||||||
|
(level as any).pauseElapsedHours !== null &&
|
||||||
|
(level as any).pauseElapsedHours !== undefined &&
|
||||||
|
(level as any).pauseResumeDate !== null;
|
||||||
|
|
||||||
|
const pauseInfo = isPausedLevel ? {
|
||||||
|
// Level is currently paused - return frozen elapsed hours at pause time
|
||||||
|
isPaused: true,
|
||||||
pausedAt: (level as any).pausedAt,
|
pausedAt: (level as any).pausedAt,
|
||||||
pauseElapsedHours: (level as any).pauseElapsedHours,
|
pauseElapsedHours: (level as any).pauseElapsedHours,
|
||||||
pauseResumeDate: (level as any).pauseResumeDate
|
pauseResumeDate: (level as any).pauseResumeDate
|
||||||
|
} : wasResumed ? {
|
||||||
|
// Level was paused but has been resumed - add pre-pause elapsed hours + time since resume
|
||||||
|
isPaused: false,
|
||||||
|
pausedAt: null,
|
||||||
|
pauseElapsedHours: Number((level as any).pauseElapsedHours), // Pre-pause elapsed hours
|
||||||
|
pauseResumeDate: (level as any).pauseResumeDate // Actual resume timestamp
|
||||||
} : undefined;
|
} : undefined;
|
||||||
|
|
||||||
const elapsedHours = await calculateElapsedWorkingHours(
|
const elapsedHours = await calculateElapsedWorkingHours(
|
||||||
|
|||||||
@ -244,18 +244,25 @@ export class PauseService {
|
|||||||
const tatHours = Number((level as any).tatHours);
|
const tatHours = Number((level as any).tatHours);
|
||||||
const remainingHours = Math.max(0, tatHours - pauseElapsedHours);
|
const remainingHours = Math.max(0, tatHours - pauseElapsedHours);
|
||||||
|
|
||||||
|
// Get which alerts have already been sent (to avoid re-sending on resume)
|
||||||
|
const tat50AlertSent = (level as any).tat50AlertSent || false;
|
||||||
|
const tat75AlertSent = (level as any).tat75AlertSent || false;
|
||||||
|
const tatBreached = (level as any).tatBreached || false;
|
||||||
|
|
||||||
// Update approval level - resume TAT
|
// Update approval level - resume TAT
|
||||||
|
// IMPORTANT: Keep pauseElapsedHours and store resumedAt (pauseResumeDate repurposed)
|
||||||
|
// This allows SLA calculation to correctly add pre-pause elapsed time
|
||||||
await level.update({
|
await level.update({
|
||||||
isPaused: false,
|
isPaused: false,
|
||||||
pausedAt: null as any,
|
pausedAt: null as any,
|
||||||
pausedBy: null as any,
|
pausedBy: null as any,
|
||||||
pauseReason: null as any,
|
pauseReason: null as any,
|
||||||
pauseResumeDate: null as any,
|
pauseResumeDate: now, // Store actual resume time (repurposed from scheduled resume date)
|
||||||
pauseTatStartTime: null as any,
|
// pauseTatStartTime: null as any, // Keep original TAT start time for reference
|
||||||
pauseElapsedHours: null as any,
|
// pauseElapsedHours is intentionally NOT cleared - needed for SLA calculations
|
||||||
status: ApprovalStatus.IN_PROGRESS,
|
status: ApprovalStatus.IN_PROGRESS,
|
||||||
tatStartTime: now, // Reset TAT start time to now
|
tatStartTime: now, // Reset TAT start time to now for new elapsed calculation
|
||||||
levelStartTime: now
|
levelStartTime: now // This is the new start time from resume
|
||||||
});
|
});
|
||||||
|
|
||||||
// Update workflow - restore previous status or default to PENDING
|
// Update workflow - restore previous status or default to PENDING
|
||||||
@ -272,15 +279,29 @@ export class PauseService {
|
|||||||
status: previousStatus // Restore previous status (PENDING or IN_PROGRESS)
|
status: previousStatus // Restore previous status (PENDING or IN_PROGRESS)
|
||||||
});
|
});
|
||||||
|
|
||||||
// Reschedule TAT jobs from resume time
|
// Reschedule TAT jobs from resume time - only for alerts that haven't been sent yet
|
||||||
if (remainingHours > 0) {
|
if (remainingHours > 0) {
|
||||||
await tatSchedulerService.scheduleTatJobs(
|
// Calculate which thresholds are still pending based on remaining time
|
||||||
|
const percentageUsedAtPause = tatHours > 0 ? (pauseElapsedHours / tatHours) * 100 : 0;
|
||||||
|
|
||||||
|
// Only schedule jobs for thresholds that:
|
||||||
|
// 1. Haven't been sent yet
|
||||||
|
// 2. Haven't been passed yet (based on percentage used at pause)
|
||||||
|
await tatSchedulerService.scheduleTatJobsOnResume(
|
||||||
requestId,
|
requestId,
|
||||||
(level as any).levelId,
|
(level as any).levelId,
|
||||||
(level as any).approverId,
|
(level as any).approverId,
|
||||||
remainingHours, // Use remaining hours, not original TAT
|
remainingHours, // Remaining TAT hours
|
||||||
now,
|
now, // Start from now
|
||||||
priority as any
|
priority as any,
|
||||||
|
{
|
||||||
|
// Pass which alerts were already sent
|
||||||
|
tat50AlertSent: tat50AlertSent,
|
||||||
|
tat75AlertSent: tat75AlertSent,
|
||||||
|
tatBreached: tatBreached,
|
||||||
|
// Pass percentage used at pause to determine which thresholds are still relevant
|
||||||
|
percentageUsedAtPause: percentageUsedAtPause
|
||||||
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -153,6 +153,178 @@ export class TatSchedulerService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Schedule TAT jobs on resume - only schedules jobs for alerts that haven't been sent yet
|
||||||
|
* @param requestId - The workflow request ID
|
||||||
|
* @param levelId - The approval level ID
|
||||||
|
* @param approverId - The approver user ID
|
||||||
|
* @param remainingTatHours - Remaining TAT duration in hours (from resume point)
|
||||||
|
* @param startTime - Resume start time
|
||||||
|
* @param priority - Request priority
|
||||||
|
* @param alertStatus - Object indicating which alerts have already been sent and percentage used at pause
|
||||||
|
*/
|
||||||
|
async scheduleTatJobsOnResume(
|
||||||
|
requestId: string,
|
||||||
|
levelId: string,
|
||||||
|
approverId: string,
|
||||||
|
remainingTatHours: number,
|
||||||
|
startTime: Date,
|
||||||
|
priority: Priority = Priority.STANDARD,
|
||||||
|
alertStatus: {
|
||||||
|
tat50AlertSent: boolean;
|
||||||
|
tat75AlertSent: boolean;
|
||||||
|
tatBreached: boolean;
|
||||||
|
percentageUsedAtPause: number;
|
||||||
|
}
|
||||||
|
): Promise<void> {
|
||||||
|
try {
|
||||||
|
if (!tatQueue) {
|
||||||
|
logger.warn(`[TAT Scheduler] TAT queue not available (Redis not connected). Skipping TAT job scheduling on resume.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const now = startTime;
|
||||||
|
const isExpress = priority === Priority.EXPRESS;
|
||||||
|
|
||||||
|
// Get current thresholds from database configuration
|
||||||
|
const thresholds = await getTatThresholds();
|
||||||
|
|
||||||
|
// Calculate original TAT from remaining + elapsed
|
||||||
|
// Example: If 35 min used (58.33%) and 25 min remaining, original TAT = 60 min
|
||||||
|
const elapsedHours = alertStatus.percentageUsedAtPause > 0
|
||||||
|
? (remainingTatHours * alertStatus.percentageUsedAtPause) / (100 - alertStatus.percentageUsedAtPause)
|
||||||
|
: 0;
|
||||||
|
const originalTatHours = elapsedHours + remainingTatHours;
|
||||||
|
|
||||||
|
logger.info(`[TAT Scheduler] ========== RESUME TAT SCHEDULING ==========`);
|
||||||
|
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
|
||||||
|
const jobsToSchedule: Array<{
|
||||||
|
type: 'threshold1' | 'threshold2' | 'breach';
|
||||||
|
threshold: number;
|
||||||
|
alreadySent: boolean;
|
||||||
|
alreadyPassed: boolean;
|
||||||
|
hoursFromNow: number;
|
||||||
|
}> = [];
|
||||||
|
|
||||||
|
// Threshold 1 (e.g., 50%)
|
||||||
|
// Skip if: already sent OR already passed the threshold
|
||||||
|
if (!alertStatus.tat50AlertSent && alertStatus.percentageUsedAtPause < thresholds.first) {
|
||||||
|
// Calculate: How many hours from NOW until we reach this threshold?
|
||||||
|
// Formula: (thresholdHours - elapsedHours)
|
||||||
|
// thresholdHours = originalTatHours * (threshold/100)
|
||||||
|
const thresholdHours = originalTatHours * (thresholds.first / 100);
|
||||||
|
const hoursFromNow = thresholdHours - elapsedHours;
|
||||||
|
|
||||||
|
if (hoursFromNow > 0) {
|
||||||
|
jobsToSchedule.push({
|
||||||
|
type: 'threshold1',
|
||||||
|
threshold: thresholds.first,
|
||||||
|
alreadySent: false,
|
||||||
|
alreadyPassed: false,
|
||||||
|
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%)
|
||||||
|
if (!alertStatus.tat75AlertSent && alertStatus.percentageUsedAtPause < thresholds.second) {
|
||||||
|
const thresholdHours = originalTatHours * (thresholds.second / 100);
|
||||||
|
const hoursFromNow = thresholdHours - elapsedHours;
|
||||||
|
|
||||||
|
if (hoursFromNow > 0) {
|
||||||
|
jobsToSchedule.push({
|
||||||
|
type: 'threshold2',
|
||||||
|
threshold: thresholds.second,
|
||||||
|
alreadySent: false,
|
||||||
|
alreadyPassed: false,
|
||||||
|
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%)
|
||||||
|
if (!alertStatus.tatBreached) {
|
||||||
|
// Breach is always scheduled for the end of remaining TAT
|
||||||
|
jobsToSchedule.push({
|
||||||
|
type: 'breach',
|
||||||
|
threshold: 100,
|
||||||
|
alreadySent: false,
|
||||||
|
alreadyPassed: false,
|
||||||
|
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) {
|
||||||
|
logger.info(`[TAT Scheduler] No TAT jobs to schedule (all alerts already sent or thresholds already passed)`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate actual times and schedule jobs
|
||||||
|
for (const job of jobsToSchedule) {
|
||||||
|
let targetTime: Date;
|
||||||
|
|
||||||
|
if (isExpress) {
|
||||||
|
targetTime = (await addWorkingHoursExpress(now, job.hoursFromNow)).toDate();
|
||||||
|
} else {
|
||||||
|
targetTime = (await addWorkingHours(now, job.hoursFromNow)).toDate();
|
||||||
|
}
|
||||||
|
|
||||||
|
const delay = calculateDelay(targetTime);
|
||||||
|
|
||||||
|
if (delay < 0) {
|
||||||
|
logger.warn(`[TAT Scheduler] Skipping ${job.type} - calculated time is in past`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const jobId = `tat-${job.type}-${requestId}-${levelId}`;
|
||||||
|
|
||||||
|
await tatQueue.add(
|
||||||
|
job.type,
|
||||||
|
{
|
||||||
|
type: job.type,
|
||||||
|
threshold: job.threshold,
|
||||||
|
requestId,
|
||||||
|
levelId,
|
||||||
|
approverId
|
||||||
|
},
|
||||||
|
{
|
||||||
|
delay: delay,
|
||||||
|
jobId: jobId,
|
||||||
|
removeOnComplete: {
|
||||||
|
age: 3600,
|
||||||
|
count: 1000
|
||||||
|
},
|
||||||
|
removeOnFail: false
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
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] ========== RESUME SCHEDULING COMPLETE ==========`);
|
||||||
|
logger.info(`[TAT Scheduler] ✅ ${jobsToSchedule.length} TAT job(s) scheduled for request ${requestId}`);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`[TAT Scheduler] Failed to schedule TAT jobs on resume:`, error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Cancel TAT jobs for a specific approval level
|
* Cancel TAT jobs for a specific approval level
|
||||||
* Useful when an approver acts before TAT expires
|
* Useful when an approver acts before TAT expires
|
||||||
|
|||||||
@ -775,17 +775,21 @@ export class WorkflowService {
|
|||||||
const currentLevel = await ApprovalLevel.findOne({
|
const currentLevel = await ApprovalLevel.findOne({
|
||||||
where: {
|
where: {
|
||||||
requestId: (wf as any).requestId,
|
requestId: (wf as any).requestId,
|
||||||
status: { [Op.in]: ['PENDING', 'IN_PROGRESS'] as any }, // IN_PROGRESS for approval levels, not workflows
|
status: { [Op.in]: ['PENDING', 'IN_PROGRESS', 'PAUSED'] as any }, // Include PAUSED to show SLA for paused levels
|
||||||
},
|
},
|
||||||
order: [['levelNumber', 'ASC']],
|
order: [['levelNumber', 'ASC']],
|
||||||
include: [{ model: User, as: 'approver', attributes: ['userId', 'email', 'displayName'] }]
|
include: [{ model: User, as: 'approver', attributes: ['userId', 'email', 'displayName'] }],
|
||||||
|
// Include pause-related fields for SLA calculation
|
||||||
|
attributes: ['levelId', 'levelNumber', 'levelName', 'approverId', 'approverEmail', 'approverName',
|
||||||
|
'tatHours', 'tatDays', 'status', 'levelStartTime', 'tatStartTime', 'levelEndTime',
|
||||||
|
'isPaused', 'pausedAt', 'pauseElapsedHours', 'pauseResumeDate', 'elapsedHours']
|
||||||
});
|
});
|
||||||
|
|
||||||
// Fetch all approval levels for this request
|
// Fetch all approval levels for this request (including pause fields for SLA calculation)
|
||||||
const approvals = await ApprovalLevel.findAll({
|
const approvals = await ApprovalLevel.findAll({
|
||||||
where: { requestId: (wf as any).requestId },
|
where: { requestId: (wf as any).requestId },
|
||||||
order: [['levelNumber', 'ASC']],
|
order: [['levelNumber', 'ASC']],
|
||||||
attributes: ['levelId', 'levelNumber', 'levelName', 'approverId', 'approverEmail', 'approverName', 'tatHours', 'tatDays', 'status', 'levelStartTime', 'tatStartTime']
|
attributes: ['levelId', 'levelNumber', 'levelName', 'approverId', 'approverEmail', 'approverName', 'tatHours', 'tatDays', 'status', 'levelStartTime', 'tatStartTime', 'isPaused', 'pausedAt', 'pauseElapsedHours', 'pauseResumeDate', 'elapsedHours']
|
||||||
});
|
});
|
||||||
|
|
||||||
// Calculate total TAT hours from all approvals
|
// Calculate total TAT hours from all approvals
|
||||||
@ -798,35 +802,131 @@ export class WorkflowService {
|
|||||||
|
|
||||||
const priority = ((wf as any).priority || 'standard').toString().toLowerCase();
|
const priority = ((wf as any).priority || 'standard').toString().toLowerCase();
|
||||||
|
|
||||||
// Calculate OVERALL request SLA (from submission to total deadline)
|
// Calculate OVERALL request SLA based on cumulative elapsed hours from all levels
|
||||||
const { calculateSLAStatus } = require('@utils/tatTimeUtils');
|
// This correctly accounts for pause periods since each level's elapsed is pause-adjusted
|
||||||
|
const { calculateSLAStatus, addWorkingHours, addWorkingHoursExpress } = require('@utils/tatTimeUtils');
|
||||||
const submissionDate = (wf as any).submissionDate;
|
const submissionDate = (wf as any).submissionDate;
|
||||||
const closureDate = (wf as any).closureDate;
|
const closureDate = (wf as any).closureDate;
|
||||||
// For completed requests, use closure_date; for active requests, use current time
|
|
||||||
const overallEndDate = closureDate || null;
|
|
||||||
|
|
||||||
let overallSLA = null;
|
let overallSLA = null;
|
||||||
|
|
||||||
if (submissionDate && totalTatHours > 0) {
|
if (submissionDate && totalTatHours > 0) {
|
||||||
try {
|
try {
|
||||||
overallSLA = await calculateSLAStatus(submissionDate, totalTatHours, priority, overallEndDate);
|
// Calculate total elapsed hours by summing from all levels (pause-adjusted)
|
||||||
|
let totalElapsedHours = 0;
|
||||||
|
|
||||||
|
for (const approval of approvals) {
|
||||||
|
const status = ((approval as any).status || '').toString().toUpperCase();
|
||||||
|
|
||||||
|
if (status === 'APPROVED' || status === 'REJECTED') {
|
||||||
|
// For completed levels, use stored elapsedHours
|
||||||
|
totalElapsedHours += Number((approval as any).elapsedHours || 0);
|
||||||
|
} else if (status === 'SKIPPED') {
|
||||||
|
continue;
|
||||||
|
} else if (status === 'PENDING' || status === 'IN_PROGRESS' || status === 'PAUSED') {
|
||||||
|
// For active/paused levels, calculate with pause handling
|
||||||
|
const levelStartTime = (approval as any).levelStartTime || (approval as any).tatStartTime;
|
||||||
|
const levelTatHours = Number((approval as any).tatHours || 0);
|
||||||
|
|
||||||
|
if (levelStartTime && levelTatHours > 0) {
|
||||||
|
const isPausedLevel = status === 'PAUSED' || (approval as any).isPaused;
|
||||||
|
const wasResumed = !isPausedLevel &&
|
||||||
|
(approval as any).pauseElapsedHours !== null &&
|
||||||
|
(approval as any).pauseElapsedHours !== undefined &&
|
||||||
|
(approval as any).pauseResumeDate !== null;
|
||||||
|
|
||||||
|
const pauseInfo = isPausedLevel ? {
|
||||||
|
isPaused: true,
|
||||||
|
pauseElapsedHours: (approval as any).pauseElapsedHours
|
||||||
|
} : wasResumed ? {
|
||||||
|
isPaused: false,
|
||||||
|
pauseElapsedHours: Number((approval as any).pauseElapsedHours),
|
||||||
|
pauseResumeDate: (approval as any).pauseResumeDate
|
||||||
|
} : undefined;
|
||||||
|
|
||||||
|
const levelSLA = await calculateSLAStatus(levelStartTime, levelTatHours, priority, null, pauseInfo);
|
||||||
|
totalElapsedHours += levelSLA.elapsedHours || 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate overall SLA metrics
|
||||||
|
const totalRemainingHours = Math.max(0, totalTatHours - totalElapsedHours);
|
||||||
|
const percentageUsed = totalTatHours > 0
|
||||||
|
? Math.min(100, Math.round((totalElapsedHours / totalTatHours) * 100))
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
// Determine status
|
||||||
|
let overallStatus: 'on_track' | 'approaching' | 'critical' | 'breached' = 'on_track';
|
||||||
|
if (percentageUsed >= 100) overallStatus = 'breached';
|
||||||
|
else if (percentageUsed >= 80) overallStatus = 'critical';
|
||||||
|
else if (percentageUsed >= 60) overallStatus = 'approaching';
|
||||||
|
|
||||||
|
// Format time display
|
||||||
|
const formatTime = (hours: number) => {
|
||||||
|
if (hours < 1) return `${Math.round(hours * 60)}m`;
|
||||||
|
const wholeHours = Math.floor(hours);
|
||||||
|
const minutes = Math.round((hours - wholeHours) * 60);
|
||||||
|
if (minutes > 0) return `${wholeHours}h ${minutes}m`;
|
||||||
|
return `${wholeHours}h`;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check if any level is paused
|
||||||
|
const isAnyLevelPaused = approvals.some((a: any) =>
|
||||||
|
((a.status || '').toString().toUpperCase() === 'PAUSED' || a.isPaused === true)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Calculate deadline
|
||||||
|
const deadline = priority === 'express'
|
||||||
|
? (await addWorkingHoursExpress(submissionDate, totalTatHours)).toDate()
|
||||||
|
: (await addWorkingHours(submissionDate, totalTatHours)).toDate();
|
||||||
|
|
||||||
|
overallSLA = {
|
||||||
|
elapsedHours: totalElapsedHours,
|
||||||
|
remainingHours: totalRemainingHours,
|
||||||
|
percentageUsed,
|
||||||
|
status: overallStatus,
|
||||||
|
isPaused: isAnyLevelPaused,
|
||||||
|
deadline: deadline.toISOString(),
|
||||||
|
elapsedText: formatTime(totalElapsedHours),
|
||||||
|
remainingText: formatTime(totalRemainingHours)
|
||||||
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('[Workflow] Error calculating overall SLA:', error);
|
logger.error('[Workflow] Error calculating overall SLA:', error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate current level SLA (if there's an active level)
|
// Calculate current level SLA (if there's an active level, including paused)
|
||||||
let currentLevelSLA = null;
|
let currentLevelSLA = null;
|
||||||
if (currentLevel) {
|
if (currentLevel) {
|
||||||
const levelStartTime = (currentLevel as any).levelStartTime || (currentLevel as any).tatStartTime;
|
const levelStartTime = (currentLevel as any).levelStartTime || (currentLevel as any).tatStartTime;
|
||||||
const levelTatHours = Number((currentLevel as any).tatHours || 0);
|
const levelTatHours = Number((currentLevel as any).tatHours || 0);
|
||||||
// For completed levels, use the level's completion time (if available)
|
// For completed levels, use the level's completion time (if available)
|
||||||
// Otherwise, if request is completed, use closure_date
|
// Otherwise, if request is completed, use closure_date
|
||||||
const levelEndDate = (currentLevel as any).completedAt || closureDate || null;
|
const levelEndDate = (currentLevel as any).levelEndTime || closureDate || null;
|
||||||
|
|
||||||
|
// Prepare pause info for SLA calculation
|
||||||
|
const isPausedLevel = (currentLevel as any).status === 'PAUSED' || (currentLevel as any).isPaused;
|
||||||
|
const wasResumed = !isPausedLevel &&
|
||||||
|
(currentLevel as any).pauseElapsedHours !== null &&
|
||||||
|
(currentLevel as any).pauseElapsedHours !== undefined &&
|
||||||
|
(currentLevel as any).pauseResumeDate !== null;
|
||||||
|
|
||||||
|
const pauseInfo = isPausedLevel ? {
|
||||||
|
isPaused: true,
|
||||||
|
pausedAt: (currentLevel as any).pausedAt,
|
||||||
|
pauseElapsedHours: (currentLevel as any).pauseElapsedHours,
|
||||||
|
pauseResumeDate: (currentLevel as any).pauseResumeDate
|
||||||
|
} : wasResumed ? {
|
||||||
|
isPaused: false,
|
||||||
|
pausedAt: null,
|
||||||
|
pauseElapsedHours: Number((currentLevel as any).pauseElapsedHours),
|
||||||
|
pauseResumeDate: (currentLevel as any).pauseResumeDate
|
||||||
|
} : undefined;
|
||||||
|
|
||||||
if (levelStartTime && levelTatHours > 0) {
|
if (levelStartTime && levelTatHours > 0) {
|
||||||
try {
|
try {
|
||||||
currentLevelSLA = await calculateSLAStatus(levelStartTime, levelTatHours, priority, levelEndDate);
|
currentLevelSLA = await calculateSLAStatus(levelStartTime, levelTatHours, priority, levelEndDate, pauseInfo);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('[Workflow] Error calculating current level SLA:', error);
|
logger.error('[Workflow] Error calculating current level SLA:', error);
|
||||||
}
|
}
|
||||||
@ -848,6 +948,13 @@ export class WorkflowService {
|
|||||||
department: (wf as any).initiator?.department,
|
department: (wf as any).initiator?.department,
|
||||||
totalLevels: (wf as any).totalLevels,
|
totalLevels: (wf as any).totalLevels,
|
||||||
totalTatHours: totalTatHours,
|
totalTatHours: totalTatHours,
|
||||||
|
isPaused: (wf as any).isPaused || false, // Workflow pause status
|
||||||
|
pauseInfo: (wf as any).isPaused ? {
|
||||||
|
isPaused: true,
|
||||||
|
pausedAt: (wf as any).pausedAt,
|
||||||
|
pauseReason: (wf as any).pauseReason,
|
||||||
|
pauseResumeDate: (wf as any).pauseResumeDate,
|
||||||
|
} : null,
|
||||||
currentLevel: currentLevel ? (currentLevel as any).levelNumber : null,
|
currentLevel: currentLevel ? (currentLevel as any).levelNumber : null,
|
||||||
currentApprover: currentLevel ? {
|
currentApprover: currentLevel ? {
|
||||||
userId: (currentLevel as any).approverId,
|
userId: (currentLevel as any).approverId,
|
||||||
@ -855,7 +962,9 @@ export class WorkflowService {
|
|||||||
name: (currentLevel as any).approverName,
|
name: (currentLevel as any).approverName,
|
||||||
levelStartTime: (currentLevel as any).levelStartTime,
|
levelStartTime: (currentLevel as any).levelStartTime,
|
||||||
tatHours: (currentLevel as any).tatHours,
|
tatHours: (currentLevel as any).tatHours,
|
||||||
sla: currentLevelSLA, // ← Backend-calculated SLA for current level
|
isPaused: (currentLevel as any).status === 'PAUSED' || (currentLevel as any).isPaused,
|
||||||
|
pauseElapsedHours: (currentLevel as any).pauseElapsedHours,
|
||||||
|
sla: currentLevelSLA, // ← Backend-calculated SLA for current level (includes pause handling)
|
||||||
} : null,
|
} : null,
|
||||||
approvals: approvals.map((a: any) => ({
|
approvals: approvals.map((a: any) => ({
|
||||||
levelId: a.levelId,
|
levelId: a.levelId,
|
||||||
@ -2576,12 +2685,25 @@ export class WorkflowService {
|
|||||||
|
|
||||||
if (levelStartTime && tatHours > 0) {
|
if (levelStartTime && tatHours > 0) {
|
||||||
try {
|
try {
|
||||||
// Prepare pause info for SLA calculation if level is paused
|
// Prepare pause info for SLA calculation
|
||||||
|
// Case 1: Level is currently paused
|
||||||
|
// Case 2: Level was paused and resumed (pauseElapsedHours and pauseResumeDate are set)
|
||||||
|
const wasResumed = !isPausedLevel &&
|
||||||
|
approval.pauseElapsedHours !== null &&
|
||||||
|
approval.pauseElapsedHours !== undefined &&
|
||||||
|
approval.pauseResumeDate !== null;
|
||||||
|
|
||||||
const pauseInfo = isPausedLevel ? {
|
const pauseInfo = isPausedLevel ? {
|
||||||
isPaused: true,
|
isPaused: true,
|
||||||
pausedAt: approval.pausedAt,
|
pausedAt: approval.pausedAt,
|
||||||
pauseElapsedHours: approval.pauseElapsedHours,
|
pauseElapsedHours: approval.pauseElapsedHours,
|
||||||
pauseResumeDate: approval.pauseResumeDate
|
pauseResumeDate: approval.pauseResumeDate
|
||||||
|
} : wasResumed ? {
|
||||||
|
// Level was paused but has been resumed
|
||||||
|
isPaused: false,
|
||||||
|
pausedAt: null,
|
||||||
|
pauseElapsedHours: Number(approval.pauseElapsedHours), // Pre-pause elapsed hours
|
||||||
|
pauseResumeDate: approval.pauseResumeDate // Actual resume timestamp
|
||||||
} : undefined;
|
} : undefined;
|
||||||
|
|
||||||
// Get comprehensive SLA status from backend utility
|
// Get comprehensive SLA status from backend utility
|
||||||
@ -2618,13 +2740,83 @@ export class WorkflowService {
|
|||||||
return approvalData;
|
return approvalData;
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Calculate overall request SLA
|
// Calculate overall request SLA based on cumulative elapsed hours from all levels
|
||||||
|
// This correctly accounts for pause periods since each level's elapsedHours is pause-adjusted
|
||||||
const submissionDate = (workflow as any).submissionDate;
|
const submissionDate = (workflow as any).submissionDate;
|
||||||
const totalTatHours = updatedApprovals.reduce((sum, a) => sum + Number(a.tatHours || 0), 0);
|
const totalTatHours = updatedApprovals.reduce((sum, a) => sum + Number(a.tatHours || 0), 0);
|
||||||
let overallSLA = null;
|
let overallSLA = null;
|
||||||
|
|
||||||
if (submissionDate && totalTatHours > 0) {
|
if (submissionDate && totalTatHours > 0) {
|
||||||
overallSLA = await calculateSLAStatus(submissionDate, totalTatHours, priority);
|
// Calculate total elapsed hours by summing elapsed hours from all levels
|
||||||
|
// This ensures pause periods are correctly excluded from the overall calculation
|
||||||
|
let totalElapsedHours = 0;
|
||||||
|
|
||||||
|
for (const approval of updatedApprovals) {
|
||||||
|
const status = (approval.status || '').toString().toUpperCase();
|
||||||
|
|
||||||
|
if (status === 'APPROVED' || status === 'REJECTED') {
|
||||||
|
// For completed levels, use the stored elapsedHours (already pause-adjusted from when level was completed)
|
||||||
|
totalElapsedHours += Number(approval.elapsedHours || 0);
|
||||||
|
} else if (status === 'SKIPPED') {
|
||||||
|
// Skipped levels don't contribute to elapsed time
|
||||||
|
continue;
|
||||||
|
} else if (status === 'PENDING' || status === 'IN_PROGRESS' || status === 'PAUSED') {
|
||||||
|
// For active/paused levels, use the SLA-calculated elapsedHours (pause-adjusted)
|
||||||
|
if (approval.sla?.elapsedHours !== undefined) {
|
||||||
|
totalElapsedHours += Number(approval.sla.elapsedHours);
|
||||||
|
} else {
|
||||||
|
totalElapsedHours += Number(approval.elapsedHours || 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// WAITING levels haven't started yet, so no elapsed time
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate overall SLA metrics based on cumulative elapsed hours
|
||||||
|
const totalRemainingHours = Math.max(0, totalTatHours - totalElapsedHours);
|
||||||
|
const percentageUsed = totalTatHours > 0
|
||||||
|
? Math.min(100, Math.round((totalElapsedHours / totalTatHours) * 100))
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
// Determine overall status
|
||||||
|
let overallStatus: 'on_track' | 'approaching' | 'critical' | 'breached' = 'on_track';
|
||||||
|
if (percentageUsed >= 100) {
|
||||||
|
overallStatus = 'breached';
|
||||||
|
} else if (percentageUsed >= 80) {
|
||||||
|
overallStatus = 'critical';
|
||||||
|
} else if (percentageUsed >= 60) {
|
||||||
|
overallStatus = 'approaching';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format time display
|
||||||
|
const formatTime = (hours: number) => {
|
||||||
|
if (hours < 1) return `${Math.round(hours * 60)}m`;
|
||||||
|
const wholeHours = Math.floor(hours);
|
||||||
|
const minutes = Math.round((hours - wholeHours) * 60);
|
||||||
|
if (minutes > 0) return `${wholeHours}h ${minutes}m`;
|
||||||
|
return `${wholeHours}h`;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check if any level is currently paused
|
||||||
|
const isAnyLevelPaused = updatedApprovals.some(a =>
|
||||||
|
(a.status || '').toString().toUpperCase() === 'PAUSED' || a.isPaused === true
|
||||||
|
);
|
||||||
|
|
||||||
|
// Calculate deadline using the original method (for deadline display only)
|
||||||
|
const { addWorkingHours, addWorkingHoursExpress } = require('@utils/tatTimeUtils');
|
||||||
|
const deadline = priority === 'express'
|
||||||
|
? (await addWorkingHoursExpress(submissionDate, totalTatHours)).toDate()
|
||||||
|
: (await addWorkingHours(submissionDate, totalTatHours)).toDate();
|
||||||
|
|
||||||
|
overallSLA = {
|
||||||
|
elapsedHours: totalElapsedHours,
|
||||||
|
remainingHours: totalRemainingHours,
|
||||||
|
percentageUsed,
|
||||||
|
status: overallStatus,
|
||||||
|
isPaused: isAnyLevelPaused,
|
||||||
|
deadline: deadline.toISOString(),
|
||||||
|
elapsedText: formatTime(totalElapsedHours),
|
||||||
|
remainingText: formatTime(totalRemainingHours)
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update summary to include comprehensive SLA
|
// Update summary to include comprehensive SLA
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user