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

547 lines
18 KiB
Go

package handlers
import (
"strconv"
"Yimaru-Backend/internal/domain"
"github.com/gofiber/fiber/v2"
)
// CreateCourseCategory godoc
// @Summary Create course category
// @Description Creates a new course category
// @Tags courses
// @Accept json
// @Produce json
// @Param category body domain.CourseCategory true "Course category payload"
// @Success 201 {object} domain.Response{data=domain.CourseCategory}
// @Failure 400 {object} domain.ErrorResponse
// @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/course-categories [post]
func (h *Handler) CreateCourseCategory(c *fiber.Ctx) error {
var req domain.CourseCategory
if err := c.BodyParser(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Invalid request body",
Error: err.Error(),
})
}
cat, err := h.courseMgmtSvc.CreateCourseCategory(c.Context(), req.Name)
if err != nil {
return c.Status(fiber.StatusBadRequest).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: cat,
})
}
// GetCourseCategoryByID godoc
// @Summary Get course category
// @Description Get course category by ID
// @Tags courses
// @Accept json
// @Produce json
// @Param id path int true "Category ID"
// @Success 200 {object} domain.Response{data=domain.CourseCategory}
// @Failure 400 {object} domain.ErrorResponse
// @Failure 404 {object} domain.ErrorResponse
// @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/course-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 || id <= 0 {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Invalid category ID",
Error: "ID must be a positive integer",
})
}
cat, err := h.courseMgmtSvc.GetCourseCategoryByID(c.Context(), id)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Failed to fetch course category",
Error: err.Error(),
})
}
return c.Status(fiber.StatusOK).JSON(domain.Response{
Message: "Course category fetched successfully",
Data: cat,
})
}
// ListActiveCourseCategories godoc
// @Summary List active course categories
// @Description Returns all active course categories
// @Tags courses
// @Accept json
// @Produce json
// @Success 200 {object} domain.Response{data=[]domain.CourseCategory}
// @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/course-categories [get]
func (h *Handler) ListActiveCourseCategories(c *fiber.Ctx) error {
cats, err := h.courseMgmtSvc.ListActiveCourseCategories(c.Context())
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Failed to fetch course categories",
Error: err.Error(),
})
}
return c.Status(fiber.StatusOK).JSON(domain.Response{
Message: "Course categories fetched successfully",
Data: cats,
})
}
// UpdateCourseCategory godoc
// @Summary Update course category
// @Description Updates a course category
// @Tags courses
// @Accept json
// @Produce json
// @Param id path int true "Category ID"
// @Param category body domain.CourseCategory true "Course category payload"
// @Success 200 {object} domain.Response{data=domain.CourseCategory}
// @Failure 400 {object} domain.ErrorResponse
// @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/course-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 || id <= 0 {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Invalid category ID",
Error: "ID must be a positive integer",
})
}
var req domain.CourseCategory
if err := c.BodyParser(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Invalid request body",
Error: err.Error(),
})
}
updated, 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.Status(fiber.StatusOK).JSON(domain.Response{
Message: "Course category updated successfully",
Data: updated,
})
}
// DeactivateCourseCategory godoc
// @Summary Deactivate course category
// @Description Deactivates a course category
// @Tags courses
// @Accept json
// @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-categories/{id}/deactivate [post]
func (h *Handler) DeactivateCourseCategory(c *fiber.Ctx) error {
idStr := c.Params("id")
id, err := strconv.ParseInt(idStr, 10, 64)
if err != nil || id <= 0 {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Invalid category ID",
Error: "ID must be a positive integer",
})
}
if err := h.courseMgmtSvc.DeactivateCourseCategory(c.Context(), id); err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Failed to deactivate course category",
Error: err.Error(),
})
}
return c.Status(fiber.StatusOK).JSON(domain.Response{
Message: "Course category deactivated",
})
}
// --- Courses handlers ---
// CreateCourse godoc
// @Summary Create course
// @Description Creates a new course
// @Tags courses
// @Accept json
// @Produce json
// @Param course body domain.Course true "Course payload"
// @Success 201 {object} domain.Response{data=domain.Course}
// @Failure 400 {object} domain.ErrorResponse
// @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/courses [post]
func (h *Handler) CreateCourse(c *fiber.Ctx) error {
var req domain.Course
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)
if err != nil {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Failed to create course",
Error: err.Error(),
})
}
return c.Status(fiber.StatusCreated).JSON(domain.Response{
Message: "Course created successfully",
Data: course,
})
}
// GetCourseByID godoc
// @Summary Get course
// @Description Get course by ID
// @Tags courses
// @Accept json
// @Produce json
// @Param id path int true "Course ID"
// @Success 200 {object} domain.Response{data=domain.Course}
// @Failure 400 {object} domain.ErrorResponse
// @Failure 404 {object} domain.ErrorResponse
// @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/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 || id <= 0 {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Invalid course ID",
Error: "ID must be a positive integer",
})
}
course, err := h.courseMgmtSvc.GetCourseByID(c.Context(), id)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Failed to fetch course",
Error: err.Error(),
})
}
return c.Status(fiber.StatusOK).JSON(domain.Response{
Message: "Course fetched successfully",
Data: course,
})
}
// ListCoursesByCategory godoc
// @Summary List courses by category
// @Description Returns courses under a given category
// @Tags courses
// @Accept json
// @Produce json
// @Param category_id path int true "Category ID"
// @Success 200 {object} domain.Response{data=[]domain.Course}
// @Failure 400 {object} domain.ErrorResponse
// @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/course-categories/{category_id}/courses [get]
func (h *Handler) ListCoursesByCategory(c *fiber.Ctx) error {
catIDStr := c.Params("category_id")
catID, err := strconv.ParseInt(catIDStr, 10, 64)
if err != nil || catID <= 0 {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Invalid category ID",
Error: "ID must be a positive integer",
})
}
courses, err := h.courseMgmtSvc.ListCoursesByCategory(c.Context(), catID)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Failed to fetch courses",
Error: err.Error(),
})
}
return c.Status(fiber.StatusOK).JSON(domain.Response{
Message: "Courses fetched successfully",
Data: courses,
})
}
// ListActiveCourses godoc
// @Summary List active courses
// @Description Returns all active courses
// @Tags courses
// @Accept json
// @Produce json
// @Success 200 {object} domain.Response{data=[]domain.Course}
// @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/courses [get]
func (h *Handler) ListActiveCourses(c *fiber.Ctx) error {
courses, err := h.courseMgmtSvc.ListActiveCourses(c.Context())
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Failed to fetch courses",
Error: err.Error(),
})
}
return c.Status(fiber.StatusOK).JSON(domain.Response{
Message: "Courses fetched successfully",
Data: courses,
})
}
// UpdateCourse godoc
// @Summary Update course
// @Description Updates a course
// @Tags courses
// @Accept json
// @Produce json
// @Param id path int true "Course ID"
// @Param course body domain.Course true "Course payload"
// @Success 200 {object} domain.Response{data=domain.Course}
// @Failure 400 {object} domain.ErrorResponse
// @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/courses/{id} [put]
func (h *Handler) UpdateCourse(c *fiber.Ctx) error {
var req domain.Course
if err := c.BodyParser(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Invalid request body",
Error: err.Error(),
})
}
updated, err := h.courseMgmtSvc.UpdateCourse(c.Context(), req)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Failed to update course",
Error: err.Error(),
})
}
return c.Status(fiber.StatusOK).JSON(domain.Response{
Message: "Course updated successfully",
Data: updated,
})
}
// DeactivateCourse godoc
// @Summary Deactivate course
// @Description Deactivates a course
// @Tags courses
// @Accept json
// @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/courses/{id}/deactivate [post]
func (h *Handler) DeactivateCourse(c *fiber.Ctx) error {
idStr := c.Params("id")
id, err := strconv.ParseInt(idStr, 10, 64)
if err != nil || id <= 0 {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Invalid course ID",
Error: "ID must be a positive integer",
})
}
if err := h.courseMgmtSvc.DeactivateCourse(c.Context(), id); err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Failed to deactivate course",
Error: err.Error(),
})
}
return c.Status(fiber.StatusOK).JSON(domain.Response{
Message: "Course deactivated",
})
}
// --- Programs, Modules, Videos, Practices, Questions, Levels ---
// For brevity: implement representative handlers for creating and listing programs, modules, videos, practices, questions, and levels.
// CreateProgram godoc
// @Summary Create program
// @Tags courses
// @Accept json
// @Produce json
// @Param program body domain.Program true "Program payload"
// @Success 201 {object} domain.Response{data=domain.Program}
// @Router /api/v1/courses/{course_id}/programs [post]
func (h *Handler) CreateProgram(c *fiber.Ctx) error {
var req domain.Program
if err := c.BodyParser(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "Invalid request body", Error: err.Error()})
}
p, err := h.courseMgmtSvc.CreateProgram(c.Context(), req)
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", Data: p})
}
// ListProgramsByCourse godoc
// @Summary List programs by course
// @Tags courses
// @Param course_id path int true "Course ID"
// @Success 200 {object} domain.Response{data=[]domain.Program}
// @Router /api/v1/courses/{course_id}/programs [get]
func (h *Handler) ListProgramsByCourse(c *fiber.Ctx) error {
courseIDStr := c.Params("course_id")
courseID, err := strconv.ParseInt(courseIDStr, 10, 64)
if err != nil || courseID <= 0 {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "Invalid course ID", Error: "ID must be a positive integer"})
}
items, err := h.courseMgmtSvc.ListProgramsByCourse(c.Context(), courseID)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to fetch programs", Error: err.Error()})
}
return c.Status(fiber.StatusOK).JSON(domain.Response{Message: "Programs fetched", Data: items})
}
// CreateModule godoc
// @Summary Create module
// @Tags courses
// @Accept json
// @Produce json
// @Param module body domain.Module true "Module payload"
// @Success 201 {object} domain.Response{data=domain.Module}
// @Router /api/v1/modules [post]
func (h *Handler) CreateModule(c *fiber.Ctx) error {
var req domain.Module
if err := c.BodyParser(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "Invalid request body", Error: err.Error()})
}
m, err := h.courseMgmtSvc.CreateModule(c.Context(), req)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to create module", Error: err.Error()})
}
return c.Status(fiber.StatusCreated).JSON(domain.Response{Message: "Module created", Data: m})
}
// ListModulesByLevel godoc
// @Summary List modules by level
// @Tags courses
// @Param level_id path int true "Level ID"
// @Success 200 {object} domain.Response{data=[]domain.Module}
// @Router /api/v1/levels/{level_id}/modules [get]
func (h *Handler) ListModulesByLevel(c *fiber.Ctx) error {
lvlStr := c.Params("level_id")
lvlID, err := strconv.ParseInt(lvlStr, 10, 64)
if err != nil || lvlID <= 0 {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "Invalid level ID", Error: "ID must be a positive integer"})
}
items, err := h.courseMgmtSvc.ListModulesByLevel(c.Context(), lvlID)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to fetch modules", Error: err.Error()})
}
return c.Status(fiber.StatusOK).JSON(domain.Response{Message: "Modules fetched", Data: items})
}
// CreateModuleVideo godoc
// @Summary Create module video
// @Tags courses
// @Accept json
// @Produce json
// @Param video body domain.ModuleVideo true "Module video payload"
// @Success 201 {object} domain.Response{data=domain.ModuleVideo}
// @Router /api/v1/module-videos [post]
func (h *Handler) CreateModuleVideo(c *fiber.Ctx) error {
var req domain.ModuleVideo
if err := c.BodyParser(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "Invalid request body", Error: err.Error()})
}
v, err := h.courseMgmtSvc.CreateModuleVideo(c.Context(), req)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to create module video", Error: err.Error()})
}
return c.Status(fiber.StatusCreated).JSON(domain.Response{Message: "Module video created", Data: v})
}
// CreatePractice godoc
// @Summary Create practice
// @Tags courses
// @Accept json
// @Produce json
// @Param practice body domain.Practice true "Practice payload"
// @Success 201 {object} domain.Response{data=domain.Practice}
// @Router /api/v1/practices [post]
func (h *Handler) CreatePractice(c *fiber.Ctx) error {
var req domain.Practice
if err := c.BodyParser(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "Invalid request body", Error: err.Error()})
}
p, err := h.courseMgmtSvc.CreatePractice(c.Context(), req)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to create practice", Error: err.Error()})
}
return c.Status(fiber.StatusCreated).JSON(domain.Response{Message: "Practice created", Data: p})
}
// CreatePracticeQuestion godoc
// @Summary Create practice question
// @Tags courses
// @Accept json
// @Produce json
// @Param question body domain.PracticeQuestion true "Practice question payload"
// @Success 201 {object} domain.Response{data=domain.PracticeQuestion}
// @Router /api/v1/practice-questions [post]
func (h *Handler) CreatePracticeQuestion(c *fiber.Ctx) error {
var req domain.PracticeQuestion
if err := c.BodyParser(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "Invalid request body", Error: err.Error()})
}
q, err := h.courseMgmtSvc.CreatePracticeQuestion(c.Context(), req)
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", Data: q})
}
// CreateLevel godoc
// @Summary Create level
// @Tags courses
// @Accept json
// @Produce json
// @Param level body domain.Level true "Level payload"
// @Success 201 {object} domain.Response{data=domain.Level}
// @Router /api/v1/levels [post]
func (h *Handler) CreateLevel(c *fiber.Ctx) error {
var req domain.Level
if err := c.BodyParser(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "Invalid request body", Error: err.Error()})
}
l, err := h.courseMgmtSvc.CreateLevel(c.Context(), req)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to create level", Error: err.Error()})
}
return c.Status(fiber.StatusCreated).JSON(domain.Response{Message: "Level created", Data: l})
}
// Helper to surface not-implemented errors for optional handlers
func notImplemented(c *fiber.Ctx, name string) error {
return c.Status(fiber.StatusNotImplemented).JSON(domain.ErrorResponse{Message: name + " not implemented", Error: "not implemented"})
}