diff --git a/internal/web_server/handlers/hierarchy_handler.go b/internal/web_server/handlers/hierarchy_handler.go index d4a4e21..c692832 100644 --- a/internal/web_server/handlers/hierarchy_handler.go +++ b/internal/web_server/handlers/hierarchy_handler.go @@ -157,6 +157,14 @@ func intOrNil(v *int32) interface{} { return *v } +func textPtr(v pgtype.Text) *string { + if !v.Valid { + return nil + } + s := v.String + return &s +} + // ListCourseCategories godoc // @Summary List course categories // @Description Legacy-compatible endpoint for listing course categories @@ -544,6 +552,125 @@ func (h *Handler) UnifiedHierarchyByCourse(c *fiber.Ctx) error { return c.JSON(domain.Response{Message: "Course hierarchy retrieved successfully", Data: rows}) } +// CourseLearningPath godoc +// @Summary Get course learning path +// @Description Legacy-compatible endpoint for course learning path +// @Tags course-management +// @Produce json +// @Param courseId path int true "Course ID" +// @Success 200 {object} domain.Response +// @Failure 400 {object} domain.ErrorResponse +// @Failure 500 {object} domain.ErrorResponse +// @Router /api/v1/course-management/courses/{courseId}/learning-path [get] +func (h *Handler) CourseLearningPath(c *fiber.Ctx) error { + courseID, err := strconv.ParseInt(c.Params("courseId"), 10, 64) + if err != nil || courseID <= 0 { + return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "Invalid course ID", Error: "courseId must be a positive integer"}) + } + + course, err := h.analyticsDB.GetCourseByID(c.Context(), courseID) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to load course", Error: err.Error()}) + } + category, err := h.analyticsDB.GetCourseCategoryByID(c.Context(), course.CategoryID) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to load course category", Error: err.Error()}) + } + rows, err := h.analyticsDB.GetFullHierarchyByCourseID(c.Context(), courseID) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to load course learning path", Error: err.Error()}) + } + + subCourseByID := map[int64]*domain.LearningPathSubCourse{} + subCourseOrder := make([]int64, 0) + for _, row := range rows { + if !row.SubModuleID.Valid { + continue + } + subModuleID := row.SubModuleID.Int64 + if _, exists := subCourseByID[subModuleID]; exists { + continue + } + title := "" + if row.SubModuleTitle.Valid { + title = row.SubModuleTitle.String + } + level := "" + if row.CefrLevel.Valid { + level = row.CefrLevel.String + } + subCourseByID[subModuleID] = &domain.LearningPathSubCourse{ + ID: subModuleID, + Title: title, + DisplayOrder: int32(len(subCourseOrder)), + Level: level, + SubLevel: level, + PrerequisiteCount: 0, + Prerequisites: []domain.LearningPathPrerequisite{}, + Videos: []domain.LearningPathVideo{}, + Practices: []domain.LearningPathPractice{}, + } + subCourseOrder = append(subCourseOrder, subModuleID) + } + + for _, subModuleID := range subCourseOrder { + node := subCourseByID[subModuleID] + videos, videoErr := h.analyticsDB.GetSubModuleVideos(c.Context(), subModuleID) + if videoErr != nil { + return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to load sub-module videos", Error: videoErr.Error()}) + } + for _, v := range videos { + node.Videos = append(node.Videos, domain.LearningPathVideo{ + ID: v.ID, + Title: v.Title, + Description: textPtr(v.Description), + VideoURL: v.VideoUrl, + Duration: int32(v.Duration.Int32), + Resolution: textPtr(v.Resolution), + DisplayOrder: v.DisplayOrder, + VimeoID: textPtr(v.VimeoID), + VimeoEmbedURL: textPtr(v.VimeoEmbedUrl), + VideoHostProvider: textPtr(v.VideoHostProvider), + }) + } + node.VideoCount = int64(len(node.Videos)) + + practices, practiceErr := h.analyticsDB.GetSubModulePractices(c.Context(), subModuleID) + if practiceErr != nil { + return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to load sub-module practices", Error: practiceErr.Error()}) + } + for _, p := range practices { + node.Practices = append(node.Practices, domain.LearningPathPractice{ + ID: p.ID, + Title: p.Title, + Description: textPtr(p.Description), + Status: p.Status, + IntroVideoURL: textPtr(p.IntroVideoUrl), + QuestionCount: p.QuestionCount, + }) + } + node.PracticeCount = int64(len(node.Practices)) + } + + subCourses := make([]domain.LearningPathSubCourse, 0, len(subCourseOrder)) + for _, id := range subCourseOrder { + subCourses = append(subCourses, *subCourseByID[id]) + } + + path := domain.LearningPath{ + CourseID: course.ID, + CourseTitle: course.Title, + Description: textPtr(course.Description), + Thumbnail: textPtr(course.Thumbnail), + IntroVideoURL: textPtr(course.IntroVideoUrl), + CategoryID: category.ID, + CategoryName: category.Name, + SubCourses: subCourses, + } + + return c.JSON(domain.Response{Message: "Course learning path retrieved successfully", Data: path}) +} + func isMissingCourseSubCategoryTableErr(err error) bool { if err == nil { return false diff --git a/internal/web_server/routes.go b/internal/web_server/routes.go index 50f0784..c3a575c 100644 --- a/internal/web_server/routes.go +++ b/internal/web_server/routes.go @@ -87,6 +87,7 @@ func (a *App) initAppRoutes() { groupV1.Put("/course-management/courses/:courseId", a.authMiddleware, a.RequirePermission("courses.update"), h.UpdateCourse) groupV1.Delete("/course-management/courses/:courseId", a.authMiddleware, a.RequirePermission("courses.delete"), h.DeleteCourse) groupV1.Post("/course-management/courses/:courseId/thumbnail", a.authMiddleware, a.RequirePermission("courses.upload_thumbnail"), h.UpdateCourseThumbnail) + groupV1.Get("/course-management/courses/:courseId/learning-path", a.authMiddleware, a.RequirePermission("learning_tree.get"), h.CourseLearningPath) groupV1.Get("/course-management/hierarchy", a.authMiddleware, a.RequirePermission("learning_tree.get"), h.UnifiedHierarchy) groupV1.Get("/course-management/human-language/hierarchy", a.authMiddleware, a.RequirePermission("learning_tree.get"), h.UnifiedHierarchy) groupV1.Get("/course-management/courses/:courseId/hierarchy", a.authMiddleware, a.RequirePermission("learning_tree.get"), h.UnifiedHierarchyByCourse)