dukcapil/migrations/000027_create_analytics_summary_tables.up.sql
Aditya Siregar aa662a321f Update
2025-09-01 12:06:14 +07:00

389 lines
16 KiB
PL/PgSQL

-- Create letter_summary table for daily aggregated statistics
CREATE TABLE IF NOT EXISTS letter_summary (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
summary_date DATE NOT NULL,
letter_type VARCHAR(20) NOT NULL, -- 'incoming' or 'outgoing'
total_count INTEGER DEFAULT 0,
pending_count INTEGER DEFAULT 0,
approved_count INTEGER DEFAULT 0,
rejected_count INTEGER DEFAULT 0,
archived_count INTEGER DEFAULT 0,
sent_count INTEGER DEFAULT 0,
avg_processing_hours DECIMAL(10,2),
min_processing_hours DECIMAL(10,2),
max_processing_hours DECIMAL(10,2),
median_processing_hours DECIMAL(10,2),
p95_processing_hours DECIMAL(10,2),
p99_processing_hours DECIMAL(10,2),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE(summary_date, letter_type)
);
-- Create department_letter_summary table
CREATE TABLE IF NOT EXISTS department_letter_summary (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
department_id UUID NOT NULL REFERENCES departments(id),
summary_date DATE NOT NULL,
incoming_count INTEGER DEFAULT 0,
outgoing_count INTEGER DEFAULT 0,
pending_incoming INTEGER DEFAULT 0,
pending_outgoing INTEGER DEFAULT 0,
approved_outgoing INTEGER DEFAULT 0,
rejected_outgoing INTEGER DEFAULT 0,
avg_response_hours DECIMAL(10,2),
completion_rate DECIMAL(5,2),
total_recipients INTEGER DEFAULT 0,
unique_senders INTEGER DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE(department_id, summary_date)
);
-- Create institution_letter_summary table
CREATE TABLE IF NOT EXISTS institution_letter_summary (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
institution_id UUID NOT NULL REFERENCES institutions(id),
summary_date DATE NOT NULL,
incoming_sent INTEGER DEFAULT 0,
outgoing_received INTEGER DEFAULT 0,
total_correspondence INTEGER DEFAULT 0,
avg_turnaround_hours DECIMAL(10,2),
last_activity_at TIMESTAMP,
priority_high_count INTEGER DEFAULT 0,
priority_medium_count INTEGER DEFAULT 0,
priority_low_count INTEGER DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE(institution_id, summary_date)
);
-- Create approval_sla_summary table
CREATE TABLE IF NOT EXISTS approval_sla_summary (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
summary_date DATE NOT NULL,
department_id UUID REFERENCES departments(id),
total_approvals INTEGER DEFAULT 0,
approved_count INTEGER DEFAULT 0,
rejected_count INTEGER DEFAULT 0,
pending_count INTEGER DEFAULT 0,
avg_approval_hours DECIMAL(10,2),
min_approval_hours DECIMAL(10,2),
max_approval_hours DECIMAL(10,2),
median_approval_hours DECIMAL(10,2),
within_sla_count INTEGER DEFAULT 0,
exceeded_sla_count INTEGER DEFAULT 0,
sla_compliance_rate DECIMAL(5,2),
avg_approval_steps DECIMAL(5,2),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE(summary_date, department_id)
);
-- Create indexes for better query performance
CREATE INDEX idx_letter_summary_date ON letter_summary(summary_date DESC);
CREATE INDEX idx_letter_summary_type_date ON letter_summary(letter_type, summary_date DESC);
CREATE INDEX idx_dept_letter_summary_dept_date ON department_letter_summary(department_id, summary_date DESC);
CREATE INDEX idx_dept_letter_summary_date ON department_letter_summary(summary_date DESC);
CREATE INDEX idx_inst_letter_summary_inst_date ON institution_letter_summary(institution_id, summary_date DESC);
CREATE INDEX idx_inst_letter_summary_date ON institution_letter_summary(summary_date DESC);
CREATE INDEX idx_approval_sla_summary_date ON approval_sla_summary(summary_date DESC);
CREATE INDEX idx_approval_sla_summary_dept_date ON approval_sla_summary(department_id, summary_date DESC);
-- Create function to update letter_summary
CREATE OR REPLACE FUNCTION update_letter_summary()
RETURNS TRIGGER AS $$
DECLARE
v_date DATE;
v_type VARCHAR(20);
BEGIN
-- Determine date and type
IF TG_TABLE_NAME = 'letters_incoming' THEN
v_type := 'incoming';
v_date := DATE(COALESCE(NEW.created_at, OLD.created_at));
ELSE
v_type := 'outgoing';
v_date := DATE(COALESCE(NEW.created_at, OLD.created_at));
END IF;
-- Update or insert summary
INSERT INTO letter_summary (
summary_date,
letter_type,
total_count,
pending_count,
approved_count,
rejected_count,
archived_count,
sent_count
)
SELECT
v_date,
v_type,
COUNT(*) as total_count,
COUNT(*) FILTER (WHERE status = 'pending' OR status = 'pending_approval') as pending_count,
COUNT(*) FILTER (WHERE status = 'approved') as approved_count,
COUNT(*) FILTER (WHERE status = 'rejected') as rejected_count,
COUNT(*) FILTER (WHERE status = 'archived') as archived_count,
COUNT(*) FILTER (WHERE status = 'sent') as sent_count
FROM (
SELECT status, created_at FROM letters_incoming WHERE DATE(created_at) = v_date AND v_type = 'incoming'
UNION ALL
SELECT status, created_at FROM letters_outgoing WHERE DATE(created_at) = v_date AND v_type = 'outgoing'
) t
ON CONFLICT (summary_date, letter_type)
DO UPDATE SET
total_count = EXCLUDED.total_count,
pending_count = EXCLUDED.pending_count,
approved_count = EXCLUDED.approved_count,
rejected_count = EXCLUDED.rejected_count,
archived_count = EXCLUDED.archived_count,
sent_count = EXCLUDED.sent_count,
updated_at = CURRENT_TIMESTAMP;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
-- Create triggers for automatic updates
CREATE TRIGGER update_letter_summary_on_incoming
AFTER INSERT OR UPDATE OR DELETE ON letters_incoming
FOR EACH ROW
EXECUTE FUNCTION update_letter_summary();
CREATE TRIGGER update_letter_summary_on_outgoing
AFTER INSERT OR UPDATE OR DELETE ON letters_outgoing
FOR EACH ROW
EXECUTE FUNCTION update_letter_summary();
-- Function to populate historical data
CREATE OR REPLACE FUNCTION populate_letter_summary_history()
RETURNS void AS $$
BEGIN
-- Populate letter_summary with historical data
INSERT INTO letter_summary (
summary_date,
letter_type,
total_count,
pending_count,
approved_count,
rejected_count,
archived_count,
sent_count,
avg_processing_hours
)
SELECT
DATE(created_at) as summary_date,
'incoming' as letter_type,
COUNT(*) as total_count,
COUNT(*) FILTER (WHERE status = 'pending') as pending_count,
COUNT(*) FILTER (WHERE status = 'approved') as approved_count,
COUNT(*) FILTER (WHERE status = 'rejected') as rejected_count,
COUNT(*) FILTER (WHERE status = 'archived') as archived_count,
0 as sent_count,
AVG(EXTRACT(EPOCH FROM (updated_at - created_at))/3600) as avg_processing_hours
FROM letters_incoming
WHERE deleted_at IS NULL
GROUP BY DATE(created_at)
UNION ALL
SELECT
DATE(created_at) as summary_date,
'outgoing' as letter_type,
COUNT(*) as total_count,
COUNT(*) FILTER (WHERE status = 'pending_approval') as pending_count,
COUNT(*) FILTER (WHERE status = 'approved') as approved_count,
COUNT(*) FILTER (WHERE status = 'rejected') as rejected_count,
COUNT(*) FILTER (WHERE status = 'archived') as archived_count,
COUNT(*) FILTER (WHERE status = 'sent') as sent_count,
AVG(EXTRACT(EPOCH FROM (updated_at - created_at))/3600) as avg_processing_hours
FROM letters_outgoing
WHERE deleted_at IS NULL
GROUP BY DATE(created_at)
ON CONFLICT (summary_date, letter_type) DO NOTHING;
-- Populate department_letter_summary
INSERT INTO department_letter_summary (
department_id,
summary_date,
incoming_count,
outgoing_count,
pending_incoming,
pending_outgoing,
approved_outgoing,
rejected_outgoing,
avg_response_hours,
completion_rate
)
SELECT
d.id as department_id,
dates.summary_date,
COALESCE(inc.count, 0) as incoming_count,
COALESCE(outg.count, 0) as outgoing_count,
COALESCE(inc.pending, 0) as pending_incoming,
COALESCE(outg.pending, 0) as pending_outgoing,
COALESCE(outg.approved, 0) as approved_outgoing,
COALESCE(outg.rejected, 0) as rejected_outgoing,
COALESCE(outg.avg_hours, 0) as avg_response_hours,
CASE
WHEN COALESCE(outg.count, 0) > 0
THEN (COALESCE(outg.completed, 0)::DECIMAL / outg.count::DECIMAL) * 100
ELSE 0
END as completion_rate
FROM departments d
CROSS JOIN (
SELECT DISTINCT DATE(created_at) as summary_date
FROM letters_incoming
UNION
SELECT DISTINCT DATE(created_at)
FROM letters_outgoing
) dates
LEFT JOIN (
SELECT
lir.recipient_department_id as dept_id,
DATE(li.created_at) as date,
COUNT(DISTINCT li.id) as count,
COUNT(DISTINCT li.id) FILTER (WHERE li.status = 'pending') as pending
FROM letters_incoming li
JOIN letter_incoming_recipients lir ON lir.letter_id = li.id
WHERE li.deleted_at IS NULL
GROUP BY lir.recipient_department_id, DATE(li.created_at)
) inc ON inc.dept_id = d.id AND inc.date = dates.summary_date
LEFT JOIN (
SELECT
lor.department_id as dept_id,
DATE(lo.created_at) as date,
COUNT(DISTINCT lo.id) as count,
COUNT(DISTINCT lo.id) FILTER (WHERE lo.status = 'pending_approval') as pending,
COUNT(DISTINCT lo.id) FILTER (WHERE lo.status = 'approved') as approved,
COUNT(DISTINCT lo.id) FILTER (WHERE lo.status = 'rejected') as rejected,
COUNT(DISTINCT lo.id) FILTER (WHERE lo.status IN ('sent', 'archived')) as completed,
AVG(EXTRACT(EPOCH FROM (lo.updated_at - lo.created_at))/3600) as avg_hours
FROM letters_outgoing lo
JOIN letter_outgoing_recipients lor ON lor.letter_id = lo.id
WHERE lo.deleted_at IS NULL
GROUP BY lor.department_id, DATE(lo.created_at)
) outg ON outg.dept_id = d.id AND outg.date = dates.summary_date
WHERE inc.count > 0 OR outg.count > 0
ON CONFLICT (department_id, summary_date) DO NOTHING;
-- Populate institution_letter_summary
INSERT INTO institution_letter_summary (
institution_id,
summary_date,
incoming_sent,
outgoing_received,
total_correspondence,
avg_turnaround_hours,
last_activity_at,
priority_high_count,
priority_medium_count,
priority_low_count
)
SELECT
i.id as institution_id,
dates.summary_date,
COALESCE(inc.count, 0) as incoming_sent,
COALESCE(outg.count, 0) as outgoing_received,
COALESCE(inc.count, 0) + COALESCE(outg.count, 0) as total_correspondence,
COALESCE((inc.avg_hours + outg.avg_hours) / 2, 0) as avg_turnaround_hours,
GREATEST(inc.last_activity, outg.last_activity) as last_activity_at,
COALESCE(inc.high_priority, 0) + COALESCE(outg.high_priority, 0) as priority_high_count,
COALESCE(inc.medium_priority, 0) + COALESCE(outg.medium_priority, 0) as priority_medium_count,
COALESCE(inc.low_priority, 0) + COALESCE(outg.low_priority, 0) as priority_low_count
FROM institutions i
CROSS JOIN (
SELECT DISTINCT DATE(created_at) as summary_date
FROM letters_incoming
UNION
SELECT DISTINCT DATE(created_at)
FROM letters_outgoing
) dates
LEFT JOIN (
SELECT
sender_institution_id as inst_id,
DATE(created_at) as date,
COUNT(*) as count,
AVG(EXTRACT(EPOCH FROM (updated_at - created_at))/3600) as avg_hours,
MAX(created_at) as last_activity,
COUNT(*) FILTER (WHERE priority_id IN (SELECT id FROM priorities WHERE level = 1)) as high_priority,
COUNT(*) FILTER (WHERE priority_id IN (SELECT id FROM priorities WHERE level = 2)) as medium_priority,
COUNT(*) FILTER (WHERE priority_id IN (SELECT id FROM priorities WHERE level = 3)) as low_priority
FROM letters_incoming
WHERE deleted_at IS NULL AND sender_institution_id IS NOT NULL
GROUP BY sender_institution_id, DATE(created_at)
) inc ON inc.inst_id = i.id AND inc.date = dates.summary_date
LEFT JOIN (
SELECT
receiver_institution_id as inst_id,
DATE(created_at) as date,
COUNT(*) as count,
AVG(EXTRACT(EPOCH FROM (updated_at - created_at))/3600) as avg_hours,
MAX(created_at) as last_activity,
COUNT(*) FILTER (WHERE priority_id IN (SELECT id FROM priorities WHERE level = 1)) as high_priority,
COUNT(*) FILTER (WHERE priority_id IN (SELECT id FROM priorities WHERE level = 2)) as medium_priority,
COUNT(*) FILTER (WHERE priority_id IN (SELECT id FROM priorities WHERE level = 3)) as low_priority
FROM letters_outgoing
WHERE deleted_at IS NULL AND receiver_institution_id IS NOT NULL
GROUP BY receiver_institution_id, DATE(created_at)
) outg ON outg.inst_id = i.id AND outg.date = dates.summary_date
WHERE inc.count > 0 OR outg.count > 0
ON CONFLICT (institution_id, summary_date) DO NOTHING;
-- Populate approval_sla_summary
INSERT INTO approval_sla_summary (
summary_date,
department_id,
total_approvals,
approved_count,
rejected_count,
pending_count,
avg_approval_hours,
min_approval_hours,
max_approval_hours,
median_approval_hours,
within_sla_count,
exceeded_sla_count,
sla_compliance_rate,
avg_approval_steps
)
SELECT
DATE(loa.created_at) as summary_date,
d.id as department_id,
COUNT(loa.id) as total_approvals,
COUNT(loa.id) FILTER (WHERE loa.status = 'approved') as approved_count,
COUNT(loa.id) FILTER (WHERE loa.status = 'rejected') as rejected_count,
COUNT(loa.id) FILTER (WHERE loa.status = 'pending') as pending_count,
AVG(EXTRACT(EPOCH FROM (loa.acted_at - loa.created_at))/3600) FILTER (WHERE loa.acted_at IS NOT NULL) as avg_approval_hours,
MIN(EXTRACT(EPOCH FROM (loa.acted_at - loa.created_at))/3600) FILTER (WHERE loa.acted_at IS NOT NULL) as min_approval_hours,
MAX(EXTRACT(EPOCH FROM (loa.acted_at - loa.created_at))/3600) FILTER (WHERE loa.acted_at IS NOT NULL) as max_approval_hours,
PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY EXTRACT(EPOCH FROM (loa.acted_at - loa.created_at))/3600) FILTER (WHERE loa.acted_at IS NOT NULL) as median_approval_hours,
COUNT(loa.id) FILTER (WHERE EXTRACT(EPOCH FROM (loa.acted_at - loa.created_at))/3600 <= 24 AND loa.acted_at IS NOT NULL) as within_sla_count,
COUNT(loa.id) FILTER (WHERE EXTRACT(EPOCH FROM (loa.acted_at - loa.created_at))/3600 > 24 AND loa.acted_at IS NOT NULL) as exceeded_sla_count,
CASE
WHEN COUNT(loa.id) FILTER (WHERE loa.acted_at IS NOT NULL) > 0
THEN (COUNT(loa.id) FILTER (WHERE EXTRACT(EPOCH FROM (loa.acted_at - loa.created_at))/3600 <= 24 AND loa.acted_at IS NOT NULL)::DECIMAL /
COUNT(loa.id) FILTER (WHERE loa.acted_at IS NOT NULL)::DECIMAL) * 100
ELSE 0
END as sla_compliance_rate,
AVG(step_counts.step_count) as avg_approval_steps
FROM letter_outgoing_approvals loa
JOIN letters_outgoing lo ON lo.id = loa.letter_id
JOIN letter_outgoing_recipients lor ON lor.letter_id = lo.id
JOIN departments d ON d.id = lor.department_id
LEFT JOIN (
SELECT letter_id, COUNT(*) as step_count
FROM letter_outgoing_approvals
GROUP BY letter_id
) step_counts ON step_counts.letter_id = loa.letter_id
WHERE lo.deleted_at IS NULL
GROUP BY DATE(loa.created_at), d.id
ON CONFLICT (summary_date, department_id) DO NOTHING;
END;
$$ LANGUAGE plpgsql;
-- Execute the function to populate historical data
SELECT populate_letter_summary_history();