Add date-range filtering to analytics dashboard API.
Support all-time, year, year+month, and custom from/to query params with filtered metrics and time-series charts. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
8bba318372
commit
024a69b74b
|
|
@ -1,6 +1,12 @@
|
||||||
-- =====================
|
-- =====================
|
||||||
-- Analytics
|
-- Analytics (date-filtered)
|
||||||
-- =====================
|
-- =====================
|
||||||
|
-- Shared optional params (nullable = all-time):
|
||||||
|
-- range_start, range_end (exclusive upper bound)
|
||||||
|
-- Required chart params:
|
||||||
|
-- series_start, series_end (inclusive dates)
|
||||||
|
-- Relative window anchor:
|
||||||
|
-- ref_date (inclusive date used for new_today/week/month)
|
||||||
|
|
||||||
-- =====================
|
-- =====================
|
||||||
-- User Analytics
|
-- User Analytics
|
||||||
|
|
@ -9,49 +15,67 @@
|
||||||
-- name: AnalyticsUsersSummary :one
|
-- name: AnalyticsUsersSummary :one
|
||||||
SELECT
|
SELECT
|
||||||
COUNT(*)::bigint AS total,
|
COUNT(*)::bigint AS total,
|
||||||
COUNT(*) FILTER (WHERE created_at >= CURRENT_DATE)::bigint AS new_today,
|
COUNT(*) FILTER (WHERE u.created_at::date = sqlc.arg('ref_date')::date)::bigint AS new_today,
|
||||||
COUNT(*) FILTER (WHERE created_at >= CURRENT_DATE - INTERVAL '7 days')::bigint AS new_this_week,
|
COUNT(*) FILTER (
|
||||||
COUNT(*) FILTER (WHERE created_at >= CURRENT_DATE - INTERVAL '30 days')::bigint AS new_this_month
|
WHERE u.created_at::date >= sqlc.arg('ref_date')::date - INTERVAL '6 days'
|
||||||
FROM users;
|
AND u.created_at::date <= sqlc.arg('ref_date')::date
|
||||||
|
)::bigint AS new_this_week,
|
||||||
|
COUNT(*) FILTER (
|
||||||
|
WHERE u.created_at::date >= sqlc.arg('ref_date')::date - INTERVAL '29 days'
|
||||||
|
AND u.created_at::date <= sqlc.arg('ref_date')::date
|
||||||
|
)::bigint AS new_this_month
|
||||||
|
FROM users u
|
||||||
|
WHERE (sqlc.narg('range_start')::timestamptz IS NULL OR u.created_at >= sqlc.narg('range_start')::timestamptz)
|
||||||
|
AND (sqlc.narg('range_end')::timestamptz IS NULL OR u.created_at < sqlc.narg('range_end')::timestamptz);
|
||||||
|
|
||||||
-- name: AnalyticsUsersByRole :many
|
-- name: AnalyticsUsersByRole :many
|
||||||
SELECT
|
SELECT
|
||||||
COALESCE(role, 'unknown') AS role,
|
COALESCE(u.role, 'unknown') AS role,
|
||||||
COUNT(*)::bigint AS count
|
COUNT(*)::bigint AS count
|
||||||
FROM users
|
FROM users u
|
||||||
GROUP BY role
|
WHERE (sqlc.narg('range_start')::timestamptz IS NULL OR u.created_at >= sqlc.narg('range_start')::timestamptz)
|
||||||
|
AND (sqlc.narg('range_end')::timestamptz IS NULL OR u.created_at < sqlc.narg('range_end')::timestamptz)
|
||||||
|
GROUP BY u.role
|
||||||
ORDER BY count DESC;
|
ORDER BY count DESC;
|
||||||
|
|
||||||
-- name: AnalyticsUsersByStatus :many
|
-- name: AnalyticsUsersByStatus :many
|
||||||
SELECT
|
SELECT
|
||||||
COALESCE(status, 'unknown') AS status,
|
COALESCE(u.status, 'unknown') AS status,
|
||||||
COUNT(*)::bigint AS count
|
COUNT(*)::bigint AS count
|
||||||
FROM users
|
FROM users u
|
||||||
GROUP BY status
|
WHERE (sqlc.narg('range_start')::timestamptz IS NULL OR u.created_at >= sqlc.narg('range_start')::timestamptz)
|
||||||
|
AND (sqlc.narg('range_end')::timestamptz IS NULL OR u.created_at < sqlc.narg('range_end')::timestamptz)
|
||||||
|
GROUP BY u.status
|
||||||
ORDER BY count DESC;
|
ORDER BY count DESC;
|
||||||
|
|
||||||
-- name: AnalyticsUsersByAgeGroup :many
|
-- name: AnalyticsUsersByAgeGroup :many
|
||||||
SELECT
|
SELECT
|
||||||
COALESCE(age_group, 'unknown') AS age_group,
|
COALESCE(u.age_group, 'unknown') AS age_group,
|
||||||
COUNT(*)::bigint AS count
|
COUNT(*)::bigint AS count
|
||||||
FROM users
|
FROM users u
|
||||||
GROUP BY age_group
|
WHERE (sqlc.narg('range_start')::timestamptz IS NULL OR u.created_at >= sqlc.narg('range_start')::timestamptz)
|
||||||
|
AND (sqlc.narg('range_end')::timestamptz IS NULL OR u.created_at < sqlc.narg('range_end')::timestamptz)
|
||||||
|
GROUP BY u.age_group
|
||||||
ORDER BY count DESC;
|
ORDER BY count DESC;
|
||||||
|
|
||||||
-- name: AnalyticsUsersByKnowledgeLevel :many
|
-- name: AnalyticsUsersByKnowledgeLevel :many
|
||||||
SELECT
|
SELECT
|
||||||
COALESCE(knowledge_level, 'unknown') AS knowledge_level,
|
COALESCE(u.knowledge_level, 'unknown') AS knowledge_level,
|
||||||
COUNT(*)::bigint AS count
|
COUNT(*)::bigint AS count
|
||||||
FROM users
|
FROM users u
|
||||||
GROUP BY knowledge_level
|
WHERE (sqlc.narg('range_start')::timestamptz IS NULL OR u.created_at >= sqlc.narg('range_start')::timestamptz)
|
||||||
|
AND (sqlc.narg('range_end')::timestamptz IS NULL OR u.created_at < sqlc.narg('range_end')::timestamptz)
|
||||||
|
GROUP BY u.knowledge_level
|
||||||
ORDER BY count DESC;
|
ORDER BY count DESC;
|
||||||
|
|
||||||
-- name: AnalyticsUsersByRegion :many
|
-- name: AnalyticsUsersByRegion :many
|
||||||
SELECT
|
SELECT
|
||||||
COALESCE(region, 'unknown') AS region,
|
COALESCE(u.region, 'unknown') AS region,
|
||||||
COUNT(*)::bigint AS count
|
COUNT(*)::bigint AS count
|
||||||
FROM users
|
FROM users u
|
||||||
GROUP BY region
|
WHERE (sqlc.narg('range_start')::timestamptz IS NULL OR u.created_at >= sqlc.narg('range_start')::timestamptz)
|
||||||
|
AND (sqlc.narg('range_end')::timestamptz IS NULL OR u.created_at < sqlc.narg('range_end')::timestamptz)
|
||||||
|
GROUP BY u.region
|
||||||
ORDER BY count DESC;
|
ORDER BY count DESC;
|
||||||
|
|
||||||
-- name: AnalyticsUserRegistrationsLast30Days :many
|
-- name: AnalyticsUserRegistrationsLast30Days :many
|
||||||
|
|
@ -59,11 +83,13 @@ SELECT
|
||||||
d.date,
|
d.date,
|
||||||
COUNT(u.id)::bigint AS count
|
COUNT(u.id)::bigint AS count
|
||||||
FROM generate_series(
|
FROM generate_series(
|
||||||
CURRENT_DATE - INTERVAL '29 days',
|
sqlc.arg('series_start')::date,
|
||||||
CURRENT_DATE,
|
sqlc.arg('series_end')::date,
|
||||||
INTERVAL '1 day'
|
INTERVAL '1 day'
|
||||||
) AS d(date)
|
) AS d(date)
|
||||||
LEFT JOIN users u ON u.created_at::date = d.date
|
LEFT JOIN users u ON u.created_at::date = d.date
|
||||||
|
AND (sqlc.narg('range_start')::timestamptz IS NULL OR u.created_at >= sqlc.narg('range_start')::timestamptz)
|
||||||
|
AND (sqlc.narg('range_end')::timestamptz IS NULL OR u.created_at < sqlc.narg('range_end')::timestamptz)
|
||||||
GROUP BY d.date
|
GROUP BY d.date
|
||||||
ORDER BY d.date;
|
ORDER BY d.date;
|
||||||
|
|
||||||
|
|
@ -74,18 +100,28 @@ ORDER BY d.date;
|
||||||
-- name: AnalyticsSubscriptionsSummary :one
|
-- name: AnalyticsSubscriptionsSummary :one
|
||||||
SELECT
|
SELECT
|
||||||
COUNT(*)::bigint AS total,
|
COUNT(*)::bigint AS total,
|
||||||
COUNT(*) FILTER (WHERE status = 'ACTIVE')::bigint AS active,
|
COUNT(*) FILTER (WHERE us.status = 'ACTIVE')::bigint AS active,
|
||||||
COUNT(*) FILTER (WHERE created_at >= CURRENT_DATE)::bigint AS new_today,
|
COUNT(*) FILTER (WHERE us.created_at::date = sqlc.arg('ref_date')::date)::bigint AS new_today,
|
||||||
COUNT(*) FILTER (WHERE created_at >= CURRENT_DATE - INTERVAL '7 days')::bigint AS new_this_week,
|
COUNT(*) FILTER (
|
||||||
COUNT(*) FILTER (WHERE created_at >= CURRENT_DATE - INTERVAL '30 days')::bigint AS new_this_month
|
WHERE us.created_at::date >= sqlc.arg('ref_date')::date - INTERVAL '6 days'
|
||||||
FROM user_subscriptions;
|
AND us.created_at::date <= sqlc.arg('ref_date')::date
|
||||||
|
)::bigint AS new_this_week,
|
||||||
|
COUNT(*) FILTER (
|
||||||
|
WHERE us.created_at::date >= sqlc.arg('ref_date')::date - INTERVAL '29 days'
|
||||||
|
AND us.created_at::date <= sqlc.arg('ref_date')::date
|
||||||
|
)::bigint AS new_this_month
|
||||||
|
FROM user_subscriptions us
|
||||||
|
WHERE (sqlc.narg('range_start')::timestamptz IS NULL OR us.created_at >= sqlc.narg('range_start')::timestamptz)
|
||||||
|
AND (sqlc.narg('range_end')::timestamptz IS NULL OR us.created_at < sqlc.narg('range_end')::timestamptz);
|
||||||
|
|
||||||
-- name: AnalyticsSubscriptionsByStatus :many
|
-- name: AnalyticsSubscriptionsByStatus :many
|
||||||
SELECT
|
SELECT
|
||||||
COALESCE(status, 'unknown') AS status,
|
COALESCE(us.status, 'unknown') AS status,
|
||||||
COUNT(*)::bigint AS count
|
COUNT(*)::bigint AS count
|
||||||
FROM user_subscriptions
|
FROM user_subscriptions us
|
||||||
GROUP BY status
|
WHERE (sqlc.narg('range_start')::timestamptz IS NULL OR us.created_at >= sqlc.narg('range_start')::timestamptz)
|
||||||
|
AND (sqlc.narg('range_end')::timestamptz IS NULL OR us.created_at < sqlc.narg('range_end')::timestamptz)
|
||||||
|
GROUP BY us.status
|
||||||
ORDER BY count DESC;
|
ORDER BY count DESC;
|
||||||
|
|
||||||
-- name: AnalyticsRevenueByPlan :many
|
-- name: AnalyticsRevenueByPlan :many
|
||||||
|
|
@ -97,6 +133,8 @@ SELECT
|
||||||
FROM payments p
|
FROM payments p
|
||||||
JOIN subscription_plans sp ON sp.id = p.plan_id
|
JOIN subscription_plans sp ON sp.id = p.plan_id
|
||||||
WHERE p.status = 'SUCCESS'
|
WHERE p.status = 'SUCCESS'
|
||||||
|
AND (sqlc.narg('range_start')::timestamptz IS NULL OR COALESCE(p.paid_at, p.created_at) >= sqlc.narg('range_start')::timestamptz)
|
||||||
|
AND (sqlc.narg('range_end')::timestamptz IS NULL OR COALESCE(p.paid_at, p.created_at) < sqlc.narg('range_end')::timestamptz)
|
||||||
GROUP BY sp.name, sp.currency
|
GROUP BY sp.name, sp.currency
|
||||||
ORDER BY total_revenue DESC;
|
ORDER BY total_revenue DESC;
|
||||||
|
|
||||||
|
|
@ -105,11 +143,13 @@ SELECT
|
||||||
d.date,
|
d.date,
|
||||||
COUNT(us.id)::bigint AS count
|
COUNT(us.id)::bigint AS count
|
||||||
FROM generate_series(
|
FROM generate_series(
|
||||||
CURRENT_DATE - INTERVAL '29 days',
|
sqlc.arg('series_start')::date,
|
||||||
CURRENT_DATE,
|
sqlc.arg('series_end')::date,
|
||||||
INTERVAL '1 day'
|
INTERVAL '1 day'
|
||||||
) AS d(date)
|
) AS d(date)
|
||||||
LEFT JOIN user_subscriptions us ON us.created_at::date = d.date
|
LEFT JOIN user_subscriptions us ON us.created_at::date = d.date
|
||||||
|
AND (sqlc.narg('range_start')::timestamptz IS NULL OR us.created_at >= sqlc.narg('range_start')::timestamptz)
|
||||||
|
AND (sqlc.narg('range_end')::timestamptz IS NULL OR us.created_at < sqlc.narg('range_end')::timestamptz)
|
||||||
GROUP BY d.date
|
GROUP BY d.date
|
||||||
ORDER BY d.date;
|
ORDER BY d.date;
|
||||||
|
|
||||||
|
|
@ -119,29 +159,35 @@ ORDER BY d.date;
|
||||||
|
|
||||||
-- name: AnalyticsPaymentsSummary :one
|
-- name: AnalyticsPaymentsSummary :one
|
||||||
SELECT
|
SELECT
|
||||||
COALESCE(SUM(amount) FILTER (WHERE status = 'SUCCESS'), 0)::float8 AS total_revenue,
|
COALESCE(SUM(p.amount) FILTER (WHERE p.status = 'SUCCESS'), 0)::float8 AS total_revenue,
|
||||||
COALESCE(AVG(amount) FILTER (WHERE status = 'SUCCESS'), 0)::float8 AS avg_value,
|
COALESCE(AVG(p.amount) FILTER (WHERE p.status = 'SUCCESS'), 0)::float8 AS avg_value,
|
||||||
COUNT(*)::bigint AS total_payments,
|
COUNT(*)::bigint AS total_payments,
|
||||||
COUNT(*) FILTER (WHERE status = 'SUCCESS')::bigint AS successful_payments
|
COUNT(*) FILTER (WHERE p.status = 'SUCCESS')::bigint AS successful_payments
|
||||||
FROM payments;
|
FROM payments p
|
||||||
|
WHERE (sqlc.narg('range_start')::timestamptz IS NULL OR COALESCE(p.paid_at, p.created_at) >= sqlc.narg('range_start')::timestamptz)
|
||||||
|
AND (sqlc.narg('range_end')::timestamptz IS NULL OR COALESCE(p.paid_at, p.created_at) < sqlc.narg('range_end')::timestamptz);
|
||||||
|
|
||||||
-- name: AnalyticsPaymentsByStatus :many
|
-- name: AnalyticsPaymentsByStatus :many
|
||||||
SELECT
|
SELECT
|
||||||
COALESCE(status, 'unknown') AS status,
|
COALESCE(p.status, 'unknown') AS status,
|
||||||
COUNT(*)::bigint AS count,
|
COUNT(*)::bigint AS count,
|
||||||
COALESCE(SUM(amount), 0)::float8 AS total_amount
|
COALESCE(SUM(p.amount), 0)::float8 AS total_amount
|
||||||
FROM payments
|
FROM payments p
|
||||||
GROUP BY status
|
WHERE (sqlc.narg('range_start')::timestamptz IS NULL OR COALESCE(p.paid_at, p.created_at) >= sqlc.narg('range_start')::timestamptz)
|
||||||
|
AND (sqlc.narg('range_end')::timestamptz IS NULL OR COALESCE(p.paid_at, p.created_at) < sqlc.narg('range_end')::timestamptz)
|
||||||
|
GROUP BY p.status
|
||||||
ORDER BY count DESC;
|
ORDER BY count DESC;
|
||||||
|
|
||||||
-- name: AnalyticsPaymentsByMethod :many
|
-- name: AnalyticsPaymentsByMethod :many
|
||||||
SELECT
|
SELECT
|
||||||
COALESCE(payment_method, 'unknown') AS payment_method,
|
COALESCE(p.payment_method, 'unknown') AS payment_method,
|
||||||
COUNT(*)::bigint AS count,
|
COUNT(*)::bigint AS count,
|
||||||
COALESCE(SUM(amount), 0)::float8 AS total_amount
|
COALESCE(SUM(p.amount), 0)::float8 AS total_amount
|
||||||
FROM payments
|
FROM payments p
|
||||||
WHERE status = 'SUCCESS'
|
WHERE p.status = 'SUCCESS'
|
||||||
GROUP BY payment_method
|
AND (sqlc.narg('range_start')::timestamptz IS NULL OR COALESCE(p.paid_at, p.created_at) >= sqlc.narg('range_start')::timestamptz)
|
||||||
|
AND (sqlc.narg('range_end')::timestamptz IS NULL OR COALESCE(p.paid_at, p.created_at) < sqlc.narg('range_end')::timestamptz)
|
||||||
|
GROUP BY p.payment_method
|
||||||
ORDER BY count DESC;
|
ORDER BY count DESC;
|
||||||
|
|
||||||
-- name: AnalyticsRevenueLast30Days :many
|
-- name: AnalyticsRevenueLast30Days :many
|
||||||
|
|
@ -149,11 +195,14 @@ SELECT
|
||||||
d.date,
|
d.date,
|
||||||
COALESCE(SUM(p.amount), 0)::float8 AS total_revenue
|
COALESCE(SUM(p.amount), 0)::float8 AS total_revenue
|
||||||
FROM generate_series(
|
FROM generate_series(
|
||||||
CURRENT_DATE - INTERVAL '29 days',
|
sqlc.arg('series_start')::date,
|
||||||
CURRENT_DATE,
|
sqlc.arg('series_end')::date,
|
||||||
INTERVAL '1 day'
|
INTERVAL '1 day'
|
||||||
) AS d(date)
|
) AS d(date)
|
||||||
LEFT JOIN payments p ON p.paid_at::date = d.date AND p.status = 'SUCCESS'
|
LEFT JOIN payments p ON COALESCE(p.paid_at, p.created_at)::date = d.date
|
||||||
|
AND p.status = 'SUCCESS'
|
||||||
|
AND (sqlc.narg('range_start')::timestamptz IS NULL OR COALESCE(p.paid_at, p.created_at) >= sqlc.narg('range_start')::timestamptz)
|
||||||
|
AND (sqlc.narg('range_end')::timestamptz IS NULL OR COALESCE(p.paid_at, p.created_at) < sqlc.narg('range_end')::timestamptz)
|
||||||
GROUP BY d.date
|
GROUP BY d.date
|
||||||
ORDER BY d.date;
|
ORDER BY d.date;
|
||||||
|
|
||||||
|
|
@ -174,23 +223,37 @@ SELECT
|
||||||
|
|
||||||
-- name: AnalyticsQuestionsCounts :one
|
-- name: AnalyticsQuestionsCounts :one
|
||||||
SELECT
|
SELECT
|
||||||
(SELECT COUNT(*)::bigint FROM questions) AS total_questions,
|
(
|
||||||
(SELECT COUNT(*)::bigint FROM question_sets) AS total_question_sets;
|
SELECT COUNT(*)::bigint
|
||||||
|
FROM questions q
|
||||||
|
WHERE (sqlc.narg('range_start')::timestamptz IS NULL OR q.created_at >= sqlc.narg('range_start')::timestamptz)
|
||||||
|
AND (sqlc.narg('range_end')::timestamptz IS NULL OR q.created_at < sqlc.narg('range_end')::timestamptz)
|
||||||
|
) AS total_questions,
|
||||||
|
(
|
||||||
|
SELECT COUNT(*)::bigint
|
||||||
|
FROM question_sets qs
|
||||||
|
WHERE (sqlc.narg('range_start')::timestamptz IS NULL OR qs.created_at >= sqlc.narg('range_start')::timestamptz)
|
||||||
|
AND (sqlc.narg('range_end')::timestamptz IS NULL OR qs.created_at < sqlc.narg('range_end')::timestamptz)
|
||||||
|
) AS total_question_sets;
|
||||||
|
|
||||||
-- name: AnalyticsQuestionsByType :many
|
-- name: AnalyticsQuestionsByType :many
|
||||||
SELECT
|
SELECT
|
||||||
COALESCE(question_type, 'unknown') AS question_type,
|
COALESCE(q.question_type, 'unknown') AS question_type,
|
||||||
COUNT(*)::bigint AS count
|
COUNT(*)::bigint AS count
|
||||||
FROM questions
|
FROM questions q
|
||||||
GROUP BY question_type
|
WHERE (sqlc.narg('range_start')::timestamptz IS NULL OR q.created_at >= sqlc.narg('range_start')::timestamptz)
|
||||||
|
AND (sqlc.narg('range_end')::timestamptz IS NULL OR q.created_at < sqlc.narg('range_end')::timestamptz)
|
||||||
|
GROUP BY q.question_type
|
||||||
ORDER BY count DESC;
|
ORDER BY count DESC;
|
||||||
|
|
||||||
-- name: AnalyticsQuestionSetsByType :many
|
-- name: AnalyticsQuestionSetsByType :many
|
||||||
SELECT
|
SELECT
|
||||||
COALESCE(set_type, 'unknown') AS set_type,
|
COALESCE(qs.set_type, 'unknown') AS set_type,
|
||||||
COUNT(*)::bigint AS count
|
COUNT(*)::bigint AS count
|
||||||
FROM question_sets
|
FROM question_sets qs
|
||||||
GROUP BY set_type
|
WHERE (sqlc.narg('range_start')::timestamptz IS NULL OR qs.created_at >= sqlc.narg('range_start')::timestamptz)
|
||||||
|
AND (sqlc.narg('range_end')::timestamptz IS NULL OR qs.created_at < sqlc.narg('range_end')::timestamptz)
|
||||||
|
GROUP BY qs.set_type
|
||||||
ORDER BY count DESC;
|
ORDER BY count DESC;
|
||||||
|
|
||||||
-- =====================
|
-- =====================
|
||||||
|
|
@ -200,24 +263,30 @@ ORDER BY count DESC;
|
||||||
-- name: AnalyticsNotificationsSummary :one
|
-- name: AnalyticsNotificationsSummary :one
|
||||||
SELECT
|
SELECT
|
||||||
COUNT(*)::bigint AS total,
|
COUNT(*)::bigint AS total,
|
||||||
COUNT(*) FILTER (WHERE is_read = TRUE)::bigint AS read,
|
COUNT(*) FILTER (WHERE n.is_read = TRUE)::bigint AS read,
|
||||||
COUNT(*) FILTER (WHERE is_read = FALSE)::bigint AS unread
|
COUNT(*) FILTER (WHERE n.is_read = FALSE)::bigint AS unread
|
||||||
FROM notifications;
|
FROM notifications n
|
||||||
|
WHERE (sqlc.narg('range_start')::timestamptz IS NULL OR n.created_at >= sqlc.narg('range_start')::timestamptz)
|
||||||
|
AND (sqlc.narg('range_end')::timestamptz IS NULL OR n.created_at < sqlc.narg('range_end')::timestamptz);
|
||||||
|
|
||||||
-- name: AnalyticsNotificationsByChannel :many
|
-- name: AnalyticsNotificationsByChannel :many
|
||||||
SELECT
|
SELECT
|
||||||
COALESCE(channel, 'unknown') AS channel,
|
COALESCE(n.channel, 'unknown') AS channel,
|
||||||
COUNT(*)::bigint AS count
|
COUNT(*)::bigint AS count
|
||||||
FROM notifications
|
FROM notifications n
|
||||||
GROUP BY channel
|
WHERE (sqlc.narg('range_start')::timestamptz IS NULL OR n.created_at >= sqlc.narg('range_start')::timestamptz)
|
||||||
|
AND (sqlc.narg('range_end')::timestamptz IS NULL OR n.created_at < sqlc.narg('range_end')::timestamptz)
|
||||||
|
GROUP BY n.channel
|
||||||
ORDER BY count DESC;
|
ORDER BY count DESC;
|
||||||
|
|
||||||
-- name: AnalyticsNotificationsByType :many
|
-- name: AnalyticsNotificationsByType :many
|
||||||
SELECT
|
SELECT
|
||||||
COALESCE(type, 'unknown') AS type,
|
COALESCE(n.type, 'unknown') AS type,
|
||||||
COUNT(*)::bigint AS count
|
COUNT(*)::bigint AS count
|
||||||
FROM notifications
|
FROM notifications n
|
||||||
GROUP BY type
|
WHERE (sqlc.narg('range_start')::timestamptz IS NULL OR n.created_at >= sqlc.narg('range_start')::timestamptz)
|
||||||
|
AND (sqlc.narg('range_end')::timestamptz IS NULL OR n.created_at < sqlc.narg('range_end')::timestamptz)
|
||||||
|
GROUP BY n.type
|
||||||
ORDER BY count DESC;
|
ORDER BY count DESC;
|
||||||
|
|
||||||
-- =====================
|
-- =====================
|
||||||
|
|
@ -227,27 +296,33 @@ ORDER BY count DESC;
|
||||||
-- name: AnalyticsIssuesSummary :one
|
-- name: AnalyticsIssuesSummary :one
|
||||||
SELECT
|
SELECT
|
||||||
COUNT(*)::bigint AS total,
|
COUNT(*)::bigint AS total,
|
||||||
COUNT(*) FILTER (WHERE status = 'resolved')::bigint AS resolved,
|
COUNT(*) FILTER (WHERE ri.status = 'resolved')::bigint AS resolved,
|
||||||
CASE
|
CASE
|
||||||
WHEN COUNT(*) > 0 THEN (COUNT(*) FILTER (WHERE status = 'resolved')::float8 / COUNT(*)::float8)
|
WHEN COUNT(*) > 0 THEN (COUNT(*) FILTER (WHERE ri.status = 'resolved')::float8 / COUNT(*)::float8)
|
||||||
ELSE 0::float8
|
ELSE 0::float8
|
||||||
END AS resolution_rate
|
END AS resolution_rate
|
||||||
FROM reported_issues;
|
FROM reported_issues ri
|
||||||
|
WHERE (sqlc.narg('range_start')::timestamptz IS NULL OR ri.created_at >= sqlc.narg('range_start')::timestamptz)
|
||||||
|
AND (sqlc.narg('range_end')::timestamptz IS NULL OR ri.created_at < sqlc.narg('range_end')::timestamptz);
|
||||||
|
|
||||||
-- name: AnalyticsIssuesByStatus :many
|
-- name: AnalyticsIssuesByStatus :many
|
||||||
SELECT
|
SELECT
|
||||||
COALESCE(status, 'unknown') AS status,
|
COALESCE(ri.status, 'unknown') AS status,
|
||||||
COUNT(*)::bigint AS count
|
COUNT(*)::bigint AS count
|
||||||
FROM reported_issues
|
FROM reported_issues ri
|
||||||
GROUP BY status
|
WHERE (sqlc.narg('range_start')::timestamptz IS NULL OR ri.created_at >= sqlc.narg('range_start')::timestamptz)
|
||||||
|
AND (sqlc.narg('range_end')::timestamptz IS NULL OR ri.created_at < sqlc.narg('range_end')::timestamptz)
|
||||||
|
GROUP BY ri.status
|
||||||
ORDER BY count DESC;
|
ORDER BY count DESC;
|
||||||
|
|
||||||
-- name: AnalyticsIssuesByType :many
|
-- name: AnalyticsIssuesByType :many
|
||||||
SELECT
|
SELECT
|
||||||
COALESCE(issue_type, 'unknown') AS issue_type,
|
COALESCE(ri.issue_type, 'unknown') AS issue_type,
|
||||||
COUNT(*)::bigint AS count
|
COUNT(*)::bigint AS count
|
||||||
FROM reported_issues
|
FROM reported_issues ri
|
||||||
GROUP BY issue_type
|
WHERE (sqlc.narg('range_start')::timestamptz IS NULL OR ri.created_at >= sqlc.narg('range_start')::timestamptz)
|
||||||
|
AND (sqlc.narg('range_end')::timestamptz IS NULL OR ri.created_at < sqlc.narg('range_end')::timestamptz)
|
||||||
|
GROUP BY ri.issue_type
|
||||||
ORDER BY count DESC;
|
ORDER BY count DESC;
|
||||||
|
|
||||||
-- =====================
|
-- =====================
|
||||||
|
|
@ -257,20 +332,26 @@ ORDER BY count DESC;
|
||||||
-- name: AnalyticsTeamSummary :one
|
-- name: AnalyticsTeamSummary :one
|
||||||
SELECT
|
SELECT
|
||||||
COUNT(*)::bigint AS total_members
|
COUNT(*)::bigint AS total_members
|
||||||
FROM team_members;
|
FROM team_members tm
|
||||||
|
WHERE (sqlc.narg('range_start')::timestamptz IS NULL OR tm.created_at >= sqlc.narg('range_start')::timestamptz)
|
||||||
|
AND (sqlc.narg('range_end')::timestamptz IS NULL OR tm.created_at < sqlc.narg('range_end')::timestamptz);
|
||||||
|
|
||||||
-- name: AnalyticsTeamByRole :many
|
-- name: AnalyticsTeamByRole :many
|
||||||
SELECT
|
SELECT
|
||||||
COALESCE(team_role, 'unknown') AS team_role,
|
COALESCE(tm.team_role, 'unknown') AS team_role,
|
||||||
COUNT(*)::bigint AS count
|
COUNT(*)::bigint AS count
|
||||||
FROM team_members
|
FROM team_members tm
|
||||||
GROUP BY team_role
|
WHERE (sqlc.narg('range_start')::timestamptz IS NULL OR tm.created_at >= sqlc.narg('range_start')::timestamptz)
|
||||||
|
AND (sqlc.narg('range_end')::timestamptz IS NULL OR tm.created_at < sqlc.narg('range_end')::timestamptz)
|
||||||
|
GROUP BY tm.team_role
|
||||||
ORDER BY count DESC;
|
ORDER BY count DESC;
|
||||||
|
|
||||||
-- name: AnalyticsTeamByStatus :many
|
-- name: AnalyticsTeamByStatus :many
|
||||||
SELECT
|
SELECT
|
||||||
COALESCE(status, 'unknown') AS status,
|
COALESCE(tm.status, 'unknown') AS status,
|
||||||
COUNT(*)::bigint AS count
|
COUNT(*)::bigint AS count
|
||||||
FROM team_members
|
FROM team_members tm
|
||||||
GROUP BY status
|
WHERE (sqlc.narg('range_start')::timestamptz IS NULL OR tm.created_at >= sqlc.narg('range_start')::timestamptz)
|
||||||
|
AND (sqlc.narg('range_end')::timestamptz IS NULL OR tm.created_at < sqlc.narg('range_end')::timestamptz)
|
||||||
|
GROUP BY tm.status
|
||||||
ORDER BY count DESC;
|
ORDER BY count DESC;
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,8 @@ package dbgen
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
|
"github.com/jackc/pgx/v5/pgtype"
|
||||||
)
|
)
|
||||||
|
|
||||||
const AnalyticsCourseCounts = `-- name: AnalyticsCourseCounts :one
|
const AnalyticsCourseCounts = `-- name: AnalyticsCourseCounts :one
|
||||||
|
|
@ -42,20 +44,27 @@ func (q *Queries) AnalyticsCourseCounts(ctx context.Context) (AnalyticsCourseCou
|
||||||
|
|
||||||
const AnalyticsIssuesByStatus = `-- name: AnalyticsIssuesByStatus :many
|
const AnalyticsIssuesByStatus = `-- name: AnalyticsIssuesByStatus :many
|
||||||
SELECT
|
SELECT
|
||||||
COALESCE(status, 'unknown') AS status,
|
COALESCE(ri.status, 'unknown') AS status,
|
||||||
COUNT(*)::bigint AS count
|
COUNT(*)::bigint AS count
|
||||||
FROM reported_issues
|
FROM reported_issues ri
|
||||||
GROUP BY status
|
WHERE ($1::timestamptz IS NULL OR ri.created_at >= $1::timestamptz)
|
||||||
|
AND ($2::timestamptz IS NULL OR ri.created_at < $2::timestamptz)
|
||||||
|
GROUP BY ri.status
|
||||||
ORDER BY count DESC
|
ORDER BY count DESC
|
||||||
`
|
`
|
||||||
|
|
||||||
|
type AnalyticsIssuesByStatusParams struct {
|
||||||
|
RangeStart pgtype.Timestamptz `json:"range_start"`
|
||||||
|
RangeEnd pgtype.Timestamptz `json:"range_end"`
|
||||||
|
}
|
||||||
|
|
||||||
type AnalyticsIssuesByStatusRow struct {
|
type AnalyticsIssuesByStatusRow struct {
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
Count int64 `json:"count"`
|
Count int64 `json:"count"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *Queries) AnalyticsIssuesByStatus(ctx context.Context) ([]AnalyticsIssuesByStatusRow, error) {
|
func (q *Queries) AnalyticsIssuesByStatus(ctx context.Context, arg AnalyticsIssuesByStatusParams) ([]AnalyticsIssuesByStatusRow, error) {
|
||||||
rows, err := q.db.Query(ctx, AnalyticsIssuesByStatus)
|
rows, err := q.db.Query(ctx, AnalyticsIssuesByStatus, arg.RangeStart, arg.RangeEnd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
@ -76,20 +85,27 @@ func (q *Queries) AnalyticsIssuesByStatus(ctx context.Context) ([]AnalyticsIssue
|
||||||
|
|
||||||
const AnalyticsIssuesByType = `-- name: AnalyticsIssuesByType :many
|
const AnalyticsIssuesByType = `-- name: AnalyticsIssuesByType :many
|
||||||
SELECT
|
SELECT
|
||||||
COALESCE(issue_type, 'unknown') AS issue_type,
|
COALESCE(ri.issue_type, 'unknown') AS issue_type,
|
||||||
COUNT(*)::bigint AS count
|
COUNT(*)::bigint AS count
|
||||||
FROM reported_issues
|
FROM reported_issues ri
|
||||||
GROUP BY issue_type
|
WHERE ($1::timestamptz IS NULL OR ri.created_at >= $1::timestamptz)
|
||||||
|
AND ($2::timestamptz IS NULL OR ri.created_at < $2::timestamptz)
|
||||||
|
GROUP BY ri.issue_type
|
||||||
ORDER BY count DESC
|
ORDER BY count DESC
|
||||||
`
|
`
|
||||||
|
|
||||||
|
type AnalyticsIssuesByTypeParams struct {
|
||||||
|
RangeStart pgtype.Timestamptz `json:"range_start"`
|
||||||
|
RangeEnd pgtype.Timestamptz `json:"range_end"`
|
||||||
|
}
|
||||||
|
|
||||||
type AnalyticsIssuesByTypeRow struct {
|
type AnalyticsIssuesByTypeRow struct {
|
||||||
IssueType string `json:"issue_type"`
|
IssueType string `json:"issue_type"`
|
||||||
Count int64 `json:"count"`
|
Count int64 `json:"count"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *Queries) AnalyticsIssuesByType(ctx context.Context) ([]AnalyticsIssuesByTypeRow, error) {
|
func (q *Queries) AnalyticsIssuesByType(ctx context.Context, arg AnalyticsIssuesByTypeParams) ([]AnalyticsIssuesByTypeRow, error) {
|
||||||
rows, err := q.db.Query(ctx, AnalyticsIssuesByType)
|
rows, err := q.db.Query(ctx, AnalyticsIssuesByType, arg.RangeStart, arg.RangeEnd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
@ -112,14 +128,21 @@ const AnalyticsIssuesSummary = `-- name: AnalyticsIssuesSummary :one
|
||||||
|
|
||||||
SELECT
|
SELECT
|
||||||
COUNT(*)::bigint AS total,
|
COUNT(*)::bigint AS total,
|
||||||
COUNT(*) FILTER (WHERE status = 'resolved')::bigint AS resolved,
|
COUNT(*) FILTER (WHERE ri.status = 'resolved')::bigint AS resolved,
|
||||||
CASE
|
CASE
|
||||||
WHEN COUNT(*) > 0 THEN (COUNT(*) FILTER (WHERE status = 'resolved')::float8 / COUNT(*)::float8)
|
WHEN COUNT(*) > 0 THEN (COUNT(*) FILTER (WHERE ri.status = 'resolved')::float8 / COUNT(*)::float8)
|
||||||
ELSE 0::float8
|
ELSE 0::float8
|
||||||
END AS resolution_rate
|
END AS resolution_rate
|
||||||
FROM reported_issues
|
FROM reported_issues ri
|
||||||
|
WHERE ($1::timestamptz IS NULL OR ri.created_at >= $1::timestamptz)
|
||||||
|
AND ($2::timestamptz IS NULL OR ri.created_at < $2::timestamptz)
|
||||||
`
|
`
|
||||||
|
|
||||||
|
type AnalyticsIssuesSummaryParams struct {
|
||||||
|
RangeStart pgtype.Timestamptz `json:"range_start"`
|
||||||
|
RangeEnd pgtype.Timestamptz `json:"range_end"`
|
||||||
|
}
|
||||||
|
|
||||||
type AnalyticsIssuesSummaryRow struct {
|
type AnalyticsIssuesSummaryRow struct {
|
||||||
Total int64 `json:"total"`
|
Total int64 `json:"total"`
|
||||||
Resolved int64 `json:"resolved"`
|
Resolved int64 `json:"resolved"`
|
||||||
|
|
@ -129,8 +152,8 @@ type AnalyticsIssuesSummaryRow struct {
|
||||||
// =====================
|
// =====================
|
||||||
// Issue Analytics
|
// Issue Analytics
|
||||||
// =====================
|
// =====================
|
||||||
func (q *Queries) AnalyticsIssuesSummary(ctx context.Context) (AnalyticsIssuesSummaryRow, error) {
|
func (q *Queries) AnalyticsIssuesSummary(ctx context.Context, arg AnalyticsIssuesSummaryParams) (AnalyticsIssuesSummaryRow, error) {
|
||||||
row := q.db.QueryRow(ctx, AnalyticsIssuesSummary)
|
row := q.db.QueryRow(ctx, AnalyticsIssuesSummary, arg.RangeStart, arg.RangeEnd)
|
||||||
var i AnalyticsIssuesSummaryRow
|
var i AnalyticsIssuesSummaryRow
|
||||||
err := row.Scan(&i.Total, &i.Resolved, &i.ResolutionRate)
|
err := row.Scan(&i.Total, &i.Resolved, &i.ResolutionRate)
|
||||||
return i, err
|
return i, err
|
||||||
|
|
@ -141,22 +164,36 @@ SELECT
|
||||||
d.date,
|
d.date,
|
||||||
COUNT(us.id)::bigint AS count
|
COUNT(us.id)::bigint AS count
|
||||||
FROM generate_series(
|
FROM generate_series(
|
||||||
CURRENT_DATE - INTERVAL '29 days',
|
$1::date,
|
||||||
CURRENT_DATE,
|
$2::date,
|
||||||
INTERVAL '1 day'
|
INTERVAL '1 day'
|
||||||
) AS d(date)
|
) AS d(date)
|
||||||
LEFT JOIN user_subscriptions us ON us.created_at::date = d.date
|
LEFT JOIN user_subscriptions us ON us.created_at::date = d.date
|
||||||
|
AND ($3::timestamptz IS NULL OR us.created_at >= $3::timestamptz)
|
||||||
|
AND ($4::timestamptz IS NULL OR us.created_at < $4::timestamptz)
|
||||||
GROUP BY d.date
|
GROUP BY d.date
|
||||||
ORDER BY d.date
|
ORDER BY d.date
|
||||||
`
|
`
|
||||||
|
|
||||||
|
type AnalyticsNewSubscriptionsLast30DaysParams struct {
|
||||||
|
SeriesStart pgtype.Date `json:"series_start"`
|
||||||
|
SeriesEnd pgtype.Date `json:"series_end"`
|
||||||
|
RangeStart pgtype.Timestamptz `json:"range_start"`
|
||||||
|
RangeEnd pgtype.Timestamptz `json:"range_end"`
|
||||||
|
}
|
||||||
|
|
||||||
type AnalyticsNewSubscriptionsLast30DaysRow struct {
|
type AnalyticsNewSubscriptionsLast30DaysRow struct {
|
||||||
Date interface{} `json:"date"`
|
Date interface{} `json:"date"`
|
||||||
Count int64 `json:"count"`
|
Count int64 `json:"count"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *Queries) AnalyticsNewSubscriptionsLast30Days(ctx context.Context) ([]AnalyticsNewSubscriptionsLast30DaysRow, error) {
|
func (q *Queries) AnalyticsNewSubscriptionsLast30Days(ctx context.Context, arg AnalyticsNewSubscriptionsLast30DaysParams) ([]AnalyticsNewSubscriptionsLast30DaysRow, error) {
|
||||||
rows, err := q.db.Query(ctx, AnalyticsNewSubscriptionsLast30Days)
|
rows, err := q.db.Query(ctx, AnalyticsNewSubscriptionsLast30Days,
|
||||||
|
arg.SeriesStart,
|
||||||
|
arg.SeriesEnd,
|
||||||
|
arg.RangeStart,
|
||||||
|
arg.RangeEnd,
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
@ -177,20 +214,27 @@ func (q *Queries) AnalyticsNewSubscriptionsLast30Days(ctx context.Context) ([]An
|
||||||
|
|
||||||
const AnalyticsNotificationsByChannel = `-- name: AnalyticsNotificationsByChannel :many
|
const AnalyticsNotificationsByChannel = `-- name: AnalyticsNotificationsByChannel :many
|
||||||
SELECT
|
SELECT
|
||||||
COALESCE(channel, 'unknown') AS channel,
|
COALESCE(n.channel, 'unknown') AS channel,
|
||||||
COUNT(*)::bigint AS count
|
COUNT(*)::bigint AS count
|
||||||
FROM notifications
|
FROM notifications n
|
||||||
GROUP BY channel
|
WHERE ($1::timestamptz IS NULL OR n.created_at >= $1::timestamptz)
|
||||||
|
AND ($2::timestamptz IS NULL OR n.created_at < $2::timestamptz)
|
||||||
|
GROUP BY n.channel
|
||||||
ORDER BY count DESC
|
ORDER BY count DESC
|
||||||
`
|
`
|
||||||
|
|
||||||
|
type AnalyticsNotificationsByChannelParams struct {
|
||||||
|
RangeStart pgtype.Timestamptz `json:"range_start"`
|
||||||
|
RangeEnd pgtype.Timestamptz `json:"range_end"`
|
||||||
|
}
|
||||||
|
|
||||||
type AnalyticsNotificationsByChannelRow struct {
|
type AnalyticsNotificationsByChannelRow struct {
|
||||||
Channel string `json:"channel"`
|
Channel string `json:"channel"`
|
||||||
Count int64 `json:"count"`
|
Count int64 `json:"count"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *Queries) AnalyticsNotificationsByChannel(ctx context.Context) ([]AnalyticsNotificationsByChannelRow, error) {
|
func (q *Queries) AnalyticsNotificationsByChannel(ctx context.Context, arg AnalyticsNotificationsByChannelParams) ([]AnalyticsNotificationsByChannelRow, error) {
|
||||||
rows, err := q.db.Query(ctx, AnalyticsNotificationsByChannel)
|
rows, err := q.db.Query(ctx, AnalyticsNotificationsByChannel, arg.RangeStart, arg.RangeEnd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
@ -211,20 +255,27 @@ func (q *Queries) AnalyticsNotificationsByChannel(ctx context.Context) ([]Analyt
|
||||||
|
|
||||||
const AnalyticsNotificationsByType = `-- name: AnalyticsNotificationsByType :many
|
const AnalyticsNotificationsByType = `-- name: AnalyticsNotificationsByType :many
|
||||||
SELECT
|
SELECT
|
||||||
COALESCE(type, 'unknown') AS type,
|
COALESCE(n.type, 'unknown') AS type,
|
||||||
COUNT(*)::bigint AS count
|
COUNT(*)::bigint AS count
|
||||||
FROM notifications
|
FROM notifications n
|
||||||
GROUP BY type
|
WHERE ($1::timestamptz IS NULL OR n.created_at >= $1::timestamptz)
|
||||||
|
AND ($2::timestamptz IS NULL OR n.created_at < $2::timestamptz)
|
||||||
|
GROUP BY n.type
|
||||||
ORDER BY count DESC
|
ORDER BY count DESC
|
||||||
`
|
`
|
||||||
|
|
||||||
|
type AnalyticsNotificationsByTypeParams struct {
|
||||||
|
RangeStart pgtype.Timestamptz `json:"range_start"`
|
||||||
|
RangeEnd pgtype.Timestamptz `json:"range_end"`
|
||||||
|
}
|
||||||
|
|
||||||
type AnalyticsNotificationsByTypeRow struct {
|
type AnalyticsNotificationsByTypeRow struct {
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
Count int64 `json:"count"`
|
Count int64 `json:"count"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *Queries) AnalyticsNotificationsByType(ctx context.Context) ([]AnalyticsNotificationsByTypeRow, error) {
|
func (q *Queries) AnalyticsNotificationsByType(ctx context.Context, arg AnalyticsNotificationsByTypeParams) ([]AnalyticsNotificationsByTypeRow, error) {
|
||||||
rows, err := q.db.Query(ctx, AnalyticsNotificationsByType)
|
rows, err := q.db.Query(ctx, AnalyticsNotificationsByType, arg.RangeStart, arg.RangeEnd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
@ -247,11 +298,18 @@ const AnalyticsNotificationsSummary = `-- name: AnalyticsNotificationsSummary :o
|
||||||
|
|
||||||
SELECT
|
SELECT
|
||||||
COUNT(*)::bigint AS total,
|
COUNT(*)::bigint AS total,
|
||||||
COUNT(*) FILTER (WHERE is_read = TRUE)::bigint AS read,
|
COUNT(*) FILTER (WHERE n.is_read = TRUE)::bigint AS read,
|
||||||
COUNT(*) FILTER (WHERE is_read = FALSE)::bigint AS unread
|
COUNT(*) FILTER (WHERE n.is_read = FALSE)::bigint AS unread
|
||||||
FROM notifications
|
FROM notifications n
|
||||||
|
WHERE ($1::timestamptz IS NULL OR n.created_at >= $1::timestamptz)
|
||||||
|
AND ($2::timestamptz IS NULL OR n.created_at < $2::timestamptz)
|
||||||
`
|
`
|
||||||
|
|
||||||
|
type AnalyticsNotificationsSummaryParams struct {
|
||||||
|
RangeStart pgtype.Timestamptz `json:"range_start"`
|
||||||
|
RangeEnd pgtype.Timestamptz `json:"range_end"`
|
||||||
|
}
|
||||||
|
|
||||||
type AnalyticsNotificationsSummaryRow struct {
|
type AnalyticsNotificationsSummaryRow struct {
|
||||||
Total int64 `json:"total"`
|
Total int64 `json:"total"`
|
||||||
Read int64 `json:"read"`
|
Read int64 `json:"read"`
|
||||||
|
|
@ -261,8 +319,8 @@ type AnalyticsNotificationsSummaryRow struct {
|
||||||
// =====================
|
// =====================
|
||||||
// Notification Analytics
|
// Notification Analytics
|
||||||
// =====================
|
// =====================
|
||||||
func (q *Queries) AnalyticsNotificationsSummary(ctx context.Context) (AnalyticsNotificationsSummaryRow, error) {
|
func (q *Queries) AnalyticsNotificationsSummary(ctx context.Context, arg AnalyticsNotificationsSummaryParams) (AnalyticsNotificationsSummaryRow, error) {
|
||||||
row := q.db.QueryRow(ctx, AnalyticsNotificationsSummary)
|
row := q.db.QueryRow(ctx, AnalyticsNotificationsSummary, arg.RangeStart, arg.RangeEnd)
|
||||||
var i AnalyticsNotificationsSummaryRow
|
var i AnalyticsNotificationsSummaryRow
|
||||||
err := row.Scan(&i.Total, &i.Read, &i.Unread)
|
err := row.Scan(&i.Total, &i.Read, &i.Unread)
|
||||||
return i, err
|
return i, err
|
||||||
|
|
@ -270,23 +328,30 @@ func (q *Queries) AnalyticsNotificationsSummary(ctx context.Context) (AnalyticsN
|
||||||
|
|
||||||
const AnalyticsPaymentsByMethod = `-- name: AnalyticsPaymentsByMethod :many
|
const AnalyticsPaymentsByMethod = `-- name: AnalyticsPaymentsByMethod :many
|
||||||
SELECT
|
SELECT
|
||||||
COALESCE(payment_method, 'unknown') AS payment_method,
|
COALESCE(p.payment_method, 'unknown') AS payment_method,
|
||||||
COUNT(*)::bigint AS count,
|
COUNT(*)::bigint AS count,
|
||||||
COALESCE(SUM(amount), 0)::float8 AS total_amount
|
COALESCE(SUM(p.amount), 0)::float8 AS total_amount
|
||||||
FROM payments
|
FROM payments p
|
||||||
WHERE status = 'SUCCESS'
|
WHERE p.status = 'SUCCESS'
|
||||||
GROUP BY payment_method
|
AND ($1::timestamptz IS NULL OR COALESCE(p.paid_at, p.created_at) >= $1::timestamptz)
|
||||||
|
AND ($2::timestamptz IS NULL OR COALESCE(p.paid_at, p.created_at) < $2::timestamptz)
|
||||||
|
GROUP BY p.payment_method
|
||||||
ORDER BY count DESC
|
ORDER BY count DESC
|
||||||
`
|
`
|
||||||
|
|
||||||
|
type AnalyticsPaymentsByMethodParams struct {
|
||||||
|
RangeStart pgtype.Timestamptz `json:"range_start"`
|
||||||
|
RangeEnd pgtype.Timestamptz `json:"range_end"`
|
||||||
|
}
|
||||||
|
|
||||||
type AnalyticsPaymentsByMethodRow struct {
|
type AnalyticsPaymentsByMethodRow struct {
|
||||||
PaymentMethod string `json:"payment_method"`
|
PaymentMethod string `json:"payment_method"`
|
||||||
Count int64 `json:"count"`
|
Count int64 `json:"count"`
|
||||||
TotalAmount float64 `json:"total_amount"`
|
TotalAmount float64 `json:"total_amount"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *Queries) AnalyticsPaymentsByMethod(ctx context.Context) ([]AnalyticsPaymentsByMethodRow, error) {
|
func (q *Queries) AnalyticsPaymentsByMethod(ctx context.Context, arg AnalyticsPaymentsByMethodParams) ([]AnalyticsPaymentsByMethodRow, error) {
|
||||||
rows, err := q.db.Query(ctx, AnalyticsPaymentsByMethod)
|
rows, err := q.db.Query(ctx, AnalyticsPaymentsByMethod, arg.RangeStart, arg.RangeEnd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
@ -307,22 +372,29 @@ func (q *Queries) AnalyticsPaymentsByMethod(ctx context.Context) ([]AnalyticsPay
|
||||||
|
|
||||||
const AnalyticsPaymentsByStatus = `-- name: AnalyticsPaymentsByStatus :many
|
const AnalyticsPaymentsByStatus = `-- name: AnalyticsPaymentsByStatus :many
|
||||||
SELECT
|
SELECT
|
||||||
COALESCE(status, 'unknown') AS status,
|
COALESCE(p.status, 'unknown') AS status,
|
||||||
COUNT(*)::bigint AS count,
|
COUNT(*)::bigint AS count,
|
||||||
COALESCE(SUM(amount), 0)::float8 AS total_amount
|
COALESCE(SUM(p.amount), 0)::float8 AS total_amount
|
||||||
FROM payments
|
FROM payments p
|
||||||
GROUP BY status
|
WHERE ($1::timestamptz IS NULL OR COALESCE(p.paid_at, p.created_at) >= $1::timestamptz)
|
||||||
|
AND ($2::timestamptz IS NULL OR COALESCE(p.paid_at, p.created_at) < $2::timestamptz)
|
||||||
|
GROUP BY p.status
|
||||||
ORDER BY count DESC
|
ORDER BY count DESC
|
||||||
`
|
`
|
||||||
|
|
||||||
|
type AnalyticsPaymentsByStatusParams struct {
|
||||||
|
RangeStart pgtype.Timestamptz `json:"range_start"`
|
||||||
|
RangeEnd pgtype.Timestamptz `json:"range_end"`
|
||||||
|
}
|
||||||
|
|
||||||
type AnalyticsPaymentsByStatusRow struct {
|
type AnalyticsPaymentsByStatusRow struct {
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
Count int64 `json:"count"`
|
Count int64 `json:"count"`
|
||||||
TotalAmount float64 `json:"total_amount"`
|
TotalAmount float64 `json:"total_amount"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *Queries) AnalyticsPaymentsByStatus(ctx context.Context) ([]AnalyticsPaymentsByStatusRow, error) {
|
func (q *Queries) AnalyticsPaymentsByStatus(ctx context.Context, arg AnalyticsPaymentsByStatusParams) ([]AnalyticsPaymentsByStatusRow, error) {
|
||||||
rows, err := q.db.Query(ctx, AnalyticsPaymentsByStatus)
|
rows, err := q.db.Query(ctx, AnalyticsPaymentsByStatus, arg.RangeStart, arg.RangeEnd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
@ -344,13 +416,20 @@ func (q *Queries) AnalyticsPaymentsByStatus(ctx context.Context) ([]AnalyticsPay
|
||||||
const AnalyticsPaymentsSummary = `-- name: AnalyticsPaymentsSummary :one
|
const AnalyticsPaymentsSummary = `-- name: AnalyticsPaymentsSummary :one
|
||||||
|
|
||||||
SELECT
|
SELECT
|
||||||
COALESCE(SUM(amount) FILTER (WHERE status = 'SUCCESS'), 0)::float8 AS total_revenue,
|
COALESCE(SUM(p.amount) FILTER (WHERE p.status = 'SUCCESS'), 0)::float8 AS total_revenue,
|
||||||
COALESCE(AVG(amount) FILTER (WHERE status = 'SUCCESS'), 0)::float8 AS avg_value,
|
COALESCE(AVG(p.amount) FILTER (WHERE p.status = 'SUCCESS'), 0)::float8 AS avg_value,
|
||||||
COUNT(*)::bigint AS total_payments,
|
COUNT(*)::bigint AS total_payments,
|
||||||
COUNT(*) FILTER (WHERE status = 'SUCCESS')::bigint AS successful_payments
|
COUNT(*) FILTER (WHERE p.status = 'SUCCESS')::bigint AS successful_payments
|
||||||
FROM payments
|
FROM payments p
|
||||||
|
WHERE ($1::timestamptz IS NULL OR COALESCE(p.paid_at, p.created_at) >= $1::timestamptz)
|
||||||
|
AND ($2::timestamptz IS NULL OR COALESCE(p.paid_at, p.created_at) < $2::timestamptz)
|
||||||
`
|
`
|
||||||
|
|
||||||
|
type AnalyticsPaymentsSummaryParams struct {
|
||||||
|
RangeStart pgtype.Timestamptz `json:"range_start"`
|
||||||
|
RangeEnd pgtype.Timestamptz `json:"range_end"`
|
||||||
|
}
|
||||||
|
|
||||||
type AnalyticsPaymentsSummaryRow struct {
|
type AnalyticsPaymentsSummaryRow struct {
|
||||||
TotalRevenue float64 `json:"total_revenue"`
|
TotalRevenue float64 `json:"total_revenue"`
|
||||||
AvgValue float64 `json:"avg_value"`
|
AvgValue float64 `json:"avg_value"`
|
||||||
|
|
@ -361,8 +440,8 @@ type AnalyticsPaymentsSummaryRow struct {
|
||||||
// =====================
|
// =====================
|
||||||
// Payment Analytics
|
// Payment Analytics
|
||||||
// =====================
|
// =====================
|
||||||
func (q *Queries) AnalyticsPaymentsSummary(ctx context.Context) (AnalyticsPaymentsSummaryRow, error) {
|
func (q *Queries) AnalyticsPaymentsSummary(ctx context.Context, arg AnalyticsPaymentsSummaryParams) (AnalyticsPaymentsSummaryRow, error) {
|
||||||
row := q.db.QueryRow(ctx, AnalyticsPaymentsSummary)
|
row := q.db.QueryRow(ctx, AnalyticsPaymentsSummary, arg.RangeStart, arg.RangeEnd)
|
||||||
var i AnalyticsPaymentsSummaryRow
|
var i AnalyticsPaymentsSummaryRow
|
||||||
err := row.Scan(
|
err := row.Scan(
|
||||||
&i.TotalRevenue,
|
&i.TotalRevenue,
|
||||||
|
|
@ -375,20 +454,27 @@ func (q *Queries) AnalyticsPaymentsSummary(ctx context.Context) (AnalyticsPaymen
|
||||||
|
|
||||||
const AnalyticsQuestionSetsByType = `-- name: AnalyticsQuestionSetsByType :many
|
const AnalyticsQuestionSetsByType = `-- name: AnalyticsQuestionSetsByType :many
|
||||||
SELECT
|
SELECT
|
||||||
COALESCE(set_type, 'unknown') AS set_type,
|
COALESCE(qs.set_type, 'unknown') AS set_type,
|
||||||
COUNT(*)::bigint AS count
|
COUNT(*)::bigint AS count
|
||||||
FROM question_sets
|
FROM question_sets qs
|
||||||
GROUP BY set_type
|
WHERE ($1::timestamptz IS NULL OR qs.created_at >= $1::timestamptz)
|
||||||
|
AND ($2::timestamptz IS NULL OR qs.created_at < $2::timestamptz)
|
||||||
|
GROUP BY qs.set_type
|
||||||
ORDER BY count DESC
|
ORDER BY count DESC
|
||||||
`
|
`
|
||||||
|
|
||||||
|
type AnalyticsQuestionSetsByTypeParams struct {
|
||||||
|
RangeStart pgtype.Timestamptz `json:"range_start"`
|
||||||
|
RangeEnd pgtype.Timestamptz `json:"range_end"`
|
||||||
|
}
|
||||||
|
|
||||||
type AnalyticsQuestionSetsByTypeRow struct {
|
type AnalyticsQuestionSetsByTypeRow struct {
|
||||||
SetType string `json:"set_type"`
|
SetType string `json:"set_type"`
|
||||||
Count int64 `json:"count"`
|
Count int64 `json:"count"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *Queries) AnalyticsQuestionSetsByType(ctx context.Context) ([]AnalyticsQuestionSetsByTypeRow, error) {
|
func (q *Queries) AnalyticsQuestionSetsByType(ctx context.Context, arg AnalyticsQuestionSetsByTypeParams) ([]AnalyticsQuestionSetsByTypeRow, error) {
|
||||||
rows, err := q.db.Query(ctx, AnalyticsQuestionSetsByType)
|
rows, err := q.db.Query(ctx, AnalyticsQuestionSetsByType, arg.RangeStart, arg.RangeEnd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
@ -409,20 +495,27 @@ func (q *Queries) AnalyticsQuestionSetsByType(ctx context.Context) ([]AnalyticsQ
|
||||||
|
|
||||||
const AnalyticsQuestionsByType = `-- name: AnalyticsQuestionsByType :many
|
const AnalyticsQuestionsByType = `-- name: AnalyticsQuestionsByType :many
|
||||||
SELECT
|
SELECT
|
||||||
COALESCE(question_type, 'unknown') AS question_type,
|
COALESCE(q.question_type, 'unknown') AS question_type,
|
||||||
COUNT(*)::bigint AS count
|
COUNT(*)::bigint AS count
|
||||||
FROM questions
|
FROM questions q
|
||||||
GROUP BY question_type
|
WHERE ($1::timestamptz IS NULL OR q.created_at >= $1::timestamptz)
|
||||||
|
AND ($2::timestamptz IS NULL OR q.created_at < $2::timestamptz)
|
||||||
|
GROUP BY q.question_type
|
||||||
ORDER BY count DESC
|
ORDER BY count DESC
|
||||||
`
|
`
|
||||||
|
|
||||||
|
type AnalyticsQuestionsByTypeParams struct {
|
||||||
|
RangeStart pgtype.Timestamptz `json:"range_start"`
|
||||||
|
RangeEnd pgtype.Timestamptz `json:"range_end"`
|
||||||
|
}
|
||||||
|
|
||||||
type AnalyticsQuestionsByTypeRow struct {
|
type AnalyticsQuestionsByTypeRow struct {
|
||||||
QuestionType string `json:"question_type"`
|
QuestionType string `json:"question_type"`
|
||||||
Count int64 `json:"count"`
|
Count int64 `json:"count"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *Queries) AnalyticsQuestionsByType(ctx context.Context) ([]AnalyticsQuestionsByTypeRow, error) {
|
func (q *Queries) AnalyticsQuestionsByType(ctx context.Context, arg AnalyticsQuestionsByTypeParams) ([]AnalyticsQuestionsByTypeRow, error) {
|
||||||
rows, err := q.db.Query(ctx, AnalyticsQuestionsByType)
|
rows, err := q.db.Query(ctx, AnalyticsQuestionsByType, arg.RangeStart, arg.RangeEnd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
@ -444,10 +537,25 @@ func (q *Queries) AnalyticsQuestionsByType(ctx context.Context) ([]AnalyticsQues
|
||||||
const AnalyticsQuestionsCounts = `-- name: AnalyticsQuestionsCounts :one
|
const AnalyticsQuestionsCounts = `-- name: AnalyticsQuestionsCounts :one
|
||||||
|
|
||||||
SELECT
|
SELECT
|
||||||
(SELECT COUNT(*)::bigint FROM questions) AS total_questions,
|
(
|
||||||
(SELECT COUNT(*)::bigint FROM question_sets) AS total_question_sets
|
SELECT COUNT(*)::bigint
|
||||||
|
FROM questions q
|
||||||
|
WHERE ($1::timestamptz IS NULL OR q.created_at >= $1::timestamptz)
|
||||||
|
AND ($2::timestamptz IS NULL OR q.created_at < $2::timestamptz)
|
||||||
|
) AS total_questions,
|
||||||
|
(
|
||||||
|
SELECT COUNT(*)::bigint
|
||||||
|
FROM question_sets qs
|
||||||
|
WHERE ($1::timestamptz IS NULL OR qs.created_at >= $1::timestamptz)
|
||||||
|
AND ($2::timestamptz IS NULL OR qs.created_at < $2::timestamptz)
|
||||||
|
) AS total_question_sets
|
||||||
`
|
`
|
||||||
|
|
||||||
|
type AnalyticsQuestionsCountsParams struct {
|
||||||
|
RangeStart pgtype.Timestamptz `json:"range_start"`
|
||||||
|
RangeEnd pgtype.Timestamptz `json:"range_end"`
|
||||||
|
}
|
||||||
|
|
||||||
type AnalyticsQuestionsCountsRow struct {
|
type AnalyticsQuestionsCountsRow struct {
|
||||||
TotalQuestions int64 `json:"total_questions"`
|
TotalQuestions int64 `json:"total_questions"`
|
||||||
TotalQuestionSets int64 `json:"total_question_sets"`
|
TotalQuestionSets int64 `json:"total_question_sets"`
|
||||||
|
|
@ -456,8 +564,8 @@ type AnalyticsQuestionsCountsRow struct {
|
||||||
// =====================
|
// =====================
|
||||||
// Content Analytics
|
// Content Analytics
|
||||||
// =====================
|
// =====================
|
||||||
func (q *Queries) AnalyticsQuestionsCounts(ctx context.Context) (AnalyticsQuestionsCountsRow, error) {
|
func (q *Queries) AnalyticsQuestionsCounts(ctx context.Context, arg AnalyticsQuestionsCountsParams) (AnalyticsQuestionsCountsRow, error) {
|
||||||
row := q.db.QueryRow(ctx, AnalyticsQuestionsCounts)
|
row := q.db.QueryRow(ctx, AnalyticsQuestionsCounts, arg.RangeStart, arg.RangeEnd)
|
||||||
var i AnalyticsQuestionsCountsRow
|
var i AnalyticsQuestionsCountsRow
|
||||||
err := row.Scan(&i.TotalQuestions, &i.TotalQuestionSets)
|
err := row.Scan(&i.TotalQuestions, &i.TotalQuestionSets)
|
||||||
return i, err
|
return i, err
|
||||||
|
|
@ -472,10 +580,17 @@ SELECT
|
||||||
FROM payments p
|
FROM payments p
|
||||||
JOIN subscription_plans sp ON sp.id = p.plan_id
|
JOIN subscription_plans sp ON sp.id = p.plan_id
|
||||||
WHERE p.status = 'SUCCESS'
|
WHERE p.status = 'SUCCESS'
|
||||||
|
AND ($1::timestamptz IS NULL OR COALESCE(p.paid_at, p.created_at) >= $1::timestamptz)
|
||||||
|
AND ($2::timestamptz IS NULL OR COALESCE(p.paid_at, p.created_at) < $2::timestamptz)
|
||||||
GROUP BY sp.name, sp.currency
|
GROUP BY sp.name, sp.currency
|
||||||
ORDER BY total_revenue DESC
|
ORDER BY total_revenue DESC
|
||||||
`
|
`
|
||||||
|
|
||||||
|
type AnalyticsRevenueByPlanParams struct {
|
||||||
|
RangeStart pgtype.Timestamptz `json:"range_start"`
|
||||||
|
RangeEnd pgtype.Timestamptz `json:"range_end"`
|
||||||
|
}
|
||||||
|
|
||||||
type AnalyticsRevenueByPlanRow struct {
|
type AnalyticsRevenueByPlanRow struct {
|
||||||
PlanName string `json:"plan_name"`
|
PlanName string `json:"plan_name"`
|
||||||
Currency string `json:"currency"`
|
Currency string `json:"currency"`
|
||||||
|
|
@ -483,8 +598,8 @@ type AnalyticsRevenueByPlanRow struct {
|
||||||
TotalRevenue float64 `json:"total_revenue"`
|
TotalRevenue float64 `json:"total_revenue"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *Queries) AnalyticsRevenueByPlan(ctx context.Context) ([]AnalyticsRevenueByPlanRow, error) {
|
func (q *Queries) AnalyticsRevenueByPlan(ctx context.Context, arg AnalyticsRevenueByPlanParams) ([]AnalyticsRevenueByPlanRow, error) {
|
||||||
rows, err := q.db.Query(ctx, AnalyticsRevenueByPlan)
|
rows, err := q.db.Query(ctx, AnalyticsRevenueByPlan, arg.RangeStart, arg.RangeEnd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
@ -513,22 +628,37 @@ SELECT
|
||||||
d.date,
|
d.date,
|
||||||
COALESCE(SUM(p.amount), 0)::float8 AS total_revenue
|
COALESCE(SUM(p.amount), 0)::float8 AS total_revenue
|
||||||
FROM generate_series(
|
FROM generate_series(
|
||||||
CURRENT_DATE - INTERVAL '29 days',
|
$1::date,
|
||||||
CURRENT_DATE,
|
$2::date,
|
||||||
INTERVAL '1 day'
|
INTERVAL '1 day'
|
||||||
) AS d(date)
|
) AS d(date)
|
||||||
LEFT JOIN payments p ON p.paid_at::date = d.date AND p.status = 'SUCCESS'
|
LEFT JOIN payments p ON COALESCE(p.paid_at, p.created_at)::date = d.date
|
||||||
|
AND p.status = 'SUCCESS'
|
||||||
|
AND ($3::timestamptz IS NULL OR COALESCE(p.paid_at, p.created_at) >= $3::timestamptz)
|
||||||
|
AND ($4::timestamptz IS NULL OR COALESCE(p.paid_at, p.created_at) < $4::timestamptz)
|
||||||
GROUP BY d.date
|
GROUP BY d.date
|
||||||
ORDER BY d.date
|
ORDER BY d.date
|
||||||
`
|
`
|
||||||
|
|
||||||
|
type AnalyticsRevenueLast30DaysParams struct {
|
||||||
|
SeriesStart pgtype.Date `json:"series_start"`
|
||||||
|
SeriesEnd pgtype.Date `json:"series_end"`
|
||||||
|
RangeStart pgtype.Timestamptz `json:"range_start"`
|
||||||
|
RangeEnd pgtype.Timestamptz `json:"range_end"`
|
||||||
|
}
|
||||||
|
|
||||||
type AnalyticsRevenueLast30DaysRow struct {
|
type AnalyticsRevenueLast30DaysRow struct {
|
||||||
Date interface{} `json:"date"`
|
Date interface{} `json:"date"`
|
||||||
TotalRevenue float64 `json:"total_revenue"`
|
TotalRevenue float64 `json:"total_revenue"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *Queries) AnalyticsRevenueLast30Days(ctx context.Context) ([]AnalyticsRevenueLast30DaysRow, error) {
|
func (q *Queries) AnalyticsRevenueLast30Days(ctx context.Context, arg AnalyticsRevenueLast30DaysParams) ([]AnalyticsRevenueLast30DaysRow, error) {
|
||||||
rows, err := q.db.Query(ctx, AnalyticsRevenueLast30Days)
|
rows, err := q.db.Query(ctx, AnalyticsRevenueLast30Days,
|
||||||
|
arg.SeriesStart,
|
||||||
|
arg.SeriesEnd,
|
||||||
|
arg.RangeStart,
|
||||||
|
arg.RangeEnd,
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
@ -549,20 +679,27 @@ func (q *Queries) AnalyticsRevenueLast30Days(ctx context.Context) ([]AnalyticsRe
|
||||||
|
|
||||||
const AnalyticsSubscriptionsByStatus = `-- name: AnalyticsSubscriptionsByStatus :many
|
const AnalyticsSubscriptionsByStatus = `-- name: AnalyticsSubscriptionsByStatus :many
|
||||||
SELECT
|
SELECT
|
||||||
COALESCE(status, 'unknown') AS status,
|
COALESCE(us.status, 'unknown') AS status,
|
||||||
COUNT(*)::bigint AS count
|
COUNT(*)::bigint AS count
|
||||||
FROM user_subscriptions
|
FROM user_subscriptions us
|
||||||
GROUP BY status
|
WHERE ($1::timestamptz IS NULL OR us.created_at >= $1::timestamptz)
|
||||||
|
AND ($2::timestamptz IS NULL OR us.created_at < $2::timestamptz)
|
||||||
|
GROUP BY us.status
|
||||||
ORDER BY count DESC
|
ORDER BY count DESC
|
||||||
`
|
`
|
||||||
|
|
||||||
|
type AnalyticsSubscriptionsByStatusParams struct {
|
||||||
|
RangeStart pgtype.Timestamptz `json:"range_start"`
|
||||||
|
RangeEnd pgtype.Timestamptz `json:"range_end"`
|
||||||
|
}
|
||||||
|
|
||||||
type AnalyticsSubscriptionsByStatusRow struct {
|
type AnalyticsSubscriptionsByStatusRow struct {
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
Count int64 `json:"count"`
|
Count int64 `json:"count"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *Queries) AnalyticsSubscriptionsByStatus(ctx context.Context) ([]AnalyticsSubscriptionsByStatusRow, error) {
|
func (q *Queries) AnalyticsSubscriptionsByStatus(ctx context.Context, arg AnalyticsSubscriptionsByStatusParams) ([]AnalyticsSubscriptionsByStatusRow, error) {
|
||||||
rows, err := q.db.Query(ctx, AnalyticsSubscriptionsByStatus)
|
rows, err := q.db.Query(ctx, AnalyticsSubscriptionsByStatus, arg.RangeStart, arg.RangeEnd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
@ -585,13 +722,27 @@ const AnalyticsSubscriptionsSummary = `-- name: AnalyticsSubscriptionsSummary :o
|
||||||
|
|
||||||
SELECT
|
SELECT
|
||||||
COUNT(*)::bigint AS total,
|
COUNT(*)::bigint AS total,
|
||||||
COUNT(*) FILTER (WHERE status = 'ACTIVE')::bigint AS active,
|
COUNT(*) FILTER (WHERE us.status = 'ACTIVE')::bigint AS active,
|
||||||
COUNT(*) FILTER (WHERE created_at >= CURRENT_DATE)::bigint AS new_today,
|
COUNT(*) FILTER (WHERE us.created_at::date = $1::date)::bigint AS new_today,
|
||||||
COUNT(*) FILTER (WHERE created_at >= CURRENT_DATE - INTERVAL '7 days')::bigint AS new_this_week,
|
COUNT(*) FILTER (
|
||||||
COUNT(*) FILTER (WHERE created_at >= CURRENT_DATE - INTERVAL '30 days')::bigint AS new_this_month
|
WHERE us.created_at::date >= $1::date - INTERVAL '6 days'
|
||||||
FROM user_subscriptions
|
AND us.created_at::date <= $1::date
|
||||||
|
)::bigint AS new_this_week,
|
||||||
|
COUNT(*) FILTER (
|
||||||
|
WHERE us.created_at::date >= $1::date - INTERVAL '29 days'
|
||||||
|
AND us.created_at::date <= $1::date
|
||||||
|
)::bigint AS new_this_month
|
||||||
|
FROM user_subscriptions us
|
||||||
|
WHERE ($2::timestamptz IS NULL OR us.created_at >= $2::timestamptz)
|
||||||
|
AND ($3::timestamptz IS NULL OR us.created_at < $3::timestamptz)
|
||||||
`
|
`
|
||||||
|
|
||||||
|
type AnalyticsSubscriptionsSummaryParams struct {
|
||||||
|
RefDate pgtype.Date `json:"ref_date"`
|
||||||
|
RangeStart pgtype.Timestamptz `json:"range_start"`
|
||||||
|
RangeEnd pgtype.Timestamptz `json:"range_end"`
|
||||||
|
}
|
||||||
|
|
||||||
type AnalyticsSubscriptionsSummaryRow struct {
|
type AnalyticsSubscriptionsSummaryRow struct {
|
||||||
Total int64 `json:"total"`
|
Total int64 `json:"total"`
|
||||||
Active int64 `json:"active"`
|
Active int64 `json:"active"`
|
||||||
|
|
@ -603,8 +754,8 @@ type AnalyticsSubscriptionsSummaryRow struct {
|
||||||
// =====================
|
// =====================
|
||||||
// Subscription Analytics
|
// Subscription Analytics
|
||||||
// =====================
|
// =====================
|
||||||
func (q *Queries) AnalyticsSubscriptionsSummary(ctx context.Context) (AnalyticsSubscriptionsSummaryRow, error) {
|
func (q *Queries) AnalyticsSubscriptionsSummary(ctx context.Context, arg AnalyticsSubscriptionsSummaryParams) (AnalyticsSubscriptionsSummaryRow, error) {
|
||||||
row := q.db.QueryRow(ctx, AnalyticsSubscriptionsSummary)
|
row := q.db.QueryRow(ctx, AnalyticsSubscriptionsSummary, arg.RefDate, arg.RangeStart, arg.RangeEnd)
|
||||||
var i AnalyticsSubscriptionsSummaryRow
|
var i AnalyticsSubscriptionsSummaryRow
|
||||||
err := row.Scan(
|
err := row.Scan(
|
||||||
&i.Total,
|
&i.Total,
|
||||||
|
|
@ -618,20 +769,27 @@ func (q *Queries) AnalyticsSubscriptionsSummary(ctx context.Context) (AnalyticsS
|
||||||
|
|
||||||
const AnalyticsTeamByRole = `-- name: AnalyticsTeamByRole :many
|
const AnalyticsTeamByRole = `-- name: AnalyticsTeamByRole :many
|
||||||
SELECT
|
SELECT
|
||||||
COALESCE(team_role, 'unknown') AS team_role,
|
COALESCE(tm.team_role, 'unknown') AS team_role,
|
||||||
COUNT(*)::bigint AS count
|
COUNT(*)::bigint AS count
|
||||||
FROM team_members
|
FROM team_members tm
|
||||||
GROUP BY team_role
|
WHERE ($1::timestamptz IS NULL OR tm.created_at >= $1::timestamptz)
|
||||||
|
AND ($2::timestamptz IS NULL OR tm.created_at < $2::timestamptz)
|
||||||
|
GROUP BY tm.team_role
|
||||||
ORDER BY count DESC
|
ORDER BY count DESC
|
||||||
`
|
`
|
||||||
|
|
||||||
|
type AnalyticsTeamByRoleParams struct {
|
||||||
|
RangeStart pgtype.Timestamptz `json:"range_start"`
|
||||||
|
RangeEnd pgtype.Timestamptz `json:"range_end"`
|
||||||
|
}
|
||||||
|
|
||||||
type AnalyticsTeamByRoleRow struct {
|
type AnalyticsTeamByRoleRow struct {
|
||||||
TeamRole string `json:"team_role"`
|
TeamRole string `json:"team_role"`
|
||||||
Count int64 `json:"count"`
|
Count int64 `json:"count"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *Queries) AnalyticsTeamByRole(ctx context.Context) ([]AnalyticsTeamByRoleRow, error) {
|
func (q *Queries) AnalyticsTeamByRole(ctx context.Context, arg AnalyticsTeamByRoleParams) ([]AnalyticsTeamByRoleRow, error) {
|
||||||
rows, err := q.db.Query(ctx, AnalyticsTeamByRole)
|
rows, err := q.db.Query(ctx, AnalyticsTeamByRole, arg.RangeStart, arg.RangeEnd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
@ -652,20 +810,27 @@ func (q *Queries) AnalyticsTeamByRole(ctx context.Context) ([]AnalyticsTeamByRol
|
||||||
|
|
||||||
const AnalyticsTeamByStatus = `-- name: AnalyticsTeamByStatus :many
|
const AnalyticsTeamByStatus = `-- name: AnalyticsTeamByStatus :many
|
||||||
SELECT
|
SELECT
|
||||||
COALESCE(status, 'unknown') AS status,
|
COALESCE(tm.status, 'unknown') AS status,
|
||||||
COUNT(*)::bigint AS count
|
COUNT(*)::bigint AS count
|
||||||
FROM team_members
|
FROM team_members tm
|
||||||
GROUP BY status
|
WHERE ($1::timestamptz IS NULL OR tm.created_at >= $1::timestamptz)
|
||||||
|
AND ($2::timestamptz IS NULL OR tm.created_at < $2::timestamptz)
|
||||||
|
GROUP BY tm.status
|
||||||
ORDER BY count DESC
|
ORDER BY count DESC
|
||||||
`
|
`
|
||||||
|
|
||||||
|
type AnalyticsTeamByStatusParams struct {
|
||||||
|
RangeStart pgtype.Timestamptz `json:"range_start"`
|
||||||
|
RangeEnd pgtype.Timestamptz `json:"range_end"`
|
||||||
|
}
|
||||||
|
|
||||||
type AnalyticsTeamByStatusRow struct {
|
type AnalyticsTeamByStatusRow struct {
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
Count int64 `json:"count"`
|
Count int64 `json:"count"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *Queries) AnalyticsTeamByStatus(ctx context.Context) ([]AnalyticsTeamByStatusRow, error) {
|
func (q *Queries) AnalyticsTeamByStatus(ctx context.Context, arg AnalyticsTeamByStatusParams) ([]AnalyticsTeamByStatusRow, error) {
|
||||||
rows, err := q.db.Query(ctx, AnalyticsTeamByStatus)
|
rows, err := q.db.Query(ctx, AnalyticsTeamByStatus, arg.RangeStart, arg.RangeEnd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
@ -688,14 +853,21 @@ const AnalyticsTeamSummary = `-- name: AnalyticsTeamSummary :one
|
||||||
|
|
||||||
SELECT
|
SELECT
|
||||||
COUNT(*)::bigint AS total_members
|
COUNT(*)::bigint AS total_members
|
||||||
FROM team_members
|
FROM team_members tm
|
||||||
|
WHERE ($1::timestamptz IS NULL OR tm.created_at >= $1::timestamptz)
|
||||||
|
AND ($2::timestamptz IS NULL OR tm.created_at < $2::timestamptz)
|
||||||
`
|
`
|
||||||
|
|
||||||
|
type AnalyticsTeamSummaryParams struct {
|
||||||
|
RangeStart pgtype.Timestamptz `json:"range_start"`
|
||||||
|
RangeEnd pgtype.Timestamptz `json:"range_end"`
|
||||||
|
}
|
||||||
|
|
||||||
// =====================
|
// =====================
|
||||||
// Team Analytics
|
// Team Analytics
|
||||||
// =====================
|
// =====================
|
||||||
func (q *Queries) AnalyticsTeamSummary(ctx context.Context) (int64, error) {
|
func (q *Queries) AnalyticsTeamSummary(ctx context.Context, arg AnalyticsTeamSummaryParams) (int64, error) {
|
||||||
row := q.db.QueryRow(ctx, AnalyticsTeamSummary)
|
row := q.db.QueryRow(ctx, AnalyticsTeamSummary, arg.RangeStart, arg.RangeEnd)
|
||||||
var total_members int64
|
var total_members int64
|
||||||
err := row.Scan(&total_members)
|
err := row.Scan(&total_members)
|
||||||
return total_members, err
|
return total_members, err
|
||||||
|
|
@ -706,22 +878,36 @@ SELECT
|
||||||
d.date,
|
d.date,
|
||||||
COUNT(u.id)::bigint AS count
|
COUNT(u.id)::bigint AS count
|
||||||
FROM generate_series(
|
FROM generate_series(
|
||||||
CURRENT_DATE - INTERVAL '29 days',
|
$1::date,
|
||||||
CURRENT_DATE,
|
$2::date,
|
||||||
INTERVAL '1 day'
|
INTERVAL '1 day'
|
||||||
) AS d(date)
|
) AS d(date)
|
||||||
LEFT JOIN users u ON u.created_at::date = d.date
|
LEFT JOIN users u ON u.created_at::date = d.date
|
||||||
|
AND ($3::timestamptz IS NULL OR u.created_at >= $3::timestamptz)
|
||||||
|
AND ($4::timestamptz IS NULL OR u.created_at < $4::timestamptz)
|
||||||
GROUP BY d.date
|
GROUP BY d.date
|
||||||
ORDER BY d.date
|
ORDER BY d.date
|
||||||
`
|
`
|
||||||
|
|
||||||
|
type AnalyticsUserRegistrationsLast30DaysParams struct {
|
||||||
|
SeriesStart pgtype.Date `json:"series_start"`
|
||||||
|
SeriesEnd pgtype.Date `json:"series_end"`
|
||||||
|
RangeStart pgtype.Timestamptz `json:"range_start"`
|
||||||
|
RangeEnd pgtype.Timestamptz `json:"range_end"`
|
||||||
|
}
|
||||||
|
|
||||||
type AnalyticsUserRegistrationsLast30DaysRow struct {
|
type AnalyticsUserRegistrationsLast30DaysRow struct {
|
||||||
Date interface{} `json:"date"`
|
Date interface{} `json:"date"`
|
||||||
Count int64 `json:"count"`
|
Count int64 `json:"count"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *Queries) AnalyticsUserRegistrationsLast30Days(ctx context.Context) ([]AnalyticsUserRegistrationsLast30DaysRow, error) {
|
func (q *Queries) AnalyticsUserRegistrationsLast30Days(ctx context.Context, arg AnalyticsUserRegistrationsLast30DaysParams) ([]AnalyticsUserRegistrationsLast30DaysRow, error) {
|
||||||
rows, err := q.db.Query(ctx, AnalyticsUserRegistrationsLast30Days)
|
rows, err := q.db.Query(ctx, AnalyticsUserRegistrationsLast30Days,
|
||||||
|
arg.SeriesStart,
|
||||||
|
arg.SeriesEnd,
|
||||||
|
arg.RangeStart,
|
||||||
|
arg.RangeEnd,
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
@ -742,20 +928,27 @@ func (q *Queries) AnalyticsUserRegistrationsLast30Days(ctx context.Context) ([]A
|
||||||
|
|
||||||
const AnalyticsUsersByAgeGroup = `-- name: AnalyticsUsersByAgeGroup :many
|
const AnalyticsUsersByAgeGroup = `-- name: AnalyticsUsersByAgeGroup :many
|
||||||
SELECT
|
SELECT
|
||||||
COALESCE(age_group, 'unknown') AS age_group,
|
COALESCE(u.age_group, 'unknown') AS age_group,
|
||||||
COUNT(*)::bigint AS count
|
COUNT(*)::bigint AS count
|
||||||
FROM users
|
FROM users u
|
||||||
GROUP BY age_group
|
WHERE ($1::timestamptz IS NULL OR u.created_at >= $1::timestamptz)
|
||||||
|
AND ($2::timestamptz IS NULL OR u.created_at < $2::timestamptz)
|
||||||
|
GROUP BY u.age_group
|
||||||
ORDER BY count DESC
|
ORDER BY count DESC
|
||||||
`
|
`
|
||||||
|
|
||||||
|
type AnalyticsUsersByAgeGroupParams struct {
|
||||||
|
RangeStart pgtype.Timestamptz `json:"range_start"`
|
||||||
|
RangeEnd pgtype.Timestamptz `json:"range_end"`
|
||||||
|
}
|
||||||
|
|
||||||
type AnalyticsUsersByAgeGroupRow struct {
|
type AnalyticsUsersByAgeGroupRow struct {
|
||||||
AgeGroup string `json:"age_group"`
|
AgeGroup string `json:"age_group"`
|
||||||
Count int64 `json:"count"`
|
Count int64 `json:"count"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *Queries) AnalyticsUsersByAgeGroup(ctx context.Context) ([]AnalyticsUsersByAgeGroupRow, error) {
|
func (q *Queries) AnalyticsUsersByAgeGroup(ctx context.Context, arg AnalyticsUsersByAgeGroupParams) ([]AnalyticsUsersByAgeGroupRow, error) {
|
||||||
rows, err := q.db.Query(ctx, AnalyticsUsersByAgeGroup)
|
rows, err := q.db.Query(ctx, AnalyticsUsersByAgeGroup, arg.RangeStart, arg.RangeEnd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
@ -776,20 +969,27 @@ func (q *Queries) AnalyticsUsersByAgeGroup(ctx context.Context) ([]AnalyticsUser
|
||||||
|
|
||||||
const AnalyticsUsersByKnowledgeLevel = `-- name: AnalyticsUsersByKnowledgeLevel :many
|
const AnalyticsUsersByKnowledgeLevel = `-- name: AnalyticsUsersByKnowledgeLevel :many
|
||||||
SELECT
|
SELECT
|
||||||
COALESCE(knowledge_level, 'unknown') AS knowledge_level,
|
COALESCE(u.knowledge_level, 'unknown') AS knowledge_level,
|
||||||
COUNT(*)::bigint AS count
|
COUNT(*)::bigint AS count
|
||||||
FROM users
|
FROM users u
|
||||||
GROUP BY knowledge_level
|
WHERE ($1::timestamptz IS NULL OR u.created_at >= $1::timestamptz)
|
||||||
|
AND ($2::timestamptz IS NULL OR u.created_at < $2::timestamptz)
|
||||||
|
GROUP BY u.knowledge_level
|
||||||
ORDER BY count DESC
|
ORDER BY count DESC
|
||||||
`
|
`
|
||||||
|
|
||||||
|
type AnalyticsUsersByKnowledgeLevelParams struct {
|
||||||
|
RangeStart pgtype.Timestamptz `json:"range_start"`
|
||||||
|
RangeEnd pgtype.Timestamptz `json:"range_end"`
|
||||||
|
}
|
||||||
|
|
||||||
type AnalyticsUsersByKnowledgeLevelRow struct {
|
type AnalyticsUsersByKnowledgeLevelRow struct {
|
||||||
KnowledgeLevel string `json:"knowledge_level"`
|
KnowledgeLevel string `json:"knowledge_level"`
|
||||||
Count int64 `json:"count"`
|
Count int64 `json:"count"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *Queries) AnalyticsUsersByKnowledgeLevel(ctx context.Context) ([]AnalyticsUsersByKnowledgeLevelRow, error) {
|
func (q *Queries) AnalyticsUsersByKnowledgeLevel(ctx context.Context, arg AnalyticsUsersByKnowledgeLevelParams) ([]AnalyticsUsersByKnowledgeLevelRow, error) {
|
||||||
rows, err := q.db.Query(ctx, AnalyticsUsersByKnowledgeLevel)
|
rows, err := q.db.Query(ctx, AnalyticsUsersByKnowledgeLevel, arg.RangeStart, arg.RangeEnd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
@ -810,20 +1010,27 @@ func (q *Queries) AnalyticsUsersByKnowledgeLevel(ctx context.Context) ([]Analyti
|
||||||
|
|
||||||
const AnalyticsUsersByRegion = `-- name: AnalyticsUsersByRegion :many
|
const AnalyticsUsersByRegion = `-- name: AnalyticsUsersByRegion :many
|
||||||
SELECT
|
SELECT
|
||||||
COALESCE(region, 'unknown') AS region,
|
COALESCE(u.region, 'unknown') AS region,
|
||||||
COUNT(*)::bigint AS count
|
COUNT(*)::bigint AS count
|
||||||
FROM users
|
FROM users u
|
||||||
GROUP BY region
|
WHERE ($1::timestamptz IS NULL OR u.created_at >= $1::timestamptz)
|
||||||
|
AND ($2::timestamptz IS NULL OR u.created_at < $2::timestamptz)
|
||||||
|
GROUP BY u.region
|
||||||
ORDER BY count DESC
|
ORDER BY count DESC
|
||||||
`
|
`
|
||||||
|
|
||||||
|
type AnalyticsUsersByRegionParams struct {
|
||||||
|
RangeStart pgtype.Timestamptz `json:"range_start"`
|
||||||
|
RangeEnd pgtype.Timestamptz `json:"range_end"`
|
||||||
|
}
|
||||||
|
|
||||||
type AnalyticsUsersByRegionRow struct {
|
type AnalyticsUsersByRegionRow struct {
|
||||||
Region string `json:"region"`
|
Region string `json:"region"`
|
||||||
Count int64 `json:"count"`
|
Count int64 `json:"count"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *Queries) AnalyticsUsersByRegion(ctx context.Context) ([]AnalyticsUsersByRegionRow, error) {
|
func (q *Queries) AnalyticsUsersByRegion(ctx context.Context, arg AnalyticsUsersByRegionParams) ([]AnalyticsUsersByRegionRow, error) {
|
||||||
rows, err := q.db.Query(ctx, AnalyticsUsersByRegion)
|
rows, err := q.db.Query(ctx, AnalyticsUsersByRegion, arg.RangeStart, arg.RangeEnd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
@ -844,20 +1051,27 @@ func (q *Queries) AnalyticsUsersByRegion(ctx context.Context) ([]AnalyticsUsersB
|
||||||
|
|
||||||
const AnalyticsUsersByRole = `-- name: AnalyticsUsersByRole :many
|
const AnalyticsUsersByRole = `-- name: AnalyticsUsersByRole :many
|
||||||
SELECT
|
SELECT
|
||||||
COALESCE(role, 'unknown') AS role,
|
COALESCE(u.role, 'unknown') AS role,
|
||||||
COUNT(*)::bigint AS count
|
COUNT(*)::bigint AS count
|
||||||
FROM users
|
FROM users u
|
||||||
GROUP BY role
|
WHERE ($1::timestamptz IS NULL OR u.created_at >= $1::timestamptz)
|
||||||
|
AND ($2::timestamptz IS NULL OR u.created_at < $2::timestamptz)
|
||||||
|
GROUP BY u.role
|
||||||
ORDER BY count DESC
|
ORDER BY count DESC
|
||||||
`
|
`
|
||||||
|
|
||||||
|
type AnalyticsUsersByRoleParams struct {
|
||||||
|
RangeStart pgtype.Timestamptz `json:"range_start"`
|
||||||
|
RangeEnd pgtype.Timestamptz `json:"range_end"`
|
||||||
|
}
|
||||||
|
|
||||||
type AnalyticsUsersByRoleRow struct {
|
type AnalyticsUsersByRoleRow struct {
|
||||||
Role string `json:"role"`
|
Role string `json:"role"`
|
||||||
Count int64 `json:"count"`
|
Count int64 `json:"count"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *Queries) AnalyticsUsersByRole(ctx context.Context) ([]AnalyticsUsersByRoleRow, error) {
|
func (q *Queries) AnalyticsUsersByRole(ctx context.Context, arg AnalyticsUsersByRoleParams) ([]AnalyticsUsersByRoleRow, error) {
|
||||||
rows, err := q.db.Query(ctx, AnalyticsUsersByRole)
|
rows, err := q.db.Query(ctx, AnalyticsUsersByRole, arg.RangeStart, arg.RangeEnd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
@ -878,20 +1092,27 @@ func (q *Queries) AnalyticsUsersByRole(ctx context.Context) ([]AnalyticsUsersByR
|
||||||
|
|
||||||
const AnalyticsUsersByStatus = `-- name: AnalyticsUsersByStatus :many
|
const AnalyticsUsersByStatus = `-- name: AnalyticsUsersByStatus :many
|
||||||
SELECT
|
SELECT
|
||||||
COALESCE(status, 'unknown') AS status,
|
COALESCE(u.status, 'unknown') AS status,
|
||||||
COUNT(*)::bigint AS count
|
COUNT(*)::bigint AS count
|
||||||
FROM users
|
FROM users u
|
||||||
GROUP BY status
|
WHERE ($1::timestamptz IS NULL OR u.created_at >= $1::timestamptz)
|
||||||
|
AND ($2::timestamptz IS NULL OR u.created_at < $2::timestamptz)
|
||||||
|
GROUP BY u.status
|
||||||
ORDER BY count DESC
|
ORDER BY count DESC
|
||||||
`
|
`
|
||||||
|
|
||||||
|
type AnalyticsUsersByStatusParams struct {
|
||||||
|
RangeStart pgtype.Timestamptz `json:"range_start"`
|
||||||
|
RangeEnd pgtype.Timestamptz `json:"range_end"`
|
||||||
|
}
|
||||||
|
|
||||||
type AnalyticsUsersByStatusRow struct {
|
type AnalyticsUsersByStatusRow struct {
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
Count int64 `json:"count"`
|
Count int64 `json:"count"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *Queries) AnalyticsUsersByStatus(ctx context.Context) ([]AnalyticsUsersByStatusRow, error) {
|
func (q *Queries) AnalyticsUsersByStatus(ctx context.Context, arg AnalyticsUsersByStatusParams) ([]AnalyticsUsersByStatusRow, error) {
|
||||||
rows, err := q.db.Query(ctx, AnalyticsUsersByStatus)
|
rows, err := q.db.Query(ctx, AnalyticsUsersByStatus, arg.RangeStart, arg.RangeEnd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
@ -915,12 +1136,26 @@ const AnalyticsUsersSummary = `-- name: AnalyticsUsersSummary :one
|
||||||
|
|
||||||
SELECT
|
SELECT
|
||||||
COUNT(*)::bigint AS total,
|
COUNT(*)::bigint AS total,
|
||||||
COUNT(*) FILTER (WHERE created_at >= CURRENT_DATE)::bigint AS new_today,
|
COUNT(*) FILTER (WHERE u.created_at::date = $1::date)::bigint AS new_today,
|
||||||
COUNT(*) FILTER (WHERE created_at >= CURRENT_DATE - INTERVAL '7 days')::bigint AS new_this_week,
|
COUNT(*) FILTER (
|
||||||
COUNT(*) FILTER (WHERE created_at >= CURRENT_DATE - INTERVAL '30 days')::bigint AS new_this_month
|
WHERE u.created_at::date >= $1::date - INTERVAL '6 days'
|
||||||
FROM users
|
AND u.created_at::date <= $1::date
|
||||||
|
)::bigint AS new_this_week,
|
||||||
|
COUNT(*) FILTER (
|
||||||
|
WHERE u.created_at::date >= $1::date - INTERVAL '29 days'
|
||||||
|
AND u.created_at::date <= $1::date
|
||||||
|
)::bigint AS new_this_month
|
||||||
|
FROM users u
|
||||||
|
WHERE ($2::timestamptz IS NULL OR u.created_at >= $2::timestamptz)
|
||||||
|
AND ($3::timestamptz IS NULL OR u.created_at < $3::timestamptz)
|
||||||
`
|
`
|
||||||
|
|
||||||
|
type AnalyticsUsersSummaryParams struct {
|
||||||
|
RefDate pgtype.Date `json:"ref_date"`
|
||||||
|
RangeStart pgtype.Timestamptz `json:"range_start"`
|
||||||
|
RangeEnd pgtype.Timestamptz `json:"range_end"`
|
||||||
|
}
|
||||||
|
|
||||||
type AnalyticsUsersSummaryRow struct {
|
type AnalyticsUsersSummaryRow struct {
|
||||||
Total int64 `json:"total"`
|
Total int64 `json:"total"`
|
||||||
NewToday int64 `json:"new_today"`
|
NewToday int64 `json:"new_today"`
|
||||||
|
|
@ -929,13 +1164,25 @@ type AnalyticsUsersSummaryRow struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
// =====================
|
// =====================
|
||||||
// Analytics
|
// Analytics (date-filtered)
|
||||||
// =====================
|
// =====================
|
||||||
|
// Shared optional params (nullable = all-time):
|
||||||
|
//
|
||||||
|
// range_start, range_end (exclusive upper bound)
|
||||||
|
//
|
||||||
|
// Required chart params:
|
||||||
|
//
|
||||||
|
// series_start, series_end (inclusive dates)
|
||||||
|
//
|
||||||
|
// Relative window anchor:
|
||||||
|
//
|
||||||
|
// ref_date (inclusive date used for new_today/week/month)
|
||||||
|
//
|
||||||
// =====================
|
// =====================
|
||||||
// User Analytics
|
// User Analytics
|
||||||
// =====================
|
// =====================
|
||||||
func (q *Queries) AnalyticsUsersSummary(ctx context.Context) (AnalyticsUsersSummaryRow, error) {
|
func (q *Queries) AnalyticsUsersSummary(ctx context.Context, arg AnalyticsUsersSummaryParams) (AnalyticsUsersSummaryRow, error) {
|
||||||
row := q.db.QueryRow(ctx, AnalyticsUsersSummary)
|
row := q.db.QueryRow(ctx, AnalyticsUsersSummary, arg.RefDate, arg.RangeStart, arg.RangeEnd)
|
||||||
var i AnalyticsUsersSummaryRow
|
var i AnalyticsUsersSummaryRow
|
||||||
err := row.Scan(
|
err := row.Scan(
|
||||||
&i.Total,
|
&i.Total,
|
||||||
|
|
|
||||||
|
|
@ -101,6 +101,17 @@ type ExamPrepUnitModuleLesson struct {
|
||||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Faq struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
Question string `json:"question"`
|
||||||
|
Answer string `json:"answer"`
|
||||||
|
Category pgtype.Text `json:"category"`
|
||||||
|
DisplayOrder int32 `json:"display_order"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||||
|
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
type GlobalSetting struct {
|
type GlobalSetting struct {
|
||||||
Key string `json:"key"`
|
Key string `json:"key"`
|
||||||
Value string `json:"value"`
|
Value string `json:"value"`
|
||||||
|
|
|
||||||
|
|
@ -110,6 +110,7 @@ type AnalyticsTeamSection struct {
|
||||||
|
|
||||||
type AnalyticsDashboard struct {
|
type AnalyticsDashboard struct {
|
||||||
GeneratedAt time.Time `json:"generated_at"`
|
GeneratedAt time.Time `json:"generated_at"`
|
||||||
|
DateFilter AnalyticsDateFilter `json:"date_filter"`
|
||||||
Users AnalyticsUsersSection `json:"users"`
|
Users AnalyticsUsersSection `json:"users"`
|
||||||
Subscriptions AnalyticsSubscriptionsSection `json:"subscriptions"`
|
Subscriptions AnalyticsSubscriptionsSection `json:"subscriptions"`
|
||||||
Payments AnalyticsPaymentsSection `json:"payments"`
|
Payments AnalyticsPaymentsSection `json:"payments"`
|
||||||
|
|
|
||||||
158
internal/domain/analytics_filter.go
Normal file
158
internal/domain/analytics_filter.go
Normal file
|
|
@ -0,0 +1,158 @@
|
||||||
|
package domain
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
AnalyticsFilterAllTime = "all_time"
|
||||||
|
AnalyticsFilterYear = "year"
|
||||||
|
AnalyticsFilterYearMonth = "year_month"
|
||||||
|
AnalyticsFilterCustom = "custom"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AnalyticsDateFilter describes the effective reporting window for dashboard analytics.
|
||||||
|
type AnalyticsDateFilter struct {
|
||||||
|
Mode string `json:"mode"`
|
||||||
|
Year *int `json:"year,omitempty"`
|
||||||
|
Month *int `json:"month,omitempty"`
|
||||||
|
From *time.Time `json:"from,omitempty"`
|
||||||
|
To *time.Time `json:"to,omitempty"`
|
||||||
|
RangeStart *time.Time `json:"range_start,omitempty"`
|
||||||
|
RangeEnd *time.Time `json:"range_end,omitempty"`
|
||||||
|
SeriesStart time.Time `json:"series_start"`
|
||||||
|
SeriesEnd time.Time `json:"series_end"`
|
||||||
|
RefDate time.Time `json:"ref_date"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseAnalyticsDateFilter resolves dashboard date filters from query params.
|
||||||
|
//
|
||||||
|
// Supported:
|
||||||
|
// - (none) => all-time totals; charts default to last 30 days
|
||||||
|
// - year=2025 => calendar year
|
||||||
|
// - year=2025&month=3 => calendar month
|
||||||
|
// - from=YYYY-MM-DD&to=YYYY-MM-DD => inclusive custom range (to required with from)
|
||||||
|
func ParseAnalyticsDateFilter(c *fiber.Ctx) (AnalyticsDateFilter, error) {
|
||||||
|
now := time.Now().UTC()
|
||||||
|
today := dateOnlyUTC(now)
|
||||||
|
|
||||||
|
fromRaw := strings.TrimSpace(c.Query("from"))
|
||||||
|
toRaw := strings.TrimSpace(c.Query("to"))
|
||||||
|
yearRaw := strings.TrimSpace(c.Query("year"))
|
||||||
|
monthRaw := strings.TrimSpace(c.Query("month"))
|
||||||
|
|
||||||
|
if fromRaw != "" || toRaw != "" {
|
||||||
|
if fromRaw == "" || toRaw == "" {
|
||||||
|
return AnalyticsDateFilter{}, fmt.Errorf("both from and to are required for a custom date range")
|
||||||
|
}
|
||||||
|
from, err := parseAnalyticsDate(fromRaw)
|
||||||
|
if err != nil {
|
||||||
|
return AnalyticsDateFilter{}, fmt.Errorf("invalid from date: %w", err)
|
||||||
|
}
|
||||||
|
to, err := parseAnalyticsDate(toRaw)
|
||||||
|
if err != nil {
|
||||||
|
return AnalyticsDateFilter{}, fmt.Errorf("invalid to date: %w", err)
|
||||||
|
}
|
||||||
|
if to.Before(from) {
|
||||||
|
return AnalyticsDateFilter{}, fmt.Errorf("to must be on or after from")
|
||||||
|
}
|
||||||
|
rangeStart := from
|
||||||
|
rangeEnd := to.AddDate(0, 0, 1)
|
||||||
|
ref := to
|
||||||
|
if today.Before(to) {
|
||||||
|
ref = today
|
||||||
|
}
|
||||||
|
return AnalyticsDateFilter{
|
||||||
|
Mode: AnalyticsFilterCustom,
|
||||||
|
From: &from,
|
||||||
|
To: &to,
|
||||||
|
RangeStart: &rangeStart,
|
||||||
|
RangeEnd: &rangeEnd,
|
||||||
|
SeriesStart: from,
|
||||||
|
SeriesEnd: to,
|
||||||
|
RefDate: ref,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if yearRaw != "" {
|
||||||
|
year, err := strconv.Atoi(yearRaw)
|
||||||
|
if err != nil || year < 2000 || year > 2100 {
|
||||||
|
return AnalyticsDateFilter{}, fmt.Errorf("year must be between 2000 and 2100")
|
||||||
|
}
|
||||||
|
|
||||||
|
if monthRaw == "" {
|
||||||
|
start := time.Date(year, time.January, 1, 0, 0, 0, 0, time.UTC)
|
||||||
|
endExclusive := start.AddDate(1, 0, 0)
|
||||||
|
lastDay := endExclusive.AddDate(0, 0, -1)
|
||||||
|
ref := lastDay
|
||||||
|
if today.Year() == year && !today.After(lastDay) {
|
||||||
|
ref = today
|
||||||
|
}
|
||||||
|
return AnalyticsDateFilter{
|
||||||
|
Mode: AnalyticsFilterYear,
|
||||||
|
Year: &year,
|
||||||
|
RangeStart: &start,
|
||||||
|
RangeEnd: &endExclusive,
|
||||||
|
SeriesStart: start,
|
||||||
|
SeriesEnd: lastDay,
|
||||||
|
RefDate: ref,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
month, err := strconv.Atoi(monthRaw)
|
||||||
|
if err != nil || month < 1 || month > 12 {
|
||||||
|
return AnalyticsDateFilter{}, fmt.Errorf("month must be between 1 and 12")
|
||||||
|
}
|
||||||
|
start := time.Date(year, time.Month(month), 1, 0, 0, 0, 0, time.UTC)
|
||||||
|
endExclusive := start.AddDate(0, 1, 0)
|
||||||
|
lastDay := endExclusive.AddDate(0, 0, -1)
|
||||||
|
ref := lastDay
|
||||||
|
if today.Year() == year && int(today.Month()) == month && !today.After(lastDay) {
|
||||||
|
ref = today
|
||||||
|
}
|
||||||
|
return AnalyticsDateFilter{
|
||||||
|
Mode: AnalyticsFilterYearMonth,
|
||||||
|
Year: &year,
|
||||||
|
Month: &month,
|
||||||
|
RangeStart: &start,
|
||||||
|
RangeEnd: &endExclusive,
|
||||||
|
SeriesStart: start,
|
||||||
|
SeriesEnd: lastDay,
|
||||||
|
RefDate: ref,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if monthRaw != "" {
|
||||||
|
return AnalyticsDateFilter{}, fmt.Errorf("month requires year")
|
||||||
|
}
|
||||||
|
|
||||||
|
seriesEnd := today
|
||||||
|
seriesStart := seriesEnd.AddDate(0, 0, -29)
|
||||||
|
return AnalyticsDateFilter{
|
||||||
|
Mode: AnalyticsFilterAllTime,
|
||||||
|
SeriesStart: seriesStart,
|
||||||
|
SeriesEnd: seriesEnd,
|
||||||
|
RefDate: today,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseAnalyticsDate(raw string) (time.Time, error) {
|
||||||
|
if t, err := time.Parse(time.RFC3339, raw); err == nil {
|
||||||
|
return dateOnlyUTC(t), nil
|
||||||
|
}
|
||||||
|
t, err := time.Parse("2006-01-02", raw)
|
||||||
|
if err != nil {
|
||||||
|
return time.Time{}, err
|
||||||
|
}
|
||||||
|
return dateOnlyUTC(t), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func dateOnlyUTC(t time.Time) time.Time {
|
||||||
|
t = t.UTC()
|
||||||
|
return time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, time.UTC)
|
||||||
|
}
|
||||||
78
internal/domain/analytics_filter_test.go
Normal file
78
internal/domain/analytics_filter_test.go
Normal file
|
|
@ -0,0 +1,78 @@
|
||||||
|
package domain
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestParseAnalyticsDateFilter_allTime(t *testing.T) {
|
||||||
|
app := fiber.New()
|
||||||
|
var got AnalyticsDateFilter
|
||||||
|
app.Get("/", func(c *fiber.Ctx) error {
|
||||||
|
var err error
|
||||||
|
got, err = ParseAnalyticsDateFilter(c)
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
|
||||||
|
req := httptest.NewRequest("GET", "/", nil)
|
||||||
|
if _, err := app.Test(req); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if got.Mode != AnalyticsFilterAllTime {
|
||||||
|
t.Fatalf("mode=%q", got.Mode)
|
||||||
|
}
|
||||||
|
if got.RangeStart != nil || got.RangeEnd != nil {
|
||||||
|
t.Fatal("expected no range bounds for all-time")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseAnalyticsDateFilter_yearMonth(t *testing.T) {
|
||||||
|
app := fiber.New()
|
||||||
|
var got AnalyticsDateFilter
|
||||||
|
app.Get("/", func(c *fiber.Ctx) error {
|
||||||
|
var err error
|
||||||
|
got, err = ParseAnalyticsDateFilter(c)
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
|
||||||
|
req := httptest.NewRequest("GET", "/?year=2025&month=3", nil)
|
||||||
|
if _, err := app.Test(req); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if got.Mode != AnalyticsFilterYearMonth {
|
||||||
|
t.Fatalf("mode=%q", got.Mode)
|
||||||
|
}
|
||||||
|
if got.RangeStart == nil || got.RangeEnd == nil {
|
||||||
|
t.Fatal("expected range bounds")
|
||||||
|
}
|
||||||
|
wantStart := time.Date(2025, time.March, 1, 0, 0, 0, 0, time.UTC)
|
||||||
|
wantEnd := time.Date(2025, time.April, 1, 0, 0, 0, 0, time.UTC)
|
||||||
|
if !got.RangeStart.Equal(wantStart) || !got.RangeEnd.Equal(wantEnd) {
|
||||||
|
t.Fatalf("range=%v..%v", got.RangeStart, got.RangeEnd)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseAnalyticsDateFilter_custom(t *testing.T) {
|
||||||
|
app := fiber.New()
|
||||||
|
var got AnalyticsDateFilter
|
||||||
|
app.Get("/", func(c *fiber.Ctx) error {
|
||||||
|
var err error
|
||||||
|
got, err = ParseAnalyticsDateFilter(c)
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
|
||||||
|
req := httptest.NewRequest("GET", "/?from=2025-01-10&to=2025-01-20", nil)
|
||||||
|
if _, err := app.Test(req); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if got.Mode != AnalyticsFilterCustom {
|
||||||
|
t.Fatalf("mode=%q", got.Mode)
|
||||||
|
}
|
||||||
|
wantEnd := time.Date(2025, time.January, 21, 0, 0, 0, 0, time.UTC)
|
||||||
|
if got.RangeEnd == nil || !got.RangeEnd.Equal(wantEnd) {
|
||||||
|
t.Fatalf("range_end=%v", got.RangeEnd)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -15,141 +15,155 @@ func toTime(v interface{}) time.Time {
|
||||||
return time.Time{}
|
return time.Time{}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetAnalyticsDashboard godoc
|
||||||
|
// @Summary Analytics dashboard
|
||||||
|
// @Description Platform analytics with optional date filters: all-time (default), year, year+month, or custom from/to range.
|
||||||
|
// @Tags analytics
|
||||||
|
// @Produce json
|
||||||
|
// @Param year query int false "Calendar year (e.g. 2025)"
|
||||||
|
// @Param month query int false "Calendar month 1-12 (requires year)"
|
||||||
|
// @Param from query string false "Custom range start (YYYY-MM-DD or RFC3339)"
|
||||||
|
// @Param to query string false "Custom range end (YYYY-MM-DD or RFC3339, inclusive)"
|
||||||
|
// @Success 200 {object} domain.AnalyticsDashboard
|
||||||
|
// @Failure 400 {object} domain.ErrorResponse
|
||||||
|
// @Failure 500 {object} domain.ErrorResponse
|
||||||
|
// @Router /api/v1/analytics/dashboard [get]
|
||||||
func (h *Handler) GetAnalyticsDashboard(c *fiber.Ctx) error {
|
func (h *Handler) GetAnalyticsDashboard(c *fiber.Ctx) error {
|
||||||
ctx := c.Context()
|
filter, err := domain.ParseAnalyticsDateFilter(c)
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||||
|
Message: "Invalid date filter",
|
||||||
|
Error: err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// ── Users ──
|
ctx := c.Context()
|
||||||
usersSummary, err := h.analyticsDB.AnalyticsUsersSummary(ctx)
|
p := newAnalyticsQueryParams(filter)
|
||||||
|
|
||||||
|
usersSummary, err := h.analyticsDB.AnalyticsUsersSummary(ctx, p.UsersSummary)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch user analytics")
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch user analytics")
|
||||||
}
|
}
|
||||||
usersByRole, err := h.analyticsDB.AnalyticsUsersByRole(ctx)
|
usersByRole, err := h.analyticsDB.AnalyticsUsersByRole(ctx, p.UsersByRole)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch users by role")
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch users by role")
|
||||||
}
|
}
|
||||||
usersByStatus, err := h.analyticsDB.AnalyticsUsersByStatus(ctx)
|
usersByStatus, err := h.analyticsDB.AnalyticsUsersByStatus(ctx, p.UsersByStatus)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch users by status")
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch users by status")
|
||||||
}
|
}
|
||||||
usersByAge, err := h.analyticsDB.AnalyticsUsersByAgeGroup(ctx)
|
usersByAge, err := h.analyticsDB.AnalyticsUsersByAgeGroup(ctx, p.UsersByAgeGroup)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch users by age group")
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch users by age group")
|
||||||
}
|
}
|
||||||
usersByKnowledge, err := h.analyticsDB.AnalyticsUsersByKnowledgeLevel(ctx)
|
usersByKnowledge, err := h.analyticsDB.AnalyticsUsersByKnowledgeLevel(ctx, p.UsersByKnowledgeLevel)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch users by knowledge level")
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch users by knowledge level")
|
||||||
}
|
}
|
||||||
usersByRegion, err := h.analyticsDB.AnalyticsUsersByRegion(ctx)
|
usersByRegion, err := h.analyticsDB.AnalyticsUsersByRegion(ctx, p.UsersByRegion)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch users by region")
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch users by region")
|
||||||
}
|
}
|
||||||
userRegs, err := h.analyticsDB.AnalyticsUserRegistrationsLast30Days(ctx)
|
userRegs, err := h.analyticsDB.AnalyticsUserRegistrationsLast30Days(ctx, p.UserRegistrationsSeries)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch user registrations")
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch user registrations")
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Subscriptions ──
|
subsSummary, err := h.analyticsDB.AnalyticsSubscriptionsSummary(ctx, p.SubscriptionsSummary)
|
||||||
subsSummary, err := h.analyticsDB.AnalyticsSubscriptionsSummary(ctx)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch subscription analytics")
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch subscription analytics")
|
||||||
}
|
}
|
||||||
subsByStatus, err := h.analyticsDB.AnalyticsSubscriptionsByStatus(ctx)
|
subsByStatus, err := h.analyticsDB.AnalyticsSubscriptionsByStatus(ctx, p.SubscriptionsByStatus)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch subscriptions by status")
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch subscriptions by status")
|
||||||
}
|
}
|
||||||
revenueByPlan, err := h.analyticsDB.AnalyticsRevenueByPlan(ctx)
|
revenueByPlan, err := h.analyticsDB.AnalyticsRevenueByPlan(ctx, p.RevenueByPlan)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch revenue by plan")
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch revenue by plan")
|
||||||
}
|
}
|
||||||
newSubs30, err := h.analyticsDB.AnalyticsNewSubscriptionsLast30Days(ctx)
|
newSubs30, err := h.analyticsDB.AnalyticsNewSubscriptionsLast30Days(ctx, p.NewSubscriptionsSeries)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch new subscriptions last 30 days")
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch new subscriptions time series")
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Payments ──
|
paymentsSummary, err := h.analyticsDB.AnalyticsPaymentsSummary(ctx, p.PaymentsSummary)
|
||||||
paymentsSummary, err := h.analyticsDB.AnalyticsPaymentsSummary(ctx)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch payment analytics")
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch payment analytics")
|
||||||
}
|
}
|
||||||
paymentsByStatus, err := h.analyticsDB.AnalyticsPaymentsByStatus(ctx)
|
paymentsByStatus, err := h.analyticsDB.AnalyticsPaymentsByStatus(ctx, p.PaymentsByStatus)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch payments by status")
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch payments by status")
|
||||||
}
|
}
|
||||||
paymentsByMethod, err := h.analyticsDB.AnalyticsPaymentsByMethod(ctx)
|
paymentsByMethod, err := h.analyticsDB.AnalyticsPaymentsByMethod(ctx, p.PaymentsByMethod)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch payments by method")
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch payments by method")
|
||||||
}
|
}
|
||||||
revenue30, err := h.analyticsDB.AnalyticsRevenueLast30Days(ctx)
|
revenue30, err := h.analyticsDB.AnalyticsRevenueLast30Days(ctx, p.RevenueSeries)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch revenue last 30 days")
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch revenue time series")
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Courses ──
|
|
||||||
courseCounts, err := h.analyticsDB.AnalyticsCourseCounts(ctx)
|
courseCounts, err := h.analyticsDB.AnalyticsCourseCounts(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch course analytics")
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch course analytics")
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Content ──
|
questionsCounts, err := h.analyticsDB.AnalyticsQuestionsCounts(ctx, p.QuestionsCounts)
|
||||||
questionsCounts, err := h.analyticsDB.AnalyticsQuestionsCounts(ctx)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch content analytics")
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch content analytics")
|
||||||
}
|
}
|
||||||
questionsByType, err := h.analyticsDB.AnalyticsQuestionsByType(ctx)
|
questionsByType, err := h.analyticsDB.AnalyticsQuestionsByType(ctx, p.QuestionsByType)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch questions by type")
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch questions by type")
|
||||||
}
|
}
|
||||||
questionSetsByType, err := h.analyticsDB.AnalyticsQuestionSetsByType(ctx)
|
questionSetsByType, err := h.analyticsDB.AnalyticsQuestionSetsByType(ctx, p.QuestionSetsByType)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch question sets by type")
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch question sets by type")
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Notifications ──
|
notifSummary, err := h.analyticsDB.AnalyticsNotificationsSummary(ctx, p.NotificationsSummary)
|
||||||
notifSummary, err := h.analyticsDB.AnalyticsNotificationsSummary(ctx)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch notification analytics")
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch notification analytics")
|
||||||
}
|
}
|
||||||
notifByChannel, err := h.analyticsDB.AnalyticsNotificationsByChannel(ctx)
|
notifByChannel, err := h.analyticsDB.AnalyticsNotificationsByChannel(ctx, p.NotificationsChannel)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch notifications by channel")
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch notifications by channel")
|
||||||
}
|
}
|
||||||
notifByType, err := h.analyticsDB.AnalyticsNotificationsByType(ctx)
|
notifByType, err := h.analyticsDB.AnalyticsNotificationsByType(ctx, p.NotificationsType)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch notifications by type")
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch notifications by type")
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Issues ──
|
issuesSummary, err := h.analyticsDB.AnalyticsIssuesSummary(ctx, p.IssuesSummary)
|
||||||
issuesSummary, err := h.analyticsDB.AnalyticsIssuesSummary(ctx)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch issue analytics")
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch issue analytics")
|
||||||
}
|
}
|
||||||
issuesByStatus, err := h.analyticsDB.AnalyticsIssuesByStatus(ctx)
|
issuesByStatus, err := h.analyticsDB.AnalyticsIssuesByStatus(ctx, p.IssuesByStatus)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch issues by status")
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch issues by status")
|
||||||
}
|
}
|
||||||
issuesByType, err := h.analyticsDB.AnalyticsIssuesByType(ctx)
|
issuesByType, err := h.analyticsDB.AnalyticsIssuesByType(ctx, p.IssuesByType)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch issues by type")
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch issues by type")
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Team ──
|
teamTotal, err := h.analyticsDB.AnalyticsTeamSummary(ctx, p.TeamSummary)
|
||||||
teamTotal, err := h.analyticsDB.AnalyticsTeamSummary(ctx)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch team analytics")
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch team analytics")
|
||||||
}
|
}
|
||||||
teamByRole, err := h.analyticsDB.AnalyticsTeamByRole(ctx)
|
teamByRole, err := h.analyticsDB.AnalyticsTeamByRole(ctx, p.TeamByRole)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch team by role")
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch team by role")
|
||||||
}
|
}
|
||||||
teamByStatus, err := h.analyticsDB.AnalyticsTeamByStatus(ctx)
|
teamByStatus, err := h.analyticsDB.AnalyticsTeamByStatus(ctx, p.TeamByStatus)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch team by status")
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch team by status")
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Map to domain types ──
|
|
||||||
dashboard := domain.AnalyticsDashboard{
|
dashboard := domain.AnalyticsDashboard{
|
||||||
GeneratedAt: time.Now(),
|
GeneratedAt: time.Now().UTC(),
|
||||||
Users: mapUsersSection(usersSummary, usersByRole, usersByStatus, usersByAge, usersByKnowledge, usersByRegion, userRegs),
|
DateFilter: filter,
|
||||||
|
Users: mapUsersSection(usersSummary, usersByRole, usersByStatus, usersByAge, usersByKnowledge, usersByRegion, userRegs),
|
||||||
Subscriptions: mapSubscriptionsSection(subsSummary, subsByStatus, revenueByPlan, newSubs30),
|
Subscriptions: mapSubscriptionsSection(subsSummary, subsByStatus, revenueByPlan, newSubs30),
|
||||||
Payments: mapPaymentsSection(paymentsSummary, paymentsByStatus, paymentsByMethod, revenue30),
|
Payments: mapPaymentsSection(paymentsSummary, paymentsByStatus, paymentsByMethod, revenue30),
|
||||||
Courses: domain.AnalyticsCoursesSection{
|
Courses: domain.AnalyticsCoursesSection{
|
||||||
|
|
|
||||||
113
internal/web_server/handlers/analytics_params.go
Normal file
113
internal/web_server/handlers/analytics_params.go
Normal file
|
|
@ -0,0 +1,113 @@
|
||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
dbgen "Yimaru-Backend/gen/db"
|
||||||
|
"Yimaru-Backend/internal/domain"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/jackc/pgx/v5/pgtype"
|
||||||
|
)
|
||||||
|
|
||||||
|
type analyticsQueryParams struct {
|
||||||
|
UsersSummary dbgen.AnalyticsUsersSummaryParams
|
||||||
|
UsersByRole dbgen.AnalyticsUsersByRoleParams
|
||||||
|
UsersByStatus dbgen.AnalyticsUsersByStatusParams
|
||||||
|
UsersByAgeGroup dbgen.AnalyticsUsersByAgeGroupParams
|
||||||
|
UsersByKnowledgeLevel dbgen.AnalyticsUsersByKnowledgeLevelParams
|
||||||
|
UsersByRegion dbgen.AnalyticsUsersByRegionParams
|
||||||
|
UserRegistrationsSeries dbgen.AnalyticsUserRegistrationsLast30DaysParams
|
||||||
|
|
||||||
|
SubscriptionsSummary dbgen.AnalyticsSubscriptionsSummaryParams
|
||||||
|
SubscriptionsByStatus dbgen.AnalyticsSubscriptionsByStatusParams
|
||||||
|
RevenueByPlan dbgen.AnalyticsRevenueByPlanParams
|
||||||
|
NewSubscriptionsSeries dbgen.AnalyticsNewSubscriptionsLast30DaysParams
|
||||||
|
|
||||||
|
PaymentsSummary dbgen.AnalyticsPaymentsSummaryParams
|
||||||
|
PaymentsByStatus dbgen.AnalyticsPaymentsByStatusParams
|
||||||
|
PaymentsByMethod dbgen.AnalyticsPaymentsByMethodParams
|
||||||
|
RevenueSeries dbgen.AnalyticsRevenueLast30DaysParams
|
||||||
|
|
||||||
|
QuestionsCounts dbgen.AnalyticsQuestionsCountsParams
|
||||||
|
QuestionsByType dbgen.AnalyticsQuestionsByTypeParams
|
||||||
|
QuestionSetsByType dbgen.AnalyticsQuestionSetsByTypeParams
|
||||||
|
|
||||||
|
NotificationsSummary dbgen.AnalyticsNotificationsSummaryParams
|
||||||
|
NotificationsChannel dbgen.AnalyticsNotificationsByChannelParams
|
||||||
|
NotificationsType dbgen.AnalyticsNotificationsByTypeParams
|
||||||
|
|
||||||
|
IssuesSummary dbgen.AnalyticsIssuesSummaryParams
|
||||||
|
IssuesByStatus dbgen.AnalyticsIssuesByStatusParams
|
||||||
|
IssuesByType dbgen.AnalyticsIssuesByTypeParams
|
||||||
|
|
||||||
|
TeamSummary dbgen.AnalyticsTeamSummaryParams
|
||||||
|
TeamByRole dbgen.AnalyticsTeamByRoleParams
|
||||||
|
TeamByStatus dbgen.AnalyticsTeamByStatusParams
|
||||||
|
}
|
||||||
|
|
||||||
|
func newAnalyticsQueryParams(f domain.AnalyticsDateFilter) analyticsQueryParams {
|
||||||
|
ref := pgAnalyticsDate(f.RefDate)
|
||||||
|
rs, re := pgAnalyticsTimestamptzPtr(f.RangeStart), pgAnalyticsTimestamptzPtr(f.RangeEnd)
|
||||||
|
series := dbgen.AnalyticsUserRegistrationsLast30DaysParams{
|
||||||
|
SeriesStart: pgAnalyticsDate(f.SeriesStart),
|
||||||
|
SeriesEnd: pgAnalyticsDate(f.SeriesEnd),
|
||||||
|
RangeStart: rs,
|
||||||
|
RangeEnd: re,
|
||||||
|
}
|
||||||
|
|
||||||
|
return analyticsQueryParams{
|
||||||
|
UsersSummary: dbgen.AnalyticsUsersSummaryParams{RefDate: ref, RangeStart: rs, RangeEnd: re},
|
||||||
|
UsersByRole: dbgen.AnalyticsUsersByRoleParams{RangeStart: rs, RangeEnd: re},
|
||||||
|
UsersByStatus: dbgen.AnalyticsUsersByStatusParams{RangeStart: rs, RangeEnd: re},
|
||||||
|
UsersByAgeGroup: dbgen.AnalyticsUsersByAgeGroupParams{RangeStart: rs, RangeEnd: re},
|
||||||
|
UsersByKnowledgeLevel: dbgen.AnalyticsUsersByKnowledgeLevelParams{RangeStart: rs, RangeEnd: re},
|
||||||
|
UsersByRegion: dbgen.AnalyticsUsersByRegionParams{RangeStart: rs, RangeEnd: re},
|
||||||
|
UserRegistrationsSeries: series,
|
||||||
|
|
||||||
|
SubscriptionsSummary: dbgen.AnalyticsSubscriptionsSummaryParams{RefDate: ref, RangeStart: rs, RangeEnd: re},
|
||||||
|
SubscriptionsByStatus: dbgen.AnalyticsSubscriptionsByStatusParams{RangeStart: rs, RangeEnd: re},
|
||||||
|
RevenueByPlan: dbgen.AnalyticsRevenueByPlanParams{RangeStart: rs, RangeEnd: re},
|
||||||
|
NewSubscriptionsSeries: dbgen.AnalyticsNewSubscriptionsLast30DaysParams{
|
||||||
|
SeriesStart: series.SeriesStart,
|
||||||
|
SeriesEnd: series.SeriesEnd,
|
||||||
|
RangeStart: rs,
|
||||||
|
RangeEnd: re,
|
||||||
|
},
|
||||||
|
|
||||||
|
PaymentsSummary: dbgen.AnalyticsPaymentsSummaryParams{RangeStart: rs, RangeEnd: re},
|
||||||
|
PaymentsByStatus: dbgen.AnalyticsPaymentsByStatusParams{RangeStart: rs, RangeEnd: re},
|
||||||
|
PaymentsByMethod: dbgen.AnalyticsPaymentsByMethodParams{RangeStart: rs, RangeEnd: re},
|
||||||
|
RevenueSeries: dbgen.AnalyticsRevenueLast30DaysParams{
|
||||||
|
SeriesStart: series.SeriesStart,
|
||||||
|
SeriesEnd: series.SeriesEnd,
|
||||||
|
RangeStart: rs,
|
||||||
|
RangeEnd: re,
|
||||||
|
},
|
||||||
|
|
||||||
|
QuestionsCounts: dbgen.AnalyticsQuestionsCountsParams{RangeStart: rs, RangeEnd: re},
|
||||||
|
QuestionsByType: dbgen.AnalyticsQuestionsByTypeParams{RangeStart: rs, RangeEnd: re},
|
||||||
|
QuestionSetsByType: dbgen.AnalyticsQuestionSetsByTypeParams{RangeStart: rs, RangeEnd: re},
|
||||||
|
|
||||||
|
NotificationsSummary: dbgen.AnalyticsNotificationsSummaryParams{RangeStart: rs, RangeEnd: re},
|
||||||
|
NotificationsChannel: dbgen.AnalyticsNotificationsByChannelParams{RangeStart: rs, RangeEnd: re},
|
||||||
|
NotificationsType: dbgen.AnalyticsNotificationsByTypeParams{RangeStart: rs, RangeEnd: re},
|
||||||
|
|
||||||
|
IssuesSummary: dbgen.AnalyticsIssuesSummaryParams{RangeStart: rs, RangeEnd: re},
|
||||||
|
IssuesByStatus: dbgen.AnalyticsIssuesByStatusParams{RangeStart: rs, RangeEnd: re},
|
||||||
|
IssuesByType: dbgen.AnalyticsIssuesByTypeParams{RangeStart: rs, RangeEnd: re},
|
||||||
|
|
||||||
|
TeamSummary: dbgen.AnalyticsTeamSummaryParams{RangeStart: rs, RangeEnd: re},
|
||||||
|
TeamByRole: dbgen.AnalyticsTeamByRoleParams{RangeStart: rs, RangeEnd: re},
|
||||||
|
TeamByStatus: dbgen.AnalyticsTeamByStatusParams{RangeStart: rs, RangeEnd: re},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func pgAnalyticsDate(t time.Time) pgtype.Date {
|
||||||
|
return pgtype.Date{Time: t.UTC(), Valid: true}
|
||||||
|
}
|
||||||
|
|
||||||
|
func pgAnalyticsTimestamptzPtr(t *time.Time) pgtype.Timestamptz {
|
||||||
|
if t == nil {
|
||||||
|
return pgtype.Timestamptz{Valid: false}
|
||||||
|
}
|
||||||
|
return pgtype.Timestamptz{Time: t.UTC(), Valid: true}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user