tat pause rsume calclulation enhanced for accuracy
This commit is contained in:
parent
a4cf77eebf
commit
18620235d8
@ -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};
|
||||
//# sourceMappingURL=conclusionApi-DZ8g8n5O.js.map
|
||||
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-CLFPR2m0.js.map
|
||||
@ -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
1
build/assets/index-BJNKp6s1.js.map
Normal file
1
build/assets/index-BJNKp6s1.js.map
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
build/assets/index-DD2tGQ-m.css
Normal file
1
build/assets/index-DD2tGQ-m.css
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -1,20 +1,20 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<!-- CSP: Allows blob URLs for file previews and cross-origin API calls during development -->
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'self' blob:; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; script-src 'self'; img-src 'self' data: https: blob:; connect-src 'self' blob: data: http://localhost:5000 http://localhost:3000 ws://localhost:5000 ws://localhost:3000 wss://localhost:5000 wss://localhost:3000; frame-src 'self' blob:; font-src 'self' https://fonts.gstatic.com data:; object-src 'none'; base-uri 'self'; form-action 'self';" />
|
||||
<link rel="icon" type="image/svg+xml" href="/royal_enfield_logo.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="description" content="Royal Enfield Approval & Request Management Portal - Streamlined approval workflows for enterprise operations" />
|
||||
<meta name="theme-color" content="#2d4a3e" />
|
||||
<title>Royal Enfield | Approval Portal</title>
|
||||
|
||||
<!-- Preload critical fonts and icons -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
|
||||
<!-- Ensure proper icon rendering and layout -->
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<!-- CSP: Allows blob URLs for file previews and cross-origin API calls during development -->
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'self' blob:; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; script-src 'self'; img-src 'self' data: https: blob:; connect-src 'self' blob: data: http://localhost:5000 http://localhost:3000 ws://localhost:5000 ws://localhost:3000 wss://localhost:5000 wss://localhost:3000; frame-src 'self' blob:; font-src 'self' https://fonts.gstatic.com data:; object-src 'none'; base-uri 'self'; form-action 'self';" />
|
||||
<link rel="icon" type="image/svg+xml" href="/royal_enfield_logo.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="description" content="Royal Enfield Approval & Request Management Portal - Streamlined approval workflows for enterprise operations" />
|
||||
<meta name="theme-color" content="#2d4a3e" />
|
||||
<title>Royal Enfield | Approval Portal</title>
|
||||
|
||||
<!-- Preload critical fonts and icons -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
|
||||
<!-- Ensure proper icon rendering and layout -->
|
||||
<style>
|
||||
/* Ensure Lucide icons render properly */
|
||||
svg {
|
||||
@ -51,8 +51,8 @@
|
||||
transform: scale(1.05);
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
</style>
|
||||
<script type="module" crossorigin src="/assets/index-ZFZ_Bqjc.js"></script>
|
||||
</style>
|
||||
<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/radix-vendor-C2EbRL2a.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/utils-vendor-DHm03ykU.js">
|
||||
@ -60,10 +60,10 @@
|
||||
<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/router-vendor-1fSSvDCY.js">
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-CiGKtlR4.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-DD2tGQ-m.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
||||
@ -19,7 +19,7 @@ export async function handlePauseResumeJob(job: Job): Promise<void> {
|
||||
logger.warn(`[Pause Resume Processor] Unknown job type: ${type}`);
|
||||
}
|
||||
} 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
|
||||
}
|
||||
}
|
||||
|
||||
@ -30,15 +30,31 @@ try {
|
||||
});
|
||||
|
||||
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) => {
|
||||
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) {
|
||||
logger.error('[Pause Resume Worker] Failed to create worker:', workerError);
|
||||
logger.error('[Pause Resume Worker] Failed to create worker:', workerError?.message || workerError);
|
||||
pauseResumeWorker = null;
|
||||
}
|
||||
|
||||
|
||||
@ -66,7 +66,7 @@ export async function handleTatJob(job: Job<TatJobData>) {
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
// 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();
|
||||
|
||||
// Pass pause information if available
|
||||
const pauseInfo = (approvalLevel as any).isPaused ? {
|
||||
isPaused: (approvalLevel as any).isPaused,
|
||||
// IMPORTANT: Check both currently paused AND previously paused/resumed levels
|
||||
// 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,
|
||||
pauseElapsedHours: (approvalLevel as any).pauseElapsedHours,
|
||||
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;
|
||||
|
||||
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
|
||||
// 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!`;
|
||||
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
|
||||
await ApprovalLevel.update(
|
||||
{
|
||||
@ -191,33 +210,45 @@ export async function handleTatJob(job: Job<TatJobData>) {
|
||||
type === 'threshold2' ? 'HIGH' :
|
||||
'MEDIUM';
|
||||
|
||||
// Send notification to approver
|
||||
await notificationService.sendToUsers([approverId], {
|
||||
title: type === 'breach' ? 'TAT Breach Alert' : 'TAT Reminder',
|
||||
body: message,
|
||||
requestId,
|
||||
requestNumber,
|
||||
url: `/request/${requestNumber}`,
|
||||
type: type,
|
||||
priority: notificationPriority,
|
||||
actionRequired: type === 'breach' || type === 'threshold2' // Require action for critical alerts
|
||||
});
|
||||
// Send notification to approver (with error handling to prevent job failure)
|
||||
try {
|
||||
await notificationService.sendToUsers([approverId], {
|
||||
title: type === 'breach' ? 'TAT Breach Alert' : 'TAT Reminder',
|
||||
body: message,
|
||||
requestId,
|
||||
requestNumber,
|
||||
url: `/request/${requestNumber}`,
|
||||
type: type,
|
||||
priority: notificationPriority,
|
||||
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 (type === 'breach') {
|
||||
const initiatorId = (workflow as any).initiatorId;
|
||||
if (initiatorId && initiatorId !== approverId) {
|
||||
await notificationService.sendToUsers([initiatorId], {
|
||||
title: 'TAT Breach - Request Delayed',
|
||||
body: `Your request ${requestNumber}: "${title}" has exceeded its TAT. The approver has been notified.`,
|
||||
requestId,
|
||||
requestNumber,
|
||||
url: `/request/${requestNumber}`,
|
||||
type: 'tat_breach_initiator',
|
||||
priority: 'HIGH',
|
||||
actionRequired: false
|
||||
});
|
||||
logger.info(`[TAT Processor] Breach notification sent to initiator ${initiatorId}`);
|
||||
try {
|
||||
await notificationService.sendToUsers([initiatorId], {
|
||||
title: 'TAT Breach - Request Delayed',
|
||||
body: `Your request ${requestNumber}: "${title}" has exceeded its TAT. The approver has been notified.`,
|
||||
requestId,
|
||||
requestNumber,
|
||||
url: `/request/${requestNumber}`,
|
||||
type: 'tat_breach_initiator',
|
||||
priority: 'HIGH',
|
||||
actionRequired: false
|
||||
});
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -30,15 +30,23 @@ try {
|
||||
});
|
||||
|
||||
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) => {
|
||||
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) {
|
||||
logger.error('[TAT Worker] Failed to create worker:', workerError);
|
||||
logger.error('[TAT Worker] Failed to create worker:', workerError?.message || workerError);
|
||||
tatWorker = null;
|
||||
}
|
||||
|
||||
|
||||
@ -8,20 +8,18 @@ import logger from '@utils/logger';
|
||||
*/
|
||||
export async function seedDefaultConfigurations(): Promise<void> {
|
||||
try {
|
||||
// Check if configurations already exist
|
||||
const count = await sequelize.query(
|
||||
'SELECT COUNT(*) as count FROM admin_configurations',
|
||||
{ type: QueryTypes.SELECT }
|
||||
);
|
||||
|
||||
if (count && (count[0] as any).count > 0) {
|
||||
// Table has data, skip seeding silently
|
||||
return;
|
||||
// Ensure pgcrypto extension is available for gen_random_uuid()
|
||||
try {
|
||||
await sequelize.query('CREATE EXTENSION IF NOT EXISTS "pgcrypto"', { type: QueryTypes.RAW });
|
||||
} 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');
|
||||
}
|
||||
|
||||
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(`
|
||||
INSERT INTO admin_configurations (
|
||||
config_id, config_key, config_category, config_value, value_type,
|
||||
@ -729,12 +727,25 @@ export async function seedDefaultConfigurations(): Promise<void> {
|
||||
NOW(),
|
||||
NOW()
|
||||
)
|
||||
ON CONFLICT (config_key) DO NOTHING
|
||||
`, { type: QueryTypes.INSERT });
|
||||
|
||||
logger.info('[Config Seed] ✅ Default configurations seeded successfully (30 settings across 7 categories)');
|
||||
} catch (error) {
|
||||
logger.error('[Config Seed] Error seeding configurations:', error);
|
||||
// Verify how many were actually inserted
|
||||
const result = await sequelize.query(
|
||||
'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
|
||||
// User can manually run seed script if needed: npm run seed:config
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -82,8 +82,30 @@ export class PauseService {
|
||||
|
||||
// Calculate elapsed hours before pause
|
||||
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
|
||||
const tatSnapshot = {
|
||||
@ -95,7 +117,7 @@ export class PauseService {
|
||||
? Math.min(100, Math.round((elapsedHours / Number((level as any).tatHours)) * 100))
|
||||
: 0),
|
||||
pausedAt: now.toISOString(),
|
||||
originalTatStartTime: levelStartTime
|
||||
originalTatStartTime: originalTatStartTime // Always use the original start time, not the resume time
|
||||
};
|
||||
|
||||
// Update approval level with pause information
|
||||
@ -105,7 +127,7 @@ export class PauseService {
|
||||
pausedBy: userId,
|
||||
pauseReason: reason,
|
||||
pauseResumeDate: resumeDate,
|
||||
pauseTatStartTime: levelStartTime,
|
||||
pauseTatStartTime: originalTatStartTime, // Always preserve the original start time
|
||||
pauseElapsedHours: elapsedHours,
|
||||
status: ApprovalStatus.PAUSED
|
||||
});
|
||||
|
||||
@ -31,7 +31,9 @@ export class TatSchedulerService {
|
||||
}
|
||||
|
||||
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
|
||||
const thresholds = await getTatThresholds();
|
||||
@ -51,7 +53,6 @@ export class TatSchedulerService {
|
||||
threshold1Time = t1.toDate();
|
||||
threshold2Time = t2.toDate();
|
||||
breachTime = tBreach.toDate();
|
||||
logger.info(`[TAT Scheduler] Using EXPRESS mode - all days, working hours only (9 AM - 6 PM)`);
|
||||
} else {
|
||||
// STANDARD: Working days only (Mon-Fri), working hours (9 AM - 6 PM), excludes holidays
|
||||
const t1 = await addWorkingHours(now, tatDurationHours * (thresholds.first / 100));
|
||||
@ -60,15 +61,9 @@ export class TatSchedulerService {
|
||||
threshold1Time = t1.toDate();
|
||||
threshold2Time = t2.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] 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')}`);
|
||||
logger.info(`[TAT Scheduler] Scheduling TAT jobs - Request: ${requestId}, Priority: ${priority}, TAT: ${tatDurationHours}h`);
|
||||
|
||||
const jobs = [
|
||||
{
|
||||
@ -142,11 +137,10 @@ export class TatSchedulerService {
|
||||
}
|
||||
);
|
||||
|
||||
logger.info(`[TAT Scheduler] Scheduled ${job.type} (${job.threshold}%)`);
|
||||
jobIndex++;
|
||||
}
|
||||
|
||||
logger.info(`[TAT Scheduler] TAT jobs scheduled for request ${requestId}`);
|
||||
logger.info(`[TAT Scheduler] ✅ TAT jobs scheduled for request ${requestId}`);
|
||||
} catch (error) {
|
||||
logger.error(`[TAT Scheduler] Failed to schedule TAT jobs:`, error);
|
||||
throw error;
|
||||
@ -184,7 +178,9 @@ export class TatSchedulerService {
|
||||
}
|
||||
|
||||
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
|
||||
const thresholds = await getTatThresholds();
|
||||
@ -196,13 +192,7 @@ export class TatSchedulerService {
|
||||
: 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}`);
|
||||
logger.info(`[TAT Scheduler] Resuming TAT scheduling - Request: ${requestId}, Remaining: ${(remainingTatHours * 60).toFixed(1)} min, Priority: ${isExpress ? 'EXPRESS' : 'STANDARD'}`);
|
||||
|
||||
// Jobs to schedule - only include those that haven't been sent and haven't been passed
|
||||
const jobsToSchedule: Array<{
|
||||
@ -230,10 +220,7 @@ export class TatSchedulerService {
|
||||
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%)
|
||||
@ -249,10 +236,7 @@ export class TatSchedulerService {
|
||||
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%)
|
||||
@ -265,13 +249,10 @@ export class TatSchedulerService {
|
||||
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)`);
|
||||
logger.info(`[TAT Scheduler] No TAT jobs to schedule (all alerts already sent)`);
|
||||
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}`);
|
||||
} catch (error) {
|
||||
logger.error(`[TAT Scheduler] Failed to schedule TAT jobs on resume:`, error);
|
||||
|
||||
@ -223,7 +223,7 @@ export async function addWorkingHoursExpress(start: Date | string, hoursToAdd: n
|
||||
return current.add(hoursToAdd, 'minute');
|
||||
}
|
||||
|
||||
// Load configuration
|
||||
// Load configuration (but don't load holidays - EXPRESS works on holidays too)
|
||||
await loadWorkingHoursCache();
|
||||
|
||||
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)
|
||||
// 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 currentHour = current.hour();
|
||||
const currentDay = current.day(); // 0 = Sunday, 6 = Saturday
|
||||
|
||||
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);
|
||||
} 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);
|
||||
}
|
||||
|
||||
@ -247,17 +252,30 @@ export async function addWorkingHoursExpress(start: Date | string, hoursToAdd: n
|
||||
const fractionalHours = hoursToAdd - wholeHours;
|
||||
|
||||
let remaining = wholeHours;
|
||||
let hoursCounted = 0;
|
||||
|
||||
// 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');
|
||||
const hour = current.hour();
|
||||
|
||||
// For express: count ALL days (including weekends/holidays)
|
||||
// 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) {
|
||||
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)
|
||||
@ -267,7 +285,7 @@ export async function addWorkingHoursExpress(start: Date | string, hoursToAdd: n
|
||||
|
||||
// Check if fractional addition pushed us past working hours
|
||||
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();
|
||||
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
|
||||
let actualStartDate = startDate;
|
||||
let prePauseElapsed = 0;
|
||||
let resumeTime = null;
|
||||
|
||||
if (pauseInfo?.pauseResumeDate && pauseInfo.pauseElapsedHours !== undefined) {
|
||||
// Was paused, now resumed
|
||||
// Use elapsed hours at pause + time from resume to end
|
||||
prePauseElapsed = pauseInfo.pauseElapsedHours;
|
||||
actualStartDate = pauseInfo.pauseResumeDate;
|
||||
resumeTime = pauseInfo.pauseResumeDate; // Store resume time for reference
|
||||
}
|
||||
|
||||
let start = dayjs(actualStartDate);
|
||||
@ -601,42 +621,54 @@ export async function calculateElapsedWorkingHours(
|
||||
endDay: TAT_CONFIG.WORK_END_DAY
|
||||
};
|
||||
|
||||
// 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
|
||||
const originalStart = start.format('YYYY-MM-DD HH:mm:ss');
|
||||
// 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;
|
||||
|
||||
// For standard priority, check working days and hours
|
||||
if (priority !== 'express') {
|
||||
const wasOutsideWorkingHours = !isWorkingTime(start);
|
||||
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
|
||||
|
||||
while (!isWorkingTime(start)) {
|
||||
const hour = start.hour();
|
||||
const day = start.day();
|
||||
// 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
|
||||
const originalStart = start.format('YYYY-MM-DD HH:mm:ss');
|
||||
|
||||
// For standard priority, check working days and hours
|
||||
if (priority !== 'express') {
|
||||
const wasOutsideWorkingHours = !isWorkingTime(start);
|
||||
|
||||
// If before work start hour on a working day, jump to work start hour
|
||||
if (day >= config.startDay && day <= config.endDay && !isHoliday(start) && hour < config.startHour) {
|
||||
start = start.hour(config.startHour);
|
||||
} else {
|
||||
// Otherwise, advance by 1 hour and check again
|
||||
start = start.add(1, 'hour');
|
||||
while (!isWorkingTime(start)) {
|
||||
const hour = start.hour();
|
||||
const day = start.day();
|
||||
|
||||
// If before work start hour on a working day, jump to work start hour
|
||||
if (day >= config.startDay && day <= config.endDay && !isHoliday(start) && hour < config.startHour) {
|
||||
start = start.hour(config.startHour);
|
||||
} else {
|
||||
// Otherwise, advance by 1 hour and check again
|
||||
start = start.add(1, 'hour');
|
||||
}
|
||||
}
|
||||
|
||||
// If start time was outside working hours, reset to clean work start time
|
||||
if (wasOutsideWorkingHours) {
|
||||
start = start.minute(0).second(0).millisecond(0);
|
||||
}
|
||||
} else {
|
||||
// For express priority, only check working hours (not days)
|
||||
const hour = start.hour();
|
||||
if (hour < config.startHour) {
|
||||
// Before hours - reset to clean start
|
||||
start = start.hour(config.startHour).minute(0).second(0).millisecond(0);
|
||||
} else if (hour >= config.endHour) {
|
||||
// After hours - reset to clean start of next day
|
||||
start = start.add(1, 'day').hour(config.startHour).minute(0).second(0).millisecond(0);
|
||||
}
|
||||
}
|
||||
|
||||
// If start time was outside working hours, reset to clean work start time
|
||||
if (wasOutsideWorkingHours) {
|
||||
start = start.minute(0).second(0).millisecond(0);
|
||||
}
|
||||
} else {
|
||||
// For express priority, only check working hours (not days)
|
||||
const hour = start.hour();
|
||||
if (hour < config.startHour) {
|
||||
// Before hours - reset to clean start
|
||||
start = start.hour(config.startHour).minute(0).second(0).millisecond(0);
|
||||
} else if (hour >= config.endHour) {
|
||||
// After hours - reset to clean start of next day
|
||||
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)) {
|
||||
return 0;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user