267 lines
11 KiB
TypeScript
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
|
|
}
|
|
|