package handlers import ( dbgen "Yimaru-Backend/gen/db" "Yimaru-Backend/internal/domain" "strconv" "strings" "github.com/gofiber/fiber/v2" "github.com/jackc/pgx/v5/pgtype" ) type createCourseSubCategoryReq struct { CategoryID int64 `json:"category_id"` Name string `json:"name"` Description *string `json:"description"` DisplayOrder *int32 `json:"display_order"` IsActive *bool `json:"is_active"` } type createLevelReq struct { CourseID int64 `json:"course_id"` CEFRLevel string `json:"cefr_level"` DisplayOrder *int32 `json:"display_order"` IsActive *bool `json:"is_active"` } type createModuleReq struct { LevelID int64 `json:"level_id"` Title string `json:"title"` Description *string `json:"description"` DisplayOrder *int32 `json:"display_order"` IsActive *bool `json:"is_active"` } type createSubModuleReq struct { ModuleID int64 `json:"module_id"` Title string `json:"title"` Description *string `json:"description"` DisplayOrder *int32 `json:"display_order"` IsActive *bool `json:"is_active"` } type createSubModuleVideoReq struct { SubModuleID int64 `json:"sub_module_id"` Title string `json:"title"` Description *string `json:"description"` VideoURL string `json:"video_url"` Duration *int32 `json:"duration"` Resolution *string `json:"resolution"` Visibility *string `json:"visibility"` InstructorID *string `json:"instructor_id"` Thumbnail *string `json:"thumbnail"` DisplayOrder *int32 `json:"display_order"` Status *string `json:"status"` } type attachSubModuleLessonReq struct { SubModuleID int64 `json:"sub_module_id"` QuestionSetID int64 `json:"question_set_id"` IntroVideoURL *string `json:"intro_video_url"` DisplayOrder *int32 `json:"display_order"` IsActive *bool `json:"is_active"` } type createSubModulePracticeReq struct { SubModuleID int64 `json:"sub_module_id"` Title string `json:"title"` Description *string `json:"description"` Thumbnail *string `json:"thumbnail"` IntroVideoURL *string `json:"intro_video_url"` QuestionSetID int64 `json:"question_set_id"` DisplayOrder *int32 `json:"display_order"` IsActive *bool `json:"is_active"` } func toText(v *string) pgtype.Text { if v == nil { return pgtype.Text{Valid: false} } return pgtype.Text{String: *v, Valid: true} } func toInt4(v *int32) pgtype.Int4 { if v == nil { return pgtype.Int4{Valid: false} } return pgtype.Int4{Int32: *v, Valid: true} } func boolOrNil(v *bool) interface{} { if v == nil { return nil } return *v } func intOrNil(v *int32) interface{} { if v == nil { return nil } return *v } // UnifiedHierarchy godoc // @Summary Get unified course hierarchy // @Description Returns full hierarchy: category -> sub-category -> course // @Tags course-management // @Produce json // @Success 200 {object} domain.Response // @Failure 500 {object} domain.ErrorResponse // @Router /api/v1/course-management/hierarchy [get] func (h *Handler) UnifiedHierarchy(c *fiber.Ctx) error { rows, err := h.analyticsDB.GetCoursesWithHierarchy(c.Context()) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to load hierarchy", Error: err.Error()}) } return c.JSON(domain.Response{Message: "Unified hierarchy retrieved successfully", Data: rows}) } // UnifiedHierarchyByCourse godoc // @Summary Get hierarchy for a course // @Description Returns hierarchy nodes for one course including levels/modules/sub-modules // @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}/hierarchy [get] func (h *Handler) UnifiedHierarchyByCourse(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()}) } rows, err := h.analyticsDB.GetFullHierarchyByCourseID(c.Context(), courseID) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to load course hierarchy", Error: err.Error()}) } return c.JSON(domain.Response{Message: "Course hierarchy retrieved successfully", Data: rows}) } // CreateCourseSubCategory godoc // @Summary Create course sub-category // @Description Creates a sub-category under a course category // @Tags course-management // @Accept json // @Produce json // @Param body body createCourseSubCategoryReq true "Create sub-category payload" // @Success 201 {object} domain.Response // @Failure 400 {object} domain.ErrorResponse // @Failure 500 {object} domain.ErrorResponse // @Router /api/v1/course-management/sub-categories [post] func (h *Handler) CreateCourseSubCategory(c *fiber.Ctx) error { var req createCourseSubCategoryReq if err := c.BodyParser(&req); err != nil { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "Invalid request body", Error: err.Error()}) } if req.CategoryID <= 0 || strings.TrimSpace(req.Name) == "" { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "category_id and name are required"}) } created, err := h.analyticsDB.CreateCourseSubCategory(c.Context(), dbgen.CreateCourseSubCategoryParams{ CategoryID: req.CategoryID, Name: req.Name, Description: toText(req.Description), Column4: intOrNil(req.DisplayOrder), Column5: boolOrNil(req.IsActive), }) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to create sub-category", Error: err.Error()}) } return c.Status(fiber.StatusCreated).JSON(domain.Response{Message: "Course sub-category created", Data: created}) } // CreateLevel godoc // @Summary Create level // @Description Creates a CEFR level under a course // @Tags course-management // @Accept json // @Produce json // @Param body body createLevelReq true "Create level payload" // @Success 201 {object} domain.Response // @Failure 400 {object} domain.ErrorResponse // @Failure 500 {object} domain.ErrorResponse // @Router /api/v1/course-management/levels [post] func (h *Handler) CreateLevel(c *fiber.Ctx) error { var req createLevelReq 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)) validCEFR := map[string]bool{"A1": true, "A2": true, "A3": true, "B1": true, "B2": true, "B3": true, "C1": true, "C2": true, "C3": true} if req.CourseID <= 0 || !validCEFR[req.CEFRLevel] { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "course_id and valid cefr_level are required"}) } created, err := h.analyticsDB.CreateLevel(c.Context(), dbgen.CreateLevelParams{ CourseID: req.CourseID, CefrLevel: req.CEFRLevel, Column3: intOrNil(req.DisplayOrder), Column4: boolOrNil(req.IsActive), }) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to create level", Error: err.Error()}) } return c.Status(fiber.StatusCreated).JSON(domain.Response{Message: "Level created", Data: created}) } // CreateModule godoc // @Summary Create module // @Description Creates a module under a level // @Tags course-management // @Accept json // @Produce json // @Param body body createModuleReq true "Create module payload" // @Success 201 {object} domain.Response // @Failure 400 {object} domain.ErrorResponse // @Failure 500 {object} domain.ErrorResponse // @Router /api/v1/course-management/modules [post] func (h *Handler) CreateModule(c *fiber.Ctx) error { var req createModuleReq if err := c.BodyParser(&req); err != nil { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "Invalid request body", Error: err.Error()}) } if req.LevelID <= 0 || strings.TrimSpace(req.Title) == "" { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "level_id and title are required"}) } created, err := h.analyticsDB.CreateModule(c.Context(), dbgen.CreateModuleParams{ LevelID: req.LevelID, Title: req.Title, Description: toText(req.Description), Column4: intOrNil(req.DisplayOrder), Column5: boolOrNil(req.IsActive), }) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to create module", Error: err.Error()}) } return c.Status(fiber.StatusCreated).JSON(domain.Response{Message: "Module created", Data: created}) } // CreateSubModule godoc // @Summary Create sub-module // @Description Creates a sub-module under a module // @Tags course-management // @Accept json // @Produce json // @Param body body createSubModuleReq true "Create sub-module payload" // @Success 201 {object} domain.Response // @Failure 400 {object} domain.ErrorResponse // @Failure 500 {object} domain.ErrorResponse // @Router /api/v1/course-management/sub-modules [post] func (h *Handler) CreateSubModule(c *fiber.Ctx) error { var req createSubModuleReq if err := c.BodyParser(&req); err != nil { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "Invalid request body", Error: err.Error()}) } if req.ModuleID <= 0 || strings.TrimSpace(req.Title) == "" { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "module_id and title are required"}) } created, err := h.analyticsDB.CreateSubModule(c.Context(), dbgen.CreateSubModuleParams{ ModuleID: req.ModuleID, Title: req.Title, Description: toText(req.Description), Column4: intOrNil(req.DisplayOrder), Column5: boolOrNil(req.IsActive), }) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to create sub-module", Error: err.Error()}) } return c.Status(fiber.StatusCreated).JSON(domain.Response{Message: "Sub-module created", Data: created}) } // CreateSubModuleVideo godoc // @Summary Create sub-module video // @Description Creates a video under a sub-module // @Tags course-management // @Accept json // @Produce json // @Param body body createSubModuleVideoReq true "Create sub-module video payload" // @Success 201 {object} domain.Response // @Failure 400 {object} domain.ErrorResponse // @Failure 500 {object} domain.ErrorResponse // @Router /api/v1/course-management/sub-module-videos [post] func (h *Handler) CreateSubModuleVideo(c *fiber.Ctx) error { var req createSubModuleVideoReq if err := c.BodyParser(&req); err != nil { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "Invalid request body", Error: err.Error()}) } if req.SubModuleID <= 0 || strings.TrimSpace(req.Title) == "" || strings.TrimSpace(req.VideoURL) == "" { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "sub_module_id, title and video_url are required"}) } created, err := h.analyticsDB.CreateSubModuleVideo(c.Context(), dbgen.CreateSubModuleVideoParams{ SubModuleID: req.SubModuleID, Title: req.Title, Description: toText(req.Description), VideoUrl: req.VideoURL, Duration: toInt4(req.Duration), Resolution: toText(req.Resolution), Column7: nil, Visibility: toText(req.Visibility), InstructorID: toText(req.InstructorID), Thumbnail: toText(req.Thumbnail), Column12: intOrNil(req.DisplayOrder), Column13: req.Status, VimeoID: pgtype.Text{Valid: false}, VimeoEmbedUrl: pgtype.Text{Valid: false}, VimeoPlayerHtml: pgtype.Text{Valid: false}, VimeoStatus: pgtype.Text{Valid: false}, Column18: nil, }) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to create sub-module video", Error: err.Error()}) } return c.Status(fiber.StatusCreated).JSON(domain.Response{Message: "Sub-module video created", Data: created}) } // AttachSubModuleLesson godoc // @Summary Attach lesson to sub-module // @Description Links a question set lesson to a sub-module // @Tags course-management // @Accept json // @Produce json // @Param body body attachSubModuleLessonReq true "Attach lesson payload" // @Success 201 {object} domain.Response // @Failure 400 {object} domain.ErrorResponse // @Failure 500 {object} domain.ErrorResponse // @Router /api/v1/course-management/sub-module-lessons [post] func (h *Handler) AttachSubModuleLesson(c *fiber.Ctx) error { var req attachSubModuleLessonReq if err := c.BodyParser(&req); err != nil { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "Invalid request body", Error: err.Error()}) } if req.SubModuleID <= 0 || req.QuestionSetID <= 0 { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "sub_module_id and question_set_id are required"}) } attached, err := h.analyticsDB.AttachQuestionSetLessonToSubModule(c.Context(), dbgen.AttachQuestionSetLessonToSubModuleParams{ SubModuleID: req.SubModuleID, QuestionSetID: req.QuestionSetID, IntroVideoUrl: toText(req.IntroVideoURL), Column4: intOrNil(req.DisplayOrder), Column5: boolOrNil(req.IsActive), }) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to attach lesson", Error: err.Error()}) } return c.Status(fiber.StatusCreated).JSON(domain.Response{Message: "Lesson attached to sub-module", Data: attached}) } // CreateSubModulePractice godoc // @Summary Create practice under sub-module // @Description Creates a sub-module practice with metadata and linked question set // @Tags course-management // @Accept json // @Produce json // @Param body body createSubModulePracticeReq true "Create practice payload" // @Success 201 {object} domain.Response // @Failure 400 {object} domain.ErrorResponse // @Failure 500 {object} domain.ErrorResponse // @Router /api/v1/course-management/sub-module-practices [post] func (h *Handler) CreateSubModulePractice(c *fiber.Ctx) error { var req createSubModulePracticeReq if err := c.BodyParser(&req); err != nil { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "Invalid request body", Error: err.Error()}) } if req.SubModuleID <= 0 || req.QuestionSetID <= 0 || strings.TrimSpace(req.Title) == "" { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "sub_module_id, title and question_set_id are required"}) } created, err := h.analyticsDB.CreateSubModulePractice(c.Context(), dbgen.CreateSubModulePracticeParams{ SubModuleID: req.SubModuleID, Title: req.Title, Description: toText(req.Description), Thumbnail: toText(req.Thumbnail), IntroVideoUrl: toText(req.IntroVideoURL), QuestionSetID: req.QuestionSetID, Column7: intOrNil(req.DisplayOrder), Column8: boolOrNil(req.IsActive), }) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to create practice", Error: err.Error()}) } return c.Status(fiber.StatusCreated).JSON(domain.Response{Message: "Practice created", Data: created}) }