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:
Yared Yemane 2026-05-15 02:15:15 -07:00
parent 8bba318372
commit 024a69b74b
8 changed files with 960 additions and 257 deletions

View File

@ -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;

View File

@ -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,

View File

@ -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"`

View File

@ -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"`

View 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)
}

View 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)
}
}

View File

@ -15,140 +15,154 @@ 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(),
DateFilter: filter,
Users: mapUsersSection(usersSummary, usersByRole, usersByStatus, usersByAge, usersByKnowledge, usersByRegion, userRegs), 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),

View 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}
}