Fix analytics dashboard course counts for LMS and exam_prep hierarchies.
Replace stub AnalyticsCourseCounts query and expose lms / exam_prep inventory in the courses section. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
7f8ef3373c
commit
a1696bf1e0
|
|
@ -209,13 +209,34 @@ ORDER BY d.date;
|
|||
-- =====================
|
||||
-- Course Analytics
|
||||
-- =====================
|
||||
-- LMS: programs -> courses -> modules -> lessons; lms_practices attach to course, module, or lesson.
|
||||
-- Exam prep: exam_prep.catalog_courses -> units -> unit_modules -> unit_module_lessons; exam_prep.lesson_practices.
|
||||
-- Legacy dashboard fields map to: programs, courses, modules, and video-bearing lessons (LMS + exam prep).
|
||||
|
||||
-- name: AnalyticsCourseCounts :one
|
||||
SELECT
|
||||
0::bigint AS total_categories,
|
||||
0::bigint AS total_courses,
|
||||
0::bigint AS total_sub_courses,
|
||||
0::bigint AS total_videos;
|
||||
(SELECT COUNT(*)::bigint FROM programs) AS total_categories,
|
||||
(SELECT COUNT(*)::bigint FROM courses) AS total_courses,
|
||||
(SELECT COUNT(*)::bigint FROM modules) AS total_sub_courses,
|
||||
(
|
||||
COALESCE((SELECT COUNT(*)::bigint FROM lessons WHERE NULLIF(BTRIM(video_url), '') IS NOT NULL), 0::bigint)
|
||||
+ COALESCE((SELECT COUNT(*)::bigint FROM exam_prep.unit_module_lessons WHERE NULLIF(BTRIM(video_url), '') IS NOT NULL), 0::bigint)
|
||||
)::bigint AS total_videos,
|
||||
(SELECT COUNT(*)::bigint FROM programs) AS lms_programs,
|
||||
(SELECT COUNT(*)::bigint FROM courses) AS lms_courses,
|
||||
(SELECT COUNT(*)::bigint FROM modules) AS lms_modules,
|
||||
(SELECT COUNT(*)::bigint FROM lessons) AS lms_lessons,
|
||||
(SELECT COUNT(*)::bigint FROM lessons WHERE NULLIF(BTRIM(video_url), '') IS NOT NULL) AS lms_lessons_with_video,
|
||||
(SELECT COUNT(*)::bigint FROM lms_practices) AS lms_practices,
|
||||
(SELECT COUNT(*)::bigint FROM lms_practices WHERE course_id IS NOT NULL) AS lms_practices_at_course,
|
||||
(SELECT COUNT(*)::bigint FROM lms_practices WHERE module_id IS NOT NULL) AS lms_practices_at_module,
|
||||
(SELECT COUNT(*)::bigint FROM lms_practices WHERE lesson_id IS NOT NULL) AS lms_practices_at_lesson,
|
||||
(SELECT COUNT(*)::bigint FROM exam_prep.catalog_courses) AS exam_prep_catalog_courses,
|
||||
(SELECT COUNT(*)::bigint FROM exam_prep.units) AS exam_prep_units,
|
||||
(SELECT COUNT(*)::bigint FROM exam_prep.unit_modules) AS exam_prep_unit_modules,
|
||||
(SELECT COUNT(*)::bigint FROM exam_prep.unit_module_lessons) AS exam_prep_lessons,
|
||||
(SELECT COUNT(*)::bigint FROM exam_prep.unit_module_lessons WHERE NULLIF(BTRIM(video_url), '') IS NOT NULL) AS exam_prep_lessons_with_video,
|
||||
(SELECT COUNT(*)::bigint FROM exam_prep.lesson_practices) AS exam_prep_lesson_practices;
|
||||
|
||||
-- =====================
|
||||
-- Content Analytics
|
||||
|
|
|
|||
62
docs/docs.go
62
docs/docs.go
|
|
@ -9638,7 +9638,14 @@ const docTemplate = `{
|
|||
"domain.AnalyticsCoursesSection": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"exam_prep": {
|
||||
"$ref": "#/definitions/domain.AnalyticsExamPrepContentCounts"
|
||||
},
|
||||
"lms": {
|
||||
"$ref": "#/definitions/domain.AnalyticsLMSContentCounts"
|
||||
},
|
||||
"total_categories": {
|
||||
"description": "Top-level keys preserved for existing clients: map to LMS programs, courses, modules, and all video lessons (LMS + exam prep).",
|
||||
"type": "integer"
|
||||
},
|
||||
"total_courses": {
|
||||
|
|
@ -9722,6 +9729,29 @@ const docTemplate = `{
|
|||
}
|
||||
}
|
||||
},
|
||||
"domain.AnalyticsExamPrepContentCounts": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"catalog_courses": {
|
||||
"type": "integer"
|
||||
},
|
||||
"lesson_practices": {
|
||||
"type": "integer"
|
||||
},
|
||||
"lessons": {
|
||||
"type": "integer"
|
||||
},
|
||||
"lessons_with_video": {
|
||||
"type": "integer"
|
||||
},
|
||||
"unit_modules": {
|
||||
"type": "integer"
|
||||
},
|
||||
"units": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"domain.AnalyticsIssuesSection": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
|
@ -9748,6 +9778,38 @@ const docTemplate = `{
|
|||
}
|
||||
}
|
||||
},
|
||||
"domain.AnalyticsLMSContentCounts": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"courses": {
|
||||
"type": "integer"
|
||||
},
|
||||
"lessons": {
|
||||
"type": "integer"
|
||||
},
|
||||
"lessons_with_video": {
|
||||
"type": "integer"
|
||||
},
|
||||
"modules": {
|
||||
"type": "integer"
|
||||
},
|
||||
"practices": {
|
||||
"type": "integer"
|
||||
},
|
||||
"practices_at_course": {
|
||||
"type": "integer"
|
||||
},
|
||||
"practices_at_lesson": {
|
||||
"type": "integer"
|
||||
},
|
||||
"practices_at_module": {
|
||||
"type": "integer"
|
||||
},
|
||||
"programs": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"domain.AnalyticsLabelAmount": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
|
|
|||
|
|
@ -9630,7 +9630,14 @@
|
|||
"domain.AnalyticsCoursesSection": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"exam_prep": {
|
||||
"$ref": "#/definitions/domain.AnalyticsExamPrepContentCounts"
|
||||
},
|
||||
"lms": {
|
||||
"$ref": "#/definitions/domain.AnalyticsLMSContentCounts"
|
||||
},
|
||||
"total_categories": {
|
||||
"description": "Top-level keys preserved for existing clients: map to LMS programs, courses, modules, and all video lessons (LMS + exam prep).",
|
||||
"type": "integer"
|
||||
},
|
||||
"total_courses": {
|
||||
|
|
@ -9714,6 +9721,29 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"domain.AnalyticsExamPrepContentCounts": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"catalog_courses": {
|
||||
"type": "integer"
|
||||
},
|
||||
"lesson_practices": {
|
||||
"type": "integer"
|
||||
},
|
||||
"lessons": {
|
||||
"type": "integer"
|
||||
},
|
||||
"lessons_with_video": {
|
||||
"type": "integer"
|
||||
},
|
||||
"unit_modules": {
|
||||
"type": "integer"
|
||||
},
|
||||
"units": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"domain.AnalyticsIssuesSection": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
|
@ -9740,6 +9770,38 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"domain.AnalyticsLMSContentCounts": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"courses": {
|
||||
"type": "integer"
|
||||
},
|
||||
"lessons": {
|
||||
"type": "integer"
|
||||
},
|
||||
"lessons_with_video": {
|
||||
"type": "integer"
|
||||
},
|
||||
"modules": {
|
||||
"type": "integer"
|
||||
},
|
||||
"practices": {
|
||||
"type": "integer"
|
||||
},
|
||||
"practices_at_course": {
|
||||
"type": "integer"
|
||||
},
|
||||
"practices_at_lesson": {
|
||||
"type": "integer"
|
||||
},
|
||||
"practices_at_module": {
|
||||
"type": "integer"
|
||||
},
|
||||
"programs": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"domain.AnalyticsLabelAmount": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
|
|
|||
|
|
@ -34,7 +34,13 @@ definitions:
|
|||
type: object
|
||||
domain.AnalyticsCoursesSection:
|
||||
properties:
|
||||
exam_prep:
|
||||
$ref: '#/definitions/domain.AnalyticsExamPrepContentCounts'
|
||||
lms:
|
||||
$ref: '#/definitions/domain.AnalyticsLMSContentCounts'
|
||||
total_categories:
|
||||
description: 'Top-level keys preserved for existing clients: map to LMS programs,
|
||||
courses, modules, and all video lessons (LMS + exam prep).'
|
||||
type: integer
|
||||
total_courses:
|
||||
type: integer
|
||||
|
|
@ -89,6 +95,21 @@ definitions:
|
|||
year:
|
||||
type: integer
|
||||
type: object
|
||||
domain.AnalyticsExamPrepContentCounts:
|
||||
properties:
|
||||
catalog_courses:
|
||||
type: integer
|
||||
lesson_practices:
|
||||
type: integer
|
||||
lessons:
|
||||
type: integer
|
||||
lessons_with_video:
|
||||
type: integer
|
||||
unit_modules:
|
||||
type: integer
|
||||
units:
|
||||
type: integer
|
||||
type: object
|
||||
domain.AnalyticsIssuesSection:
|
||||
properties:
|
||||
by_status:
|
||||
|
|
@ -106,6 +127,27 @@ definitions:
|
|||
total_issues:
|
||||
type: integer
|
||||
type: object
|
||||
domain.AnalyticsLMSContentCounts:
|
||||
properties:
|
||||
courses:
|
||||
type: integer
|
||||
lessons:
|
||||
type: integer
|
||||
lessons_with_video:
|
||||
type: integer
|
||||
modules:
|
||||
type: integer
|
||||
practices:
|
||||
type: integer
|
||||
practices_at_course:
|
||||
type: integer
|
||||
practices_at_lesson:
|
||||
type: integer
|
||||
practices_at_module:
|
||||
type: integer
|
||||
programs:
|
||||
type: integer
|
||||
type: object
|
||||
domain.AnalyticsLabelAmount:
|
||||
properties:
|
||||
amount:
|
||||
|
|
|
|||
|
|
@ -14,10 +14,28 @@ import (
|
|||
const AnalyticsCourseCounts = `-- name: AnalyticsCourseCounts :one
|
||||
|
||||
SELECT
|
||||
0::bigint AS total_categories,
|
||||
0::bigint AS total_courses,
|
||||
0::bigint AS total_sub_courses,
|
||||
0::bigint AS total_videos
|
||||
(SELECT COUNT(*)::bigint FROM programs) AS total_categories,
|
||||
(SELECT COUNT(*)::bigint FROM courses) AS total_courses,
|
||||
(SELECT COUNT(*)::bigint FROM modules) AS total_sub_courses,
|
||||
(
|
||||
COALESCE((SELECT COUNT(*)::bigint FROM lessons WHERE NULLIF(BTRIM(video_url), '') IS NOT NULL), 0::bigint)
|
||||
+ COALESCE((SELECT COUNT(*)::bigint FROM exam_prep.unit_module_lessons WHERE NULLIF(BTRIM(video_url), '') IS NOT NULL), 0::bigint)
|
||||
)::bigint AS total_videos,
|
||||
(SELECT COUNT(*)::bigint FROM programs) AS lms_programs,
|
||||
(SELECT COUNT(*)::bigint FROM courses) AS lms_courses,
|
||||
(SELECT COUNT(*)::bigint FROM modules) AS lms_modules,
|
||||
(SELECT COUNT(*)::bigint FROM lessons) AS lms_lessons,
|
||||
(SELECT COUNT(*)::bigint FROM lessons WHERE NULLIF(BTRIM(video_url), '') IS NOT NULL) AS lms_lessons_with_video,
|
||||
(SELECT COUNT(*)::bigint FROM lms_practices) AS lms_practices,
|
||||
(SELECT COUNT(*)::bigint FROM lms_practices WHERE course_id IS NOT NULL) AS lms_practices_at_course,
|
||||
(SELECT COUNT(*)::bigint FROM lms_practices WHERE module_id IS NOT NULL) AS lms_practices_at_module,
|
||||
(SELECT COUNT(*)::bigint FROM lms_practices WHERE lesson_id IS NOT NULL) AS lms_practices_at_lesson,
|
||||
(SELECT COUNT(*)::bigint FROM exam_prep.catalog_courses) AS exam_prep_catalog_courses,
|
||||
(SELECT COUNT(*)::bigint FROM exam_prep.units) AS exam_prep_units,
|
||||
(SELECT COUNT(*)::bigint FROM exam_prep.unit_modules) AS exam_prep_unit_modules,
|
||||
(SELECT COUNT(*)::bigint FROM exam_prep.unit_module_lessons) AS exam_prep_lessons,
|
||||
(SELECT COUNT(*)::bigint FROM exam_prep.unit_module_lessons WHERE NULLIF(BTRIM(video_url), '') IS NOT NULL) AS exam_prep_lessons_with_video,
|
||||
(SELECT COUNT(*)::bigint FROM exam_prep.lesson_practices) AS exam_prep_lesson_practices
|
||||
`
|
||||
|
||||
type AnalyticsCourseCountsRow struct {
|
||||
|
|
@ -25,11 +43,29 @@ type AnalyticsCourseCountsRow struct {
|
|||
TotalCourses int64 `json:"total_courses"`
|
||||
TotalSubCourses int64 `json:"total_sub_courses"`
|
||||
TotalVideos int64 `json:"total_videos"`
|
||||
LmsPrograms int64 `json:"lms_programs"`
|
||||
LmsCourses int64 `json:"lms_courses"`
|
||||
LmsModules int64 `json:"lms_modules"`
|
||||
LmsLessons int64 `json:"lms_lessons"`
|
||||
LmsLessonsWithVideo int64 `json:"lms_lessons_with_video"`
|
||||
LmsPractices int64 `json:"lms_practices"`
|
||||
LmsPracticesAtCourse int64 `json:"lms_practices_at_course"`
|
||||
LmsPracticesAtModule int64 `json:"lms_practices_at_module"`
|
||||
LmsPracticesAtLesson int64 `json:"lms_practices_at_lesson"`
|
||||
ExamPrepCatalogCourses int64 `json:"exam_prep_catalog_courses"`
|
||||
ExamPrepUnits int64 `json:"exam_prep_units"`
|
||||
ExamPrepUnitModules int64 `json:"exam_prep_unit_modules"`
|
||||
ExamPrepLessons int64 `json:"exam_prep_lessons"`
|
||||
ExamPrepLessonsWithVideo int64 `json:"exam_prep_lessons_with_video"`
|
||||
ExamPrepLessonPractices int64 `json:"exam_prep_lesson_practices"`
|
||||
}
|
||||
|
||||
// =====================
|
||||
// Course Analytics
|
||||
// =====================
|
||||
// LMS: programs -> courses -> modules -> lessons; lms_practices attach to course, module, or lesson.
|
||||
// Exam prep: exam_prep.catalog_courses -> units -> unit_modules -> unit_module_lessons; exam_prep.lesson_practices.
|
||||
// Legacy dashboard fields map to: programs, courses, modules, and video-bearing lessons (LMS + exam prep).
|
||||
func (q *Queries) AnalyticsCourseCounts(ctx context.Context) (AnalyticsCourseCountsRow, error) {
|
||||
row := q.db.QueryRow(ctx, AnalyticsCourseCounts)
|
||||
var i AnalyticsCourseCountsRow
|
||||
|
|
@ -38,6 +74,21 @@ func (q *Queries) AnalyticsCourseCounts(ctx context.Context) (AnalyticsCourseCou
|
|||
&i.TotalCourses,
|
||||
&i.TotalSubCourses,
|
||||
&i.TotalVideos,
|
||||
&i.LmsPrograms,
|
||||
&i.LmsCourses,
|
||||
&i.LmsModules,
|
||||
&i.LmsLessons,
|
||||
&i.LmsLessonsWithVideo,
|
||||
&i.LmsPractices,
|
||||
&i.LmsPracticesAtCourse,
|
||||
&i.LmsPracticesAtModule,
|
||||
&i.LmsPracticesAtLesson,
|
||||
&i.ExamPrepCatalogCourses,
|
||||
&i.ExamPrepUnits,
|
||||
&i.ExamPrepUnitModules,
|
||||
&i.ExamPrepLessons,
|
||||
&i.ExamPrepLessonsWithVideo,
|
||||
&i.ExamPrepLessonPractices,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -69,11 +69,38 @@ type AnalyticsPaymentsSection struct {
|
|||
RevenueLast30Days []AnalyticsRevenueTimePoint `json:"revenue_last_30_days"`
|
||||
}
|
||||
|
||||
// AnalyticsLMSContentCounts reflects the LMS hierarchy (Learn English): programs → courses → modules → lessons.
|
||||
type AnalyticsLMSContentCounts struct {
|
||||
Programs int64 `json:"programs"`
|
||||
Courses int64 `json:"courses"`
|
||||
Modules int64 `json:"modules"`
|
||||
Lessons int64 `json:"lessons"`
|
||||
LessonsWithVideo int64 `json:"lessons_with_video"`
|
||||
Practices int64 `json:"practices"`
|
||||
PracticesAtCourse int64 `json:"practices_at_course"`
|
||||
PracticesAtModule int64 `json:"practices_at_module"`
|
||||
PracticesAtLesson int64 `json:"practices_at_lesson"`
|
||||
}
|
||||
|
||||
// AnalyticsExamPrepContentCounts reflects the exam_prep schema: catalog_courses → units → unit_modules → lessons → lesson_practices.
|
||||
type AnalyticsExamPrepContentCounts struct {
|
||||
CatalogCourses int64 `json:"catalog_courses"`
|
||||
Units int64 `json:"units"`
|
||||
UnitModules int64 `json:"unit_modules"`
|
||||
Lessons int64 `json:"lessons"`
|
||||
LessonsWithVideo int64 `json:"lessons_with_video"`
|
||||
LessonPractices int64 `json:"lesson_practices"`
|
||||
}
|
||||
|
||||
type AnalyticsCoursesSection struct {
|
||||
// Top-level keys preserved for existing clients: map to LMS programs, courses, modules, and all video lessons (LMS + exam prep).
|
||||
TotalCategories int64 `json:"total_categories"`
|
||||
TotalCourses int64 `json:"total_courses"`
|
||||
TotalSubCourses int64 `json:"total_sub_courses"`
|
||||
TotalVideos int64 `json:"total_videos"`
|
||||
|
||||
LMS AnalyticsLMSContentCounts `json:"lms"`
|
||||
ExamPrep AnalyticsExamPrepContentCounts `json:"exam_prep"`
|
||||
}
|
||||
|
||||
type AnalyticsContentSection struct {
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ func toTime(v interface{}) 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.
|
||||
// @Description Platform analytics with optional date filters: all-time (default), year, year+month, or custom from/to range. The courses section includes LMS (programs→courses→modules→lessons, lms_practices) and exam_prep (catalog_courses→units→unit_modules→lessons, lesson_practices) inventory counts.
|
||||
// @Tags analytics
|
||||
// @Produce json
|
||||
// @Param year query int false "Calendar year (e.g. 2025)"
|
||||
|
|
@ -166,12 +166,7 @@ func (h *Handler) GetAnalyticsDashboard(c *fiber.Ctx) error {
|
|||
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,
|
||||
},
|
||||
Courses: mapCoursesSection(courseCounts),
|
||||
Content: mapContentSection(questionsCounts, questionsByType, questionSetsByType),
|
||||
Notifications: mapNotificationsSection(notifSummary, notifByChannel, notifByType),
|
||||
Issues: mapIssuesSection(issuesSummary, issuesByStatus, issuesByType),
|
||||
|
|
@ -181,6 +176,34 @@ func (h *Handler) GetAnalyticsDashboard(c *fiber.Ctx) error {
|
|||
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,
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user