package handlers import ( dbgen "Yimaru-Backend/gen/db" "Yimaru-Backend/internal/domain" "time" "github.com/gofiber/fiber/v2" ) func toTime(v interface{}) time.Time { if t, ok := v.(time.Time); ok { return t } return time.Time{} } func (h *Handler) GetAnalyticsDashboard(c *fiber.Ctx) error { ctx := c.Context() // ── Users ── usersSummary, err := h.analyticsDB.AnalyticsUsersSummary(ctx) if err != nil { return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch user analytics") } usersByRole, err := h.analyticsDB.AnalyticsUsersByRole(ctx) if err != nil { return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch users by role") } usersByStatus, err := h.analyticsDB.AnalyticsUsersByStatus(ctx) if err != nil { return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch users by status") } usersByAge, err := h.analyticsDB.AnalyticsUsersByAgeGroup(ctx) if err != nil { return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch users by age group") } usersByKnowledge, err := h.analyticsDB.AnalyticsUsersByKnowledgeLevel(ctx) if err != nil { return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch users by knowledge level") } usersByRegion, err := h.analyticsDB.AnalyticsUsersByRegion(ctx) if err != nil { return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch users by region") } userRegs, err := h.analyticsDB.AnalyticsUserRegistrationsLast30Days(ctx) if err != nil { return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch user registrations") } // ── Subscriptions ── subsSummary, err := h.analyticsDB.AnalyticsSubscriptionsSummary(ctx) if err != nil { return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch subscription analytics") } subsByStatus, err := h.analyticsDB.AnalyticsSubscriptionsByStatus(ctx) if err != nil { return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch subscriptions by status") } revenueByPlan, err := h.analyticsDB.AnalyticsRevenueByPlan(ctx) if err != nil { return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch revenue by plan") } newSubs30, err := h.analyticsDB.AnalyticsNewSubscriptionsLast30Days(ctx) if err != nil { return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch new subscriptions last 30 days") } // ── Payments ── paymentsSummary, err := h.analyticsDB.AnalyticsPaymentsSummary(ctx) if err != nil { return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch payment analytics") } paymentsByStatus, err := h.analyticsDB.AnalyticsPaymentsByStatus(ctx) if err != nil { return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch payments by status") } paymentsByMethod, err := h.analyticsDB.AnalyticsPaymentsByMethod(ctx) if err != nil { return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch payments by method") } revenue30, err := h.analyticsDB.AnalyticsRevenueLast30Days(ctx) if err != nil { return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch revenue last 30 days") } // ── Courses ── courseCounts, err := h.analyticsDB.AnalyticsCourseCounts(ctx) if err != nil { return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch course analytics") } // ── Content ── questionsCounts, err := h.analyticsDB.AnalyticsQuestionsCounts(ctx) if err != nil { return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch content analytics") } questionsByType, err := h.analyticsDB.AnalyticsQuestionsByType(ctx) if err != nil { return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch questions by type") } questionSetsByType, err := h.analyticsDB.AnalyticsQuestionSetsByType(ctx) if err != nil { return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch question sets by type") } // ── Notifications ── notifSummary, err := h.analyticsDB.AnalyticsNotificationsSummary(ctx) if err != nil { return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch notification analytics") } notifByChannel, err := h.analyticsDB.AnalyticsNotificationsByChannel(ctx) if err != nil { return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch notifications by channel") } notifByType, err := h.analyticsDB.AnalyticsNotificationsByType(ctx) if err != nil { return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch notifications by type") } // ── Issues ── issuesSummary, err := h.analyticsDB.AnalyticsIssuesSummary(ctx) if err != nil { return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch issue analytics") } issuesByStatus, err := h.analyticsDB.AnalyticsIssuesByStatus(ctx) if err != nil { return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch issues by status") } issuesByType, err := h.analyticsDB.AnalyticsIssuesByType(ctx) if err != nil { return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch issues by type") } // ── Team ── teamTotal, err := h.analyticsDB.AnalyticsTeamSummary(ctx) if err != nil { return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch team analytics") } teamByRole, err := h.analyticsDB.AnalyticsTeamByRole(ctx) if err != nil { return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch team by role") } teamByStatus, err := h.analyticsDB.AnalyticsTeamByStatus(ctx) if err != nil { return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch team by status") } // ── Map to domain types ── dashboard := domain.AnalyticsDashboard{ GeneratedAt: time.Now(), Users: mapUsersSection(usersSummary, usersByRole, usersByStatus, usersByAge, usersByKnowledge, usersByRegion, userRegs), Subscriptions: mapSubscriptionsSection(subsSummary, subsByStatus, revenueByPlan, newSubs30), Payments: mapPaymentsSection(paymentsSummary, paymentsByStatus, paymentsByMethod, revenue30), Courses: domain.AnalyticsCoursesSection{ TotalCategories: courseCounts.TotalCategories, TotalCourses: courseCounts.TotalCourses, TotalSubCourses: courseCounts.TotalSubCourses, TotalVideos: courseCounts.TotalVideos, }, Content: mapContentSection(questionsCounts, questionsByType, questionSetsByType), Notifications: mapNotificationsSection(notifSummary, notifByChannel, notifByType), Issues: mapIssuesSection(issuesSummary, issuesByStatus, issuesByType), Team: mapTeamSection(teamTotal, teamByRole, teamByStatus), } return c.JSON(dashboard) } func mapUsersSection( summary dbgen.AnalyticsUsersSummaryRow, byRole []dbgen.AnalyticsUsersByRoleRow, byStatus []dbgen.AnalyticsUsersByStatusRow, byAge []dbgen.AnalyticsUsersByAgeGroupRow, byKnowledge []dbgen.AnalyticsUsersByKnowledgeLevelRow, byRegion []dbgen.AnalyticsUsersByRegionRow, regs []dbgen.AnalyticsUserRegistrationsLast30DaysRow, ) domain.AnalyticsUsersSection { roles := make([]domain.AnalyticsLabelCount, len(byRole)) for i, r := range byRole { roles[i] = domain.AnalyticsLabelCount{Label: r.Role, Count: r.Count} } statuses := make([]domain.AnalyticsLabelCount, len(byStatus)) for i, r := range byStatus { statuses[i] = domain.AnalyticsLabelCount{Label: r.Status, Count: r.Count} } ages := make([]domain.AnalyticsLabelCount, len(byAge)) for i, r := range byAge { ages[i] = domain.AnalyticsLabelCount{Label: r.AgeGroup, Count: r.Count} } knowledge := make([]domain.AnalyticsLabelCount, len(byKnowledge)) for i, r := range byKnowledge { knowledge[i] = domain.AnalyticsLabelCount{Label: r.KnowledgeLevel, Count: r.Count} } regions := make([]domain.AnalyticsLabelCount, len(byRegion)) for i, r := range byRegion { regions[i] = domain.AnalyticsLabelCount{Label: r.Region, Count: r.Count} } timePoints := make([]domain.AnalyticsTimePoint, len(regs)) for i, r := range regs { timePoints[i] = domain.AnalyticsTimePoint{Date: toTime(r.Date), Count: r.Count} } return domain.AnalyticsUsersSection{ TotalUsers: summary.Total, NewToday: summary.NewToday, NewWeek: summary.NewThisWeek, NewMonth: summary.NewThisMonth, ByRole: roles, ByStatus: statuses, ByAgeGroup: ages, ByKnowledgeLevel: knowledge, ByRegion: regions, RegistrationsLast30Days: timePoints, } } func mapSubscriptionsSection( summary dbgen.AnalyticsSubscriptionsSummaryRow, byStatus []dbgen.AnalyticsSubscriptionsByStatusRow, byPlan []dbgen.AnalyticsRevenueByPlanRow, newSubs []dbgen.AnalyticsNewSubscriptionsLast30DaysRow, ) domain.AnalyticsSubscriptionsSection { statuses := make([]domain.AnalyticsLabelCount, len(byStatus)) for i, r := range byStatus { statuses[i] = domain.AnalyticsLabelCount{Label: r.Status, Count: r.Count} } plans := make([]domain.AnalyticsRevenueByPlan, len(byPlan)) for i, r := range byPlan { plans[i] = domain.AnalyticsRevenueByPlan{PlanName: r.PlanName, Currency: r.Currency, Revenue: r.TotalRevenue} } timePoints := make([]domain.AnalyticsTimePoint, len(newSubs)) for i, r := range newSubs { timePoints[i] = domain.AnalyticsTimePoint{Date: toTime(r.Date), Count: r.Count} } return domain.AnalyticsSubscriptionsSection{ TotalSubscriptions: summary.Total, ActiveSubscriptions: summary.Active, NewToday: summary.NewToday, NewWeek: summary.NewThisWeek, NewMonth: summary.NewThisMonth, ByStatus: statuses, RevenueByPlan: plans, NewSubscriptionsLast30Days: timePoints, } } func mapPaymentsSection( summary dbgen.AnalyticsPaymentsSummaryRow, byStatus []dbgen.AnalyticsPaymentsByStatusRow, byMethod []dbgen.AnalyticsPaymentsByMethodRow, revenue []dbgen.AnalyticsRevenueLast30DaysRow, ) domain.AnalyticsPaymentsSection { statuses := make([]domain.AnalyticsLabelAmount, len(byStatus)) for i, r := range byStatus { statuses[i] = domain.AnalyticsLabelAmount{Label: r.Status, Count: r.Count, Amount: r.TotalAmount} } methods := make([]domain.AnalyticsLabelAmount, len(byMethod)) for i, r := range byMethod { methods[i] = domain.AnalyticsLabelAmount{Label: r.PaymentMethod, Count: r.Count, Amount: r.TotalAmount} } timePoints := make([]domain.AnalyticsRevenueTimePoint, len(revenue)) for i, r := range revenue { timePoints[i] = domain.AnalyticsRevenueTimePoint{Date: toTime(r.Date), Revenue: r.TotalRevenue} } return domain.AnalyticsPaymentsSection{ TotalRevenue: summary.TotalRevenue, AvgTransactionValue: summary.AvgValue, TotalPayments: summary.TotalPayments, SuccessfulPayments: summary.SuccessfulPayments, ByStatus: statuses, ByMethod: methods, RevenueLast30Days: timePoints, } } func mapContentSection( counts dbgen.AnalyticsQuestionsCountsRow, byType []dbgen.AnalyticsQuestionsByTypeRow, setsByType []dbgen.AnalyticsQuestionSetsByTypeRow, ) domain.AnalyticsContentSection { qTypes := make([]domain.AnalyticsLabelCount, len(byType)) for i, r := range byType { qTypes[i] = domain.AnalyticsLabelCount{Label: r.QuestionType, Count: r.Count} } sTypes := make([]domain.AnalyticsLabelCount, len(setsByType)) for i, r := range setsByType { sTypes[i] = domain.AnalyticsLabelCount{Label: r.SetType, Count: r.Count} } return domain.AnalyticsContentSection{ TotalQuestions: counts.TotalQuestions, TotalQuestionSets: counts.TotalQuestionSets, QuestionsByType: qTypes, QuestionSetsByType: sTypes, } } func mapNotificationsSection( summary dbgen.AnalyticsNotificationsSummaryRow, byChannel []dbgen.AnalyticsNotificationsByChannelRow, byType []dbgen.AnalyticsNotificationsByTypeRow, ) domain.AnalyticsNotificationsSection { channels := make([]domain.AnalyticsLabelCount, len(byChannel)) for i, r := range byChannel { channels[i] = domain.AnalyticsLabelCount{Label: r.Channel, Count: r.Count} } types := make([]domain.AnalyticsLabelCount, len(byType)) for i, r := range byType { types[i] = domain.AnalyticsLabelCount{Label: r.Type, Count: r.Count} } return domain.AnalyticsNotificationsSection{ TotalSent: summary.Total, ReadCount: summary.Read, UnreadCount: summary.Unread, ByChannel: channels, ByType: types, } } func mapIssuesSection( summary dbgen.AnalyticsIssuesSummaryRow, byStatus []dbgen.AnalyticsIssuesByStatusRow, byType []dbgen.AnalyticsIssuesByTypeRow, ) domain.AnalyticsIssuesSection { statuses := make([]domain.AnalyticsLabelCount, len(byStatus)) for i, r := range byStatus { statuses[i] = domain.AnalyticsLabelCount{Label: r.Status, Count: r.Count} } types := make([]domain.AnalyticsLabelCount, len(byType)) for i, r := range byType { types[i] = domain.AnalyticsLabelCount{Label: r.IssueType, Count: r.Count} } return domain.AnalyticsIssuesSection{ TotalIssues: summary.Total, ResolvedIssues: summary.Resolved, ResolutionRate: summary.ResolutionRate, ByStatus: statuses, ByType: types, } } func mapTeamSection( totalMembers int64, byRole []dbgen.AnalyticsTeamByRoleRow, byStatus []dbgen.AnalyticsTeamByStatusRow, ) domain.AnalyticsTeamSection { roles := make([]domain.AnalyticsLabelCount, len(byRole)) for i, r := range byRole { roles[i] = domain.AnalyticsLabelCount{Label: r.TeamRole, Count: r.Count} } statuses := make([]domain.AnalyticsLabelCount, len(byStatus)) for i, r := range byStatus { statuses[i] = domain.AnalyticsLabelCount{Label: r.Status, Count: r.Count} } return domain.AnalyticsTeamSection{ TotalMembers: totalMembers, ByRole: roles, ByStatus: statuses, } }