Expose GET /course-management/categories/:categoryId/courses so legacy tab/API callers no longer fail with Cannot GET during initial content load. Made-with: Cursor
1032 lines
39 KiB
Go
1032 lines
39 KiB
Go
package handlers
|
|
|
|
import (
|
|
dbgen "Yimaru-Backend/gen/db"
|
|
"Yimaru-Backend/internal/domain"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/gofiber/fiber/v2"
|
|
"github.com/jackc/pgx/v5/pgtype"
|
|
)
|
|
|
|
type createCourseSubCategoryReq struct {
|
|
CategoryID int64 `json:"category_id"`
|
|
Name string `json:"name"`
|
|
Description *string `json:"description"`
|
|
DisplayOrder *int32 `json:"display_order"`
|
|
IsActive *bool `json:"is_active"`
|
|
}
|
|
|
|
type createCourseCategoryReq struct {
|
|
Name string `json:"name"`
|
|
IsActive *bool `json:"is_active"`
|
|
}
|
|
|
|
type createCourseReq struct {
|
|
CategoryID int64 `json:"category_id"`
|
|
SubCategoryID *int64 `json:"sub_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 {
|
|
CourseID int64 `json:"course_id"`
|
|
CEFRLevel string `json:"cefr_level"`
|
|
DisplayOrder *int32 `json:"display_order"`
|
|
IsActive *bool `json:"is_active"`
|
|
}
|
|
|
|
type createModuleReq struct {
|
|
LevelID int64 `json:"level_id"`
|
|
Title string `json:"title"`
|
|
Description *string `json:"description"`
|
|
DisplayOrder *int32 `json:"display_order"`
|
|
IsActive *bool `json:"is_active"`
|
|
}
|
|
|
|
type createSubModuleReq struct {
|
|
ModuleID int64 `json:"module_id"`
|
|
Title string `json:"title"`
|
|
Description *string `json:"description"`
|
|
DisplayOrder *int32 `json:"display_order"`
|
|
IsActive *bool `json:"is_active"`
|
|
}
|
|
|
|
type createSubModuleVideoReq struct {
|
|
SubModuleID int64 `json:"sub_module_id"`
|
|
Title string `json:"title"`
|
|
Description *string `json:"description"`
|
|
VideoURL string `json:"video_url"`
|
|
Duration *int32 `json:"duration"`
|
|
Resolution *string `json:"resolution"`
|
|
Visibility *string `json:"visibility"`
|
|
InstructorID *string `json:"instructor_id"`
|
|
Thumbnail *string `json:"thumbnail"`
|
|
DisplayOrder *int32 `json:"display_order"`
|
|
Status *string `json:"status"`
|
|
}
|
|
|
|
type updateSubModuleReq struct {
|
|
Title *string `json:"title"`
|
|
Description *string `json:"description"`
|
|
IsActive *bool `json:"is_active"`
|
|
}
|
|
|
|
type updateSubModuleVideoReq struct {
|
|
Title *string `json:"title"`
|
|
Description *string `json:"description"`
|
|
VideoURL *string `json:"video_url"`
|
|
}
|
|
|
|
type updatePracticeReq struct {
|
|
Title *string `json:"title"`
|
|
Description *string `json:"description"`
|
|
Persona *string `json:"persona"`
|
|
IsActive *bool `json:"is_active"`
|
|
}
|
|
|
|
type attachSubModuleLessonReq struct {
|
|
SubModuleID int64 `json:"sub_module_id"`
|
|
QuestionSetID int64 `json:"question_set_id"`
|
|
IntroVideoURL *string `json:"intro_video_url"`
|
|
DisplayOrder *int32 `json:"display_order"`
|
|
IsActive *bool `json:"is_active"`
|
|
}
|
|
|
|
type createSubModulePracticeReq struct {
|
|
SubModuleID int64 `json:"sub_module_id"`
|
|
Title string `json:"title"`
|
|
Description *string `json:"description"`
|
|
Thumbnail *string `json:"thumbnail"`
|
|
IntroVideoURL *string `json:"intro_video_url"`
|
|
QuestionSetID int64 `json:"question_set_id"`
|
|
DisplayOrder *int32 `json:"display_order"`
|
|
IsActive *bool `json:"is_active"`
|
|
}
|
|
|
|
type legacyHierarchyRow struct {
|
|
CategoryID int64 `json:"category_id"`
|
|
CategoryName string `json:"category_name"`
|
|
SubCategoryID *int64 `json:"sub_category_id"`
|
|
SubCategoryName *string `json:"sub_category_name"`
|
|
CourseID *int64 `json:"course_id"`
|
|
CourseTitle *string `json:"course_title"`
|
|
}
|
|
|
|
func toText(v *string) pgtype.Text {
|
|
if v == nil {
|
|
return pgtype.Text{Valid: false}
|
|
}
|
|
return pgtype.Text{String: *v, Valid: true}
|
|
}
|
|
|
|
func toInt4(v *int32) pgtype.Int4 {
|
|
if v == nil {
|
|
return pgtype.Int4{Valid: false}
|
|
}
|
|
return pgtype.Int4{Int32: *v, Valid: true}
|
|
}
|
|
|
|
func boolOrNil(v *bool) interface{} {
|
|
if v == nil {
|
|
return nil
|
|
}
|
|
return *v
|
|
}
|
|
|
|
func intOrNil(v *int32) interface{} {
|
|
if v == nil {
|
|
return nil
|
|
}
|
|
return *v
|
|
}
|
|
|
|
// ListCourseCategories godoc
|
|
// @Summary List course categories
|
|
// @Description Legacy-compatible endpoint for listing course categories
|
|
// @Tags course-management
|
|
// @Produce json
|
|
// @Success 200 {object} domain.Response
|
|
// @Failure 500 {object} domain.ErrorResponse
|
|
// @Router /api/v1/course-management/categories [get]
|
|
func (h *Handler) ListCourseCategories(c *fiber.Ctx) error {
|
|
rows, err := h.analyticsDB.GetAllCourseCategories(c.Context(), dbgen.GetAllCourseCategoriesParams{
|
|
Offset: pgtype.Int4{Int32: 0, Valid: true},
|
|
Limit: pgtype.Int4{Int32: 10000, Valid: true},
|
|
})
|
|
if err != nil {
|
|
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to load categories", Error: err.Error()})
|
|
}
|
|
|
|
total := 0
|
|
if len(rows) > 0 {
|
|
total = int(rows[0].TotalCount)
|
|
}
|
|
|
|
return c.JSON(domain.Response{
|
|
Message: "Categories retrieved successfully",
|
|
Data: map[string]interface{}{
|
|
"categories": rows,
|
|
"total_count": total,
|
|
},
|
|
})
|
|
}
|
|
|
|
// ListCoursesByCategory godoc
|
|
// @Summary List courses by category
|
|
// @Description Legacy-compatible endpoint that returns courses for one category
|
|
// @Tags course-management
|
|
// @Produce json
|
|
// @Param categoryId path int true "Category ID"
|
|
// @Param offset query int false "Offset"
|
|
// @Param limit query int false "Limit"
|
|
// @Success 200 {object} domain.Response
|
|
// @Failure 400 {object} domain.ErrorResponse
|
|
// @Failure 500 {object} domain.ErrorResponse
|
|
// @Router /api/v1/course-management/categories/{categoryId}/courses [get]
|
|
func (h *Handler) ListCoursesByCategory(c *fiber.Ctx) error {
|
|
categoryID, err := strconv.ParseInt(c.Params("categoryId"), 10, 64)
|
|
if err != nil || categoryID <= 0 {
|
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "Invalid category ID", Error: "categoryId must be a positive integer"})
|
|
}
|
|
|
|
offset := int32(c.QueryInt("offset", 0))
|
|
if offset < 0 {
|
|
offset = 0
|
|
}
|
|
limit := int32(c.QueryInt("limit", 10000))
|
|
if limit <= 0 {
|
|
limit = 10000
|
|
}
|
|
|
|
rows, err := h.analyticsDB.GetCoursesByCategory(c.Context(), dbgen.GetCoursesByCategoryParams{
|
|
CategoryID: categoryID,
|
|
Offset: pgtype.Int4{Int32: offset, Valid: true},
|
|
Limit: pgtype.Int4{Int32: limit, Valid: true},
|
|
})
|
|
if err != nil {
|
|
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to load courses", Error: err.Error()})
|
|
}
|
|
|
|
total := 0
|
|
if len(rows) > 0 {
|
|
total = int(rows[0].TotalCount)
|
|
}
|
|
|
|
return c.JSON(domain.Response{
|
|
Message: "Courses retrieved successfully",
|
|
Data: map[string]interface{}{
|
|
"courses": rows,
|
|
"total_count": total,
|
|
},
|
|
})
|
|
}
|
|
|
|
// CreateCourseCategory godoc
|
|
// @Summary Create course category
|
|
// @Description Legacy-compatible endpoint for creating a course category
|
|
// @Tags course-management
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Param body body createCourseCategoryReq true "Create category payload"
|
|
// @Success 201 {object} domain.Response
|
|
// @Failure 400 {object} domain.ErrorResponse
|
|
// @Failure 500 {object} domain.ErrorResponse
|
|
// @Router /api/v1/course-management/categories [post]
|
|
func (h *Handler) CreateCourseCategory(c *fiber.Ctx) error {
|
|
var req createCourseCategoryReq
|
|
if err := c.BodyParser(&req); err != nil {
|
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "Invalid request body", Error: err.Error()})
|
|
}
|
|
if strings.TrimSpace(req.Name) == "" {
|
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "name is required"})
|
|
}
|
|
|
|
created, err := h.analyticsDB.CreateCourseCategory(c.Context(), dbgen.CreateCourseCategoryParams{
|
|
Name: req.Name,
|
|
Column2: boolOrNil(req.IsActive),
|
|
})
|
|
if err != nil {
|
|
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to create category", Error: err.Error()})
|
|
}
|
|
|
|
return c.Status(fiber.StatusCreated).JSON(domain.Response{Message: "Course category created", Data: created})
|
|
}
|
|
|
|
func (h *Handler) DeleteCourseCategory(c *fiber.Ctx) error {
|
|
categoryID, err := strconv.ParseInt(c.Params("categoryId"), 10, 64)
|
|
if err != nil || categoryID <= 0 {
|
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "Invalid category ID", Error: "categoryId must be a positive integer"})
|
|
}
|
|
|
|
if err := h.analyticsDB.DeleteCourseCategoryCompat(c.Context(), categoryID); err != nil {
|
|
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to delete category", Error: err.Error()})
|
|
}
|
|
|
|
return c.JSON(domain.Response{Message: "Course category deleted"})
|
|
}
|
|
|
|
// 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"})
|
|
}
|
|
|
|
isActive := true
|
|
if req.IsActive != nil {
|
|
isActive = *req.IsActive
|
|
}
|
|
description := ""
|
|
if req.Description != nil {
|
|
description = *req.Description
|
|
}
|
|
thumbnail := ""
|
|
if req.Thumbnail != nil {
|
|
thumbnail = *req.Thumbnail
|
|
}
|
|
introVideoURL := ""
|
|
if req.IntroVideoURL != nil {
|
|
introVideoURL = *req.IntroVideoURL
|
|
}
|
|
|
|
created, err := h.analyticsDB.CreateCourseCompat(
|
|
c.Context(),
|
|
req.CategoryID,
|
|
req.SubCategoryID,
|
|
req.Title,
|
|
description,
|
|
thumbnail,
|
|
introVideoURL,
|
|
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
|
|
// @Summary Get unified course hierarchy
|
|
// @Description Returns full hierarchy: category -> sub-category -> course
|
|
// @Tags course-management
|
|
// @Produce json
|
|
// @Success 200 {object} domain.Response
|
|
// @Failure 500 {object} domain.ErrorResponse
|
|
// @Router /api/v1/course-management/hierarchy [get]
|
|
func (h *Handler) UnifiedHierarchy(c *fiber.Ctx) error {
|
|
rows, err := h.analyticsDB.GetCoursesWithHierarchy(c.Context())
|
|
if err != nil {
|
|
if isMissingCourseSubCategoryTableErr(err) {
|
|
legacyRows, legacyErr := h.buildLegacyHierarchyRows(c)
|
|
if legacyErr != nil {
|
|
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to load hierarchy", Error: legacyErr.Error()})
|
|
}
|
|
return c.JSON(domain.Response{Message: "Unified hierarchy retrieved successfully", Data: legacyRows})
|
|
}
|
|
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to load hierarchy", Error: err.Error()})
|
|
}
|
|
return c.JSON(domain.Response{Message: "Unified hierarchy retrieved successfully", Data: rows})
|
|
}
|
|
|
|
// UnifiedHierarchyByCourse godoc
|
|
// @Summary Get hierarchy for a course
|
|
// @Description Returns hierarchy nodes for one course including levels/modules/sub-modules
|
|
// @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}/hierarchy [get]
|
|
func (h *Handler) UnifiedHierarchyByCourse(c *fiber.Ctx) 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()})
|
|
}
|
|
rows, err := h.analyticsDB.GetFullHierarchyByCourseID(c.Context(), courseID)
|
|
if err != nil {
|
|
if isMissingCourseSubCategoryTableErr(err) {
|
|
course, getCourseErr := h.analyticsDB.GetCourseByID(c.Context(), courseID)
|
|
if getCourseErr != nil {
|
|
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to load course hierarchy", Error: getCourseErr.Error()})
|
|
}
|
|
return c.JSON(domain.Response{
|
|
Message: "Course hierarchy retrieved successfully",
|
|
Data: []map[string]interface{}{
|
|
{
|
|
"course_id": course.ID,
|
|
"course_title": course.Title,
|
|
"level_id": nil,
|
|
"cefr_level": nil,
|
|
"module_id": nil,
|
|
"module_title": nil,
|
|
"sub_module_id": nil,
|
|
"sub_module_title": nil,
|
|
},
|
|
},
|
|
})
|
|
}
|
|
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to load course hierarchy", Error: err.Error()})
|
|
}
|
|
return c.JSON(domain.Response{Message: "Course hierarchy retrieved successfully", Data: rows})
|
|
}
|
|
|
|
func isMissingCourseSubCategoryTableErr(err error) bool {
|
|
if err == nil {
|
|
return false
|
|
}
|
|
return strings.Contains(strings.ToLower(err.Error()), "relation \"course_sub_categories\" does not exist")
|
|
}
|
|
|
|
func (h *Handler) buildLegacyHierarchyRows(c *fiber.Ctx) ([]legacyHierarchyRow, error) {
|
|
categories, err := h.analyticsDB.GetAllCourseCategories(c.Context(), dbgen.GetAllCourseCategoriesParams{
|
|
Offset: pgtype.Int4{Int32: 0, Valid: true},
|
|
Limit: pgtype.Int4{Int32: 10000, Valid: true},
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
out := make([]legacyHierarchyRow, 0, len(categories))
|
|
for _, cat := range categories {
|
|
courses, courseErr := h.analyticsDB.GetCoursesByCategory(c.Context(), dbgen.GetCoursesByCategoryParams{
|
|
CategoryID: cat.ID,
|
|
Offset: pgtype.Int4{Int32: 0, Valid: true},
|
|
Limit: pgtype.Int4{Int32: 10000, Valid: true},
|
|
})
|
|
if courseErr != nil {
|
|
return nil, courseErr
|
|
}
|
|
|
|
if len(courses) == 0 {
|
|
out = append(out, legacyHierarchyRow{
|
|
CategoryID: cat.ID,
|
|
CategoryName: cat.Name,
|
|
SubCategoryID: nil,
|
|
SubCategoryName: nil,
|
|
CourseID: nil,
|
|
CourseTitle: nil,
|
|
})
|
|
continue
|
|
}
|
|
|
|
for _, course := range courses {
|
|
courseID := course.ID
|
|
courseTitle := course.Title
|
|
out = append(out, legacyHierarchyRow{
|
|
CategoryID: cat.ID,
|
|
CategoryName: cat.Name,
|
|
SubCategoryID: nil,
|
|
SubCategoryName: nil,
|
|
CourseID: &courseID,
|
|
CourseTitle: &courseTitle,
|
|
})
|
|
}
|
|
}
|
|
|
|
return out, nil
|
|
}
|
|
|
|
// CreateCourseSubCategory godoc
|
|
// @Summary Create course sub-category
|
|
// @Description Creates a sub-category under a course category
|
|
// @Tags course-management
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Param body body createCourseSubCategoryReq true "Create sub-category payload"
|
|
// @Success 201 {object} domain.Response
|
|
// @Failure 400 {object} domain.ErrorResponse
|
|
// @Failure 500 {object} domain.ErrorResponse
|
|
// @Router /api/v1/course-management/sub-categories [post]
|
|
func (h *Handler) CreateCourseSubCategory(c *fiber.Ctx) error {
|
|
var req createCourseSubCategoryReq
|
|
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.Name) == "" {
|
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "category_id and name are required"})
|
|
}
|
|
created, err := h.analyticsDB.CreateCourseSubCategory(c.Context(), dbgen.CreateCourseSubCategoryParams{
|
|
CategoryID: req.CategoryID,
|
|
Name: req.Name,
|
|
Description: toText(req.Description),
|
|
Column4: intOrNil(req.DisplayOrder),
|
|
Column5: boolOrNil(req.IsActive),
|
|
})
|
|
if err != nil {
|
|
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to create sub-category", Error: err.Error()})
|
|
}
|
|
return c.Status(fiber.StatusCreated).JSON(domain.Response{Message: "Course sub-category created", Data: created})
|
|
}
|
|
|
|
func (h *Handler) DeleteCourseSubCategory(c *fiber.Ctx) error {
|
|
subCategoryID, err := strconv.ParseInt(c.Params("subCategoryId"), 10, 64)
|
|
if err != nil || subCategoryID <= 0 {
|
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "Invalid sub-category ID", Error: "subCategoryId must be a positive integer"})
|
|
}
|
|
|
|
if err := h.analyticsDB.DeleteCourseSubCategoryCompat(c.Context(), subCategoryID); err != nil {
|
|
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to delete sub-category", Error: err.Error()})
|
|
}
|
|
|
|
return c.JSON(domain.Response{Message: "Course sub-category deleted"})
|
|
}
|
|
|
|
// CreateLevel godoc
|
|
// @Summary Create level
|
|
// @Description Creates a CEFR level under a course
|
|
// @Tags course-management
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Param body body createLevelReq true "Create level payload"
|
|
// @Success 201 {object} domain.Response
|
|
// @Failure 400 {object} domain.ErrorResponse
|
|
// @Failure 500 {object} domain.ErrorResponse
|
|
// @Router /api/v1/course-management/levels [post]
|
|
func (h *Handler) CreateLevel(c *fiber.Ctx) error {
|
|
var req createLevelReq
|
|
if err := c.BodyParser(&req); err != nil {
|
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "Invalid request body", Error: err.Error()})
|
|
}
|
|
req.CEFRLevel = strings.ToUpper(strings.TrimSpace(req.CEFRLevel))
|
|
validCEFR := map[string]bool{"A1": true, "A2": true, "A3": true, "B1": true, "B2": true, "B3": true, "C1": true, "C2": true, "C3": true}
|
|
if req.CourseID <= 0 || !validCEFR[req.CEFRLevel] {
|
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "course_id and valid cefr_level are required"})
|
|
}
|
|
created, err := h.analyticsDB.CreateLevel(c.Context(), dbgen.CreateLevelParams{
|
|
CourseID: req.CourseID,
|
|
CefrLevel: req.CEFRLevel,
|
|
Column3: intOrNil(req.DisplayOrder),
|
|
Column4: boolOrNil(req.IsActive),
|
|
})
|
|
if err != nil {
|
|
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to create level", Error: err.Error()})
|
|
}
|
|
return c.Status(fiber.StatusCreated).JSON(domain.Response{Message: "Level created", Data: created})
|
|
}
|
|
|
|
// CreateModule godoc
|
|
// @Summary Create module
|
|
// @Description Creates a module under a level
|
|
// @Tags course-management
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Param body body createModuleReq true "Create module payload"
|
|
// @Success 201 {object} domain.Response
|
|
// @Failure 400 {object} domain.ErrorResponse
|
|
// @Failure 500 {object} domain.ErrorResponse
|
|
// @Router /api/v1/course-management/modules [post]
|
|
func (h *Handler) CreateModule(c *fiber.Ctx) error {
|
|
var req createModuleReq
|
|
if err := c.BodyParser(&req); err != nil {
|
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "Invalid request body", Error: err.Error()})
|
|
}
|
|
if req.LevelID <= 0 || strings.TrimSpace(req.Title) == "" {
|
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "level_id and title are required"})
|
|
}
|
|
created, err := h.analyticsDB.CreateModule(c.Context(), dbgen.CreateModuleParams{
|
|
LevelID: req.LevelID,
|
|
Title: req.Title,
|
|
Description: toText(req.Description),
|
|
Column4: intOrNil(req.DisplayOrder),
|
|
Column5: boolOrNil(req.IsActive),
|
|
})
|
|
if err != nil {
|
|
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to create module", Error: err.Error()})
|
|
}
|
|
return c.Status(fiber.StatusCreated).JSON(domain.Response{Message: "Module created", Data: created})
|
|
}
|
|
|
|
// CreateSubModule godoc
|
|
// @Summary Create sub-module
|
|
// @Description Creates a sub-module under a module
|
|
// @Tags course-management
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Param body body createSubModuleReq true "Create sub-module payload"
|
|
// @Success 201 {object} domain.Response
|
|
// @Failure 400 {object} domain.ErrorResponse
|
|
// @Failure 500 {object} domain.ErrorResponse
|
|
// @Router /api/v1/course-management/sub-modules [post]
|
|
func (h *Handler) CreateSubModule(c *fiber.Ctx) error {
|
|
var req createSubModuleReq
|
|
if err := c.BodyParser(&req); err != nil {
|
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "Invalid request body", Error: err.Error()})
|
|
}
|
|
if req.ModuleID <= 0 || strings.TrimSpace(req.Title) == "" {
|
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "module_id and title are required"})
|
|
}
|
|
created, err := h.analyticsDB.CreateSubModule(c.Context(), dbgen.CreateSubModuleParams{
|
|
ModuleID: req.ModuleID,
|
|
Title: req.Title,
|
|
Description: toText(req.Description),
|
|
Column4: intOrNil(req.DisplayOrder),
|
|
Column5: boolOrNil(req.IsActive),
|
|
})
|
|
if err != nil {
|
|
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to create sub-module", Error: err.Error()})
|
|
}
|
|
return c.Status(fiber.StatusCreated).JSON(domain.Response{Message: "Sub-module created", Data: created})
|
|
}
|
|
|
|
// CreateSubModuleVideo godoc
|
|
// @Summary Create sub-module video
|
|
// @Description Creates a video under a sub-module
|
|
// @Tags course-management
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Param body body createSubModuleVideoReq true "Create sub-module video payload"
|
|
// @Success 201 {object} domain.Response
|
|
// @Failure 400 {object} domain.ErrorResponse
|
|
// @Failure 500 {object} domain.ErrorResponse
|
|
// @Router /api/v1/course-management/sub-module-videos [post]
|
|
func (h *Handler) CreateSubModuleVideo(c *fiber.Ctx) error {
|
|
var req createSubModuleVideoReq
|
|
if err := c.BodyParser(&req); err != nil {
|
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "Invalid request body", Error: err.Error()})
|
|
}
|
|
if req.SubModuleID <= 0 || strings.TrimSpace(req.Title) == "" || strings.TrimSpace(req.VideoURL) == "" {
|
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "sub_module_id, title and video_url are required"})
|
|
}
|
|
created, err := h.analyticsDB.CreateSubModuleVideo(c.Context(), dbgen.CreateSubModuleVideoParams{
|
|
SubModuleID: req.SubModuleID,
|
|
Title: req.Title,
|
|
Description: toText(req.Description),
|
|
VideoUrl: req.VideoURL,
|
|
Duration: toInt4(req.Duration),
|
|
Resolution: toText(req.Resolution),
|
|
Column7: nil,
|
|
Visibility: toText(req.Visibility),
|
|
InstructorID: toText(req.InstructorID),
|
|
Thumbnail: toText(req.Thumbnail),
|
|
Column12: intOrNil(req.DisplayOrder),
|
|
Column13: req.Status,
|
|
VimeoID: pgtype.Text{Valid: false},
|
|
VimeoEmbedUrl: pgtype.Text{Valid: false},
|
|
VimeoPlayerHtml: pgtype.Text{Valid: false},
|
|
VimeoStatus: pgtype.Text{Valid: false},
|
|
Column18: nil,
|
|
})
|
|
if err != nil {
|
|
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to create sub-module video", Error: err.Error()})
|
|
}
|
|
return c.Status(fiber.StatusCreated).JSON(domain.Response{Message: "Sub-module video created", Data: created})
|
|
}
|
|
|
|
// AttachSubModuleLesson godoc
|
|
// @Summary Attach lesson to sub-module
|
|
// @Description Links a question set lesson to a sub-module
|
|
// @Tags course-management
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Param body body attachSubModuleLessonReq true "Attach lesson payload"
|
|
// @Success 201 {object} domain.Response
|
|
// @Failure 400 {object} domain.ErrorResponse
|
|
// @Failure 500 {object} domain.ErrorResponse
|
|
// @Router /api/v1/course-management/sub-module-lessons [post]
|
|
func (h *Handler) AttachSubModuleLesson(c *fiber.Ctx) error {
|
|
var req attachSubModuleLessonReq
|
|
if err := c.BodyParser(&req); err != nil {
|
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "Invalid request body", Error: err.Error()})
|
|
}
|
|
if req.SubModuleID <= 0 || req.QuestionSetID <= 0 {
|
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "sub_module_id and question_set_id are required"})
|
|
}
|
|
attached, err := h.analyticsDB.AttachQuestionSetLessonToSubModule(c.Context(), dbgen.AttachQuestionSetLessonToSubModuleParams{
|
|
SubModuleID: req.SubModuleID,
|
|
QuestionSetID: req.QuestionSetID,
|
|
IntroVideoUrl: toText(req.IntroVideoURL),
|
|
Column4: intOrNil(req.DisplayOrder),
|
|
Column5: boolOrNil(req.IsActive),
|
|
})
|
|
if err != nil {
|
|
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to attach lesson", Error: err.Error()})
|
|
}
|
|
return c.Status(fiber.StatusCreated).JSON(domain.Response{Message: "Lesson attached to sub-module", Data: attached})
|
|
}
|
|
|
|
// CreateSubModulePractice godoc
|
|
// @Summary Create practice under sub-module
|
|
// @Description Creates a sub-module practice with metadata and linked question set
|
|
// @Tags course-management
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Param body body createSubModulePracticeReq true "Create practice payload"
|
|
// @Success 201 {object} domain.Response
|
|
// @Failure 400 {object} domain.ErrorResponse
|
|
// @Failure 500 {object} domain.ErrorResponse
|
|
// @Router /api/v1/course-management/sub-module-practices [post]
|
|
func (h *Handler) CreateSubModulePractice(c *fiber.Ctx) error {
|
|
var req createSubModulePracticeReq
|
|
if err := c.BodyParser(&req); err != nil {
|
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "Invalid request body", Error: err.Error()})
|
|
}
|
|
if req.SubModuleID <= 0 || req.QuestionSetID <= 0 || strings.TrimSpace(req.Title) == "" {
|
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "sub_module_id, title and question_set_id are required"})
|
|
}
|
|
created, err := h.analyticsDB.CreateSubModulePractice(c.Context(), dbgen.CreateSubModulePracticeParams{
|
|
SubModuleID: req.SubModuleID,
|
|
Title: req.Title,
|
|
Description: toText(req.Description),
|
|
Thumbnail: toText(req.Thumbnail),
|
|
IntroVideoUrl: toText(req.IntroVideoURL),
|
|
QuestionSetID: req.QuestionSetID,
|
|
Column7: intOrNil(req.DisplayOrder),
|
|
Column8: boolOrNil(req.IsActive),
|
|
})
|
|
if err != nil {
|
|
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to create practice", Error: err.Error()})
|
|
}
|
|
return c.Status(fiber.StatusCreated).JSON(domain.Response{Message: "Practice created", Data: created})
|
|
}
|
|
|
|
func (h *Handler) GetSubModuleVideos(c *fiber.Ctx) error {
|
|
subModuleID, err := strconv.ParseInt(c.Params("subModuleId"), 10, 64)
|
|
if err != nil || subModuleID <= 0 {
|
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "Invalid sub-module ID", Error: "subModuleId must be a positive integer"})
|
|
}
|
|
|
|
videos, err := h.analyticsDB.GetSubModuleVideos(c.Context(), subModuleID)
|
|
if err != nil {
|
|
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to load sub-module videos", Error: err.Error()})
|
|
}
|
|
|
|
return c.JSON(domain.Response{
|
|
Message: "Sub-module videos retrieved successfully",
|
|
Data: map[string]interface{}{
|
|
"videos": videos,
|
|
"total_count": len(videos),
|
|
},
|
|
})
|
|
}
|
|
|
|
func (h *Handler) UpdateSubModule(c *fiber.Ctx) error {
|
|
subModuleID, err := strconv.ParseInt(c.Params("subModuleId"), 10, 64)
|
|
if err != nil || subModuleID <= 0 {
|
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "Invalid sub-module ID", Error: "subModuleId must be a positive integer"})
|
|
}
|
|
|
|
existing, err := h.analyticsDB.GetSubModuleByIDCompat(c.Context(), subModuleID)
|
|
if err != nil {
|
|
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to load sub-module", Error: err.Error()})
|
|
}
|
|
|
|
var req updateSubModuleReq
|
|
if err := c.BodyParser(&req); err != nil {
|
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "Invalid request body", Error: err.Error()})
|
|
}
|
|
|
|
title := existing.Title
|
|
if req.Title != nil {
|
|
title = *req.Title
|
|
}
|
|
description := ""
|
|
if existing.Description.Valid {
|
|
description = existing.Description.String
|
|
}
|
|
if req.Description != nil {
|
|
description = *req.Description
|
|
}
|
|
isActive := existing.IsActive
|
|
if req.IsActive != nil {
|
|
isActive = *req.IsActive
|
|
}
|
|
|
|
if err := h.analyticsDB.UpdateSubModuleCompat(c.Context(), subModuleID, title, description, isActive); err != nil {
|
|
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to update sub-module", Error: err.Error()})
|
|
}
|
|
|
|
updated, err := h.analyticsDB.GetSubModuleByIDCompat(c.Context(), subModuleID)
|
|
if err != nil {
|
|
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Sub-module updated but failed to fetch latest record", Error: err.Error()})
|
|
}
|
|
return c.JSON(domain.Response{Message: "Sub-module updated", Data: updated})
|
|
}
|
|
|
|
func (h *Handler) DeleteSubModule(c *fiber.Ctx) error {
|
|
subModuleID, err := strconv.ParseInt(c.Params("subModuleId"), 10, 64)
|
|
if err != nil || subModuleID <= 0 {
|
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "Invalid sub-module ID", Error: "subModuleId must be a positive integer"})
|
|
}
|
|
|
|
if err := h.analyticsDB.DeleteSubModuleCompat(c.Context(), subModuleID); err != nil {
|
|
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to delete sub-module", Error: err.Error()})
|
|
}
|
|
return c.JSON(domain.Response{Message: "Sub-module deleted"})
|
|
}
|
|
|
|
func (h *Handler) DeleteModule(c *fiber.Ctx) error {
|
|
moduleID, err := strconv.ParseInt(c.Params("moduleId"), 10, 64)
|
|
if err != nil || moduleID <= 0 {
|
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "Invalid module ID", Error: "moduleId must be a positive integer"})
|
|
}
|
|
|
|
if err := h.analyticsDB.DeleteModuleCompat(c.Context(), moduleID); err != nil {
|
|
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to delete module", Error: err.Error()})
|
|
}
|
|
return c.JSON(domain.Response{Message: "Module deleted"})
|
|
}
|
|
|
|
func (h *Handler) UpdateSubModuleVideo(c *fiber.Ctx) error {
|
|
videoID, err := strconv.ParseInt(c.Params("videoId"), 10, 64)
|
|
if err != nil || videoID <= 0 {
|
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "Invalid video ID", Error: "videoId must be a positive integer"})
|
|
}
|
|
|
|
var req updateSubModuleVideoReq
|
|
if err := c.BodyParser(&req); err != nil {
|
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "Invalid request body", Error: err.Error()})
|
|
}
|
|
if req.Title == nil || strings.TrimSpace(*req.Title) == "" || req.VideoURL == nil || strings.TrimSpace(*req.VideoURL) == "" {
|
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "title and video_url are required"})
|
|
}
|
|
|
|
description := ""
|
|
if req.Description != nil {
|
|
description = *req.Description
|
|
}
|
|
|
|
if err := h.analyticsDB.UpdateSubModuleVideoCompat(c.Context(), videoID, *req.Title, description, *req.VideoURL); err != nil {
|
|
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to update sub-module video", Error: err.Error()})
|
|
}
|
|
return c.JSON(domain.Response{Message: "Sub-module video updated"})
|
|
}
|
|
|
|
func (h *Handler) DeleteSubModuleVideo(c *fiber.Ctx) error {
|
|
videoID, err := strconv.ParseInt(c.Params("videoId"), 10, 64)
|
|
if err != nil || videoID <= 0 {
|
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "Invalid video ID", Error: "videoId must be a positive integer"})
|
|
}
|
|
|
|
if err := h.analyticsDB.DeleteSubModuleVideoCompat(c.Context(), videoID); err != nil {
|
|
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to delete sub-module video", Error: err.Error()})
|
|
}
|
|
return c.JSON(domain.Response{Message: "Sub-module video deleted"})
|
|
}
|
|
|
|
func (h *Handler) UpdatePractice(c *fiber.Ctx) error {
|
|
practiceID, err := strconv.ParseInt(c.Params("practiceId"), 10, 64)
|
|
if err != nil || practiceID <= 0 {
|
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "Invalid practice ID", Error: "practiceId must be a positive integer"})
|
|
}
|
|
|
|
var req updatePracticeReq
|
|
if err := c.BodyParser(&req); err != nil {
|
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "Invalid request body", Error: err.Error()})
|
|
}
|
|
|
|
if req.IsActive != nil {
|
|
if err := h.analyticsDB.UpdatePracticeStatusCompat(c.Context(), practiceID, *req.IsActive); err != nil {
|
|
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to update practice status", Error: err.Error()})
|
|
}
|
|
return c.JSON(domain.Response{Message: "Practice status updated"})
|
|
}
|
|
|
|
title := ""
|
|
if req.Title != nil {
|
|
title = *req.Title
|
|
}
|
|
description := ""
|
|
if req.Description != nil {
|
|
description = *req.Description
|
|
}
|
|
persona := ""
|
|
if req.Persona != nil {
|
|
persona = *req.Persona
|
|
}
|
|
if strings.TrimSpace(title) == "" {
|
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "title is required"})
|
|
}
|
|
|
|
if err := h.analyticsDB.UpdatePracticeCompat(c.Context(), practiceID, title, description, persona); err != nil {
|
|
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to update practice", Error: err.Error()})
|
|
}
|
|
return c.JSON(domain.Response{Message: "Practice updated"})
|
|
}
|
|
|
|
func (h *Handler) DeletePractice(c *fiber.Ctx) error {
|
|
practiceID, err := strconv.ParseInt(c.Params("practiceId"), 10, 64)
|
|
if err != nil || practiceID <= 0 {
|
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "Invalid practice ID", Error: "practiceId must be a positive integer"})
|
|
}
|
|
|
|
if err := h.analyticsDB.DeletePracticeCompat(c.Context(), practiceID); err != nil {
|
|
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to delete practice", Error: err.Error()})
|
|
}
|
|
return c.JSON(domain.Response{Message: "Practice deleted"})
|
|
}
|
|
|