Reports migrations
This commit is contained in:
parent
1e678ea2e5
commit
bfcca47353
382
supabase/migrations/20260303090000_add_report_rpcs.sql
Normal file
382
supabase/migrations/20260303090000_add_report_rpcs.sql
Normal file
|
|
@ -0,0 +1,382 @@
|
||||||
|
-- =============================================================
|
||||||
|
-- Reports Infrastructure: pg_trgm + Server-side RPC functions
|
||||||
|
-- =============================================================
|
||||||
|
-- All RPCs accept a date range (p_start, p_end) and use
|
||||||
|
-- SECURITY INVOKER so existing RLS policies apply.
|
||||||
|
-- =============================================================
|
||||||
|
|
||||||
|
-- Enable pg_trgm for fuzzy subject clustering
|
||||||
|
CREATE EXTENSION IF NOT EXISTS pg_trgm;
|
||||||
|
|
||||||
|
-- GIN trigram indexes for subject similarity searches
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_tickets_subject_trgm
|
||||||
|
ON tickets USING gin (lower(subject) gin_trgm_ops);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_tasks_title_trgm
|
||||||
|
ON tasks USING gin (lower(title) gin_trgm_ops);
|
||||||
|
|
||||||
|
-- ----- 1. Tickets by status -----
|
||||||
|
CREATE OR REPLACE FUNCTION report_tickets_by_status(
|
||||||
|
p_start timestamptz,
|
||||||
|
p_end timestamptz
|
||||||
|
)
|
||||||
|
RETURNS TABLE(status text, count bigint)
|
||||||
|
LANGUAGE sql STABLE SECURITY INVOKER
|
||||||
|
AS $$
|
||||||
|
SELECT t.status, count(*)
|
||||||
|
FROM tickets t
|
||||||
|
WHERE t.created_at >= p_start
|
||||||
|
AND t.created_at < p_end
|
||||||
|
GROUP BY t.status
|
||||||
|
ORDER BY count(*) DESC;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
-- ----- 2. Tasks by status -----
|
||||||
|
CREATE OR REPLACE FUNCTION report_tasks_by_status(
|
||||||
|
p_start timestamptz,
|
||||||
|
p_end timestamptz
|
||||||
|
)
|
||||||
|
RETURNS TABLE(status text, count bigint)
|
||||||
|
LANGUAGE sql STABLE SECURITY INVOKER
|
||||||
|
AS $$
|
||||||
|
SELECT t.status::text, count(*)
|
||||||
|
FROM tasks t
|
||||||
|
WHERE t.created_at >= p_start
|
||||||
|
AND t.created_at < p_end
|
||||||
|
GROUP BY t.status
|
||||||
|
ORDER BY count(*) DESC;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
-- ----- 3. Tasks created by hour of day -----
|
||||||
|
CREATE OR REPLACE FUNCTION report_tasks_by_hour(
|
||||||
|
p_start timestamptz,
|
||||||
|
p_end timestamptz
|
||||||
|
)
|
||||||
|
RETURNS TABLE(hour int, count bigint)
|
||||||
|
LANGUAGE sql STABLE SECURITY INVOKER
|
||||||
|
AS $$
|
||||||
|
SELECT EXTRACT(HOUR FROM t.created_at AT TIME ZONE 'Asia/Manila')::int AS hour,
|
||||||
|
count(*)
|
||||||
|
FROM tasks t
|
||||||
|
WHERE t.created_at >= p_start
|
||||||
|
AND t.created_at < p_end
|
||||||
|
GROUP BY hour
|
||||||
|
ORDER BY hour;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
-- ----- 4. Tickets created by hour of day -----
|
||||||
|
CREATE OR REPLACE FUNCTION report_tickets_by_hour(
|
||||||
|
p_start timestamptz,
|
||||||
|
p_end timestamptz
|
||||||
|
)
|
||||||
|
RETURNS TABLE(hour int, count bigint)
|
||||||
|
LANGUAGE sql STABLE SECURITY INVOKER
|
||||||
|
AS $$
|
||||||
|
SELECT EXTRACT(HOUR FROM t.created_at AT TIME ZONE 'Asia/Manila')::int AS hour,
|
||||||
|
count(*)
|
||||||
|
FROM tickets t
|
||||||
|
WHERE t.created_at >= p_start
|
||||||
|
AND t.created_at < p_end
|
||||||
|
GROUP BY hour
|
||||||
|
ORDER BY hour;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
-- ----- 5. Top offices by ticket count -----
|
||||||
|
CREATE OR REPLACE FUNCTION report_top_offices_tickets(
|
||||||
|
p_start timestamptz,
|
||||||
|
p_end timestamptz,
|
||||||
|
p_limit int DEFAULT 10
|
||||||
|
)
|
||||||
|
RETURNS TABLE(office_name text, count bigint)
|
||||||
|
LANGUAGE sql STABLE SECURITY INVOKER
|
||||||
|
AS $$
|
||||||
|
SELECT o.name AS office_name, count(*)
|
||||||
|
FROM tickets t
|
||||||
|
JOIN offices o ON o.id = t.office_id
|
||||||
|
WHERE t.created_at >= p_start
|
||||||
|
AND t.created_at < p_end
|
||||||
|
GROUP BY o.name
|
||||||
|
ORDER BY count(*) DESC
|
||||||
|
LIMIT p_limit;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
-- ----- 6. Top offices by task count -----
|
||||||
|
CREATE OR REPLACE FUNCTION report_top_offices_tasks(
|
||||||
|
p_start timestamptz,
|
||||||
|
p_end timestamptz,
|
||||||
|
p_limit int DEFAULT 10
|
||||||
|
)
|
||||||
|
RETURNS TABLE(office_name text, count bigint)
|
||||||
|
LANGUAGE sql STABLE SECURITY INVOKER
|
||||||
|
AS $$
|
||||||
|
SELECT o.name AS office_name, count(*)
|
||||||
|
FROM tasks t
|
||||||
|
JOIN offices o ON o.id = t.office_id
|
||||||
|
WHERE t.created_at >= p_start
|
||||||
|
AND t.created_at < p_end
|
||||||
|
GROUP BY o.name
|
||||||
|
ORDER BY count(*) DESC
|
||||||
|
LIMIT p_limit;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
-- ----- 7. Top ticket subjects (pg_trgm clustering) -----
|
||||||
|
CREATE OR REPLACE FUNCTION report_top_ticket_subjects(
|
||||||
|
p_start timestamptz,
|
||||||
|
p_end timestamptz,
|
||||||
|
p_limit int DEFAULT 10,
|
||||||
|
p_threshold float DEFAULT 0.4
|
||||||
|
)
|
||||||
|
RETURNS TABLE(subject_group text, ticket_count bigint)
|
||||||
|
LANGUAGE plpgsql STABLE SECURITY INVOKER
|
||||||
|
AS $$
|
||||||
|
DECLARE
|
||||||
|
rec RECORD;
|
||||||
|
cluster_leaders text[] := '{}';
|
||||||
|
cluster_counts bigint[] := '{}';
|
||||||
|
i int;
|
||||||
|
matched boolean;
|
||||||
|
best_idx int;
|
||||||
|
best_sim float;
|
||||||
|
cur_sim float;
|
||||||
|
BEGIN
|
||||||
|
-- Iterate subjects ordered by frequency (greedy clustering)
|
||||||
|
FOR rec IN
|
||||||
|
SELECT lower(trim(t.subject)) AS norm_subject, count(*) AS cnt
|
||||||
|
FROM tickets t
|
||||||
|
WHERE t.created_at >= p_start
|
||||||
|
AND t.created_at < p_end
|
||||||
|
AND t.subject IS NOT NULL
|
||||||
|
AND trim(t.subject) <> ''
|
||||||
|
GROUP BY norm_subject
|
||||||
|
ORDER BY cnt DESC
|
||||||
|
LOOP
|
||||||
|
matched := false;
|
||||||
|
best_idx := 0;
|
||||||
|
best_sim := 0;
|
||||||
|
|
||||||
|
FOR i IN 1..coalesce(array_length(cluster_leaders, 1), 0) LOOP
|
||||||
|
cur_sim := similarity(rec.norm_subject, cluster_leaders[i]);
|
||||||
|
IF cur_sim >= p_threshold AND cur_sim > best_sim THEN
|
||||||
|
best_sim := cur_sim;
|
||||||
|
best_idx := i;
|
||||||
|
matched := true;
|
||||||
|
END IF;
|
||||||
|
END LOOP;
|
||||||
|
|
||||||
|
IF matched THEN
|
||||||
|
cluster_counts[best_idx] := cluster_counts[best_idx] + rec.cnt;
|
||||||
|
ELSE
|
||||||
|
cluster_leaders := array_append(cluster_leaders, rec.norm_subject);
|
||||||
|
cluster_counts := array_append(cluster_counts, rec.cnt);
|
||||||
|
END IF;
|
||||||
|
END LOOP;
|
||||||
|
|
||||||
|
-- Return top clusters
|
||||||
|
RETURN QUERY
|
||||||
|
SELECT sub.cl AS subject_group, sub.cc AS ticket_count
|
||||||
|
FROM (
|
||||||
|
SELECT unnest(cluster_leaders) AS cl,
|
||||||
|
unnest(cluster_counts) AS cc
|
||||||
|
) sub
|
||||||
|
ORDER BY sub.cc DESC
|
||||||
|
LIMIT p_limit;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
-- ----- 8. Top task subjects (pg_trgm clustering) -----
|
||||||
|
CREATE OR REPLACE FUNCTION report_top_task_subjects(
|
||||||
|
p_start timestamptz,
|
||||||
|
p_end timestamptz,
|
||||||
|
p_limit int DEFAULT 10,
|
||||||
|
p_threshold float DEFAULT 0.4
|
||||||
|
)
|
||||||
|
RETURNS TABLE(subject_group text, task_count bigint)
|
||||||
|
LANGUAGE plpgsql STABLE SECURITY INVOKER
|
||||||
|
AS $$
|
||||||
|
DECLARE
|
||||||
|
rec RECORD;
|
||||||
|
cluster_leaders text[] := '{}';
|
||||||
|
cluster_counts bigint[] := '{}';
|
||||||
|
i int;
|
||||||
|
matched boolean;
|
||||||
|
best_idx int;
|
||||||
|
best_sim float;
|
||||||
|
cur_sim float;
|
||||||
|
BEGIN
|
||||||
|
FOR rec IN
|
||||||
|
SELECT lower(trim(t.title)) AS norm_title, count(*) AS cnt
|
||||||
|
FROM tasks t
|
||||||
|
WHERE t.created_at >= p_start
|
||||||
|
AND t.created_at < p_end
|
||||||
|
AND t.title IS NOT NULL
|
||||||
|
AND trim(t.title) <> ''
|
||||||
|
GROUP BY norm_title
|
||||||
|
ORDER BY cnt DESC
|
||||||
|
LOOP
|
||||||
|
matched := false;
|
||||||
|
best_idx := 0;
|
||||||
|
best_sim := 0;
|
||||||
|
|
||||||
|
FOR i IN 1..coalesce(array_length(cluster_leaders, 1), 0) LOOP
|
||||||
|
cur_sim := similarity(rec.norm_title, cluster_leaders[i]);
|
||||||
|
IF cur_sim >= p_threshold AND cur_sim > best_sim THEN
|
||||||
|
best_sim := cur_sim;
|
||||||
|
best_idx := i;
|
||||||
|
matched := true;
|
||||||
|
END IF;
|
||||||
|
END LOOP;
|
||||||
|
|
||||||
|
IF matched THEN
|
||||||
|
cluster_counts[best_idx] := cluster_counts[best_idx] + rec.cnt;
|
||||||
|
ELSE
|
||||||
|
cluster_leaders := array_append(cluster_leaders, rec.norm_title);
|
||||||
|
cluster_counts := array_append(cluster_counts, rec.cnt);
|
||||||
|
END IF;
|
||||||
|
END LOOP;
|
||||||
|
|
||||||
|
RETURN QUERY
|
||||||
|
SELECT sub.cl AS subject_group, sub.cc AS task_count
|
||||||
|
FROM (
|
||||||
|
SELECT unnest(cluster_leaders) AS cl,
|
||||||
|
unnest(cluster_counts) AS cc
|
||||||
|
) sub
|
||||||
|
ORDER BY sub.cc DESC
|
||||||
|
LIMIT p_limit;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
-- ----- 9. Monthly overview (tickets + tasks) -----
|
||||||
|
CREATE OR REPLACE FUNCTION report_monthly_overview(
|
||||||
|
p_start timestamptz,
|
||||||
|
p_end timestamptz
|
||||||
|
)
|
||||||
|
RETURNS TABLE(month text, ticket_count bigint, task_count bigint)
|
||||||
|
LANGUAGE sql STABLE SECURITY INVOKER
|
||||||
|
AS $$
|
||||||
|
WITH months AS (
|
||||||
|
SELECT to_char(date_trunc('month', d), 'YYYY-MM') AS m
|
||||||
|
FROM generate_series(
|
||||||
|
date_trunc('month', p_start),
|
||||||
|
date_trunc('month', p_end - interval '1 day'),
|
||||||
|
interval '1 month'
|
||||||
|
) d
|
||||||
|
),
|
||||||
|
tk AS (
|
||||||
|
SELECT to_char(date_trunc('month', created_at), 'YYYY-MM') AS m,
|
||||||
|
count(*) AS cnt
|
||||||
|
FROM tickets
|
||||||
|
WHERE created_at >= p_start AND created_at < p_end
|
||||||
|
GROUP BY m
|
||||||
|
),
|
||||||
|
ta AS (
|
||||||
|
SELECT to_char(date_trunc('month', created_at), 'YYYY-MM') AS m,
|
||||||
|
count(*) AS cnt
|
||||||
|
FROM tasks
|
||||||
|
WHERE created_at >= p_start AND created_at < p_end
|
||||||
|
GROUP BY m
|
||||||
|
)
|
||||||
|
SELECT months.m AS month,
|
||||||
|
coalesce(tk.cnt, 0) AS ticket_count,
|
||||||
|
coalesce(ta.cnt, 0) AS task_count
|
||||||
|
FROM months
|
||||||
|
LEFT JOIN tk ON tk.m = months.m
|
||||||
|
LEFT JOIN ta ON ta.m = months.m
|
||||||
|
ORDER BY months.m;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
-- ----- 10. Request type distribution -----
|
||||||
|
CREATE OR REPLACE FUNCTION report_request_type_distribution(
|
||||||
|
p_start timestamptz,
|
||||||
|
p_end timestamptz
|
||||||
|
)
|
||||||
|
RETURNS TABLE(request_type text, count bigint)
|
||||||
|
LANGUAGE sql STABLE SECURITY INVOKER
|
||||||
|
AS $$
|
||||||
|
SELECT coalesce(t.request_type::text, 'Unspecified') AS request_type,
|
||||||
|
count(*)
|
||||||
|
FROM tasks t
|
||||||
|
WHERE t.created_at >= p_start
|
||||||
|
AND t.created_at < p_end
|
||||||
|
GROUP BY request_type
|
||||||
|
ORDER BY count(*) DESC;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
-- ----- 11. Request category distribution -----
|
||||||
|
CREATE OR REPLACE FUNCTION report_request_category_distribution(
|
||||||
|
p_start timestamptz,
|
||||||
|
p_end timestamptz
|
||||||
|
)
|
||||||
|
RETURNS TABLE(request_category text, count bigint)
|
||||||
|
LANGUAGE sql STABLE SECURITY INVOKER
|
||||||
|
AS $$
|
||||||
|
SELECT coalesce(t.request_category::text, 'Unspecified') AS request_category,
|
||||||
|
count(*)
|
||||||
|
FROM tasks t
|
||||||
|
WHERE t.created_at >= p_start
|
||||||
|
AND t.created_at < p_end
|
||||||
|
GROUP BY request_category
|
||||||
|
ORDER BY count(*) DESC;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
-- ----- 12. Avg resolution time by office -----
|
||||||
|
CREATE OR REPLACE FUNCTION report_avg_resolution_by_office(
|
||||||
|
p_start timestamptz,
|
||||||
|
p_end timestamptz
|
||||||
|
)
|
||||||
|
RETURNS TABLE(office_name text, avg_hours float)
|
||||||
|
LANGUAGE sql STABLE SECURITY INVOKER
|
||||||
|
AS $$
|
||||||
|
SELECT o.name AS office_name,
|
||||||
|
avg(EXTRACT(EPOCH FROM (t.completed_at - t.created_at)) / 3600.0)::float AS avg_hours
|
||||||
|
FROM tasks t
|
||||||
|
JOIN offices o ON o.id = t.office_id
|
||||||
|
WHERE t.created_at >= p_start
|
||||||
|
AND t.created_at < p_end
|
||||||
|
AND t.completed_at IS NOT NULL
|
||||||
|
AND t.status::text = 'completed'
|
||||||
|
GROUP BY o.name
|
||||||
|
ORDER BY avg_hours DESC;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
-- ----- 13. Staff workload (assigned vs completed) -----
|
||||||
|
CREATE OR REPLACE FUNCTION report_staff_workload(
|
||||||
|
p_start timestamptz,
|
||||||
|
p_end timestamptz
|
||||||
|
)
|
||||||
|
RETURNS TABLE(staff_name text, assigned_count bigint, completed_count bigint)
|
||||||
|
LANGUAGE sql STABLE SECURITY INVOKER
|
||||||
|
AS $$
|
||||||
|
SELECT p.full_name AS staff_name,
|
||||||
|
count(DISTINCT ta.task_id) AS assigned_count,
|
||||||
|
count(DISTINCT CASE WHEN t.status::text = 'completed'
|
||||||
|
AND t.completed_at >= p_start
|
||||||
|
AND t.completed_at < p_end
|
||||||
|
THEN t.id END) AS completed_count
|
||||||
|
FROM task_assignments ta
|
||||||
|
JOIN profiles p ON p.id = ta.user_id
|
||||||
|
JOIN tasks t ON t.id = ta.task_id
|
||||||
|
WHERE ta.created_at >= p_start
|
||||||
|
AND ta.created_at < p_end
|
||||||
|
GROUP BY p.full_name
|
||||||
|
ORDER BY assigned_count DESC;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
-- ----- 14. Ticket-to-task conversion rate -----
|
||||||
|
CREATE OR REPLACE FUNCTION report_ticket_to_task_rate(
|
||||||
|
p_start timestamptz,
|
||||||
|
p_end timestamptz
|
||||||
|
)
|
||||||
|
RETURNS TABLE(total_tickets bigint, promoted_tickets bigint, conversion_rate float)
|
||||||
|
LANGUAGE sql STABLE SECURITY INVOKER
|
||||||
|
AS $$
|
||||||
|
SELECT
|
||||||
|
count(*)::bigint AS total_tickets,
|
||||||
|
count(*) FILTER (WHERE promoted_at IS NOT NULL)::bigint AS promoted_tickets,
|
||||||
|
CASE WHEN count(*) > 0
|
||||||
|
THEN (count(*) FILTER (WHERE promoted_at IS NOT NULL)::float / count(*)::float * 100)
|
||||||
|
ELSE 0
|
||||||
|
END AS conversion_rate
|
||||||
|
FROM tickets
|
||||||
|
WHERE created_at >= p_start
|
||||||
|
AND created_at < p_end;
|
||||||
|
$$;
|
||||||
Loading…
Reference in New Issue
Block a user