Expand course hierarchy read APIs and practice retrieval.
Add list/detail endpoints for courses, levels, modules, submodules, and submodule practices; extend course listing queries; add lesson update support and clean up removed route paths. Made-with: Cursor
This commit is contained in:
parent
343ce470cc
commit
1026354c24
|
|
@ -33,6 +33,57 @@ ORDER BY display_order ASC, id ASC
|
|||
LIMIT sqlc.narg('limit')::INT
|
||||
OFFSET sqlc.narg('offset')::INT;
|
||||
|
||||
-- name: GetAllCourses :many
|
||||
SELECT
|
||||
COUNT(*) OVER () AS total_count,
|
||||
c.id,
|
||||
c.category_id,
|
||||
c.sub_category_id,
|
||||
c.title,
|
||||
c.description,
|
||||
c.thumbnail,
|
||||
c.intro_video_url,
|
||||
c.is_active
|
||||
FROM courses c
|
||||
ORDER BY c.display_order ASC, c.id ASC
|
||||
LIMIT sqlc.narg('limit')::INT
|
||||
OFFSET sqlc.narg('offset')::INT;
|
||||
|
||||
-- name: GetHumanLanguageCourses :many
|
||||
SELECT
|
||||
COUNT(*) OVER () AS total_count,
|
||||
c.id,
|
||||
c.category_id,
|
||||
c.sub_category_id,
|
||||
c.title,
|
||||
c.description,
|
||||
c.thumbnail,
|
||||
c.intro_video_url,
|
||||
c.is_active
|
||||
FROM courses c
|
||||
JOIN course_categories cc ON cc.id = c.category_id
|
||||
WHERE lower(trim(cc.name)) = 'human language'
|
||||
ORDER BY c.display_order ASC, c.id ASC
|
||||
LIMIT sqlc.narg('limit')::INT
|
||||
OFFSET sqlc.narg('offset')::INT;
|
||||
|
||||
-- name: GetCoursesBySubCategory :many
|
||||
SELECT
|
||||
COUNT(*) OVER () AS total_count,
|
||||
c.id,
|
||||
c.category_id,
|
||||
c.sub_category_id,
|
||||
c.title,
|
||||
c.description,
|
||||
c.thumbnail,
|
||||
c.intro_video_url,
|
||||
c.is_active
|
||||
FROM courses c
|
||||
WHERE c.sub_category_id = $1
|
||||
ORDER BY c.display_order ASC, c.id ASC
|
||||
LIMIT sqlc.narg('limit')::INT
|
||||
OFFSET sqlc.narg('offset')::INT;
|
||||
|
||||
|
||||
-- name: UpdateCourse :exec
|
||||
UPDATE courses
|
||||
|
|
|
|||
|
|
@ -19,6 +19,20 @@ WHERE course_id = $1
|
|||
AND is_active = TRUE
|
||||
ORDER BY display_order ASC, id ASC;
|
||||
|
||||
-- name: GetAllLevels :many
|
||||
SELECT
|
||||
COUNT(*) OVER () AS total_count,
|
||||
l.*
|
||||
FROM levels l
|
||||
ORDER BY l.display_order ASC, l.id ASC
|
||||
LIMIT sqlc.narg('limit')::INT
|
||||
OFFSET sqlc.narg('offset')::INT;
|
||||
|
||||
-- name: GetLevelByID :one
|
||||
SELECT *
|
||||
FROM levels
|
||||
WHERE id = $1;
|
||||
|
||||
-- name: GetModulesByLevelID :many
|
||||
SELECT *
|
||||
FROM modules
|
||||
|
|
@ -26,6 +40,20 @@ WHERE level_id = $1
|
|||
AND is_active = TRUE
|
||||
ORDER BY display_order ASC, id ASC;
|
||||
|
||||
-- name: GetAllModules :many
|
||||
SELECT
|
||||
COUNT(*) OVER () AS total_count,
|
||||
m.*
|
||||
FROM modules m
|
||||
ORDER BY m.display_order ASC, m.id ASC
|
||||
LIMIT sqlc.narg('limit')::INT
|
||||
OFFSET sqlc.narg('offset')::INT;
|
||||
|
||||
-- name: GetModuleByID :one
|
||||
SELECT *
|
||||
FROM modules
|
||||
WHERE id = $1;
|
||||
|
||||
-- name: GetSubModulesByModuleID :many
|
||||
SELECT *
|
||||
FROM sub_modules
|
||||
|
|
@ -33,6 +61,20 @@ WHERE module_id = $1
|
|||
AND is_active = TRUE
|
||||
ORDER BY display_order ASC, id ASC;
|
||||
|
||||
-- name: GetAllSubModules :many
|
||||
SELECT
|
||||
COUNT(*) OVER () AS total_count,
|
||||
sm.*
|
||||
FROM sub_modules sm
|
||||
ORDER BY sm.display_order ASC, sm.id ASC
|
||||
LIMIT sqlc.narg('limit')::INT
|
||||
OFFSET sqlc.narg('offset')::INT;
|
||||
|
||||
-- name: GetSubModuleByID :one
|
||||
SELECT *
|
||||
FROM sub_modules
|
||||
WHERE id = $1;
|
||||
|
||||
-- name: GetSubModuleVideos :many
|
||||
SELECT *
|
||||
FROM sub_module_videos
|
||||
|
|
@ -100,6 +142,26 @@ WHERE smp.sub_module_id = $1
|
|||
AND qs.set_type = 'PRACTICE'
|
||||
ORDER BY smp.display_order ASC, smp.id ASC;
|
||||
|
||||
-- name: GetSubModulePracticeByID :one
|
||||
SELECT
|
||||
smp.id,
|
||||
smp.sub_module_id,
|
||||
smp.title,
|
||||
smp.description,
|
||||
smp.thumbnail,
|
||||
smp.intro_video_url,
|
||||
smp.question_set_id,
|
||||
smp.display_order,
|
||||
smp.is_active,
|
||||
qs.status,
|
||||
qs.set_type,
|
||||
(SELECT COUNT(*) FROM question_set_items qsi WHERE qsi.set_id = qs.id) AS question_count
|
||||
FROM sub_module_practices smp
|
||||
JOIN question_sets qs ON qs.id = smp.question_set_id
|
||||
WHERE smp.id = $1
|
||||
AND smp.is_active = TRUE
|
||||
AND qs.set_type = 'PRACTICE';
|
||||
|
||||
-- name: GetFullHierarchyByCourseID :many
|
||||
SELECT
|
||||
c.id AS course_id,
|
||||
|
|
|
|||
|
|
@ -67,6 +67,70 @@ func (q *Queries) DeleteCourse(ctx context.Context, id int64) error {
|
|||
return err
|
||||
}
|
||||
|
||||
const GetAllCourses = `-- name: GetAllCourses :many
|
||||
SELECT
|
||||
COUNT(*) OVER () AS total_count,
|
||||
c.id,
|
||||
c.category_id,
|
||||
c.sub_category_id,
|
||||
c.title,
|
||||
c.description,
|
||||
c.thumbnail,
|
||||
c.intro_video_url,
|
||||
c.is_active
|
||||
FROM courses c
|
||||
ORDER BY c.display_order ASC, c.id ASC
|
||||
LIMIT $2::INT
|
||||
OFFSET $1::INT
|
||||
`
|
||||
|
||||
type GetAllCoursesParams struct {
|
||||
Offset pgtype.Int4 `json:"offset"`
|
||||
Limit pgtype.Int4 `json:"limit"`
|
||||
}
|
||||
|
||||
type GetAllCoursesRow struct {
|
||||
TotalCount int64 `json:"total_count"`
|
||||
ID int64 `json:"id"`
|
||||
CategoryID int64 `json:"category_id"`
|
||||
SubCategoryID pgtype.Int8 `json:"sub_category_id"`
|
||||
Title string `json:"title"`
|
||||
Description pgtype.Text `json:"description"`
|
||||
Thumbnail pgtype.Text `json:"thumbnail"`
|
||||
IntroVideoUrl pgtype.Text `json:"intro_video_url"`
|
||||
IsActive bool `json:"is_active"`
|
||||
}
|
||||
|
||||
func (q *Queries) GetAllCourses(ctx context.Context, arg GetAllCoursesParams) ([]GetAllCoursesRow, error) {
|
||||
rows, err := q.db.Query(ctx, GetAllCourses, arg.Offset, arg.Limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []GetAllCoursesRow
|
||||
for rows.Next() {
|
||||
var i GetAllCoursesRow
|
||||
if err := rows.Scan(
|
||||
&i.TotalCount,
|
||||
&i.ID,
|
||||
&i.CategoryID,
|
||||
&i.SubCategoryID,
|
||||
&i.Title,
|
||||
&i.Description,
|
||||
&i.Thumbnail,
|
||||
&i.IntroVideoUrl,
|
||||
&i.IsActive,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const GetCourseByID = `-- name: GetCourseByID :one
|
||||
SELECT id, category_id, title, description, is_active, thumbnail, intro_video_url, display_order, sub_category_id
|
||||
FROM courses
|
||||
|
|
@ -153,6 +217,138 @@ func (q *Queries) GetCoursesByCategory(ctx context.Context, arg GetCoursesByCate
|
|||
return items, nil
|
||||
}
|
||||
|
||||
const GetCoursesBySubCategory = `-- name: GetCoursesBySubCategory :many
|
||||
SELECT
|
||||
COUNT(*) OVER () AS total_count,
|
||||
c.id,
|
||||
c.category_id,
|
||||
c.sub_category_id,
|
||||
c.title,
|
||||
c.description,
|
||||
c.thumbnail,
|
||||
c.intro_video_url,
|
||||
c.is_active
|
||||
FROM courses c
|
||||
WHERE c.sub_category_id = $1
|
||||
ORDER BY c.display_order ASC, c.id ASC
|
||||
LIMIT $3::INT
|
||||
OFFSET $2::INT
|
||||
`
|
||||
|
||||
type GetCoursesBySubCategoryParams struct {
|
||||
SubCategoryID pgtype.Int8 `json:"sub_category_id"`
|
||||
Offset pgtype.Int4 `json:"offset"`
|
||||
Limit pgtype.Int4 `json:"limit"`
|
||||
}
|
||||
|
||||
type GetCoursesBySubCategoryRow struct {
|
||||
TotalCount int64 `json:"total_count"`
|
||||
ID int64 `json:"id"`
|
||||
CategoryID int64 `json:"category_id"`
|
||||
SubCategoryID pgtype.Int8 `json:"sub_category_id"`
|
||||
Title string `json:"title"`
|
||||
Description pgtype.Text `json:"description"`
|
||||
Thumbnail pgtype.Text `json:"thumbnail"`
|
||||
IntroVideoUrl pgtype.Text `json:"intro_video_url"`
|
||||
IsActive bool `json:"is_active"`
|
||||
}
|
||||
|
||||
func (q *Queries) GetCoursesBySubCategory(ctx context.Context, arg GetCoursesBySubCategoryParams) ([]GetCoursesBySubCategoryRow, error) {
|
||||
rows, err := q.db.Query(ctx, GetCoursesBySubCategory, arg.SubCategoryID, arg.Offset, arg.Limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []GetCoursesBySubCategoryRow
|
||||
for rows.Next() {
|
||||
var i GetCoursesBySubCategoryRow
|
||||
if err := rows.Scan(
|
||||
&i.TotalCount,
|
||||
&i.ID,
|
||||
&i.CategoryID,
|
||||
&i.SubCategoryID,
|
||||
&i.Title,
|
||||
&i.Description,
|
||||
&i.Thumbnail,
|
||||
&i.IntroVideoUrl,
|
||||
&i.IsActive,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const GetHumanLanguageCourses = `-- name: GetHumanLanguageCourses :many
|
||||
SELECT
|
||||
COUNT(*) OVER () AS total_count,
|
||||
c.id,
|
||||
c.category_id,
|
||||
c.sub_category_id,
|
||||
c.title,
|
||||
c.description,
|
||||
c.thumbnail,
|
||||
c.intro_video_url,
|
||||
c.is_active
|
||||
FROM courses c
|
||||
JOIN course_categories cc ON cc.id = c.category_id
|
||||
WHERE lower(trim(cc.name)) = 'human language'
|
||||
ORDER BY c.display_order ASC, c.id ASC
|
||||
LIMIT $2::INT
|
||||
OFFSET $1::INT
|
||||
`
|
||||
|
||||
type GetHumanLanguageCoursesParams struct {
|
||||
Offset pgtype.Int4 `json:"offset"`
|
||||
Limit pgtype.Int4 `json:"limit"`
|
||||
}
|
||||
|
||||
type GetHumanLanguageCoursesRow struct {
|
||||
TotalCount int64 `json:"total_count"`
|
||||
ID int64 `json:"id"`
|
||||
CategoryID int64 `json:"category_id"`
|
||||
SubCategoryID pgtype.Int8 `json:"sub_category_id"`
|
||||
Title string `json:"title"`
|
||||
Description pgtype.Text `json:"description"`
|
||||
Thumbnail pgtype.Text `json:"thumbnail"`
|
||||
IntroVideoUrl pgtype.Text `json:"intro_video_url"`
|
||||
IsActive bool `json:"is_active"`
|
||||
}
|
||||
|
||||
func (q *Queries) GetHumanLanguageCourses(ctx context.Context, arg GetHumanLanguageCoursesParams) ([]GetHumanLanguageCoursesRow, error) {
|
||||
rows, err := q.db.Query(ctx, GetHumanLanguageCourses, arg.Offset, arg.Limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []GetHumanLanguageCoursesRow
|
||||
for rows.Next() {
|
||||
var i GetHumanLanguageCoursesRow
|
||||
if err := rows.Scan(
|
||||
&i.TotalCount,
|
||||
&i.ID,
|
||||
&i.CategoryID,
|
||||
&i.SubCategoryID,
|
||||
&i.Title,
|
||||
&i.Description,
|
||||
&i.Thumbnail,
|
||||
&i.IntroVideoUrl,
|
||||
&i.IsActive,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const ReorderCourses = `-- name: ReorderCourses :exec
|
||||
UPDATE courses
|
||||
SET display_order = bulk.position
|
||||
|
|
|
|||
|
|
@ -364,6 +364,171 @@ func (q *Queries) CreateSubModuleVideo(ctx context.Context, arg CreateSubModuleV
|
|||
return i, err
|
||||
}
|
||||
|
||||
const GetAllLevels = `-- name: GetAllLevels :many
|
||||
SELECT
|
||||
COUNT(*) OVER () AS total_count,
|
||||
l.id, l.course_id, l.cefr_level, l.display_order, l.is_active, l.created_at
|
||||
FROM levels l
|
||||
ORDER BY l.display_order ASC, l.id ASC
|
||||
LIMIT $2::INT
|
||||
OFFSET $1::INT
|
||||
`
|
||||
|
||||
type GetAllLevelsParams struct {
|
||||
Offset pgtype.Int4 `json:"offset"`
|
||||
Limit pgtype.Int4 `json:"limit"`
|
||||
}
|
||||
|
||||
type GetAllLevelsRow struct {
|
||||
TotalCount int64 `json:"total_count"`
|
||||
ID int64 `json:"id"`
|
||||
CourseID int64 `json:"course_id"`
|
||||
CefrLevel string `json:"cefr_level"`
|
||||
DisplayOrder int32 `json:"display_order"`
|
||||
IsActive bool `json:"is_active"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
}
|
||||
|
||||
func (q *Queries) GetAllLevels(ctx context.Context, arg GetAllLevelsParams) ([]GetAllLevelsRow, error) {
|
||||
rows, err := q.db.Query(ctx, GetAllLevels, arg.Offset, arg.Limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []GetAllLevelsRow
|
||||
for rows.Next() {
|
||||
var i GetAllLevelsRow
|
||||
if err := rows.Scan(
|
||||
&i.TotalCount,
|
||||
&i.ID,
|
||||
&i.CourseID,
|
||||
&i.CefrLevel,
|
||||
&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 GetAllModules = `-- name: GetAllModules :many
|
||||
SELECT
|
||||
COUNT(*) OVER () AS total_count,
|
||||
m.id, m.level_id, m.title, m.description, m.display_order, m.is_active, m.created_at
|
||||
FROM modules m
|
||||
ORDER BY m.display_order ASC, m.id ASC
|
||||
LIMIT $2::INT
|
||||
OFFSET $1::INT
|
||||
`
|
||||
|
||||
type GetAllModulesParams struct {
|
||||
Offset pgtype.Int4 `json:"offset"`
|
||||
Limit pgtype.Int4 `json:"limit"`
|
||||
}
|
||||
|
||||
type GetAllModulesRow struct {
|
||||
TotalCount int64 `json:"total_count"`
|
||||
ID int64 `json:"id"`
|
||||
LevelID int64 `json:"level_id"`
|
||||
Title string `json:"title"`
|
||||
Description pgtype.Text `json:"description"`
|
||||
DisplayOrder int32 `json:"display_order"`
|
||||
IsActive bool `json:"is_active"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
}
|
||||
|
||||
func (q *Queries) GetAllModules(ctx context.Context, arg GetAllModulesParams) ([]GetAllModulesRow, error) {
|
||||
rows, err := q.db.Query(ctx, GetAllModules, arg.Offset, arg.Limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []GetAllModulesRow
|
||||
for rows.Next() {
|
||||
var i GetAllModulesRow
|
||||
if err := rows.Scan(
|
||||
&i.TotalCount,
|
||||
&i.ID,
|
||||
&i.LevelID,
|
||||
&i.Title,
|
||||
&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 GetAllSubModules = `-- name: GetAllSubModules :many
|
||||
SELECT
|
||||
COUNT(*) OVER () AS total_count,
|
||||
sm.id, sm.module_id, sm.title, sm.description, sm.display_order, sm.is_active, sm.created_at, sm.legacy_sub_course_id
|
||||
FROM sub_modules sm
|
||||
ORDER BY sm.display_order ASC, sm.id ASC
|
||||
LIMIT $2::INT
|
||||
OFFSET $1::INT
|
||||
`
|
||||
|
||||
type GetAllSubModulesParams struct {
|
||||
Offset pgtype.Int4 `json:"offset"`
|
||||
Limit pgtype.Int4 `json:"limit"`
|
||||
}
|
||||
|
||||
type GetAllSubModulesRow struct {
|
||||
TotalCount int64 `json:"total_count"`
|
||||
ID int64 `json:"id"`
|
||||
ModuleID int64 `json:"module_id"`
|
||||
Title string `json:"title"`
|
||||
Description pgtype.Text `json:"description"`
|
||||
DisplayOrder int32 `json:"display_order"`
|
||||
IsActive bool `json:"is_active"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
LegacySubCourseID pgtype.Int8 `json:"legacy_sub_course_id"`
|
||||
}
|
||||
|
||||
func (q *Queries) GetAllSubModules(ctx context.Context, arg GetAllSubModulesParams) ([]GetAllSubModulesRow, error) {
|
||||
rows, err := q.db.Query(ctx, GetAllSubModules, arg.Offset, arg.Limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []GetAllSubModulesRow
|
||||
for rows.Next() {
|
||||
var i GetAllSubModulesRow
|
||||
if err := rows.Scan(
|
||||
&i.TotalCount,
|
||||
&i.ID,
|
||||
&i.ModuleID,
|
||||
&i.Title,
|
||||
&i.Description,
|
||||
&i.DisplayOrder,
|
||||
&i.IsActive,
|
||||
&i.CreatedAt,
|
||||
&i.LegacySubCourseID,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const GetCourseSubCategories = `-- name: GetCourseSubCategories :many
|
||||
SELECT
|
||||
COUNT(*) OVER () AS total_count,
|
||||
|
|
@ -607,6 +772,26 @@ func (q *Queries) GetHumanLanguageCourseSubCategories(ctx context.Context, arg G
|
|||
return items, nil
|
||||
}
|
||||
|
||||
const GetLevelByID = `-- name: GetLevelByID :one
|
||||
SELECT id, course_id, cefr_level, display_order, is_active, created_at
|
||||
FROM levels
|
||||
WHERE id = $1
|
||||
`
|
||||
|
||||
func (q *Queries) GetLevelByID(ctx context.Context, id int64) (Level, error) {
|
||||
row := q.db.QueryRow(ctx, GetLevelByID, id)
|
||||
var i Level
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.CourseID,
|
||||
&i.CefrLevel,
|
||||
&i.DisplayOrder,
|
||||
&i.IsActive,
|
||||
&i.CreatedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const GetLevelsByCourseID = `-- name: GetLevelsByCourseID :many
|
||||
SELECT id, course_id, cefr_level, display_order, is_active, created_at
|
||||
FROM levels
|
||||
|
|
@ -642,6 +827,27 @@ func (q *Queries) GetLevelsByCourseID(ctx context.Context, courseID int64) ([]Le
|
|||
return items, nil
|
||||
}
|
||||
|
||||
const GetModuleByID = `-- name: GetModuleByID :one
|
||||
SELECT id, level_id, title, description, display_order, is_active, created_at
|
||||
FROM modules
|
||||
WHERE id = $1
|
||||
`
|
||||
|
||||
func (q *Queries) GetModuleByID(ctx context.Context, id int64) (Module, error) {
|
||||
row := q.db.QueryRow(ctx, GetModuleByID, id)
|
||||
var i Module
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.LevelID,
|
||||
&i.Title,
|
||||
&i.Description,
|
||||
&i.DisplayOrder,
|
||||
&i.IsActive,
|
||||
&i.CreatedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const GetModulesByLevelID = `-- name: GetModulesByLevelID :many
|
||||
SELECT id, level_id, title, description, display_order, is_active, created_at
|
||||
FROM modules
|
||||
|
|
@ -678,6 +884,28 @@ func (q *Queries) GetModulesByLevelID(ctx context.Context, levelID int64) ([]Mod
|
|||
return items, nil
|
||||
}
|
||||
|
||||
const GetSubModuleByID = `-- name: GetSubModuleByID :one
|
||||
SELECT id, module_id, title, description, display_order, is_active, created_at, legacy_sub_course_id
|
||||
FROM sub_modules
|
||||
WHERE id = $1
|
||||
`
|
||||
|
||||
func (q *Queries) GetSubModuleByID(ctx context.Context, id int64) (SubModule, error) {
|
||||
row := q.db.QueryRow(ctx, GetSubModuleByID, id)
|
||||
var i SubModule
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.ModuleID,
|
||||
&i.Title,
|
||||
&i.Description,
|
||||
&i.DisplayOrder,
|
||||
&i.IsActive,
|
||||
&i.CreatedAt,
|
||||
&i.LegacySubCourseID,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const GetSubModuleLessonByID = `-- name: GetSubModuleLessonByID :one
|
||||
SELECT
|
||||
smp.id,
|
||||
|
|
@ -798,6 +1026,62 @@ func (q *Queries) GetSubModuleLessons(ctx context.Context, subModuleID int64) ([
|
|||
return items, nil
|
||||
}
|
||||
|
||||
const GetSubModulePracticeByID = `-- name: GetSubModulePracticeByID :one
|
||||
SELECT
|
||||
smp.id,
|
||||
smp.sub_module_id,
|
||||
smp.title,
|
||||
smp.description,
|
||||
smp.thumbnail,
|
||||
smp.intro_video_url,
|
||||
smp.question_set_id,
|
||||
smp.display_order,
|
||||
smp.is_active,
|
||||
qs.status,
|
||||
qs.set_type,
|
||||
(SELECT COUNT(*) FROM question_set_items qsi WHERE qsi.set_id = qs.id) AS question_count
|
||||
FROM sub_module_practices smp
|
||||
JOIN question_sets qs ON qs.id = smp.question_set_id
|
||||
WHERE smp.id = $1
|
||||
AND smp.is_active = TRUE
|
||||
AND qs.set_type = 'PRACTICE'
|
||||
`
|
||||
|
||||
type GetSubModulePracticeByIDRow struct {
|
||||
ID int64 `json:"id"`
|
||||
SubModuleID int64 `json:"sub_module_id"`
|
||||
Title string `json:"title"`
|
||||
Description pgtype.Text `json:"description"`
|
||||
Thumbnail pgtype.Text `json:"thumbnail"`
|
||||
IntroVideoUrl pgtype.Text `json:"intro_video_url"`
|
||||
QuestionSetID int64 `json:"question_set_id"`
|
||||
DisplayOrder int32 `json:"display_order"`
|
||||
IsActive bool `json:"is_active"`
|
||||
Status string `json:"status"`
|
||||
SetType string `json:"set_type"`
|
||||
QuestionCount int64 `json:"question_count"`
|
||||
}
|
||||
|
||||
func (q *Queries) GetSubModulePracticeByID(ctx context.Context, id int64) (GetSubModulePracticeByIDRow, error) {
|
||||
row := q.db.QueryRow(ctx, GetSubModulePracticeByID, id)
|
||||
var i GetSubModulePracticeByIDRow
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.SubModuleID,
|
||||
&i.Title,
|
||||
&i.Description,
|
||||
&i.Thumbnail,
|
||||
&i.IntroVideoUrl,
|
||||
&i.QuestionSetID,
|
||||
&i.DisplayOrder,
|
||||
&i.IsActive,
|
||||
&i.Status,
|
||||
&i.SetType,
|
||||
&i.QuestionCount,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const GetSubModulePractices = `-- name: GetSubModulePractices :many
|
||||
SELECT
|
||||
smp.id,
|
||||
|
|
|
|||
|
|
@ -269,6 +269,501 @@ func (h *Handler) ListCoursesByCategory(c *fiber.Ctx) error {
|
|||
})
|
||||
}
|
||||
|
||||
// ListAllCourses godoc
|
||||
// @Summary List all courses
|
||||
// @Description Returns all courses with pagination
|
||||
// @Tags course-management
|
||||
// @Produce json
|
||||
// @Param offset query int false "Offset"
|
||||
// @Param limit query int false "Limit"
|
||||
// @Success 200 {object} domain.Response
|
||||
// @Failure 500 {object} domain.ErrorResponse
|
||||
// @Router /api/v1/course-management/courses [get]
|
||||
func (h *Handler) ListAllCourses(c *fiber.Ctx) 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.GetAllCourses(c.Context(), dbgen.GetAllCoursesParams{
|
||||
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,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// ListHumanLanguageCourses godoc
|
||||
// @Summary List Human Language courses
|
||||
// @Description Returns all courses under Human Language category
|
||||
// @Tags course-management
|
||||
// @Produce json
|
||||
// @Param offset query int false "Offset"
|
||||
// @Param limit query int false "Limit"
|
||||
// @Success 200 {object} domain.Response
|
||||
// @Failure 500 {object} domain.ErrorResponse
|
||||
// @Router /api/v1/course-management/human-language/courses [get]
|
||||
func (h *Handler) ListHumanLanguageCourses(c *fiber.Ctx) 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.GetHumanLanguageCourses(c.Context(), dbgen.GetHumanLanguageCoursesParams{
|
||||
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 Human Language courses", Error: err.Error()})
|
||||
}
|
||||
|
||||
total := 0
|
||||
if len(rows) > 0 {
|
||||
total = int(rows[0].TotalCount)
|
||||
}
|
||||
|
||||
return c.JSON(domain.Response{
|
||||
Message: "Human Language courses retrieved successfully",
|
||||
Data: map[string]interface{}{
|
||||
"courses": rows,
|
||||
"total_count": total,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// ListCoursesBySubCategory godoc
|
||||
// @Summary List courses by sub-category
|
||||
// @Description Returns courses for one sub-category
|
||||
// @Tags course-management
|
||||
// @Produce json
|
||||
// @Param subCategoryId path int true "Sub-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/sub-categories/{subCategoryId}/courses [get]
|
||||
func (h *Handler) ListCoursesBySubCategory(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"})
|
||||
}
|
||||
|
||||
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.GetCoursesBySubCategory(c.Context(), dbgen.GetCoursesBySubCategoryParams{
|
||||
SubCategoryID: pgtype.Int8{Int64: subCategoryID, Valid: true},
|
||||
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,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// GetCourseByID godoc
|
||||
// @Summary Get course detail
|
||||
// @Description Returns one course by ID
|
||||
// @Tags course-management
|
||||
// @Produce json
|
||||
// @Param courseId path int true "Course ID"
|
||||
// @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/courses/{courseId} [get]
|
||||
func (h *Handler) GetCourseByID(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",
|
||||
})
|
||||
}
|
||||
|
||||
course, err := h.analyticsDB.GetCourseByID(c.Context(), courseID)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{
|
||||
Message: "Course not found",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
return c.JSON(domain.Response{
|
||||
Message: "Course retrieved successfully",
|
||||
Data: course,
|
||||
})
|
||||
}
|
||||
|
||||
// ListAllLevels godoc
|
||||
// @Summary List all levels
|
||||
// @Description Returns all levels with pagination
|
||||
// @Tags course-management
|
||||
// @Produce json
|
||||
// @Param offset query int false "Offset"
|
||||
// @Param limit query int false "Limit"
|
||||
// @Success 200 {object} domain.Response
|
||||
// @Failure 500 {object} domain.ErrorResponse
|
||||
// @Router /api/v1/course-management/levels [get]
|
||||
func (h *Handler) ListAllLevels(c *fiber.Ctx) 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.GetAllLevels(c.Context(), dbgen.GetAllLevelsParams{
|
||||
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 levels", Error: err.Error()})
|
||||
}
|
||||
|
||||
total := 0
|
||||
if len(rows) > 0 {
|
||||
total = int(rows[0].TotalCount)
|
||||
}
|
||||
|
||||
return c.JSON(domain.Response{
|
||||
Message: "Levels retrieved successfully",
|
||||
Data: map[string]interface{}{
|
||||
"levels": rows,
|
||||
"total_count": total,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// ListLevelsByCourse godoc
|
||||
// @Summary List levels by course
|
||||
// @Description Returns all active levels for one 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}/levels [get]
|
||||
func (h *Handler) ListLevelsByCourse(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",
|
||||
})
|
||||
}
|
||||
|
||||
rows, err := h.analyticsDB.GetLevelsByCourseID(c.Context(), courseID)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to load levels", Error: err.Error()})
|
||||
}
|
||||
|
||||
return c.JSON(domain.Response{
|
||||
Message: "Levels retrieved successfully",
|
||||
Data: map[string]interface{}{
|
||||
"levels": rows,
|
||||
"total_count": len(rows),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// GetLevelByID godoc
|
||||
// @Summary Get level detail
|
||||
// @Description Returns one level by ID
|
||||
// @Tags course-management
|
||||
// @Produce json
|
||||
// @Param levelId path int true "Level ID"
|
||||
// @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/levels/{levelId} [get]
|
||||
func (h *Handler) GetLevelByID(c *fiber.Ctx) error {
|
||||
levelID, err := strconv.ParseInt(c.Params("levelId"), 10, 64)
|
||||
if err != nil || levelID <= 0 {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||
Message: "Invalid level ID",
|
||||
Error: "levelId must be a positive integer",
|
||||
})
|
||||
}
|
||||
|
||||
level, err := h.analyticsDB.GetLevelByID(c.Context(), levelID)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{
|
||||
Message: "Level not found",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
return c.JSON(domain.Response{
|
||||
Message: "Level retrieved successfully",
|
||||
Data: level,
|
||||
})
|
||||
}
|
||||
|
||||
// ListAllModules godoc
|
||||
// @Summary List all modules
|
||||
// @Description Returns all modules with pagination
|
||||
// @Tags course-management
|
||||
// @Produce json
|
||||
// @Param offset query int false "Offset"
|
||||
// @Param limit query int false "Limit"
|
||||
// @Success 200 {object} domain.Response
|
||||
// @Failure 500 {object} domain.ErrorResponse
|
||||
// @Router /api/v1/course-management/modules [get]
|
||||
func (h *Handler) ListAllModules(c *fiber.Ctx) 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.GetAllModules(c.Context(), dbgen.GetAllModulesParams{
|
||||
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 modules", Error: err.Error()})
|
||||
}
|
||||
|
||||
total := 0
|
||||
if len(rows) > 0 {
|
||||
total = int(rows[0].TotalCount)
|
||||
}
|
||||
|
||||
return c.JSON(domain.Response{
|
||||
Message: "Modules retrieved successfully",
|
||||
Data: map[string]interface{}{
|
||||
"modules": rows,
|
||||
"total_count": total,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// ListModulesByLevel godoc
|
||||
// @Summary List modules by level
|
||||
// @Description Returns all active modules for one level
|
||||
// @Tags course-management
|
||||
// @Produce json
|
||||
// @Param levelId path int true "Level ID"
|
||||
// @Success 200 {object} domain.Response
|
||||
// @Failure 400 {object} domain.ErrorResponse
|
||||
// @Failure 500 {object} domain.ErrorResponse
|
||||
// @Router /api/v1/course-management/levels/{levelId}/modules [get]
|
||||
func (h *Handler) ListModulesByLevel(c *fiber.Ctx) error {
|
||||
levelID, err := strconv.ParseInt(c.Params("levelId"), 10, 64)
|
||||
if err != nil || levelID <= 0 {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||
Message: "Invalid level ID",
|
||||
Error: "levelId must be a positive integer",
|
||||
})
|
||||
}
|
||||
|
||||
rows, err := h.analyticsDB.GetModulesByLevelID(c.Context(), levelID)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to load modules", Error: err.Error()})
|
||||
}
|
||||
|
||||
return c.JSON(domain.Response{
|
||||
Message: "Modules retrieved successfully",
|
||||
Data: map[string]interface{}{
|
||||
"modules": rows,
|
||||
"total_count": len(rows),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// GetModuleByID godoc
|
||||
// @Summary Get module detail
|
||||
// @Description Returns one module by ID
|
||||
// @Tags course-management
|
||||
// @Produce json
|
||||
// @Param moduleId path int true "Module ID"
|
||||
// @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/modules/{moduleId} [get]
|
||||
func (h *Handler) GetModuleByID(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",
|
||||
})
|
||||
}
|
||||
|
||||
mod, err := h.analyticsDB.GetModuleByID(c.Context(), moduleID)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{
|
||||
Message: "Module not found",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
return c.JSON(domain.Response{
|
||||
Message: "Module retrieved successfully",
|
||||
Data: mod,
|
||||
})
|
||||
}
|
||||
|
||||
// ListAllSubModules godoc
|
||||
// @Summary List all sub-modules
|
||||
// @Description Returns all sub-modules with pagination
|
||||
// @Tags course-management
|
||||
// @Produce json
|
||||
// @Param offset query int false "Offset"
|
||||
// @Param limit query int false "Limit"
|
||||
// @Success 200 {object} domain.Response
|
||||
// @Failure 500 {object} domain.ErrorResponse
|
||||
// @Router /api/v1/course-management/sub-modules [get]
|
||||
func (h *Handler) ListAllSubModules(c *fiber.Ctx) 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.GetAllSubModules(c.Context(), dbgen.GetAllSubModulesParams{
|
||||
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-modules", Error: err.Error()})
|
||||
}
|
||||
|
||||
total := 0
|
||||
if len(rows) > 0 {
|
||||
total = int(rows[0].TotalCount)
|
||||
}
|
||||
|
||||
return c.JSON(domain.Response{
|
||||
Message: "Sub-modules retrieved successfully",
|
||||
Data: map[string]interface{}{
|
||||
"sub_modules": rows,
|
||||
"total_count": total,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// ListSubModulesByModule godoc
|
||||
// @Summary List sub-modules by module
|
||||
// @Description Returns all active sub-modules for one module
|
||||
// @Tags course-management
|
||||
// @Produce json
|
||||
// @Param moduleId path int true "Module ID"
|
||||
// @Success 200 {object} domain.Response
|
||||
// @Failure 400 {object} domain.ErrorResponse
|
||||
// @Failure 500 {object} domain.ErrorResponse
|
||||
// @Router /api/v1/course-management/modules/{moduleId}/sub-modules [get]
|
||||
func (h *Handler) ListSubModulesByModule(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",
|
||||
})
|
||||
}
|
||||
|
||||
rows, err := h.analyticsDB.GetSubModulesByModuleID(c.Context(), moduleID)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to load sub-modules", Error: err.Error()})
|
||||
}
|
||||
|
||||
return c.JSON(domain.Response{
|
||||
Message: "Sub-modules retrieved successfully",
|
||||
Data: map[string]interface{}{
|
||||
"sub_modules": rows,
|
||||
"total_count": len(rows),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// GetSubModuleByID godoc
|
||||
// @Summary Get sub-module detail
|
||||
// @Description Returns one sub-module by ID
|
||||
// @Tags course-management
|
||||
// @Produce json
|
||||
// @Param subModuleId path int true "Sub-module ID"
|
||||
// @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/sub-modules/{subModuleId} [get]
|
||||
func (h *Handler) GetSubModuleByID(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",
|
||||
})
|
||||
}
|
||||
|
||||
subModule, err := h.analyticsDB.GetSubModuleByID(c.Context(), subModuleID)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{
|
||||
Message: "Sub-module not found",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
return c.JSON(domain.Response{
|
||||
Message: "Sub-module retrieved successfully",
|
||||
Data: subModule,
|
||||
})
|
||||
}
|
||||
|
||||
// ListCourseSubCategories godoc
|
||||
// @Summary List course sub-categories
|
||||
// @Description Returns all active course sub-categories
|
||||
|
|
@ -1383,6 +1878,78 @@ func (h *Handler) CreateSubModulePractice(c *fiber.Ctx) error {
|
|||
return c.Status(fiber.StatusCreated).JSON(domain.Response{Message: "Practice created", Data: created})
|
||||
}
|
||||
|
||||
// GetSubModulePractices godoc
|
||||
// @Summary Get practices under sub-module
|
||||
// @Description Returns all active practices attached to a sub-module
|
||||
// @Tags course-management
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param subModuleId path int true "Sub-module ID"
|
||||
// @Success 200 {object} domain.Response
|
||||
// @Failure 400 {object} domain.ErrorResponse
|
||||
// @Failure 500 {object} domain.ErrorResponse
|
||||
// @Router /api/v1/course-management/sub-modules/{subModuleId}/practices [get]
|
||||
func (h *Handler) GetSubModulePractices(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",
|
||||
})
|
||||
}
|
||||
|
||||
practices, err := h.analyticsDB.GetSubModulePractices(c.Context(), subModuleID)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||
Message: "Failed to load sub-module practices",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
return c.JSON(domain.Response{
|
||||
Message: "Sub-module practices retrieved successfully",
|
||||
Data: map[string]interface{}{
|
||||
"practices": practices,
|
||||
"total_count": len(practices),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// GetSubModulePracticeByID godoc
|
||||
// @Summary Get practice detail
|
||||
// @Description Returns one active practice by practice ID
|
||||
// @Tags course-management
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param practiceId path int true "Practice ID"
|
||||
// @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/practices/{practiceId} [get]
|
||||
func (h *Handler) GetSubModulePracticeByID(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",
|
||||
})
|
||||
}
|
||||
|
||||
practice, err := h.analyticsDB.GetSubModulePracticeByID(c.Context(), practiceID)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{
|
||||
Message: "Practice not found",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
return c.JSON(domain.Response{
|
||||
Message: "Practice retrieved successfully",
|
||||
Data: practice,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *Handler) GetSubModuleVideos(c *fiber.Ctx) error {
|
||||
subModuleID, err := strconv.ParseInt(c.Params("subModuleId"), 10, 64)
|
||||
if err != nil || subModuleID <= 0 {
|
||||
|
|
|
|||
|
|
@ -80,7 +80,19 @@ 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/courses", a.authMiddleware, a.RequirePermission("learning_tree.get"), h.ListCoursesByCategory)
|
||||
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)
|
||||
groupV1.Get("/course-management/courses/:courseId", a.authMiddleware, a.RequirePermission("learning_tree.get"), h.GetCourseByID)
|
||||
groupV1.Get("/course-management/levels", a.authMiddleware, a.RequirePermission("learning_tree.get"), h.ListAllLevels)
|
||||
groupV1.Get("/course-management/courses/:courseId/levels", a.authMiddleware, a.RequirePermission("learning_tree.get"), h.ListLevelsByCourse)
|
||||
groupV1.Get("/course-management/levels/:levelId", a.authMiddleware, a.RequirePermission("learning_tree.get"), h.GetLevelByID)
|
||||
groupV1.Get("/course-management/modules", a.authMiddleware, a.RequirePermission("learning_tree.get"), h.ListAllModules)
|
||||
groupV1.Get("/course-management/levels/:levelId/modules", a.authMiddleware, a.RequirePermission("learning_tree.get"), h.ListModulesByLevel)
|
||||
groupV1.Get("/course-management/modules/:moduleId", a.authMiddleware, a.RequirePermission("learning_tree.get"), h.GetModuleByID)
|
||||
groupV1.Get("/course-management/sub-modules", a.authMiddleware, a.RequirePermission("learning_tree.get"), h.ListAllSubModules)
|
||||
groupV1.Get("/course-management/modules/:moduleId/sub-modules", a.authMiddleware, a.RequirePermission("learning_tree.get"), h.ListSubModulesByModule)
|
||||
groupV1.Get("/course-management/sub-modules/:subModuleId", a.authMiddleware, a.RequirePermission("learning_tree.get"), h.GetSubModuleByID)
|
||||
groupV1.Post("/course-management/categories", a.authMiddleware, a.RequirePermission("course_categories.create"), h.CreateCourseCategory)
|
||||
groupV1.Delete("/course-management/categories/:categoryId", a.authMiddleware, a.RequirePermission("course_categories.delete"), h.DeleteCourseCategory)
|
||||
groupV1.Post("/course-management/courses", a.authMiddleware, a.RequirePermission("courses.create"), h.CreateCourse)
|
||||
|
|
@ -101,7 +113,6 @@ func (a *App) initAppRoutes() {
|
|||
groupV1.Post("/course-management/sub-modules", a.authMiddleware, a.RequirePermission("subcourses.create"), h.CreateSubModule)
|
||||
groupV1.Put("/course-management/sub-modules/:subModuleId", a.authMiddleware, a.RequirePermission("subcourses.update"), h.UpdateSubModule)
|
||||
groupV1.Delete("/course-management/sub-modules/:subModuleId", a.authMiddleware, a.RequirePermission("subcourses.delete"), h.DeleteSubModule)
|
||||
groupV1.Get("/course-management/sub-modules/:subModuleId/videos", a.authMiddleware, a.RequirePermission("videos.list"), h.GetSubModuleVideos)
|
||||
groupV1.Post("/course-management/sub-module-videos", a.authMiddleware, a.RequirePermission("videos.create"), h.CreateSubModuleVideo)
|
||||
groupV1.Put("/course-management/sub-module-videos/:videoId", a.authMiddleware, a.RequirePermission("videos.update"), h.UpdateSubModuleVideo)
|
||||
groupV1.Delete("/course-management/sub-module-videos/:videoId", a.authMiddleware, a.RequirePermission("videos.delete"), h.DeleteSubModuleVideo)
|
||||
|
|
@ -109,6 +120,8 @@ func (a *App) initAppRoutes() {
|
|||
groupV1.Get("/course-management/sub-module-lessons/:lessonId", a.authMiddleware, a.RequirePermission("question_sets.get"), h.GetSubModuleLessonByID)
|
||||
groupV1.Put("/course-management/sub-module-lessons/:lessonId", a.authMiddleware, a.RequirePermission("question_sets.update"), h.UpdateSubModuleLesson)
|
||||
groupV1.Post("/course-management/sub-module-lessons", a.authMiddleware, a.RequirePermission("question_sets.update"), h.AttachSubModuleLesson)
|
||||
groupV1.Get("/course-management/sub-modules/:subModuleId/practices", a.authMiddleware, a.RequirePermission("question_sets.list"), h.GetSubModulePractices)
|
||||
groupV1.Get("/course-management/practices/:practiceId", a.authMiddleware, a.RequirePermission("question_sets.get"), h.GetSubModulePracticeByID)
|
||||
groupV1.Post("/course-management/sub-module-practices", a.authMiddleware, a.RequirePermission("question_sets.update"), h.CreateSubModulePractice)
|
||||
groupV1.Put("/course-management/practices/:practiceId", a.authMiddleware, a.RequirePermission("question_sets.update"), h.UpdatePractice)
|
||||
groupV1.Delete("/course-management/practices/:practiceId", a.authMiddleware, a.RequirePermission("question_sets.delete"), h.DeletePractice)
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user