package handlers import ( "Yimaru-Backend/internal/domain" "fmt" "strconv" "github.com/gofiber/fiber/v2" ) // Course Category Handlers type createCourseCategoryReq struct { Name string `json:"name" validate:"required"` } type courseCategoryRes struct { ID int64 `json:"id"` Name string `json:"name"` IsActive bool `json:"is_active"` CreatedAt string `json:"created_at"` } // CreateCourseCategory godoc // @Summary Create a new course category // @Description Creates a new course category with the provided name // @Tags course-categories // @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(), }) } category, err := h.courseMgmtSvc.CreateCourseCategory(c.Context(), req.Name) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ Message: "Failed to create course category", Error: err.Error(), }) } return c.Status(fiber.StatusCreated).JSON(domain.Response{ Message: "Course category created successfully", Data: courseCategoryRes{ ID: category.ID, Name: category.Name, IsActive: category.IsActive, CreatedAt: category.CreatedAt.String(), }, }) } // GetCourseCategoryByID godoc // @Summary Get course category by ID // @Description Returns a single course category by its ID // @Tags course-categories // @Produce json // @Param id path int true "Category 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/categories/{id} [get] func (h *Handler) GetCourseCategoryByID(c *fiber.Ctx) error { idStr := c.Params("id") id, err := strconv.ParseInt(idStr, 10, 64) if err != nil { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ Message: "Invalid category ID", Error: err.Error(), }) } category, err := h.courseMgmtSvc.GetCourseCategoryByID(c.Context(), id) if err != nil { return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{ Message: "Course category not found", Error: err.Error(), }) } return c.JSON(domain.Response{ Message: "Course category retrieved successfully", Data: courseCategoryRes{ ID: category.ID, Name: category.Name, IsActive: category.IsActive, CreatedAt: category.CreatedAt.String(), }, }) } type getAllCourseCategoriesRes struct { Categories []courseCategoryRes `json:"categories"` TotalCount int64 `json:"total_count"` } // GetAllCourseCategories godoc // @Summary Get all course categories // @Description Returns a paginated list of all course categories // @Tags course-categories // @Produce json // @Param limit query int false "Limit" default(10) // @Param offset query int false "Offset" default(0) // @Success 200 {object} domain.Response // @Failure 400 {object} domain.ErrorResponse // @Failure 500 {object} domain.ErrorResponse // @Router /api/v1/course-management/categories [get] func (h *Handler) GetAllCourseCategories(c *fiber.Ctx) error { limitStr := c.Query("limit", "10") offsetStr := c.Query("offset", "0") limit, err := strconv.Atoi(limitStr) if err != nil { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ Message: "Invalid limit parameter", Error: err.Error(), }) } offset, err := strconv.Atoi(offsetStr) if err != nil { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ Message: "Invalid offset parameter", Error: err.Error(), }) } categories, totalCount, err := h.courseMgmtSvc.GetAllCourseCategories(c.Context(), int32(limit), int32(offset)) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ Message: "Failed to retrieve course categories", Error: err.Error(), }) } var categoryResponses []courseCategoryRes for _, category := range categories { categoryResponses = append(categoryResponses, courseCategoryRes{ ID: category.ID, Name: category.Name, IsActive: category.IsActive, CreatedAt: category.CreatedAt.String(), }) } return c.JSON(domain.Response{ Message: "Course categories retrieved successfully", Data: getAllCourseCategoriesRes{ Categories: categoryResponses, TotalCount: totalCount, }, }) } type updateCourseCategoryReq struct { Name *string `json:"name"` IsActive *bool `json:"is_active"` } // UpdateCourseCategory godoc // @Summary Update course category // @Description Updates a course category's name and/or active status // @Tags course-categories // @Accept json // @Produce json // @Param id path int true "Category ID" // @Param body body updateCourseCategoryReq true "Update category payload" // @Success 200 {object} domain.Response // @Failure 400 {object} domain.ErrorResponse // @Failure 500 {object} domain.ErrorResponse // @Router /api/v1/course-management/categories/{id} [put] func (h *Handler) UpdateCourseCategory(c *fiber.Ctx) error { idStr := c.Params("id") id, err := strconv.ParseInt(idStr, 10, 64) if err != nil { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ Message: "Invalid category ID", Error: err.Error(), }) } var req updateCourseCategoryReq if err := c.BodyParser(&req); err != nil { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ Message: "Invalid request body", Error: err.Error(), }) } err = h.courseMgmtSvc.UpdateCourseCategory(c.Context(), id, req.Name, req.IsActive) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ Message: "Failed to update course category", Error: err.Error(), }) } return c.JSON(domain.Response{ Message: "Course category updated successfully", }) } // DeleteCourseCategory godoc // @Summary Delete course category // @Description Deletes a course category by its ID // @Tags course-categories // @Produce json // @Param id path int true "Category ID" // @Success 200 {object} domain.Response // @Failure 400 {object} domain.ErrorResponse // @Failure 500 {object} domain.ErrorResponse // @Router /api/v1/course-management/categories/{id} [delete] func (h *Handler) DeleteCourseCategory(c *fiber.Ctx) error { idStr := c.Params("id") id, err := strconv.ParseInt(idStr, 10, 64) if err != nil { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ Message: "Invalid category ID", Error: err.Error(), }) } err = h.courseMgmtSvc.DeleteCourseCategory(c.Context(), id) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ Message: "Failed to delete course category", Error: err.Error(), }) } return c.JSON(domain.Response{ Message: "Course category deleted successfully", }) } // Course Handlers type createCourseReq struct { CategoryID int64 `json:"category_id" validate:"required"` Title string `json:"title" validate:"required"` Description *string `json:"description"` Thumbnail *string `json:"thumbnail"` } type courseRes struct { ID int64 `json:"id"` CategoryID int64 `json:"category_id"` Title string `json:"title"` Description *string `json:"description"` Thumbnail *string `json:"thumbnail"` IsActive bool `json:"is_active"` } // CreateCourse godoc // @Summary Create a new course // @Description Creates a new course under a specific category // @Tags courses // @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(), }) } course, err := h.courseMgmtSvc.CreateCourse(c.Context(), req.CategoryID, req.Title, req.Description, req.Thumbnail) 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 successfully", Data: courseRes{ ID: course.ID, CategoryID: course.CategoryID, Title: course.Title, Description: course.Description, Thumbnail: course.Thumbnail, IsActive: course.IsActive, }, }) } // GetCourseByID godoc // @Summary Get course by ID // @Description Returns a single course by its ID // @Tags courses // @Produce json // @Param id 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/{id} [get] func (h *Handler) GetCourseByID(c *fiber.Ctx) error { idStr := c.Params("id") id, err := strconv.ParseInt(idStr, 10, 64) if err != nil { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ Message: "Invalid course ID", Error: err.Error(), }) } course, err := h.courseMgmtSvc.GetCourseByID(c.Context(), id) 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: courseRes{ ID: course.ID, CategoryID: course.CategoryID, Title: course.Title, Description: course.Description, Thumbnail: course.Thumbnail, IsActive: course.IsActive, }, }) } type getCoursesByCategoryRes struct { Courses []courseRes `json:"courses"` TotalCount int64 `json:"total_count"` } // GetCoursesByCategory godoc // @Summary Get courses by category // @Description Returns a paginated list of courses under a specific category // @Tags courses // @Produce json // @Param categoryId path int true "Category ID" // @Param limit query int false "Limit" default(10) // @Param offset query int false "Offset" default(0) // @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) GetCoursesByCategory(c *fiber.Ctx) error { categoryIDStr := c.Params("categoryId") categoryID, err := strconv.ParseInt(categoryIDStr, 10, 64) if err != nil { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ Message: "Invalid category ID", Error: err.Error(), }) } limitStr := c.Query("limit", "10") offsetStr := c.Query("offset", "0") limit, err := strconv.Atoi(limitStr) if err != nil { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ Message: "Invalid limit parameter", Error: err.Error(), }) } offset, err := strconv.Atoi(offsetStr) if err != nil { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ Message: "Invalid offset parameter", Error: err.Error(), }) } courses, totalCount, err := h.courseMgmtSvc.GetCoursesByCategory(c.Context(), categoryID, int32(limit), int32(offset)) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ Message: "Failed to retrieve courses", Error: err.Error(), }) } var courseResponses []courseRes for _, course := range courses { courseResponses = append(courseResponses, courseRes{ ID: course.ID, CategoryID: course.CategoryID, Title: course.Title, Description: course.Description, Thumbnail: course.Thumbnail, IsActive: course.IsActive, }) } return c.JSON(domain.Response{ Message: "Courses retrieved successfully", Data: getCoursesByCategoryRes{ Courses: courseResponses, TotalCount: totalCount, }, }) } type updateCourseReq struct { Title *string `json:"title"` Description *string `json:"description"` Thumbnail *string `json:"thumbnail"` IsActive *bool `json:"is_active"` } // UpdateCourse godoc // @Summary Update course // @Description Updates a course's title, description, and/or active status // @Tags courses // @Accept json // @Produce json // @Param id 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/{id} [put] func (h *Handler) UpdateCourse(c *fiber.Ctx) error { idStr := c.Params("id") id, err := strconv.ParseInt(idStr, 10, 64) if err != nil { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ Message: "Invalid course ID", Error: err.Error(), }) } 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(), }) } err = h.courseMgmtSvc.UpdateCourse(c.Context(), id, req.Title, req.Description, req.Thumbnail, req.IsActive) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ Message: "Failed to update course", Error: err.Error(), }) } return c.JSON(domain.Response{ Message: "Course updated successfully", }) } // DeleteCourse godoc // @Summary Delete course // @Description Deletes a course by its ID // @Tags courses // @Produce json // @Param id 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/{id} [delete] func (h *Handler) DeleteCourse(c *fiber.Ctx) error { idStr := c.Params("id") id, err := strconv.ParseInt(idStr, 10, 64) if err != nil { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ Message: "Invalid course ID", Error: err.Error(), }) } err = h.courseMgmtSvc.DeleteCourse(c.Context(), id) if 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 successfully", }) } // Sub-course Handlers type createSubCourseReq 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"` Level string `json:"level" validate:"required"` // BEGINNER, INTERMEDIATE, ADVANCED } type subCourseRes struct { ID int64 `json:"id"` CourseID int64 `json:"course_id"` Title string `json:"title"` Description *string `json:"description"` Thumbnail *string `json:"thumbnail"` DisplayOrder int32 `json:"display_order"` Level string `json:"level"` IsActive bool `json:"is_active"` } // CreateSubCourse godoc // @Summary Create a new sub-course // @Description Creates a new sub-course under a specific course // @Tags sub-courses // @Accept json // @Produce json // @Param body body createSubCourseReq true "Create sub-course payload" // @Success 201 {object} domain.Response // @Failure 400 {object} domain.ErrorResponse // @Failure 500 {object} domain.ErrorResponse // @Router /api/v1/course-management/sub-courses [post] func (h *Handler) CreateSubCourse(c *fiber.Ctx) error { var req createSubCourseReq if err := c.BodyParser(&req); err != nil { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ Message: "Invalid request body", Error: err.Error(), }) } subCourse, err := h.courseMgmtSvc.CreateSubCourse(c.Context(), req.CourseID, req.Title, req.Description, req.Thumbnail, req.DisplayOrder, req.Level) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ Message: "Failed to create sub-course", Error: err.Error(), }) } return c.Status(fiber.StatusCreated).JSON(domain.Response{ Message: "Sub-course created successfully", Data: subCourseRes{ ID: subCourse.ID, CourseID: subCourse.CourseID, Title: subCourse.Title, Description: subCourse.Description, Thumbnail: subCourse.Thumbnail, DisplayOrder: subCourse.DisplayOrder, Level: subCourse.Level, IsActive: subCourse.IsActive, }, }) } // GetSubCourseByID godoc // @Summary Get sub-course by ID // @Description Returns a single sub-course by its ID // @Tags sub-courses // @Produce json // @Param id path int true "Sub-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/sub-courses/{id} [get] func (h *Handler) GetSubCourseByID(c *fiber.Ctx) error { idStr := c.Params("id") id, err := strconv.ParseInt(idStr, 10, 64) if err != nil { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ Message: "Invalid sub-course ID", Error: err.Error(), }) } subCourse, err := h.courseMgmtSvc.GetSubCourseByID(c.Context(), id) if err != nil { return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{ Message: "Sub-course not found", Error: err.Error(), }) } return c.JSON(domain.Response{ Message: "Sub-course retrieved successfully", Data: subCourseRes{ ID: subCourse.ID, CourseID: subCourse.CourseID, Title: subCourse.Title, Description: subCourse.Description, Thumbnail: subCourse.Thumbnail, DisplayOrder: subCourse.DisplayOrder, Level: subCourse.Level, IsActive: subCourse.IsActive, }, }) } type getSubCoursesByCourseRes struct { SubCourses []subCourseRes `json:"sub_courses"` TotalCount int64 `json:"total_count"` } // GetSubCoursesByCourse godoc // @Summary Get sub-courses by course // @Description Returns all sub-courses under a specific course // @Tags sub-courses // @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}/sub-courses [get] func (h *Handler) GetSubCoursesByCourse(c *fiber.Ctx) error { courseIDStr := c.Params("courseId") courseID, err := strconv.ParseInt(courseIDStr, 10, 64) if err != nil { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ Message: "Invalid course ID", Error: err.Error(), }) } subCourses, totalCount, err := h.courseMgmtSvc.GetSubCoursesByCourse(c.Context(), courseID) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ Message: "Failed to retrieve sub-courses", Error: err.Error(), }) } var subCourseResponses []subCourseRes for _, sc := range subCourses { subCourseResponses = append(subCourseResponses, subCourseRes{ ID: sc.ID, CourseID: sc.CourseID, Title: sc.Title, Description: sc.Description, Thumbnail: sc.Thumbnail, DisplayOrder: sc.DisplayOrder, Level: sc.Level, IsActive: sc.IsActive, }) } return c.JSON(domain.Response{ Message: "Sub-courses retrieved successfully", Data: getSubCoursesByCourseRes{ SubCourses: subCourseResponses, TotalCount: totalCount, }, }) } // ListSubCoursesByCourse godoc // @Summary List active sub-courses by course // @Description Returns a list of active sub-courses under a specific course // @Tags sub-courses // @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}/sub-courses/list [get] func (h *Handler) ListSubCoursesByCourse(c *fiber.Ctx) error { courseIDStr := c.Params("courseId") courseID, err := strconv.ParseInt(courseIDStr, 10, 64) if err != nil { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ Message: "Invalid course ID", Error: err.Error(), }) } subCourses, err := h.courseMgmtSvc.ListSubCoursesByCourse(c.Context(), courseID) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ Message: "Failed to retrieve sub-courses", Error: err.Error(), }) } var subCourseResponses []subCourseRes for _, sc := range subCourses { subCourseResponses = append(subCourseResponses, subCourseRes{ ID: sc.ID, CourseID: sc.CourseID, Title: sc.Title, Description: sc.Description, Thumbnail: sc.Thumbnail, DisplayOrder: sc.DisplayOrder, Level: sc.Level, IsActive: sc.IsActive, }) } return c.JSON(domain.Response{ Message: "Sub-courses retrieved successfully", Data: subCourseResponses, }) } // ListActiveSubCourses godoc // @Summary List all active sub-courses // @Description Returns a list of all active sub-courses // @Tags sub-courses // @Produce json // @Success 200 {object} domain.Response // @Failure 500 {object} domain.ErrorResponse // @Router /api/v1/course-management/sub-courses/active [get] func (h *Handler) ListActiveSubCourses(c *fiber.Ctx) error { subCourses, err := h.courseMgmtSvc.ListActiveSubCourses(c.Context()) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ Message: "Failed to retrieve active sub-courses", Error: err.Error(), }) } var subCourseResponses []subCourseRes for _, sc := range subCourses { subCourseResponses = append(subCourseResponses, subCourseRes{ ID: sc.ID, CourseID: sc.CourseID, Title: sc.Title, Description: sc.Description, Thumbnail: sc.Thumbnail, DisplayOrder: sc.DisplayOrder, Level: sc.Level, IsActive: sc.IsActive, }) } return c.JSON(domain.Response{ Message: "Active sub-courses retrieved successfully", Data: subCourseResponses, }) } type updateSubCourseReq struct { Title *string `json:"title"` Description *string `json:"description"` Thumbnail *string `json:"thumbnail"` DisplayOrder *int32 `json:"display_order"` Level *string `json:"level"` IsActive *bool `json:"is_active"` } // UpdateSubCourse godoc // @Summary Update sub-course // @Description Updates a sub-course's fields // @Tags sub-courses // @Accept json // @Produce json // @Param id path int true "Sub-course ID" // @Param body body updateSubCourseReq true "Update sub-course payload" // @Success 200 {object} domain.Response // @Failure 400 {object} domain.ErrorResponse // @Failure 500 {object} domain.ErrorResponse // @Router /api/v1/course-management/sub-courses/{id} [patch] func (h *Handler) UpdateSubCourse(c *fiber.Ctx) error { idStr := c.Params("id") id, err := strconv.ParseInt(idStr, 10, 64) if err != nil { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ Message: "Invalid sub-course ID", Error: err.Error(), }) } var req updateSubCourseReq if err := c.BodyParser(&req); err != nil { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ Message: "Invalid request body", Error: err.Error(), }) } err = h.courseMgmtSvc.UpdateSubCourse(c.Context(), id, req.Title, req.Description, req.Thumbnail, req.DisplayOrder, req.Level, req.IsActive) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ Message: "Failed to update sub-course", Error: err.Error(), }) } return c.JSON(domain.Response{ Message: "Sub-course updated successfully", }) } // DeactivateSubCourse godoc // @Summary Deactivate sub-course // @Description Deactivates a sub-course by its ID // @Tags sub-courses // @Produce json // @Param id path int true "Sub-course ID" // @Success 200 {object} domain.Response // @Failure 400 {object} domain.ErrorResponse // @Failure 500 {object} domain.ErrorResponse // @Router /api/v1/course-management/sub-courses/{id}/deactivate [put] func (h *Handler) DeactivateSubCourse(c *fiber.Ctx) error { idStr := c.Params("id") id, err := strconv.ParseInt(idStr, 10, 64) if err != nil { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ Message: "Invalid sub-course ID", Error: err.Error(), }) } err = h.courseMgmtSvc.DeactivateSubCourse(c.Context(), id) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ Message: "Failed to deactivate sub-course", Error: err.Error(), }) } return c.JSON(domain.Response{ Message: "Sub-course deactivated successfully", }) } // DeleteSubCourse godoc // @Summary Delete sub-course // @Description Deletes a sub-course by its ID // @Tags sub-courses // @Produce json // @Param id path int true "Sub-course ID" // @Success 200 {object} domain.Response // @Failure 400 {object} domain.ErrorResponse // @Failure 500 {object} domain.ErrorResponse // @Router /api/v1/course-management/sub-courses/{id} [delete] func (h *Handler) DeleteSubCourse(c *fiber.Ctx) error { idStr := c.Params("id") id, err := strconv.ParseInt(idStr, 10, 64) if err != nil { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ Message: "Invalid sub-course ID", Error: err.Error(), }) } _, err = h.courseMgmtSvc.DeleteSubCourse(c.Context(), id) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ Message: "Failed to delete sub-course", Error: err.Error(), }) } return c.JSON(domain.Response{ Message: "Sub-course deleted successfully", }) } // Sub-course Video Handlers type createSubCourseVideoReq struct { SubCourseID int64 `json:"sub_course_id" validate:"required"` Title string `json:"title" validate:"required"` Description *string `json:"description"` VideoURL string `json:"video_url" validate:"required"` Duration int32 `json:"duration" validate:"required"` Resolution *string `json:"resolution"` InstructorID *string `json:"instructor_id"` Thumbnail *string `json:"thumbnail"` Visibility *string `json:"visibility"` DisplayOrder *int32 `json:"display_order"` Status *string `json:"status"` // DRAFT, PUBLISHED, INACTIVE, ARCHIVED } type subCourseVideoRes struct { ID int64 `json:"id"` SubCourseID int64 `json:"sub_course_id"` Title string `json:"title"` Description *string `json:"description"` VideoURL string `json:"video_url"` Duration int32 `json:"duration"` Resolution *string `json:"resolution"` InstructorID *string `json:"instructor_id"` Thumbnail *string `json:"thumbnail"` Visibility *string `json:"visibility"` DisplayOrder int32 `json:"display_order"` IsPublished bool `json:"is_published"` PublishDate *string `json:"publish_date"` Status string `json:"status"` VimeoID *string `json:"vimeo_id,omitempty"` VimeoEmbedURL *string `json:"vimeo_embed_url,omitempty"` VimeoPlayerHTML *string `json:"vimeo_player_html,omitempty"` VimeoStatus *string `json:"vimeo_status,omitempty"` } type createVimeoVideoReq struct { SubCourseID int64 `json:"sub_course_id" validate:"required"` Title string `json:"title" validate:"required"` Description *string `json:"description"` SourceURL string `json:"source_url" validate:"required,url"` FileSize int64 `json:"file_size" validate:"required,gt=0"` Duration int32 `json:"duration"` Resolution *string `json:"resolution"` InstructorID *string `json:"instructor_id"` Thumbnail *string `json:"thumbnail"` Visibility *string `json:"visibility"` DisplayOrder *int32 `json:"display_order"` } type createVideoFromVimeoIDReq struct { SubCourseID int64 `json:"sub_course_id" validate:"required"` VimeoVideoID string `json:"vimeo_video_id" validate:"required"` Title string `json:"title" validate:"required"` Description *string `json:"description"` DisplayOrder *int32 `json:"display_order"` InstructorID *string `json:"instructor_id"` } // CreateSubCourseVideo godoc // @Summary Create a new sub-course video // @Description Creates a new video under a specific sub-course // @Tags sub-course-videos // @Accept json // @Produce json // @Param body body createSubCourseVideoReq true "Create video payload" // @Success 201 {object} domain.Response // @Failure 400 {object} domain.ErrorResponse // @Failure 500 {object} domain.ErrorResponse // @Router /api/v1/course-management/videos [post] func (h *Handler) CreateSubCourseVideo(c *fiber.Ctx) error { var req createSubCourseVideoReq if err := c.BodyParser(&req); err != nil { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ Message: "Invalid request body", Error: err.Error(), }) } video, err := h.courseMgmtSvc.CreateSubCourseVideo(c.Context(), req.SubCourseID, req.Title, req.Description, req.VideoURL, req.Duration, req.Resolution, req.InstructorID, req.Thumbnail, req.Visibility, req.DisplayOrder, req.Status) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ Message: "Failed to create sub-course video", Error: err.Error(), }) } var publishDate *string if video.PublishDate != nil { pd := video.PublishDate.String() publishDate = &pd } return c.Status(fiber.StatusCreated).JSON(domain.Response{ Message: "Sub-course video created successfully", Data: subCourseVideoRes{ ID: video.ID, SubCourseID: video.SubCourseID, Title: video.Title, Description: video.Description, VideoURL: video.VideoURL, Duration: video.Duration, Resolution: video.Resolution, InstructorID: video.InstructorID, Thumbnail: video.Thumbnail, Visibility: video.Visibility, DisplayOrder: video.DisplayOrder, IsPublished: video.IsPublished, PublishDate: publishDate, Status: video.Status, }, }) } // GetSubCourseVideoByID godoc // @Summary Get sub-course video by ID // @Description Returns a single video by its ID // @Tags sub-course-videos // @Produce json // @Param id path int true "Video ID" // @Success 200 {object} domain.Response // @Failure 400 {object} domain.ErrorResponse // @Failure 404 {object} domain.ErrorResponse // @Router /api/v1/course-management/videos/{id} [get] func (h *Handler) GetSubCourseVideoByID(c *fiber.Ctx) error { idStr := c.Params("id") id, err := strconv.ParseInt(idStr, 10, 64) if err != nil { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ Message: "Invalid video ID", Error: err.Error(), }) } video, err := h.courseMgmtSvc.GetSubCourseVideoByID(c.Context(), id) if err != nil { return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{ Message: "Video not found", Error: err.Error(), }) } var publishDate *string if video.PublishDate != nil { pd := video.PublishDate.String() publishDate = &pd } return c.JSON(domain.Response{ Message: "Video retrieved successfully", Data: subCourseVideoRes{ ID: video.ID, SubCourseID: video.SubCourseID, Title: video.Title, Description: video.Description, VideoURL: video.VideoURL, Duration: video.Duration, Resolution: video.Resolution, InstructorID: video.InstructorID, Thumbnail: video.Thumbnail, Visibility: video.Visibility, DisplayOrder: video.DisplayOrder, IsPublished: video.IsPublished, PublishDate: publishDate, Status: video.Status, }, }) } type getVideosBySubCourseRes struct { Videos []subCourseVideoRes `json:"videos"` TotalCount int64 `json:"total_count"` } // GetVideosBySubCourse godoc // @Summary Get videos by sub-course // @Description Returns all videos under a specific sub-course // @Tags sub-course-videos // @Produce json // @Param subCourseId path int true "Sub-course ID" // @Success 200 {object} domain.Response // @Failure 400 {object} domain.ErrorResponse // @Failure 500 {object} domain.ErrorResponse // @Router /api/v1/course-management/sub-courses/{subCourseId}/videos [get] func (h *Handler) GetVideosBySubCourse(c *fiber.Ctx) error { subCourseIDStr := c.Params("subCourseId") subCourseID, err := strconv.ParseInt(subCourseIDStr, 10, 64) if err != nil { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ Message: "Invalid sub-course ID", Error: err.Error(), }) } videos, totalCount, err := h.courseMgmtSvc.GetVideosBySubCourse(c.Context(), subCourseID) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ Message: "Failed to retrieve videos", Error: err.Error(), }) } var videoResponses []subCourseVideoRes for _, v := range videos { var publishDate *string if v.PublishDate != nil { pd := v.PublishDate.String() publishDate = &pd } videoResponses = append(videoResponses, subCourseVideoRes{ ID: v.ID, SubCourseID: v.SubCourseID, Title: v.Title, Description: v.Description, VideoURL: v.VideoURL, Duration: v.Duration, Resolution: v.Resolution, InstructorID: v.InstructorID, Thumbnail: v.Thumbnail, Visibility: v.Visibility, DisplayOrder: v.DisplayOrder, IsPublished: v.IsPublished, PublishDate: publishDate, Status: v.Status, }) } return c.JSON(domain.Response{ Message: "Videos retrieved successfully", Data: getVideosBySubCourseRes{ Videos: videoResponses, TotalCount: totalCount, }, }) } // GetPublishedVideosBySubCourse godoc // @Summary Get published videos by sub-course // @Description Returns all published videos under a specific sub-course // @Tags sub-course-videos // @Produce json // @Param subCourseId path int true "Sub-course ID" // @Success 200 {object} domain.Response // @Failure 400 {object} domain.ErrorResponse // @Failure 500 {object} domain.ErrorResponse // @Router /api/v1/course-management/sub-courses/{subCourseId}/videos/published [get] func (h *Handler) GetPublishedVideosBySubCourse(c *fiber.Ctx) error { subCourseIDStr := c.Params("subCourseId") subCourseID, err := strconv.ParseInt(subCourseIDStr, 10, 64) if err != nil { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ Message: "Invalid sub-course ID", Error: err.Error(), }) } videos, err := h.courseMgmtSvc.GetPublishedVideosBySubCourse(c.Context(), subCourseID) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ Message: "Failed to retrieve published videos", Error: err.Error(), }) } var videoResponses []subCourseVideoRes for _, v := range videos { var publishDate *string if v.PublishDate != nil { pd := v.PublishDate.String() publishDate = &pd } videoResponses = append(videoResponses, subCourseVideoRes{ ID: v.ID, SubCourseID: v.SubCourseID, Title: v.Title, Description: v.Description, VideoURL: v.VideoURL, Duration: v.Duration, Resolution: v.Resolution, InstructorID: v.InstructorID, Thumbnail: v.Thumbnail, Visibility: v.Visibility, DisplayOrder: v.DisplayOrder, IsPublished: v.IsPublished, PublishDate: publishDate, Status: v.Status, }) } return c.JSON(domain.Response{ Message: "Published videos retrieved successfully", Data: videoResponses, }) } // PublishSubCourseVideo godoc // @Summary Publish sub-course video // @Description Publishes a video by its ID // @Tags sub-course-videos // @Produce json // @Param id path int true "Video ID" // @Success 200 {object} domain.Response // @Failure 400 {object} domain.ErrorResponse // @Failure 500 {object} domain.ErrorResponse // @Router /api/v1/course-management/videos/{id}/publish [put] func (h *Handler) PublishSubCourseVideo(c *fiber.Ctx) error { idStr := c.Params("id") id, err := strconv.ParseInt(idStr, 10, 64) if err != nil { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ Message: "Invalid video ID", Error: err.Error(), }) } err = h.courseMgmtSvc.PublishSubCourseVideo(c.Context(), id) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ Message: "Failed to publish video", Error: err.Error(), }) } return c.JSON(domain.Response{ Message: "Video published successfully", }) } type updateSubCourseVideoReq struct { 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"` Thumbnail *string `json:"thumbnail"` DisplayOrder *int32 `json:"display_order"` Status *string `json:"status"` // DRAFT, PUBLISHED, INACTIVE, ARCHIVED } // UpdateSubCourseVideo godoc // @Summary Update sub-course video // @Description Updates a video's fields // @Tags sub-course-videos // @Accept json // @Produce json // @Param id path int true "Video ID" // @Param body body updateSubCourseVideoReq true "Update video payload" // @Success 200 {object} domain.Response // @Failure 400 {object} domain.ErrorResponse // @Failure 500 {object} domain.ErrorResponse // @Router /api/v1/course-management/videos/{id} [put] func (h *Handler) UpdateSubCourseVideo(c *fiber.Ctx) error { idStr := c.Params("id") id, err := strconv.ParseInt(idStr, 10, 64) if err != nil { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ Message: "Invalid video ID", Error: err.Error(), }) } var req updateSubCourseVideoReq if err := c.BodyParser(&req); err != nil { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ Message: "Invalid request body", Error: err.Error(), }) } err = h.courseMgmtSvc.UpdateSubCourseVideo(c.Context(), id, req.Title, req.Description, req.VideoURL, req.Duration, req.Resolution, req.Visibility, req.Thumbnail, req.DisplayOrder, req.Status) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ Message: "Failed to update video", Error: err.Error(), }) } return c.JSON(domain.Response{ Message: "Video updated successfully", }) } // DeleteSubCourseVideo godoc // @Summary Delete sub-course video // @Description Archives a video by its ID (soft delete) // @Tags sub-course-videos // @Produce json // @Param id path int true "Video ID" // @Success 200 {object} domain.Response // @Failure 400 {object} domain.ErrorResponse // @Failure 500 {object} domain.ErrorResponse // @Router /api/v1/course-management/videos/{id} [delete] func (h *Handler) DeleteSubCourseVideo(c *fiber.Ctx) error { idStr := c.Params("id") id, err := strconv.ParseInt(idStr, 10, 64) if err != nil { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ Message: "Invalid video ID", Error: err.Error(), }) } err = h.courseMgmtSvc.ArchiveSubCourseVideo(c.Context(), id) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ Message: "Failed to delete video", Error: err.Error(), }) } return c.JSON(domain.Response{ Message: "Video deleted successfully", }) } // NOTE: Practice and Practice Question handlers have been removed. // Use the unified questions system at /api/v1/questions and /api/v1/question-sets instead. // Create a question set with set_type="PRACTICE" and owner_type="SUB_COURSE" to replace practices. // Learning Tree Handler // GetFullLearningTree godoc // @Summary Get full learning tree // @Description Returns the complete learning tree structure with courses and sub-courses // @Tags learning-tree // @Produce json // @Success 200 {object} domain.Response // @Failure 500 {object} domain.ErrorResponse // @Router /api/v1/course-management/learning-tree [get] func (h *Handler) GetFullLearningTree(c *fiber.Ctx) error { courses, err := h.courseMgmtSvc.GetFullLearningTree(c.Context()) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ Message: "Failed to retrieve learning tree", Error: err.Error(), }) } return c.JSON(domain.Response{ Message: "Learning tree retrieved successfully", Data: courses, }) } // CreateSubCourseVideoWithVimeo godoc // @Summary Create a new sub-course video with Vimeo upload // @Description Creates a video by uploading to Vimeo from a source URL // @Tags sub-course-videos // @Accept json // @Produce json // @Param body body createVimeoVideoReq true "Create Vimeo video payload" // @Success 201 {object} domain.Response // @Failure 400 {object} domain.ErrorResponse // @Failure 500 {object} domain.ErrorResponse // @Router /api/v1/course-management/videos/vimeo [post] func (h *Handler) CreateSubCourseVideoWithVimeo(c *fiber.Ctx) error { var req createVimeoVideoReq if err := c.BodyParser(&req); err != nil { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ Message: "Invalid request body", Error: err.Error(), }) } if valErrs, ok := h.validator.Validate(c, req); !ok { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ Message: "Validation failed", Error: fmt.Sprintf("%v", valErrs), }) } video, err := h.courseMgmtSvc.CreateSubCourseVideoWithVimeo( c.Context(), req.SubCourseID, req.Title, req.Description, req.SourceURL, req.FileSize, req.Duration, req.Resolution, req.InstructorID, req.Thumbnail, req.Visibility, req.DisplayOrder, ) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ Message: "Failed to create video with Vimeo", Error: err.Error(), }) } return c.Status(fiber.StatusCreated).JSON(domain.Response{ Message: "Video created and uploaded to Vimeo successfully", Data: mapVideoToResponse(video), Success: true, }) } // CreateSubCourseVideoFromVimeoID godoc // @Summary Create a sub-course video from existing Vimeo video // @Description Creates a video record from an existing Vimeo video ID // @Tags sub-course-videos // @Accept json // @Produce json // @Param body body createVideoFromVimeoIDReq true "Create from Vimeo ID payload" // @Success 201 {object} domain.Response // @Failure 400 {object} domain.ErrorResponse // @Failure 500 {object} domain.ErrorResponse // @Router /api/v1/course-management/videos/vimeo/import [post] func (h *Handler) CreateSubCourseVideoFromVimeoID(c *fiber.Ctx) error { var req createVideoFromVimeoIDReq if err := c.BodyParser(&req); err != nil { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ Message: "Invalid request body", Error: err.Error(), }) } if valErrs, ok := h.validator.Validate(c, req); !ok { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ Message: "Validation failed", Error: fmt.Sprintf("%v", valErrs), }) } video, err := h.courseMgmtSvc.CreateSubCourseVideoFromVimeoID( c.Context(), req.SubCourseID, req.VimeoVideoID, req.Title, req.Description, req.DisplayOrder, req.InstructorID, ) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ Message: "Failed to import video from Vimeo", Error: err.Error(), }) } return c.Status(fiber.StatusCreated).JSON(domain.Response{ Message: "Video imported from Vimeo successfully", Data: mapVideoToResponse(video), Success: true, }) } // Helper function to map video to response func mapVideoToResponse(video domain.SubCourseVideo) subCourseVideoRes { var publishDate *string if video.PublishDate != nil { pd := video.PublishDate.Format("2006-01-02T15:04:05Z07:00") publishDate = &pd } return subCourseVideoRes{ ID: video.ID, SubCourseID: video.SubCourseID, Title: video.Title, Description: video.Description, VideoURL: video.VideoURL, Duration: video.Duration, Resolution: video.Resolution, InstructorID: video.InstructorID, Thumbnail: video.Thumbnail, Visibility: video.Visibility, DisplayOrder: video.DisplayOrder, IsPublished: video.IsPublished, PublishDate: publishDate, Status: video.Status, VimeoID: video.VimeoID, VimeoEmbedURL: video.VimeoEmbedURL, VimeoPlayerHTML: video.VimeoPlayerHTML, VimeoStatus: video.VimeoStatus, } }