diff --git a/internal/web_server/handlers/course_management.go b/internal/web_server/handlers/course_management.go index f4c6da5..33928ab 100644 --- a/internal/web_server/handlers/course_management.go +++ b/internal/web_server/handlers/course_management.go @@ -614,6 +614,252 @@ func isValidSubLevelForLevel(level, subLevel string) bool { } } +func isValidHumanLanguageCEFRLevel(level string) bool { + switch strings.ToUpper(strings.TrimSpace(level)) { + case string(domain.SubCourseSubLevelA1), + string(domain.SubCourseSubLevelA2), + string(domain.SubCourseSubLevelA3), + string(domain.SubCourseSubLevelB1), + string(domain.SubCourseSubLevelB2), + string(domain.SubCourseSubLevelB3), + string(domain.SubCourseSubLevelC1), + string(domain.SubCourseSubLevelC2), + string(domain.SubCourseSubLevelC3): + return true + default: + return false + } +} + +func coarseLevelFromCEFR(cefr string) string { + switch strings.ToUpper(strings.TrimSpace(cefr)) { + case string(domain.SubCourseSubLevelA1), string(domain.SubCourseSubLevelA2), string(domain.SubCourseSubLevelA3): + return string(domain.SubCourseLevelBeginner) + case string(domain.SubCourseSubLevelB1), string(domain.SubCourseSubLevelB2), string(domain.SubCourseSubLevelB3): + return string(domain.SubCourseLevelIntermediate) + default: + return string(domain.SubCourseLevelAdvanced) + } +} + +func (h *Handler) ensureCourseIsHumanLanguage(ctx context.Context, courseID int64) (domain.Course, error) { + course, err := h.courseMgmtSvc.GetCourseByID(ctx, courseID) + if err != nil { + return domain.Course{}, err + } + category, err := h.courseMgmtSvc.GetCourseCategoryByID(ctx, course.CategoryID) + if err != nil { + return domain.Course{}, err + } + categoryName := strings.ToLower(strings.TrimSpace(category.Name)) + if !strings.Contains(categoryName, "language") { + return domain.Course{}, fmt.Errorf("course is not under a human language category") + } + return course, nil +} + +type createHumanLanguageLessonReq struct { + CourseID int64 `json:"course_id" validate:"required"` + Title string `json:"title" validate:"required"` + Description *string `json:"description"` + Thumbnail *string `json:"thumbnail"` + DisplayOrder *int32 `json:"display_order"` + CEFRLevel string `json:"cefr_level" validate:"required"` +} + +type updateHumanLanguageLessonReq struct { + Title *string `json:"title"` + Description *string `json:"description"` + Thumbnail *string `json:"thumbnail"` + DisplayOrder *int32 `json:"display_order"` + CEFRLevel *string `json:"cefr_level"` + IsActive *bool `json:"is_active"` +} + +type humanLanguageLessonRes struct { + ID int64 `json:"id"` + CourseID int64 `json:"course_id"` + Title string `json:"title"` + Description *string `json:"description,omitempty"` + Thumbnail *string `json:"thumbnail,omitempty"` + DisplayOrder int32 `json:"display_order"` + Level string `json:"level"` + VideoCount int64 `json:"video_count"` + PracticeCount int64 `json:"practice_count"` + Videos []domain.LearningPathVideo `json:"videos"` + Practices []domain.LearningPathPractice `json:"practices"` +} + +type getHumanLanguageLessonsRes struct { + CourseID int64 `json:"course_id"` + CourseTitle string `json:"course_title"` + CEFRLevel string `json:"cefr_level"` + Lessons []humanLanguageLessonRes `json:"lessons"` +} + +// CreateHumanLanguageLesson godoc +// @Summary Create human-language lesson unit +// @Description Creates a lesson unit under a human-language course using CEFR level (A1..C3) +// @Tags human-language +// @Accept json +// @Produce json +// @Param body body createHumanLanguageLessonReq true "Create human-language lesson payload" +// @Success 201 {object} domain.Response +// @Failure 400 {object} domain.ErrorResponse +// @Failure 500 {object} domain.ErrorResponse +// @Router /api/v1/course-management/human-language/lessons [post] +func (h *Handler) CreateHumanLanguageLesson(c *fiber.Ctx) error { + var req createHumanLanguageLessonReq + if err := c.BodyParser(&req); err != nil { + return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "Invalid request body", Error: err.Error()}) + } + req.CEFRLevel = strings.ToUpper(strings.TrimSpace(req.CEFRLevel)) + if !isValidHumanLanguageCEFRLevel(req.CEFRLevel) { + return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ + Message: "Invalid CEFR level", + Error: "Use one of: A1,A2,A3,B1,B2,B3,C1,C2,C3", + }) + } + if _, err := h.ensureCourseIsHumanLanguage(c.Context(), req.CourseID); err != nil { + return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "Invalid human-language course", Error: err.Error()}) + } + + created, err := h.courseMgmtSvc.CreateSubCourse( + c.Context(), + req.CourseID, + req.Title, + req.Description, + req.Thumbnail, + req.DisplayOrder, + coarseLevelFromCEFR(req.CEFRLevel), + req.CEFRLevel, + ) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to create human-language lesson", Error: err.Error()}) + } + + return c.Status(fiber.StatusCreated).JSON(domain.Response{ + Message: "Human-language lesson created successfully", + Data: subCourseRes{ + ID: created.ID, + CourseID: created.CourseID, + Title: created.Title, + Description: created.Description, + Thumbnail: created.Thumbnail, + DisplayOrder: created.DisplayOrder, + Level: created.SubLevel, + SubLevel: "", + IsActive: created.IsActive, + }, + }) +} + +// UpdateHumanLanguageLesson godoc +// @Summary Update human-language lesson unit +// @Description Updates a human-language lesson unit and CEFR level +// @Tags human-language +// @Accept json +// @Produce json +// @Param id path int true "Lesson (sub-course) ID" +// @Param body body updateHumanLanguageLessonReq true "Update human-language lesson payload" +// @Success 200 {object} domain.Response +// @Failure 400 {object} domain.ErrorResponse +// @Failure 500 {object} domain.ErrorResponse +// @Router /api/v1/course-management/human-language/lessons/{id} [patch] +func (h *Handler) UpdateHumanLanguageLesson(c *fiber.Ctx) error { + id, err := strconv.ParseInt(c.Params("id"), 10, 64) + if err != nil { + return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "Invalid lesson ID", Error: err.Error()}) + } + var req updateHumanLanguageLessonReq + if err := c.BodyParser(&req); err != nil { + return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "Invalid request body", Error: err.Error()}) + } + + var levelPtr *string + var subLevelPtr *string + if req.CEFRLevel != nil { + normalized := strings.ToUpper(strings.TrimSpace(*req.CEFRLevel)) + if !isValidHumanLanguageCEFRLevel(normalized) { + return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ + Message: "Invalid CEFR level", + Error: "Use one of: A1,A2,A3,B1,B2,B3,C1,C2,C3", + }) + } + level := coarseLevelFromCEFR(normalized) + levelPtr = &level + subLevelPtr = &normalized + } + + if err := h.courseMgmtSvc.UpdateSubCourse(c.Context(), id, req.Title, req.Description, req.Thumbnail, req.DisplayOrder, levelPtr, subLevelPtr, req.IsActive); err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to update human-language lesson", Error: err.Error()}) + } + return c.JSON(domain.Response{Message: "Human-language lesson updated successfully"}) +} + +// GetHumanLanguageLessonsByCourse godoc +// @Summary Get human-language lessons by CEFR level +// @Description Returns lessons for a human-language course filtered by CEFR level (A1..C3) +// @Tags human-language +// @Produce json +// @Param courseId path int true "Course ID" +// @Param cefr_level query string true "CEFR level (A1..C3)" +// @Success 200 {object} domain.Response +// @Failure 400 {object} domain.ErrorResponse +// @Failure 500 {object} domain.ErrorResponse +// @Router /api/v1/course-management/human-language/courses/{courseId}/lessons [get] +func (h *Handler) GetHumanLanguageLessonsByCourse(c *fiber.Ctx) error { + courseID, err := strconv.ParseInt(c.Params("courseId"), 10, 64) + if err != nil { + return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "Invalid course ID", Error: err.Error()}) + } + cefrLevel := strings.ToUpper(strings.TrimSpace(c.Query("cefr_level"))) + if !isValidHumanLanguageCEFRLevel(cefrLevel) { + return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ + Message: "Invalid CEFR level", + Error: "Use one of: A1,A2,A3,B1,B2,B3,C1,C2,C3", + }) + } + if _, err := h.ensureCourseIsHumanLanguage(c.Context(), courseID); err != nil { + return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "Invalid human-language course", Error: err.Error()}) + } + + path, err := h.courseMgmtSvc.GetCourseLearningPath(c.Context(), courseID) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to retrieve learning path", Error: err.Error()}) + } + + lessons := make([]humanLanguageLessonRes, 0) + for _, sc := range path.SubCourses { + if strings.ToUpper(strings.TrimSpace(sc.SubLevel)) != cefrLevel { + continue + } + lessons = append(lessons, humanLanguageLessonRes{ + ID: sc.ID, + CourseID: courseID, + Title: sc.Title, + Description: sc.Description, + Thumbnail: sc.Thumbnail, + DisplayOrder: sc.DisplayOrder, + Level: sc.SubLevel, + VideoCount: int64(len(sc.Videos)), + PracticeCount: int64(len(sc.Practices)), + Videos: sc.Videos, + Practices: sc.Practices, + }) + } + + return c.JSON(domain.Response{ + Message: "Human-language lessons retrieved successfully", + Data: getHumanLanguageLessonsRes{ + CourseID: path.CourseID, + CourseTitle: path.CourseTitle, + CEFRLevel: cefrLevel, + Lessons: lessons, + }, + }) +} + // CreateSubCourse godoc // @Summary Create a new sub-course // @Description Creates a new sub-course under a specific course diff --git a/internal/web_server/routes.go b/internal/web_server/routes.go index 0b4ec02..929d42d 100644 --- a/internal/web_server/routes.go +++ b/internal/web_server/routes.go @@ -128,6 +128,9 @@ func (a *App) initAppRoutes() { // Learning Tree groupV1.Get("/course-management/learning-tree", a.authMiddleware, a.RequirePermission("learning_tree.get"), h.GetFullLearningTree) groupV1.Get("/course-management/courses/:courseId/learning-path", a.authMiddleware, a.RequirePermission("learning_tree.get"), h.GetCourseLearningPath) + groupV1.Get("/course-management/human-language/courses/:courseId/lessons", a.authMiddleware, a.RequirePermission("learning_tree.get"), h.GetHumanLanguageLessonsByCourse) + groupV1.Post("/course-management/human-language/lessons", a.authMiddleware, a.RequirePermission("subcourses.create"), h.CreateHumanLanguageLesson) + groupV1.Patch("/course-management/human-language/lessons/:id", a.authMiddleware, a.RequirePermission("subcourses.update"), h.UpdateHumanLanguageLesson) // Questions groupV1.Post("/questions", a.authMiddleware, a.RequirePermission("questions.create"), h.CreateQuestion)