Add profile field breakdowns to analytics dashboard.
Expose user counts by education_level, occupation, learning_goal, and language_challange on GET /api/v1/analytics/dashboard. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
f7d4b5c3fb
commit
e957eacf80
|
|
@ -68,6 +68,46 @@ WHERE (sqlc.narg('range_start')::timestamptz IS NULL OR u.created_at >= sqlc.nar
|
|||
GROUP BY u.knowledge_level
|
||||
ORDER BY count DESC;
|
||||
|
||||
-- name: AnalyticsUsersByEducationLevel :many
|
||||
SELECT
|
||||
COALESCE(NULLIF(TRIM(u.education_level), ''), 'unknown')::text AS education_level,
|
||||
COUNT(*)::bigint AS count
|
||||
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 COALESCE(NULLIF(TRIM(u.education_level), ''), 'unknown')
|
||||
ORDER BY count DESC;
|
||||
|
||||
-- name: AnalyticsUsersByOccupation :many
|
||||
SELECT
|
||||
COALESCE(NULLIF(TRIM(u.occupation), ''), 'unknown')::text AS occupation,
|
||||
COUNT(*)::bigint AS count
|
||||
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 COALESCE(NULLIF(TRIM(u.occupation), ''), 'unknown')
|
||||
ORDER BY count DESC;
|
||||
|
||||
-- name: AnalyticsUsersByLearningGoal :many
|
||||
SELECT
|
||||
COALESCE(NULLIF(TRIM(u.learning_goal), ''), 'unknown')::text AS learning_goal,
|
||||
COUNT(*)::bigint AS count
|
||||
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 COALESCE(NULLIF(TRIM(u.learning_goal), ''), 'unknown')
|
||||
ORDER BY count DESC;
|
||||
|
||||
-- name: AnalyticsUsersByLanguageChallange :many
|
||||
SELECT
|
||||
COALESCE(NULLIF(TRIM(u.language_challange), ''), 'unknown')::text AS language_challange,
|
||||
COUNT(*)::bigint AS count
|
||||
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 COALESCE(NULLIF(TRIM(u.language_challange), ''), 'unknown')
|
||||
ORDER BY count DESC;
|
||||
|
||||
-- name: AnalyticsUsersByRegion :many
|
||||
SELECT
|
||||
COALESCE(u.region, 'unknown') AS region,
|
||||
|
|
|
|||
|
|
@ -1080,6 +1080,47 @@ func (q *Queries) AnalyticsUsersByAgeGroup(ctx context.Context, arg AnalyticsUse
|
|||
return items, nil
|
||||
}
|
||||
|
||||
const AnalyticsUsersByEducationLevel = `-- name: AnalyticsUsersByEducationLevel :many
|
||||
SELECT
|
||||
COALESCE(NULLIF(TRIM(u.education_level), ''), 'unknown')::text AS education_level,
|
||||
COUNT(*)::bigint AS count
|
||||
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 COALESCE(NULLIF(TRIM(u.education_level), ''), 'unknown')
|
||||
ORDER BY count DESC
|
||||
`
|
||||
|
||||
type AnalyticsUsersByEducationLevelParams struct {
|
||||
RangeStart pgtype.Timestamptz `json:"range_start"`
|
||||
RangeEnd pgtype.Timestamptz `json:"range_end"`
|
||||
}
|
||||
|
||||
type AnalyticsUsersByEducationLevelRow struct {
|
||||
EducationLevel string `json:"education_level"`
|
||||
Count int64 `json:"count"`
|
||||
}
|
||||
|
||||
func (q *Queries) AnalyticsUsersByEducationLevel(ctx context.Context, arg AnalyticsUsersByEducationLevelParams) ([]AnalyticsUsersByEducationLevelRow, error) {
|
||||
rows, err := q.db.Query(ctx, AnalyticsUsersByEducationLevel, arg.RangeStart, arg.RangeEnd)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []AnalyticsUsersByEducationLevelRow
|
||||
for rows.Next() {
|
||||
var i AnalyticsUsersByEducationLevelRow
|
||||
if err := rows.Scan(&i.EducationLevel, &i.Count); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const AnalyticsUsersByKnowledgeLevel = `-- name: AnalyticsUsersByKnowledgeLevel :many
|
||||
SELECT
|
||||
COALESCE(u.knowledge_level, 'unknown') AS knowledge_level,
|
||||
|
|
@ -1121,6 +1162,129 @@ func (q *Queries) AnalyticsUsersByKnowledgeLevel(ctx context.Context, arg Analyt
|
|||
return items, nil
|
||||
}
|
||||
|
||||
const AnalyticsUsersByLanguageChallange = `-- name: AnalyticsUsersByLanguageChallange :many
|
||||
SELECT
|
||||
COALESCE(NULLIF(TRIM(u.language_challange), ''), 'unknown')::text AS language_challange,
|
||||
COUNT(*)::bigint AS count
|
||||
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 COALESCE(NULLIF(TRIM(u.language_challange), ''), 'unknown')
|
||||
ORDER BY count DESC
|
||||
`
|
||||
|
||||
type AnalyticsUsersByLanguageChallangeParams struct {
|
||||
RangeStart pgtype.Timestamptz `json:"range_start"`
|
||||
RangeEnd pgtype.Timestamptz `json:"range_end"`
|
||||
}
|
||||
|
||||
type AnalyticsUsersByLanguageChallangeRow struct {
|
||||
LanguageChallange string `json:"language_challange"`
|
||||
Count int64 `json:"count"`
|
||||
}
|
||||
|
||||
func (q *Queries) AnalyticsUsersByLanguageChallange(ctx context.Context, arg AnalyticsUsersByLanguageChallangeParams) ([]AnalyticsUsersByLanguageChallangeRow, error) {
|
||||
rows, err := q.db.Query(ctx, AnalyticsUsersByLanguageChallange, arg.RangeStart, arg.RangeEnd)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []AnalyticsUsersByLanguageChallangeRow
|
||||
for rows.Next() {
|
||||
var i AnalyticsUsersByLanguageChallangeRow
|
||||
if err := rows.Scan(&i.LanguageChallange, &i.Count); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const AnalyticsUsersByLearningGoal = `-- name: AnalyticsUsersByLearningGoal :many
|
||||
SELECT
|
||||
COALESCE(NULLIF(TRIM(u.learning_goal), ''), 'unknown')::text AS learning_goal,
|
||||
COUNT(*)::bigint AS count
|
||||
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 COALESCE(NULLIF(TRIM(u.learning_goal), ''), 'unknown')
|
||||
ORDER BY count DESC
|
||||
`
|
||||
|
||||
type AnalyticsUsersByLearningGoalParams struct {
|
||||
RangeStart pgtype.Timestamptz `json:"range_start"`
|
||||
RangeEnd pgtype.Timestamptz `json:"range_end"`
|
||||
}
|
||||
|
||||
type AnalyticsUsersByLearningGoalRow struct {
|
||||
LearningGoal string `json:"learning_goal"`
|
||||
Count int64 `json:"count"`
|
||||
}
|
||||
|
||||
func (q *Queries) AnalyticsUsersByLearningGoal(ctx context.Context, arg AnalyticsUsersByLearningGoalParams) ([]AnalyticsUsersByLearningGoalRow, error) {
|
||||
rows, err := q.db.Query(ctx, AnalyticsUsersByLearningGoal, arg.RangeStart, arg.RangeEnd)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []AnalyticsUsersByLearningGoalRow
|
||||
for rows.Next() {
|
||||
var i AnalyticsUsersByLearningGoalRow
|
||||
if err := rows.Scan(&i.LearningGoal, &i.Count); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const AnalyticsUsersByOccupation = `-- name: AnalyticsUsersByOccupation :many
|
||||
SELECT
|
||||
COALESCE(NULLIF(TRIM(u.occupation), ''), 'unknown')::text AS occupation,
|
||||
COUNT(*)::bigint AS count
|
||||
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 COALESCE(NULLIF(TRIM(u.occupation), ''), 'unknown')
|
||||
ORDER BY count DESC
|
||||
`
|
||||
|
||||
type AnalyticsUsersByOccupationParams struct {
|
||||
RangeStart pgtype.Timestamptz `json:"range_start"`
|
||||
RangeEnd pgtype.Timestamptz `json:"range_end"`
|
||||
}
|
||||
|
||||
type AnalyticsUsersByOccupationRow struct {
|
||||
Occupation string `json:"occupation"`
|
||||
Count int64 `json:"count"`
|
||||
}
|
||||
|
||||
func (q *Queries) AnalyticsUsersByOccupation(ctx context.Context, arg AnalyticsUsersByOccupationParams) ([]AnalyticsUsersByOccupationRow, error) {
|
||||
rows, err := q.db.Query(ctx, AnalyticsUsersByOccupation, arg.RangeStart, arg.RangeEnd)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []AnalyticsUsersByOccupationRow
|
||||
for rows.Next() {
|
||||
var i AnalyticsUsersByOccupationRow
|
||||
if err := rows.Scan(&i.Occupation, &i.Count); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const AnalyticsUsersByRegion = `-- name: AnalyticsUsersByRegion :many
|
||||
SELECT
|
||||
COALESCE(u.region, 'unknown') AS region,
|
||||
|
|
|
|||
|
|
@ -127,6 +127,17 @@ type Faq struct {
|
|||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||
}
|
||||
|
||||
type FieldOption struct {
|
||||
ID int64 `json:"id"`
|
||||
FieldKey string `json:"field_key"`
|
||||
Code string `json:"code"`
|
||||
Label string `json:"label"`
|
||||
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"`
|
||||
|
|
|
|||
|
|
@ -47,6 +47,10 @@ type AnalyticsUsersSection struct {
|
|||
ByRole []AnalyticsLabelCount `json:"by_role"`
|
||||
ByStatus []AnalyticsLabelCount `json:"by_status"`
|
||||
ByAgeGroup []AnalyticsLabelCount `json:"by_age_group"`
|
||||
ByEducationLevel []AnalyticsLabelCount `json:"by_education_level"`
|
||||
ByOccupation []AnalyticsLabelCount `json:"by_occupation"`
|
||||
ByLearningGoal []AnalyticsLabelCount `json:"by_learning_goal"`
|
||||
ByLanguageChallange []AnalyticsLabelCount `json:"by_language_challange"`
|
||||
ByKnowledgeLevel []AnalyticsLabelCount `json:"by_knowledge_level"`
|
||||
ByRegion []AnalyticsLabelCount `json:"by_region"`
|
||||
|
||||
|
|
|
|||
|
|
@ -61,6 +61,22 @@ func (h *Handler) GetAnalyticsDashboard(c *fiber.Ctx) error {
|
|||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch users by age group")
|
||||
}
|
||||
usersByEducation, err := h.analyticsDB.AnalyticsUsersByEducationLevel(ctx, p.UsersByEducationLevel)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch users by education level")
|
||||
}
|
||||
usersByOccupation, err := h.analyticsDB.AnalyticsUsersByOccupation(ctx, p.UsersByOccupation)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch users by occupation")
|
||||
}
|
||||
usersByLearningGoal, err := h.analyticsDB.AnalyticsUsersByLearningGoal(ctx, p.UsersByLearningGoal)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch users by learning goal")
|
||||
}
|
||||
usersByLanguageChallange, err := h.analyticsDB.AnalyticsUsersByLanguageChallange(ctx, p.UsersByLanguageChallange)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch users by language challenge")
|
||||
}
|
||||
usersByKnowledge, err := h.analyticsDB.AnalyticsUsersByKnowledgeLevel(ctx, p.UsersByKnowledgeLevel)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch users by knowledge level")
|
||||
|
|
@ -179,7 +195,11 @@ func (h *Handler) GetAnalyticsDashboard(c *fiber.Ctx) error {
|
|||
dashboard := domain.AnalyticsDashboard{
|
||||
GeneratedAt: time.Now().UTC(),
|
||||
DateFilter: filter,
|
||||
Users: mapUsersSection(usersSummary, usersByRole, usersByStatus, usersByAge, usersByKnowledge, usersByRegion, userRegs),
|
||||
Users: mapUsersSection(
|
||||
usersSummary, usersByRole, usersByStatus, usersByAge,
|
||||
usersByEducation, usersByOccupation, usersByLearningGoal, usersByLanguageChallange,
|
||||
usersByKnowledge, usersByRegion, userRegs,
|
||||
),
|
||||
Subscriptions: mapSubscriptionsSection(subsSummary, subsByStatus, revenueByPlan, newSubs30),
|
||||
Payments: mapPaymentsSection(paymentsSummary, paymentsByStatus, paymentsByMethod, revenue30, revenueMonthlyRows, monthlyRevenueYear),
|
||||
Courses: mapCoursesSection(courseCounts),
|
||||
|
|
@ -225,6 +245,10 @@ func mapUsersSection(
|
|||
byRole []dbgen.AnalyticsUsersByRoleRow,
|
||||
byStatus []dbgen.AnalyticsUsersByStatusRow,
|
||||
byAge []dbgen.AnalyticsUsersByAgeGroupRow,
|
||||
byEducation []dbgen.AnalyticsUsersByEducationLevelRow,
|
||||
byOccupation []dbgen.AnalyticsUsersByOccupationRow,
|
||||
byLearningGoal []dbgen.AnalyticsUsersByLearningGoalRow,
|
||||
byLanguageChallange []dbgen.AnalyticsUsersByLanguageChallangeRow,
|
||||
byKnowledge []dbgen.AnalyticsUsersByKnowledgeLevelRow,
|
||||
byRegion []dbgen.AnalyticsUsersByRegionRow,
|
||||
regs []dbgen.AnalyticsUserRegistrationsLast30DaysRow,
|
||||
|
|
@ -241,6 +265,22 @@ func mapUsersSection(
|
|||
for i, r := range byAge {
|
||||
ages[i] = domain.AnalyticsLabelCount{Label: r.AgeGroup, Count: r.Count}
|
||||
}
|
||||
education := make([]domain.AnalyticsLabelCount, len(byEducation))
|
||||
for i, r := range byEducation {
|
||||
education[i] = domain.AnalyticsLabelCount{Label: r.EducationLevel, Count: r.Count}
|
||||
}
|
||||
occupations := make([]domain.AnalyticsLabelCount, len(byOccupation))
|
||||
for i, r := range byOccupation {
|
||||
occupations[i] = domain.AnalyticsLabelCount{Label: r.Occupation, Count: r.Count}
|
||||
}
|
||||
learningGoals := make([]domain.AnalyticsLabelCount, len(byLearningGoal))
|
||||
for i, r := range byLearningGoal {
|
||||
learningGoals[i] = domain.AnalyticsLabelCount{Label: r.LearningGoal, Count: r.Count}
|
||||
}
|
||||
languageChallanges := make([]domain.AnalyticsLabelCount, len(byLanguageChallange))
|
||||
for i, r := range byLanguageChallange {
|
||||
languageChallanges[i] = domain.AnalyticsLabelCount{Label: r.LanguageChallange, Count: r.Count}
|
||||
}
|
||||
knowledge := make([]domain.AnalyticsLabelCount, len(byKnowledge))
|
||||
for i, r := range byKnowledge {
|
||||
knowledge[i] = domain.AnalyticsLabelCount{Label: r.KnowledgeLevel, Count: r.Count}
|
||||
|
|
@ -261,6 +301,10 @@ func mapUsersSection(
|
|||
ByRole: roles,
|
||||
ByStatus: statuses,
|
||||
ByAgeGroup: ages,
|
||||
ByEducationLevel: education,
|
||||
ByOccupation: occupations,
|
||||
ByLearningGoal: learningGoals,
|
||||
ByLanguageChallange: languageChallanges,
|
||||
ByKnowledgeLevel: knowledge,
|
||||
ByRegion: regions,
|
||||
RegistrationsLast30Days: timePoints,
|
||||
|
|
|
|||
|
|
@ -13,6 +13,10 @@ type analyticsQueryParams struct {
|
|||
UsersByRole dbgen.AnalyticsUsersByRoleParams
|
||||
UsersByStatus dbgen.AnalyticsUsersByStatusParams
|
||||
UsersByAgeGroup dbgen.AnalyticsUsersByAgeGroupParams
|
||||
UsersByEducationLevel dbgen.AnalyticsUsersByEducationLevelParams
|
||||
UsersByOccupation dbgen.AnalyticsUsersByOccupationParams
|
||||
UsersByLearningGoal dbgen.AnalyticsUsersByLearningGoalParams
|
||||
UsersByLanguageChallange dbgen.AnalyticsUsersByLanguageChallangeParams
|
||||
UsersByKnowledgeLevel dbgen.AnalyticsUsersByKnowledgeLevelParams
|
||||
UsersByRegion dbgen.AnalyticsUsersByRegionParams
|
||||
UserRegistrationsSeries dbgen.AnalyticsUserRegistrationsLast30DaysParams
|
||||
|
|
@ -59,6 +63,10 @@ func newAnalyticsQueryParams(f domain.AnalyticsDateFilter) analyticsQueryParams
|
|||
UsersByRole: dbgen.AnalyticsUsersByRoleParams{RangeStart: rs, RangeEnd: re},
|
||||
UsersByStatus: dbgen.AnalyticsUsersByStatusParams{RangeStart: rs, RangeEnd: re},
|
||||
UsersByAgeGroup: dbgen.AnalyticsUsersByAgeGroupParams{RangeStart: rs, RangeEnd: re},
|
||||
UsersByEducationLevel: dbgen.AnalyticsUsersByEducationLevelParams{RangeStart: rs, RangeEnd: re},
|
||||
UsersByOccupation: dbgen.AnalyticsUsersByOccupationParams{RangeStart: rs, RangeEnd: re},
|
||||
UsersByLearningGoal: dbgen.AnalyticsUsersByLearningGoalParams{RangeStart: rs, RangeEnd: re},
|
||||
UsersByLanguageChallange: dbgen.AnalyticsUsersByLanguageChallangeParams{RangeStart: rs, RangeEnd: re},
|
||||
UsersByKnowledgeLevel: dbgen.AnalyticsUsersByKnowledgeLevelParams{RangeStart: rs, RangeEnd: re},
|
||||
UsersByRegion: dbgen.AnalyticsUsersByRegionParams{RangeStart: rs, RangeEnd: re},
|
||||
UserRegistrationsSeries: series,
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user