Yimaru-BackEnd/internal/web_server/handlers/analytics_handler.go

360 lines
14 KiB
Go

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