learning flow fixes
This commit is contained in:
parent
f9da45da62
commit
3500db6435
66
README.md
66
README.md
|
|
@ -90,17 +90,17 @@ created_at – Audit timestamp
|
|||
|
||||
Relationships:
|
||||
|
||||
One Course Category → Many Courses
|
||||
One Course Category → Many Course Sub-categories
|
||||
|
||||
Course Category
|
||||
└── Courses[]
|
||||
└── Course Sub-categories[]
|
||||
|
||||
2. Course
|
||||
2. Course Sub-category
|
||||
|
||||
Table: courses
|
||||
Table: course_sub_categories
|
||||
|
||||
Purpose:
|
||||
Represents a full course offering under a category.
|
||||
A grouping within a category (e.g., Speaking, Listening under Learning English).
|
||||
|
||||
Key Fields:
|
||||
|
||||
|
|
@ -114,23 +114,23 @@ Relationships:
|
|||
|
||||
Belongs to one Course Category
|
||||
|
||||
Has many Sub-courses
|
||||
Has many Courses
|
||||
|
||||
Course Category
|
||||
└── Course
|
||||
└── Sub-courses[]
|
||||
└── Course Sub-category
|
||||
└── Courses[]
|
||||
|
||||
3. Sub-course
|
||||
3. Course
|
||||
|
||||
Table: sub_courses
|
||||
Table: courses
|
||||
|
||||
Purpose:
|
||||
A learning unit within a course representing different skill levels
|
||||
A learning unit within a sub-category representing different skill levels
|
||||
(e.g., Beginner, Intermediate, Advanced).
|
||||
|
||||
Key Fields:
|
||||
|
||||
course_id – FK → courses.id
|
||||
sub_category_id – FK → course_sub_categories.id
|
||||
|
||||
title, description
|
||||
|
||||
|
|
@ -144,27 +144,27 @@ is_active
|
|||
|
||||
Relationships:
|
||||
|
||||
Belongs to one Course
|
||||
Belongs to one Course Sub-category
|
||||
|
||||
Has many Sub-course Videos
|
||||
Has many Course Videos
|
||||
|
||||
Has many Practices
|
||||
|
||||
Course
|
||||
└── Sub-course
|
||||
├── Sub-course Videos[]
|
||||
Course Sub-category
|
||||
└── Course
|
||||
├── Course Videos[]
|
||||
└── Practices[]
|
||||
|
||||
4. Sub-course Video
|
||||
4. Course Video
|
||||
|
||||
Table: sub_course_videos
|
||||
Table: course_videos
|
||||
|
||||
Purpose:
|
||||
Video learning content attached to a sub-course.
|
||||
Video learning content attached to a course.
|
||||
|
||||
Key Fields:
|
||||
|
||||
sub_course_id – FK → sub_courses.id
|
||||
course_id – FK → courses.id
|
||||
|
||||
title, description
|
||||
|
||||
|
|
@ -190,21 +190,21 @@ is_active
|
|||
|
||||
Relationships:
|
||||
|
||||
Belongs to one Sub-course
|
||||
Belongs to one Course
|
||||
|
||||
Sub-course
|
||||
└── Sub-course Video
|
||||
Course
|
||||
└── Course Video
|
||||
|
||||
5. Practice
|
||||
|
||||
Table: practices
|
||||
|
||||
Purpose:
|
||||
Exercises or assessments that belong to a sub-course.
|
||||
Exercises or assessments that belong to a course.
|
||||
|
||||
Key Fields:
|
||||
|
||||
sub_course_id – FK → sub_courses.id
|
||||
course_id – FK → courses.id
|
||||
|
||||
title, description
|
||||
|
||||
|
|
@ -216,11 +216,11 @@ is_active
|
|||
|
||||
Relationships:
|
||||
|
||||
Belongs to one Sub-course
|
||||
Belongs to one Course
|
||||
|
||||
One Practice → Many Practice Questions
|
||||
|
||||
Sub-course
|
||||
Course
|
||||
└── Practice
|
||||
└── Practice Questions[]
|
||||
|
||||
|
|
@ -258,17 +258,17 @@ Practice
|
|||
|
||||
Complete Hierarchical Flow (Compact View)
|
||||
Course Category
|
||||
└── Course
|
||||
└── Sub-course (with levels: BEGINNER, INTERMEDIATE, ADVANCED)
|
||||
├── Sub-course Video
|
||||
└── Course Sub-category
|
||||
└── Course (with levels: BEGINNER, INTERMEDIATE, ADVANCED)
|
||||
├── Course Video
|
||||
└── Practice
|
||||
└── Practice Question
|
||||
|
||||
Architectural Observations
|
||||
|
||||
Simple three-level hierarchy: Category → Course → Sub-course
|
||||
Simple three-level hierarchy: Category → Sub-category → Course
|
||||
|
||||
Level is now a property of sub-course, not a separate entity
|
||||
Level is now a property of Course, not a separate entity
|
||||
|
||||
Cascade deletes ensure referential integrity
|
||||
|
||||
|
|
|
|||
3
db/migrations/000023_reorder_support.down.sql
Normal file
3
db/migrations/000023_reorder_support.down.sql
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
ALTER TABLE course_categories DROP COLUMN display_order;
|
||||
ALTER TABLE courses DROP COLUMN display_order;
|
||||
ALTER TABLE question_sets DROP COLUMN display_order;
|
||||
3
db/migrations/000023_reorder_support.up.sql
Normal file
3
db/migrations/000023_reorder_support.up.sql
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
ALTER TABLE course_categories ADD COLUMN display_order INT NOT NULL DEFAULT 0;
|
||||
ALTER TABLE courses ADD COLUMN display_order INT NOT NULL DEFAULT 0;
|
||||
ALTER TABLE question_sets ADD COLUMN display_order INT NOT NULL DEFAULT 0;
|
||||
|
|
@ -21,7 +21,7 @@ SELECT
|
|||
is_active,
|
||||
created_at
|
||||
FROM course_categories
|
||||
ORDER BY created_at DESC
|
||||
ORDER BY display_order ASC, created_at DESC
|
||||
LIMIT sqlc.narg('limit')::INT
|
||||
OFFSET sqlc.narg('offset')::INT;
|
||||
|
||||
|
|
@ -37,3 +37,11 @@ WHERE id = $3;
|
|||
-- name: DeleteCourseCategory :exec
|
||||
DELETE FROM course_categories
|
||||
WHERE id = $1;
|
||||
|
||||
-- name: ReorderCourseCategories :exec
|
||||
UPDATE course_categories
|
||||
SET display_order = bulk.position
|
||||
FROM (
|
||||
SELECT unnest(@ids::BIGINT[]) AS id, unnest(@positions::INT[]) AS position
|
||||
) AS bulk
|
||||
WHERE course_categories.id = bulk.id;
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ SELECT
|
|||
is_active
|
||||
FROM courses
|
||||
WHERE category_id = $1
|
||||
ORDER BY id DESC
|
||||
ORDER BY display_order ASC, id ASC
|
||||
LIMIT sqlc.narg('limit')::INT
|
||||
OFFSET sqlc.narg('offset')::INT;
|
||||
|
||||
|
|
@ -48,3 +48,11 @@ WHERE id = $6;
|
|||
-- name: DeleteCourse :exec
|
||||
DELETE FROM courses
|
||||
WHERE id = $1;
|
||||
|
||||
-- name: ReorderCourses :exec
|
||||
UPDATE courses
|
||||
SET display_order = bulk.position
|
||||
FROM (
|
||||
SELECT unnest(@ids::BIGINT[]) AS id, unnest(@positions::INT[]) AS position
|
||||
) AS bulk
|
||||
WHERE courses.id = bulk.id;
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ SELECT id, title, description, persona, status,
|
|||
FROM question_sets qs
|
||||
WHERE qs.owner_type = 'SUB_COURSE' AND qs.owner_id = $1
|
||||
AND qs.set_type = 'PRACTICE' AND qs.status = 'PUBLISHED'
|
||||
ORDER BY qs.created_at;
|
||||
ORDER BY qs.display_order ASC, qs.created_at;
|
||||
|
||||
-- name: GetSubCoursePrerequisitesForLearningPath :many
|
||||
SELECT p.prerequisite_sub_course_id, sc.title, sc.level
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ FROM question_sets
|
|||
WHERE owner_type = $1
|
||||
AND owner_id = $2
|
||||
AND status != 'ARCHIVED'
|
||||
ORDER BY created_at DESC;
|
||||
ORDER BY display_order ASC, created_at DESC;
|
||||
|
||||
-- name: GetQuestionSetsByType :many
|
||||
SELECT
|
||||
|
|
@ -114,3 +114,11 @@ SET
|
|||
sub_course_video_id = $1,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = $2;
|
||||
|
||||
-- name: ReorderQuestionSets :exec
|
||||
UPDATE question_sets
|
||||
SET display_order = bulk.position
|
||||
FROM (
|
||||
SELECT unnest(@ids::BIGINT[]) AS id, unnest(@positions::INT[]) AS position
|
||||
) AS bulk
|
||||
WHERE question_sets.id = bulk.id;
|
||||
|
|
|
|||
|
|
@ -112,3 +112,11 @@ WHERE id = $1;
|
|||
-- name: DeleteSubCourseVideo :exec
|
||||
DELETE FROM sub_course_videos
|
||||
WHERE id = $1;
|
||||
|
||||
-- name: ReorderSubCourseVideos :exec
|
||||
UPDATE sub_course_videos
|
||||
SET display_order = bulk.position
|
||||
FROM (
|
||||
SELECT unnest(@ids::BIGINT[]) AS id, unnest(@positions::INT[]) AS position
|
||||
) AS bulk
|
||||
WHERE sub_course_videos.id = bulk.id;
|
||||
|
|
|
|||
|
|
@ -80,3 +80,11 @@ RETURNING *;
|
|||
UPDATE sub_courses
|
||||
SET is_active = FALSE
|
||||
WHERE id = $1;
|
||||
|
||||
-- name: ReorderSubCourses :exec
|
||||
UPDATE sub_courses
|
||||
SET display_order = bulk.position
|
||||
FROM (
|
||||
SELECT unnest(@ids::BIGINT[]) AS id, unnest(@positions::INT[]) AS position
|
||||
) AS bulk
|
||||
WHERE sub_courses.id = bulk.id;
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ INSERT INTO course_categories (
|
|||
is_active
|
||||
)
|
||||
VALUES ($1, COALESCE($2, true))
|
||||
RETURNING id, name, is_active, created_at
|
||||
RETURNING id, name, is_active, created_at, display_order
|
||||
`
|
||||
|
||||
type CreateCourseCategoryParams struct {
|
||||
|
|
@ -33,6 +33,7 @@ func (q *Queries) CreateCourseCategory(ctx context.Context, arg CreateCourseCate
|
|||
&i.Name,
|
||||
&i.IsActive,
|
||||
&i.CreatedAt,
|
||||
&i.DisplayOrder,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
|
@ -55,7 +56,7 @@ SELECT
|
|||
is_active,
|
||||
created_at
|
||||
FROM course_categories
|
||||
ORDER BY created_at DESC
|
||||
ORDER BY display_order ASC, created_at DESC
|
||||
LIMIT $2::INT
|
||||
OFFSET $1::INT
|
||||
`
|
||||
|
|
@ -100,7 +101,7 @@ func (q *Queries) GetAllCourseCategories(ctx context.Context, arg GetAllCourseCa
|
|||
}
|
||||
|
||||
const GetCourseCategoryByID = `-- name: GetCourseCategoryByID :one
|
||||
SELECT id, name, is_active, created_at
|
||||
SELECT id, name, is_active, created_at, display_order
|
||||
FROM course_categories
|
||||
WHERE id = $1
|
||||
`
|
||||
|
|
@ -113,10 +114,30 @@ func (q *Queries) GetCourseCategoryByID(ctx context.Context, id int64) (CourseCa
|
|||
&i.Name,
|
||||
&i.IsActive,
|
||||
&i.CreatedAt,
|
||||
&i.DisplayOrder,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const ReorderCourseCategories = `-- name: ReorderCourseCategories :exec
|
||||
UPDATE course_categories
|
||||
SET display_order = bulk.position
|
||||
FROM (
|
||||
SELECT unnest($1::BIGINT[]) AS id, unnest($2::INT[]) AS position
|
||||
) AS bulk
|
||||
WHERE course_categories.id = bulk.id
|
||||
`
|
||||
|
||||
type ReorderCourseCategoriesParams struct {
|
||||
Ids []int64 `json:"ids"`
|
||||
Positions []int32 `json:"positions"`
|
||||
}
|
||||
|
||||
func (q *Queries) ReorderCourseCategories(ctx context.Context, arg ReorderCourseCategoriesParams) error {
|
||||
_, err := q.db.Exec(ctx, ReorderCourseCategories, arg.Ids, arg.Positions)
|
||||
return err
|
||||
}
|
||||
|
||||
const UpdateCourseCategory = `-- name: UpdateCourseCategory :exec
|
||||
UPDATE course_categories
|
||||
SET
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ INSERT INTO courses (
|
|||
is_active
|
||||
)
|
||||
VALUES ($1, $2, $3, $4, $5, COALESCE($6, true))
|
||||
RETURNING id, category_id, title, description, is_active, thumbnail, intro_video_url
|
||||
RETURNING id, category_id, title, description, is_active, thumbnail, intro_video_url, display_order
|
||||
`
|
||||
|
||||
type CreateCourseParams struct {
|
||||
|
|
@ -51,6 +51,7 @@ func (q *Queries) CreateCourse(ctx context.Context, arg CreateCourseParams) (Cou
|
|||
&i.IsActive,
|
||||
&i.Thumbnail,
|
||||
&i.IntroVideoUrl,
|
||||
&i.DisplayOrder,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
|
@ -66,7 +67,7 @@ func (q *Queries) DeleteCourse(ctx context.Context, id int64) error {
|
|||
}
|
||||
|
||||
const GetCourseByID = `-- name: GetCourseByID :one
|
||||
SELECT id, category_id, title, description, is_active, thumbnail, intro_video_url
|
||||
SELECT id, category_id, title, description, is_active, thumbnail, intro_video_url, display_order
|
||||
FROM courses
|
||||
WHERE id = $1
|
||||
`
|
||||
|
|
@ -82,6 +83,7 @@ func (q *Queries) GetCourseByID(ctx context.Context, id int64) (Course, error) {
|
|||
&i.IsActive,
|
||||
&i.Thumbnail,
|
||||
&i.IntroVideoUrl,
|
||||
&i.DisplayOrder,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
|
@ -98,7 +100,7 @@ SELECT
|
|||
is_active
|
||||
FROM courses
|
||||
WHERE category_id = $1
|
||||
ORDER BY id DESC
|
||||
ORDER BY display_order ASC, id ASC
|
||||
LIMIT $3::INT
|
||||
OFFSET $2::INT
|
||||
`
|
||||
|
|
@ -149,6 +151,25 @@ func (q *Queries) GetCoursesByCategory(ctx context.Context, arg GetCoursesByCate
|
|||
return items, nil
|
||||
}
|
||||
|
||||
const ReorderCourses = `-- name: ReorderCourses :exec
|
||||
UPDATE courses
|
||||
SET display_order = bulk.position
|
||||
FROM (
|
||||
SELECT unnest($1::BIGINT[]) AS id, unnest($2::INT[]) AS position
|
||||
) AS bulk
|
||||
WHERE courses.id = bulk.id
|
||||
`
|
||||
|
||||
type ReorderCoursesParams struct {
|
||||
Ids []int64 `json:"ids"`
|
||||
Positions []int32 `json:"positions"`
|
||||
}
|
||||
|
||||
func (q *Queries) ReorderCourses(ctx context.Context, arg ReorderCoursesParams) error {
|
||||
_, err := q.db.Exec(ctx, ReorderCourses, arg.Ids, arg.Positions)
|
||||
return err
|
||||
}
|
||||
|
||||
const UpdateCourse = `-- name: UpdateCourse :exec
|
||||
UPDATE courses
|
||||
SET
|
||||
|
|
|
|||
|
|
@ -145,7 +145,7 @@ SELECT id, title, description, persona, status,
|
|||
FROM question_sets qs
|
||||
WHERE qs.owner_type = 'SUB_COURSE' AND qs.owner_id = $1
|
||||
AND qs.set_type = 'PRACTICE' AND qs.status = 'PUBLISHED'
|
||||
ORDER BY qs.created_at
|
||||
ORDER BY qs.display_order ASC, qs.created_at
|
||||
`
|
||||
|
||||
type GetSubCoursePracticesForLearningPathRow struct {
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ type Course struct {
|
|||
IsActive bool `json:"is_active"`
|
||||
Thumbnail pgtype.Text `json:"thumbnail"`
|
||||
IntroVideoUrl pgtype.Text `json:"intro_video_url"`
|
||||
DisplayOrder int32 `json:"display_order"`
|
||||
}
|
||||
|
||||
type CourseCategory struct {
|
||||
|
|
@ -37,6 +38,7 @@ type CourseCategory struct {
|
|||
Name string `json:"name"`
|
||||
IsActive bool `json:"is_active"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
DisplayOrder int32 `json:"display_order"`
|
||||
}
|
||||
|
||||
type Device struct {
|
||||
|
|
@ -170,6 +172,7 @@ type QuestionSet struct {
|
|||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||
SubCourseVideoID pgtype.Int8 `json:"sub_course_video_id"`
|
||||
DisplayOrder int32 `json:"display_order"`
|
||||
}
|
||||
|
||||
type QuestionSetItem struct {
|
||||
|
|
|
|||
|
|
@ -198,7 +198,7 @@ func (q *Queries) GetQuestionSetItems(ctx context.Context, setID int64) ([]GetQu
|
|||
}
|
||||
|
||||
const GetQuestionSetsContainingQuestion = `-- name: GetQuestionSetsContainingQuestion :many
|
||||
SELECT qs.id, qs.title, qs.description, qs.set_type, qs.owner_type, qs.owner_id, qs.banner_image, qs.persona, qs.time_limit_minutes, qs.passing_score, qs.shuffle_questions, qs.status, qs.created_at, qs.updated_at, qs.sub_course_video_id
|
||||
SELECT qs.id, qs.title, qs.description, qs.set_type, qs.owner_type, qs.owner_id, qs.banner_image, qs.persona, qs.time_limit_minutes, qs.passing_score, qs.shuffle_questions, qs.status, qs.created_at, qs.updated_at, qs.sub_course_video_id, qs.display_order
|
||||
FROM question_sets qs
|
||||
JOIN question_set_items qsi ON qsi.set_id = qs.id
|
||||
WHERE qsi.question_id = $1
|
||||
|
|
@ -230,6 +230,7 @@ func (q *Queries) GetQuestionSetsContainingQuestion(ctx context.Context, questio
|
|||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.SubCourseVideoID,
|
||||
&i.DisplayOrder,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@ INSERT INTO question_sets (
|
|||
sub_course_video_id
|
||||
)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, COALESCE($10, false), COALESCE($11, 'DRAFT'), $12)
|
||||
RETURNING id, title, description, set_type, owner_type, owner_id, banner_image, persona, time_limit_minutes, passing_score, shuffle_questions, status, created_at, updated_at, sub_course_video_id
|
||||
RETURNING id, title, description, set_type, owner_type, owner_id, banner_image, persona, time_limit_minutes, passing_score, shuffle_questions, status, created_at, updated_at, sub_course_video_id, display_order
|
||||
`
|
||||
|
||||
type CreateQuestionSetParams struct {
|
||||
|
|
@ -117,6 +117,7 @@ func (q *Queries) CreateQuestionSet(ctx context.Context, arg CreateQuestionSetPa
|
|||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.SubCourseVideoID,
|
||||
&i.DisplayOrder,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
|
@ -132,7 +133,7 @@ func (q *Queries) DeleteQuestionSet(ctx context.Context, id int64) error {
|
|||
}
|
||||
|
||||
const GetInitialAssessmentSet = `-- name: GetInitialAssessmentSet :one
|
||||
SELECT id, title, description, set_type, owner_type, owner_id, banner_image, persona, time_limit_minutes, passing_score, shuffle_questions, status, created_at, updated_at, sub_course_video_id
|
||||
SELECT id, title, description, set_type, owner_type, owner_id, banner_image, persona, time_limit_minutes, passing_score, shuffle_questions, status, created_at, updated_at, sub_course_video_id, display_order
|
||||
FROM question_sets
|
||||
WHERE set_type = 'INITIAL_ASSESSMENT'
|
||||
AND status = 'PUBLISHED'
|
||||
|
|
@ -159,12 +160,13 @@ func (q *Queries) GetInitialAssessmentSet(ctx context.Context) (QuestionSet, err
|
|||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.SubCourseVideoID,
|
||||
&i.DisplayOrder,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const GetPublishedQuestionSetsByOwner = `-- name: GetPublishedQuestionSetsByOwner :many
|
||||
SELECT id, title, description, set_type, owner_type, owner_id, banner_image, persona, time_limit_minutes, passing_score, shuffle_questions, status, created_at, updated_at, sub_course_video_id
|
||||
SELECT id, title, description, set_type, owner_type, owner_id, banner_image, persona, time_limit_minutes, passing_score, shuffle_questions, status, created_at, updated_at, sub_course_video_id, display_order
|
||||
FROM question_sets
|
||||
WHERE owner_type = $1
|
||||
AND owner_id = $2
|
||||
|
|
@ -202,6 +204,7 @@ func (q *Queries) GetPublishedQuestionSetsByOwner(ctx context.Context, arg GetPu
|
|||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.SubCourseVideoID,
|
||||
&i.DisplayOrder,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -214,7 +217,7 @@ func (q *Queries) GetPublishedQuestionSetsByOwner(ctx context.Context, arg GetPu
|
|||
}
|
||||
|
||||
const GetQuestionSetByID = `-- name: GetQuestionSetByID :one
|
||||
SELECT id, title, description, set_type, owner_type, owner_id, banner_image, persona, time_limit_minutes, passing_score, shuffle_questions, status, created_at, updated_at, sub_course_video_id
|
||||
SELECT id, title, description, set_type, owner_type, owner_id, banner_image, persona, time_limit_minutes, passing_score, shuffle_questions, status, created_at, updated_at, sub_course_video_id, display_order
|
||||
FROM question_sets
|
||||
WHERE id = $1
|
||||
`
|
||||
|
|
@ -238,17 +241,18 @@ func (q *Queries) GetQuestionSetByID(ctx context.Context, id int64) (QuestionSet
|
|||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.SubCourseVideoID,
|
||||
&i.DisplayOrder,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const GetQuestionSetsByOwner = `-- name: GetQuestionSetsByOwner :many
|
||||
SELECT id, title, description, set_type, owner_type, owner_id, banner_image, persona, time_limit_minutes, passing_score, shuffle_questions, status, created_at, updated_at, sub_course_video_id
|
||||
SELECT id, title, description, set_type, owner_type, owner_id, banner_image, persona, time_limit_minutes, passing_score, shuffle_questions, status, created_at, updated_at, sub_course_video_id, display_order
|
||||
FROM question_sets
|
||||
WHERE owner_type = $1
|
||||
AND owner_id = $2
|
||||
AND status != 'ARCHIVED'
|
||||
ORDER BY created_at DESC
|
||||
ORDER BY display_order ASC, created_at DESC
|
||||
`
|
||||
|
||||
type GetQuestionSetsByOwnerParams struct {
|
||||
|
|
@ -281,6 +285,7 @@ func (q *Queries) GetQuestionSetsByOwner(ctx context.Context, arg GetQuestionSet
|
|||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.SubCourseVideoID,
|
||||
&i.DisplayOrder,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -295,7 +300,7 @@ func (q *Queries) GetQuestionSetsByOwner(ctx context.Context, arg GetQuestionSet
|
|||
const GetQuestionSetsByType = `-- name: GetQuestionSetsByType :many
|
||||
SELECT
|
||||
COUNT(*) OVER () AS total_count,
|
||||
qs.id, qs.title, qs.description, qs.set_type, qs.owner_type, qs.owner_id, qs.banner_image, qs.persona, qs.time_limit_minutes, qs.passing_score, qs.shuffle_questions, qs.status, qs.created_at, qs.updated_at, qs.sub_course_video_id
|
||||
qs.id, qs.title, qs.description, qs.set_type, qs.owner_type, qs.owner_id, qs.banner_image, qs.persona, qs.time_limit_minutes, qs.passing_score, qs.shuffle_questions, qs.status, qs.created_at, qs.updated_at, qs.sub_course_video_id, qs.display_order
|
||||
FROM question_sets qs
|
||||
WHERE set_type = $1
|
||||
AND status != 'ARCHIVED'
|
||||
|
|
@ -327,6 +332,7 @@ type GetQuestionSetsByTypeRow struct {
|
|||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||
SubCourseVideoID pgtype.Int8 `json:"sub_course_video_id"`
|
||||
DisplayOrder int32 `json:"display_order"`
|
||||
}
|
||||
|
||||
func (q *Queries) GetQuestionSetsByType(ctx context.Context, arg GetQuestionSetsByTypeParams) ([]GetQuestionSetsByTypeRow, error) {
|
||||
|
|
@ -355,6 +361,7 @@ func (q *Queries) GetQuestionSetsByType(ctx context.Context, arg GetQuestionSets
|
|||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.SubCourseVideoID,
|
||||
&i.DisplayOrder,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -435,6 +442,25 @@ func (q *Queries) RemoveUserPersonaFromQuestionSet(ctx context.Context, arg Remo
|
|||
return err
|
||||
}
|
||||
|
||||
const ReorderQuestionSets = `-- name: ReorderQuestionSets :exec
|
||||
UPDATE question_sets
|
||||
SET display_order = bulk.position
|
||||
FROM (
|
||||
SELECT unnest($1::BIGINT[]) AS id, unnest($2::INT[]) AS position
|
||||
) AS bulk
|
||||
WHERE question_sets.id = bulk.id
|
||||
`
|
||||
|
||||
type ReorderQuestionSetsParams struct {
|
||||
Ids []int64 `json:"ids"`
|
||||
Positions []int32 `json:"positions"`
|
||||
}
|
||||
|
||||
func (q *Queries) ReorderQuestionSets(ctx context.Context, arg ReorderQuestionSetsParams) error {
|
||||
_, err := q.db.Exec(ctx, ReorderQuestionSets, arg.Ids, arg.Positions)
|
||||
return err
|
||||
}
|
||||
|
||||
const UpdateQuestionSet = `-- name: UpdateQuestionSet :exec
|
||||
UPDATE question_sets
|
||||
SET
|
||||
|
|
|
|||
|
|
@ -345,6 +345,25 @@ func (q *Queries) PublishSubCourseVideo(ctx context.Context, id int64) error {
|
|||
return err
|
||||
}
|
||||
|
||||
const ReorderSubCourseVideos = `-- name: ReorderSubCourseVideos :exec
|
||||
UPDATE sub_course_videos
|
||||
SET display_order = bulk.position
|
||||
FROM (
|
||||
SELECT unnest($1::BIGINT[]) AS id, unnest($2::INT[]) AS position
|
||||
) AS bulk
|
||||
WHERE sub_course_videos.id = bulk.id
|
||||
`
|
||||
|
||||
type ReorderSubCourseVideosParams struct {
|
||||
Ids []int64 `json:"ids"`
|
||||
Positions []int32 `json:"positions"`
|
||||
}
|
||||
|
||||
func (q *Queries) ReorderSubCourseVideos(ctx context.Context, arg ReorderSubCourseVideosParams) error {
|
||||
_, err := q.db.Exec(ctx, ReorderSubCourseVideos, arg.Ids, arg.Positions)
|
||||
return err
|
||||
}
|
||||
|
||||
const UpdateSubCourseVideo = `-- name: UpdateSubCourseVideo :exec
|
||||
UPDATE sub_course_videos
|
||||
SET
|
||||
|
|
|
|||
|
|
@ -261,6 +261,25 @@ func (q *Queries) ListSubCoursesByCourse(ctx context.Context, courseID int64) ([
|
|||
return items, nil
|
||||
}
|
||||
|
||||
const ReorderSubCourses = `-- name: ReorderSubCourses :exec
|
||||
UPDATE sub_courses
|
||||
SET display_order = bulk.position
|
||||
FROM (
|
||||
SELECT unnest($1::BIGINT[]) AS id, unnest($2::INT[]) AS position
|
||||
) AS bulk
|
||||
WHERE sub_courses.id = bulk.id
|
||||
`
|
||||
|
||||
type ReorderSubCoursesParams struct {
|
||||
Ids []int64 `json:"ids"`
|
||||
Positions []int32 `json:"positions"`
|
||||
}
|
||||
|
||||
func (q *Queries) ReorderSubCourses(ctx context.Context, arg ReorderSubCoursesParams) error {
|
||||
_, err := q.db.Exec(ctx, ReorderSubCourses, arg.Ids, arg.Positions)
|
||||
return err
|
||||
}
|
||||
|
||||
const UpdateSubCourse = `-- name: UpdateSubCourse :exec
|
||||
UPDATE sub_courses
|
||||
SET
|
||||
|
|
|
|||
|
|
@ -175,6 +175,13 @@ type CourseStore interface {
|
|||
|
||||
// Learning Path (full nested structure for a course)
|
||||
GetCourseLearningPath(ctx context.Context, courseID int64) (domain.LearningPath, error)
|
||||
|
||||
// Reorder (drag-and-drop support)
|
||||
ReorderCourseCategories(ctx context.Context, ids []int64, positions []int32) error
|
||||
ReorderCourses(ctx context.Context, ids []int64, positions []int32) error
|
||||
ReorderSubCourses(ctx context.Context, ids []int64, positions []int32) error
|
||||
ReorderSubCourseVideos(ctx context.Context, ids []int64, positions []int32) error
|
||||
ReorderQuestionSets(ctx context.Context, ids []int64, positions []int32) error
|
||||
}
|
||||
|
||||
type ProgressionStore interface {
|
||||
|
|
|
|||
|
|
@ -119,3 +119,10 @@ func (s *Store) DeleteCourseCategory(
|
|||
|
||||
return s.queries.DeleteCourseCategory(ctx, id)
|
||||
}
|
||||
|
||||
func (s *Store) ReorderCourseCategories(ctx context.Context, ids []int64, positions []int32) error {
|
||||
return s.queries.ReorderCourseCategories(ctx, dbgen.ReorderCourseCategoriesParams{
|
||||
Ids: ids,
|
||||
Positions: positions,
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -158,6 +158,13 @@ func mapCourse(row dbgen.Course) domain.Course {
|
|||
}
|
||||
}
|
||||
|
||||
func (s *Store) ReorderCourses(ctx context.Context, ids []int64, positions []int32) error {
|
||||
return s.queries.ReorderCourses(ctx, dbgen.ReorderCoursesParams{
|
||||
Ids: ids,
|
||||
Positions: positions,
|
||||
})
|
||||
}
|
||||
|
||||
func ptrText(t pgtype.Text) *string {
|
||||
if t.Valid {
|
||||
return &t.String
|
||||
|
|
|
|||
|
|
@ -809,3 +809,10 @@ func (s *Store) GetUserPersonasByQuestionSetID(ctx context.Context, questionSetI
|
|||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (s *Store) ReorderQuestionSets(ctx context.Context, ids []int64, positions []int32) error {
|
||||
return s.queries.ReorderQuestionSets(ctx, dbgen.ReorderQuestionSetsParams{
|
||||
Ids: ids,
|
||||
Positions: positions,
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -283,3 +283,10 @@ func (s *Store) GetVideoByVimeoID(ctx context.Context, vimeoID string) (domain.S
|
|||
}
|
||||
return mapSubCourseVideoRow(row), nil
|
||||
}
|
||||
|
||||
func (s *Store) ReorderSubCourseVideos(ctx context.Context, ids []int64, positions []int32) error {
|
||||
return s.queries.ReorderSubCourseVideos(ctx, dbgen.ReorderSubCourseVideosParams{
|
||||
Ids: ids,
|
||||
Positions: positions,
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -233,4 +233,11 @@ func (s *Store) DeleteSubCourse(
|
|||
}, nil
|
||||
}
|
||||
|
||||
func (s *Store) ReorderSubCourses(ctx context.Context, ids []int64, positions []int32) error {
|
||||
return s.queries.ReorderSubCourses(ctx, dbgen.ReorderSubCoursesParams{
|
||||
Ids: ids,
|
||||
Positions: positions,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -12,3 +12,23 @@ func (s *Service) GetFullLearningTree(ctx context.Context) ([]domain.TreeCourse,
|
|||
func (s *Service) GetCourseLearningPath(ctx context.Context, courseID int64) (domain.LearningPath, error) {
|
||||
return s.courseStore.GetCourseLearningPath(ctx, courseID)
|
||||
}
|
||||
|
||||
func (s *Service) ReorderCourseCategories(ctx context.Context, ids []int64, positions []int32) error {
|
||||
return s.courseStore.ReorderCourseCategories(ctx, ids, positions)
|
||||
}
|
||||
|
||||
func (s *Service) ReorderCourses(ctx context.Context, ids []int64, positions []int32) error {
|
||||
return s.courseStore.ReorderCourses(ctx, ids, positions)
|
||||
}
|
||||
|
||||
func (s *Service) ReorderSubCourses(ctx context.Context, ids []int64, positions []int32) error {
|
||||
return s.courseStore.ReorderSubCourses(ctx, ids, positions)
|
||||
}
|
||||
|
||||
func (s *Service) ReorderSubCourseVideos(ctx context.Context, ids []int64, positions []int32) error {
|
||||
return s.courseStore.ReorderSubCourseVideos(ctx, ids, positions)
|
||||
}
|
||||
|
||||
func (s *Service) ReorderQuestionSets(ctx context.Context, ids []int64, positions []int32) error {
|
||||
return s.courseStore.ReorderQuestionSets(ctx, ids, positions)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ var AllPermissions = []domain.PermissionSeed{
|
|||
{Key: "course_categories.get", Name: "Get Course Category", Description: "Get a course category by ID", GroupName: "Course Categories"},
|
||||
{Key: "course_categories.update", Name: "Update Course Category", Description: "Update a course category", GroupName: "Course Categories"},
|
||||
{Key: "course_categories.delete", Name: "Delete Course Category", Description: "Delete a course category", GroupName: "Course Categories"},
|
||||
{Key: "course_categories.reorder", Name: "Reorder Course Categories", Description: "Reorder course categories", GroupName: "Course Categories"},
|
||||
|
||||
// Course Management - Courses
|
||||
{Key: "courses.create", Name: "Create Course", Description: "Create a new course", GroupName: "Courses"},
|
||||
|
|
@ -17,6 +18,7 @@ var AllPermissions = []domain.PermissionSeed{
|
|||
{Key: "courses.update", Name: "Update Course", Description: "Update a course", GroupName: "Courses"},
|
||||
{Key: "courses.upload_thumbnail", Name: "Upload Course Thumbnail", Description: "Upload course thumbnail image", GroupName: "Courses"},
|
||||
{Key: "courses.delete", Name: "Delete Course", Description: "Delete a course", GroupName: "Courses"},
|
||||
{Key: "courses.reorder", Name: "Reorder Courses", Description: "Reorder courses", GroupName: "Courses"},
|
||||
|
||||
// Course Management - Sub-courses
|
||||
{Key: "subcourses.create", Name: "Create Sub-course", Description: "Create a new sub-course", GroupName: "Sub-courses"},
|
||||
|
|
@ -28,6 +30,7 @@ var AllPermissions = []domain.PermissionSeed{
|
|||
{Key: "subcourses.upload_thumbnail", Name: "Upload Sub-course Thumbnail", Description: "Upload sub-course thumbnail", GroupName: "Sub-courses"},
|
||||
{Key: "subcourses.deactivate", Name: "Deactivate Sub-course", Description: "Deactivate a sub-course", GroupName: "Sub-courses"},
|
||||
{Key: "subcourses.delete", Name: "Delete Sub-course", Description: "Delete a sub-course", GroupName: "Sub-courses"},
|
||||
{Key: "subcourses.reorder", Name: "Reorder Sub-courses", Description: "Reorder sub-courses", GroupName: "Sub-courses"},
|
||||
|
||||
// Course Management - Videos
|
||||
{Key: "videos.create", Name: "Create Video", Description: "Create a sub-course video", GroupName: "Videos"},
|
||||
|
|
@ -40,9 +43,11 @@ var AllPermissions = []domain.PermissionSeed{
|
|||
{Key: "videos.publish", Name: "Publish Video", Description: "Publish a video", GroupName: "Videos"},
|
||||
{Key: "videos.update", Name: "Update Video", Description: "Update a video", GroupName: "Videos"},
|
||||
{Key: "videos.delete", Name: "Delete Video", Description: "Delete a video", GroupName: "Videos"},
|
||||
{Key: "videos.reorder", Name: "Reorder Videos", Description: "Reorder videos", GroupName: "Videos"},
|
||||
|
||||
// Learning Tree
|
||||
{Key: "learning_tree.get", Name: "Get Learning Tree", Description: "Get full learning tree", GroupName: "Learning Tree"},
|
||||
{Key: "practices.reorder", Name: "Reorder Practices", Description: "Reorder practices", GroupName: "Learning Tree"},
|
||||
|
||||
// Questions
|
||||
{Key: "questions.create", Name: "Create Question", Description: "Create a new question", GroupName: "Questions"},
|
||||
|
|
@ -222,13 +227,13 @@ var AllPermissions = []domain.PermissionSeed{
|
|||
var DefaultRolePermissions = map[string][]string{
|
||||
"ADMIN": {
|
||||
// Course Management (full access)
|
||||
"course_categories.create", "course_categories.list", "course_categories.get", "course_categories.update", "course_categories.delete",
|
||||
"courses.create", "courses.get", "courses.list_by_category", "courses.update", "courses.upload_thumbnail", "courses.delete",
|
||||
"course_categories.create", "course_categories.list", "course_categories.get", "course_categories.update", "course_categories.delete", "course_categories.reorder",
|
||||
"courses.create", "courses.get", "courses.list_by_category", "courses.update", "courses.upload_thumbnail", "courses.delete", "courses.reorder",
|
||||
"subcourses.create", "subcourses.get", "subcourses.list_by_course", "subcourses.list_by_course_list", "subcourses.list_active",
|
||||
"subcourses.update", "subcourses.upload_thumbnail", "subcourses.deactivate", "subcourses.delete",
|
||||
"subcourses.update", "subcourses.upload_thumbnail", "subcourses.deactivate", "subcourses.delete", "subcourses.reorder",
|
||||
"videos.create", "videos.create_vimeo", "videos.upload", "videos.import_vimeo", "videos.get",
|
||||
"videos.list_by_subcourse", "videos.list_published", "videos.publish", "videos.update", "videos.delete",
|
||||
"learning_tree.get",
|
||||
"videos.list_by_subcourse", "videos.list_published", "videos.publish", "videos.update", "videos.delete", "videos.reorder",
|
||||
"learning_tree.get", "practices.reorder",
|
||||
|
||||
// Questions (full access)
|
||||
"questions.create", "questions.list", "questions.search", "questions.get", "questions.update", "questions.delete",
|
||||
|
|
|
|||
|
|
@ -1455,6 +1455,257 @@ func (h *Handler) GetCourseLearningPath(c *fiber.Ctx) error {
|
|||
})
|
||||
}
|
||||
|
||||
// Reorder Handlers — support drag-and-drop ordering from admin panel
|
||||
|
||||
type reorderItem struct {
|
||||
ID int64 `json:"id" validate:"required"`
|
||||
Position int32 `json:"position"`
|
||||
}
|
||||
|
||||
type reorderReq struct {
|
||||
Items []reorderItem `json:"items" validate:"required,min=1"`
|
||||
}
|
||||
|
||||
func parseReorderItems(items []reorderItem) ([]int64, []int32) {
|
||||
ids := make([]int64, len(items))
|
||||
positions := make([]int32, len(items))
|
||||
for i, item := range items {
|
||||
ids[i] = item.ID
|
||||
positions[i] = item.Position
|
||||
}
|
||||
return ids, positions
|
||||
}
|
||||
|
||||
// ReorderCourseCategories godoc
|
||||
// @Summary Reorder course categories
|
||||
// @Description Updates the display_order of course categories for drag-and-drop sorting
|
||||
// @Tags course-categories
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param body body reorderReq true "Reorder payload"
|
||||
// @Success 200 {object} domain.Response
|
||||
// @Failure 400 {object} domain.ErrorResponse
|
||||
// @Failure 500 {object} domain.ErrorResponse
|
||||
// @Router /api/v1/course-management/categories/reorder [put]
|
||||
func (h *Handler) ReorderCourseCategories(c *fiber.Ctx) error {
|
||||
var req reorderReq
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||
Message: "Invalid request body",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
if len(req.Items) == 0 {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||
Message: "Items array is required",
|
||||
Error: "items must not be empty",
|
||||
})
|
||||
}
|
||||
|
||||
ids, positions := parseReorderItems(req.Items)
|
||||
if err := h.courseMgmtSvc.ReorderCourseCategories(c.Context(), ids, positions); err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||
Message: "Failed to reorder course categories",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
actorID := c.Locals("user_id").(int64)
|
||||
actorRole := string(c.Locals("role").(domain.Role))
|
||||
ip := c.IP()
|
||||
ua := c.Get("User-Agent")
|
||||
meta, _ := json.Marshal(map[string]interface{}{"ids": ids, "positions": positions})
|
||||
go h.activityLogSvc.RecordAction(context.Background(), &actorID, &actorRole, domain.ActionCategoryUpdated, domain.ResourceCategory, nil, "Reordered course categories", meta, &ip, &ua)
|
||||
|
||||
return c.JSON(domain.Response{
|
||||
Message: "Course categories reordered successfully",
|
||||
})
|
||||
}
|
||||
|
||||
// ReorderCourses godoc
|
||||
// @Summary Reorder courses within a category
|
||||
// @Description Updates the display_order of courses for drag-and-drop sorting
|
||||
// @Tags courses
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param body body reorderReq true "Reorder payload"
|
||||
// @Success 200 {object} domain.Response
|
||||
// @Failure 400 {object} domain.ErrorResponse
|
||||
// @Failure 500 {object} domain.ErrorResponse
|
||||
// @Router /api/v1/course-management/courses/reorder [put]
|
||||
func (h *Handler) ReorderCourses(c *fiber.Ctx) error {
|
||||
var req reorderReq
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||
Message: "Invalid request body",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
if len(req.Items) == 0 {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||
Message: "Items array is required",
|
||||
Error: "items must not be empty",
|
||||
})
|
||||
}
|
||||
|
||||
ids, positions := parseReorderItems(req.Items)
|
||||
if err := h.courseMgmtSvc.ReorderCourses(c.Context(), ids, positions); err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||
Message: "Failed to reorder courses",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
actorID := c.Locals("user_id").(int64)
|
||||
actorRole := string(c.Locals("role").(domain.Role))
|
||||
ip := c.IP()
|
||||
ua := c.Get("User-Agent")
|
||||
meta, _ := json.Marshal(map[string]interface{}{"ids": ids, "positions": positions})
|
||||
go h.activityLogSvc.RecordAction(context.Background(), &actorID, &actorRole, domain.ActionCourseUpdated, domain.ResourceCourse, nil, "Reordered courses", meta, &ip, &ua)
|
||||
|
||||
return c.JSON(domain.Response{
|
||||
Message: "Courses reordered successfully",
|
||||
})
|
||||
}
|
||||
|
||||
// ReorderSubCourses godoc
|
||||
// @Summary Reorder sub-courses within a course
|
||||
// @Description Updates the display_order of sub-courses for drag-and-drop sorting
|
||||
// @Tags sub-courses
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param body body reorderReq true "Reorder payload"
|
||||
// @Success 200 {object} domain.Response
|
||||
// @Failure 400 {object} domain.ErrorResponse
|
||||
// @Failure 500 {object} domain.ErrorResponse
|
||||
// @Router /api/v1/course-management/sub-courses/reorder [put]
|
||||
func (h *Handler) ReorderSubCourses(c *fiber.Ctx) error {
|
||||
var req reorderReq
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||
Message: "Invalid request body",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
if len(req.Items) == 0 {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||
Message: "Items array is required",
|
||||
Error: "items must not be empty",
|
||||
})
|
||||
}
|
||||
|
||||
ids, positions := parseReorderItems(req.Items)
|
||||
if err := h.courseMgmtSvc.ReorderSubCourses(c.Context(), ids, positions); err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||
Message: "Failed to reorder sub-courses",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
actorID := c.Locals("user_id").(int64)
|
||||
actorRole := string(c.Locals("role").(domain.Role))
|
||||
ip := c.IP()
|
||||
ua := c.Get("User-Agent")
|
||||
meta, _ := json.Marshal(map[string]interface{}{"ids": ids, "positions": positions})
|
||||
go h.activityLogSvc.RecordAction(context.Background(), &actorID, &actorRole, domain.ActionSubCourseUpdated, domain.ResourceSubCourse, nil, "Reordered sub-courses", meta, &ip, &ua)
|
||||
|
||||
return c.JSON(domain.Response{
|
||||
Message: "Sub-courses reordered successfully",
|
||||
})
|
||||
}
|
||||
|
||||
// ReorderSubCourseVideos godoc
|
||||
// @Summary Reorder videos within a sub-course
|
||||
// @Description Updates the display_order of videos for drag-and-drop sorting
|
||||
// @Tags sub-course-videos
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param body body reorderReq true "Reorder payload"
|
||||
// @Success 200 {object} domain.Response
|
||||
// @Failure 400 {object} domain.ErrorResponse
|
||||
// @Failure 500 {object} domain.ErrorResponse
|
||||
// @Router /api/v1/course-management/videos/reorder [put]
|
||||
func (h *Handler) ReorderSubCourseVideos(c *fiber.Ctx) error {
|
||||
var req reorderReq
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||
Message: "Invalid request body",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
if len(req.Items) == 0 {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||
Message: "Items array is required",
|
||||
Error: "items must not be empty",
|
||||
})
|
||||
}
|
||||
|
||||
ids, positions := parseReorderItems(req.Items)
|
||||
if err := h.courseMgmtSvc.ReorderSubCourseVideos(c.Context(), ids, positions); err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||
Message: "Failed to reorder videos",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
actorID := c.Locals("user_id").(int64)
|
||||
actorRole := string(c.Locals("role").(domain.Role))
|
||||
ip := c.IP()
|
||||
ua := c.Get("User-Agent")
|
||||
meta, _ := json.Marshal(map[string]interface{}{"ids": ids, "positions": positions})
|
||||
go h.activityLogSvc.RecordAction(context.Background(), &actorID, &actorRole, domain.ActionVideoUpdated, domain.ResourceVideo, nil, "Reordered sub-course videos", meta, &ip, &ua)
|
||||
|
||||
return c.JSON(domain.Response{
|
||||
Message: "Videos reordered successfully",
|
||||
})
|
||||
}
|
||||
|
||||
// ReorderPractices godoc
|
||||
// @Summary Reorder practices (question sets) within a sub-course
|
||||
// @Description Updates the display_order of practices for drag-and-drop sorting
|
||||
// @Tags question-sets
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param body body reorderReq true "Reorder payload"
|
||||
// @Success 200 {object} domain.Response
|
||||
// @Failure 400 {object} domain.ErrorResponse
|
||||
// @Failure 500 {object} domain.ErrorResponse
|
||||
// @Router /api/v1/course-management/practices/reorder [put]
|
||||
func (h *Handler) ReorderPractices(c *fiber.Ctx) error {
|
||||
var req reorderReq
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||
Message: "Invalid request body",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
if len(req.Items) == 0 {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||
Message: "Items array is required",
|
||||
Error: "items must not be empty",
|
||||
})
|
||||
}
|
||||
|
||||
ids, positions := parseReorderItems(req.Items)
|
||||
if err := h.courseMgmtSvc.ReorderQuestionSets(c.Context(), ids, positions); err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||
Message: "Failed to reorder practices",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
actorID := c.Locals("user_id").(int64)
|
||||
actorRole := string(c.Locals("role").(domain.Role))
|
||||
ip := c.IP()
|
||||
ua := c.Get("User-Agent")
|
||||
meta, _ := json.Marshal(map[string]interface{}{"ids": ids, "positions": positions})
|
||||
go h.activityLogSvc.RecordAction(context.Background(), &actorID, &actorRole, domain.ActionQuestionSetUpdated, domain.ResourceQuestionSet, nil, "Reordered practices", meta, &ip, &ua)
|
||||
|
||||
return c.JSON(domain.Response{
|
||||
Message: "Practices reordered successfully",
|
||||
})
|
||||
}
|
||||
|
||||
// UploadSubCourseVideo godoc
|
||||
// @Summary Upload a video file and create sub-course video
|
||||
// @Description Accepts a video file upload, uploads it to Vimeo via TUS, and creates a sub-course video record
|
||||
|
|
|
|||
|
|
@ -71,6 +71,15 @@ func (a *App) initAppRoutes() {
|
|||
groupV1.Get("/assessment/questions", h.ListAssessmentQuestions)
|
||||
groupV1.Get("/assessment/questions/:id", h.GetAssessmentQuestionByID)
|
||||
|
||||
// Reorder (drag-and-drop support)
|
||||
// Keep static reorder routes before dynamic `/:id` routes to avoid route collisions
|
||||
// (e.g., `/courses/reorder` being parsed as `/courses/:id`).
|
||||
groupV1.Put("/course-management/categories/reorder", a.authMiddleware, a.RequirePermission("course_categories.reorder"), h.ReorderCourseCategories)
|
||||
groupV1.Put("/course-management/courses/reorder", a.authMiddleware, a.RequirePermission("courses.reorder"), h.ReorderCourses)
|
||||
groupV1.Put("/course-management/sub-courses/reorder", a.authMiddleware, a.RequirePermission("subcourses.reorder"), h.ReorderSubCourses)
|
||||
groupV1.Put("/course-management/videos/reorder", a.authMiddleware, a.RequirePermission("videos.reorder"), h.ReorderSubCourseVideos)
|
||||
groupV1.Put("/course-management/practices/reorder", a.authMiddleware, a.RequirePermission("practices.reorder"), h.ReorderPractices)
|
||||
|
||||
// Course Categories
|
||||
groupV1.Post("/course-management/categories", a.authMiddleware, a.RequirePermission("course_categories.create"), h.CreateCourseCategory)
|
||||
groupV1.Get("/course-management/categories", a.authMiddleware, a.RequirePermission("course_categories.list"), h.GetAllCourseCategories)
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user