Record playback heartbeats via POST /api/v1/videos/engagement/heartbeat and expose completion, replay, and drop-off rates on the analytics dashboard. Co-authored-by: Cursor <cursoragent@cursor.com>
536 lines
21 KiB
Go
536 lines
21 KiB
Go
package handlers
|
||
|
||
import (
|
||
dbgen "Yimaru-Backend/gen/db"
|
||
"Yimaru-Backend/internal/domain"
|
||
"time"
|
||
|
||
"github.com/gofiber/fiber/v2"
|
||
)
|
||
|
||
// Short month labels for analytics monthly charts (aligned with UTC calendar months).
|
||
var analyticsShortMonthLabels = []string{
|
||
"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
|
||
}
|
||
|
||
func toTime(v interface{}) time.Time {
|
||
if t, ok := v.(time.Time); ok {
|
||
return t
|
||
}
|
||
return time.Time{}
|
||
}
|
||
|
||
// GetAnalyticsDashboard godoc
|
||
// @Summary Analytics dashboard
|
||
// @Description Platform analytics with optional date filters: all-time (default), year, year+month, or custom from/to range. When year is set, payments.revenue_monthly returns Jan–Dec SUCCESS revenue totals (UTC) per currency for that calendar year — use for yearly revenue charts. Daily series remains in revenue_last_30_days (see date_filter.series_*). Courses section counts LMS + exam_prep inventory.
|
||
// @Tags analytics
|
||
// @Produce json
|
||
// @Param year query int false "Calendar year (e.g. 2025)"
|
||
// @Param month query int false "Calendar month 1-12 (requires year)"
|
||
// @Param from query string false "Custom range start (YYYY-MM-DD or RFC3339)"
|
||
// @Param to query string false "Custom range end (YYYY-MM-DD or RFC3339, inclusive)"
|
||
// @Success 200 {object} domain.AnalyticsDashboard
|
||
// @Failure 400 {object} domain.ErrorResponse
|
||
// @Failure 500 {object} domain.ErrorResponse
|
||
// @Router /api/v1/analytics/dashboard [get]
|
||
func (h *Handler) GetAnalyticsDashboard(c *fiber.Ctx) error {
|
||
filter, err := domain.ParseAnalyticsDateFilter(c)
|
||
if err != nil {
|
||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||
Message: "Invalid date filter",
|
||
Error: err.Error(),
|
||
})
|
||
}
|
||
|
||
ctx := c.Context()
|
||
p := newAnalyticsQueryParams(filter)
|
||
|
||
usersSummary, err := h.analyticsDB.AnalyticsUsersSummary(ctx, p.UsersSummary)
|
||
if err != nil {
|
||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch user analytics")
|
||
}
|
||
usersByRole, err := h.analyticsDB.AnalyticsUsersByRole(ctx, p.UsersByRole)
|
||
if err != nil {
|
||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch users by role")
|
||
}
|
||
usersByStatus, err := h.analyticsDB.AnalyticsUsersByStatus(ctx, p.UsersByStatus)
|
||
if err != nil {
|
||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch users by status")
|
||
}
|
||
usersByAge, err := h.analyticsDB.AnalyticsUsersByAgeGroup(ctx, p.UsersByAgeGroup)
|
||
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")
|
||
}
|
||
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")
|
||
}
|
||
userRegs, err := h.analyticsDB.AnalyticsUserRegistrationsLast30Days(ctx, p.UserRegistrationsSeries)
|
||
if err != nil {
|
||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch user registrations")
|
||
}
|
||
|
||
subsSummary, err := h.analyticsDB.AnalyticsSubscriptionsSummary(ctx, p.SubscriptionsSummary)
|
||
if err != nil {
|
||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch subscription analytics")
|
||
}
|
||
subsByStatus, err := h.analyticsDB.AnalyticsSubscriptionsByStatus(ctx, p.SubscriptionsByStatus)
|
||
if err != nil {
|
||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch subscriptions by status")
|
||
}
|
||
revenueByPlan, err := h.analyticsDB.AnalyticsRevenueByPlan(ctx, p.RevenueByPlan)
|
||
if err != nil {
|
||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch revenue by plan")
|
||
}
|
||
newSubs30, err := h.analyticsDB.AnalyticsNewSubscriptionsLast30Days(ctx, p.NewSubscriptionsSeries)
|
||
if err != nil {
|
||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch new subscriptions time series")
|
||
}
|
||
|
||
paymentsSummary, err := h.analyticsDB.AnalyticsPaymentsSummary(ctx, p.PaymentsSummary)
|
||
if err != nil {
|
||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch payment analytics")
|
||
}
|
||
paymentsByStatus, err := h.analyticsDB.AnalyticsPaymentsByStatus(ctx, p.PaymentsByStatus)
|
||
if err != nil {
|
||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch payments by status")
|
||
}
|
||
paymentsByMethod, err := h.analyticsDB.AnalyticsPaymentsByMethod(ctx, p.PaymentsByMethod)
|
||
if err != nil {
|
||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch payments by method")
|
||
}
|
||
revenue30, err := h.analyticsDB.AnalyticsRevenueLast30Days(ctx, p.RevenueSeries)
|
||
if err != nil {
|
||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch revenue time series")
|
||
}
|
||
|
||
var revenueMonthlyRows []dbgen.AnalyticsRevenueMonthlyByYearRow
|
||
var monthlyRevenueYear *int
|
||
if filter.Year != nil {
|
||
rowsMonthly, err := h.analyticsDB.AnalyticsRevenueMonthlyByYear(ctx, int32(*filter.Year))
|
||
if err != nil {
|
||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch monthly revenue series")
|
||
}
|
||
revenueMonthlyRows = rowsMonthly
|
||
monthlyRevenueYear = filter.Year
|
||
}
|
||
|
||
courseCounts, err := h.analyticsDB.AnalyticsCourseCounts(ctx)
|
||
if err != nil {
|
||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch course analytics")
|
||
}
|
||
|
||
questionsCounts, err := h.analyticsDB.AnalyticsQuestionsCounts(ctx, p.QuestionsCounts)
|
||
if err != nil {
|
||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch content analytics")
|
||
}
|
||
questionsByType, err := h.analyticsDB.AnalyticsQuestionsByType(ctx, p.QuestionsByType)
|
||
if err != nil {
|
||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch questions by type")
|
||
}
|
||
questionSetsByType, err := h.analyticsDB.AnalyticsQuestionSetsByType(ctx, p.QuestionSetsByType)
|
||
if err != nil {
|
||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch question sets by type")
|
||
}
|
||
|
||
notifSummary, err := h.analyticsDB.AnalyticsNotificationsSummary(ctx, p.NotificationsSummary)
|
||
if err != nil {
|
||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch notification analytics")
|
||
}
|
||
notifByChannel, err := h.analyticsDB.AnalyticsNotificationsByChannel(ctx, p.NotificationsChannel)
|
||
if err != nil {
|
||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch notifications by channel")
|
||
}
|
||
notifByType, err := h.analyticsDB.AnalyticsNotificationsByType(ctx, p.NotificationsType)
|
||
if err != nil {
|
||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch notifications by type")
|
||
}
|
||
|
||
issuesSummary, err := h.analyticsDB.AnalyticsIssuesSummary(ctx, p.IssuesSummary)
|
||
if err != nil {
|
||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch issue analytics")
|
||
}
|
||
issuesByStatus, err := h.analyticsDB.AnalyticsIssuesByStatus(ctx, p.IssuesByStatus)
|
||
if err != nil {
|
||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch issues by status")
|
||
}
|
||
issuesByType, err := h.analyticsDB.AnalyticsIssuesByType(ctx, p.IssuesByType)
|
||
if err != nil {
|
||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch issues by type")
|
||
}
|
||
|
||
teamTotal, err := h.analyticsDB.AnalyticsTeamSummary(ctx, p.TeamSummary)
|
||
if err != nil {
|
||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch team analytics")
|
||
}
|
||
teamByRole, err := h.analyticsDB.AnalyticsTeamByRole(ctx, p.TeamByRole)
|
||
if err != nil {
|
||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch team by role")
|
||
}
|
||
teamByStatus, err := h.analyticsDB.AnalyticsTeamByStatus(ctx, p.TeamByStatus)
|
||
if err != nil {
|
||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch team by status")
|
||
}
|
||
|
||
videoSummary, err := h.analyticsDB.AnalyticsVideoEngagementSummary(ctx, p.VideoEngagementSummary)
|
||
if err != nil {
|
||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch video engagement analytics")
|
||
}
|
||
videoDropOff, err := h.analyticsDB.AnalyticsVideoDropOffByCheckpoint(ctx, p.VideoDropOffByCheckpoint)
|
||
if err != nil {
|
||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch video drop-off analytics")
|
||
}
|
||
|
||
dashboard := domain.AnalyticsDashboard{
|
||
GeneratedAt: time.Now().UTC(),
|
||
DateFilter: filter,
|
||
Users: mapUsersSection(
|
||
usersSummary, usersByRole, usersByStatus, usersByAge,
|
||
usersByEducation, usersByOccupation, usersByLearningGoal, usersByLanguageChallange,
|
||
usersByKnowledge, usersByCountry, usersByRegion, userRegs,
|
||
),
|
||
Subscriptions: mapSubscriptionsSection(subsSummary, subsByStatus, revenueByPlan, newSubs30),
|
||
Payments: mapPaymentsSection(paymentsSummary, paymentsByStatus, paymentsByMethod, revenue30, revenueMonthlyRows, monthlyRevenueYear),
|
||
Courses: mapCoursesSection(courseCounts),
|
||
Videos: mapVideosSection(videoSummary, videoDropOff),
|
||
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 mapCoursesSection(r dbgen.AnalyticsCourseCountsRow) domain.AnalyticsCoursesSection {
|
||
return domain.AnalyticsCoursesSection{
|
||
TotalCategories: r.TotalCategories,
|
||
TotalCourses: r.TotalCourses,
|
||
TotalSubCourses: r.TotalSubCourses,
|
||
TotalVideos: r.TotalVideos,
|
||
LMS: domain.AnalyticsLMSContentCounts{
|
||
Programs: r.LmsPrograms,
|
||
Courses: r.LmsCourses,
|
||
Modules: r.LmsModules,
|
||
Lessons: r.LmsLessons,
|
||
LessonsWithVideo: r.LmsLessonsWithVideo,
|
||
Practices: r.LmsPractices,
|
||
PracticesAtCourse: r.LmsPracticesAtCourse,
|
||
PracticesAtModule: r.LmsPracticesAtModule,
|
||
PracticesAtLesson: r.LmsPracticesAtLesson,
|
||
},
|
||
ExamPrep: domain.AnalyticsExamPrepContentCounts{
|
||
CatalogCourses: r.ExamPrepCatalogCourses,
|
||
Units: r.ExamPrepUnits,
|
||
UnitModules: r.ExamPrepUnitModules,
|
||
Lessons: r.ExamPrepLessons,
|
||
LessonsWithVideo: r.ExamPrepLessonsWithVideo,
|
||
LessonPractices: r.ExamPrepLessonPractices,
|
||
},
|
||
}
|
||
}
|
||
|
||
func mapUsersSection(
|
||
summary dbgen.AnalyticsUsersSummaryRow,
|
||
byRole []dbgen.AnalyticsUsersByRoleRow,
|
||
byStatus []dbgen.AnalyticsUsersByStatusRow,
|
||
byAge []dbgen.AnalyticsUsersByAgeGroupRow,
|
||
byEducation []dbgen.AnalyticsUsersByEducationLevelRow,
|
||
byOccupation []dbgen.AnalyticsUsersByOccupationRow,
|
||
byLearningGoal []dbgen.AnalyticsUsersByLearningGoalRow,
|
||
byLanguageChallange []dbgen.AnalyticsUsersByLanguageChallangeRow,
|
||
byKnowledge []dbgen.AnalyticsUsersByKnowledgeLevelRow,
|
||
byCountry []dbgen.AnalyticsUsersByCountryRow,
|
||
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}
|
||
}
|
||
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}
|
||
}
|
||
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}
|
||
}
|
||
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,
|
||
ByEducationLevel: education,
|
||
ByOccupation: occupations,
|
||
ByLearningGoal: learningGoals,
|
||
ByLanguageChallange: languageChallanges,
|
||
ByKnowledgeLevel: knowledge,
|
||
ByCountry: countries,
|
||
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,
|
||
revenueMonthly []dbgen.AnalyticsRevenueMonthlyByYearRow,
|
||
monthlyYear *int,
|
||
) 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}
|
||
}
|
||
monthlyPoints := make([]domain.AnalyticsMonthlyRevenuePoint, 0, len(revenueMonthly))
|
||
for _, r := range revenueMonthly {
|
||
m := int(r.Month)
|
||
label := ""
|
||
if m >= 1 && m <= 12 {
|
||
label = analyticsShortMonthLabels[m-1]
|
||
}
|
||
ms := time.Time{}
|
||
if r.MonthStart.Valid {
|
||
t := r.MonthStart.Time.UTC()
|
||
ms = time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, time.UTC)
|
||
}
|
||
monthlyPoints = append(monthlyPoints, domain.AnalyticsMonthlyRevenuePoint{
|
||
Month: m,
|
||
MonthStart: ms,
|
||
Label: label,
|
||
Currency: r.Currency,
|
||
Revenue: r.TotalRevenue,
|
||
})
|
||
}
|
||
|
||
return domain.AnalyticsPaymentsSection{
|
||
TotalRevenue: summary.TotalRevenue,
|
||
AvgTransactionValue: summary.AvgValue,
|
||
TotalPayments: summary.TotalPayments,
|
||
SuccessfulPayments: summary.SuccessfulPayments,
|
||
ByStatus: statuses,
|
||
ByMethod: methods,
|
||
RevenueLast30Days: timePoints,
|
||
RevenueMonthly: monthlyPoints,
|
||
MonthlyRevenueYear: monthlyYear,
|
||
}
|
||
}
|
||
|
||
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,
|
||
}
|
||
}
|
||
|
||
func mapVideosSection(
|
||
summary dbgen.AnalyticsVideoEngagementSummaryRow,
|
||
dropOff []dbgen.AnalyticsVideoDropOffByCheckpointRow,
|
||
) domain.AnalyticsVideosSection {
|
||
checkpoints := make([]domain.AnalyticsVideoDropOffPoint, len(dropOff))
|
||
for i, r := range dropOff {
|
||
checkpoints[i] = domain.AnalyticsVideoDropOffPoint{
|
||
CheckpointPercent: int(r.CheckpointPercent),
|
||
TotalSessions: r.TotalSessions,
|
||
ViewersReached: r.ViewersReached,
|
||
DropOffRate: r.DropOffRate,
|
||
}
|
||
}
|
||
|
||
var completionRate, replayRate float64
|
||
if summary.TotalSessions > 0 {
|
||
completionRate = float64(summary.CompletedSessions) / float64(summary.TotalSessions)
|
||
}
|
||
if summary.UniqueVideoStarts > 0 {
|
||
replayRate = float64(summary.UsersWhoReplayed) / float64(summary.UniqueVideoStarts)
|
||
}
|
||
|
||
return domain.AnalyticsVideosSection{
|
||
TotalWatchSessions: summary.TotalSessions,
|
||
CompletedSessions: summary.CompletedSessions,
|
||
ReplaySessions: summary.ReplaySessions,
|
||
UniqueVideoStarts: summary.UniqueVideoStarts,
|
||
UsersWhoReplayed: summary.UsersWhoReplayed,
|
||
CompletionRate: completionRate,
|
||
ReplayRate: replayRate,
|
||
DropOffByCheckpoint: checkpoints,
|
||
}
|
||
}
|