diff --git a/db/query/analytics.sql b/db/query/analytics.sql index 391ebaf..6ccfa92 100644 --- a/db/query/analytics.sql +++ b/db/query/analytics.sql @@ -108,6 +108,16 @@ WHERE (sqlc.narg('range_start')::timestamptz IS NULL OR u.created_at >= sqlc.nar GROUP BY COALESCE(NULLIF(TRIM(u.language_challange), ''), 'unknown') ORDER BY count DESC; +-- name: AnalyticsUsersByCountry :many +SELECT + COALESCE(NULLIF(TRIM(u.country), ''), 'unknown')::text AS country, + 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.country), ''), 'unknown') +ORDER BY count DESC; + -- name: AnalyticsUsersByRegion :many SELECT COALESCE(u.region, 'unknown') AS region, diff --git a/gen/db/analytics.sql.go b/gen/db/analytics.sql.go index 2c1b053..cbd1dd5 100644 --- a/gen/db/analytics.sql.go +++ b/gen/db/analytics.sql.go @@ -1080,6 +1080,47 @@ func (q *Queries) AnalyticsUsersByAgeGroup(ctx context.Context, arg AnalyticsUse return items, nil } +const AnalyticsUsersByCountry = `-- name: AnalyticsUsersByCountry :many +SELECT + COALESCE(NULLIF(TRIM(u.country), ''), 'unknown')::text AS country, + 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.country), ''), 'unknown') +ORDER BY count DESC +` + +type AnalyticsUsersByCountryParams struct { + RangeStart pgtype.Timestamptz `json:"range_start"` + RangeEnd pgtype.Timestamptz `json:"range_end"` +} + +type AnalyticsUsersByCountryRow struct { + Country string `json:"country"` + Count int64 `json:"count"` +} + +func (q *Queries) AnalyticsUsersByCountry(ctx context.Context, arg AnalyticsUsersByCountryParams) ([]AnalyticsUsersByCountryRow, error) { + rows, err := q.db.Query(ctx, AnalyticsUsersByCountry, arg.RangeStart, arg.RangeEnd) + if err != nil { + return nil, err + } + defer rows.Close() + var items []AnalyticsUsersByCountryRow + for rows.Next() { + var i AnalyticsUsersByCountryRow + if err := rows.Scan(&i.Country, &i.Count); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + const AnalyticsUsersByEducationLevel = `-- name: AnalyticsUsersByEducationLevel :many SELECT COALESCE(NULLIF(TRIM(u.education_level), ''), 'unknown')::text AS education_level, diff --git a/internal/domain/analytics.go b/internal/domain/analytics.go index aa9ddea..0441cb8 100644 --- a/internal/domain/analytics.go +++ b/internal/domain/analytics.go @@ -52,6 +52,7 @@ type AnalyticsUsersSection struct { ByLearningGoal []AnalyticsLabelCount `json:"by_learning_goal"` ByLanguageChallange []AnalyticsLabelCount `json:"by_language_challange"` ByKnowledgeLevel []AnalyticsLabelCount `json:"by_knowledge_level"` + ByCountry []AnalyticsLabelCount `json:"by_country"` ByRegion []AnalyticsLabelCount `json:"by_region"` RegistrationsLast30Days []AnalyticsTimePoint `json:"registrations_last_30_days"` diff --git a/internal/web_server/handlers/analytics_handler.go b/internal/web_server/handlers/analytics_handler.go index 35ebb08..79c9d07 100644 --- a/internal/web_server/handlers/analytics_handler.go +++ b/internal/web_server/handlers/analytics_handler.go @@ -81,6 +81,10 @@ func (h *Handler) GetAnalyticsDashboard(c *fiber.Ctx) error { if err != nil { return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch users by knowledge level") } + usersByCountry, err := h.analyticsDB.AnalyticsUsersByCountry(ctx, p.UsersByCountry) + if err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch users by country") + } usersByRegion, err := h.analyticsDB.AnalyticsUsersByRegion(ctx, p.UsersByRegion) if err != nil { return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch users by region") @@ -198,7 +202,7 @@ func (h *Handler) GetAnalyticsDashboard(c *fiber.Ctx) error { Users: mapUsersSection( usersSummary, usersByRole, usersByStatus, usersByAge, usersByEducation, usersByOccupation, usersByLearningGoal, usersByLanguageChallange, - usersByKnowledge, usersByRegion, userRegs, + usersByKnowledge, usersByCountry, usersByRegion, userRegs, ), Subscriptions: mapSubscriptionsSection(subsSummary, subsByStatus, revenueByPlan, newSubs30), Payments: mapPaymentsSection(paymentsSummary, paymentsByStatus, paymentsByMethod, revenue30, revenueMonthlyRows, monthlyRevenueYear), @@ -250,6 +254,7 @@ func mapUsersSection( byLearningGoal []dbgen.AnalyticsUsersByLearningGoalRow, byLanguageChallange []dbgen.AnalyticsUsersByLanguageChallangeRow, byKnowledge []dbgen.AnalyticsUsersByKnowledgeLevelRow, + byCountry []dbgen.AnalyticsUsersByCountryRow, byRegion []dbgen.AnalyticsUsersByRegionRow, regs []dbgen.AnalyticsUserRegistrationsLast30DaysRow, ) domain.AnalyticsUsersSection { @@ -285,6 +290,10 @@ func mapUsersSection( for i, r := range byKnowledge { knowledge[i] = domain.AnalyticsLabelCount{Label: r.KnowledgeLevel, Count: r.Count} } + countries := make([]domain.AnalyticsLabelCount, len(byCountry)) + for i, r := range byCountry { + countries[i] = domain.AnalyticsLabelCount{Label: r.Country, Count: r.Count} + } regions := make([]domain.AnalyticsLabelCount, len(byRegion)) for i, r := range byRegion { regions[i] = domain.AnalyticsLabelCount{Label: r.Region, Count: r.Count} @@ -306,6 +315,7 @@ func mapUsersSection( ByLearningGoal: learningGoals, ByLanguageChallange: languageChallanges, ByKnowledgeLevel: knowledge, + ByCountry: countries, ByRegion: regions, RegistrationsLast30Days: timePoints, } diff --git a/internal/web_server/handlers/analytics_params.go b/internal/web_server/handlers/analytics_params.go index b11ab1a..a21c5c4 100644 --- a/internal/web_server/handlers/analytics_params.go +++ b/internal/web_server/handlers/analytics_params.go @@ -18,6 +18,7 @@ type analyticsQueryParams struct { UsersByLearningGoal dbgen.AnalyticsUsersByLearningGoalParams UsersByLanguageChallange dbgen.AnalyticsUsersByLanguageChallangeParams UsersByKnowledgeLevel dbgen.AnalyticsUsersByKnowledgeLevelParams + UsersByCountry dbgen.AnalyticsUsersByCountryParams UsersByRegion dbgen.AnalyticsUsersByRegionParams UserRegistrationsSeries dbgen.AnalyticsUserRegistrationsLast30DaysParams @@ -68,6 +69,7 @@ func newAnalyticsQueryParams(f domain.AnalyticsDateFilter) analyticsQueryParams UsersByLearningGoal: dbgen.AnalyticsUsersByLearningGoalParams{RangeStart: rs, RangeEnd: re}, UsersByLanguageChallange: dbgen.AnalyticsUsersByLanguageChallangeParams{RangeStart: rs, RangeEnd: re}, UsersByKnowledgeLevel: dbgen.AnalyticsUsersByKnowledgeLevelParams{RangeStart: rs, RangeEnd: re}, + UsersByCountry: dbgen.AnalyticsUsersByCountryParams{RangeStart: rs, RangeEnd: re}, UsersByRegion: dbgen.AnalyticsUsersByRegionParams{RangeStart: rs, RangeEnd: re}, UserRegistrationsSeries: series,