avg cycle time, my request stat mismatch resolved

This commit is contained in:
laxmanhalaki 2025-11-26 11:54:29 +05:30
parent 0435159e2f
commit 7fd5d58080
7 changed files with 125 additions and 54 deletions

View File

@ -1,2 +1,2 @@
import{a as t}from"./index-C-Pt4yOr.js";import"./radix-vendor-BP4rDxsU.js";import"./charts-vendor-Cji9-Yri.js";import"./utils-vendor-DHm03ykU.js";import"./ui-vendor-DB5PynB_.js";import"./socket-vendor-TjCxX7sJ.js";import"./router-vendor-1fSSvDCY.js";async function l(n){return(await t.post(`/conclusions/${n}/generate`)).data.data}async function m(n,o){return(await t.post(`/conclusions/${n}/finalize`,{finalRemark:o})).data.data}async function d(n){return(await t.get(`/conclusions/${n}`)).data.data}export{m as finalizeConclusion,l as generateConclusion,d as getConclusion}; import{a as t}from"./index-BJC2x1CB.js";import"./radix-vendor-BP4rDxsU.js";import"./charts-vendor-Cji9-Yri.js";import"./utils-vendor-DHm03ykU.js";import"./ui-vendor-DB5PynB_.js";import"./socket-vendor-TjCxX7sJ.js";import"./router-vendor-1fSSvDCY.js";async function l(n){return(await t.post(`/conclusions/${n}/generate`)).data.data}async function m(n,o){return(await t.post(`/conclusions/${n}/finalize`,{finalRemark:o})).data.data}async function d(n){return(await t.get(`/conclusions/${n}`)).data.data}export{m as finalizeConclusion,l as generateConclusion,d as getConclusion};
//# sourceMappingURL=conclusionApi-DaHT8E_1.js.map //# sourceMappingURL=conclusionApi-BNkt5Ttj.js.map

View File

@ -1 +1 @@
{"version":3,"file":"conclusionApi-DaHT8E_1.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":"0PAwBA,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-BNkt5Ttj.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":"0PAwBA,eAAsBA,EAAmBC,EAMtC,CAED,OADiB,MAAMC,EAAU,KAAK,gBAAgBD,CAAS,WAAW,GAC1D,KAAK,IACvB,CAaA,eAAsBE,EAAmBF,EAAmBG,EAMzD,CAED,OADiB,MAAMF,EAAU,KAAK,gBAAgBD,CAAS,YAAa,CAAE,YAAAG,EAAa,GAC3E,KAAK,IACvB,CAKA,eAAsBC,EAAcJ,EAA8C,CAEhF,OADiB,MAAMC,EAAU,IAAI,gBAAgBD,CAAS,EAAE,GAChD,KAAK,IACvB"}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -52,7 +52,7 @@
transition: transform 0.2s ease; transition: transform 0.2s ease;
} }
</style> </style>
<script type="module" crossorigin src="/assets/index-C-Pt4yOr.js"></script> <script type="module" crossorigin src="/assets/index-BJC2x1CB.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-BP4rDxsU.js"> <link rel="modulepreload" crossorigin href="/assets/radix-vendor-BP4rDxsU.js">
<link rel="modulepreload" crossorigin href="/assets/utils-vendor-DHm03ykU.js"> <link rel="modulepreload" crossorigin href="/assets/utils-vendor-DHm03ykU.js">

View File

@ -133,15 +133,42 @@ export class DashboardService {
search?: string, search?: string,
slaCompliance?: string slaCompliance?: string
) { ) {
const range = this.parseDateRange(dateRange, startDate, endDate); // Check if date range should be applied
const applyDateRange = dateRange !== undefined && dateRange !== null;
const range = applyDateRange ? this.parseDateRange(dateRange, startDate, endDate) : null;
// Check if user is admin or management (has broader access) // Check if user is admin or management (has broader access)
const user = await User.findByPk(userId); const user = await User.findByPk(userId);
const isAdmin = user?.hasManagementAccess() || false; const isAdmin = user?.hasManagementAccess() || false;
// Build filter conditions (excluding status - stats should show all statuses) // Build filter conditions
let filterConditions = ''; let filterConditions = '';
const replacements: any = { start: range.start, end: range.end, userId }; const replacements: any = { userId };
// Add date range to replacements if date range is applied
if (applyDateRange && range) {
replacements.start = range.start;
replacements.end = range.end;
}
// Status filter
if (status && status !== 'all') {
const statusUpper = status.toUpperCase();
if (statusUpper === 'PENDING') {
// Pending includes both PENDING and IN_PROGRESS
filterConditions += ` AND (wf.status = 'PENDING' OR wf.status = 'IN_PROGRESS')`;
} else if (statusUpper === 'CLOSED') {
filterConditions += ` AND wf.status = 'CLOSED'`;
} else if (statusUpper === 'REJECTED') {
filterConditions += ` AND wf.status = 'REJECTED'`;
} else if (statusUpper === 'APPROVED') {
filterConditions += ` AND wf.status = 'APPROVED'`;
} else {
// Fallback: use the uppercase value as-is
filterConditions += ` AND wf.status = :status`;
replacements.status = statusUpper;
}
}
// Priority filter // Priority filter
if (priority && priority !== 'all') { if (priority && priority !== 'all') {
@ -223,26 +250,42 @@ export class DashboardService {
// Organization Level: Admin/Management see ALL requests across organization // Organization Level: Admin/Management see ALL requests across organization
// Personal Level: Regular users see only requests they INITIATED // Personal Level: Regular users see only requests they INITIATED
// Note: For pending/open requests, count ALL pending requests regardless of creation date // Note: If dateRange is provided, filter by submission_date. Otherwise, show all requests.
// For approved/rejected, count only those submitted in date range (use submission_date, not created_at) // For pending/open requests, if no date range, count ALL pending requests regardless of creation date
let whereClauseForDateRange = ` // For approved/rejected/closed, if date range is provided, count only those submitted in date range
WHERE wf.submission_date BETWEEN :start AND :end const dateFilterClause = applyDateRange
? `wf.submission_date BETWEEN :start AND :end AND wf.submission_date IS NOT NULL`
: `1=1`; // No date filter - show all requests
let whereClauseForAllRequests = `
WHERE ${dateFilterClause}
AND wf.is_draft = false AND wf.is_draft = false
AND (wf.is_deleted IS NULL OR wf.is_deleted = false) AND (wf.is_deleted IS NULL OR wf.is_deleted = false)
AND wf.submission_date IS NOT NULL
${!isAdmin ? `AND wf.initiator_id = :userId` : ''} ${!isAdmin ? `AND wf.initiator_id = :userId` : ''}
${filterConditions} ${filterConditions}
`; `;
let whereClauseForAllPending = ` // For pending requests, if no date range is applied, don't filter by date at all
WHERE wf.is_draft = false // This ensures pending requests are always counted regardless of submission date
const pendingDateFilterClause = applyDateRange
? `wf.submission_date BETWEEN :start AND :end AND wf.submission_date IS NOT NULL`
: `1=1`; // No date filter for pending requests
let whereClauseForPending = `
WHERE ${pendingDateFilterClause}
AND wf.is_draft = false
AND (wf.is_deleted IS NULL OR wf.is_deleted = false) AND (wf.is_deleted IS NULL OR wf.is_deleted = false)
AND (wf.status = 'PENDING' OR wf.status = 'IN_PROGRESS') AND (wf.status = 'PENDING' OR wf.status = 'IN_PROGRESS')
${!isAdmin ? `AND wf.initiator_id = :userId` : ''} ${!isAdmin ? `AND wf.initiator_id = :userId` : ''}
${filterConditions} ${filterConditions.replace(/AND \(wf\.status = 'PENDING' OR wf\.status = 'IN_PROGRESS'\)|AND wf\.status = 'PENDING'|AND wf\.status = 'IN_PROGRESS'/g, '').trim()}
`; `;
// Get total, approved, rejected, and closed requests created in date range // Clean up any double ANDs
whereClauseForPending = whereClauseForPending.replace(/\s+AND\s+AND/g, ' AND');
// Get total, approved, rejected, and closed requests
// If date range is applied, only count requests submitted in that range
// If no date range, count all requests matching other filters
const result = await sequelize.query(` const result = await sequelize.query(`
SELECT SELECT
COUNT(*)::int AS total_requests, COUNT(*)::int AS total_requests,
@ -250,19 +293,20 @@ export class DashboardService {
COUNT(CASE WHEN wf.status = 'REJECTED' THEN 1 END)::int AS rejected_requests, COUNT(CASE WHEN wf.status = 'REJECTED' THEN 1 END)::int AS rejected_requests,
COUNT(CASE WHEN wf.status = 'CLOSED' THEN 1 END)::int AS closed_requests COUNT(CASE WHEN wf.status = 'CLOSED' THEN 1 END)::int AS closed_requests
FROM workflow_requests wf FROM workflow_requests wf
${whereClauseForDateRange} ${whereClauseForAllRequests}
`, { `, {
replacements, replacements,
type: QueryTypes.SELECT type: QueryTypes.SELECT
}); });
// Get ALL pending/open requests (regardless of creation date) // Get ALL pending/open requests
// Organization Level (Admin): All pending requests across organization // Organization Level (Admin): All pending requests across organization
// Personal Level (Regular User): Only pending requests they initiated // Personal Level (Regular User): Only pending requests they initiated
// If no date range, count all pending requests regardless of submission date
const pendingResult = await sequelize.query(` const pendingResult = await sequelize.query(`
SELECT COUNT(*)::int AS open_requests SELECT COUNT(*)::int AS open_requests
FROM workflow_requests wf FROM workflow_requests wf
${whereClauseForAllPending} ${whereClauseForPending}
`, { `, {
replacements, replacements,
type: QueryTypes.SELECT type: QueryTypes.SELECT
@ -311,11 +355,11 @@ export class DashboardService {
// For regular users: only their initiated requests // For regular users: only their initiated requests
// For admin: all requests // For admin: all requests
// Include requests that were COMPLETED (APPROVED, REJECTED, or CLOSED) within the date range // Include only CLOSED requests (ignore APPROVED and REJECTED)
// CLOSED status represents approved requests that were finalized with a conclusion remark // CLOSED status represents requests that were finalized with a conclusion remark
// This ensures we capture all requests that finished during the period, regardless of when they started // This ensures we capture all requests that finished during the period, regardless of when they started
let whereClause = ` let whereClause = `
WHERE wf.status IN ('APPROVED', 'REJECTED', 'CLOSED') WHERE wf.status = 'CLOSED'
AND wf.is_draft = false AND wf.is_draft = false
AND wf.submission_date IS NOT NULL AND wf.submission_date IS NOT NULL
AND ( AND (
@ -325,7 +369,7 @@ export class DashboardService {
${!isAdmin ? `AND wf.initiator_id = :userId` : ''} ${!isAdmin ? `AND wf.initiator_id = :userId` : ''}
`; `;
// Get completed requests with their submission and closure dates // Get closed requests with their submission and closure dates
const completedRequests = await sequelize.query(` const completedRequests = await sequelize.query(`
SELECT SELECT
wf.request_id, wf.request_id,
@ -340,11 +384,11 @@ export class DashboardService {
type: QueryTypes.SELECT type: QueryTypes.SELECT
}); });
// Calculate cycle time using working hours for each request // Calculate cycle time using working hours for each request, grouped by priority
const { calculateElapsedWorkingHours } = await import('@utils/tatTimeUtils'); const { calculateElapsedWorkingHours } = await import('@utils/tatTimeUtils');
const cycleTimes: number[] = []; const priorityCycleTimes = new Map<string, number[]>();
logger.info(`[Dashboard] Calculating cycle time for ${completedRequests.length} completed requests`); logger.info(`[Dashboard] Calculating cycle time for ${completedRequests.length} closed requests`);
for (const req of completedRequests as any) { for (const req of completedRequests as any) {
const submissionDate = req.submission_date; const submissionDate = req.submission_date;
@ -362,7 +406,13 @@ export class DashboardService {
completionDate, completionDate,
priority priority
); );
cycleTimes.push(elapsedHours);
// Group by priority
if (!priorityCycleTimes.has(priority)) {
priorityCycleTimes.set(priority, []);
}
priorityCycleTimes.get(priority)!.push(elapsedHours);
logger.info(`[Dashboard] Request ${req.request_id} (${priority}): ${elapsedHours.toFixed(2)}h (submission: ${submissionDate}, completion: ${completionDate})`); logger.info(`[Dashboard] Request ${req.request_id} (${priority}): ${elapsedHours.toFixed(2)}h (submission: ${submissionDate}, completion: ${completionDate})`);
} catch (error) { } catch (error) {
logger.error(`[Dashboard] Error calculating cycle time for request ${req.request_id}:`, error); logger.error(`[Dashboard] Error calculating cycle time for request ${req.request_id}:`, error);
@ -376,6 +426,31 @@ export class DashboardService {
// This ensures consistency between Dashboard and All Requests screen // This ensures consistency between Dashboard and All Requests screen
} }
// Calculate average per priority
const expressCycleTimes = priorityCycleTimes.get('express') || [];
const standardCycleTimes = priorityCycleTimes.get('standard') || [];
const expressAvg = expressCycleTimes.length > 0
? Math.round((expressCycleTimes.reduce((sum, hours) => sum + hours, 0) / expressCycleTimes.length) * 100) / 100
: 0;
const standardAvg = standardCycleTimes.length > 0
? Math.round((standardCycleTimes.reduce((sum, hours) => sum + hours, 0) / standardCycleTimes.length) * 100) / 100
: 0;
// Calculate overall average as average of EXPRESS and STANDARD averages
// This is the average of the two priority averages (not weighted by count)
let avgCycleTimeHours = 0;
if (expressAvg > 0 && standardAvg > 0) {
avgCycleTimeHours = Math.round(((expressAvg + standardAvg) / 2) * 100) / 100;
} else if (expressAvg > 0) {
avgCycleTimeHours = expressAvg;
} else if (standardAvg > 0) {
avgCycleTimeHours = standardAvg;
}
logger.info(`[Dashboard] Cycle time calculation: EXPRESS=${expressAvg.toFixed(2)}h (${expressCycleTimes.length} requests), STANDARD=${standardAvg.toFixed(2)}h (${standardCycleTimes.length} requests), Overall=${avgCycleTimeHours.toFixed(2)}h`);
// Count ALL requests (pending, in-progress, approved, rejected, closed) that have currently breached TAT // Count ALL requests (pending, in-progress, approved, rejected, closed) that have currently breached TAT
// Use the same logic as Requests screen: check currentLevelSLA status using calculateSLAStatus // Use the same logic as Requests screen: check currentLevelSLA status using calculateSLAStatus
// This ensures delayedWorkflows matches what users see when filtering for "breached" in All Requests screen // This ensures delayedWorkflows matches what users see when filtering for "breached" in All Requests screen
@ -396,12 +471,12 @@ export class DashboardService {
FROM workflow_requests wf FROM workflow_requests wf
LEFT JOIN approval_levels al ON al.request_id = wf.request_id LEFT JOIN approval_levels al ON al.request_id = wf.request_id
AND al.level_number = wf.current_level AND al.level_number = wf.current_level
AND (al.status = 'IN_PROGRESS' OR (wf.status IN ('APPROVED', 'REJECTED', 'CLOSED') AND al.status = 'APPROVED')) AND (al.status = 'IN_PROGRESS' OR (wf.status = 'CLOSED' AND al.status = 'APPROVED'))
WHERE wf.is_draft = false WHERE wf.is_draft = false
AND wf.submission_date IS NOT NULL AND wf.submission_date IS NOT NULL
AND ( AND (
-- Completed requests: must be completed in date range -- Completed requests: must be CLOSED in date range (ignore APPROVED and REJECTED)
(wf.status IN ('APPROVED', 'REJECTED', 'CLOSED') (wf.status = 'CLOSED'
AND ( AND (
(wf.closure_date IS NOT NULL AND wf.closure_date BETWEEN :start AND :end) (wf.closure_date IS NOT NULL AND wf.closure_date BETWEEN :start AND :end)
OR (wf.closure_date IS NULL AND wf.updated_at BETWEEN :start AND :end) OR (wf.closure_date IS NULL AND wf.updated_at BETWEEN :start AND :end)
@ -439,7 +514,7 @@ export class DashboardService {
let recalculatedCompliantCount = 0; let recalculatedCompliantCount = 0;
for (const req of allRequestsBreached as any) { for (const req of allRequestsBreached as any) {
const isCompleted = req.status === 'APPROVED' || req.status === 'REJECTED' || req.status === 'CLOSED'; const isCompleted = req.status === 'CLOSED';
// Check current level SLA (same logic as Requests screen) // Check current level SLA (same logic as Requests screen)
let isBreached = false; let isBreached = false;
@ -493,27 +568,20 @@ export class DashboardService {
// Total delayed workflows = completed breached + currently pending/in-progress breached // Total delayed workflows = completed breached + currently pending/in-progress breached
const totalDelayedWorkflows = finalBreachedCount + pendingBreachedCount; const totalDelayedWorkflows = finalBreachedCount + pendingBreachedCount;
// Compliant workflows = all completed requests (APPROVED, REJECTED, CLOSED) that did NOT breach TAT // Compliant workflows = all CLOSED requests that did NOT breach TAT
// This includes: // This includes:
// - Approved requests that were approved within TAT // - Closed requests that were closed within TAT
// - Closed requests that were closed within TAT
// - Rejected requests that were rejected within TAT (before TAT was exceeded)
// Use recalculated compliant count from above which uses same logic as Requests screen // Use recalculated compliant count from above which uses same logic as Requests screen
// Note: Only counting CLOSED requests now (APPROVED and REJECTED are ignored)
const totalCompleted = recalculatedBreachedCount + recalculatedCompliantCount; const totalCompleted = recalculatedBreachedCount + recalculatedCompliantCount;
const compliantCount = recalculatedCompliantCount; const compliantCount = recalculatedCompliantCount;
// Compliance percentage = (compliant / total completed) * 100 // Compliance percentage = (compliant / total completed) * 100
// This shows what percentage of completed requests (approved/closed/rejected) were completed within TAT // This shows what percentage of CLOSED requests were completed within TAT
const compliancePercent = totalCompleted > 0 ? Math.round((compliantCount / totalCompleted) * 100) : 0; const compliancePercent = totalCompleted > 0 ? Math.round((compliantCount / totalCompleted) * 100) : 0;
// Calculate average cycle time (rounded to 2 decimal places for accuracy) // Average cycle time is already calculated above from priority averages
const sum = cycleTimes.reduce((sum, hours) => sum + hours, 0); logger.info(`[Dashboard] Compliance calculation: ${totalCompleted} total completed (CLOSED), ${finalBreachedCount} breached, ${compliantCount} compliant`);
const avgCycleTimeHours = cycleTimes.length > 0
? Math.round((sum / cycleTimes.length) * 100) / 100
: 0;
logger.info(`[Dashboard] Cycle time calculation: ${cycleTimes.length} requests included, sum: ${sum.toFixed(2)}h, average: ${avgCycleTimeHours.toFixed(2)}h`);
logger.info(`[Dashboard] Compliance calculation: ${totalCompleted} total completed (APPROVED/REJECTED/CLOSED), ${finalBreachedCount} breached, ${compliantCount} compliant`);
logger.info(`[Dashboard] Breached requests (using Requests screen logic): ${finalBreachedCount} completed breached + ${pendingBreachedCount} pending/in-progress breached = ${totalDelayedWorkflows} total delayed`); logger.info(`[Dashboard] Breached requests (using Requests screen logic): ${finalBreachedCount} completed breached + ${pendingBreachedCount} pending/in-progress breached = ${totalDelayedWorkflows} total delayed`);
return { return {
@ -1650,12 +1718,15 @@ export class DashboardService {
type: QueryTypes.SELECT type: QueryTypes.SELECT
}); });
// Get only COMPLETED requests for cycle time calculation // Get only CLOSED requests for cycle time calculation (ignore APPROVED and REJECTED)
let whereClauseCompleted = ` let whereClauseCompleted = `
WHERE wf.submission_date BETWEEN :start AND :end WHERE wf.status = 'CLOSED'
AND wf.status IN ('APPROVED', 'REJECTED')
AND wf.is_draft = false AND wf.is_draft = false
AND wf.submission_date IS NOT NULL AND wf.submission_date IS NOT NULL
AND (
(wf.closure_date IS NOT NULL AND wf.closure_date BETWEEN :start AND :end)
OR (wf.closure_date IS NULL AND wf.updated_at BETWEEN :start AND :end)
)
${!isAdmin ? `AND wf.initiator_id = :userId` : ''} ${!isAdmin ? `AND wf.initiator_id = :userId` : ''}
`; `;