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

450 lines
15 KiB
Go

package handlers
import (
"Yimaru-Backend/internal/domain"
"errors"
"strconv"
"time"
"github.com/gofiber/fiber/v2"
)
// --- Request / Response types ---
type addPrerequisiteReq struct {
PrerequisiteSubCourseID int64 `json:"prerequisite_sub_course_id" validate:"required"`
}
type prerequisiteRes struct {
ID int64 `json:"id"`
SubCourseID int64 `json:"sub_course_id"`
PrerequisiteSubCourseID int64 `json:"prerequisite_sub_course_id"`
PrerequisiteTitle string `json:"prerequisite_title"`
PrerequisiteLevel string `json:"prerequisite_level"`
PrerequisiteDisplayOrder int32 `json:"prerequisite_display_order"`
}
type dependentRes struct {
ID int64 `json:"id"`
SubCourseID int64 `json:"sub_course_id"`
PrerequisiteSubCourseID int64 `json:"prerequisite_sub_course_id"`
DependentTitle string `json:"dependent_title"`
DependentLevel string `json:"dependent_level"`
}
type updateProgressReq struct {
ProgressPercentage int16 `json:"progress_percentage" validate:"required,min=0,max=100"`
}
type subCourseProgressRes struct {
SubCourseID int64 `json:"sub_course_id"`
Title string `json:"title"`
Description *string `json:"description,omitempty"`
Thumbnail *string `json:"thumbnail,omitempty"`
DisplayOrder int32 `json:"display_order"`
Level string `json:"level"`
ProgressStatus string `json:"progress_status"`
ProgressPercentage int16 `json:"progress_percentage"`
StartedAt *time.Time `json:"started_at,omitempty"`
CompletedAt *time.Time `json:"completed_at,omitempty"`
IsLocked bool `json:"is_locked"`
}
type userProgressRes struct {
SubCourseID int64 `json:"sub_course_id"`
SubCourseTitle string `json:"sub_course_title"`
SubCourseLevel string `json:"sub_course_level"`
Status string `json:"status"`
ProgressPercentage int16 `json:"progress_percentage"`
StartedAt *time.Time `json:"started_at,omitempty"`
CompletedAt *time.Time `json:"completed_at,omitempty"`
}
func mapSubCourseProgress(items []domain.SubCourseWithProgress) []subCourseProgressRes {
res := make([]subCourseProgressRes, 0, len(items))
for _, item := range items {
res = append(res, subCourseProgressRes{
SubCourseID: item.SubCourseID,
Title: item.Title,
Description: item.Description,
Thumbnail: item.Thumbnail,
DisplayOrder: item.DisplayOrder,
Level: item.Level,
ProgressStatus: string(item.ProgressStatus),
ProgressPercentage: item.ProgressPercentage,
StartedAt: item.StartedAt,
CompletedAt: item.CompletedAt,
IsLocked: item.IsLocked,
})
}
return res
}
// --- Prerequisite Handlers (admin) ---
// AddSubCoursePrerequisite godoc
// @Summary Add prerequisite to sub-course
// @Description Link a prerequisite sub-course that must be completed before accessing this sub-course
// @Tags progression
// @Accept json
// @Produce json
// @Param id path int true "Sub-course ID"
// @Param body body addPrerequisiteReq true "Prerequisite sub-course ID"
// @Success 201 {object} domain.Response
// @Failure 400 {object} domain.ErrorResponse
// @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/course-management/sub-courses/{id}/prerequisites [post]
func (h *Handler) AddSubCoursePrerequisite(c *fiber.Ctx) error {
subCourseID, err := strconv.ParseInt(c.Params("id"), 10, 64)
if err != nil {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Invalid sub-course ID",
Error: err.Error(),
})
}
var req addPrerequisiteReq
if err := c.BodyParser(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Invalid request body",
Error: err.Error(),
})
}
if err := h.courseMgmtSvc.AddSubCoursePrerequisite(c.Context(), subCourseID, req.PrerequisiteSubCourseID); err != nil {
if errors.Is(err, domain.ErrSelfPrerequisite) {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Invalid prerequisite",
Error: err.Error(),
})
}
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Failed to add prerequisite",
Error: err.Error(),
})
}
return c.Status(fiber.StatusCreated).JSON(domain.Response{
Message: "Prerequisite added successfully",
})
}
// GetSubCoursePrerequisites godoc
// @Summary Get sub-course prerequisites
// @Description Returns all prerequisites for a sub-course
// @Tags progression
// @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}/prerequisites [get]
func (h *Handler) GetSubCoursePrerequisites(c *fiber.Ctx) error {
subCourseID, err := strconv.ParseInt(c.Params("id"), 10, 64)
if err != nil {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Invalid sub-course ID",
Error: err.Error(),
})
}
prerequisites, err := h.courseMgmtSvc.GetSubCoursePrerequisites(c.Context(), subCourseID)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Failed to get prerequisites",
Error: err.Error(),
})
}
var res []prerequisiteRes
for _, p := range prerequisites {
res = append(res, prerequisiteRes{
ID: p.ID,
SubCourseID: p.SubCourseID,
PrerequisiteSubCourseID: p.PrerequisiteSubCourseID,
PrerequisiteTitle: p.PrerequisiteTitle,
PrerequisiteLevel: p.PrerequisiteLevel,
PrerequisiteDisplayOrder: p.PrerequisiteDisplayOrder,
})
}
return c.JSON(domain.Response{
Message: "Prerequisites retrieved successfully",
Data: res,
})
}
// RemoveSubCoursePrerequisite godoc
// @Summary Remove prerequisite from sub-course
// @Description Unlink a prerequisite from a sub-course
// @Tags progression
// @Produce json
// @Param id path int true "Sub-course ID"
// @Param prerequisiteId path int true "Prerequisite 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}/prerequisites/{prerequisiteId} [delete]
func (h *Handler) RemoveSubCoursePrerequisite(c *fiber.Ctx) error {
subCourseID, err := strconv.ParseInt(c.Params("id"), 10, 64)
if err != nil {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Invalid sub-course ID",
Error: err.Error(),
})
}
prerequisiteID, err := strconv.ParseInt(c.Params("prerequisiteId"), 10, 64)
if err != nil {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Invalid prerequisite ID",
Error: err.Error(),
})
}
if err := h.courseMgmtSvc.RemoveSubCoursePrerequisite(c.Context(), subCourseID, prerequisiteID); err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Failed to remove prerequisite",
Error: err.Error(),
})
}
return c.JSON(domain.Response{
Message: "Prerequisite removed successfully",
})
}
// --- User Progress Handlers ---
// StartSubCourse godoc
// @Summary Start a sub-course
// @Description Mark a sub-course as started for the authenticated user (checks prerequisites)
// @Tags progression
// @Produce json
// @Param id path int true "Sub-course ID"
// @Success 201 {object} domain.Response
// @Failure 400 {object} domain.ErrorResponse
// @Failure 403 {object} domain.ErrorResponse
// @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/progress/sub-courses/{id}/start [post]
func (h *Handler) StartSubCourse(c *fiber.Ctx) error {
userID := c.Locals("user_id").(int64)
subCourseID, err := strconv.ParseInt(c.Params("id"), 10, 64)
if err != nil {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Invalid sub-course ID",
Error: err.Error(),
})
}
progress, err := h.courseMgmtSvc.StartSubCourse(c.Context(), userID, subCourseID)
if err != nil {
if errors.Is(err, domain.ErrPrerequisiteNotMet) {
return c.Status(fiber.StatusForbidden).JSON(domain.ErrorResponse{
Message: "Cannot start sub-course",
Error: "Prerequisites not completed",
})
}
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Failed to start sub-course",
Error: err.Error(),
})
}
return c.Status(fiber.StatusCreated).JSON(domain.Response{
Message: "Sub-course started",
Data: userProgressRes{
SubCourseID: progress.SubCourseID,
Status: string(progress.Status),
ProgressPercentage: progress.ProgressPercentage,
StartedAt: progress.StartedAt,
},
})
}
// UpdateSubCourseProgress godoc
// @Summary Update sub-course progress
// @Description Update the progress percentage for a sub-course
// @Tags progression
// @Accept json
// @Produce json
// @Param id path int true "Sub-course ID"
// @Param body body updateProgressReq true "Progress update"
// @Success 200 {object} domain.Response
// @Failure 400 {object} domain.ErrorResponse
// @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/progress/sub-courses/{id} [put]
func (h *Handler) UpdateSubCourseProgress(c *fiber.Ctx) error {
userID := c.Locals("user_id").(int64)
subCourseID, err := strconv.ParseInt(c.Params("id"), 10, 64)
if err != nil {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Invalid sub-course ID",
Error: err.Error(),
})
}
var req updateProgressReq
if err := c.BodyParser(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Invalid request body",
Error: err.Error(),
})
}
if err := h.courseMgmtSvc.UpdateSubCourseProgress(c.Context(), userID, subCourseID, req.ProgressPercentage); err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Failed to update progress",
Error: err.Error(),
})
}
return c.JSON(domain.Response{
Message: "Progress updated successfully",
})
}
// CompleteSubCourse godoc
// @Summary Complete a sub-course
// @Description Mark a sub-course as completed for the authenticated user
// @Tags progression
// @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/progress/sub-courses/{id}/complete [post]
func (h *Handler) CompleteSubCourse(c *fiber.Ctx) error {
userID := c.Locals("user_id").(int64)
subCourseID, err := strconv.ParseInt(c.Params("id"), 10, 64)
if err != nil {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Invalid sub-course ID",
Error: err.Error(),
})
}
if err := h.courseMgmtSvc.CompleteSubCourse(c.Context(), userID, subCourseID); err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Failed to complete sub-course",
Error: err.Error(),
})
}
return c.JSON(domain.Response{
Message: "Sub-course completed",
})
}
// CheckSubCourseAccess godoc
// @Summary Check sub-course access
// @Description Check if the authenticated user has completed all prerequisites for a sub-course
// @Tags progression
// @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/progress/sub-courses/{id}/access [get]
func (h *Handler) CheckSubCourseAccess(c *fiber.Ctx) error {
userID := c.Locals("user_id").(int64)
subCourseID, err := strconv.ParseInt(c.Params("id"), 10, 64)
if err != nil {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Invalid sub-course ID",
Error: err.Error(),
})
}
accessible, err := h.courseMgmtSvc.CheckSubCourseAccess(c.Context(), userID, subCourseID)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Failed to check access",
Error: err.Error(),
})
}
return c.JSON(domain.Response{
Message: "Access check completed",
Data: fiber.Map{
"accessible": accessible,
},
})
}
// GetUserCourseProgress godoc
// @Summary Get user's course progress
// @Description Returns the authenticated user's progress for all sub-courses in a course, including lock status
// @Tags progression
// @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/progress/courses/{courseId} [get]
func (h *Handler) GetUserCourseProgress(c *fiber.Ctx) error {
userID := c.Locals("user_id").(int64)
courseID, err := strconv.ParseInt(c.Params("courseId"), 10, 64)
if err != nil {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Invalid course ID",
Error: err.Error(),
})
}
items, err := h.courseMgmtSvc.GetSubCoursesWithProgress(c.Context(), userID, courseID)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Failed to get course progress",
Error: err.Error(),
})
}
return c.JSON(domain.Response{
Message: "Course progress retrieved successfully",
Data: mapSubCourseProgress(items),
})
}
// GetUserCourseProgressForAdmin godoc
// @Summary Get learner's course progress (admin)
// @Description Returns a target learner's progress for all sub-courses in a course, including lock status
// @Tags progression
// @Produce json
// @Param userId path int true "Learner User ID"
// @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/admin/users/{userId}/progress/courses/{courseId} [get]
func (h *Handler) GetUserCourseProgressForAdmin(c *fiber.Ctx) error {
targetUserID, err := strconv.ParseInt(c.Params("userId"), 10, 64)
if err != nil {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Invalid user ID",
Error: err.Error(),
})
}
courseID, err := strconv.ParseInt(c.Params("courseId"), 10, 64)
if err != nil {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Invalid course ID",
Error: err.Error(),
})
}
items, err := h.courseMgmtSvc.GetSubCoursesWithProgress(c.Context(), targetUserID, courseID)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Failed to get learner course progress",
Error: err.Error(),
})
}
return c.JSON(domain.Response{
Message: "Learner course progress retrieved successfully",
Data: mapSubCourseProgress(items),
})
}