diff --git a/internal/web_server/handlers/hierarchy_handler.go b/internal/web_server/handlers/hierarchy_handler.go index 75de9e6..794d6f0 100644 --- a/internal/web_server/handlers/hierarchy_handler.go +++ b/internal/web_server/handlers/hierarchy_handler.go @@ -1,6 +1,7 @@ package handlers import ( + "context" dbgen "Yimaru-Backend/gen/db" "Yimaru-Backend/internal/domain" "errors" @@ -2244,9 +2245,62 @@ func (h *Handler) GetSubModulePractices(c *fiber.Ctx) error { }) } +func practiceRowFromSubModuleQuestionSet(qs domain.QuestionSet) dbgen.GetSubModulePracticeByIDRow { + row := dbgen.GetSubModulePracticeByIDRow{ + ID: 0, + Title: qs.Title, + QuestionSetID: qs.ID, + DisplayOrder: 0, + Status: qs.Status, + SetType: qs.SetType, + IsActive: !strings.EqualFold(qs.Status, "ARCHIVED"), + } + if qs.OwnerID != nil { + row.SubModuleID = *qs.OwnerID + } + if qs.Description != nil { + row.Description = pgtype.Text{String: *qs.Description, Valid: true} + } + if qs.IntroVideoURL != nil { + row.IntroVideoUrl = pgtype.Text{String: *qs.IntroVideoURL, Valid: true} + } + return row +} + +// resolveCourseManagementPractice loads an active sub_module_practices row, or a SUB_MODULE-owned PRACTICE +// question_set with no bridge row (id is always question_sets.id in that case). +func (h *Handler) resolveCourseManagementPractice(ctx context.Context, id int64) (dbgen.GetSubModulePracticeByIDRow, error) { + row, err := h.analyticsDB.GetSubModulePracticeByID(ctx, id) + if err == nil { + return row, nil + } + if !errors.Is(err, pgx.ErrNoRows) { + return dbgen.GetSubModulePracticeByIDRow{}, err + } + qs, err := h.questionsSvc.GetQuestionSetByID(ctx, id) + if err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return dbgen.GetSubModulePracticeByIDRow{}, pgx.ErrNoRows + } + return dbgen.GetSubModulePracticeByIDRow{}, err + } + if !strings.EqualFold(qs.SetType, string(domain.QuestionSetTypePractice)) { + return dbgen.GetSubModulePracticeByIDRow{}, pgx.ErrNoRows + } + if qs.OwnerType == nil || !strings.EqualFold(*qs.OwnerType, "SUB_MODULE") || qs.OwnerID == nil { + return dbgen.GetSubModulePracticeByIDRow{}, pgx.ErrNoRows + } + out := practiceRowFromSubModuleQuestionSet(qs) + _, total, err := h.questionsSvc.GetQuestionSetItemsPaginated(ctx, qs.ID, nil, 1, 0) + if err == nil { + out.QuestionCount = total + } + return out, nil +} + // GetSubModulePracticeByID godoc // @Summary Get practice detail -// @Description Returns one active practice. practiceId may be sub_module_practices.id or the linked question_sets.id. +// @Description Returns one practice. practiceId may be sub_module_practices.id, question_sets.id, or (if no bridge row) a SUB_MODULE PRACTICE question set id. // @Tags course-management // @Accept json // @Produce json @@ -2265,10 +2319,16 @@ func (h *Handler) GetSubModulePracticeByID(c *fiber.Ctx) error { }) } - practice, err := h.analyticsDB.GetSubModulePracticeByID(c.Context(), practiceID) + practice, err := h.resolveCourseManagementPractice(c.Context(), practiceID) if err != nil { - return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{ - Message: "Practice not found", + if errors.Is(err, pgx.ErrNoRows) { + return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{ + Message: "Practice not found", + Error: err.Error(), + }) + } + return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ + Message: "Failed to load practice", Error: err.Error(), }) } @@ -2283,7 +2343,7 @@ func (h *Handler) GetSubModulePracticeByID(c *fiber.Ctx) error { // GetSubModulePracticeDetail godoc // @Summary Get practice with full question list -// @Description Returns one active practice with question-set fields and the ordered question list (full item detail). practiceId may be sub_module_practices.id or the linked question_sets.id. +// @Description Returns practice metadata and ordered questions. practiceId may be sub_module_practices.id, linked question_sets.id, or a SUB_MODULE PRACTICE set id when no sub_module_practices row exists. // @Tags course-management // @Produce json // @Param practiceId path int true "Practice row id or question set id" @@ -2300,16 +2360,23 @@ func (h *Handler) GetSubModulePracticeDetail(c *fiber.Ctx) error { Error: "practiceId must be a positive integer", }) } - practice, err := h.analyticsDB.GetSubModulePracticeByID(c.Context(), practiceID) + practice, err := h.resolveCourseManagementPractice(c.Context(), practiceID) if err != nil { - return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{ - Message: "Practice not found", + if errors.Is(err, pgx.ErrNoRows) { + return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{ + Message: "Practice not found", + Error: err.Error(), + }) + } + return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ + Message: "Failed to load practice", Error: err.Error(), }) } const pageSize int32 = 500 var allItems []domain.QuestionSetItemWithQuestion var offset int32 + var totalCount int64 for { batch, total, err := h.questionsSvc.GetQuestionSetItemsPaginated(c.Context(), practice.QuestionSetID, nil, pageSize, offset) if err != nil { @@ -2318,12 +2385,14 @@ func (h *Handler) GetSubModulePracticeDetail(c *fiber.Ctx) error { Error: err.Error(), }) } + totalCount = total allItems = append(allItems, batch...) if int64(len(allItems)) >= total || len(batch) == 0 { break } offset += pageSize } + practice.QuestionCount = totalCount return c.JSON(domain.Response{ Message: "Practice retrieved successfully", Success: true,