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};
//# sourceMappingURL=conclusionApi-DaHT8E_1.js.map
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-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;
}
</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/radix-vendor-BP4rDxsU.js">
<link rel="modulepreload" crossorigin href="/assets/utils-vendor-DHm03ykU.js">

View File

@ -133,15 +133,42 @@ export class DashboardService {
search?: 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)
const user = await User.findByPk(userId);
const isAdmin = user?.hasManagementAccess() || false;
// Build filter conditions (excluding status - stats should show all statuses)
// Build filter conditions
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
if (priority && priority !== 'all') {
@ -223,26 +250,42 @@ export class DashboardService {
// Organization Level: Admin/Management see ALL requests across organization
// Personal Level: Regular users see only requests they INITIATED
// Note: For pending/open requests, count ALL pending requests regardless of creation date
// For approved/rejected, count only those submitted in date range (use submission_date, not created_at)
let whereClauseForDateRange = `
WHERE wf.submission_date BETWEEN :start AND :end
// Note: If dateRange is provided, filter by submission_date. Otherwise, show all requests.
// For pending/open requests, if no date range, count ALL pending requests regardless of creation date
// For approved/rejected/closed, if date range is provided, count only those submitted in date range
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_deleted IS NULL OR wf.is_deleted = false)
AND wf.submission_date IS NOT NULL
${!isAdmin ? `AND wf.initiator_id = :userId` : ''}
${filterConditions}
`;
let whereClauseForAllPending = `
WHERE wf.is_draft = false
// For pending requests, if no date range is applied, don't filter by date at all
// 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.status = 'PENDING' OR wf.status = 'IN_PROGRESS')
${!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(`
SELECT
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 = 'CLOSED' THEN 1 END)::int AS closed_requests
FROM workflow_requests wf
${whereClauseForDateRange}
${whereClauseForAllRequests}
`, {
replacements,
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
// 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(`
SELECT COUNT(*)::int AS open_requests
FROM workflow_requests wf
${whereClauseForAllPending}
${whereClauseForPending}
`, {
replacements,
type: QueryTypes.SELECT
@ -311,11 +355,11 @@ export class DashboardService {
// For regular users: only their initiated requests
// For admin: all requests
// Include requests that were COMPLETED (APPROVED, REJECTED, or CLOSED) within the date range
// CLOSED status represents approved requests that were finalized with a conclusion remark
// Include only CLOSED requests (ignore APPROVED and REJECTED)
// 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
let whereClause = `
WHERE wf.status IN ('APPROVED', 'REJECTED', 'CLOSED')
WHERE wf.status = 'CLOSED'
AND wf.is_draft = false
AND wf.submission_date IS NOT NULL
AND (
@ -325,7 +369,7 @@ export class DashboardService {
${!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(`
SELECT
wf.request_id,
@ -340,11 +384,11 @@ export class DashboardService {
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 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) {
const submissionDate = req.submission_date;
@ -362,7 +406,13 @@ export class DashboardService {
completionDate,
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})`);
} catch (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
}
// 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
// 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
@ -396,12 +471,12 @@ export class DashboardService {
FROM workflow_requests wf
LEFT JOIN approval_levels al ON al.request_id = wf.request_id
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
AND wf.submission_date IS NOT NULL
AND (
-- Completed requests: must be completed in date range
(wf.status IN ('APPROVED', 'REJECTED', 'CLOSED')
-- Completed requests: must be CLOSED in date range (ignore APPROVED and REJECTED)
(wf.status = 'CLOSED'
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)
@ -439,7 +514,7 @@ export class DashboardService {
let recalculatedCompliantCount = 0;
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)
let isBreached = false;
@ -493,27 +568,20 @@ export class DashboardService {
// Total delayed workflows = completed breached + currently pending/in-progress breached
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:
// - Approved requests that were approved within TAT
// - Closed requests that were closed within TAT
// - Rejected requests that were rejected within TAT (before TAT was exceeded)
// - Closed requests that were closed within TAT
// 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 compliantCount = recalculatedCompliantCount;
// 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;
// Calculate average cycle time (rounded to 2 decimal places for accuracy)
const sum = cycleTimes.reduce((sum, hours) => sum + hours, 0);
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`);
// Average cycle time is already calculated above from priority averages
logger.info(`[Dashboard] Compliance calculation: ${totalCompleted} total completed (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`);
return {
@ -1650,12 +1718,15 @@ export class DashboardService {
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 = `
WHERE wf.submission_date BETWEEN :start AND :end
AND wf.status IN ('APPROVED', 'REJECTED')
WHERE wf.status = 'CLOSED'
AND wf.is_draft = false
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` : ''}
`;