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};
|
||||
//# sourceMappingURL=conclusionApi-U5wrbP17.js.map
|
||||
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-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;
|
||||
}
|
||||
</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/radix-vendor-C2EbRL2a.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/utils-vendor-DHm03ykU.js">
|
||||
|
||||
@ -40,11 +40,26 @@ export class ApprovalService {
|
||||
const now = new Date();
|
||||
|
||||
// Calculate elapsed hours using working hours logic (with pause handling)
|
||||
const pauseInfo = (level as any).isPaused ? {
|
||||
isPaused: (level as any).isPaused,
|
||||
// Case 1: Level is currently paused (isPaused = true)
|
||||
// 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,
|
||||
pauseElapsedHours: (level as any).pauseElapsedHours,
|
||||
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;
|
||||
|
||||
const elapsedHours = await calculateElapsedWorkingHours(
|
||||
|
||||
@ -244,18 +244,25 @@ export class PauseService {
|
||||
const tatHours = Number((level as any).tatHours);
|
||||
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
|
||||
// IMPORTANT: Keep pauseElapsedHours and store resumedAt (pauseResumeDate repurposed)
|
||||
// This allows SLA calculation to correctly add pre-pause elapsed time
|
||||
await level.update({
|
||||
isPaused: false,
|
||||
pausedAt: null as any,
|
||||
pausedBy: null as any,
|
||||
pauseReason: null as any,
|
||||
pauseResumeDate: null as any,
|
||||
pauseTatStartTime: null as any,
|
||||
pauseElapsedHours: null as any,
|
||||
pauseResumeDate: now, // Store actual resume time (repurposed from scheduled resume date)
|
||||
// pauseTatStartTime: null as any, // Keep original TAT start time for reference
|
||||
// pauseElapsedHours is intentionally NOT cleared - needed for SLA calculations
|
||||
status: ApprovalStatus.IN_PROGRESS,
|
||||
tatStartTime: now, // Reset TAT start time to now
|
||||
levelStartTime: now
|
||||
tatStartTime: now, // Reset TAT start time to now for new elapsed calculation
|
||||
levelStartTime: now // This is the new start time from resume
|
||||
});
|
||||
|
||||
// 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)
|
||||
});
|
||||
|
||||
// Reschedule TAT jobs from resume time
|
||||
// Reschedule TAT jobs from resume time - only for alerts that haven't been sent yet
|
||||
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,
|
||||
(level as any).levelId,
|
||||
(level as any).approverId,
|
||||
remainingHours, // Use remaining hours, not original TAT
|
||||
now,
|
||||
priority as any
|
||||
remainingHours, // Remaining TAT hours
|
||||
now, // Start from now
|
||||
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
|
||||
* Useful when an approver acts before TAT expires
|
||||
|
||||
@ -775,17 +775,21 @@ export class WorkflowService {
|
||||
const currentLevel = await ApprovalLevel.findOne({
|
||||
where: {
|
||||
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']],
|
||||
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({
|
||||
where: { requestId: (wf as any).requestId },
|
||||
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
|
||||
@ -798,35 +802,131 @@ export class WorkflowService {
|
||||
|
||||
const priority = ((wf as any).priority || 'standard').toString().toLowerCase();
|
||||
|
||||
// Calculate OVERALL request SLA (from submission to total deadline)
|
||||
const { calculateSLAStatus } = require('@utils/tatTimeUtils');
|
||||
// Calculate OVERALL request SLA based on cumulative elapsed hours from all levels
|
||||
// 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 closureDate = (wf as any).closureDate;
|
||||
// For completed requests, use closure_date; for active requests, use current time
|
||||
const overallEndDate = closureDate || null;
|
||||
|
||||
let overallSLA = null;
|
||||
|
||||
if (submissionDate && totalTatHours > 0) {
|
||||
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) {
|
||||
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;
|
||||
if (currentLevel) {
|
||||
const levelStartTime = (currentLevel as any).levelStartTime || (currentLevel as any).tatStartTime;
|
||||
const levelTatHours = Number((currentLevel as any).tatHours || 0);
|
||||
// For completed levels, use the level's completion time (if available)
|
||||
// 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) {
|
||||
try {
|
||||
currentLevelSLA = await calculateSLAStatus(levelStartTime, levelTatHours, priority, levelEndDate);
|
||||
currentLevelSLA = await calculateSLAStatus(levelStartTime, levelTatHours, priority, levelEndDate, pauseInfo);
|
||||
} catch (error) {
|
||||
logger.error('[Workflow] Error calculating current level SLA:', error);
|
||||
}
|
||||
@ -848,6 +948,13 @@ export class WorkflowService {
|
||||
department: (wf as any).initiator?.department,
|
||||
totalLevels: (wf as any).totalLevels,
|
||||
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,
|
||||
currentApprover: currentLevel ? {
|
||||
userId: (currentLevel as any).approverId,
|
||||
@ -855,7 +962,9 @@ export class WorkflowService {
|
||||
name: (currentLevel as any).approverName,
|
||||
levelStartTime: (currentLevel as any).levelStartTime,
|
||||
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,
|
||||
approvals: approvals.map((a: any) => ({
|
||||
levelId: a.levelId,
|
||||
@ -2576,12 +2685,25 @@ export class WorkflowService {
|
||||
|
||||
if (levelStartTime && tatHours > 0) {
|
||||
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 ? {
|
||||
isPaused: true,
|
||||
pausedAt: approval.pausedAt,
|
||||
pauseElapsedHours: approval.pauseElapsedHours,
|
||||
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;
|
||||
|
||||
// Get comprehensive SLA status from backend utility
|
||||
@ -2618,13 +2740,83 @@ export class WorkflowService {
|
||||
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 totalTatHours = updatedApprovals.reduce((sum, a) => sum + Number(a.tatHours || 0), 0);
|
||||
let overallSLA = null;
|
||||
|
||||
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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user