389 lines
16 KiB
PL/PgSQL
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(); |