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
|
||||
|
|
@ -9,49 +15,67 @@
|
|||
-- name: AnalyticsUsersSummary :one
|
||||
SELECT
|
||||
COUNT(*)::bigint AS total,
|
||||
COUNT(*) FILTER (WHERE created_at >= CURRENT_DATE)::bigint AS new_today,
|
||||
COUNT(*) FILTER (WHERE created_at >= CURRENT_DATE - INTERVAL '7 days')::bigint AS new_this_week,
|
||||
COUNT(*) FILTER (WHERE created_at >= CURRENT_DATE - INTERVAL '30 days')::bigint AS new_this_month
|
||||
FROM users;
|
||||
COUNT(*) FILTER (WHERE u.created_at::date = sqlc.arg('ref_date')::date)::bigint AS new_today,
|
||||
COUNT(*) FILTER (
|
||||
WHERE u.created_at::date >= sqlc.arg('ref_date')::date - INTERVAL '6 days'
|
||||
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
|
||||
SELECT
|
||||
COALESCE(role, 'unknown') AS role,
|
||||
COALESCE(u.role, 'unknown') AS role,
|
||||
COUNT(*)::bigint AS count
|
||||
FROM users
|
||||
GROUP BY role
|
||||
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)
|
||||
GROUP BY u.role
|
||||
ORDER BY count DESC;
|
||||
|
||||
-- name: AnalyticsUsersByStatus :many
|
||||
SELECT
|
||||
COALESCE(status, 'unknown') AS status,
|
||||
COALESCE(u.status, 'unknown') AS status,
|
||||
COUNT(*)::bigint AS count
|
||||
FROM users
|
||||
GROUP BY status
|
||||
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)
|
||||
GROUP BY u.status
|
||||
ORDER BY count DESC;
|
||||
|
||||
-- name: AnalyticsUsersByAgeGroup :many
|
||||
SELECT
|
||||
COALESCE(age_group, 'unknown') AS age_group,
|
||||
COALESCE(u.age_group, 'unknown') AS age_group,
|
||||
COUNT(*)::bigint AS count
|
||||
FROM users
|
||||
GROUP BY age_group
|
||||
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)
|
||||
GROUP BY u.age_group
|
||||
ORDER BY count DESC;
|
||||
|
||||
-- name: AnalyticsUsersByKnowledgeLevel :many
|
||||
SELECT
|
||||
COALESCE(knowledge_level, 'unknown') AS knowledge_level,
|
||||
COALESCE(u.knowledge_level, 'unknown') AS knowledge_level,
|
||||
COUNT(*)::bigint AS count
|
||||
FROM users
|
||||
GROUP BY knowledge_level
|
||||
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)
|
||||
GROUP BY u.knowledge_level
|
||||
ORDER BY count DESC;
|
||||
|
||||
-- name: AnalyticsUsersByRegion :many
|
||||
SELECT
|
||||
COALESCE(region, 'unknown') AS region,
|
||||
COALESCE(u.region, 'unknown') AS region,
|
||||
COUNT(*)::bigint AS count
|
||||
FROM users
|
||||
GROUP BY region
|
||||
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)
|
||||
GROUP BY u.region
|
||||
ORDER BY count DESC;
|
||||
|
||||
-- name: AnalyticsUserRegistrationsLast30Days :many
|
||||
|
|
@ -59,11 +83,13 @@ SELECT
|
|||
d.date,
|
||||
COUNT(u.id)::bigint AS count
|
||||
FROM generate_series(
|
||||
CURRENT_DATE - INTERVAL '29 days',
|
||||
CURRENT_DATE,
|
||||
sqlc.arg('series_start')::date,
|
||||
sqlc.arg('series_end')::date,
|
||||
INTERVAL '1 day'
|
||||
) AS 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
|
||||
ORDER BY d.date;
|
||||
|
||||
|
|
@ -74,18 +100,28 @@ ORDER BY d.date;
|
|||
-- name: AnalyticsSubscriptionsSummary :one
|
||||
SELECT
|
||||
COUNT(*)::bigint AS total,
|
||||
COUNT(*) FILTER (WHERE status = 'ACTIVE')::bigint AS active,
|
||||
COUNT(*) FILTER (WHERE created_at >= CURRENT_DATE)::bigint AS new_today,
|
||||
COUNT(*) FILTER (WHERE created_at >= CURRENT_DATE - INTERVAL '7 days')::bigint AS new_this_week,
|
||||
COUNT(*) FILTER (WHERE created_at >= CURRENT_DATE - INTERVAL '30 days')::bigint AS new_this_month
|
||||
FROM user_subscriptions;
|
||||
COUNT(*) FILTER (WHERE us.status = 'ACTIVE')::bigint AS active,
|
||||
COUNT(*) FILTER (WHERE us.created_at::date = sqlc.arg('ref_date')::date)::bigint AS new_today,
|
||||
COUNT(*) FILTER (
|
||||
WHERE us.created_at::date >= sqlc.arg('ref_date')::date - INTERVAL '6 days'
|
||||
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
|
||||
SELECT
|
||||
COALESCE(status, 'unknown') AS status,
|
||||
COALESCE(us.status, 'unknown') AS status,
|
||||
COUNT(*)::bigint AS count
|
||||
FROM user_subscriptions
|
||||
GROUP BY status
|
||||
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)
|
||||
GROUP BY us.status
|
||||
ORDER BY count DESC;
|
||||
|
||||
-- name: AnalyticsRevenueByPlan :many
|
||||
|
|
@ -97,6 +133,8 @@ SELECT
|
|||
FROM payments p
|
||||
JOIN subscription_plans sp ON sp.id = p.plan_id
|
||||
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
|
||||
ORDER BY total_revenue DESC;
|
||||
|
||||
|
|
@ -105,11 +143,13 @@ SELECT
|
|||
d.date,
|
||||
COUNT(us.id)::bigint AS count
|
||||
FROM generate_series(
|
||||
CURRENT_DATE - INTERVAL '29 days',
|
||||
CURRENT_DATE,
|
||||
sqlc.arg('series_start')::date,
|
||||
sqlc.arg('series_end')::date,
|
||||
INTERVAL '1 day'
|
||||
) AS 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
|
||||
ORDER BY d.date;
|
||||
|
||||
|
|
@ -119,29 +159,35 @@ ORDER BY d.date;
|
|||
|
||||
-- name: AnalyticsPaymentsSummary :one
|
||||
SELECT
|
||||
COALESCE(SUM(amount) FILTER (WHERE status = 'SUCCESS'), 0)::float8 AS total_revenue,
|
||||
COALESCE(AVG(amount) FILTER (WHERE status = 'SUCCESS'), 0)::float8 AS avg_value,
|
||||
COALESCE(SUM(p.amount) FILTER (WHERE p.status = 'SUCCESS'), 0)::float8 AS total_revenue,
|
||||
COALESCE(AVG(p.amount) FILTER (WHERE p.status = 'SUCCESS'), 0)::float8 AS avg_value,
|
||||
COUNT(*)::bigint AS total_payments,
|
||||
COUNT(*) FILTER (WHERE status = 'SUCCESS')::bigint AS successful_payments
|
||||
FROM payments;
|
||||
COUNT(*) FILTER (WHERE p.status = 'SUCCESS')::bigint AS successful_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
|
||||
SELECT
|
||||
COALESCE(status, 'unknown') AS status,
|
||||
COALESCE(p.status, 'unknown') AS status,
|
||||
COUNT(*)::bigint AS count,
|
||||
COALESCE(SUM(amount), 0)::float8 AS total_amount
|
||||
FROM payments
|
||||
GROUP BY status
|
||||
COALESCE(SUM(p.amount), 0)::float8 AS total_amount
|
||||
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)
|
||||
GROUP BY p.status
|
||||
ORDER BY count DESC;
|
||||
|
||||
-- name: AnalyticsPaymentsByMethod :many
|
||||
SELECT
|
||||
COALESCE(payment_method, 'unknown') AS payment_method,
|
||||
COALESCE(p.payment_method, 'unknown') AS payment_method,
|
||||
COUNT(*)::bigint AS count,
|
||||
COALESCE(SUM(amount), 0)::float8 AS total_amount
|
||||
FROM payments
|
||||
WHERE status = 'SUCCESS'
|
||||
GROUP BY payment_method
|
||||
COALESCE(SUM(p.amount), 0)::float8 AS total_amount
|
||||
FROM payments p
|
||||
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 p.payment_method
|
||||
ORDER BY count DESC;
|
||||
|
||||
-- name: AnalyticsRevenueLast30Days :many
|
||||
|
|
@ -149,11 +195,14 @@ SELECT
|
|||
d.date,
|
||||
COALESCE(SUM(p.amount), 0)::float8 AS total_revenue
|
||||
FROM generate_series(
|
||||
CURRENT_DATE - INTERVAL '29 days',
|
||||
CURRENT_DATE,
|
||||
sqlc.arg('series_start')::date,
|
||||
sqlc.arg('series_end')::date,
|
||||
INTERVAL '1 day'
|
||||
) 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
|
||||
ORDER BY d.date;
|
||||
|
||||
|
|
@ -174,23 +223,37 @@ SELECT
|
|||
|
||||
-- name: AnalyticsQuestionsCounts :one
|
||||
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
|
||||
SELECT
|
||||
COALESCE(question_type, 'unknown') AS question_type,
|
||||
COALESCE(q.question_type, 'unknown') AS question_type,
|
||||
COUNT(*)::bigint AS count
|
||||
FROM questions
|
||||
GROUP BY question_type
|
||||
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)
|
||||
GROUP BY q.question_type
|
||||
ORDER BY count DESC;
|
||||
|
||||
-- name: AnalyticsQuestionSetsByType :many
|
||||
SELECT
|
||||
COALESCE(set_type, 'unknown') AS set_type,
|
||||
COALESCE(qs.set_type, 'unknown') AS set_type,
|
||||
COUNT(*)::bigint AS count
|
||||
FROM question_sets
|
||||
GROUP BY set_type
|
||||
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)
|
||||
GROUP BY qs.set_type
|
||||
ORDER BY count DESC;
|
||||
|
||||
-- =====================
|
||||
|
|
@ -200,24 +263,30 @@ ORDER BY count DESC;
|
|||
-- name: AnalyticsNotificationsSummary :one
|
||||
SELECT
|
||||
COUNT(*)::bigint AS total,
|
||||
COUNT(*) FILTER (WHERE is_read = TRUE)::bigint AS read,
|
||||
COUNT(*) FILTER (WHERE is_read = FALSE)::bigint AS unread
|
||||
FROM notifications;
|
||||
COUNT(*) FILTER (WHERE n.is_read = TRUE)::bigint AS read,
|
||||
COUNT(*) FILTER (WHERE n.is_read = FALSE)::bigint AS unread
|
||||
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
|
||||
SELECT
|
||||
COALESCE(channel, 'unknown') AS channel,
|
||||
COALESCE(n.channel, 'unknown') AS channel,
|
||||
COUNT(*)::bigint AS count
|
||||
FROM notifications
|
||||
GROUP BY channel
|
||||
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)
|
||||
GROUP BY n.channel
|
||||
ORDER BY count DESC;
|
||||
|
||||
-- name: AnalyticsNotificationsByType :many
|
||||
SELECT
|
||||
COALESCE(type, 'unknown') AS type,
|
||||
COALESCE(n.type, 'unknown') AS type,
|
||||
COUNT(*)::bigint AS count
|
||||
FROM notifications
|
||||
GROUP BY type
|
||||
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)
|
||||
GROUP BY n.type
|
||||
ORDER BY count DESC;
|
||||
|
||||
-- =====================
|
||||
|
|
@ -227,27 +296,33 @@ ORDER BY count DESC;
|
|||
-- name: AnalyticsIssuesSummary :one
|
||||
SELECT
|
||||
COUNT(*)::bigint AS total,
|
||||
COUNT(*) FILTER (WHERE status = 'resolved')::bigint AS resolved,
|
||||
COUNT(*) FILTER (WHERE ri.status = 'resolved')::bigint AS resolved,
|
||||
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
|
||||
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
|
||||
SELECT
|
||||
COALESCE(status, 'unknown') AS status,
|
||||
COALESCE(ri.status, 'unknown') AS status,
|
||||
COUNT(*)::bigint AS count
|
||||
FROM reported_issues
|
||||
GROUP BY status
|
||||
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)
|
||||
GROUP BY ri.status
|
||||
ORDER BY count DESC;
|
||||
|
||||
-- name: AnalyticsIssuesByType :many
|
||||
SELECT
|
||||
COALESCE(issue_type, 'unknown') AS issue_type,
|
||||
COALESCE(ri.issue_type, 'unknown') AS issue_type,
|
||||
COUNT(*)::bigint AS count
|
||||
FROM reported_issues
|
||||
GROUP BY issue_type
|
||||
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)
|
||||
GROUP BY ri.issue_type
|
||||
ORDER BY count DESC;
|
||||
|
||||
-- =====================
|
||||
|
|
@ -257,20 +332,26 @@ ORDER BY count DESC;
|
|||
-- name: AnalyticsTeamSummary :one
|
||||
SELECT
|
||||
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
|
||||
SELECT
|
||||
COALESCE(team_role, 'unknown') AS team_role,
|
||||
COALESCE(tm.team_role, 'unknown') AS team_role,
|
||||
COUNT(*)::bigint AS count
|
||||
FROM team_members
|
||||
GROUP BY team_role
|
||||
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)
|
||||
GROUP BY tm.team_role
|
||||
ORDER BY count DESC;
|
||||
|
||||
-- name: AnalyticsTeamByStatus :many
|
||||
SELECT
|
||||
COALESCE(status, 'unknown') AS status,
|
||||
COALESCE(tm.status, 'unknown') AS status,
|
||||
COUNT(*)::bigint AS count
|
||||
FROM team_members
|
||||
GROUP BY status
|
||||
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)
|
||||
GROUP BY tm.status
|
||||
ORDER BY count DESC;
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@ package dbgen
|
|||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
)
|
||||
|
||||
const AnalyticsCourseCounts = `-- name: AnalyticsCourseCounts :one
|
||||
|
|
@ -42,20 +44,27 @@ func (q *Queries) AnalyticsCourseCounts(ctx context.Context) (AnalyticsCourseCou
|
|||
|
||||
const AnalyticsIssuesByStatus = `-- name: AnalyticsIssuesByStatus :many
|
||||
SELECT
|
||||
COALESCE(status, 'unknown') AS status,
|
||||
COALESCE(ri.status, 'unknown') AS status,
|
||||
COUNT(*)::bigint AS count
|
||||
FROM reported_issues
|
||||
GROUP BY status
|
||||
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)
|
||||
GROUP BY ri.status
|
||||
ORDER BY count DESC
|
||||
`
|
||||
|
||||
type AnalyticsIssuesByStatusParams struct {
|
||||
RangeStart pgtype.Timestamptz `json:"range_start"`
|
||||
RangeEnd pgtype.Timestamptz `json:"range_end"`
|
||||
}
|
||||
|
||||
type AnalyticsIssuesByStatusRow struct {
|
||||
Status string `json:"status"`
|
||||
Count int64 `json:"count"`
|
||||
}
|
||||
|
||||
func (q *Queries) AnalyticsIssuesByStatus(ctx context.Context) ([]AnalyticsIssuesByStatusRow, error) {
|
||||
rows, err := q.db.Query(ctx, AnalyticsIssuesByStatus)
|
||||
func (q *Queries) AnalyticsIssuesByStatus(ctx context.Context, arg AnalyticsIssuesByStatusParams) ([]AnalyticsIssuesByStatusRow, error) {
|
||||
rows, err := q.db.Query(ctx, AnalyticsIssuesByStatus, arg.RangeStart, arg.RangeEnd)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -76,20 +85,27 @@ func (q *Queries) AnalyticsIssuesByStatus(ctx context.Context) ([]AnalyticsIssue
|
|||
|
||||
const AnalyticsIssuesByType = `-- name: AnalyticsIssuesByType :many
|
||||
SELECT
|
||||
COALESCE(issue_type, 'unknown') AS issue_type,
|
||||
COALESCE(ri.issue_type, 'unknown') AS issue_type,
|
||||
COUNT(*)::bigint AS count
|
||||
FROM reported_issues
|
||||
GROUP BY issue_type
|
||||
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)
|
||||
GROUP BY ri.issue_type
|
||||
ORDER BY count DESC
|
||||
`
|
||||
|
||||
type AnalyticsIssuesByTypeParams struct {
|
||||
RangeStart pgtype.Timestamptz `json:"range_start"`
|
||||
RangeEnd pgtype.Timestamptz `json:"range_end"`
|
||||
}
|
||||
|
||||
type AnalyticsIssuesByTypeRow struct {
|
||||
IssueType string `json:"issue_type"`
|
||||
Count int64 `json:"count"`
|
||||
}
|
||||
|
||||
func (q *Queries) AnalyticsIssuesByType(ctx context.Context) ([]AnalyticsIssuesByTypeRow, error) {
|
||||
rows, err := q.db.Query(ctx, AnalyticsIssuesByType)
|
||||
func (q *Queries) AnalyticsIssuesByType(ctx context.Context, arg AnalyticsIssuesByTypeParams) ([]AnalyticsIssuesByTypeRow, error) {
|
||||
rows, err := q.db.Query(ctx, AnalyticsIssuesByType, arg.RangeStart, arg.RangeEnd)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -112,14 +128,21 @@ const AnalyticsIssuesSummary = `-- name: AnalyticsIssuesSummary :one
|
|||
|
||||
SELECT
|
||||
COUNT(*)::bigint AS total,
|
||||
COUNT(*) FILTER (WHERE status = 'resolved')::bigint AS resolved,
|
||||
COUNT(*) FILTER (WHERE ri.status = 'resolved')::bigint AS resolved,
|
||||
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
|
||||
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 {
|
||||
Total int64 `json:"total"`
|
||||
Resolved int64 `json:"resolved"`
|
||||
|
|
@ -129,8 +152,8 @@ type AnalyticsIssuesSummaryRow struct {
|
|||
// =====================
|
||||
// Issue Analytics
|
||||
// =====================
|
||||
func (q *Queries) AnalyticsIssuesSummary(ctx context.Context) (AnalyticsIssuesSummaryRow, error) {
|
||||
row := q.db.QueryRow(ctx, AnalyticsIssuesSummary)
|
||||
func (q *Queries) AnalyticsIssuesSummary(ctx context.Context, arg AnalyticsIssuesSummaryParams) (AnalyticsIssuesSummaryRow, error) {
|
||||
row := q.db.QueryRow(ctx, AnalyticsIssuesSummary, arg.RangeStart, arg.RangeEnd)
|
||||
var i AnalyticsIssuesSummaryRow
|
||||
err := row.Scan(&i.Total, &i.Resolved, &i.ResolutionRate)
|
||||
return i, err
|
||||
|
|
@ -141,22 +164,36 @@ SELECT
|
|||
d.date,
|
||||
COUNT(us.id)::bigint AS count
|
||||
FROM generate_series(
|
||||
CURRENT_DATE - INTERVAL '29 days',
|
||||
CURRENT_DATE,
|
||||
$1::date,
|
||||
$2::date,
|
||||
INTERVAL '1 day'
|
||||
) AS 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
|
||||
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 {
|
||||
Date interface{} `json:"date"`
|
||||
Count int64 `json:"count"`
|
||||
}
|
||||
|
||||
func (q *Queries) AnalyticsNewSubscriptionsLast30Days(ctx context.Context) ([]AnalyticsNewSubscriptionsLast30DaysRow, error) {
|
||||
rows, err := q.db.Query(ctx, AnalyticsNewSubscriptionsLast30Days)
|
||||
func (q *Queries) AnalyticsNewSubscriptionsLast30Days(ctx context.Context, arg AnalyticsNewSubscriptionsLast30DaysParams) ([]AnalyticsNewSubscriptionsLast30DaysRow, error) {
|
||||
rows, err := q.db.Query(ctx, AnalyticsNewSubscriptionsLast30Days,
|
||||
arg.SeriesStart,
|
||||
arg.SeriesEnd,
|
||||
arg.RangeStart,
|
||||
arg.RangeEnd,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -177,20 +214,27 @@ func (q *Queries) AnalyticsNewSubscriptionsLast30Days(ctx context.Context) ([]An
|
|||
|
||||
const AnalyticsNotificationsByChannel = `-- name: AnalyticsNotificationsByChannel :many
|
||||
SELECT
|
||||
COALESCE(channel, 'unknown') AS channel,
|
||||
COALESCE(n.channel, 'unknown') AS channel,
|
||||
COUNT(*)::bigint AS count
|
||||
FROM notifications
|
||||
GROUP BY channel
|
||||
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)
|
||||
GROUP BY n.channel
|
||||
ORDER BY count DESC
|
||||
`
|
||||
|
||||
type AnalyticsNotificationsByChannelParams struct {
|
||||
RangeStart pgtype.Timestamptz `json:"range_start"`
|
||||
RangeEnd pgtype.Timestamptz `json:"range_end"`
|
||||
}
|
||||
|
||||
type AnalyticsNotificationsByChannelRow struct {
|
||||
Channel string `json:"channel"`
|
||||
Count int64 `json:"count"`
|
||||
}
|
||||
|
||||
func (q *Queries) AnalyticsNotificationsByChannel(ctx context.Context) ([]AnalyticsNotificationsByChannelRow, error) {
|
||||
rows, err := q.db.Query(ctx, AnalyticsNotificationsByChannel)
|
||||
func (q *Queries) AnalyticsNotificationsByChannel(ctx context.Context, arg AnalyticsNotificationsByChannelParams) ([]AnalyticsNotificationsByChannelRow, error) {
|
||||
rows, err := q.db.Query(ctx, AnalyticsNotificationsByChannel, arg.RangeStart, arg.RangeEnd)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -211,20 +255,27 @@ func (q *Queries) AnalyticsNotificationsByChannel(ctx context.Context) ([]Analyt
|
|||
|
||||
const AnalyticsNotificationsByType = `-- name: AnalyticsNotificationsByType :many
|
||||
SELECT
|
||||
COALESCE(type, 'unknown') AS type,
|
||||
COALESCE(n.type, 'unknown') AS type,
|
||||
COUNT(*)::bigint AS count
|
||||
FROM notifications
|
||||
GROUP BY type
|
||||
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)
|
||||
GROUP BY n.type
|
||||
ORDER BY count DESC
|
||||
`
|
||||
|
||||
type AnalyticsNotificationsByTypeParams struct {
|
||||
RangeStart pgtype.Timestamptz `json:"range_start"`
|
||||
RangeEnd pgtype.Timestamptz `json:"range_end"`
|
||||
}
|
||||
|
||||
type AnalyticsNotificationsByTypeRow struct {
|
||||
Type string `json:"type"`
|
||||
Count int64 `json:"count"`
|
||||
}
|
||||
|
||||
func (q *Queries) AnalyticsNotificationsByType(ctx context.Context) ([]AnalyticsNotificationsByTypeRow, error) {
|
||||
rows, err := q.db.Query(ctx, AnalyticsNotificationsByType)
|
||||
func (q *Queries) AnalyticsNotificationsByType(ctx context.Context, arg AnalyticsNotificationsByTypeParams) ([]AnalyticsNotificationsByTypeRow, error) {
|
||||
rows, err := q.db.Query(ctx, AnalyticsNotificationsByType, arg.RangeStart, arg.RangeEnd)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -247,11 +298,18 @@ const AnalyticsNotificationsSummary = `-- name: AnalyticsNotificationsSummary :o
|
|||
|
||||
SELECT
|
||||
COUNT(*)::bigint AS total,
|
||||
COUNT(*) FILTER (WHERE is_read = TRUE)::bigint AS read,
|
||||
COUNT(*) FILTER (WHERE is_read = FALSE)::bigint AS unread
|
||||
FROM notifications
|
||||
COUNT(*) FILTER (WHERE n.is_read = TRUE)::bigint AS read,
|
||||
COUNT(*) FILTER (WHERE n.is_read = FALSE)::bigint AS unread
|
||||
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 {
|
||||
Total int64 `json:"total"`
|
||||
Read int64 `json:"read"`
|
||||
|
|
@ -261,8 +319,8 @@ type AnalyticsNotificationsSummaryRow struct {
|
|||
// =====================
|
||||
// Notification Analytics
|
||||
// =====================
|
||||
func (q *Queries) AnalyticsNotificationsSummary(ctx context.Context) (AnalyticsNotificationsSummaryRow, error) {
|
||||
row := q.db.QueryRow(ctx, AnalyticsNotificationsSummary)
|
||||
func (q *Queries) AnalyticsNotificationsSummary(ctx context.Context, arg AnalyticsNotificationsSummaryParams) (AnalyticsNotificationsSummaryRow, error) {
|
||||
row := q.db.QueryRow(ctx, AnalyticsNotificationsSummary, arg.RangeStart, arg.RangeEnd)
|
||||
var i AnalyticsNotificationsSummaryRow
|
||||
err := row.Scan(&i.Total, &i.Read, &i.Unread)
|
||||
return i, err
|
||||
|
|
@ -270,23 +328,30 @@ func (q *Queries) AnalyticsNotificationsSummary(ctx context.Context) (AnalyticsN
|
|||
|
||||
const AnalyticsPaymentsByMethod = `-- name: AnalyticsPaymentsByMethod :many
|
||||
SELECT
|
||||
COALESCE(payment_method, 'unknown') AS payment_method,
|
||||
COALESCE(p.payment_method, 'unknown') AS payment_method,
|
||||
COUNT(*)::bigint AS count,
|
||||
COALESCE(SUM(amount), 0)::float8 AS total_amount
|
||||
FROM payments
|
||||
WHERE status = 'SUCCESS'
|
||||
GROUP BY payment_method
|
||||
COALESCE(SUM(p.amount), 0)::float8 AS total_amount
|
||||
FROM payments p
|
||||
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 p.payment_method
|
||||
ORDER BY count DESC
|
||||
`
|
||||
|
||||
type AnalyticsPaymentsByMethodParams struct {
|
||||
RangeStart pgtype.Timestamptz `json:"range_start"`
|
||||
RangeEnd pgtype.Timestamptz `json:"range_end"`
|
||||
}
|
||||
|
||||
type AnalyticsPaymentsByMethodRow struct {
|
||||
PaymentMethod string `json:"payment_method"`
|
||||
Count int64 `json:"count"`
|
||||
TotalAmount float64 `json:"total_amount"`
|
||||
}
|
||||
|
||||
func (q *Queries) AnalyticsPaymentsByMethod(ctx context.Context) ([]AnalyticsPaymentsByMethodRow, error) {
|
||||
rows, err := q.db.Query(ctx, AnalyticsPaymentsByMethod)
|
||||
func (q *Queries) AnalyticsPaymentsByMethod(ctx context.Context, arg AnalyticsPaymentsByMethodParams) ([]AnalyticsPaymentsByMethodRow, error) {
|
||||
rows, err := q.db.Query(ctx, AnalyticsPaymentsByMethod, arg.RangeStart, arg.RangeEnd)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -307,22 +372,29 @@ func (q *Queries) AnalyticsPaymentsByMethod(ctx context.Context) ([]AnalyticsPay
|
|||
|
||||
const AnalyticsPaymentsByStatus = `-- name: AnalyticsPaymentsByStatus :many
|
||||
SELECT
|
||||
COALESCE(status, 'unknown') AS status,
|
||||
COALESCE(p.status, 'unknown') AS status,
|
||||
COUNT(*)::bigint AS count,
|
||||
COALESCE(SUM(amount), 0)::float8 AS total_amount
|
||||
FROM payments
|
||||
GROUP BY status
|
||||
COALESCE(SUM(p.amount), 0)::float8 AS total_amount
|
||||
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)
|
||||
GROUP BY p.status
|
||||
ORDER BY count DESC
|
||||
`
|
||||
|
||||
type AnalyticsPaymentsByStatusParams struct {
|
||||
RangeStart pgtype.Timestamptz `json:"range_start"`
|
||||
RangeEnd pgtype.Timestamptz `json:"range_end"`
|
||||
}
|
||||
|
||||
type AnalyticsPaymentsByStatusRow struct {
|
||||
Status string `json:"status"`
|
||||
Count int64 `json:"count"`
|
||||
TotalAmount float64 `json:"total_amount"`
|
||||
}
|
||||
|
||||
func (q *Queries) AnalyticsPaymentsByStatus(ctx context.Context) ([]AnalyticsPaymentsByStatusRow, error) {
|
||||
rows, err := q.db.Query(ctx, AnalyticsPaymentsByStatus)
|
||||
func (q *Queries) AnalyticsPaymentsByStatus(ctx context.Context, arg AnalyticsPaymentsByStatusParams) ([]AnalyticsPaymentsByStatusRow, error) {
|
||||
rows, err := q.db.Query(ctx, AnalyticsPaymentsByStatus, arg.RangeStart, arg.RangeEnd)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -344,13 +416,20 @@ func (q *Queries) AnalyticsPaymentsByStatus(ctx context.Context) ([]AnalyticsPay
|
|||
const AnalyticsPaymentsSummary = `-- name: AnalyticsPaymentsSummary :one
|
||||
|
||||
SELECT
|
||||
COALESCE(SUM(amount) FILTER (WHERE status = 'SUCCESS'), 0)::float8 AS total_revenue,
|
||||
COALESCE(AVG(amount) FILTER (WHERE status = 'SUCCESS'), 0)::float8 AS avg_value,
|
||||
COALESCE(SUM(p.amount) FILTER (WHERE p.status = 'SUCCESS'), 0)::float8 AS total_revenue,
|
||||
COALESCE(AVG(p.amount) FILTER (WHERE p.status = 'SUCCESS'), 0)::float8 AS avg_value,
|
||||
COUNT(*)::bigint AS total_payments,
|
||||
COUNT(*) FILTER (WHERE status = 'SUCCESS')::bigint AS successful_payments
|
||||
FROM payments
|
||||
COUNT(*) FILTER (WHERE p.status = 'SUCCESS')::bigint AS successful_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 {
|
||||
TotalRevenue float64 `json:"total_revenue"`
|
||||
AvgValue float64 `json:"avg_value"`
|
||||
|
|
@ -361,8 +440,8 @@ type AnalyticsPaymentsSummaryRow struct {
|
|||
// =====================
|
||||
// Payment Analytics
|
||||
// =====================
|
||||
func (q *Queries) AnalyticsPaymentsSummary(ctx context.Context) (AnalyticsPaymentsSummaryRow, error) {
|
||||
row := q.db.QueryRow(ctx, AnalyticsPaymentsSummary)
|
||||
func (q *Queries) AnalyticsPaymentsSummary(ctx context.Context, arg AnalyticsPaymentsSummaryParams) (AnalyticsPaymentsSummaryRow, error) {
|
||||
row := q.db.QueryRow(ctx, AnalyticsPaymentsSummary, arg.RangeStart, arg.RangeEnd)
|
||||
var i AnalyticsPaymentsSummaryRow
|
||||
err := row.Scan(
|
||||
&i.TotalRevenue,
|
||||
|
|
@ -375,20 +454,27 @@ func (q *Queries) AnalyticsPaymentsSummary(ctx context.Context) (AnalyticsPaymen
|
|||
|
||||
const AnalyticsQuestionSetsByType = `-- name: AnalyticsQuestionSetsByType :many
|
||||
SELECT
|
||||
COALESCE(set_type, 'unknown') AS set_type,
|
||||
COALESCE(qs.set_type, 'unknown') AS set_type,
|
||||
COUNT(*)::bigint AS count
|
||||
FROM question_sets
|
||||
GROUP BY set_type
|
||||
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)
|
||||
GROUP BY qs.set_type
|
||||
ORDER BY count DESC
|
||||
`
|
||||
|
||||
type AnalyticsQuestionSetsByTypeParams struct {
|
||||
RangeStart pgtype.Timestamptz `json:"range_start"`
|
||||
RangeEnd pgtype.Timestamptz `json:"range_end"`
|
||||
}
|
||||
|
||||
type AnalyticsQuestionSetsByTypeRow struct {
|
||||
SetType string `json:"set_type"`
|
||||
Count int64 `json:"count"`
|
||||
}
|
||||
|
||||
func (q *Queries) AnalyticsQuestionSetsByType(ctx context.Context) ([]AnalyticsQuestionSetsByTypeRow, error) {
|
||||
rows, err := q.db.Query(ctx, AnalyticsQuestionSetsByType)
|
||||
func (q *Queries) AnalyticsQuestionSetsByType(ctx context.Context, arg AnalyticsQuestionSetsByTypeParams) ([]AnalyticsQuestionSetsByTypeRow, error) {
|
||||
rows, err := q.db.Query(ctx, AnalyticsQuestionSetsByType, arg.RangeStart, arg.RangeEnd)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -409,20 +495,27 @@ func (q *Queries) AnalyticsQuestionSetsByType(ctx context.Context) ([]AnalyticsQ
|
|||
|
||||
const AnalyticsQuestionsByType = `-- name: AnalyticsQuestionsByType :many
|
||||
SELECT
|
||||
COALESCE(question_type, 'unknown') AS question_type,
|
||||
COALESCE(q.question_type, 'unknown') AS question_type,
|
||||
COUNT(*)::bigint AS count
|
||||
FROM questions
|
||||
GROUP BY question_type
|
||||
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)
|
||||
GROUP BY q.question_type
|
||||
ORDER BY count DESC
|
||||
`
|
||||
|
||||
type AnalyticsQuestionsByTypeParams struct {
|
||||
RangeStart pgtype.Timestamptz `json:"range_start"`
|
||||
RangeEnd pgtype.Timestamptz `json:"range_end"`
|
||||
}
|
||||
|
||||
type AnalyticsQuestionsByTypeRow struct {
|
||||
QuestionType string `json:"question_type"`
|
||||
Count int64 `json:"count"`
|
||||
}
|
||||
|
||||
func (q *Queries) AnalyticsQuestionsByType(ctx context.Context) ([]AnalyticsQuestionsByTypeRow, error) {
|
||||
rows, err := q.db.Query(ctx, AnalyticsQuestionsByType)
|
||||
func (q *Queries) AnalyticsQuestionsByType(ctx context.Context, arg AnalyticsQuestionsByTypeParams) ([]AnalyticsQuestionsByTypeRow, error) {
|
||||
rows, err := q.db.Query(ctx, AnalyticsQuestionsByType, arg.RangeStart, arg.RangeEnd)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -444,10 +537,25 @@ func (q *Queries) AnalyticsQuestionsByType(ctx context.Context) ([]AnalyticsQues
|
|||
const AnalyticsQuestionsCounts = `-- name: AnalyticsQuestionsCounts :one
|
||||
|
||||
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 {
|
||||
TotalQuestions int64 `json:"total_questions"`
|
||||
TotalQuestionSets int64 `json:"total_question_sets"`
|
||||
|
|
@ -456,8 +564,8 @@ type AnalyticsQuestionsCountsRow struct {
|
|||
// =====================
|
||||
// Content Analytics
|
||||
// =====================
|
||||
func (q *Queries) AnalyticsQuestionsCounts(ctx context.Context) (AnalyticsQuestionsCountsRow, error) {
|
||||
row := q.db.QueryRow(ctx, AnalyticsQuestionsCounts)
|
||||
func (q *Queries) AnalyticsQuestionsCounts(ctx context.Context, arg AnalyticsQuestionsCountsParams) (AnalyticsQuestionsCountsRow, error) {
|
||||
row := q.db.QueryRow(ctx, AnalyticsQuestionsCounts, arg.RangeStart, arg.RangeEnd)
|
||||
var i AnalyticsQuestionsCountsRow
|
||||
err := row.Scan(&i.TotalQuestions, &i.TotalQuestionSets)
|
||||
return i, err
|
||||
|
|
@ -472,10 +580,17 @@ SELECT
|
|||
FROM payments p
|
||||
JOIN subscription_plans sp ON sp.id = p.plan_id
|
||||
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
|
||||
ORDER BY total_revenue DESC
|
||||
`
|
||||
|
||||
type AnalyticsRevenueByPlanParams struct {
|
||||
RangeStart pgtype.Timestamptz `json:"range_start"`
|
||||
RangeEnd pgtype.Timestamptz `json:"range_end"`
|
||||
}
|
||||
|
||||
type AnalyticsRevenueByPlanRow struct {
|
||||
PlanName string `json:"plan_name"`
|
||||
Currency string `json:"currency"`
|
||||
|
|
@ -483,8 +598,8 @@ type AnalyticsRevenueByPlanRow struct {
|
|||
TotalRevenue float64 `json:"total_revenue"`
|
||||
}
|
||||
|
||||
func (q *Queries) AnalyticsRevenueByPlan(ctx context.Context) ([]AnalyticsRevenueByPlanRow, error) {
|
||||
rows, err := q.db.Query(ctx, AnalyticsRevenueByPlan)
|
||||
func (q *Queries) AnalyticsRevenueByPlan(ctx context.Context, arg AnalyticsRevenueByPlanParams) ([]AnalyticsRevenueByPlanRow, error) {
|
||||
rows, err := q.db.Query(ctx, AnalyticsRevenueByPlan, arg.RangeStart, arg.RangeEnd)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -513,22 +628,37 @@ SELECT
|
|||
d.date,
|
||||
COALESCE(SUM(p.amount), 0)::float8 AS total_revenue
|
||||
FROM generate_series(
|
||||
CURRENT_DATE - INTERVAL '29 days',
|
||||
CURRENT_DATE,
|
||||
$1::date,
|
||||
$2::date,
|
||||
INTERVAL '1 day'
|
||||
) 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
|
||||
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 {
|
||||
Date interface{} `json:"date"`
|
||||
TotalRevenue float64 `json:"total_revenue"`
|
||||
}
|
||||
|
||||
func (q *Queries) AnalyticsRevenueLast30Days(ctx context.Context) ([]AnalyticsRevenueLast30DaysRow, error) {
|
||||
rows, err := q.db.Query(ctx, AnalyticsRevenueLast30Days)
|
||||
func (q *Queries) AnalyticsRevenueLast30Days(ctx context.Context, arg AnalyticsRevenueLast30DaysParams) ([]AnalyticsRevenueLast30DaysRow, error) {
|
||||
rows, err := q.db.Query(ctx, AnalyticsRevenueLast30Days,
|
||||
arg.SeriesStart,
|
||||
arg.SeriesEnd,
|
||||
arg.RangeStart,
|
||||
arg.RangeEnd,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -549,20 +679,27 @@ func (q *Queries) AnalyticsRevenueLast30Days(ctx context.Context) ([]AnalyticsRe
|
|||
|
||||
const AnalyticsSubscriptionsByStatus = `-- name: AnalyticsSubscriptionsByStatus :many
|
||||
SELECT
|
||||
COALESCE(status, 'unknown') AS status,
|
||||
COALESCE(us.status, 'unknown') AS status,
|
||||
COUNT(*)::bigint AS count
|
||||
FROM user_subscriptions
|
||||
GROUP BY status
|
||||
FROM user_subscriptions us
|
||||
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
|
||||
`
|
||||
|
||||
type AnalyticsSubscriptionsByStatusParams struct {
|
||||
RangeStart pgtype.Timestamptz `json:"range_start"`
|
||||
RangeEnd pgtype.Timestamptz `json:"range_end"`
|
||||
}
|
||||
|
||||
type AnalyticsSubscriptionsByStatusRow struct {
|
||||
Status string `json:"status"`
|
||||
Count int64 `json:"count"`
|
||||
}
|
||||
|
||||
func (q *Queries) AnalyticsSubscriptionsByStatus(ctx context.Context) ([]AnalyticsSubscriptionsByStatusRow, error) {
|
||||
rows, err := q.db.Query(ctx, AnalyticsSubscriptionsByStatus)
|
||||
func (q *Queries) AnalyticsSubscriptionsByStatus(ctx context.Context, arg AnalyticsSubscriptionsByStatusParams) ([]AnalyticsSubscriptionsByStatusRow, error) {
|
||||
rows, err := q.db.Query(ctx, AnalyticsSubscriptionsByStatus, arg.RangeStart, arg.RangeEnd)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -585,13 +722,27 @@ const AnalyticsSubscriptionsSummary = `-- name: AnalyticsSubscriptionsSummary :o
|
|||
|
||||
SELECT
|
||||
COUNT(*)::bigint AS total,
|
||||
COUNT(*) FILTER (WHERE status = 'ACTIVE')::bigint AS active,
|
||||
COUNT(*) FILTER (WHERE created_at >= CURRENT_DATE)::bigint AS new_today,
|
||||
COUNT(*) FILTER (WHERE created_at >= CURRENT_DATE - INTERVAL '7 days')::bigint AS new_this_week,
|
||||
COUNT(*) FILTER (WHERE created_at >= CURRENT_DATE - INTERVAL '30 days')::bigint AS new_this_month
|
||||
FROM user_subscriptions
|
||||
COUNT(*) FILTER (WHERE us.status = 'ACTIVE')::bigint AS active,
|
||||
COUNT(*) FILTER (WHERE us.created_at::date = $1::date)::bigint AS new_today,
|
||||
COUNT(*) FILTER (
|
||||
WHERE us.created_at::date >= $1::date - INTERVAL '6 days'
|
||||
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 {
|
||||
Total int64 `json:"total"`
|
||||
Active int64 `json:"active"`
|
||||
|
|
@ -603,8 +754,8 @@ type AnalyticsSubscriptionsSummaryRow struct {
|
|||
// =====================
|
||||
// Subscription Analytics
|
||||
// =====================
|
||||
func (q *Queries) AnalyticsSubscriptionsSummary(ctx context.Context) (AnalyticsSubscriptionsSummaryRow, error) {
|
||||
row := q.db.QueryRow(ctx, AnalyticsSubscriptionsSummary)
|
||||
func (q *Queries) AnalyticsSubscriptionsSummary(ctx context.Context, arg AnalyticsSubscriptionsSummaryParams) (AnalyticsSubscriptionsSummaryRow, error) {
|
||||
row := q.db.QueryRow(ctx, AnalyticsSubscriptionsSummary, arg.RefDate, arg.RangeStart, arg.RangeEnd)
|
||||
var i AnalyticsSubscriptionsSummaryRow
|
||||
err := row.Scan(
|
||||
&i.Total,
|
||||
|
|
@ -618,20 +769,27 @@ func (q *Queries) AnalyticsSubscriptionsSummary(ctx context.Context) (AnalyticsS
|
|||
|
||||
const AnalyticsTeamByRole = `-- name: AnalyticsTeamByRole :many
|
||||
SELECT
|
||||
COALESCE(team_role, 'unknown') AS team_role,
|
||||
COALESCE(tm.team_role, 'unknown') AS team_role,
|
||||
COUNT(*)::bigint AS count
|
||||
FROM team_members
|
||||
GROUP BY team_role
|
||||
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)
|
||||
GROUP BY tm.team_role
|
||||
ORDER BY count DESC
|
||||
`
|
||||
|
||||
type AnalyticsTeamByRoleParams struct {
|
||||
RangeStart pgtype.Timestamptz `json:"range_start"`
|
||||
RangeEnd pgtype.Timestamptz `json:"range_end"`
|
||||
}
|
||||
|
||||
type AnalyticsTeamByRoleRow struct {
|
||||
TeamRole string `json:"team_role"`
|
||||
Count int64 `json:"count"`
|
||||
}
|
||||
|
||||
func (q *Queries) AnalyticsTeamByRole(ctx context.Context) ([]AnalyticsTeamByRoleRow, error) {
|
||||
rows, err := q.db.Query(ctx, AnalyticsTeamByRole)
|
||||
func (q *Queries) AnalyticsTeamByRole(ctx context.Context, arg AnalyticsTeamByRoleParams) ([]AnalyticsTeamByRoleRow, error) {
|
||||
rows, err := q.db.Query(ctx, AnalyticsTeamByRole, arg.RangeStart, arg.RangeEnd)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -652,20 +810,27 @@ func (q *Queries) AnalyticsTeamByRole(ctx context.Context) ([]AnalyticsTeamByRol
|
|||
|
||||
const AnalyticsTeamByStatus = `-- name: AnalyticsTeamByStatus :many
|
||||
SELECT
|
||||
COALESCE(status, 'unknown') AS status,
|
||||
COALESCE(tm.status, 'unknown') AS status,
|
||||
COUNT(*)::bigint AS count
|
||||
FROM team_members
|
||||
GROUP BY status
|
||||
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)
|
||||
GROUP BY tm.status
|
||||
ORDER BY count DESC
|
||||
`
|
||||
|
||||
type AnalyticsTeamByStatusParams struct {
|
||||
RangeStart pgtype.Timestamptz `json:"range_start"`
|
||||
RangeEnd pgtype.Timestamptz `json:"range_end"`
|
||||
}
|
||||
|
||||
type AnalyticsTeamByStatusRow struct {
|
||||
Status string `json:"status"`
|
||||
Count int64 `json:"count"`
|
||||
}
|
||||
|
||||
func (q *Queries) AnalyticsTeamByStatus(ctx context.Context) ([]AnalyticsTeamByStatusRow, error) {
|
||||
rows, err := q.db.Query(ctx, AnalyticsTeamByStatus)
|
||||
func (q *Queries) AnalyticsTeamByStatus(ctx context.Context, arg AnalyticsTeamByStatusParams) ([]AnalyticsTeamByStatusRow, error) {
|
||||
rows, err := q.db.Query(ctx, AnalyticsTeamByStatus, arg.RangeStart, arg.RangeEnd)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -688,14 +853,21 @@ const AnalyticsTeamSummary = `-- name: AnalyticsTeamSummary :one
|
|||
|
||||
SELECT
|
||||
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
|
||||
// =====================
|
||||
func (q *Queries) AnalyticsTeamSummary(ctx context.Context) (int64, error) {
|
||||
row := q.db.QueryRow(ctx, AnalyticsTeamSummary)
|
||||
func (q *Queries) AnalyticsTeamSummary(ctx context.Context, arg AnalyticsTeamSummaryParams) (int64, error) {
|
||||
row := q.db.QueryRow(ctx, AnalyticsTeamSummary, arg.RangeStart, arg.RangeEnd)
|
||||
var total_members int64
|
||||
err := row.Scan(&total_members)
|
||||
return total_members, err
|
||||
|
|
@ -706,22 +878,36 @@ SELECT
|
|||
d.date,
|
||||
COUNT(u.id)::bigint AS count
|
||||
FROM generate_series(
|
||||
CURRENT_DATE - INTERVAL '29 days',
|
||||
CURRENT_DATE,
|
||||
$1::date,
|
||||
$2::date,
|
||||
INTERVAL '1 day'
|
||||
) AS 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
|
||||
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 {
|
||||
Date interface{} `json:"date"`
|
||||
Count int64 `json:"count"`
|
||||
}
|
||||
|
||||
func (q *Queries) AnalyticsUserRegistrationsLast30Days(ctx context.Context) ([]AnalyticsUserRegistrationsLast30DaysRow, error) {
|
||||
rows, err := q.db.Query(ctx, AnalyticsUserRegistrationsLast30Days)
|
||||
func (q *Queries) AnalyticsUserRegistrationsLast30Days(ctx context.Context, arg AnalyticsUserRegistrationsLast30DaysParams) ([]AnalyticsUserRegistrationsLast30DaysRow, error) {
|
||||
rows, err := q.db.Query(ctx, AnalyticsUserRegistrationsLast30Days,
|
||||
arg.SeriesStart,
|
||||
arg.SeriesEnd,
|
||||
arg.RangeStart,
|
||||
arg.RangeEnd,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -742,20 +928,27 @@ func (q *Queries) AnalyticsUserRegistrationsLast30Days(ctx context.Context) ([]A
|
|||
|
||||
const AnalyticsUsersByAgeGroup = `-- name: AnalyticsUsersByAgeGroup :many
|
||||
SELECT
|
||||
COALESCE(age_group, 'unknown') AS age_group,
|
||||
COALESCE(u.age_group, 'unknown') AS age_group,
|
||||
COUNT(*)::bigint AS count
|
||||
FROM users
|
||||
GROUP BY age_group
|
||||
FROM users u
|
||||
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
|
||||
`
|
||||
|
||||
type AnalyticsUsersByAgeGroupParams struct {
|
||||
RangeStart pgtype.Timestamptz `json:"range_start"`
|
||||
RangeEnd pgtype.Timestamptz `json:"range_end"`
|
||||
}
|
||||
|
||||
type AnalyticsUsersByAgeGroupRow struct {
|
||||
AgeGroup string `json:"age_group"`
|
||||
Count int64 `json:"count"`
|
||||
}
|
||||
|
||||
func (q *Queries) AnalyticsUsersByAgeGroup(ctx context.Context) ([]AnalyticsUsersByAgeGroupRow, error) {
|
||||
rows, err := q.db.Query(ctx, AnalyticsUsersByAgeGroup)
|
||||
func (q *Queries) AnalyticsUsersByAgeGroup(ctx context.Context, arg AnalyticsUsersByAgeGroupParams) ([]AnalyticsUsersByAgeGroupRow, error) {
|
||||
rows, err := q.db.Query(ctx, AnalyticsUsersByAgeGroup, arg.RangeStart, arg.RangeEnd)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -776,20 +969,27 @@ func (q *Queries) AnalyticsUsersByAgeGroup(ctx context.Context) ([]AnalyticsUser
|
|||
|
||||
const AnalyticsUsersByKnowledgeLevel = `-- name: AnalyticsUsersByKnowledgeLevel :many
|
||||
SELECT
|
||||
COALESCE(knowledge_level, 'unknown') AS knowledge_level,
|
||||
COALESCE(u.knowledge_level, 'unknown') AS knowledge_level,
|
||||
COUNT(*)::bigint AS count
|
||||
FROM users
|
||||
GROUP BY knowledge_level
|
||||
FROM users u
|
||||
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
|
||||
`
|
||||
|
||||
type AnalyticsUsersByKnowledgeLevelParams struct {
|
||||
RangeStart pgtype.Timestamptz `json:"range_start"`
|
||||
RangeEnd pgtype.Timestamptz `json:"range_end"`
|
||||
}
|
||||
|
||||
type AnalyticsUsersByKnowledgeLevelRow struct {
|
||||
KnowledgeLevel string `json:"knowledge_level"`
|
||||
Count int64 `json:"count"`
|
||||
}
|
||||
|
||||
func (q *Queries) AnalyticsUsersByKnowledgeLevel(ctx context.Context) ([]AnalyticsUsersByKnowledgeLevelRow, error) {
|
||||
rows, err := q.db.Query(ctx, AnalyticsUsersByKnowledgeLevel)
|
||||
func (q *Queries) AnalyticsUsersByKnowledgeLevel(ctx context.Context, arg AnalyticsUsersByKnowledgeLevelParams) ([]AnalyticsUsersByKnowledgeLevelRow, error) {
|
||||
rows, err := q.db.Query(ctx, AnalyticsUsersByKnowledgeLevel, arg.RangeStart, arg.RangeEnd)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -810,20 +1010,27 @@ func (q *Queries) AnalyticsUsersByKnowledgeLevel(ctx context.Context) ([]Analyti
|
|||
|
||||
const AnalyticsUsersByRegion = `-- name: AnalyticsUsersByRegion :many
|
||||
SELECT
|
||||
COALESCE(region, 'unknown') AS region,
|
||||
COALESCE(u.region, 'unknown') AS region,
|
||||
COUNT(*)::bigint AS count
|
||||
FROM users
|
||||
GROUP BY region
|
||||
FROM users u
|
||||
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
|
||||
`
|
||||
|
||||
type AnalyticsUsersByRegionParams struct {
|
||||
RangeStart pgtype.Timestamptz `json:"range_start"`
|
||||
RangeEnd pgtype.Timestamptz `json:"range_end"`
|
||||
}
|
||||
|
||||
type AnalyticsUsersByRegionRow struct {
|
||||
Region string `json:"region"`
|
||||
Count int64 `json:"count"`
|
||||
}
|
||||
|
||||
func (q *Queries) AnalyticsUsersByRegion(ctx context.Context) ([]AnalyticsUsersByRegionRow, error) {
|
||||
rows, err := q.db.Query(ctx, AnalyticsUsersByRegion)
|
||||
func (q *Queries) AnalyticsUsersByRegion(ctx context.Context, arg AnalyticsUsersByRegionParams) ([]AnalyticsUsersByRegionRow, error) {
|
||||
rows, err := q.db.Query(ctx, AnalyticsUsersByRegion, arg.RangeStart, arg.RangeEnd)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -844,20 +1051,27 @@ func (q *Queries) AnalyticsUsersByRegion(ctx context.Context) ([]AnalyticsUsersB
|
|||
|
||||
const AnalyticsUsersByRole = `-- name: AnalyticsUsersByRole :many
|
||||
SELECT
|
||||
COALESCE(role, 'unknown') AS role,
|
||||
COALESCE(u.role, 'unknown') AS role,
|
||||
COUNT(*)::bigint AS count
|
||||
FROM users
|
||||
GROUP BY role
|
||||
FROM users u
|
||||
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
|
||||
`
|
||||
|
||||
type AnalyticsUsersByRoleParams struct {
|
||||
RangeStart pgtype.Timestamptz `json:"range_start"`
|
||||
RangeEnd pgtype.Timestamptz `json:"range_end"`
|
||||
}
|
||||
|
||||
type AnalyticsUsersByRoleRow struct {
|
||||
Role string `json:"role"`
|
||||
Count int64 `json:"count"`
|
||||
}
|
||||
|
||||
func (q *Queries) AnalyticsUsersByRole(ctx context.Context) ([]AnalyticsUsersByRoleRow, error) {
|
||||
rows, err := q.db.Query(ctx, AnalyticsUsersByRole)
|
||||
func (q *Queries) AnalyticsUsersByRole(ctx context.Context, arg AnalyticsUsersByRoleParams) ([]AnalyticsUsersByRoleRow, error) {
|
||||
rows, err := q.db.Query(ctx, AnalyticsUsersByRole, arg.RangeStart, arg.RangeEnd)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -878,20 +1092,27 @@ func (q *Queries) AnalyticsUsersByRole(ctx context.Context) ([]AnalyticsUsersByR
|
|||
|
||||
const AnalyticsUsersByStatus = `-- name: AnalyticsUsersByStatus :many
|
||||
SELECT
|
||||
COALESCE(status, 'unknown') AS status,
|
||||
COALESCE(u.status, 'unknown') AS status,
|
||||
COUNT(*)::bigint AS count
|
||||
FROM users
|
||||
GROUP BY status
|
||||
FROM users u
|
||||
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
|
||||
`
|
||||
|
||||
type AnalyticsUsersByStatusParams struct {
|
||||
RangeStart pgtype.Timestamptz `json:"range_start"`
|
||||
RangeEnd pgtype.Timestamptz `json:"range_end"`
|
||||
}
|
||||
|
||||
type AnalyticsUsersByStatusRow struct {
|
||||
Status string `json:"status"`
|
||||
Count int64 `json:"count"`
|
||||
}
|
||||
|
||||
func (q *Queries) AnalyticsUsersByStatus(ctx context.Context) ([]AnalyticsUsersByStatusRow, error) {
|
||||
rows, err := q.db.Query(ctx, AnalyticsUsersByStatus)
|
||||
func (q *Queries) AnalyticsUsersByStatus(ctx context.Context, arg AnalyticsUsersByStatusParams) ([]AnalyticsUsersByStatusRow, error) {
|
||||
rows, err := q.db.Query(ctx, AnalyticsUsersByStatus, arg.RangeStart, arg.RangeEnd)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -915,12 +1136,26 @@ const AnalyticsUsersSummary = `-- name: AnalyticsUsersSummary :one
|
|||
|
||||
SELECT
|
||||
COUNT(*)::bigint AS total,
|
||||
COUNT(*) FILTER (WHERE created_at >= CURRENT_DATE)::bigint AS new_today,
|
||||
COUNT(*) FILTER (WHERE created_at >= CURRENT_DATE - INTERVAL '7 days')::bigint AS new_this_week,
|
||||
COUNT(*) FILTER (WHERE created_at >= CURRENT_DATE - INTERVAL '30 days')::bigint AS new_this_month
|
||||
FROM users
|
||||
COUNT(*) FILTER (WHERE u.created_at::date = $1::date)::bigint AS new_today,
|
||||
COUNT(*) FILTER (
|
||||
WHERE u.created_at::date >= $1::date - INTERVAL '6 days'
|
||||
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 {
|
||||
Total int64 `json:"total"`
|
||||
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
|
||||
// =====================
|
||||
func (q *Queries) AnalyticsUsersSummary(ctx context.Context) (AnalyticsUsersSummaryRow, error) {
|
||||
row := q.db.QueryRow(ctx, AnalyticsUsersSummary)
|
||||
func (q *Queries) AnalyticsUsersSummary(ctx context.Context, arg AnalyticsUsersSummaryParams) (AnalyticsUsersSummaryRow, error) {
|
||||
row := q.db.QueryRow(ctx, AnalyticsUsersSummary, arg.RefDate, arg.RangeStart, arg.RangeEnd)
|
||||
var i AnalyticsUsersSummaryRow
|
||||
err := row.Scan(
|
||||
&i.Total,
|
||||
|
|
|
|||
|
|
@ -101,6 +101,17 @@ type ExamPrepUnitModuleLesson struct {
|
|||
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 {
|
||||
Key string `json:"key"`
|
||||
Value string `json:"value"`
|
||||
|
|
|
|||
|
|
@ -110,6 +110,7 @@ type AnalyticsTeamSection struct {
|
|||
|
||||
type AnalyticsDashboard struct {
|
||||
GeneratedAt time.Time `json:"generated_at"`
|
||||
DateFilter AnalyticsDateFilter `json:"date_filter"`
|
||||
Users AnalyticsUsersSection `json:"users"`
|
||||
Subscriptions AnalyticsSubscriptionsSection `json:"subscriptions"`
|
||||
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,140 +15,154 @@ func toTime(v interface{}) 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 {
|
||||
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 ──
|
||||
usersSummary, err := h.analyticsDB.AnalyticsUsersSummary(ctx)
|
||||
ctx := c.Context()
|
||||
p := newAnalyticsQueryParams(filter)
|
||||
|
||||
usersSummary, err := h.analyticsDB.AnalyticsUsersSummary(ctx, p.UsersSummary)
|
||||
if err != nil {
|
||||
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 {
|
||||
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 {
|
||||
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 {
|
||||
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 {
|
||||
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 {
|
||||
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 {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch user registrations")
|
||||
}
|
||||
|
||||
// ── Subscriptions ──
|
||||
subsSummary, err := h.analyticsDB.AnalyticsSubscriptionsSummary(ctx)
|
||||
subsSummary, err := h.analyticsDB.AnalyticsSubscriptionsSummary(ctx, p.SubscriptionsSummary)
|
||||
if err != nil {
|
||||
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 {
|
||||
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 {
|
||||
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 {
|
||||
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)
|
||||
paymentsSummary, err := h.analyticsDB.AnalyticsPaymentsSummary(ctx, p.PaymentsSummary)
|
||||
if err != nil {
|
||||
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 {
|
||||
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 {
|
||||
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 {
|
||||
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)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch course analytics")
|
||||
}
|
||||
|
||||
// ── Content ──
|
||||
questionsCounts, err := h.analyticsDB.AnalyticsQuestionsCounts(ctx)
|
||||
questionsCounts, err := h.analyticsDB.AnalyticsQuestionsCounts(ctx, p.QuestionsCounts)
|
||||
if err != nil {
|
||||
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 {
|
||||
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 {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch question sets by type")
|
||||
}
|
||||
|
||||
// ── Notifications ──
|
||||
notifSummary, err := h.analyticsDB.AnalyticsNotificationsSummary(ctx)
|
||||
notifSummary, err := h.analyticsDB.AnalyticsNotificationsSummary(ctx, p.NotificationsSummary)
|
||||
if err != nil {
|
||||
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 {
|
||||
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 {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch notifications by type")
|
||||
}
|
||||
|
||||
// ── Issues ──
|
||||
issuesSummary, err := h.analyticsDB.AnalyticsIssuesSummary(ctx)
|
||||
issuesSummary, err := h.analyticsDB.AnalyticsIssuesSummary(ctx, p.IssuesSummary)
|
||||
if err != nil {
|
||||
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 {
|
||||
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 {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch issues by type")
|
||||
}
|
||||
|
||||
// ── Team ──
|
||||
teamTotal, err := h.analyticsDB.AnalyticsTeamSummary(ctx)
|
||||
teamTotal, err := h.analyticsDB.AnalyticsTeamSummary(ctx, p.TeamSummary)
|
||||
if err != nil {
|
||||
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 {
|
||||
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 {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch team by status")
|
||||
}
|
||||
|
||||
// ── Map to domain types ──
|
||||
dashboard := domain.AnalyticsDashboard{
|
||||
GeneratedAt: time.Now(),
|
||||
GeneratedAt: time.Now().UTC(),
|
||||
DateFilter: filter,
|
||||
Users: mapUsersSection(usersSummary, usersByRole, usersByStatus, usersByAge, usersByKnowledge, usersByRegion, userRegs),
|
||||
Subscriptions: mapSubscriptionsSection(subsSummary, subsByStatus, revenueByPlan, newSubs30),
|
||||
Payments: mapPaymentsSection(paymentsSummary, paymentsByStatus, paymentsByMethod, revenue30),
|
||||
|
|
|
|||
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