360 lines
14 KiB
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,
|
|
}
|
|
}
|