diff --git a/db/query/hierarchy.sql b/db/query/hierarchy.sql index 320c05a..e1204b3 100644 --- a/db/query/hierarchy.sql +++ b/db/query/hierarchy.sql @@ -246,6 +246,25 @@ ORDER BY csc.display_order ASC, csc.id ASC LIMIT sqlc.narg('limit')::INT OFFSET sqlc.narg('offset')::INT; +-- name: GetCourseSubCategoriesByCategoryID :many +SELECT + COUNT(*) OVER () AS total_count, + csc.id, + csc.category_id, + cc.name AS category_name, + csc.name, + csc.description, + csc.display_order, + csc.is_active, + csc.created_at +FROM course_sub_categories csc +JOIN course_categories cc ON cc.id = csc.category_id +WHERE csc.category_id = $1 + AND csc.is_active = TRUE +ORDER BY csc.display_order ASC, csc.id ASC +LIMIT sqlc.narg('limit')::INT +OFFSET sqlc.narg('offset')::INT; + -- name: GetHumanLanguageCourseSubCategories :many SELECT COUNT(*) OVER () AS total_count, diff --git a/docs/docs.go b/docs/docs.go index dc0bc20..b24ad6e 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -979,6 +979,65 @@ const docTemplate = `{ } } }, + "/api/v1/course-management/categories/{categoryId}/sub-categories": { + "get": { + "description": "Returns active sub-categories for the given category ID", + "produces": [ + "application/json" + ], + "tags": [ + "course-management" + ], + "summary": "List sub-categories for a course category", + "parameters": [ + { + "type": "integer", + "description": "Category ID", + "name": "categoryId", + "in": "path", + "required": true + }, + { + "type": "integer", + "description": "Offset", + "name": "offset", + "in": "query" + }, + { + "type": "integer", + "description": "Limit", + "name": "limit", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, "/api/v1/course-management/courses": { "get": { "description": "Returns all courses with pagination", diff --git a/docs/swagger.json b/docs/swagger.json index 1096361..2841611 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -971,6 +971,65 @@ } } }, + "/api/v1/course-management/categories/{categoryId}/sub-categories": { + "get": { + "description": "Returns active sub-categories for the given category ID", + "produces": [ + "application/json" + ], + "tags": [ + "course-management" + ], + "summary": "List sub-categories for a course category", + "parameters": [ + { + "type": "integer", + "description": "Category ID", + "name": "categoryId", + "in": "path", + "required": true + }, + { + "type": "integer", + "description": "Offset", + "name": "offset", + "in": "query" + }, + { + "type": "integer", + "description": "Limit", + "name": "limit", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, "/api/v1/course-management/courses": { "get": { "description": "Returns all courses with pagination", diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 5955e23..6ab3ed4 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -2742,6 +2742,45 @@ paths: summary: List courses by category tags: - course-management + /api/v1/course-management/categories/{categoryId}/sub-categories: + get: + description: Returns active sub-categories for the given category ID + parameters: + - description: Category ID + in: path + name: categoryId + required: true + type: integer + - description: Offset + in: query + name: offset + type: integer + - description: Limit + in: query + name: limit + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/domain.Response' + "400": + description: Bad Request + schema: + $ref: '#/definitions/domain.ErrorResponse' + "404": + description: Not Found + schema: + $ref: '#/definitions/domain.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/domain.ErrorResponse' + summary: List sub-categories for a course category + tags: + - course-management /api/v1/course-management/courses: get: description: Returns all courses with pagination diff --git a/gen/db/hierarchy.sql.go b/gen/db/hierarchy.sql.go index d46b37e..19d565f 100644 --- a/gen/db/hierarchy.sql.go +++ b/gen/db/hierarchy.sql.go @@ -775,6 +775,74 @@ func (q *Queries) GetCourseSubCategories(ctx context.Context, arg GetCourseSubCa return items, nil } +const GetCourseSubCategoriesByCategoryID = `-- name: GetCourseSubCategoriesByCategoryID :many +SELECT + COUNT(*) OVER () AS total_count, + csc.id, + csc.category_id, + cc.name AS category_name, + csc.name, + csc.description, + csc.display_order, + csc.is_active, + csc.created_at +FROM course_sub_categories csc +JOIN course_categories cc ON cc.id = csc.category_id +WHERE csc.category_id = $1 + AND csc.is_active = TRUE +ORDER BY csc.display_order ASC, csc.id ASC +LIMIT $3::INT +OFFSET $2::INT +` + +type GetCourseSubCategoriesByCategoryIDParams struct { + CategoryID int64 `json:"category_id"` + Offset pgtype.Int4 `json:"offset"` + Limit pgtype.Int4 `json:"limit"` +} + +type GetCourseSubCategoriesByCategoryIDRow struct { + TotalCount int64 `json:"total_count"` + ID int64 `json:"id"` + CategoryID int64 `json:"category_id"` + CategoryName string `json:"category_name"` + Name string `json:"name"` + Description pgtype.Text `json:"description"` + DisplayOrder int32 `json:"display_order"` + IsActive bool `json:"is_active"` + CreatedAt pgtype.Timestamptz `json:"created_at"` +} + +func (q *Queries) GetCourseSubCategoriesByCategoryID(ctx context.Context, arg GetCourseSubCategoriesByCategoryIDParams) ([]GetCourseSubCategoriesByCategoryIDRow, error) { + rows, err := q.db.Query(ctx, GetCourseSubCategoriesByCategoryID, arg.CategoryID, arg.Offset, arg.Limit) + if err != nil { + return nil, err + } + defer rows.Close() + var items []GetCourseSubCategoriesByCategoryIDRow + for rows.Next() { + var i GetCourseSubCategoriesByCategoryIDRow + if err := rows.Scan( + &i.TotalCount, + &i.ID, + &i.CategoryID, + &i.CategoryName, + &i.Name, + &i.Description, + &i.DisplayOrder, + &i.IsActive, + &i.CreatedAt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + const GetCoursesWithHierarchy = `-- name: GetCoursesWithHierarchy :many SELECT cc.id AS category_id, diff --git a/internal/web_server/handlers/hierarchy_handler.go b/internal/web_server/handlers/hierarchy_handler.go index 28bfe47..ad5816d 100644 --- a/internal/web_server/handlers/hierarchy_handler.go +++ b/internal/web_server/handlers/hierarchy_handler.go @@ -3,11 +3,13 @@ package handlers import ( dbgen "Yimaru-Backend/gen/db" "Yimaru-Backend/internal/domain" + "errors" "strconv" "strings" "unicode/utf8" "github.com/gofiber/fiber/v2" + "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgtype" ) @@ -908,6 +910,75 @@ func (h *Handler) ListCourseSubCategories(c *fiber.Ctx) error { }) } +// ListCourseSubCategoriesByCategory godoc +// @Summary List sub-categories for a course category +// @Description Returns active sub-categories for the given category ID +// @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 404 {object} domain.ErrorResponse +// @Failure 500 {object} domain.ErrorResponse +// @Router /api/v1/course-management/categories/{categoryId}/sub-categories [get] +func (h *Handler) ListCourseSubCategoriesByCategory(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.GetCourseCategoryByID(c.Context(), categoryID); err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{ + Message: "Category not found", + Error: err.Error(), + }) + } + return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ + Message: "Failed to load category", + Error: err.Error(), + }) + } + + 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.GetCourseSubCategoriesByCategoryID(c.Context(), dbgen.GetCourseSubCategoriesByCategoryIDParams{ + 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 sub-categories", + Error: err.Error(), + }) + } + + total := 0 + if len(rows) > 0 { + total = int(rows[0].TotalCount) + } + + return c.JSON(domain.Response{ + Message: "Sub-categories retrieved successfully", + Data: map[string]interface{}{ + "sub_categories": rows, + "total_count": total, + }, + }) +} + // ListHumanLanguageCourseSubCategories godoc // @Summary List Human Language sub-categories // @Description Returns active sub-categories under Human Language category diff --git a/internal/web_server/routes.go b/internal/web_server/routes.go index 8c69c7f..348611e 100644 --- a/internal/web_server/routes.go +++ b/internal/web_server/routes.go @@ -80,6 +80,7 @@ func (a *App) initAppRoutes() { // 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/:categoryId/sub-categories", a.authMiddleware, a.RequirePermission("learning_tree.get"), h.ListCourseSubCategoriesByCategory) groupV1.Get("/course-management/courses", a.authMiddleware, a.RequirePermission("learning_tree.get"), h.ListAllCourses) groupV1.Get("/course-management/human-language/courses", a.authMiddleware, a.RequirePermission("learning_tree.get"), h.ListHumanLanguageCourses) groupV1.Get("/course-management/sub-categories/:subCategoryId/courses", a.authMiddleware, a.RequirePermission("learning_tree.get"), h.ListCoursesBySubCategory)