542 lines
18 KiB
Go
542 lines
18 KiB
Go
package handlers
|
|
|
|
import (
|
|
"Yimaru-Backend/internal/domain"
|
|
"errors"
|
|
"math"
|
|
"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"`
|
|
}
|
|
|
|
type courseProgressSummaryRes struct {
|
|
CourseID int64 `json:"course_id"`
|
|
LearnerUserID int64 `json:"learner_user_id"`
|
|
OverallProgressPercentage int16 `json:"overall_progress_percentage"`
|
|
TotalSubCourses int32 `json:"total_sub_courses"`
|
|
CompletedSubCourses int32 `json:"completed_sub_courses"`
|
|
InProgressSubCourses int32 `json:"in_progress_sub_courses"`
|
|
NotStartedSubCourses int32 `json:"not_started_sub_courses"`
|
|
LockedSubCourses int32 `json:"locked_sub_courses"`
|
|
}
|
|
|
|
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),
|
|
})
|
|
}
|
|
|
|
// GetUserCourseProgressSummaryForAdmin godoc
|
|
// @Summary Get learner's course progress summary (admin)
|
|
// @Description Returns course-level aggregated progress metrics for a target learner
|
|
// @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}/summary [get]
|
|
func (h *Handler) GetUserCourseProgressSummaryForAdmin(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 summary",
|
|
Error: err.Error(),
|
|
})
|
|
}
|
|
|
|
var (
|
|
completedCount int32
|
|
inProgressCount int32
|
|
notStartedCount int32
|
|
lockedCount int32
|
|
sumPercentage int64
|
|
)
|
|
|
|
for _, item := range items {
|
|
sumPercentage += int64(item.ProgressPercentage)
|
|
switch item.ProgressStatus {
|
|
case domain.ProgressStatusCompleted:
|
|
completedCount++
|
|
case domain.ProgressStatusInProgress:
|
|
inProgressCount++
|
|
default:
|
|
notStartedCount++
|
|
}
|
|
if item.IsLocked {
|
|
lockedCount++
|
|
}
|
|
}
|
|
|
|
totalSubCourses := int32(len(items))
|
|
overall := int16(0)
|
|
if totalSubCourses > 0 {
|
|
overall = int16(math.Round(float64(sumPercentage) / float64(totalSubCourses)))
|
|
}
|
|
|
|
return c.JSON(domain.Response{
|
|
Message: "Learner course progress summary retrieved successfully",
|
|
Data: courseProgressSummaryRes{
|
|
CourseID: courseID,
|
|
LearnerUserID: targetUserID,
|
|
OverallProgressPercentage: overall,
|
|
TotalSubCourses: totalSubCourses,
|
|
CompletedSubCourses: completedCount,
|
|
InProgressSubCourses: inProgressCount,
|
|
NotStartedSubCourses: notStartedCount,
|
|
LockedSubCourses: lockedCount,
|
|
},
|
|
})
|
|
}
|