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 createCourseCategoryReq struct { Name string `json:"name"` IsActive *bool `json:"is_active"` } type createCourseReq struct { CategoryID int64 `json:"category_id"` SubCategoryID *int64 `json:"sub_category_id"` Title string `json:"title"` Description *string `json:"description"` Thumbnail *string `json:"thumbnail"` IntroVideoURL *string `json:"intro_video_url"` IsActive *bool `json:"is_active"` } type updateCourseReq struct { Title *string `json:"title"` Description *string `json:"description"` Thumbnail *string `json:"thumbnail"` IntroVideoURL *string `json:"intro_video_url"` IsActive *bool `json:"is_active"` } type updateCourseThumbnailReq struct { ThumbnailURL string `json:"thumbnail_url"` } 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 updateSubModuleReq struct { Title *string `json:"title"` Description *string `json:"description"` IsActive *bool `json:"is_active"` } type updateSubModuleVideoReq struct { Title *string `json:"title"` Description *string `json:"description"` VideoURL *string `json:"video_url"` } type updatePracticeReq struct { Title *string `json:"title"` Description *string `json:"description"` Persona *string `json:"persona"` IsActive *bool `json:"is_active"` } 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 updateLessonQuestionReq struct { QuestionID int64 `json:"question_id"` DisplayOrder *int32 `json:"display_order"` } type updateSubModuleLessonReq 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"` Title *string `json:"title"` Description *string `json:"description"` BannerImage *string `json:"banner_image"` Persona *string `json:"persona"` TimeLimitMinutes *int32 `json:"time_limit_minutes"` PassingScore *int32 `json:"passing_score"` ShuffleQuestions *bool `json:"shuffle_questions"` Status *string `json:"status"` SubCourseVideoID *int64 `json:"sub_course_video_id"` Questions []updateLessonQuestionReq `json:"questions"` } 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"` } type legacyHierarchyRow struct { CategoryID int64 `json:"category_id"` CategoryName string `json:"category_name"` SubCategoryID *int64 `json:"sub_category_id"` SubCategoryName *string `json:"sub_category_name"` CourseID *int64 `json:"course_id"` CourseTitle *string `json:"course_title"` } 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 } 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 // @Tags course-management // @Produce json // @Success 200 {object} domain.Response // @Failure 500 {object} domain.ErrorResponse // @Router /api/v1/course-management/categories [get] func (h *Handler) ListCourseCategories(c *fiber.Ctx) error { rows, err := h.analyticsDB.GetAllCourseCategories(c.Context(), dbgen.GetAllCourseCategoriesParams{ Offset: pgtype.Int4{Int32: 0, Valid: true}, Limit: pgtype.Int4{Int32: 10000, Valid: true}, }) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to load categories", Error: err.Error()}) } total := 0 if len(rows) > 0 { total = int(rows[0].TotalCount) } return c.JSON(domain.Response{ Message: "Categories retrieved successfully", Data: map[string]interface{}{ "categories": rows, "total_count": total, }, }) } // ListCoursesByCategory godoc // @Summary List courses by category // @Description Legacy-compatible endpoint that returns courses for one category // @Tags course-management // @Produce json // @Param categoryId path int true "Category ID" // @Param offset query int false "Offset" // @Param limit query int false "Limit" // @Success 200 {object} domain.Response // @Failure 400 {object} domain.ErrorResponse // @Failure 500 {object} domain.ErrorResponse // @Router /api/v1/course-management/categories/{categoryId}/courses [get] func (h *Handler) ListCoursesByCategory(c *fiber.Ctx) error { categoryID, err := strconv.ParseInt(c.Params("categoryId"), 10, 64) if err != nil || categoryID <= 0 { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "Invalid category ID", Error: "categoryId must be a positive integer"}) } offset := int32(c.QueryInt("offset", 0)) if offset < 0 { offset = 0 } limit := int32(c.QueryInt("limit", 10000)) if limit <= 0 { limit = 10000 } rows, err := h.analyticsDB.GetCoursesByCategory(c.Context(), dbgen.GetCoursesByCategoryParams{ CategoryID: categoryID, Offset: pgtype.Int4{Int32: offset, Valid: true}, Limit: pgtype.Int4{Int32: limit, Valid: true}, }) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to load courses", Error: err.Error()}) } total := 0 if len(rows) > 0 { total = int(rows[0].TotalCount) } return c.JSON(domain.Response{ Message: "Courses retrieved successfully", Data: map[string]interface{}{ "courses": rows, "total_count": total, }, }) } // ListAllCourses godoc // @Summary List all courses // @Description Returns all courses with pagination // @Tags course-management // @Produce json // @Param offset query int false "Offset" // @Param limit query int false "Limit" // @Success 200 {object} domain.Response // @Failure 500 {object} domain.ErrorResponse // @Router /api/v1/course-management/courses [get] func (h *Handler) ListAllCourses(c *fiber.Ctx) error { offset := int32(c.QueryInt("offset", 0)) if offset < 0 { offset = 0 } limit := int32(c.QueryInt("limit", 10000)) if limit <= 0 { limit = 10000 } rows, err := h.analyticsDB.GetAllCourses(c.Context(), dbgen.GetAllCoursesParams{ Offset: pgtype.Int4{Int32: offset, Valid: true}, Limit: pgtype.Int4{Int32: limit, Valid: true}, }) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to load courses", Error: err.Error()}) } total := 0 if len(rows) > 0 { total = int(rows[0].TotalCount) } return c.JSON(domain.Response{ Message: "Courses retrieved successfully", Data: map[string]interface{}{ "courses": rows, "total_count": total, }, }) } // ListHumanLanguageCourses godoc // @Summary List Human Language courses // @Description Returns all courses under Human Language category // @Tags course-management // @Produce json // @Param offset query int false "Offset" // @Param limit query int false "Limit" // @Success 200 {object} domain.Response // @Failure 500 {object} domain.ErrorResponse // @Router /api/v1/course-management/human-language/courses [get] func (h *Handler) ListHumanLanguageCourses(c *fiber.Ctx) error { offset := int32(c.QueryInt("offset", 0)) if offset < 0 { offset = 0 } limit := int32(c.QueryInt("limit", 10000)) if limit <= 0 { limit = 10000 } rows, err := h.analyticsDB.GetHumanLanguageCourses(c.Context(), dbgen.GetHumanLanguageCoursesParams{ Offset: pgtype.Int4{Int32: offset, Valid: true}, Limit: pgtype.Int4{Int32: limit, Valid: true}, }) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to load Human Language courses", Error: err.Error()}) } total := 0 if len(rows) > 0 { total = int(rows[0].TotalCount) } return c.JSON(domain.Response{ Message: "Human Language courses retrieved successfully", Data: map[string]interface{}{ "courses": rows, "total_count": total, }, }) } // ListCoursesBySubCategory godoc // @Summary List courses by sub-category // @Description Returns courses for one sub-category // @Tags course-management // @Produce json // @Param subCategoryId path int true "Sub-category ID" // @Param offset query int false "Offset" // @Param limit query int false "Limit" // @Success 200 {object} domain.Response // @Failure 400 {object} domain.ErrorResponse // @Failure 500 {object} domain.ErrorResponse // @Router /api/v1/course-management/sub-categories/{subCategoryId}/courses [get] func (h *Handler) ListCoursesBySubCategory(c *fiber.Ctx) error { subCategoryID, err := strconv.ParseInt(c.Params("subCategoryId"), 10, 64) if err != nil || subCategoryID <= 0 { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "Invalid sub-category ID", Error: "subCategoryId must be a positive integer"}) } offset := int32(c.QueryInt("offset", 0)) if offset < 0 { offset = 0 } limit := int32(c.QueryInt("limit", 10000)) if limit <= 0 { limit = 10000 } rows, err := h.analyticsDB.GetCoursesBySubCategory(c.Context(), dbgen.GetCoursesBySubCategoryParams{ SubCategoryID: pgtype.Int8{Int64: subCategoryID, Valid: true}, Offset: pgtype.Int4{Int32: offset, Valid: true}, Limit: pgtype.Int4{Int32: limit, Valid: true}, }) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to load courses", Error: err.Error()}) } total := 0 if len(rows) > 0 { total = int(rows[0].TotalCount) } return c.JSON(domain.Response{ Message: "Courses retrieved successfully", Data: map[string]interface{}{ "courses": rows, "total_count": total, }, }) } // GetCourseByID godoc // @Summary Get course detail // @Description Returns one course by ID // @Tags course-management // @Produce json // @Param courseId path int true "Course ID" // @Success 200 {object} domain.Response // @Failure 400 {object} domain.ErrorResponse // @Failure 404 {object} domain.ErrorResponse // @Failure 500 {object} domain.ErrorResponse // @Router /api/v1/course-management/courses/{courseId} [get] func (h *Handler) GetCourseByID(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.StatusNotFound).JSON(domain.ErrorResponse{ Message: "Course not found", Error: err.Error(), }) } return c.JSON(domain.Response{ Message: "Course retrieved successfully", Data: course, }) } // ListAllLevels godoc // @Summary List all levels // @Description Returns all levels with pagination // @Tags course-management // @Produce json // @Param offset query int false "Offset" // @Param limit query int false "Limit" // @Success 200 {object} domain.Response // @Failure 500 {object} domain.ErrorResponse // @Router /api/v1/course-management/levels [get] func (h *Handler) ListAllLevels(c *fiber.Ctx) error { offset := int32(c.QueryInt("offset", 0)) if offset < 0 { offset = 0 } limit := int32(c.QueryInt("limit", 10000)) if limit <= 0 { limit = 10000 } rows, err := h.analyticsDB.GetAllLevels(c.Context(), dbgen.GetAllLevelsParams{ Offset: pgtype.Int4{Int32: offset, Valid: true}, Limit: pgtype.Int4{Int32: limit, Valid: true}, }) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to load levels", Error: err.Error()}) } total := 0 if len(rows) > 0 { total = int(rows[0].TotalCount) } return c.JSON(domain.Response{ Message: "Levels retrieved successfully", Data: map[string]interface{}{ "levels": rows, "total_count": total, }, }) } // ListLevelsByCourse godoc // @Summary List levels by course // @Description Returns all active levels for one course // @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}/levels [get] func (h *Handler) ListLevelsByCourse(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", }) } rows, err := h.analyticsDB.GetLevelsByCourseID(c.Context(), courseID) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to load levels", Error: err.Error()}) } return c.JSON(domain.Response{ Message: "Levels retrieved successfully", Data: map[string]interface{}{ "levels": rows, "total_count": len(rows), }, }) } // GetLevelByID godoc // @Summary Get level detail // @Description Returns one level by ID // @Tags course-management // @Produce json // @Param levelId path int true "Level ID" // @Success 200 {object} domain.Response // @Failure 400 {object} domain.ErrorResponse // @Failure 404 {object} domain.ErrorResponse // @Failure 500 {object} domain.ErrorResponse // @Router /api/v1/course-management/levels/{levelId} [get] func (h *Handler) GetLevelByID(c *fiber.Ctx) error { levelID, err := strconv.ParseInt(c.Params("levelId"), 10, 64) if err != nil || levelID <= 0 { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ Message: "Invalid level ID", Error: "levelId must be a positive integer", }) } level, err := h.analyticsDB.GetLevelByID(c.Context(), levelID) if err != nil { return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{ Message: "Level not found", Error: err.Error(), }) } return c.JSON(domain.Response{ Message: "Level retrieved successfully", Data: level, }) } // ListAllModules godoc // @Summary List all modules // @Description Returns all modules with pagination // @Tags course-management // @Produce json // @Param offset query int false "Offset" // @Param limit query int false "Limit" // @Success 200 {object} domain.Response // @Failure 500 {object} domain.ErrorResponse // @Router /api/v1/course-management/modules [get] func (h *Handler) ListAllModules(c *fiber.Ctx) error { offset := int32(c.QueryInt("offset", 0)) if offset < 0 { offset = 0 } limit := int32(c.QueryInt("limit", 10000)) if limit <= 0 { limit = 10000 } rows, err := h.analyticsDB.GetAllModules(c.Context(), dbgen.GetAllModulesParams{ Offset: pgtype.Int4{Int32: offset, Valid: true}, Limit: pgtype.Int4{Int32: limit, Valid: true}, }) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to load modules", Error: err.Error()}) } total := 0 if len(rows) > 0 { total = int(rows[0].TotalCount) } return c.JSON(domain.Response{ Message: "Modules retrieved successfully", Data: map[string]interface{}{ "modules": rows, "total_count": total, }, }) } // ListModulesByLevel godoc // @Summary List modules by level // @Description Returns all active modules for one level // @Tags course-management // @Produce json // @Param levelId path int true "Level ID" // @Success 200 {object} domain.Response // @Failure 400 {object} domain.ErrorResponse // @Failure 500 {object} domain.ErrorResponse // @Router /api/v1/course-management/levels/{levelId}/modules [get] func (h *Handler) ListModulesByLevel(c *fiber.Ctx) error { levelID, err := strconv.ParseInt(c.Params("levelId"), 10, 64) if err != nil || levelID <= 0 { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ Message: "Invalid level ID", Error: "levelId must be a positive integer", }) } rows, err := h.analyticsDB.GetModulesByLevelID(c.Context(), levelID) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to load modules", Error: err.Error()}) } return c.JSON(domain.Response{ Message: "Modules retrieved successfully", Data: map[string]interface{}{ "modules": rows, "total_count": len(rows), }, }) } // GetModuleByID godoc // @Summary Get module detail // @Description Returns one module by ID // @Tags course-management // @Produce json // @Param moduleId path int true "Module ID" // @Success 200 {object} domain.Response // @Failure 400 {object} domain.ErrorResponse // @Failure 404 {object} domain.ErrorResponse // @Failure 500 {object} domain.ErrorResponse // @Router /api/v1/course-management/modules/{moduleId} [get] func (h *Handler) GetModuleByID(c *fiber.Ctx) error { moduleID, err := strconv.ParseInt(c.Params("moduleId"), 10, 64) if err != nil || moduleID <= 0 { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ Message: "Invalid module ID", Error: "moduleId must be a positive integer", }) } mod, err := h.analyticsDB.GetModuleByID(c.Context(), moduleID) if err != nil { return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{ Message: "Module not found", Error: err.Error(), }) } return c.JSON(domain.Response{ Message: "Module retrieved successfully", Data: mod, }) } // ListAllSubModules godoc // @Summary List all sub-modules // @Description Returns all sub-modules with pagination // @Tags course-management // @Produce json // @Param offset query int false "Offset" // @Param limit query int false "Limit" // @Success 200 {object} domain.Response // @Failure 500 {object} domain.ErrorResponse // @Router /api/v1/course-management/sub-modules [get] func (h *Handler) ListAllSubModules(c *fiber.Ctx) error { offset := int32(c.QueryInt("offset", 0)) if offset < 0 { offset = 0 } limit := int32(c.QueryInt("limit", 10000)) if limit <= 0 { limit = 10000 } rows, err := h.analyticsDB.GetAllSubModules(c.Context(), dbgen.GetAllSubModulesParams{ Offset: pgtype.Int4{Int32: offset, Valid: true}, Limit: pgtype.Int4{Int32: limit, Valid: true}, }) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to load sub-modules", Error: err.Error()}) } total := 0 if len(rows) > 0 { total = int(rows[0].TotalCount) } return c.JSON(domain.Response{ Message: "Sub-modules retrieved successfully", Data: map[string]interface{}{ "sub_modules": rows, "total_count": total, }, }) } // ListSubModulesByModule godoc // @Summary List sub-modules by module // @Description Returns all active sub-modules for one module // @Tags course-management // @Produce json // @Param moduleId path int true "Module ID" // @Success 200 {object} domain.Response // @Failure 400 {object} domain.ErrorResponse // @Failure 500 {object} domain.ErrorResponse // @Router /api/v1/course-management/modules/{moduleId}/sub-modules [get] func (h *Handler) ListSubModulesByModule(c *fiber.Ctx) error { moduleID, err := strconv.ParseInt(c.Params("moduleId"), 10, 64) if err != nil || moduleID <= 0 { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ Message: "Invalid module ID", Error: "moduleId must be a positive integer", }) } rows, err := h.analyticsDB.GetSubModulesByModuleID(c.Context(), moduleID) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to load sub-modules", Error: err.Error()}) } return c.JSON(domain.Response{ Message: "Sub-modules retrieved successfully", Data: map[string]interface{}{ "sub_modules": rows, "total_count": len(rows), }, }) } // GetSubModuleByID godoc // @Summary Get sub-module detail // @Description Returns one sub-module by ID // @Tags course-management // @Produce json // @Param subModuleId path int true "Sub-module ID" // @Success 200 {object} domain.Response // @Failure 400 {object} domain.ErrorResponse // @Failure 404 {object} domain.ErrorResponse // @Failure 500 {object} domain.ErrorResponse // @Router /api/v1/course-management/sub-modules/{subModuleId} [get] func (h *Handler) GetSubModuleByID(c *fiber.Ctx) error { subModuleID, err := strconv.ParseInt(c.Params("subModuleId"), 10, 64) if err != nil || subModuleID <= 0 { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ Message: "Invalid sub-module ID", Error: "subModuleId must be a positive integer", }) } subModule, err := h.analyticsDB.GetSubModuleByID(c.Context(), subModuleID) if err != nil { return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{ Message: "Sub-module not found", Error: err.Error(), }) } return c.JSON(domain.Response{ Message: "Sub-module retrieved successfully", Data: subModule, }) } // ListCourseSubCategories godoc // @Summary List course sub-categories // @Description Returns all active course sub-categories // @Tags course-management // @Produce json // @Param offset query int false "Offset" // @Param limit query int false "Limit" // @Success 200 {object} domain.Response // @Failure 500 {object} domain.ErrorResponse // @Router /api/v1/course-management/sub-categories [get] func (h *Handler) ListCourseSubCategories(c *fiber.Ctx) error { offset := int32(c.QueryInt("offset", 0)) if offset < 0 { offset = 0 } limit := int32(c.QueryInt("limit", 10000)) if limit <= 0 { limit = 10000 } rows, err := h.analyticsDB.GetCourseSubCategories(c.Context(), dbgen.GetCourseSubCategoriesParams{ Offset: pgtype.Int4{Int32: offset, Valid: true}, Limit: pgtype.Int4{Int32: limit, Valid: true}, }) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to load sub-categories", Error: err.Error()}) } total := 0 if len(rows) > 0 { total = int(rows[0].TotalCount) } return c.JSON(domain.Response{ Message: "Sub-categories retrieved successfully", Data: map[string]interface{}{ "sub_categories": rows, "total_count": total, }, }) } // ListHumanLanguageCourseSubCategories godoc // @Summary List Human Language sub-categories // @Description Returns active sub-categories under Human Language category // @Tags course-management // @Produce json // @Param offset query int false "Offset" // @Param limit query int false "Limit" // @Success 200 {object} domain.Response // @Failure 500 {object} domain.ErrorResponse // @Router /api/v1/course-management/human-language/sub-categories [get] func (h *Handler) ListHumanLanguageCourseSubCategories(c *fiber.Ctx) error { offset := int32(c.QueryInt("offset", 0)) if offset < 0 { offset = 0 } limit := int32(c.QueryInt("limit", 10000)) if limit <= 0 { limit = 10000 } rows, err := h.analyticsDB.GetHumanLanguageCourseSubCategories(c.Context(), dbgen.GetHumanLanguageCourseSubCategoriesParams{ Offset: pgtype.Int4{Int32: offset, Valid: true}, Limit: pgtype.Int4{Int32: limit, Valid: true}, }) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to load Human Language sub-categories", Error: err.Error()}) } total := 0 if len(rows) > 0 { total = int(rows[0].TotalCount) } return c.JSON(domain.Response{ Message: "Human Language sub-categories retrieved successfully", Data: map[string]interface{}{ "sub_categories": rows, "total_count": total, }, }) } // CreateCourseCategory godoc // @Summary Create course category // @Description Legacy-compatible endpoint for creating a course category // @Tags course-management // @Accept json // @Produce json // @Param body body createCourseCategoryReq true "Create category payload" // @Success 201 {object} domain.Response // @Failure 400 {object} domain.ErrorResponse // @Failure 500 {object} domain.ErrorResponse // @Router /api/v1/course-management/categories [post] func (h *Handler) CreateCourseCategory(c *fiber.Ctx) error { var req createCourseCategoryReq if err := c.BodyParser(&req); err != nil { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "Invalid request body", Error: err.Error()}) } if strings.TrimSpace(req.Name) == "" { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "name is required"}) } created, err := h.analyticsDB.CreateCourseCategory(c.Context(), dbgen.CreateCourseCategoryParams{ Name: req.Name, Column2: boolOrNil(req.IsActive), }) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to create category", Error: err.Error()}) } return c.Status(fiber.StatusCreated).JSON(domain.Response{Message: "Course category created", Data: created}) } func (h *Handler) DeleteCourseCategory(c *fiber.Ctx) error { categoryID, err := strconv.ParseInt(c.Params("categoryId"), 10, 64) if err != nil || categoryID <= 0 { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "Invalid category ID", Error: "categoryId must be a positive integer"}) } if err := h.analyticsDB.DeleteCourseCategoryCompat(c.Context(), categoryID); err != nil { return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to delete category", Error: err.Error()}) } return c.JSON(domain.Response{Message: "Course category deleted"}) } // CreateCourse godoc // @Summary Create course // @Description Legacy-compatible endpoint for creating a course // @Tags course-management // @Accept json // @Produce json // @Param body body createCourseReq true "Create course payload" // @Success 201 {object} domain.Response // @Failure 400 {object} domain.ErrorResponse // @Failure 500 {object} domain.ErrorResponse // @Router /api/v1/course-management/courses [post] func (h *Handler) CreateCourse(c *fiber.Ctx) error { var req createCourseReq 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.Title) == "" { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "category_id and title are required"}) } isActive := true if req.IsActive != nil { isActive = *req.IsActive } description := "" if req.Description != nil { description = *req.Description } thumbnail := "" if req.Thumbnail != nil { thumbnail = *req.Thumbnail } introVideoURL := "" if req.IntroVideoURL != nil { introVideoURL = *req.IntroVideoURL } created, err := h.analyticsDB.CreateCourseCompat( c.Context(), req.CategoryID, req.SubCategoryID, req.Title, description, thumbnail, introVideoURL, isActive, ) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to create course", Error: err.Error()}) } return c.Status(fiber.StatusCreated).JSON(domain.Response{Message: "Course created", Data: created}) } // UpdateCourse godoc // @Summary Update course // @Description Legacy-compatible endpoint for updating a course // @Tags course-management // @Accept json // @Produce json // @Param courseId path int true "Course ID" // @Param body body updateCourseReq true "Update course payload" // @Success 200 {object} domain.Response // @Failure 400 {object} domain.ErrorResponse // @Failure 500 {object} domain.ErrorResponse // @Router /api/v1/course-management/courses/{courseId} [put] func (h *Handler) UpdateCourse(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"}) } var req updateCourseReq if err := c.BodyParser(&req); err != nil { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "Invalid request body", Error: err.Error()}) } existing, 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()}) } title := existing.Title if req.Title != nil { title = *req.Title } description := existing.Description if req.Description != nil { description = toText(req.Description) } thumbnail := existing.Thumbnail if req.Thumbnail != nil { thumbnail = toText(req.Thumbnail) } introVideo := existing.IntroVideoUrl if req.IntroVideoURL != nil { introVideo = toText(req.IntroVideoURL) } isActive := existing.IsActive if req.IsActive != nil { isActive = *req.IsActive } if err := h.analyticsDB.UpdateCourse(c.Context(), dbgen.UpdateCourseParams{ Title: title, Description: description, Thumbnail: thumbnail, IntroVideoUrl: introVideo, IsActive: isActive, ID: courseID, }); err != nil { return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to update course", Error: err.Error()}) } updated, err := h.analyticsDB.GetCourseByID(c.Context(), courseID) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Course updated but failed to fetch latest record", Error: err.Error()}) } return c.JSON(domain.Response{Message: "Course updated", Data: updated}) } // DeleteCourse godoc // @Summary Delete course // @Description Legacy-compatible endpoint for deleting a course // @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} [delete] func (h *Handler) DeleteCourse(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"}) } if err := h.analyticsDB.DeleteCourse(c.Context(), courseID); err != nil { return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to delete course", Error: err.Error()}) } return c.JSON(domain.Response{Message: "Course deleted"}) } // UpdateCourseThumbnail godoc // @Summary Update course thumbnail // @Description Legacy-compatible endpoint for updating course thumbnail // @Tags course-management // @Accept json // @Produce json // @Param courseId path int true "Course ID" // @Param body body updateCourseThumbnailReq true "Update course thumbnail payload" // @Success 200 {object} domain.Response // @Failure 400 {object} domain.ErrorResponse // @Failure 500 {object} domain.ErrorResponse // @Router /api/v1/course-management/courses/{courseId}/thumbnail [post] func (h *Handler) UpdateCourseThumbnail(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"}) } var req updateCourseThumbnailReq if err := c.BodyParser(&req); err != nil { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "Invalid request body", Error: err.Error()}) } existing, 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()}) } thumb := req.ThumbnailURL if strings.TrimSpace(thumb) == "" { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "thumbnail_url is required"}) } if err := h.analyticsDB.UpdateCourse(c.Context(), dbgen.UpdateCourseParams{ Title: existing.Title, Description: existing.Description, Thumbnail: pgtype.Text{String: thumb, Valid: true}, IntroVideoUrl: existing.IntroVideoUrl, IsActive: existing.IsActive, ID: courseID, }); err != nil { return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to update course thumbnail", Error: err.Error()}) } updated, err := h.analyticsDB.GetCourseByID(c.Context(), courseID) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Thumbnail updated but failed to fetch latest record", Error: err.Error()}) } return c.JSON(domain.Response{Message: "Course thumbnail updated", Data: updated}) } // 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 { if isMissingCourseSubCategoryTableErr(err) { legacyRows, legacyErr := h.buildLegacyHierarchyRows(c) if legacyErr != nil { return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to load hierarchy", Error: legacyErr.Error()}) } return c.JSON(domain.Response{Message: "Unified hierarchy retrieved successfully", Data: legacyRows}) } 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 { if isMissingCourseSubCategoryTableErr(err) { course, getCourseErr := h.analyticsDB.GetCourseByID(c.Context(), courseID) if getCourseErr != nil { return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to load course hierarchy", Error: getCourseErr.Error()}) } return c.JSON(domain.Response{ Message: "Course hierarchy retrieved successfully", Data: []map[string]interface{}{ { "course_id": course.ID, "course_title": course.Title, "level_id": nil, "cefr_level": nil, "module_id": nil, "module_title": nil, "sub_module_id": nil, "sub_module_title": 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}) } // 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 } return strings.Contains(strings.ToLower(err.Error()), "relation \"course_sub_categories\" does not exist") } func (h *Handler) buildLegacyHierarchyRows(c *fiber.Ctx) ([]legacyHierarchyRow, error) { categories, err := h.analyticsDB.GetAllCourseCategories(c.Context(), dbgen.GetAllCourseCategoriesParams{ Offset: pgtype.Int4{Int32: 0, Valid: true}, Limit: pgtype.Int4{Int32: 10000, Valid: true}, }) if err != nil { return nil, err } out := make([]legacyHierarchyRow, 0, len(categories)) for _, cat := range categories { courses, courseErr := h.analyticsDB.GetCoursesByCategory(c.Context(), dbgen.GetCoursesByCategoryParams{ CategoryID: cat.ID, Offset: pgtype.Int4{Int32: 0, Valid: true}, Limit: pgtype.Int4{Int32: 10000, Valid: true}, }) if courseErr != nil { return nil, courseErr } if len(courses) == 0 { out = append(out, legacyHierarchyRow{ CategoryID: cat.ID, CategoryName: cat.Name, SubCategoryID: nil, SubCategoryName: nil, CourseID: nil, CourseTitle: nil, }) continue } for _, course := range courses { courseID := course.ID courseTitle := course.Title out = append(out, legacyHierarchyRow{ CategoryID: cat.ID, CategoryName: cat.Name, SubCategoryID: nil, SubCategoryName: nil, CourseID: &courseID, CourseTitle: &courseTitle, }) } } return out, nil } // 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}) } func (h *Handler) DeleteCourseSubCategory(c *fiber.Ctx) error { subCategoryID, err := strconv.ParseInt(c.Params("subCategoryId"), 10, 64) if err != nil || subCategoryID <= 0 { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "Invalid sub-category ID", Error: "subCategoryId must be a positive integer"}) } if err := h.analyticsDB.DeleteCourseSubCategoryCompat(c.Context(), subCategoryID); err != nil { return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to delete sub-category", Error: err.Error()}) } return c.JSON(domain.Response{Message: "Course sub-category deleted"}) } // 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}) } // GetSubModuleLessons godoc // @Summary Get lessons under sub-module // @Description Returns all active lessons attached to a sub-module with question-set details // @Tags course-management // @Accept json // @Produce json // @Param subModuleId path int true "Sub-module ID" // @Success 200 {object} domain.Response // @Failure 400 {object} domain.ErrorResponse // @Failure 500 {object} domain.ErrorResponse // @Router /api/v1/course-management/sub-modules/{subModuleId}/lessons [get] func (h *Handler) GetSubModuleLessons(c *fiber.Ctx) error { subModuleID, err := strconv.ParseInt(c.Params("subModuleId"), 10, 64) if err != nil || subModuleID <= 0 { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ Message: "Invalid sub-module ID", Error: "subModuleId must be a valid positive integer", }) } lessons, err := h.analyticsDB.GetSubModuleLessons(c.Context(), subModuleID) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ Message: "Failed to get sub-module lessons", Error: err.Error(), }) } return c.Status(fiber.StatusOK).JSON(domain.Response{ Message: "Sub-module lessons retrieved successfully", Data: lessons, }) } // GetSubModuleLessonByID godoc // @Summary Get lesson detail // @Description Returns one active lesson detail by lesson ID // @Tags course-management // @Accept json // @Produce json // @Param lessonId path int true "Lesson ID" // @Success 200 {object} domain.Response // @Failure 400 {object} domain.ErrorResponse // @Failure 404 {object} domain.ErrorResponse // @Failure 500 {object} domain.ErrorResponse // @Router /api/v1/course-management/sub-module-lessons/{lessonId} [get] func (h *Handler) GetSubModuleLessonByID(c *fiber.Ctx) error { lessonID, err := strconv.ParseInt(c.Params("lessonId"), 10, 64) if err != nil || lessonID <= 0 { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ Message: "Invalid lesson ID", Error: "lessonId must be a valid positive integer", }) } lesson, err := h.analyticsDB.GetSubModuleLessonByID(c.Context(), lessonID) if err != nil { return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{ Message: "Lesson not found", Error: err.Error(), }) } return c.Status(fiber.StatusOK).JSON(domain.Response{ Message: "Lesson detail retrieved successfully", Data: lesson, }) } // UpdateSubModuleLesson godoc // @Summary Update lesson detail // @Description Updates lesson metadata, linked question-set metadata, and optionally replaces lesson questions // @Tags course-management // @Accept json // @Produce json // @Param lessonId path int true "Lesson ID" // @Param body body updateSubModuleLessonReq true "Update lesson payload" // @Success 200 {object} domain.Response // @Failure 400 {object} domain.ErrorResponse // @Failure 404 {object} domain.ErrorResponse // @Failure 500 {object} domain.ErrorResponse // @Router /api/v1/course-management/sub-module-lessons/{lessonId} [put] func (h *Handler) UpdateSubModuleLesson(c *fiber.Ctx) error { lessonID, err := strconv.ParseInt(c.Params("lessonId"), 10, 64) if err != nil || lessonID <= 0 { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ Message: "Invalid lesson ID", Error: "lessonId must be a valid positive integer", }) } var req updateSubModuleLessonReq if err := c.BodyParser(&req); err != nil { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ Message: "Invalid request body", Error: err.Error(), }) } currentLesson, err := h.analyticsDB.GetSubModuleLessonByID(c.Context(), lessonID) if err != nil { return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{ Message: "Lesson not found", Error: err.Error(), }) } targetSubModuleID := currentLesson.SubModuleID if req.SubModuleID != nil { if *req.SubModuleID <= 0 { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "sub_module_id must be a positive integer"}) } targetSubModuleID = *req.SubModuleID } targetQuestionSetID := currentLesson.QuestionSetID if req.QuestionSetID != nil { if *req.QuestionSetID <= 0 { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "question_set_id must be a positive integer"}) } targetQuestionSetID = *req.QuestionSetID } targetIntroVideoURL := currentLesson.IntroVideoUrl if req.IntroVideoURL != nil { targetIntroVideoURL = toText(req.IntroVideoURL) } targetDisplayOrder := currentLesson.DisplayOrder if req.DisplayOrder != nil { targetDisplayOrder = *req.DisplayOrder } targetIsActive := currentLesson.IsActive if req.IsActive != nil { targetIsActive = *req.IsActive } if _, err := h.analyticsDB.UpdateSubModuleLesson(c.Context(), dbgen.UpdateSubModuleLessonParams{ SubModuleID: targetSubModuleID, QuestionSetID: targetQuestionSetID, IntroVideoUrl: targetIntroVideoURL, DisplayOrder: targetDisplayOrder, IsActive: targetIsActive, ID: lessonID, }); err != nil { return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ Message: "Failed to update lesson", Error: err.Error(), }) } currentSet, err := h.questionsSvc.GetQuestionSetByID(c.Context(), targetQuestionSetID) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ Message: "Failed to load linked question set", Error: err.Error(), }) } shouldUpdateSet := req.Title != nil || req.Description != nil || req.BannerImage != nil || req.Persona != nil || req.TimeLimitMinutes != nil || req.PassingScore != nil || req.ShuffleQuestions != nil || req.Status != nil || req.SubCourseVideoID != nil if shouldUpdateSet { title := currentSet.Title if req.Title != nil { title = *req.Title } input := domain.CreateQuestionSetInput{ Title: title, Description: currentSet.Description, BannerImage: currentSet.BannerImage, Persona: currentSet.Persona, TimeLimitMinutes: currentSet.TimeLimitMinutes, PassingScore: currentSet.PassingScore, SubCourseVideoID: currentSet.SubCourseVideoID, IntroVideoURL: req.IntroVideoURL, ShuffleQuestions: ¤tSet.ShuffleQuestions, } currentStatus := currentSet.Status input.Status = ¤tStatus if req.Description != nil { input.Description = req.Description } if req.BannerImage != nil { input.BannerImage = req.BannerImage } if req.Persona != nil { input.Persona = req.Persona } if req.TimeLimitMinutes != nil { input.TimeLimitMinutes = req.TimeLimitMinutes } if req.PassingScore != nil { input.PassingScore = req.PassingScore } if req.ShuffleQuestions != nil { input.ShuffleQuestions = req.ShuffleQuestions } if req.Status != nil { input.Status = req.Status } if req.SubCourseVideoID != nil { input.SubCourseVideoID = req.SubCourseVideoID } if err := h.questionsSvc.UpdateQuestionSet(c.Context(), targetQuestionSetID, input); err != nil { return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ Message: "Failed to update linked question set", Error: err.Error(), }) } } if req.Questions != nil { seen := make(map[int64]struct{}, len(req.Questions)) for idx, q := range req.Questions { if q.QuestionID <= 0 { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "Each question_id must be a positive integer"}) } if _, exists := seen[q.QuestionID]; exists { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "Duplicate question_id values are not allowed"}) } seen[q.QuestionID] = struct{}{} order := q.DisplayOrder if order == nil { defaultOrder := int32(idx) order = &defaultOrder } if _, err := h.questionsSvc.GetQuestionByID(c.Context(), q.QuestionID); err != nil { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ Message: "Invalid question_id in questions payload", Error: err.Error(), }) } if _, err := h.questionsSvc.AddQuestionToSet(c.Context(), targetQuestionSetID, q.QuestionID, order); err != nil { return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ Message: "Failed to upsert lesson question", Error: err.Error(), }) } } existingItems, err := h.questionsSvc.GetQuestionSetItems(c.Context(), targetQuestionSetID) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ Message: "Failed to load existing lesson questions", Error: err.Error(), }) } for _, item := range existingItems { if _, keep := seen[item.QuestionID]; keep { continue } if err := h.questionsSvc.RemoveQuestionFromSet(c.Context(), targetQuestionSetID, item.QuestionID); err != nil { return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ Message: "Failed to remove question from lesson", Error: err.Error(), }) } } } updatedLesson, err := h.analyticsDB.GetSubModuleLessonByID(c.Context(), lessonID) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ Message: "Lesson updated but failed to fetch latest detail", Error: err.Error(), }) } updatedQuestions, err := h.questionsSvc.GetQuestionSetItems(c.Context(), updatedLesson.QuestionSetID) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ Message: "Lesson updated but failed to fetch latest questions", Error: err.Error(), }) } return c.Status(fiber.StatusOK).JSON(domain.Response{ Message: "Lesson updated successfully", Data: map[string]interface{}{ "lesson": updatedLesson, "questions": updatedQuestions, }, }) } // 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}) } // GetSubModulePractices godoc // @Summary Get practices under sub-module // @Description Returns all active practices attached to a sub-module // @Tags course-management // @Accept json // @Produce json // @Param subModuleId path int true "Sub-module ID" // @Success 200 {object} domain.Response // @Failure 400 {object} domain.ErrorResponse // @Failure 500 {object} domain.ErrorResponse // @Router /api/v1/course-management/sub-modules/{subModuleId}/practices [get] func (h *Handler) GetSubModulePractices(c *fiber.Ctx) error { subModuleID, err := strconv.ParseInt(c.Params("subModuleId"), 10, 64) if err != nil || subModuleID <= 0 { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ Message: "Invalid sub-module ID", Error: "subModuleId must be a positive integer", }) } practices, err := h.analyticsDB.GetSubModulePractices(c.Context(), subModuleID) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ Message: "Failed to load sub-module practices", Error: err.Error(), }) } return c.JSON(domain.Response{ Message: "Sub-module practices retrieved successfully", Data: map[string]interface{}{ "practices": practices, "total_count": len(practices), }, }) } // GetSubModulePracticeByID godoc // @Summary Get practice detail // @Description Returns one active practice by practice ID // @Tags course-management // @Accept json // @Produce json // @Param practiceId path int true "Practice ID" // @Success 200 {object} domain.Response // @Failure 400 {object} domain.ErrorResponse // @Failure 404 {object} domain.ErrorResponse // @Failure 500 {object} domain.ErrorResponse // @Router /api/v1/course-management/practices/{practiceId} [get] func (h *Handler) GetSubModulePracticeByID(c *fiber.Ctx) error { practiceID, err := strconv.ParseInt(c.Params("practiceId"), 10, 64) if err != nil || practiceID <= 0 { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ Message: "Invalid practice ID", Error: "practiceId must be a positive integer", }) } practice, err := h.analyticsDB.GetSubModulePracticeByID(c.Context(), practiceID) if err != nil { return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{ Message: "Practice not found", Error: err.Error(), }) } return c.JSON(domain.Response{ Message: "Practice retrieved successfully", Data: practice, }) } func (h *Handler) GetSubModuleVideos(c *fiber.Ctx) error { subModuleID, err := strconv.ParseInt(c.Params("subModuleId"), 10, 64) if err != nil || subModuleID <= 0 { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "Invalid sub-module ID", Error: "subModuleId must be a positive integer"}) } videos, err := h.analyticsDB.GetSubModuleVideos(c.Context(), subModuleID) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to load sub-module videos", Error: err.Error()}) } return c.JSON(domain.Response{ Message: "Sub-module videos retrieved successfully", Data: map[string]interface{}{ "videos": videos, "total_count": len(videos), }, }) } func (h *Handler) UpdateSubModule(c *fiber.Ctx) error { subModuleID, err := strconv.ParseInt(c.Params("subModuleId"), 10, 64) if err != nil || subModuleID <= 0 { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "Invalid sub-module ID", Error: "subModuleId must be a positive integer"}) } existing, err := h.analyticsDB.GetSubModuleByIDCompat(c.Context(), subModuleID) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to load sub-module", Error: err.Error()}) } var req updateSubModuleReq if err := c.BodyParser(&req); err != nil { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "Invalid request body", Error: err.Error()}) } title := existing.Title if req.Title != nil { title = *req.Title } description := "" if existing.Description.Valid { description = existing.Description.String } if req.Description != nil { description = *req.Description } isActive := existing.IsActive if req.IsActive != nil { isActive = *req.IsActive } if err := h.analyticsDB.UpdateSubModuleCompat(c.Context(), subModuleID, title, description, isActive); err != nil { return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to update sub-module", Error: err.Error()}) } updated, err := h.analyticsDB.GetSubModuleByIDCompat(c.Context(), subModuleID) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Sub-module updated but failed to fetch latest record", Error: err.Error()}) } return c.JSON(domain.Response{Message: "Sub-module updated", Data: updated}) } func (h *Handler) DeleteSubModule(c *fiber.Ctx) error { subModuleID, err := strconv.ParseInt(c.Params("subModuleId"), 10, 64) if err != nil || subModuleID <= 0 { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "Invalid sub-module ID", Error: "subModuleId must be a positive integer"}) } if err := h.analyticsDB.DeleteSubModuleCompat(c.Context(), subModuleID); err != nil { return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to delete sub-module", Error: err.Error()}) } return c.JSON(domain.Response{Message: "Sub-module deleted"}) } func (h *Handler) DeleteModule(c *fiber.Ctx) error { moduleID, err := strconv.ParseInt(c.Params("moduleId"), 10, 64) if err != nil || moduleID <= 0 { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "Invalid module ID", Error: "moduleId must be a positive integer"}) } if err := h.analyticsDB.DeleteModuleCompat(c.Context(), moduleID); err != nil { return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to delete module", Error: err.Error()}) } return c.JSON(domain.Response{Message: "Module deleted"}) } func (h *Handler) UpdateSubModuleVideo(c *fiber.Ctx) error { videoID, err := strconv.ParseInt(c.Params("videoId"), 10, 64) if err != nil || videoID <= 0 { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "Invalid video ID", Error: "videoId must be a positive integer"}) } var req updateSubModuleVideoReq if err := c.BodyParser(&req); err != nil { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "Invalid request body", Error: err.Error()}) } if req.Title == nil || strings.TrimSpace(*req.Title) == "" || req.VideoURL == nil || strings.TrimSpace(*req.VideoURL) == "" { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "title and video_url are required"}) } description := "" if req.Description != nil { description = *req.Description } if err := h.analyticsDB.UpdateSubModuleVideoCompat(c.Context(), videoID, *req.Title, description, *req.VideoURL); err != nil { return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to update sub-module video", Error: err.Error()}) } return c.JSON(domain.Response{Message: "Sub-module video updated"}) } func (h *Handler) DeleteSubModuleVideo(c *fiber.Ctx) error { videoID, err := strconv.ParseInt(c.Params("videoId"), 10, 64) if err != nil || videoID <= 0 { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "Invalid video ID", Error: "videoId must be a positive integer"}) } if err := h.analyticsDB.DeleteSubModuleVideoCompat(c.Context(), videoID); err != nil { return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to delete sub-module video", Error: err.Error()}) } return c.JSON(domain.Response{Message: "Sub-module video deleted"}) } func (h *Handler) UpdatePractice(c *fiber.Ctx) error { practiceID, err := strconv.ParseInt(c.Params("practiceId"), 10, 64) if err != nil || practiceID <= 0 { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "Invalid practice ID", Error: "practiceId must be a positive integer"}) } var req updatePracticeReq if err := c.BodyParser(&req); err != nil { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "Invalid request body", Error: err.Error()}) } if req.IsActive != nil { if err := h.analyticsDB.UpdatePracticeStatusCompat(c.Context(), practiceID, *req.IsActive); err != nil { return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to update practice status", Error: err.Error()}) } return c.JSON(domain.Response{Message: "Practice status updated"}) } title := "" if req.Title != nil { title = *req.Title } description := "" if req.Description != nil { description = *req.Description } persona := "" if req.Persona != nil { persona = *req.Persona } if strings.TrimSpace(title) == "" { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "title is required"}) } if err := h.analyticsDB.UpdatePracticeCompat(c.Context(), practiceID, title, description, persona); err != nil { return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to update practice", Error: err.Error()}) } return c.JSON(domain.Response{Message: "Practice updated"}) } func (h *Handler) DeletePractice(c *fiber.Ctx) error { practiceID, err := strconv.ParseInt(c.Params("practiceId"), 10, 64) if err != nil || practiceID <= 0 { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "Invalid practice ID", Error: "practiceId must be a positive integer"}) } if err := h.analyticsDB.DeletePracticeCompat(c.Context(), practiceID); err != nil { return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to delete practice", Error: err.Error()}) } return c.JSON(domain.Response{Message: "Practice deleted"}) }