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"` } // --- 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(), }) } var res []subCourseProgressRes 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 c.JSON(domain.Response{ Message: "Course progress retrieved successfully", Data: res, }) }