Yimaru-BackEnd/internal/web_server/handlers/course_management.go

2152 lines
65 KiB
Go

package handlers
import (
"Yimaru-Backend/internal/domain"
"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"`
}
type courseRes struct {
ID int64 `json:"id"`
CategoryID int64 `json:"category_id"`
Title string `json:"title"`
Description *string `json:"description"`
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)
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,
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,
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,
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"`
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.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",
})
}
// Program Handlers
type createProgramReq 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"`
}
type programRes 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"`
IsActive bool `json:"is_active"`
}
// CreateProgram godoc
// @Summary Create a new program
// @Description Creates a new program under a specific course
// @Tags programs
// @Accept json
// @Produce json
// @Param body body createProgramReq true "Create program payload"
// @Success 201 {object} domain.Response
// @Failure 400 {object} domain.ErrorResponse
// @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/course-management/programs [post]
func (h *Handler) CreateProgram(c *fiber.Ctx) error {
var req createProgramReq
if err := c.BodyParser(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Invalid request body",
Error: err.Error(),
})
}
program, err := h.courseMgmtSvc.CreateProgram(c.Context(), req.CourseID, req.Title, req.Description, req.Thumbnail, req.DisplayOrder)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Failed to create program",
Error: err.Error(),
})
}
return c.Status(fiber.StatusCreated).JSON(domain.Response{
Message: "Program created successfully",
Data: programRes{
ID: program.ID,
CourseID: program.CourseID,
Title: program.Title,
Description: program.Description,
Thumbnail: program.Thumbnail,
DisplayOrder: program.DisplayOrder,
IsActive: program.IsActive,
},
})
}
// GetProgramByID godoc
// @Summary Get program by ID
// @Description Returns a single program by its ID
// @Tags programs
// @Produce json
// @Param id path int true "Program 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/programs/{id} [get]
func (h *Handler) GetProgramByID(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 program ID",
Error: err.Error(),
})
}
program, err := h.courseMgmtSvc.GetProgramByID(c.Context(), id)
if err != nil {
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{
Message: "Program not found",
Error: err.Error(),
})
}
return c.JSON(domain.Response{
Message: "Program retrieved successfully",
Data: programRes{
ID: program.ID,
CourseID: program.CourseID,
Title: program.Title,
Description: program.Description,
Thumbnail: program.Thumbnail,
DisplayOrder: program.DisplayOrder,
IsActive: program.IsActive,
},
})
}
type getProgramsByCourseRes struct {
Programs []programRes `json:"programs"`
TotalCount int64 `json:"total_count"`
}
// GetProgramsByCourse godoc
// @Summary Get programs by course
// @Description Returns all programs under a specific course with total count
// @Tags programs
// @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}/programs [get]
func (h *Handler) GetProgramsByCourse(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(),
})
}
programs, totalCount, err := h.courseMgmtSvc.GetProgramsByCourse(c.Context(), courseID)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Failed to retrieve programs",
Error: err.Error(),
})
}
var programResponses []programRes
for _, program := range programs {
programResponses = append(programResponses, programRes{
ID: program.ID,
CourseID: program.CourseID,
Title: program.Title,
Description: program.Description,
Thumbnail: program.Thumbnail,
DisplayOrder: program.DisplayOrder,
IsActive: program.IsActive,
})
}
return c.JSON(domain.Response{
Message: "Programs retrieved successfully",
Data: getProgramsByCourseRes{
Programs: programResponses,
TotalCount: totalCount,
},
})
}
type listProgramsByCourseRes struct {
Programs []programRes `json:"programs"`
}
// ListProgramsByCourse godoc
// @Summary List programs by course
// @Description Returns a simple list of programs under a specific course
// @Tags programs
// @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}/programs/list [get]
func (h *Handler) ListProgramsByCourse(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(),
})
}
programs, err := h.courseMgmtSvc.ListProgramsByCourse(c.Context(), courseID)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Failed to retrieve programs",
Error: err.Error(),
})
}
var programResponses []programRes
for _, program := range programs {
programResponses = append(programResponses, programRes{
ID: program.ID,
CourseID: program.CourseID,
Title: program.Title,
Description: program.Description,
Thumbnail: program.Thumbnail,
DisplayOrder: program.DisplayOrder,
IsActive: program.IsActive,
})
}
return c.JSON(domain.Response{
Message: "Programs retrieved successfully",
Data: listProgramsByCourseRes{
Programs: programResponses,
},
})
}
type listActiveProgramsRes struct {
Programs []programRes `json:"programs"`
}
// ListActivePrograms godoc
// @Summary List active programs
// @Description Returns all active programs across all courses
// @Tags programs
// @Produce json
// @Success 200 {object} domain.Response
// @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/course-management/programs/active [get]
func (h *Handler) ListActivePrograms(c *fiber.Ctx) error {
programs, err := h.courseMgmtSvc.ListActivePrograms(c.Context())
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Failed to retrieve active programs",
Error: err.Error(),
})
}
var programResponses []programRes
for _, program := range programs {
programResponses = append(programResponses, programRes{
ID: program.ID,
CourseID: program.CourseID,
Title: program.Title,
Description: program.Description,
Thumbnail: program.Thumbnail,
DisplayOrder: program.DisplayOrder,
IsActive: program.IsActive,
})
}
return c.JSON(domain.Response{
Message: "Active programs retrieved successfully",
Data: listActiveProgramsRes{
Programs: programResponses,
},
})
}
type updateProgramPartialReq struct {
Title *string `json:"title"`
Description *string `json:"description"`
Thumbnail *string `json:"thumbnail"`
DisplayOrder *int32 `json:"display_order"`
IsActive *bool `json:"is_active"`
}
// UpdateProgramPartial godoc
// @Summary Update program partially
// @Description Updates selected fields of a program
// @Tags programs
// @Accept json
// @Produce json
// @Param id path int true "Program ID"
// @Param body body updateProgramPartialReq true "Update program payload"
// @Success 200 {object} domain.Response
// @Failure 400 {object} domain.ErrorResponse
// @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/course-management/programs/{id} [patch]
func (h *Handler) UpdateProgramPartial(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 program ID",
Error: err.Error(),
})
}
var req updateProgramPartialReq
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.UpdateProgramPartial(c.Context(), id, req.Title, req.Description, req.Thumbnail, req.DisplayOrder, req.IsActive)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Failed to update program",
Error: err.Error(),
})
}
return c.JSON(domain.Response{
Message: "Program updated successfully",
})
}
type updateProgramFullReq 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"`
IsActive bool `json:"is_active"`
}
// UpdateProgramFull godoc
// @Summary Update program fully
// @Description Updates all fields of a program
// @Tags programs
// @Accept json
// @Produce json
// @Param id path int true "Program ID"
// @Param body body updateProgramFullReq true "Update program payload"
// @Success 200 {object} domain.Response
// @Failure 400 {object} domain.ErrorResponse
// @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/course-management/programs/{id}/full [put]
func (h *Handler) UpdateProgramFull(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 program ID",
Error: err.Error(),
})
}
var req updateProgramFullReq
if err := c.BodyParser(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Invalid request body",
Error: err.Error(),
})
}
program := domain.Program{
ID: id,
CourseID: req.CourseID,
Title: req.Title,
Description: req.Description,
Thumbnail: req.Thumbnail,
DisplayOrder: req.DisplayOrder,
IsActive: req.IsActive,
}
updatedProgram, err := h.courseMgmtSvc.UpdateProgramFull(c.Context(), program)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Failed to update program",
Error: err.Error(),
})
}
return c.JSON(domain.Response{
Message: "Program updated successfully",
Data: programRes{
ID: updatedProgram.ID,
CourseID: updatedProgram.CourseID,
Title: updatedProgram.Title,
Description: updatedProgram.Description,
Thumbnail: updatedProgram.Thumbnail,
DisplayOrder: updatedProgram.DisplayOrder,
IsActive: updatedProgram.IsActive,
},
})
}
// DeactivateProgram godoc
// @Summary Deactivate program
// @Description Deactivates a program by setting is_active to false
// @Tags programs
// @Produce json
// @Param id path int true "Program ID"
// @Success 200 {object} domain.Response
// @Failure 400 {object} domain.ErrorResponse
// @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/course-management/programs/{id}/deactivate [put]
func (h *Handler) DeactivateProgram(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 program ID",
Error: err.Error(),
})
}
err = h.courseMgmtSvc.DeactivateProgram(c.Context(), id)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Failed to deactivate program",
Error: err.Error(),
})
}
return c.JSON(domain.Response{
Message: "Program deactivated successfully",
})
}
// DeleteProgram godoc
// @Summary Delete program
// @Description Deletes a program by its ID
// @Tags programs
// @Produce json
// @Param id path int true "Program ID"
// @Success 200 {object} domain.Response
// @Failure 400 {object} domain.ErrorResponse
// @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/course-management/programs/{id} [delete]
func (h *Handler) DeleteProgram(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 program ID",
Error: err.Error(),
})
}
deletedProgram, err := h.courseMgmtSvc.DeleteProgram(c.Context(), id)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Failed to delete program",
Error: err.Error(),
})
}
return c.JSON(domain.Response{
Message: "Program deleted successfully",
Data: programRes{
ID: deletedProgram.ID,
CourseID: deletedProgram.CourseID,
Title: deletedProgram.Title,
Description: deletedProgram.Description,
Thumbnail: deletedProgram.Thumbnail,
DisplayOrder: deletedProgram.DisplayOrder,
IsActive: deletedProgram.IsActive,
},
})
}
// Level Handlers
type createLevelReq struct {
ProgramID int64 `json:"program_id" validate:"required"`
Title string `json:"title" validate:"required"`
Description *string `json:"description"`
LevelIndex int `json:"level_index" validate:"required"`
IsActive *bool `json:"is_active"`
}
type levelRes struct {
ID int64 `json:"id"`
ProgramID int64 `json:"program_id"`
Title string `json:"title"`
Description *string `json:"description"`
LevelIndex int `json:"level_index"`
NumberOfModules int `json:"number_of_modules"`
NumberOfPractices int `json:"number_of_practices"`
NumberOfVideos int `json:"number_of_videos"`
IsActive bool `json:"is_active"`
}
// CreateLevel godoc
// @Summary Create a new level
// @Description Creates a new level under a specific program
// @Tags levels
// @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(),
})
}
level, err := h.courseMgmtSvc.CreateLevel(c.Context(), req.ProgramID, req.Title, req.Description, req.LevelIndex, 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 successfully",
Data: levelRes{
ID: level.ID,
ProgramID: level.ProgramID,
Title: level.Title,
Description: level.Description,
LevelIndex: level.LevelIndex,
NumberOfModules: level.NumberOfModules,
NumberOfPractices: level.NumberOfPractices,
NumberOfVideos: level.NumberOfVideos,
IsActive: level.IsActive,
},
})
}
type getLevelsByProgramRes struct {
Levels []levelRes `json:"levels"`
}
// GetLevelsByProgram godoc
// @Summary Get levels by program
// @Description Returns all levels under a specific program
// @Tags levels
// @Produce json
// @Param programId path int true "Program ID"
// @Success 200 {object} domain.Response
// @Failure 400 {object} domain.ErrorResponse
// @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/course-management/programs/{programId}/levels [get]
func (h *Handler) GetLevelsByProgram(c *fiber.Ctx) error {
programIDStr := c.Params("programId")
programID, err := strconv.ParseInt(programIDStr, 10, 64)
if err != nil {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Invalid program ID",
Error: err.Error(),
})
}
levels, err := h.courseMgmtSvc.GetLevelsByProgram(c.Context(), programID)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Failed to retrieve levels",
Error: err.Error(),
})
}
var levelResponses []levelRes
for _, level := range levels {
levelResponses = append(levelResponses, levelRes{
ID: level.ID,
ProgramID: level.ProgramID,
Title: level.Title,
Description: level.Description,
LevelIndex: level.LevelIndex,
NumberOfModules: level.NumberOfModules,
NumberOfPractices: level.NumberOfPractices,
NumberOfVideos: level.NumberOfVideos,
IsActive: level.IsActive,
})
}
return c.JSON(domain.Response{
Message: "Levels retrieved successfully",
Data: getLevelsByProgramRes{
Levels: levelResponses,
},
})
}
type updateLevelReq struct {
Title *string `json:"title"`
Description *string `json:"description"`
LevelIndex *int `json:"level_index"`
IsActive *bool `json:"is_active"`
}
// UpdateLevel godoc
// @Summary Update level
// @Description Updates a level's title, description, index, and/or active status
// @Tags levels
// @Accept json
// @Produce json
// @Param id path int true "Level ID"
// @Param body body updateLevelReq true "Update level payload"
// @Success 200 {object} domain.Response
// @Failure 400 {object} domain.ErrorResponse
// @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/course-management/levels/{id} [put]
func (h *Handler) UpdateLevel(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 level ID",
Error: err.Error(),
})
}
var req updateLevelReq
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.UpdateLevel(c.Context(), id, req.Title, req.Description, req.LevelIndex, req.IsActive)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Failed to update level",
Error: err.Error(),
})
}
return c.JSON(domain.Response{
Message: "Level updated successfully",
})
}
// IncrementLevelModuleCount godoc
// @Summary Increment level module count
// @Description Increments the module count for a specific level
// @Tags levels
// @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}/increment-module [put]
func (h *Handler) IncrementLevelModuleCount(c *fiber.Ctx) error {
levelIDStr := c.Params("levelId")
levelID, err := strconv.ParseInt(levelIDStr, 10, 64)
if err != nil {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Invalid level ID",
Error: err.Error(),
})
}
err = h.courseMgmtSvc.IncrementLevelModuleCount(c.Context(), levelID)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Failed to increment level module count",
Error: err.Error(),
})
}
return c.JSON(domain.Response{
Message: "Level module count incremented successfully",
})
}
// IncrementLevelPracticeCount godoc
// @Summary Increment level practice count
// @Description Increments the practice count for a specific level
// @Tags levels
// @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}/increment-practice [put]
func (h *Handler) IncrementLevelPracticeCount(c *fiber.Ctx) error {
levelIDStr := c.Params("levelId")
levelID, err := strconv.ParseInt(levelIDStr, 10, 64)
if err != nil {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Invalid level ID",
Error: err.Error(),
})
}
err = h.courseMgmtSvc.IncrementLevelPracticeCount(c.Context(), levelID)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Failed to increment level practice count",
Error: err.Error(),
})
}
return c.JSON(domain.Response{
Message: "Level practice count incremented successfully",
})
}
// IncrementLevelVideoCount godoc
// @Summary Increment level video count
// @Description Increments the video count for a specific level
// @Tags levels
// @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}/increment-video [put]
func (h *Handler) IncrementLevelVideoCount(c *fiber.Ctx) error {
levelIDStr := c.Params("levelId")
levelID, err := strconv.ParseInt(levelIDStr, 10, 64)
if err != nil {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Invalid level ID",
Error: err.Error(),
})
}
err = h.courseMgmtSvc.IncrementLevelVideoCount(c.Context(), levelID)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Failed to increment level video count",
Error: err.Error(),
})
}
return c.JSON(domain.Response{
Message: "Level video count incremented successfully",
})
}
// DeleteLevel godoc
// @Summary Delete level
// @Description Deletes a level by its ID
// @Tags levels
// @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} [delete]
func (h *Handler) DeleteLevel(c *fiber.Ctx) error {
levelIDStr := c.Params("levelId")
levelID, err := strconv.ParseInt(levelIDStr, 10, 64)
if err != nil {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Invalid level ID",
Error: err.Error(),
})
}
err = h.courseMgmtSvc.DeleteLevel(c.Context(), levelID)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Failed to delete level",
Error: err.Error(),
})
}
return c.JSON(domain.Response{
Message: "Level deleted successfully",
})
}
// Module Handlers
type createModuleReq struct {
LevelID int64 `json:"level_id" validate:"required"`
Title string `json:"title" validate:"required"`
Content *string `json:"content"`
DisplayOrder *int32 `json:"display_order"`
}
type moduleRes struct {
ID int64 `json:"id"`
LevelID int64 `json:"level_id"`
Title string `json:"title"`
Content *string `json:"content"`
DisplayOrder int32 `json:"display_order"`
IsActive bool `json:"is_active"`
}
// CreateModule godoc
// @Summary Create a new module
// @Description Creates a new module under a specific level
// @Tags modules
// @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(),
})
}
module, err := h.courseMgmtSvc.CreateModule(c.Context(), req.LevelID, req.Title, req.Content, req.DisplayOrder)
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 successfully",
Data: moduleRes{
ID: module.ID,
LevelID: module.LevelID,
Title: module.Title,
Content: module.Content,
DisplayOrder: module.DisplayOrder,
IsActive: module.IsActive,
},
})
}
type getModulesByLevelRes struct {
Modules []moduleRes `json:"modules"`
TotalCount int64 `json:"total_count"`
}
// GetModulesByLevel godoc
// @Summary Get modules by level
// @Description Returns a paginated list of modules under a specific level
// @Tags modules
// @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) GetModulesByLevel(c *fiber.Ctx) error {
levelIDStr := c.Params("levelId")
levelID, err := strconv.ParseInt(levelIDStr, 10, 64)
if err != nil {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Invalid level ID",
Error: err.Error(),
})
}
modules, totalCount, err := h.courseMgmtSvc.GetModulesByLevel(c.Context(), levelID)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Failed to retrieve modules",
Error: err.Error(),
})
}
var moduleResponses []moduleRes
for _, module := range modules {
moduleResponses = append(moduleResponses, moduleRes{
ID: module.ID,
LevelID: module.LevelID,
Title: module.Title,
Content: module.Content,
DisplayOrder: module.DisplayOrder,
IsActive: module.IsActive,
})
}
return c.JSON(domain.Response{
Message: "Modules retrieved successfully",
Data: getModulesByLevelRes{
Modules: moduleResponses,
TotalCount: totalCount,
},
})
}
type updateModuleReq struct {
Title *string `json:"title"`
Content *string `json:"content"`
DisplayOrder *int32 `json:"display_order"`
IsActive *bool `json:"is_active"`
}
// UpdateModule godoc
// @Summary Update module
// @Description Updates a module's title, content, display order, and/or active status
// @Tags modules
// @Accept json
// @Produce json
// @Param id path int true "Module ID"
// @Param body body updateModuleReq true "Update module payload"
// @Success 200 {object} domain.Response
// @Failure 400 {object} domain.ErrorResponse
// @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/course-management/modules/{id} [put]
func (h *Handler) UpdateModule(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 module ID",
Error: err.Error(),
})
}
var req updateModuleReq
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.UpdateModule(c.Context(), id, req.Title, req.Content, req.DisplayOrder, req.IsActive)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Failed to update module",
Error: err.Error(),
})
}
return c.JSON(domain.Response{
Message: "Module updated successfully",
})
}
// DeleteModule godoc
// @Summary Delete module
// @Description Deletes a module by its ID
// @Tags modules
// @Produce json
// @Param id 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/{id} [delete]
func (h *Handler) DeleteModule(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 module ID",
Error: err.Error(),
})
}
err = h.courseMgmtSvc.DeleteModule(c.Context(), id)
if 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 successfully",
})
}
// Module Video Handlers
type createModuleVideoReq struct {
ModuleID int64 `json:"module_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"`
}
type moduleVideoRes struct {
ID int64 `json:"id"`
ModuleID int64 `json:"module_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"`
IsPublished bool `json:"is_published"`
PublishDate *string `json:"publish_date,omitempty"`
IsActive bool `json:"is_active"`
}
// CreateModuleVideo godoc
// @Summary Create a new module video
// @Description Creates a new video under a specific module
// @Tags module-videos
// @Accept json
// @Produce json
// @Param body body createModuleVideoReq 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) CreateModuleVideo(c *fiber.Ctx) error {
var req createModuleVideoReq
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.CreateModuleVideo(c.Context(), req.ModuleID, req.Title, req.Description, req.VideoURL, req.Duration, req.Resolution, req.InstructorID, req.Thumbnail, req.Visibility)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Failed to create module video",
Error: err.Error(),
})
}
var publishDateStr *string
if video.PublishDate != nil {
publishDateStr = new(string)
*publishDateStr = video.PublishDate.String()
}
return c.Status(fiber.StatusCreated).JSON(domain.Response{
Message: "Module video created successfully",
Data: moduleVideoRes{
ID: video.ID,
ModuleID: video.ModuleID,
Title: video.Title,
Description: video.Description,
VideoURL: video.VideoURL,
Duration: video.Duration,
Resolution: video.Resolution,
InstructorID: video.InstructorID,
Thumbnail: video.Thumbnail,
Visibility: video.Visibility,
IsPublished: video.IsPublished,
PublishDate: publishDateStr,
IsActive: video.IsActive,
},
})
}
// PublishModuleVideo godoc
// @Summary Publish module video
// @Description Publishes a module video by setting publish date
// @Tags module-videos
// @Produce json
// @Param videoId 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/{videoId}/publish [put]
func (h *Handler) PublishModuleVideo(c *fiber.Ctx) error {
videoIDStr := c.Params("videoId")
videoID, err := strconv.ParseInt(videoIDStr, 10, 64)
if err != nil {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Invalid video ID",
Error: err.Error(),
})
}
err = h.courseMgmtSvc.PublishModuleVideo(c.Context(), videoID)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Failed to publish module video",
Error: err.Error(),
})
}
return c.JSON(domain.Response{
Message: "Module video published successfully",
})
}
type getPublishedVideosByModuleRes struct {
Videos []moduleVideoRes `json:"videos"`
}
// GetPublishedVideosByModule godoc
// @Summary Get published videos by module
// @Description Returns all published videos under a specific module
// @Tags module-videos
// @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}/videos/published [get]
func (h *Handler) GetPublishedVideosByModule(c *fiber.Ctx) error {
moduleIDStr := c.Params("moduleId")
moduleID, err := strconv.ParseInt(moduleIDStr, 10, 64)
if err != nil {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Invalid module ID",
Error: err.Error(),
})
}
videos, err := h.courseMgmtSvc.GetPublishedVideosByModule(c.Context(), moduleID)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Failed to retrieve published videos",
Error: err.Error(),
})
}
var videoResponses []moduleVideoRes
for _, video := range videos {
var publishDateStr *string
if video.PublishDate != nil {
publishDateStr = new(string)
*publishDateStr = video.PublishDate.String()
}
videoResponses = append(videoResponses, moduleVideoRes{
ID: video.ID,
ModuleID: video.ModuleID,
Title: video.Title,
Description: video.Description,
VideoURL: video.VideoURL,
Duration: video.Duration,
Resolution: video.Resolution,
InstructorID: video.InstructorID,
Thumbnail: video.Thumbnail,
Visibility: video.Visibility,
IsPublished: video.IsPublished,
PublishDate: publishDateStr,
IsActive: video.IsActive,
})
}
return c.JSON(domain.Response{
Message: "Published videos retrieved successfully",
Data: getPublishedVideosByModuleRes{
Videos: videoResponses,
},
})
}
type updateModuleVideoReq 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"`
IsActive *bool `json:"is_active"`
}
// UpdateModuleVideo godoc
// @Summary Update module video
// @Description Updates a module video's fields
// @Tags module-videos
// @Accept json
// @Produce json
// @Param id path int true "Video ID"
// @Param body body updateModuleVideoReq 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) UpdateModuleVideo(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 updateModuleVideoReq
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.UpdateModuleVideo(c.Context(), id, req.Title, req.Description, req.VideoURL, req.Duration, req.Resolution, req.Visibility, req.Thumbnail, req.IsActive)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Failed to update module video",
Error: err.Error(),
})
}
return c.JSON(domain.Response{
Message: "Module video updated successfully",
})
}
// DeleteModuleVideo godoc
// @Summary Delete module video
// @Description Deletes a module video by its ID
// @Tags module-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) DeleteModuleVideo(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.DeleteModuleVideo(c.Context(), id)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Failed to delete module video",
Error: err.Error(),
})
}
return c.JSON(domain.Response{
Message: "Module video deleted successfully",
})
}
// Practice Handlers
type createPracticeReq struct {
OwnerType string `json:"owner_type" validate:"required"`
OwnerID int64 `json:"owner_id" validate:"required"`
Title string `json:"title" validate:"required"`
Description *string `json:"description"`
BannerImage *string `json:"banner_image"`
Persona *string `json:"persona"`
IsActive *bool `json:"is_active"`
}
type practiceRes struct {
ID int64 `json:"id"`
OwnerType string `json:"owner_type"`
OwnerID int64 `json:"owner_id"`
Title string `json:"title"`
Description *string `json:"description"`
BannerImage *string `json:"banner_image"`
Persona *string `json:"persona"`
IsActive bool `json:"is_active"`
}
// CreatePractice godoc
// @Summary Create a new practice
// @Description Creates a new practice for a specific owner (module or level)
// @Tags practices
// @Accept json
// @Produce json
// @Param body body createPracticeReq true "Create practice payload"
// @Success 201 {object} domain.Response
// @Failure 400 {object} domain.ErrorResponse
// @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/course-management/practices [post]
func (h *Handler) CreatePractice(c *fiber.Ctx) error {
var req createPracticeReq
if err := c.BodyParser(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Invalid request body",
Error: err.Error(),
})
}
practice, err := h.courseMgmtSvc.CreatePractice(c.Context(), req.OwnerType, req.OwnerID, req.Title, req.Description, req.BannerImage, req.Persona, 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 successfully",
Data: practiceRes{
ID: practice.ID,
OwnerType: practice.OwnerType,
OwnerID: practice.OwnerID,
Title: practice.Title,
Description: practice.Description,
BannerImage: practice.BannerImage,
Persona: practice.Persona,
IsActive: practice.IsActive,
},
})
}
type getPracticesByOwnerRes struct {
Practices []practiceRes `json:"practices"`
}
// GetPracticesByOwner godoc
// @Summary Get practices by owner
// @Description Returns all practices for a specific owner type and ID
// @Tags practices
// @Produce json
// @Param ownerType path string true "Owner Type"
// @Param ownerId path int true "Owner ID"
// @Success 200 {object} domain.Response
// @Failure 400 {object} domain.ErrorResponse
// @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/course-management/owners/{ownerType}/{ownerId}/practices [get]
func (h *Handler) GetPracticesByOwner(c *fiber.Ctx) error {
ownerType := c.Params("ownerType")
ownerIDStr := c.Params("ownerId")
ownerID, err := strconv.ParseInt(ownerIDStr, 10, 64)
if err != nil {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Invalid owner ID",
Error: err.Error(),
})
}
practices, err := h.courseMgmtSvc.GetPracticesByOwner(c.Context(), ownerType, ownerID)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Failed to retrieve practices",
Error: err.Error(),
})
}
var practiceResponses []practiceRes
for _, practice := range practices {
practiceResponses = append(practiceResponses, practiceRes{
ID: practice.ID,
OwnerType: practice.OwnerType,
OwnerID: practice.OwnerID,
Title: practice.Title,
Description: practice.Description,
BannerImage: practice.BannerImage,
Persona: practice.Persona,
IsActive: practice.IsActive,
})
}
return c.JSON(domain.Response{
Message: "Practices retrieved successfully",
Data: getPracticesByOwnerRes{
Practices: practiceResponses,
},
})
}
type updatePracticeReq struct {
Title *string `json:"title"`
Description *string `json:"description"`
BannerImage *string `json:"banner_image"`
Persona *string `json:"persona"`
IsActive *bool `json:"is_active"`
}
// UpdatePractice godoc
// @Summary Update practice
// @Description Updates a practice's fields
// @Tags practices
// @Accept json
// @Produce json
// @Param id path int true "Practice ID"
// @Param body body updatePracticeReq true "Update practice payload"
// @Success 200 {object} domain.Response
// @Failure 400 {object} domain.ErrorResponse
// @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/course-management/practices/{id} [put]
func (h *Handler) UpdatePractice(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 practice ID",
Error: err.Error(),
})
}
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(),
})
}
err = h.courseMgmtSvc.UpdatePractice(c.Context(), id, req.Title, req.Description, req.BannerImage, req.Persona, req.IsActive)
if 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 successfully",
})
}
// DeletePractice godoc
// @Summary Delete practice
// @Description Deletes a practice by its ID
// @Tags practices
// @Produce json
// @Param id path int true "Practice ID"
// @Success 200 {object} domain.Response
// @Failure 400 {object} domain.ErrorResponse
// @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/course-management/practices/{id} [delete]
func (h *Handler) DeletePractice(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 practice ID",
Error: err.Error(),
})
}
err = h.courseMgmtSvc.DeletePractice(c.Context(), id)
if 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 successfully",
})
}
// Practice Question Handlers
type createPracticeQuestionReq struct {
PracticeID int64 `json:"practice_id" validate:"required"`
Question string `json:"question" validate:"required"`
QuestionVoicePrompt *string `json:"question_voice_prompt"`
SampleAnswerVoicePrompt *string `json:"sample_answer_voice_prompt"`
SampleAnswer *string `json:"sample_answer"`
Tips *string `json:"tips"`
QType string `json:"q_type" validate:"required"`
}
type practiceQuestionRes struct {
ID int64 `json:"id"`
PracticeID int64 `json:"practice_id"`
Question string `json:"question"`
QuestionVoicePrompt *string `json:"question_voice_prompt"`
SampleAnswerVoicePrompt *string `json:"sample_answer_voice_prompt"`
SampleAnswer *string `json:"sample_answer"`
Tips *string `json:"tips"`
Type string `json:"type"`
}
// CreatePracticeQuestion godoc
// @Summary Create a new practice question
// @Description Creates a new question under a specific practice
// @Tags practice-questions
// @Accept json
// @Produce json
// @Param body body createPracticeQuestionReq true "Create question payload"
// @Success 201 {object} domain.Response
// @Failure 400 {object} domain.ErrorResponse
// @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/course-management/questions [post]
func (h *Handler) CreatePracticeQuestion(c *fiber.Ctx) error {
var req createPracticeQuestionReq
if err := c.BodyParser(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Invalid request body",
Error: err.Error(),
})
}
question, err := h.courseMgmtSvc.CreatePracticeQuestion(c.Context(), req.PracticeID, req.Question, req.QuestionVoicePrompt, req.SampleAnswerVoicePrompt, req.SampleAnswer, req.Tips, req.QType)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Failed to create practice question",
Error: err.Error(),
})
}
return c.Status(fiber.StatusCreated).JSON(domain.Response{
Message: "Practice question created successfully",
Data: practiceQuestionRes{
ID: question.ID,
PracticeID: question.PracticeID,
Question: question.Question,
QuestionVoicePrompt: question.QuestionVoicePrompt,
SampleAnswerVoicePrompt: question.SampleAnswerVoicePrompt,
SampleAnswer: question.SampleAnswer,
Tips: question.Tips,
Type: question.Type,
},
})
}
type getQuestionsByPracticeRes struct {
Questions []practiceQuestionRes `json:"questions"`
}
// GetQuestionsByPractice godoc
// @Summary Get questions by practice
// @Description Returns all questions under a specific practice
// @Tags practice-questions
// @Produce json
// @Param practiceId path int true "Practice ID"
// @Success 200 {object} domain.Response
// @Failure 400 {object} domain.ErrorResponse
// @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/course-management/practices/{practiceId}/questions [get]
func (h *Handler) GetQuestionsByPractice(c *fiber.Ctx) error {
practiceIDStr := c.Params("practiceId")
practiceID, err := strconv.ParseInt(practiceIDStr, 10, 64)
if err != nil {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Invalid practice ID",
Error: err.Error(),
})
}
questions, err := h.courseMgmtSvc.GetQuestionsByPractice(c.Context(), practiceID)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Failed to retrieve questions",
Error: err.Error(),
})
}
var questionResponses []practiceQuestionRes
for _, question := range questions {
questionResponses = append(questionResponses, practiceQuestionRes{
ID: question.ID,
PracticeID: question.PracticeID,
Question: question.Question,
QuestionVoicePrompt: question.QuestionVoicePrompt,
SampleAnswerVoicePrompt: question.SampleAnswerVoicePrompt,
SampleAnswer: question.SampleAnswer,
Tips: question.Tips,
Type: question.Type,
})
}
return c.JSON(domain.Response{
Message: "Questions retrieved successfully",
Data: getQuestionsByPracticeRes{
Questions: questionResponses,
},
})
}
type updatePracticeQuestionReq struct {
Question *string `json:"question"`
SampleAnswer *string `json:"sample_answer"`
Tips *string `json:"tips"`
QType *string `json:"q_type"`
}
// UpdatePracticeQuestion godoc
// @Summary Update practice question
// @Description Updates a practice question's fields
// @Tags practice-questions
// @Accept json
// @Produce json
// @Param id path int true "Question ID"
// @Param body body updatePracticeQuestionReq true "Update question payload"
// @Success 200 {object} domain.Response
// @Failure 400 {object} domain.ErrorResponse
// @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/course-management/questions/{id} [put]
func (h *Handler) UpdatePracticeQuestion(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 question ID",
Error: err.Error(),
})
}
var req updatePracticeQuestionReq
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.UpdatePracticeQuestion(c.Context(), id, req.Question, req.SampleAnswer, req.Tips, req.QType)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Failed to update practice question",
Error: err.Error(),
})
}
return c.JSON(domain.Response{
Message: "Practice question updated successfully",
})
}
// DeletePracticeQuestion godoc
// @Summary Delete practice question
// @Description Deletes a practice question by its ID
// @Tags practice-questions
// @Produce json
// @Param id path int true "Question ID"
// @Success 200 {object} domain.Response
// @Failure 400 {object} domain.ErrorResponse
// @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/course-management/questions/{id} [delete]
func (h *Handler) DeletePracticeQuestion(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 question ID",
Error: err.Error(),
})
}
err = h.courseMgmtSvc.DeletePracticeQuestion(c.Context(), id)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Failed to delete practice question",
Error: err.Error(),
})
}
return c.JSON(domain.Response{
Message: "Practice question deleted successfully",
})
}
// Learning Tree Handler
// GetFullLearningTree godoc
// @Summary Get full learning tree
// @Description Returns the complete learning tree structure with courses, programs, levels, and modules
// @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,
})
}