import { WorkflowRequest } from '@models/WorkflowRequest'; import { ApprovalLevel } from '@models/ApprovalLevel'; import { Participant } from '@models/Participant'; import { Activity } from '@models/Activity'; import { WorkNote } from '@models/WorkNote'; import { Document } from '@models/Document'; import { TatAlert } from '@models/TatAlert'; import { User } from '@models/User'; import { Op, QueryTypes } from 'sequelize'; import { sequelize } from '@config/database'; import dayjs from 'dayjs'; import logger from '@utils/logger'; import { calculateSLAStatus } from '@utils/tatTimeUtils'; interface DateRangeFilter { start: Date; end: Date; } export class DashboardService { /** * Parse date range string to Date objects */ private parseDateRange(dateRange?: string): DateRangeFilter { const now = dayjs(); switch (dateRange) { case 'today': return { start: now.startOf('day').toDate(), end: now.endOf('day').toDate() }; case 'week': return { start: now.startOf('week').toDate(), end: now.endOf('week').toDate() }; case 'month': return { start: now.startOf('month').toDate(), end: now.endOf('month').toDate() }; case 'quarter': // Calculate quarter manually since dayjs doesn't support it by default const currentMonth = now.month(); const quarterStartMonth = Math.floor(currentMonth / 3) * 3; return { start: now.month(quarterStartMonth).startOf('month').toDate(), end: now.month(quarterStartMonth + 2).endOf('month').toDate() }; case 'year': return { start: now.startOf('year').toDate(), end: now.endOf('year').toDate() }; default: // Default to last 30 days return { start: now.subtract(30, 'day').toDate(), end: now.toDate() }; } } /** * Get all KPIs for dashboard */ async getKPIs(userId: string, dateRange?: string) { const range = this.parseDateRange(dateRange); // Run all KPI queries in parallel for performance const [ requestStats, tatEfficiency, approverLoad, engagement, aiInsights ] = await Promise.all([ this.getRequestStats(userId, dateRange), this.getTATEfficiency(userId, dateRange), this.getApproverLoad(userId, dateRange), this.getEngagementStats(userId, dateRange), this.getAIInsights(userId, dateRange) ]); return { requestVolume: requestStats, tatEfficiency, approverLoad, engagement, aiInsights, dateRange: { start: range.start, end: range.end, label: dateRange || 'last30days' } }; } /** * Get request volume and status statistics */ async getRequestStats(userId: string, dateRange?: string) { const range = this.parseDateRange(dateRange); // Check if user is admin const user = await User.findByPk(userId); const isAdmin = (user as any)?.isAdmin || false; // For regular users: show only requests they INITIATED (not participated in) // For admin: show all requests let whereClause = ` WHERE wf.created_at BETWEEN :start AND :end AND wf.is_draft = false ${!isAdmin ? `AND wf.initiator_id = :userId` : ''} `; const result = await sequelize.query(` SELECT COUNT(*)::int AS total_requests, COUNT(CASE WHEN wf.status = 'PENDING' OR wf.status = 'IN_PROGRESS' THEN 1 END)::int AS open_requests, COUNT(CASE WHEN wf.status = 'APPROVED' THEN 1 END)::int AS approved_requests, COUNT(CASE WHEN wf.status = 'REJECTED' THEN 1 END)::int AS rejected_requests FROM workflow_requests wf ${whereClause} `, { replacements: { start: range.start, end: range.end, userId }, type: QueryTypes.SELECT }); // Get draft count separately const draftResult = await sequelize.query(` SELECT COUNT(*)::int AS draft_count FROM workflow_requests wf WHERE wf.is_draft = true ${!isAdmin ? `AND wf.initiator_id = :userId` : ''} `, { replacements: { userId }, type: QueryTypes.SELECT }); const stats = result[0] as any; const drafts = (draftResult[0] as any); return { totalRequests: stats.total_requests || 0, openRequests: stats.open_requests || 0, approvedRequests: stats.approved_requests || 0, rejectedRequests: stats.rejected_requests || 0, draftRequests: drafts.draft_count || 0, changeFromPrevious: { total: '+0', open: '+0', approved: '+0', rejected: '+0' } }; } /** * Get TAT efficiency metrics */ async getTATEfficiency(userId: string, dateRange?: string) { const range = this.parseDateRange(dateRange); // Check if user is admin const user = await User.findByPk(userId); const isAdmin = (user as any)?.isAdmin || false; // For regular users: only their initiated requests // For admin: all requests let whereClause = ` WHERE wf.created_at BETWEEN :start AND :end AND wf.status IN ('APPROVED', 'REJECTED') AND wf.is_draft = false ${!isAdmin ? `AND wf.initiator_id = :userId` : ''} `; const result = await sequelize.query(` SELECT COUNT(*)::int AS total_completed, COUNT(CASE WHEN EXISTS ( SELECT 1 FROM tat_alerts ta WHERE ta.request_id = wf.request_id AND ta.is_breached = true ) THEN 1 END)::int AS breached_count, AVG( EXTRACT(EPOCH FROM (wf.updated_at - wf.submission_date)) / 3600 )::numeric AS avg_cycle_time_hours FROM workflow_requests wf ${whereClause} `, { replacements: { start: range.start, end: range.end, userId }, type: QueryTypes.SELECT }); const stats = result[0] as any; const totalCompleted = stats.total_completed || 0; const breachedCount = stats.breached_count || 0; const compliantCount = totalCompleted - breachedCount; const compliancePercent = totalCompleted > 0 ? Math.round((compliantCount / totalCompleted) * 100) : 0; return { avgTATCompliance: compliancePercent, avgCycleTimeHours: Math.round(parseFloat(stats.avg_cycle_time_hours || 0) * 10) / 10, avgCycleTimeDays: Math.round((parseFloat(stats.avg_cycle_time_hours || 0) / 24) * 10) / 10, delayedWorkflows: breachedCount, totalCompleted, compliantWorkflows: compliantCount, changeFromPrevious: { compliance: '+5.8%', // TODO: Calculate actual change cycleTime: '-0.5h' } }; } /** * Get approver load statistics */ async getApproverLoad(userId: string, dateRange?: string) { const range = this.parseDateRange(dateRange); // Get pending actions where user is the CURRENT active approver // This means: the request is at this user's level AND it's the current level const pendingResult = await sequelize.query(` SELECT COUNT(DISTINCT al.level_id)::int AS pending_count FROM approval_levels al JOIN workflow_requests wf ON al.request_id = wf.request_id WHERE al.approver_id = :userId AND al.status = 'IN_PROGRESS' AND wf.status IN ('PENDING', 'IN_PROGRESS') AND wf.is_draft = false AND al.level_number = wf.current_level `, { replacements: { userId }, type: QueryTypes.SELECT }); // Get completed approvals in date range const completedResult = await sequelize.query(` SELECT COUNT(*)::int AS completed_today, COUNT(CASE WHEN al.action_date >= :weekStart THEN 1 END)::int AS completed_this_week FROM approval_levels al WHERE al.approver_id = :userId AND al.status IN ('APPROVED', 'REJECTED') AND al.action_date BETWEEN :start AND :end `, { replacements: { userId, start: range.start, end: range.end, weekStart: dayjs().startOf('week').toDate() }, type: QueryTypes.SELECT }); const pending = (pendingResult[0] as any); const completed = (completedResult[0] as any); return { pendingActions: pending.pending_count || 0, completedToday: completed.completed_today || 0, completedThisWeek: completed.completed_this_week || 0, changeFromPrevious: { pending: '+2', completed: '+15%' } }; } /** * Get engagement and quality metrics */ async getEngagementStats(userId: string, dateRange?: string) { const range = this.parseDateRange(dateRange); // Check if user is admin const user = await User.findByPk(userId); const isAdmin = (user as any)?.isAdmin || false; // Get work notes count - uses created_at // For regular users: only from requests they initiated let workNotesWhereClause = ` WHERE wn.created_at BETWEEN :start AND :end ${!isAdmin ? `AND EXISTS ( SELECT 1 FROM workflow_requests wf WHERE wf.request_id = wn.request_id AND wf.initiator_id = :userId AND wf.is_draft = false )` : ''} `; const workNotesResult = await sequelize.query(` SELECT COUNT(*)::int AS work_notes_count FROM work_notes wn ${workNotesWhereClause} `, { replacements: { start: range.start, end: range.end, userId }, type: QueryTypes.SELECT }); // Get documents count - uses uploaded_at // For regular users: only from requests they initiated let documentsWhereClause = ` WHERE d.uploaded_at BETWEEN :start AND :end ${!isAdmin ? `AND EXISTS ( SELECT 1 FROM workflow_requests wf WHERE wf.request_id = d.request_id AND wf.initiator_id = :userId AND wf.is_draft = false )` : ''} `; const documentsResult = await sequelize.query(` SELECT COUNT(*)::int AS documents_count FROM documents d ${documentsWhereClause} `, { replacements: { start: range.start, end: range.end, userId }, type: QueryTypes.SELECT }); const workNotes = (workNotesResult[0] as any); const documents = (documentsResult[0] as any); return { workNotesAdded: workNotes.work_notes_count || 0, attachmentsUploaded: documents.documents_count || 0, changeFromPrevious: { workNotes: '+25', attachments: '+8' } }; } /** * Get AI insights and closure metrics */ async getAIInsights(userId: string, dateRange?: string) { const range = this.parseDateRange(dateRange); // Check if user is admin const user = await User.findByPk(userId); const isAdmin = (user as any)?.isAdmin || false; // For regular users: only their initiated requests let whereClause = ` WHERE wf.created_at BETWEEN :start AND :end AND wf.status = 'APPROVED' AND wf.conclusion_remark IS NOT NULL AND wf.is_draft = false ${!isAdmin ? `AND wf.initiator_id = :userId` : ''} `; const result = await sequelize.query(` SELECT COUNT(*)::int AS total_with_conclusion, AVG(LENGTH(wf.conclusion_remark))::numeric AS avg_remark_length, COUNT(CASE WHEN wf.ai_generated_conclusion IS NOT NULL AND wf.ai_generated_conclusion != '' THEN 1 END)::int AS ai_generated_count, COUNT(CASE WHEN wf.ai_generated_conclusion IS NULL OR wf.ai_generated_conclusion = '' THEN 1 END)::int AS manual_count FROM workflow_requests wf ${whereClause} `, { replacements: { start: range.start, end: range.end, userId }, type: QueryTypes.SELECT }); const stats = result[0] as any; const totalWithConclusion = stats.total_with_conclusion || 0; const aiCount = stats.ai_generated_count || 0; const aiAdoptionPercent = totalWithConclusion > 0 ? Math.round((aiCount / totalWithConclusion) * 100) : 0; return { avgConclusionRemarkLength: Math.round(parseFloat(stats.avg_remark_length || 0)), aiSummaryAdoptionPercent: aiAdoptionPercent, totalWithConclusion, aiGeneratedCount: aiCount, manualCount: stats.manual_count || 0, changeFromPrevious: { adoption: '+12%', length: '+50 chars' } }; } /** * Get AI Remark Utilization with monthly trends */ async getAIRemarkUtilization(userId: string, dateRange?: string) { const range = this.parseDateRange(dateRange); // Check if user is admin const user = await User.findByPk(userId); const isAdmin = (user as any)?.isAdmin || false; // For regular users: only their initiated requests const userFilter = !isAdmin ? `AND cr.edited_by = :userId` : ''; // Get overall metrics const overallMetrics = await sequelize.query(` SELECT COUNT(*)::int AS total_usage, COUNT(CASE WHEN cr.is_edited = true THEN 1 END)::int AS total_edits, ROUND( (COUNT(CASE WHEN cr.is_edited = true THEN 1 END)::numeric / NULLIF(COUNT(*)::numeric, 0)) * 100, 0 )::int AS edit_rate FROM conclusion_remarks cr WHERE cr.generated_at BETWEEN :start AND :end ${userFilter} `, { replacements: { start: range.start, end: range.end, userId }, type: QueryTypes.SELECT }); // Get monthly trends (last 7 months) const monthlyTrends = await sequelize.query(` SELECT TO_CHAR(DATE_TRUNC('month', cr.generated_at), 'Mon') AS month, EXTRACT(MONTH FROM cr.generated_at)::int AS month_num, COUNT(*)::int AS ai_usage, COUNT(CASE WHEN cr.is_edited = true THEN 1 END)::int AS manual_edits FROM conclusion_remarks cr WHERE cr.generated_at >= NOW() - INTERVAL '7 months' ${userFilter} GROUP BY month, month_num ORDER BY month_num ASC `, { replacements: { userId }, type: QueryTypes.SELECT }); const stats = overallMetrics[0] as any; return { totalUsage: stats.total_usage || 0, totalEdits: stats.total_edits || 0, editRate: stats.edit_rate || 0, monthlyTrends: monthlyTrends.map((m: any) => ({ month: m.month, aiUsage: m.ai_usage, manualEdits: m.manual_edits })) }; } /** * Get Approver Performance metrics with pagination */ async getApproverPerformance(userId: string, dateRange?: string, page: number = 1, limit: number = 10) { const range = this.parseDateRange(dateRange); // Check if user is admin const user = await User.findByPk(userId); const isAdmin = (user as any)?.isAdmin || false; // For regular users: return empty (only admins should see this) if (!isAdmin) { return { performance: [], currentPage: page, totalPages: 0, totalRecords: 0, limit }; } // Calculate offset const offset = (page - 1) * limit; // Get total count const countResult = await sequelize.query(` SELECT COUNT(DISTINCT al.approver_id) as total FROM approval_levels al WHERE al.action_date BETWEEN :start AND :end AND al.status IN ('APPROVED', 'REJECTED') HAVING COUNT(*) > 0 `, { replacements: { start: range.start, end: range.end }, type: QueryTypes.SELECT }); const totalRecords = Number((countResult[0] as any)?.total || 0); const totalPages = Math.ceil(totalRecords / limit); // Get approver performance metrics const approverMetrics = await sequelize.query(` SELECT al.approver_id, al.approver_name, COUNT(*)::int AS total_approved, ROUND( AVG( CASE WHEN al.tat_breached = false THEN 100 ELSE 0 END ), 0 )::int AS tat_compliance_percent, ROUND(AVG(al.elapsed_hours)::numeric, 1) AS avg_response_hours, COUNT(CASE WHEN al.status = 'PENDING' THEN 1 END)::int AS pending_count FROM approval_levels al WHERE al.action_date BETWEEN :start AND :end AND al.status IN ('APPROVED', 'REJECTED') GROUP BY al.approver_id, al.approver_name HAVING COUNT(*) > 0 ORDER BY total_approved DESC LIMIT :limit OFFSET :offset `, { replacements: { start: range.start, end: range.end, limit, offset }, type: QueryTypes.SELECT }); return { performance: approverMetrics.map((a: any) => ({ approverId: a.approver_id, approverName: a.approver_name, totalApproved: a.total_approved, tatCompliancePercent: a.tat_compliance_percent, avgResponseHours: parseFloat(a.avg_response_hours || 0), pendingCount: a.pending_count })), currentPage: page, totalPages, totalRecords, limit }; } /** * Get recent activity feed with pagination */ async getRecentActivity(userId: string, page: number = 1, limit: number = 10) { // Check if user is admin const user = await User.findByPk(userId); const isAdmin = (user as any)?.isAdmin || false; // For regular users: only activities from their initiated requests OR where they're a participant let whereClause = isAdmin ? '' : ` AND ( wf.initiator_id = :userId OR EXISTS ( SELECT 1 FROM participants p WHERE p.request_id = a.request_id AND p.user_id = :userId ) ) `; // Calculate offset const offset = (page - 1) * limit; // Get total count const countResult = await sequelize.query(` SELECT COUNT(*) as total FROM activities a JOIN workflow_requests wf ON a.request_id = wf.request_id WHERE a.created_at >= NOW() - INTERVAL '7 days' ${whereClause} `, { replacements: { userId }, type: QueryTypes.SELECT }); const totalRecords = Number((countResult[0] as any).total); const totalPages = Math.ceil(totalRecords / limit); // Get paginated activities const activities = await sequelize.query(` SELECT a.activity_id, a.request_id, a.activity_type AS type, a.activity_description, a.activity_category, a.user_id, a.user_name, a.created_at AS timestamp, wf.request_number, wf.title AS request_title, wf.priority FROM activities a JOIN workflow_requests wf ON a.request_id = wf.request_id WHERE a.created_at >= NOW() - INTERVAL '7 days' ${whereClause} ORDER BY a.created_at DESC LIMIT :limit OFFSET :offset `, { replacements: { userId, limit, offset }, type: QueryTypes.SELECT }); return { activities: activities.map((a: any) => ({ activityId: a.activity_id, requestId: a.request_id, requestNumber: a.request_number, requestTitle: a.request_title, type: a.type, action: a.activity_description || a.type, details: a.activity_category, userId: a.user_id, userName: a.user_name, timestamp: a.timestamp, priority: (a.priority || '').toLowerCase() })), currentPage: page, totalPages, totalRecords, limit }; } /** * Get critical requests (breached TAT or approaching deadline) with pagination */ async getCriticalRequests(userId: string, page: number = 1, limit: number = 10) { // Check if user is admin const user = await User.findByPk(userId); const isAdmin = (user as any)?.isAdmin || false; // For regular users: show only their initiated requests OR where they are current approver let whereClause = ` WHERE wf.status IN ('PENDING', 'IN_PROGRESS') AND wf.is_draft = false ${!isAdmin ? `AND ( wf.initiator_id = :userId OR EXISTS ( SELECT 1 FROM approval_levels al WHERE al.request_id = wf.request_id AND al.approver_id = :userId AND al.level_number = wf.current_level AND al.status = 'IN_PROGRESS' ) )` : ''} `; const criticalCondition = ` AND ( -- Has TAT breaches EXISTS ( SELECT 1 FROM tat_alerts ta WHERE ta.request_id = wf.request_id AND (ta.is_breached = true OR ta.threshold_percentage >= 75) ) -- Or is express priority OR wf.priority = 'EXPRESS' ) `; // Calculate offset const offset = (page - 1) * limit; // Get total count const countResult = await sequelize.query(` SELECT COUNT(*) as total FROM workflow_requests wf ${whereClause} ${criticalCondition} `, { replacements: { userId }, type: QueryTypes.SELECT }); const totalRecords = Number((countResult[0] as any).total); const totalPages = Math.ceil(totalRecords / limit); const criticalRequests = await sequelize.query(` SELECT wf.request_id, wf.request_number, wf.title, wf.priority, wf.status, wf.current_level, wf.total_levels, wf.submission_date, wf.total_tat_hours, ( SELECT COUNT(*)::int FROM tat_alerts ta WHERE ta.request_id = wf.request_id AND ta.is_breached = true ) AS breach_count, ( SELECT al.tat_hours FROM approval_levels al WHERE al.request_id = wf.request_id AND al.level_number = wf.current_level LIMIT 1 ) AS current_level_tat_hours, ( SELECT al.level_start_time FROM approval_levels al WHERE al.request_id = wf.request_id AND al.level_number = wf.current_level LIMIT 1 ) AS current_level_start_time FROM workflow_requests wf ${whereClause} ${criticalCondition} ORDER BY CASE WHEN wf.priority = 'EXPRESS' THEN 1 ELSE 2 END, breach_count DESC, wf.created_at ASC LIMIT :limit OFFSET :offset `, { replacements: { userId, limit, offset }, type: QueryTypes.SELECT }); // Calculate working hours TAT for each critical request's current level const criticalWithSLA = await Promise.all(criticalRequests.map(async (req: any) => { const priority = (req.priority || 'standard').toLowerCase(); const currentLevelTatHours = parseFloat(req.current_level_tat_hours) || 0; const currentLevelStartTime = req.current_level_start_time; let currentLevelRemainingHours = currentLevelTatHours; if (currentLevelStartTime && currentLevelTatHours > 0) { try { // Use working hours calculation for current level const slaData = await calculateSLAStatus(currentLevelStartTime, currentLevelTatHours, priority); currentLevelRemainingHours = slaData.remainingHours; } catch (error) { logger.error(`[Dashboard] Error calculating SLA for critical request ${req.request_id}:`, error); } } return { requestId: req.request_id, requestNumber: req.request_number, title: req.title, priority, status: (req.status || '').toLowerCase(), currentLevel: req.current_level, totalLevels: req.total_levels, submissionDate: req.submission_date, totalTATHours: currentLevelRemainingHours, // Current level remaining hours originalTATHours: currentLevelTatHours, // Original TAT hours allocated for current level breachCount: req.breach_count || 0, isCritical: req.breach_count > 0 || req.priority === 'EXPRESS' }; })); return { criticalRequests: criticalWithSLA, currentPage: page, totalPages, totalRecords, limit }; } /** * Get upcoming deadlines with pagination */ async getUpcomingDeadlines(userId: string, page: number = 1, limit: number = 10) { // Check if user is admin const user = await User.findByPk(userId); const isAdmin = (user as any)?.isAdmin || false; // For regular users: only show CURRENT LEVEL where they are the approver // For admins: show all current active levels let whereClause = ` WHERE wf.status IN ('PENDING', 'IN_PROGRESS') AND wf.is_draft = false AND al.status = 'IN_PROGRESS' AND al.level_number = wf.current_level ${!isAdmin ? `AND al.approver_id = :userId` : ''} `; // Calculate offset const offset = (page - 1) * limit; // Get total count const countResult = await sequelize.query(` SELECT COUNT(*) as total FROM approval_levels al JOIN workflow_requests wf ON al.request_id = wf.request_id ${whereClause} `, { replacements: { userId }, type: QueryTypes.SELECT }); const totalRecords = Number((countResult[0] as any).total); const totalPages = Math.ceil(totalRecords / limit); const deadlines = await sequelize.query(` SELECT al.level_id, al.request_id, al.level_number, al.approver_name, al.approver_email, al.tat_hours, al.level_start_time, wf.request_number, wf.title AS request_title, wf.priority, wf.current_level, wf.total_levels FROM approval_levels al JOIN workflow_requests wf ON al.request_id = wf.request_id ${whereClause} ORDER BY al.level_start_time ASC LIMIT :limit OFFSET :offset `, { replacements: { userId, limit, offset }, type: QueryTypes.SELECT }); // Calculate working hours TAT for each deadline const deadlinesWithSLA = await Promise.all(deadlines.map(async (d: any) => { const priority = (d.priority || 'standard').toLowerCase(); const tatHours = parseFloat(d.tat_hours) || 0; const levelStartTime = d.level_start_time; let elapsedHours = 0; let remainingHours = tatHours; let tatPercentageUsed = 0; if (levelStartTime && tatHours > 0) { try { // Use working hours calculation (same as RequestDetail screen) const slaData = await calculateSLAStatus(levelStartTime, tatHours, priority); elapsedHours = slaData.elapsedHours; remainingHours = slaData.remainingHours; tatPercentageUsed = slaData.percentageUsed; } catch (error) { logger.error(`[Dashboard] Error calculating SLA for level ${d.level_id}:`, error); } } return { levelId: d.level_id, requestId: d.request_id, requestNumber: d.request_number, requestTitle: d.request_title, levelNumber: d.level_number, currentLevel: d.current_level, totalLevels: d.total_levels, approverName: d.approver_name, approverEmail: d.approver_email, tatHours, elapsedHours, remainingHours, tatPercentageUsed, levelStartTime, priority }; })); // Sort by TAT percentage used (descending) const sortedDeadlines = deadlinesWithSLA.sort((a, b) => b.tatPercentageUsed - a.tatPercentageUsed); return { deadlines: sortedDeadlines, currentPage: page, totalPages, totalRecords, limit }; } /** * Get department-wise statistics */ async getDepartmentStats(userId: string, dateRange?: string) { const range = this.parseDateRange(dateRange); // Check if user is admin const user = await User.findByPk(userId); const isAdmin = (user as any)?.isAdmin || false; // For regular users: only their initiated requests let whereClause = ` WHERE wf.created_at BETWEEN :start AND :end AND wf.is_draft = false ${!isAdmin ? `AND wf.initiator_id = :userId` : ''} `; const deptStats = await sequelize.query(` SELECT COALESCE(u.department, 'Unknown') AS department, COUNT(*)::int AS total_requests, COUNT(CASE WHEN wf.status = 'APPROVED' THEN 1 END)::int AS approved, COUNT(CASE WHEN wf.status = 'REJECTED' THEN 1 END)::int AS rejected, COUNT(CASE WHEN wf.status IN ('PENDING', 'IN_PROGRESS') THEN 1 END)::int AS in_progress FROM workflow_requests wf JOIN users u ON wf.initiator_id = u.user_id ${whereClause} GROUP BY u.department ORDER BY total_requests DESC LIMIT 10 `, { replacements: { start: range.start, end: range.end, userId }, type: QueryTypes.SELECT }); return deptStats.map((d: any) => ({ department: d.department, totalRequests: d.total_requests, approved: d.approved, rejected: d.rejected, inProgress: d.in_progress, approvalRate: d.total_requests > 0 ? Math.round((d.approved / d.total_requests) * 100) : 0 })); } /** * Get priority distribution statistics */ async getPriorityDistribution(userId: string, dateRange?: string) { const range = this.parseDateRange(dateRange); // Check if user is admin const user = await User.findByPk(userId); const isAdmin = (user as any)?.isAdmin || false; // For regular users: only their initiated requests let whereClause = ` WHERE wf.created_at BETWEEN :start AND :end AND wf.is_draft = false ${!isAdmin ? `AND wf.initiator_id = :userId` : ''} `; const priorityStats = await sequelize.query(` SELECT wf.priority, COUNT(*)::int AS total_count, AVG( EXTRACT(EPOCH FROM (wf.updated_at - wf.submission_date)) / 3600 )::numeric AS avg_cycle_time_hours, COUNT(CASE WHEN wf.status = 'APPROVED' THEN 1 END)::int AS approved_count, COUNT(CASE WHEN EXISTS ( SELECT 1 FROM tat_alerts ta WHERE ta.request_id = wf.request_id AND ta.is_breached = true ) THEN 1 END)::int AS breached_count FROM workflow_requests wf ${whereClause} GROUP BY wf.priority `, { replacements: { start: range.start, end: range.end, userId }, type: QueryTypes.SELECT }); return priorityStats.map((p: any) => ({ priority: (p.priority || 'STANDARD').toLowerCase(), totalCount: p.total_count, avgCycleTimeHours: Math.round(parseFloat(p.avg_cycle_time_hours || 0) * 10) / 10, approvedCount: p.approved_count, breachedCount: p.breached_count, complianceRate: p.total_count > 0 ? Math.round(((p.total_count - p.breached_count) / p.total_count) * 100) : 0 })); } } export const dashboardService = new DashboardService();