course CRUD fix

This commit is contained in:
Yared Yemane 2026-04-14 04:06:49 -07:00
parent b1a1b97a0a
commit 5858aeb744
2 changed files with 202 additions and 0 deletions

View File

@ -23,6 +23,27 @@ type createCourseCategoryReq struct {
IsActive *bool `json:"is_active"` IsActive *bool `json:"is_active"`
} }
type createCourseReq struct {
CategoryID int64 `json:"category_id"`
Title string `json:"title"`
Description *string `json:"description"`
Thumbnail *string `json:"thumbnail"`
IntroVideoURL *string `json:"intro_video_url"`
IsActive *bool `json:"is_active"`
}
type updateCourseReq struct {
Title *string `json:"title"`
Description *string `json:"description"`
Thumbnail *string `json:"thumbnail"`
IntroVideoURL *string `json:"intro_video_url"`
IsActive *bool `json:"is_active"`
}
type updateCourseThumbnailReq struct {
ThumbnailURL string `json:"thumbnail_url"`
}
type createLevelReq struct { type createLevelReq struct {
CourseID int64 `json:"course_id"` CourseID int64 `json:"course_id"`
CEFRLevel string `json:"cefr_level"` CEFRLevel string `json:"cefr_level"`
@ -178,6 +199,183 @@ func (h *Handler) CreateCourseCategory(c *fiber.Ctx) error {
return c.Status(fiber.StatusCreated).JSON(domain.Response{Message: "Course category created", Data: created}) return c.Status(fiber.StatusCreated).JSON(domain.Response{Message: "Course category created", Data: created})
} }
// CreateCourse godoc
// @Summary Create course
// @Description Legacy-compatible endpoint for creating a course
// @Tags course-management
// @Accept json
// @Produce json
// @Param body body createCourseReq true "Create course payload"
// @Success 201 {object} domain.Response
// @Failure 400 {object} domain.ErrorResponse
// @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/course-management/courses [post]
func (h *Handler) CreateCourse(c *fiber.Ctx) error {
var req createCourseReq
if err := c.BodyParser(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "Invalid request body", Error: err.Error()})
}
if req.CategoryID <= 0 || strings.TrimSpace(req.Title) == "" {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "category_id and title are required"})
}
created, err := h.analyticsDB.CreateCourse(c.Context(), dbgen.CreateCourseParams{
CategoryID: req.CategoryID,
Title: req.Title,
Description: toText(req.Description),
Thumbnail: toText(req.Thumbnail),
IntroVideoUrl: toText(req.IntroVideoURL),
Column6: boolOrNil(req.IsActive),
})
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to create course", Error: err.Error()})
}
return c.Status(fiber.StatusCreated).JSON(domain.Response{Message: "Course created", Data: created})
}
// UpdateCourse godoc
// @Summary Update course
// @Description Legacy-compatible endpoint for updating a course
// @Tags course-management
// @Accept json
// @Produce json
// @Param courseId path int true "Course ID"
// @Param body body updateCourseReq true "Update course payload"
// @Success 200 {object} domain.Response
// @Failure 400 {object} domain.ErrorResponse
// @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/course-management/courses/{courseId} [put]
func (h *Handler) UpdateCourse(c *fiber.Ctx) error {
courseID, err := strconv.ParseInt(c.Params("courseId"), 10, 64)
if err != nil || courseID <= 0 {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "Invalid course ID", Error: "courseId must be a positive integer"})
}
var req updateCourseReq
if err := c.BodyParser(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "Invalid request body", Error: err.Error()})
}
existing, err := h.analyticsDB.GetCourseByID(c.Context(), courseID)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to load course", Error: err.Error()})
}
title := existing.Title
if req.Title != nil {
title = *req.Title
}
description := existing.Description
if req.Description != nil {
description = toText(req.Description)
}
thumbnail := existing.Thumbnail
if req.Thumbnail != nil {
thumbnail = toText(req.Thumbnail)
}
introVideo := existing.IntroVideoUrl
if req.IntroVideoURL != nil {
introVideo = toText(req.IntroVideoURL)
}
isActive := existing.IsActive
if req.IsActive != nil {
isActive = *req.IsActive
}
if err := h.analyticsDB.UpdateCourse(c.Context(), dbgen.UpdateCourseParams{
Title: title,
Description: description,
Thumbnail: thumbnail,
IntroVideoUrl: introVideo,
IsActive: isActive,
ID: courseID,
}); err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to update course", Error: err.Error()})
}
updated, err := h.analyticsDB.GetCourseByID(c.Context(), courseID)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Course updated but failed to fetch latest record", Error: err.Error()})
}
return c.JSON(domain.Response{Message: "Course updated", Data: updated})
}
// DeleteCourse godoc
// @Summary Delete course
// @Description Legacy-compatible endpoint for deleting a course
// @Tags course-management
// @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/course-management/courses/{courseId} [delete]
func (h *Handler) DeleteCourse(c *fiber.Ctx) error {
courseID, err := strconv.ParseInt(c.Params("courseId"), 10, 64)
if err != nil || courseID <= 0 {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "Invalid course ID", Error: "courseId must be a positive integer"})
}
if err := h.analyticsDB.DeleteCourse(c.Context(), courseID); err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to delete course", Error: err.Error()})
}
return c.JSON(domain.Response{Message: "Course deleted"})
}
// UpdateCourseThumbnail godoc
// @Summary Update course thumbnail
// @Description Legacy-compatible endpoint for updating course thumbnail
// @Tags course-management
// @Accept json
// @Produce json
// @Param courseId path int true "Course ID"
// @Param body body updateCourseThumbnailReq true "Update course thumbnail payload"
// @Success 200 {object} domain.Response
// @Failure 400 {object} domain.ErrorResponse
// @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/course-management/courses/{courseId}/thumbnail [post]
func (h *Handler) UpdateCourseThumbnail(c *fiber.Ctx) error {
courseID, err := strconv.ParseInt(c.Params("courseId"), 10, 64)
if err != nil || courseID <= 0 {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "Invalid course ID", Error: "courseId must be a positive integer"})
}
var req updateCourseThumbnailReq
if err := c.BodyParser(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "Invalid request body", Error: err.Error()})
}
existing, err := h.analyticsDB.GetCourseByID(c.Context(), courseID)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to load course", Error: err.Error()})
}
thumb := req.ThumbnailURL
if strings.TrimSpace(thumb) == "" {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "thumbnail_url is required"})
}
if err := h.analyticsDB.UpdateCourse(c.Context(), dbgen.UpdateCourseParams{
Title: existing.Title,
Description: existing.Description,
Thumbnail: pgtype.Text{String: thumb, Valid: true},
IntroVideoUrl: existing.IntroVideoUrl,
IsActive: existing.IsActive,
ID: courseID,
}); err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to update course thumbnail", Error: err.Error()})
}
updated, err := h.analyticsDB.GetCourseByID(c.Context(), courseID)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Thumbnail updated but failed to fetch latest record", Error: err.Error()})
}
return c.JSON(domain.Response{Message: "Course thumbnail updated", Data: updated})
}
// UnifiedHierarchy godoc // UnifiedHierarchy godoc
// @Summary Get unified course hierarchy // @Summary Get unified course hierarchy
// @Description Returns full hierarchy: category -> sub-category -> course // @Description Returns full hierarchy: category -> sub-category -> course

View File

@ -81,6 +81,10 @@ func (a *App) initAppRoutes() {
// Unified Course Management (single hierarchy model) // Unified Course Management (single hierarchy model)
groupV1.Get("/course-management/categories", a.authMiddleware, a.RequirePermission("learning_tree.get"), h.ListCourseCategories) groupV1.Get("/course-management/categories", a.authMiddleware, a.RequirePermission("learning_tree.get"), h.ListCourseCategories)
groupV1.Post("/course-management/categories", a.authMiddleware, a.RequirePermission("course_categories.create"), h.CreateCourseCategory) groupV1.Post("/course-management/categories", a.authMiddleware, a.RequirePermission("course_categories.create"), h.CreateCourseCategory)
groupV1.Post("/course-management/courses", a.authMiddleware, a.RequirePermission("courses.create"), h.CreateCourse)
groupV1.Put("/course-management/courses/:courseId", a.authMiddleware, a.RequirePermission("courses.update"), h.UpdateCourse)
groupV1.Delete("/course-management/courses/:courseId", a.authMiddleware, a.RequirePermission("courses.delete"), h.DeleteCourse)
groupV1.Post("/course-management/courses/:courseId/thumbnail", a.authMiddleware, a.RequirePermission("courses.upload_thumbnail"), h.UpdateCourseThumbnail)
groupV1.Get("/course-management/hierarchy", a.authMiddleware, a.RequirePermission("learning_tree.get"), h.UnifiedHierarchy) groupV1.Get("/course-management/hierarchy", a.authMiddleware, a.RequirePermission("learning_tree.get"), h.UnifiedHierarchy)
groupV1.Get("/course-management/courses/:courseId/hierarchy", a.authMiddleware, a.RequirePermission("learning_tree.get"), h.UnifiedHierarchyByCourse) groupV1.Get("/course-management/courses/:courseId/hierarchy", a.authMiddleware, a.RequirePermission("learning_tree.get"), h.UnifiedHierarchyByCourse)
groupV1.Post("/course-management/sub-categories", a.authMiddleware, a.RequirePermission("course_categories.create"), h.CreateCourseSubCategory) groupV1.Post("/course-management/sub-categories", a.authMiddleware, a.RequirePermission("course_categories.create"), h.CreateCourseSubCategory)