Re_Backend/src/migrations/20251104-create-kpi-views.ts

267 lines
11 KiB
TypeScript

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<void> {
// 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<void> {
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
}