-- 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();