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};
|
import{a as t}from"./index-BJNKp6s1.js";import"./radix-vendor-C2EbRL2a.js";import"./charts-vendor-Cji9-Yri.js";import"./utils-vendor-DHm03ykU.js";import"./ui-vendor-biwEPLZp.js";import"./socket-vendor-TjCxX7sJ.js";import"./redux-vendor-tbZCm13o.js";import"./router-vendor-1fSSvDCY.js";async function m(n){return(await t.post(`/conclusions/${n}/generate`)).data.data}async function d(n,o){return(await t.post(`/conclusions/${n}/finalize`,{finalRemark:o})).data.data}async function f(n){return(await t.get(`/conclusions/${n}`)).data.data}export{d as finalizeConclusion,m as generateConclusion,f as getConclusion};
|
||||||
//# sourceMappingURL=conclusionApi-DZ8g8n5O.js.map
|
//# sourceMappingURL=conclusionApi-CLFPR2m0.js.map
|
||||||
@ -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
@ -52,7 +52,7 @@
|
|||||||
transition: transform 0.2s ease;
|
transition: transform 0.2s ease;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<script type="module" crossorigin src="/assets/index-ZFZ_Bqjc.js"></script>
|
<script type="module" crossorigin src="/assets/index-BJNKp6s1.js"></script>
|
||||||
<link rel="modulepreload" crossorigin href="/assets/charts-vendor-Cji9-Yri.js">
|
<link rel="modulepreload" crossorigin href="/assets/charts-vendor-Cji9-Yri.js">
|
||||||
<link rel="modulepreload" crossorigin href="/assets/radix-vendor-C2EbRL2a.js">
|
<link rel="modulepreload" crossorigin href="/assets/radix-vendor-C2EbRL2a.js">
|
||||||
<link rel="modulepreload" crossorigin href="/assets/utils-vendor-DHm03ykU.js">
|
<link rel="modulepreload" crossorigin href="/assets/utils-vendor-DHm03ykU.js">
|
||||||
@ -60,7 +60,7 @@
|
|||||||
<link rel="modulepreload" crossorigin href="/assets/socket-vendor-TjCxX7sJ.js">
|
<link rel="modulepreload" crossorigin href="/assets/socket-vendor-TjCxX7sJ.js">
|
||||||
<link rel="modulepreload" crossorigin href="/assets/redux-vendor-tbZCm13o.js">
|
<link rel="modulepreload" crossorigin href="/assets/redux-vendor-tbZCm13o.js">
|
||||||
<link rel="modulepreload" crossorigin href="/assets/router-vendor-1fSSvDCY.js">
|
<link rel="modulepreload" crossorigin href="/assets/router-vendor-1fSSvDCY.js">
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-CiGKtlR4.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-DD2tGQ-m.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
@ -19,7 +19,7 @@ export async function handlePauseResumeJob(job: Job): Promise<void> {
|
|||||||
logger.warn(`[Pause Resume Processor] Unknown job type: ${type}`);
|
logger.warn(`[Pause Resume Processor] Unknown job type: ${type}`);
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
logger.error(`[Pause Resume Processor] Failed to process job ${job.id}:`, error);
|
logger.error(`[Pause Resume Processor] Failed to process job ${job.id}:`, error?.message || error);
|
||||||
throw error; // Re-throw to trigger retry mechanism
|
throw error; // Re-throw to trigger retry mechanism
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -30,15 +30,31 @@ try {
|
|||||||
});
|
});
|
||||||
|
|
||||||
pauseResumeWorker.on('failed', (job, err) => {
|
pauseResumeWorker.on('failed', (job, err) => {
|
||||||
logger.error(`[Pause Resume Worker] Failed: ${job?.name} (${job?.id})`, err.message);
|
logger.error(`[Pause Resume Worker] Failed: ${job?.name} (${job?.id})`, err?.message || err);
|
||||||
});
|
});
|
||||||
|
|
||||||
pauseResumeWorker.on('error', (err) => {
|
pauseResumeWorker.on('error', (err) => {
|
||||||
logger.error('[Pause Resume Worker] Error:', err.message);
|
// Connection errors are common if Redis is unavailable - log as warning
|
||||||
|
const errorCode = (err as any)?.code;
|
||||||
|
const isConnectionError = err?.message?.includes('connect') ||
|
||||||
|
err?.message?.includes('ECONNREFUSED') ||
|
||||||
|
err?.message?.includes('Redis') ||
|
||||||
|
errorCode === 'ECONNREFUSED';
|
||||||
|
if (isConnectionError) {
|
||||||
|
logger.warn('[Pause Resume Worker] Connection issue (Redis may be unavailable):', err?.message || errorCode || String(err));
|
||||||
|
} else {
|
||||||
|
// Log full error details for non-connection errors to diagnose issues
|
||||||
|
logger.error('[Pause Resume Worker] Error:', {
|
||||||
|
message: err?.message || 'Unknown error',
|
||||||
|
code: errorCode,
|
||||||
|
name: err?.name,
|
||||||
|
stack: err?.stack
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (workerError: any) {
|
} catch (workerError: any) {
|
||||||
logger.error('[Pause Resume Worker] Failed to create worker:', workerError);
|
logger.error('[Pause Resume Worker] Failed to create worker:', workerError?.message || workerError);
|
||||||
pauseResumeWorker = null;
|
pauseResumeWorker = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -66,7 +66,7 @@ export async function handleTatJob(job: Job<TatJobData>) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const tatHours = Number((approvalLevel as any).tatHours || 0);
|
const tatHours = Number((approvalLevel as any).tatHours || 0);
|
||||||
const levelStartTime = (approvalLevel as any).levelStartTime || (approvalLevel as any).createdAt;
|
const levelStartTime = (approvalLevel as any).levelStartTime || (approvalLevel as any).createdAt || (approvalLevel as any).tatStartTime;
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
|
|
||||||
// FIXED: Use proper working hours calculation instead of calendar hours
|
// FIXED: Use proper working hours calculation instead of calendar hours
|
||||||
@ -74,15 +74,30 @@ export async function handleTatJob(job: Job<TatJobData>) {
|
|||||||
const priority = ((workflow as any).priority || 'STANDARD').toString().toLowerCase();
|
const priority = ((workflow as any).priority || 'STANDARD').toString().toLowerCase();
|
||||||
|
|
||||||
// Pass pause information if available
|
// Pass pause information if available
|
||||||
const pauseInfo = (approvalLevel as any).isPaused ? {
|
// IMPORTANT: Check both currently paused AND previously paused/resumed levels
|
||||||
isPaused: (approvalLevel as any).isPaused,
|
// For resumed levels, we need to include pauseElapsedHours and pauseResumeDate
|
||||||
|
// so the calculation includes pre-pause elapsed time
|
||||||
|
const isCurrentlyPaused = (approvalLevel as any).isPaused === true;
|
||||||
|
const wasResumed = !isCurrentlyPaused &&
|
||||||
|
(approvalLevel as any).pauseElapsedHours !== null &&
|
||||||
|
(approvalLevel as any).pauseElapsedHours !== undefined &&
|
||||||
|
(approvalLevel as any).pauseResumeDate !== null;
|
||||||
|
|
||||||
|
const pauseInfo = isCurrentlyPaused ? {
|
||||||
|
isPaused: true,
|
||||||
pausedAt: (approvalLevel as any).pausedAt,
|
pausedAt: (approvalLevel as any).pausedAt,
|
||||||
pauseElapsedHours: (approvalLevel as any).pauseElapsedHours,
|
pauseElapsedHours: (approvalLevel as any).pauseElapsedHours,
|
||||||
pauseResumeDate: (approvalLevel as any).pauseResumeDate
|
pauseResumeDate: (approvalLevel as any).pauseResumeDate
|
||||||
|
} : wasResumed ? {
|
||||||
|
// Level was paused but has been resumed - include pre-pause elapsed hours
|
||||||
|
isPaused: false,
|
||||||
|
pausedAt: null,
|
||||||
|
pauseElapsedHours: Number((approvalLevel as any).pauseElapsedHours), // Pre-pause elapsed hours
|
||||||
|
pauseResumeDate: (approvalLevel as any).pauseResumeDate // Actual resume timestamp
|
||||||
} : undefined;
|
} : undefined;
|
||||||
|
|
||||||
const elapsedHours = await calculateElapsedWorkingHours(levelStartTime, now, priority, pauseInfo);
|
const elapsedHours = await calculateElapsedWorkingHours(levelStartTime, now, priority, pauseInfo);
|
||||||
const remainingHours = Math.max(0, tatHours - elapsedHours);
|
let remainingHours = Math.max(0, tatHours - elapsedHours);
|
||||||
|
|
||||||
// Calculate expected completion time using proper working hours calculation
|
// Calculate expected completion time using proper working hours calculation
|
||||||
// EXPRESS: includes weekends but only during working hours
|
// EXPRESS: includes weekends but only during working hours
|
||||||
@ -137,6 +152,10 @@ export async function handleTatJob(job: Job<TatJobData>) {
|
|||||||
message = `TAT breached for Request ${requestNumber}: ${title}. Immediate action required!`;
|
message = `TAT breached for Request ${requestNumber}: ${title}. Immediate action required!`;
|
||||||
activityDetails = 'TAT deadline reached - Breach notification';
|
activityDetails = 'TAT deadline reached - Breach notification';
|
||||||
|
|
||||||
|
// When breached, ensure remaining hours is 0 (no rounding errors)
|
||||||
|
// If elapsedHours >= tatHours, remainingHours should be exactly 0
|
||||||
|
remainingHours = 0;
|
||||||
|
|
||||||
// Update TAT status in database with comprehensive tracking
|
// Update TAT status in database with comprehensive tracking
|
||||||
await ApprovalLevel.update(
|
await ApprovalLevel.update(
|
||||||
{
|
{
|
||||||
@ -191,7 +210,8 @@ export async function handleTatJob(job: Job<TatJobData>) {
|
|||||||
type === 'threshold2' ? 'HIGH' :
|
type === 'threshold2' ? 'HIGH' :
|
||||||
'MEDIUM';
|
'MEDIUM';
|
||||||
|
|
||||||
// Send notification to approver
|
// Send notification to approver (with error handling to prevent job failure)
|
||||||
|
try {
|
||||||
await notificationService.sendToUsers([approverId], {
|
await notificationService.sendToUsers([approverId], {
|
||||||
title: type === 'breach' ? 'TAT Breach Alert' : 'TAT Reminder',
|
title: type === 'breach' ? 'TAT Breach Alert' : 'TAT Reminder',
|
||||||
body: message,
|
body: message,
|
||||||
@ -202,11 +222,18 @@ export async function handleTatJob(job: Job<TatJobData>) {
|
|||||||
priority: notificationPriority,
|
priority: notificationPriority,
|
||||||
actionRequired: type === 'breach' || type === 'threshold2' // Require action for critical alerts
|
actionRequired: type === 'breach' || type === 'threshold2' // Require action for critical alerts
|
||||||
});
|
});
|
||||||
|
logger.info(`[TAT Processor] ✅ Notification sent to approver ${approverId} for ${type} (${threshold}%)`);
|
||||||
|
} catch (notificationError: any) {
|
||||||
|
logger.error(`[TAT Processor] ❌ Failed to send notification to approver ${approverId} for ${type}:`, notificationError?.message || notificationError);
|
||||||
|
// Don't fail the job - alert is already created, notification failure is non-critical
|
||||||
|
// The alert will still be visible in the UI even if push notification fails
|
||||||
|
}
|
||||||
|
|
||||||
// If breached, also notify the initiator (workflow creator)
|
// If breached, also notify the initiator (workflow creator)
|
||||||
if (type === 'breach') {
|
if (type === 'breach') {
|
||||||
const initiatorId = (workflow as any).initiatorId;
|
const initiatorId = (workflow as any).initiatorId;
|
||||||
if (initiatorId && initiatorId !== approverId) {
|
if (initiatorId && initiatorId !== approverId) {
|
||||||
|
try {
|
||||||
await notificationService.sendToUsers([initiatorId], {
|
await notificationService.sendToUsers([initiatorId], {
|
||||||
title: 'TAT Breach - Request Delayed',
|
title: 'TAT Breach - Request Delayed',
|
||||||
body: `Your request ${requestNumber}: "${title}" has exceeded its TAT. The approver has been notified.`,
|
body: `Your request ${requestNumber}: "${title}" has exceeded its TAT. The approver has been notified.`,
|
||||||
@ -217,7 +244,11 @@ export async function handleTatJob(job: Job<TatJobData>) {
|
|||||||
priority: 'HIGH',
|
priority: 'HIGH',
|
||||||
actionRequired: false
|
actionRequired: false
|
||||||
});
|
});
|
||||||
logger.info(`[TAT Processor] Breach notification sent to initiator ${initiatorId}`);
|
logger.info(`[TAT Processor] ✅ Breach notification sent to initiator ${initiatorId}`);
|
||||||
|
} catch (initiatorNotifyError: any) {
|
||||||
|
logger.error(`[TAT Processor] ❌ Failed to send breach notification to initiator ${initiatorId}:`, initiatorNotifyError?.message || initiatorNotifyError);
|
||||||
|
// Don't fail the job - notification failure is non-critical
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -30,15 +30,23 @@ try {
|
|||||||
});
|
});
|
||||||
|
|
||||||
tatWorker.on('failed', (job, err) => {
|
tatWorker.on('failed', (job, err) => {
|
||||||
logger.error(`[TAT Worker] Failed: ${job?.name}`, err.message);
|
logger.error(`[TAT Worker] Failed: ${job?.name} (${job?.id})`, err?.message || err);
|
||||||
});
|
});
|
||||||
|
|
||||||
tatWorker.on('error', (err) => {
|
tatWorker.on('error', (err) => {
|
||||||
logger.error('[TAT Worker] Error:', err.message);
|
// Connection errors are common if Redis is unavailable - log as warning
|
||||||
|
const isConnectionError = err?.message?.includes('connect') ||
|
||||||
|
err?.message?.includes('ECONNREFUSED') ||
|
||||||
|
err?.message?.includes('Redis');
|
||||||
|
if (isConnectionError) {
|
||||||
|
logger.warn('[TAT Worker] Connection issue (Redis may be unavailable):', err?.message || err);
|
||||||
|
} else {
|
||||||
|
logger.error('[TAT Worker] Error:', err?.message || err);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (workerError: any) {
|
} catch (workerError: any) {
|
||||||
logger.error('[TAT Worker] Failed to create worker:', workerError);
|
logger.error('[TAT Worker] Failed to create worker:', workerError?.message || workerError);
|
||||||
tatWorker = null;
|
tatWorker = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -8,20 +8,18 @@ import logger from '@utils/logger';
|
|||||||
*/
|
*/
|
||||||
export async function seedDefaultConfigurations(): Promise<void> {
|
export async function seedDefaultConfigurations(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
// Check if configurations already exist
|
// Ensure pgcrypto extension is available for gen_random_uuid()
|
||||||
const count = await sequelize.query(
|
try {
|
||||||
'SELECT COUNT(*) as count FROM admin_configurations',
|
await sequelize.query('CREATE EXTENSION IF NOT EXISTS "pgcrypto"', { type: QueryTypes.RAW });
|
||||||
{ type: QueryTypes.SELECT }
|
} catch (extError: any) {
|
||||||
);
|
// Extension might already exist or user might not have permission - continue
|
||||||
|
logger.debug('[Config Seed] pgcrypto extension check:', extError?.message || 'already exists');
|
||||||
if (count && (count[0] as any).count > 0) {
|
|
||||||
// Table has data, skip seeding silently
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info('[Config Seed] Seeding default configurations...');
|
logger.info('[Config Seed] Seeding default configurations (duplicates will be skipped automatically)...');
|
||||||
|
|
||||||
// Insert default configurations
|
// Insert default configurations with ON CONFLICT handling
|
||||||
|
// This allows re-running the seed without errors if configs already exist
|
||||||
await sequelize.query(`
|
await sequelize.query(`
|
||||||
INSERT INTO admin_configurations (
|
INSERT INTO admin_configurations (
|
||||||
config_id, config_key, config_category, config_value, value_type,
|
config_id, config_key, config_category, config_value, value_type,
|
||||||
@ -729,12 +727,25 @@ export async function seedDefaultConfigurations(): Promise<void> {
|
|||||||
NOW(),
|
NOW(),
|
||||||
NOW()
|
NOW()
|
||||||
)
|
)
|
||||||
|
ON CONFLICT (config_key) DO NOTHING
|
||||||
`, { type: QueryTypes.INSERT });
|
`, { type: QueryTypes.INSERT });
|
||||||
|
|
||||||
logger.info('[Config Seed] ✅ Default configurations seeded successfully (30 settings across 7 categories)');
|
// Verify how many were actually inserted
|
||||||
} catch (error) {
|
const result = await sequelize.query(
|
||||||
logger.error('[Config Seed] Error seeding configurations:', error);
|
'SELECT COUNT(*) as count FROM admin_configurations',
|
||||||
|
{ type: QueryTypes.SELECT }
|
||||||
|
);
|
||||||
|
const totalCount = result && (result[0] as any).count ? (result[0] as any).count : 0;
|
||||||
|
|
||||||
|
logger.info(`[Config Seed] ✅ Configuration seeding complete. Total configurations: ${totalCount}`);
|
||||||
|
} catch (error: any) {
|
||||||
|
logger.error('[Config Seed] ❌ Error seeding configurations:', {
|
||||||
|
message: error?.message || String(error),
|
||||||
|
stack: error?.stack,
|
||||||
|
name: error?.name
|
||||||
|
});
|
||||||
// Don't throw - let server start even if seeding fails
|
// Don't throw - let server start even if seeding fails
|
||||||
|
// User can manually run seed script if needed: npm run seed:config
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -82,8 +82,30 @@ export class PauseService {
|
|||||||
|
|
||||||
// Calculate elapsed hours before pause
|
// Calculate elapsed hours before pause
|
||||||
const priority = ((workflow as any).priority || 'STANDARD').toString().toLowerCase();
|
const priority = ((workflow as any).priority || 'STANDARD').toString().toLowerCase();
|
||||||
const levelStartTime = (level as any).levelStartTime || (level as any).tatStartTime || (level as any).createdAt;
|
|
||||||
const elapsedHours = await calculateElapsedWorkingHours(levelStartTime, now, priority);
|
// Check if this level was previously paused and resumed
|
||||||
|
// If so, we need to account for the previous pauseElapsedHours
|
||||||
|
// IMPORTANT: Convert to number to avoid string concatenation (DB returns DECIMAL as string)
|
||||||
|
const previousPauseElapsedHours = Number((level as any).pauseElapsedHours || 0);
|
||||||
|
const previousResumeDate = (level as any).pauseResumeDate;
|
||||||
|
const originalTatStartTime = (level as any).pauseTatStartTime || (level as any).levelStartTime || (level as any).tatStartTime || (level as any).createdAt;
|
||||||
|
|
||||||
|
let elapsedHours: number;
|
||||||
|
let levelStartTimeForCalculation: Date;
|
||||||
|
|
||||||
|
if (previousPauseElapsedHours > 0 && previousResumeDate) {
|
||||||
|
// This is a second (or subsequent) pause
|
||||||
|
// Calculate: previous elapsed hours + time from resume to now
|
||||||
|
levelStartTimeForCalculation = previousResumeDate; // Start from last resume time
|
||||||
|
const timeSinceResume = await calculateElapsedWorkingHours(levelStartTimeForCalculation, now, priority);
|
||||||
|
elapsedHours = previousPauseElapsedHours + Number(timeSinceResume);
|
||||||
|
|
||||||
|
logger.info(`[Pause] Second pause detected - Previous elapsed: ${previousPauseElapsedHours}h, Since resume: ${timeSinceResume}h, Total: ${elapsedHours}h`);
|
||||||
|
} else {
|
||||||
|
// First pause - calculate from original start time
|
||||||
|
levelStartTimeForCalculation = originalTatStartTime;
|
||||||
|
elapsedHours = await calculateElapsedWorkingHours(levelStartTimeForCalculation, now, priority);
|
||||||
|
}
|
||||||
|
|
||||||
// Store TAT snapshot
|
// Store TAT snapshot
|
||||||
const tatSnapshot = {
|
const tatSnapshot = {
|
||||||
@ -95,7 +117,7 @@ export class PauseService {
|
|||||||
? Math.min(100, Math.round((elapsedHours / Number((level as any).tatHours)) * 100))
|
? Math.min(100, Math.round((elapsedHours / Number((level as any).tatHours)) * 100))
|
||||||
: 0),
|
: 0),
|
||||||
pausedAt: now.toISOString(),
|
pausedAt: now.toISOString(),
|
||||||
originalTatStartTime: levelStartTime
|
originalTatStartTime: originalTatStartTime // Always use the original start time, not the resume time
|
||||||
};
|
};
|
||||||
|
|
||||||
// Update approval level with pause information
|
// Update approval level with pause information
|
||||||
@ -105,7 +127,7 @@ export class PauseService {
|
|||||||
pausedBy: userId,
|
pausedBy: userId,
|
||||||
pauseReason: reason,
|
pauseReason: reason,
|
||||||
pauseResumeDate: resumeDate,
|
pauseResumeDate: resumeDate,
|
||||||
pauseTatStartTime: levelStartTime,
|
pauseTatStartTime: originalTatStartTime, // Always preserve the original start time
|
||||||
pauseElapsedHours: elapsedHours,
|
pauseElapsedHours: elapsedHours,
|
||||||
status: ApprovalStatus.PAUSED
|
status: ApprovalStatus.PAUSED
|
||||||
});
|
});
|
||||||
|
|||||||
@ -31,7 +31,9 @@ export class TatSchedulerService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const now = startTime || new Date();
|
const now = startTime || new Date();
|
||||||
const isExpress = priority === Priority.EXPRESS;
|
// Handle both enum and string (case-insensitive) priority values
|
||||||
|
const priorityStr = typeof priority === 'string' ? priority.toUpperCase() : priority;
|
||||||
|
const isExpress = priorityStr === Priority.EXPRESS || priorityStr === 'EXPRESS';
|
||||||
|
|
||||||
// Get current thresholds from database configuration
|
// Get current thresholds from database configuration
|
||||||
const thresholds = await getTatThresholds();
|
const thresholds = await getTatThresholds();
|
||||||
@ -51,7 +53,6 @@ export class TatSchedulerService {
|
|||||||
threshold1Time = t1.toDate();
|
threshold1Time = t1.toDate();
|
||||||
threshold2Time = t2.toDate();
|
threshold2Time = t2.toDate();
|
||||||
breachTime = tBreach.toDate();
|
breachTime = tBreach.toDate();
|
||||||
logger.info(`[TAT Scheduler] Using EXPRESS mode - all days, working hours only (9 AM - 6 PM)`);
|
|
||||||
} else {
|
} else {
|
||||||
// STANDARD: Working days only (Mon-Fri), working hours (9 AM - 6 PM), excludes holidays
|
// STANDARD: Working days only (Mon-Fri), working hours (9 AM - 6 PM), excludes holidays
|
||||||
const t1 = await addWorkingHours(now, tatDurationHours * (thresholds.first / 100));
|
const t1 = await addWorkingHours(now, tatDurationHours * (thresholds.first / 100));
|
||||||
@ -60,15 +61,9 @@ export class TatSchedulerService {
|
|||||||
threshold1Time = t1.toDate();
|
threshold1Time = t1.toDate();
|
||||||
threshold2Time = t2.toDate();
|
threshold2Time = t2.toDate();
|
||||||
breachTime = tBreach.toDate();
|
breachTime = tBreach.toDate();
|
||||||
logger.info(`[TAT Scheduler] Using STANDARD mode - weekdays only, working hours (9 AM - 6 PM), excludes holidays`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info(`[TAT Scheduler] Calculating TAT milestones for request ${requestId}, level ${levelId}`);
|
logger.info(`[TAT Scheduler] Scheduling TAT jobs - Request: ${requestId}, Priority: ${priority}, TAT: ${tatDurationHours}h`);
|
||||||
logger.info(`[TAT Scheduler] Priority: ${priority}, TAT Hours: ${tatDurationHours}`);
|
|
||||||
logger.info(`[TAT Scheduler] Start: ${dayjs(now).format('YYYY-MM-DD HH:mm')}`);
|
|
||||||
logger.info(`[TAT Scheduler] Threshold 1 (${thresholds.first}%): ${dayjs(threshold1Time).format('YYYY-MM-DD HH:mm')}`);
|
|
||||||
logger.info(`[TAT Scheduler] Threshold 2 (${thresholds.second}%): ${dayjs(threshold2Time).format('YYYY-MM-DD HH:mm')}`);
|
|
||||||
logger.info(`[TAT Scheduler] Breach (100%): ${dayjs(breachTime).format('YYYY-MM-DD HH:mm')}`);
|
|
||||||
|
|
||||||
const jobs = [
|
const jobs = [
|
||||||
{
|
{
|
||||||
@ -142,11 +137,10 @@ export class TatSchedulerService {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
logger.info(`[TAT Scheduler] Scheduled ${job.type} (${job.threshold}%)`);
|
|
||||||
jobIndex++;
|
jobIndex++;
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info(`[TAT Scheduler] TAT jobs scheduled for request ${requestId}`);
|
logger.info(`[TAT Scheduler] ✅ TAT jobs scheduled for request ${requestId}`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`[TAT Scheduler] Failed to schedule TAT jobs:`, error);
|
logger.error(`[TAT Scheduler] Failed to schedule TAT jobs:`, error);
|
||||||
throw error;
|
throw error;
|
||||||
@ -184,7 +178,9 @@ export class TatSchedulerService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const now = startTime;
|
const now = startTime;
|
||||||
const isExpress = priority === Priority.EXPRESS;
|
// Handle both enum and string (case-insensitive) priority values
|
||||||
|
const priorityStr = typeof priority === 'string' ? priority.toUpperCase() : priority;
|
||||||
|
const isExpress = priorityStr === Priority.EXPRESS || priorityStr === 'EXPRESS';
|
||||||
|
|
||||||
// Get current thresholds from database configuration
|
// Get current thresholds from database configuration
|
||||||
const thresholds = await getTatThresholds();
|
const thresholds = await getTatThresholds();
|
||||||
@ -196,13 +192,7 @@ export class TatSchedulerService {
|
|||||||
: 0;
|
: 0;
|
||||||
const originalTatHours = elapsedHours + remainingTatHours;
|
const originalTatHours = elapsedHours + remainingTatHours;
|
||||||
|
|
||||||
logger.info(`[TAT Scheduler] ========== RESUME TAT SCHEDULING ==========`);
|
logger.info(`[TAT Scheduler] Resuming TAT scheduling - Request: ${requestId}, Remaining: ${(remainingTatHours * 60).toFixed(1)} min, Priority: ${isExpress ? 'EXPRESS' : 'STANDARD'}`);
|
||||||
logger.info(`[TAT Scheduler] Request: ${requestId}, Level: ${levelId}`);
|
|
||||||
logger.info(`[TAT Scheduler] Original TAT: ${(originalTatHours * 60).toFixed(1)} min (${originalTatHours.toFixed(2)} hrs)`);
|
|
||||||
logger.info(`[TAT Scheduler] Elapsed before pause: ${(elapsedHours * 60).toFixed(1)} min (${alertStatus.percentageUsedAtPause.toFixed(1)}%)`);
|
|
||||||
logger.info(`[TAT Scheduler] Remaining after resume: ${(remainingTatHours * 60).toFixed(1)} min`);
|
|
||||||
logger.info(`[TAT Scheduler] Thresholds configured - First: ${thresholds.first}%, Second: ${thresholds.second}%`);
|
|
||||||
logger.info(`[TAT Scheduler] Alerts already sent - ${thresholds.first}%: ${alertStatus.tat50AlertSent}, ${thresholds.second}%: ${alertStatus.tat75AlertSent}, Breach: ${alertStatus.tatBreached}`);
|
|
||||||
|
|
||||||
// Jobs to schedule - only include those that haven't been sent and haven't been passed
|
// Jobs to schedule - only include those that haven't been sent and haven't been passed
|
||||||
const jobsToSchedule: Array<{
|
const jobsToSchedule: Array<{
|
||||||
@ -230,10 +220,7 @@ export class TatSchedulerService {
|
|||||||
alreadyPassed: false,
|
alreadyPassed: false,
|
||||||
hoursFromNow: hoursFromNow
|
hoursFromNow: hoursFromNow
|
||||||
});
|
});
|
||||||
logger.info(`[TAT Scheduler] → ${thresholds.first}% alert: Schedule in ${(hoursFromNow * 60).toFixed(1)} min`);
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
logger.info(`[TAT Scheduler] → ${thresholds.first}% alert: SKIP (${alertStatus.tat50AlertSent ? 'already sent' : 'already passed'})`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Threshold 2 (e.g., 75%)
|
// Threshold 2 (e.g., 75%)
|
||||||
@ -249,10 +236,7 @@ export class TatSchedulerService {
|
|||||||
alreadyPassed: false,
|
alreadyPassed: false,
|
||||||
hoursFromNow: hoursFromNow
|
hoursFromNow: hoursFromNow
|
||||||
});
|
});
|
||||||
logger.info(`[TAT Scheduler] → ${thresholds.second}% alert: Schedule in ${(hoursFromNow * 60).toFixed(1)} min`);
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
logger.info(`[TAT Scheduler] → ${thresholds.second}% alert: SKIP (${alertStatus.tat75AlertSent ? 'already sent' : 'already passed'})`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Breach (100%)
|
// Breach (100%)
|
||||||
@ -265,13 +249,10 @@ export class TatSchedulerService {
|
|||||||
alreadyPassed: false,
|
alreadyPassed: false,
|
||||||
hoursFromNow: remainingTatHours
|
hoursFromNow: remainingTatHours
|
||||||
});
|
});
|
||||||
logger.info(`[TAT Scheduler] → 100% breach: Schedule in ${(remainingTatHours * 60).toFixed(1)} min`);
|
|
||||||
} else {
|
|
||||||
logger.info(`[TAT Scheduler] → 100% breach: SKIP (already sent)`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (jobsToSchedule.length === 0) {
|
if (jobsToSchedule.length === 0) {
|
||||||
logger.info(`[TAT Scheduler] No TAT jobs to schedule (all alerts already sent or thresholds already passed)`);
|
logger.info(`[TAT Scheduler] No TAT jobs to schedule (all alerts already sent)`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -314,10 +295,9 @@ export class TatSchedulerService {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
logger.info(`[TAT Scheduler] ✓ Scheduled ${job.type} (${job.threshold}%) for ${dayjs(targetTime).format('YYYY-MM-DD HH:mm')} (in ${(job.hoursFromNow * 60).toFixed(1)} working minutes)`);
|
logger.info(`[TAT Scheduler] ✓ Scheduled ${job.type} (${job.threshold}%) for ${dayjs(targetTime).format('YYYY-MM-DD HH:mm')}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info(`[TAT Scheduler] ========== RESUME SCHEDULING COMPLETE ==========`);
|
|
||||||
logger.info(`[TAT Scheduler] ✅ ${jobsToSchedule.length} TAT job(s) scheduled for request ${requestId}`);
|
logger.info(`[TAT Scheduler] ✅ ${jobsToSchedule.length} TAT job(s) scheduled for request ${requestId}`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`[TAT Scheduler] Failed to schedule TAT jobs on resume:`, error);
|
logger.error(`[TAT Scheduler] Failed to schedule TAT jobs on resume:`, error);
|
||||||
|
|||||||
@ -223,7 +223,7 @@ export async function addWorkingHoursExpress(start: Date | string, hoursToAdd: n
|
|||||||
return current.add(hoursToAdd, 'minute');
|
return current.add(hoursToAdd, 'minute');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load configuration
|
// Load configuration (but don't load holidays - EXPRESS works on holidays too)
|
||||||
await loadWorkingHoursCache();
|
await loadWorkingHoursCache();
|
||||||
|
|
||||||
const config = workingHoursCache || {
|
const config = workingHoursCache || {
|
||||||
@ -234,11 +234,16 @@ export async function addWorkingHoursExpress(start: Date | string, hoursToAdd: n
|
|||||||
};
|
};
|
||||||
|
|
||||||
// If start time is outside working hours, advance to work start hour (reset to clean hour)
|
// If start time is outside working hours, advance to work start hour (reset to clean hour)
|
||||||
|
// IMPORTANT: For EXPRESS, we work on ALL days (weekends, holidays), so we don't skip them
|
||||||
const originalStart = current.format('YYYY-MM-DD HH:mm:ss');
|
const originalStart = current.format('YYYY-MM-DD HH:mm:ss');
|
||||||
const currentHour = current.hour();
|
const currentHour = current.hour();
|
||||||
|
const currentDay = current.day(); // 0 = Sunday, 6 = Saturday
|
||||||
|
|
||||||
if (currentHour < config.startHour) {
|
if (currentHour < config.startHour) {
|
||||||
|
// Before work hours - jump to work start hour on the same day (even if weekend/holiday)
|
||||||
current = current.hour(config.startHour).minute(0).second(0).millisecond(0);
|
current = current.hour(config.startHour).minute(0).second(0).millisecond(0);
|
||||||
} else if (currentHour >= config.endHour) {
|
} else if (currentHour >= config.endHour) {
|
||||||
|
// After work hours - go to next day's work start hour (even if weekend/holiday)
|
||||||
current = current.add(1, 'day').hour(config.startHour).minute(0).second(0).millisecond(0);
|
current = current.add(1, 'day').hour(config.startHour).minute(0).second(0).millisecond(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -247,17 +252,30 @@ export async function addWorkingHoursExpress(start: Date | string, hoursToAdd: n
|
|||||||
const fractionalHours = hoursToAdd - wholeHours;
|
const fractionalHours = hoursToAdd - wholeHours;
|
||||||
|
|
||||||
let remaining = wholeHours;
|
let remaining = wholeHours;
|
||||||
|
let hoursCounted = 0;
|
||||||
|
|
||||||
// Add whole hours
|
// Add whole hours
|
||||||
while (remaining > 0) {
|
// CRITICAL: For EXPRESS, count ALL days (weekends, holidays) - only check working hours (9 AM - 6 PM)
|
||||||
|
let iterations = 0;
|
||||||
|
const maxIterations = 10000; // Safety limit
|
||||||
|
|
||||||
|
while (remaining > 0 && iterations < maxIterations) {
|
||||||
current = current.add(1, 'hour');
|
current = current.add(1, 'hour');
|
||||||
const hour = current.hour();
|
const hour = current.hour();
|
||||||
|
|
||||||
// For express: count ALL days (including weekends/holidays)
|
// For express: count ALL days (including weekends/holidays)
|
||||||
// But only during working hours (configured start - end hour)
|
// But only during working hours (configured start - end hour)
|
||||||
|
// NO checks for day of week or holidays - EXPRESS works 7 days a week
|
||||||
if (hour >= config.startHour && hour < config.endHour) {
|
if (hour >= config.startHour && hour < config.endHour) {
|
||||||
remaining -= 1;
|
remaining -= 1;
|
||||||
}
|
}
|
||||||
|
// If outside working hours, we just continue to next hour without counting
|
||||||
|
// This ensures we only count 9 AM - 6 PM on any day
|
||||||
|
iterations++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (iterations >= maxIterations) {
|
||||||
|
console.error(`[EXPRESS TAT] Safety break - exceeded ${maxIterations} iterations`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add fractional part (convert to minutes)
|
// Add fractional part (convert to minutes)
|
||||||
@ -267,7 +285,7 @@ export async function addWorkingHoursExpress(start: Date | string, hoursToAdd: n
|
|||||||
|
|
||||||
// Check if fractional addition pushed us past working hours
|
// Check if fractional addition pushed us past working hours
|
||||||
if (current.hour() >= config.endHour) {
|
if (current.hour() >= config.endHour) {
|
||||||
// Overflow to next day's working hours
|
// Overflow to next day's working hours (even if weekend/holiday)
|
||||||
const excessMinutes = (current.hour() - config.endHour) * 60 + current.minute();
|
const excessMinutes = (current.hour() - config.endHour) * 60 + current.minute();
|
||||||
current = current.add(1, 'day').hour(config.startHour).minute(excessMinutes).second(0).millisecond(0);
|
current = current.add(1, 'day').hour(config.startHour).minute(excessMinutes).second(0).millisecond(0);
|
||||||
}
|
}
|
||||||
@ -577,12 +595,14 @@ export async function calculateElapsedWorkingHours(
|
|||||||
// If was paused but now resumed, calculate from resume date
|
// If was paused but now resumed, calculate from resume date
|
||||||
let actualStartDate = startDate;
|
let actualStartDate = startDate;
|
||||||
let prePauseElapsed = 0;
|
let prePauseElapsed = 0;
|
||||||
|
let resumeTime = null;
|
||||||
|
|
||||||
if (pauseInfo?.pauseResumeDate && pauseInfo.pauseElapsedHours !== undefined) {
|
if (pauseInfo?.pauseResumeDate && pauseInfo.pauseElapsedHours !== undefined) {
|
||||||
// Was paused, now resumed
|
// Was paused, now resumed
|
||||||
// Use elapsed hours at pause + time from resume to end
|
// Use elapsed hours at pause + time from resume to end
|
||||||
prePauseElapsed = pauseInfo.pauseElapsedHours;
|
prePauseElapsed = pauseInfo.pauseElapsedHours;
|
||||||
actualStartDate = pauseInfo.pauseResumeDate;
|
actualStartDate = pauseInfo.pauseResumeDate;
|
||||||
|
resumeTime = pauseInfo.pauseResumeDate; // Store resume time for reference
|
||||||
}
|
}
|
||||||
|
|
||||||
let start = dayjs(actualStartDate);
|
let start = dayjs(actualStartDate);
|
||||||
@ -601,6 +621,16 @@ export async function calculateElapsedWorkingHours(
|
|||||||
endDay: TAT_CONFIG.WORK_END_DAY
|
endDay: TAT_CONFIG.WORK_END_DAY
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// CRITICAL: For resumed levels, we must use the exact resume time as start
|
||||||
|
// Do NOT advance resume time to next working period - resume time is the actual moment TAT resumed
|
||||||
|
// Only advance if we're calculating from original start (not resumed)
|
||||||
|
const isResumedLevel = resumeTime !== null;
|
||||||
|
|
||||||
|
if (!isResumedLevel) {
|
||||||
|
// Only adjust start time if this is NOT a resumed level
|
||||||
|
// For resumed levels, use exact resume time (even if outside working hours)
|
||||||
|
// The working hours calculation below will handle skipping non-working periods
|
||||||
|
|
||||||
// CRITICAL FIX: If start time is outside working hours, advance to next working period
|
// CRITICAL FIX: If start time is outside working hours, advance to next working period
|
||||||
// This ensures we only count elapsed time when TAT is actually running
|
// This ensures we only count elapsed time when TAT is actually running
|
||||||
const originalStart = start.format('YYYY-MM-DD HH:mm:ss');
|
const originalStart = start.format('YYYY-MM-DD HH:mm:ss');
|
||||||
@ -637,6 +667,8 @@ export async function calculateElapsedWorkingHours(
|
|||||||
start = start.add(1, 'day').hour(config.startHour).minute(0).second(0).millisecond(0);
|
start = start.add(1, 'day').hour(config.startHour).minute(0).second(0).millisecond(0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
// For resumed levels, keep the exact resume time - the day-by-day calculation below will handle working hours correctly
|
||||||
|
|
||||||
if (end.isBefore(start)) {
|
if (end.isBefore(start)) {
|
||||||
return 0;
|
return 0;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user