import { QueryInterface } from 'sequelize'; /** * Migration to create database views for KPI reporting * These views pre-aggregate data for faster reporting queries */ export async function up(queryInterface: QueryInterface): Promise { // 1. Request Volume & Status Summary View await queryInterface.sequelize.query(` CREATE OR REPLACE VIEW vw_request_volume_summary AS SELECT w.request_id, w.request_number, w.title, w.status, w.priority, w.template_type, w.submission_date, w.closure_date, w.created_at, u.user_id as initiator_id, u.display_name as initiator_name, u.department as initiator_department, EXTRACT(EPOCH FROM (COALESCE(w.closure_date, NOW()) - w.submission_date)) / 3600 as cycle_time_hours, EXTRACT(EPOCH FROM (NOW() - w.submission_date)) / 3600 as age_hours, w.current_level, w.total_levels, w.total_tat_hours, CASE WHEN w.status IN ('APPROVED', 'REJECTED', 'CLOSED') THEN 'COMPLETED' WHEN w.status = 'DRAFT' THEN 'DRAFT' ELSE 'IN_PROGRESS' END as status_category FROM workflow_requests w LEFT JOIN users u ON w.initiator_id = u.user_id WHERE w.is_deleted = false; `); // 2. TAT Compliance View await queryInterface.sequelize.query(` CREATE OR REPLACE VIEW vw_tat_compliance AS SELECT al.level_id, al.request_id, w.request_number, w.priority, w.status as request_status, al.level_number, al.approver_id, al.approver_name, u.department as approver_department, al.status as level_status, al.tat_hours as allocated_hours, al.elapsed_hours, al.remaining_hours, al.tat_percentage_used, al.level_start_time, al.level_end_time, al.action_date, al.tat50_alert_sent, al.tat75_alert_sent, al.tat_breached, CASE WHEN al.status IN ('APPROVED', 'REJECTED') AND al.elapsed_hours <= al.tat_hours THEN true WHEN al.status IN ('APPROVED', 'REJECTED') AND al.elapsed_hours > al.tat_hours THEN false WHEN al.status IN ('PENDING', 'IN_PROGRESS') AND al.tat_percentage_used >= 100 THEN false ELSE null END as completed_within_tat, CASE WHEN al.tat_percentage_used < 50 THEN 'ON_TRACK' WHEN al.tat_percentage_used < 75 THEN 'AT_RISK' WHEN al.tat_percentage_used < 100 THEN 'CRITICAL' ELSE 'BREACHED' END as tat_status, CASE WHEN al.status IN ('APPROVED', 'REJECTED') THEN al.tat_hours - al.elapsed_hours ELSE 0 END as time_saved_hours FROM approval_levels al JOIN workflow_requests w ON al.request_id = w.request_id LEFT JOIN users u ON al.approver_id = u.user_id WHERE w.is_deleted = false; `); // 3. Approver Performance View await queryInterface.sequelize.query(` CREATE OR REPLACE VIEW vw_approver_performance AS SELECT al.approver_id, u.display_name as approver_name, u.department, u.designation, COUNT(*) as total_assignments, COUNT(CASE WHEN al.status = 'PENDING' THEN 1 END) as pending_count, COUNT(CASE WHEN al.status = 'IN_PROGRESS' THEN 1 END) as in_progress_count, COUNT(CASE WHEN al.status = 'APPROVED' THEN 1 END) as approved_count, COUNT(CASE WHEN al.status = 'REJECTED' THEN 1 END) as rejected_count, AVG(CASE WHEN al.status IN ('APPROVED', 'REJECTED') THEN al.elapsed_hours END) as avg_response_time_hours, SUM(CASE WHEN al.elapsed_hours <= al.tat_hours AND al.status IN ('APPROVED', 'REJECTED') THEN 1 ELSE 0 END)::FLOAT / NULLIF(COUNT(CASE WHEN al.status IN ('APPROVED', 'REJECTED') THEN 1 END), 0) * 100 as tat_compliance_percentage, COUNT(CASE WHEN al.tat_breached = true THEN 1 END) as breaches_count, MIN(CASE WHEN al.status = 'PENDING' OR al.status = 'IN_PROGRESS' THEN EXTRACT(EPOCH FROM (NOW() - al.level_start_time)) / 3600 END) as oldest_pending_hours FROM approval_levels al JOIN users u ON al.approver_id = u.user_id JOIN workflow_requests w ON al.request_id = w.request_id WHERE w.is_deleted = false GROUP BY al.approver_id, u.display_name, u.department, u.designation; `); // 4. TAT Alerts Summary View await queryInterface.sequelize.query(` CREATE OR REPLACE VIEW vw_tat_alerts_summary AS SELECT ta.alert_id, ta.request_id, w.request_number, w.title as request_title, w.priority, ta.level_id, al.level_number, ta.approver_id, ta.alert_type, ta.threshold_percentage, ta.tat_hours_allocated, ta.tat_hours_elapsed, ta.tat_hours_remaining, ta.alert_sent_at, ta.expected_completion_time, ta.is_breached, ta.was_completed_on_time, ta.completion_time, al.status as level_status, EXTRACT(EPOCH FROM (ta.alert_sent_at - ta.level_start_time)) / 3600 as hours_before_alert, CASE WHEN ta.completion_time IS NOT NULL THEN EXTRACT(EPOCH FROM (ta.completion_time - ta.alert_sent_at)) / 3600 ELSE NULL END as response_time_after_alert_hours, ta.metadata FROM tat_alerts ta JOIN workflow_requests w ON ta.request_id = w.request_id JOIN approval_levels al ON ta.level_id = al.level_id WHERE w.is_deleted = false ORDER BY ta.alert_sent_at DESC; `); // 5. Department-wise Workflow Summary View await queryInterface.sequelize.query(` CREATE OR REPLACE VIEW vw_department_summary AS SELECT u.department, COUNT(DISTINCT w.request_id) as total_requests, COUNT(DISTINCT CASE WHEN w.status = 'DRAFT' THEN w.request_id END) as draft_requests, COUNT(DISTINCT CASE WHEN w.status IN ('PENDING', 'IN_PROGRESS') THEN w.request_id END) as open_requests, COUNT(DISTINCT CASE WHEN w.status = 'APPROVED' THEN w.request_id END) as approved_requests, COUNT(DISTINCT CASE WHEN w.status = 'REJECTED' THEN w.request_id END) as rejected_requests, AVG(CASE WHEN w.closure_date IS NOT NULL THEN EXTRACT(EPOCH FROM (w.closure_date - w.submission_date)) / 3600 END) as avg_cycle_time_hours, COUNT(DISTINCT CASE WHEN w.priority = 'EXPRESS' THEN w.request_id END) as express_priority_count, COUNT(DISTINCT CASE WHEN w.priority = 'STANDARD' THEN w.request_id END) as standard_priority_count FROM users u LEFT JOIN workflow_requests w ON u.user_id = w.initiator_id AND w.is_deleted = false WHERE u.department IS NOT NULL GROUP BY u.department; `); // 6. Daily/Weekly KPI Metrics View await queryInterface.sequelize.query(` CREATE OR REPLACE VIEW vw_daily_kpi_metrics AS SELECT DATE(w.created_at) as date, COUNT(*) as requests_created, COUNT(CASE WHEN w.submission_date IS NOT NULL AND DATE(w.submission_date) = DATE(w.created_at) THEN 1 END) as requests_submitted, COUNT(CASE WHEN w.closure_date IS NOT NULL AND DATE(w.closure_date) = DATE(w.created_at) THEN 1 END) as requests_closed, COUNT(CASE WHEN w.status = 'APPROVED' AND DATE(w.closure_date) = DATE(w.created_at) THEN 1 END) as requests_approved, COUNT(CASE WHEN w.status = 'REJECTED' AND DATE(w.closure_date) = DATE(w.created_at) THEN 1 END) as requests_rejected, AVG(CASE WHEN w.closure_date IS NOT NULL AND DATE(w.closure_date) = DATE(w.created_at) THEN EXTRACT(EPOCH FROM (w.closure_date - w.submission_date)) / 3600 END) as avg_completion_time_hours FROM workflow_requests w WHERE w.is_deleted = false GROUP BY DATE(w.created_at) ORDER BY DATE(w.created_at) DESC; `); // 7. Workflow Aging Report View await queryInterface.sequelize.query(` CREATE OR REPLACE VIEW vw_workflow_aging AS SELECT w.request_id, w.request_number, w.title, w.status, w.priority, w.current_level, w.total_levels, w.submission_date, EXTRACT(EPOCH FROM (NOW() - w.submission_date)) / (3600 * 24) as age_days, CASE WHEN EXTRACT(EPOCH FROM (NOW() - w.submission_date)) / (3600 * 24) < 3 THEN 'FRESH' WHEN EXTRACT(EPOCH FROM (NOW() - w.submission_date)) / (3600 * 24) < 7 THEN 'NORMAL' WHEN EXTRACT(EPOCH FROM (NOW() - w.submission_date)) / (3600 * 24) < 14 THEN 'AGING' ELSE 'CRITICAL' END as age_category, al.approver_name as current_approver, al.level_start_time as current_level_start, EXTRACT(EPOCH FROM (NOW() - al.level_start_time)) / 3600 as current_level_age_hours, al.tat_hours as current_level_tat_hours, al.tat_percentage_used as current_level_tat_used FROM workflow_requests w LEFT JOIN approval_levels al ON w.request_id = al.request_id AND al.level_number = w.current_level AND al.status IN ('PENDING', 'IN_PROGRESS') WHERE w.status IN ('PENDING', 'IN_PROGRESS') AND w.is_deleted = false ORDER BY age_days DESC; `); // 8. Engagement & Quality Metrics View await queryInterface.sequelize.query(` CREATE OR REPLACE VIEW vw_engagement_metrics AS SELECT w.request_id, w.request_number, w.title, w.status, COUNT(DISTINCT wn.note_id) as work_notes_count, COUNT(DISTINCT d.document_id) as documents_count, COUNT(DISTINCT p.participant_id) as spectators_count, COUNT(DISTINCT al.approver_id) as approvers_count, MAX(wn.created_at) as last_comment_date, MAX(d.uploaded_at) as last_document_date, CASE WHEN COUNT(DISTINCT wn.note_id) > 10 THEN 'HIGH' WHEN COUNT(DISTINCT wn.note_id) > 5 THEN 'MEDIUM' ELSE 'LOW' END as engagement_level FROM workflow_requests w LEFT JOIN work_notes wn ON w.request_id = wn.request_id AND wn.is_deleted = false LEFT JOIN documents d ON w.request_id = d.request_id AND d.is_deleted = false LEFT JOIN participants p ON w.request_id = p.request_id AND p.participant_type = 'SPECTATOR' LEFT JOIN approval_levels al ON w.request_id = al.request_id WHERE w.is_deleted = false GROUP BY w.request_id, w.request_number, w.title, w.status; `); // KPI views created } export async function down(queryInterface: QueryInterface): Promise { await queryInterface.sequelize.query('DROP VIEW IF EXISTS vw_engagement_metrics;'); await queryInterface.sequelize.query('DROP VIEW IF EXISTS vw_workflow_aging;'); await queryInterface.sequelize.query('DROP VIEW IF EXISTS vw_daily_kpi_metrics;'); await queryInterface.sequelize.query('DROP VIEW IF EXISTS vw_department_summary;'); await queryInterface.sequelize.query('DROP VIEW IF EXISTS vw_tat_alerts_summary;'); await queryInterface.sequelize.query('DROP VIEW IF EXISTS vw_approver_performance;'); await queryInterface.sequelize.query('DROP VIEW IF EXISTS vw_tat_compliance;'); await queryInterface.sequelize.query('DROP VIEW IF EXISTS vw_request_volume_summary;'); // KPI views dropped }