added more structure to lessons

This commit is contained in:
Yared Yemane 2026-04-17 08:27:40 -07:00
parent 1026354c24
commit 518c3ee751
10 changed files with 2854 additions and 445 deletions

View File

@ -0,0 +1,18 @@
-- Restores legacy lesson columns. Rows will have NULL question_set_id until repopulated.
ALTER TABLE sub_module_lessons
ADD COLUMN IF NOT EXISTS question_set_id BIGINT REFERENCES question_sets(id) ON DELETE CASCADE,
ADD COLUMN IF NOT EXISTS intro_video_url TEXT;
UPDATE sub_module_lessons
SET intro_video_url = teaching_video_url
WHERE teaching_video_url IS NOT NULL;
ALTER TABLE sub_module_lessons
DROP COLUMN IF EXISTS title,
DROP COLUMN IF EXISTS description,
DROP COLUMN IF EXISTS thumbnail,
DROP COLUMN IF EXISTS teaching_text,
DROP COLUMN IF EXISTS teaching_image_url,
DROP COLUMN IF EXISTS teaching_audio_url,
DROP COLUMN IF EXISTS teaching_video_url;

View File

@ -0,0 +1,37 @@
-- Lessons are teaching content only (text, images, audio, video, thumbnail).
-- Question sets remain linked to practices, not lessons.
ALTER TABLE sub_module_lessons
ADD COLUMN IF NOT EXISTS title VARCHAR(255),
ADD COLUMN IF NOT EXISTS description TEXT,
ADD COLUMN IF NOT EXISTS thumbnail TEXT,
ADD COLUMN IF NOT EXISTS teaching_text TEXT,
ADD COLUMN IF NOT EXISTS teaching_image_url TEXT,
ADD COLUMN IF NOT EXISTS teaching_audio_url TEXT,
ADD COLUMN IF NOT EXISTS teaching_video_url TEXT;
UPDATE sub_module_lessons sml
SET
title = qs.title,
description = qs.description
FROM question_sets qs
WHERE sml.question_set_id IS NOT NULL
AND qs.id = sml.question_set_id;
UPDATE sub_module_lessons
SET title = 'Lesson'
WHERE title IS NULL OR trim(title) = '';
UPDATE sub_module_lessons
SET teaching_video_url = intro_video_url
WHERE intro_video_url IS NOT NULL;
ALTER TABLE sub_module_lessons DROP CONSTRAINT IF EXISTS sub_module_lessons_question_set_id_fkey;
ALTER TABLE sub_module_lessons DROP CONSTRAINT IF EXISTS sub_module_lessons_question_set_id_key;
ALTER TABLE sub_module_lessons DROP COLUMN IF EXISTS question_set_id;
ALTER TABLE sub_module_lessons DROP COLUMN IF EXISTS intro_video_url;
ALTER TABLE sub_module_lessons
ALTER COLUMN title SET NOT NULL,
ALTER COLUMN title SET DEFAULT 'Lesson';

View File

@ -83,43 +83,17 @@ WHERE sub_module_id = $1
ORDER BY display_order ASC, id ASC; ORDER BY display_order ASC, id ASC;
-- name: GetSubModuleLessons :many -- name: GetSubModuleLessons :many
SELECT SELECT *
smp.id, FROM sub_module_lessons
smp.sub_module_id, WHERE sub_module_id = $1
smp.question_set_id, AND is_active = TRUE
smp.intro_video_url, ORDER BY display_order ASC, id ASC;
smp.display_order,
smp.is_active,
qs.title,
qs.description,
qs.status,
qs.set_type,
(SELECT COUNT(*) FROM question_set_items qsi WHERE qsi.set_id = qs.id) AS question_count
FROM sub_module_lessons smp
JOIN question_sets qs ON qs.id = smp.question_set_id
WHERE smp.sub_module_id = $1
AND smp.is_active = TRUE
AND qs.set_type = 'QUIZ'
ORDER BY smp.display_order ASC, smp.id ASC;
-- name: GetSubModuleLessonByID :one -- name: GetSubModuleLessonByID :one
SELECT SELECT *
smp.id, FROM sub_module_lessons
smp.sub_module_id, WHERE id = $1
smp.question_set_id, AND is_active = TRUE;
smp.intro_video_url,
smp.display_order,
smp.is_active,
qs.title,
qs.description,
qs.status,
qs.set_type,
(SELECT COUNT(*) FROM question_set_items qsi WHERE qsi.set_id = qs.id) AS question_count
FROM sub_module_lessons 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 = 'QUIZ';
-- name: GetSubModulePractices :many -- name: GetSubModulePractices :many
SELECT SELECT
@ -289,26 +263,47 @@ VALUES (
) )
RETURNING *; RETURNING *;
-- name: AttachQuestionSetLessonToSubModule :one -- name: CreateSubModuleLesson :one
INSERT INTO sub_module_lessons ( INSERT INTO sub_module_lessons (
sub_module_id, sub_module_id,
question_set_id, title,
intro_video_url, description,
thumbnail,
teaching_text,
teaching_image_url,
teaching_audio_url,
teaching_video_url,
display_order, display_order,
is_active is_active
) )
VALUES ($1, $2, $3, COALESCE($4, 0), COALESCE($5, TRUE)) VALUES (
$1,
$2,
$3,
$4,
$5,
$6,
$7,
$8,
COALESCE($9, 0),
COALESCE($10, TRUE)
)
RETURNING *; RETURNING *;
-- name: UpdateSubModuleLesson :one -- name: UpdateSubModuleLesson :one
UPDATE sub_module_lessons UPDATE sub_module_lessons
SET SET
sub_module_id = $1, sub_module_id = $1,
question_set_id = $2, title = $2,
intro_video_url = $3, description = $3,
display_order = $4, thumbnail = $4,
is_active = $5 teaching_text = $5,
WHERE id = $6 teaching_image_url = $6,
teaching_audio_url = $7,
teaching_video_url = $8,
display_order = $9,
is_active = $10
WHERE id = $11
RETURNING *; RETURNING *;
-- name: CreateSubModulePractice :one -- name: CreateSubModulePractice :one

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -911,19 +911,6 @@ definitions:
required: required:
- user_id - user_id
type: object type: object
handlers.attachSubModuleLessonReq:
properties:
display_order:
type: integer
intro_video_url:
type: string
is_active:
type: boolean
question_set_id:
type: integer
sub_module_id:
type: integer
type: object
handlers.autoRenewReq: handlers.autoRenewReq:
properties: properties:
auto_renew: auto_renew:
@ -1133,6 +1120,29 @@ definitions:
- set_type - set_type
- title - title
type: object type: object
handlers.createSubModuleLessonReq:
properties:
description:
type: string
display_order:
type: integer
is_active:
type: boolean
sub_module_id:
type: integer
teaching_audio_url:
type: string
teaching_image_url:
type: string
teaching_text:
type: string
teaching_video_url:
type: string
thumbnail:
type: string
title:
type: string
type: object
handlers.createSubModulePracticeReq: handlers.createSubModulePracticeReq:
properties: properties:
description: description:
@ -1488,6 +1498,29 @@ definitions:
title: title:
type: string type: string
type: object type: object
handlers.updateSubModuleLessonReq:
properties:
description:
type: string
display_order:
type: integer
is_active:
type: boolean
sub_module_id:
type: integer
teaching_audio_url:
type: string
teaching_image_url:
type: string
teaching_text:
type: string
teaching_video_url:
type: string
thumbnail:
type: string
title:
type: string
type: object
handlers.verifyOTPReq: handlers.verifyOTPReq:
properties: properties:
otp: otp:
@ -2447,6 +2480,31 @@ paths:
tags: tags:
- course-management - course-management
/api/v1/course-management/courses: /api/v1/course-management/courses:
get:
description: Returns all courses with pagination
parameters:
- description: Offset
in: query
name: offset
type: integer
- description: Limit
in: query
name: limit
type: integer
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/domain.Response'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/domain.ErrorResponse'
summary: List all courses
tags:
- course-management
post: post:
consumes: consumes:
- application/json - application/json
@ -2503,6 +2561,36 @@ paths:
summary: Delete course summary: Delete course
tags: tags:
- course-management - course-management
get:
description: Returns one course by ID
parameters:
- description: Course ID
in: path
name: courseId
required: true
type: integer
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/domain.Response'
"400":
description: Bad Request
schema:
$ref: '#/definitions/domain.ErrorResponse'
"404":
description: Not Found
schema:
$ref: '#/definitions/domain.ErrorResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/domain.ErrorResponse'
summary: Get course detail
tags:
- course-management
put: put:
consumes: consumes:
- application/json - application/json
@ -2591,6 +2679,33 @@ paths:
summary: Get course learning path summary: Get course learning path
tags: tags:
- course-management - course-management
/api/v1/course-management/courses/{courseId}/levels:
get:
description: Returns all active levels for one course
parameters:
- description: Course ID
in: path
name: courseId
required: true
type: integer
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/domain.Response'
"400":
description: Bad Request
schema:
$ref: '#/definitions/domain.ErrorResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/domain.ErrorResponse'
summary: List levels by course
tags:
- course-management
/api/v1/course-management/courses/{courseId}/thumbnail: /api/v1/course-management/courses/{courseId}/thumbnail:
post: post:
consumes: consumes:
@ -2643,7 +2758,84 @@ paths:
summary: Get unified course hierarchy summary: Get unified course hierarchy
tags: tags:
- course-management - course-management
/api/v1/course-management/human-language/courses:
get:
description: Returns all courses under Human Language category
parameters:
- description: Offset
in: query
name: offset
type: integer
- description: Limit
in: query
name: limit
type: integer
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/domain.Response'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/domain.ErrorResponse'
summary: List Human Language courses
tags:
- course-management
/api/v1/course-management/human-language/sub-categories:
get:
description: Returns active sub-categories under Human Language category
parameters:
- description: Offset
in: query
name: offset
type: integer
- description: Limit
in: query
name: limit
type: integer
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/domain.Response'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/domain.ErrorResponse'
summary: List Human Language sub-categories
tags:
- course-management
/api/v1/course-management/levels: /api/v1/course-management/levels:
get:
description: Returns all levels with pagination
parameters:
- description: Offset
in: query
name: offset
type: integer
- description: Limit
in: query
name: limit
type: integer
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/domain.Response'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/domain.ErrorResponse'
summary: List all levels
tags:
- course-management
post: post:
consumes: consumes:
- application/json - application/json
@ -2673,7 +2865,90 @@ paths:
summary: Create level summary: Create level
tags: tags:
- course-management - course-management
/api/v1/course-management/levels/{levelId}:
get:
description: Returns one level by ID
parameters:
- description: Level ID
in: path
name: levelId
required: true
type: integer
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/domain.Response'
"400":
description: Bad Request
schema:
$ref: '#/definitions/domain.ErrorResponse'
"404":
description: Not Found
schema:
$ref: '#/definitions/domain.ErrorResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/domain.ErrorResponse'
summary: Get level detail
tags:
- course-management
/api/v1/course-management/levels/{levelId}/modules:
get:
description: Returns all active modules for one level
parameters:
- description: Level ID
in: path
name: levelId
required: true
type: integer
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/domain.Response'
"400":
description: Bad Request
schema:
$ref: '#/definitions/domain.ErrorResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/domain.ErrorResponse'
summary: List modules by level
tags:
- course-management
/api/v1/course-management/modules: /api/v1/course-management/modules:
get:
description: Returns all modules with pagination
parameters:
- description: Offset
in: query
name: offset
type: integer
- description: Limit
in: query
name: limit
type: integer
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/domain.Response'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/domain.ErrorResponse'
summary: List all modules
tags:
- course-management
post: post:
consumes: consumes:
- application/json - application/json
@ -2703,7 +2978,123 @@ paths:
summary: Create module summary: Create module
tags: tags:
- course-management - course-management
/api/v1/course-management/modules/{moduleId}:
get:
description: Returns one module by ID
parameters:
- description: Module ID
in: path
name: moduleId
required: true
type: integer
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/domain.Response'
"400":
description: Bad Request
schema:
$ref: '#/definitions/domain.ErrorResponse'
"404":
description: Not Found
schema:
$ref: '#/definitions/domain.ErrorResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/domain.ErrorResponse'
summary: Get module detail
tags:
- course-management
/api/v1/course-management/modules/{moduleId}/sub-modules:
get:
description: Returns all active sub-modules for one module
parameters:
- description: Module ID
in: path
name: moduleId
required: true
type: integer
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/domain.Response'
"400":
description: Bad Request
schema:
$ref: '#/definitions/domain.ErrorResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/domain.ErrorResponse'
summary: List sub-modules by module
tags:
- course-management
/api/v1/course-management/practices/{practiceId}:
get:
consumes:
- application/json
description: Returns one active practice by practice ID
parameters:
- description: Practice ID
in: path
name: practiceId
required: true
type: integer
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/domain.Response'
"400":
description: Bad Request
schema:
$ref: '#/definitions/domain.ErrorResponse'
"404":
description: Not Found
schema:
$ref: '#/definitions/domain.ErrorResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/domain.ErrorResponse'
summary: Get practice detail
tags:
- course-management
/api/v1/course-management/sub-categories: /api/v1/course-management/sub-categories:
get:
description: Returns all active course sub-categories
parameters:
- description: Offset
in: query
name: offset
type: integer
- description: Limit
in: query
name: limit
type: integer
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/domain.Response'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/domain.ErrorResponse'
summary: List course sub-categories
tags:
- course-management
post: post:
consumes: consumes:
- application/json - application/json
@ -2733,18 +3124,54 @@ paths:
summary: Create course sub-category summary: Create course sub-category
tags: tags:
- course-management - course-management
/api/v1/course-management/sub-categories/{subCategoryId}/courses:
get:
description: Returns courses for one sub-category
parameters:
- description: Sub-category ID
in: path
name: subCategoryId
required: true
type: integer
- description: Offset
in: query
name: offset
type: integer
- description: Limit
in: query
name: limit
type: integer
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/domain.Response'
"400":
description: Bad Request
schema:
$ref: '#/definitions/domain.ErrorResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/domain.ErrorResponse'
summary: List courses by sub-category
tags:
- course-management
/api/v1/course-management/sub-module-lessons: /api/v1/course-management/sub-module-lessons:
post: post:
consumes: consumes:
- application/json - application/json
description: Links a question set lesson to a sub-module description: Creates a sub-module lesson with teaching content (text, image,
audio, video URLs) and optional thumbnail
parameters: parameters:
- description: Attach lesson payload - description: Create lesson payload
in: body in: body
name: body name: body
required: true required: true
schema: schema:
$ref: '#/definitions/handlers.attachSubModuleLessonReq' $ref: '#/definitions/handlers.createSubModuleLessonReq'
produces: produces:
- application/json - application/json
responses: responses:
@ -2760,7 +3187,79 @@ paths:
description: Internal Server Error description: Internal Server Error
schema: schema:
$ref: '#/definitions/domain.ErrorResponse' $ref: '#/definitions/domain.ErrorResponse'
summary: Attach lesson to sub-module summary: Create lesson under sub-module
tags:
- course-management
/api/v1/course-management/sub-module-lessons/{lessonId}:
get:
consumes:
- application/json
description: Returns one active lesson detail by lesson ID
parameters:
- description: Lesson ID
in: path
name: lessonId
required: true
type: integer
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/domain.Response'
"400":
description: Bad Request
schema:
$ref: '#/definitions/domain.ErrorResponse'
"404":
description: Not Found
schema:
$ref: '#/definitions/domain.ErrorResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/domain.ErrorResponse'
summary: Get lesson detail
tags:
- course-management
put:
consumes:
- application/json
description: Updates lesson teaching content, thumbnail, ordering, and active
flag
parameters:
- description: Lesson ID
in: path
name: lessonId
required: true
type: integer
- description: Update lesson payload
in: body
name: body
required: true
schema:
$ref: '#/definitions/handlers.updateSubModuleLessonReq'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/domain.Response'
"400":
description: Bad Request
schema:
$ref: '#/definitions/domain.ErrorResponse'
"404":
description: Not Found
schema:
$ref: '#/definitions/domain.ErrorResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/domain.ErrorResponse'
summary: Update lesson detail
tags: tags:
- course-management - course-management
/api/v1/course-management/sub-module-practices: /api/v1/course-management/sub-module-practices:
@ -2825,6 +3324,31 @@ paths:
tags: tags:
- course-management - course-management
/api/v1/course-management/sub-modules: /api/v1/course-management/sub-modules:
get:
description: Returns all sub-modules with pagination
parameters:
- description: Offset
in: query
name: offset
type: integer
- description: Limit
in: query
name: limit
type: integer
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/domain.Response'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/domain.ErrorResponse'
summary: List all sub-modules
tags:
- course-management
post: post:
consumes: consumes:
- application/json - application/json
@ -2854,6 +3378,95 @@ paths:
summary: Create sub-module summary: Create sub-module
tags: tags:
- course-management - course-management
/api/v1/course-management/sub-modules/{subModuleId}:
get:
description: Returns one sub-module by ID
parameters:
- description: Sub-module ID
in: path
name: subModuleId
required: true
type: integer
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/domain.Response'
"400":
description: Bad Request
schema:
$ref: '#/definitions/domain.ErrorResponse'
"404":
description: Not Found
schema:
$ref: '#/definitions/domain.ErrorResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/domain.ErrorResponse'
summary: Get sub-module detail
tags:
- course-management
/api/v1/course-management/sub-modules/{subModuleId}/lessons:
get:
consumes:
- application/json
description: Returns all active lessons for a sub-module (teaching content metadata)
parameters:
- description: Sub-module ID
in: path
name: subModuleId
required: true
type: integer
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/domain.Response'
"400":
description: Bad Request
schema:
$ref: '#/definitions/domain.ErrorResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/domain.ErrorResponse'
summary: Get lessons under sub-module
tags:
- course-management
/api/v1/course-management/sub-modules/{subModuleId}/practices:
get:
consumes:
- application/json
description: Returns all active practices attached to a sub-module
parameters:
- description: Sub-module ID
in: path
name: subModuleId
required: true
type: integer
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/domain.Response'
"400":
description: Bad Request
schema:
$ref: '#/definitions/domain.ErrorResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/domain.ErrorResponse'
summary: Get practices under sub-module
tags:
- course-management
/api/v1/files/audio: /api/v1/files/audio:
post: post:
consumes: consumes:

View File

@ -11,47 +11,6 @@ import (
"github.com/jackc/pgx/v5/pgtype" "github.com/jackc/pgx/v5/pgtype"
) )
const AttachQuestionSetLessonToSubModule = `-- name: AttachQuestionSetLessonToSubModule :one
INSERT INTO sub_module_lessons (
sub_module_id,
question_set_id,
intro_video_url,
display_order,
is_active
)
VALUES ($1, $2, $3, COALESCE($4, 0), COALESCE($5, TRUE))
RETURNING id, sub_module_id, question_set_id, intro_video_url, display_order, is_active, created_at
`
type AttachQuestionSetLessonToSubModuleParams struct {
SubModuleID int64 `json:"sub_module_id"`
QuestionSetID int64 `json:"question_set_id"`
IntroVideoUrl pgtype.Text `json:"intro_video_url"`
Column4 interface{} `json:"column_4"`
Column5 interface{} `json:"column_5"`
}
func (q *Queries) AttachQuestionSetLessonToSubModule(ctx context.Context, arg AttachQuestionSetLessonToSubModuleParams) (SubModuleLesson, error) {
row := q.db.QueryRow(ctx, AttachQuestionSetLessonToSubModule,
arg.SubModuleID,
arg.QuestionSetID,
arg.IntroVideoUrl,
arg.Column4,
arg.Column5,
)
var i SubModuleLesson
err := row.Scan(
&i.ID,
&i.SubModuleID,
&i.QuestionSetID,
&i.IntroVideoUrl,
&i.DisplayOrder,
&i.IsActive,
&i.CreatedAt,
)
return i, err
}
const CreateCourseSubCategory = `-- name: CreateCourseSubCategory :one const CreateCourseSubCategory = `-- name: CreateCourseSubCategory :one
INSERT INTO course_sub_categories ( INSERT INTO course_sub_categories (
category_id, category_id,
@ -213,6 +172,78 @@ func (q *Queries) CreateSubModule(ctx context.Context, arg CreateSubModuleParams
return i, err return i, err
} }
const CreateSubModuleLesson = `-- name: CreateSubModuleLesson :one
INSERT INTO sub_module_lessons (
sub_module_id,
title,
description,
thumbnail,
teaching_text,
teaching_image_url,
teaching_audio_url,
teaching_video_url,
display_order,
is_active
)
VALUES (
$1,
$2,
$3,
$4,
$5,
$6,
$7,
$8,
COALESCE($9, 0),
COALESCE($10, TRUE)
)
RETURNING id, sub_module_id, display_order, is_active, created_at, title, description, thumbnail, teaching_text, teaching_image_url, teaching_audio_url, teaching_video_url
`
type CreateSubModuleLessonParams struct {
SubModuleID int64 `json:"sub_module_id"`
Title string `json:"title"`
Description pgtype.Text `json:"description"`
Thumbnail pgtype.Text `json:"thumbnail"`
TeachingText pgtype.Text `json:"teaching_text"`
TeachingImageUrl pgtype.Text `json:"teaching_image_url"`
TeachingAudioUrl pgtype.Text `json:"teaching_audio_url"`
TeachingVideoUrl pgtype.Text `json:"teaching_video_url"`
Column9 interface{} `json:"column_9"`
Column10 interface{} `json:"column_10"`
}
func (q *Queries) CreateSubModuleLesson(ctx context.Context, arg CreateSubModuleLessonParams) (SubModuleLesson, error) {
row := q.db.QueryRow(ctx, CreateSubModuleLesson,
arg.SubModuleID,
arg.Title,
arg.Description,
arg.Thumbnail,
arg.TeachingText,
arg.TeachingImageUrl,
arg.TeachingAudioUrl,
arg.TeachingVideoUrl,
arg.Column9,
arg.Column10,
)
var i SubModuleLesson
err := row.Scan(
&i.ID,
&i.SubModuleID,
&i.DisplayOrder,
&i.IsActive,
&i.CreatedAt,
&i.Title,
&i.Description,
&i.Thumbnail,
&i.TeachingText,
&i.TeachingImageUrl,
&i.TeachingAudioUrl,
&i.TeachingVideoUrl,
)
return i, err
}
const CreateSubModulePractice = `-- name: CreateSubModulePractice :one const CreateSubModulePractice = `-- name: CreateSubModulePractice :one
INSERT INTO sub_module_practices ( INSERT INTO sub_module_practices (
sub_module_id, sub_module_id,
@ -907,114 +938,62 @@ func (q *Queries) GetSubModuleByID(ctx context.Context, id int64) (SubModule, er
} }
const GetSubModuleLessonByID = `-- name: GetSubModuleLessonByID :one const GetSubModuleLessonByID = `-- name: GetSubModuleLessonByID :one
SELECT SELECT id, sub_module_id, display_order, is_active, created_at, title, description, thumbnail, teaching_text, teaching_image_url, teaching_audio_url, teaching_video_url
smp.id, FROM sub_module_lessons
smp.sub_module_id, WHERE id = $1
smp.question_set_id, AND is_active = TRUE
smp.intro_video_url,
smp.display_order,
smp.is_active,
qs.title,
qs.description,
qs.status,
qs.set_type,
(SELECT COUNT(*) FROM question_set_items qsi WHERE qsi.set_id = qs.id) AS question_count
FROM sub_module_lessons 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 = 'QUIZ'
` `
type GetSubModuleLessonByIDRow struct { func (q *Queries) GetSubModuleLessonByID(ctx context.Context, id int64) (SubModuleLesson, error) {
ID int64 `json:"id"`
SubModuleID int64 `json:"sub_module_id"`
QuestionSetID int64 `json:"question_set_id"`
IntroVideoUrl pgtype.Text `json:"intro_video_url"`
DisplayOrder int32 `json:"display_order"`
IsActive bool `json:"is_active"`
Title string `json:"title"`
Description pgtype.Text `json:"description"`
Status string `json:"status"`
SetType string `json:"set_type"`
QuestionCount int64 `json:"question_count"`
}
func (q *Queries) GetSubModuleLessonByID(ctx context.Context, id int64) (GetSubModuleLessonByIDRow, error) {
row := q.db.QueryRow(ctx, GetSubModuleLessonByID, id) row := q.db.QueryRow(ctx, GetSubModuleLessonByID, id)
var i GetSubModuleLessonByIDRow var i SubModuleLesson
err := row.Scan( err := row.Scan(
&i.ID, &i.ID,
&i.SubModuleID, &i.SubModuleID,
&i.QuestionSetID,
&i.IntroVideoUrl,
&i.DisplayOrder, &i.DisplayOrder,
&i.IsActive, &i.IsActive,
&i.CreatedAt,
&i.Title, &i.Title,
&i.Description, &i.Description,
&i.Status, &i.Thumbnail,
&i.SetType, &i.TeachingText,
&i.QuestionCount, &i.TeachingImageUrl,
&i.TeachingAudioUrl,
&i.TeachingVideoUrl,
) )
return i, err return i, err
} }
const GetSubModuleLessons = `-- name: GetSubModuleLessons :many const GetSubModuleLessons = `-- name: GetSubModuleLessons :many
SELECT SELECT id, sub_module_id, display_order, is_active, created_at, title, description, thumbnail, teaching_text, teaching_image_url, teaching_audio_url, teaching_video_url
smp.id, FROM sub_module_lessons
smp.sub_module_id, WHERE sub_module_id = $1
smp.question_set_id, AND is_active = TRUE
smp.intro_video_url, ORDER BY display_order ASC, id ASC
smp.display_order,
smp.is_active,
qs.title,
qs.description,
qs.status,
qs.set_type,
(SELECT COUNT(*) FROM question_set_items qsi WHERE qsi.set_id = qs.id) AS question_count
FROM sub_module_lessons smp
JOIN question_sets qs ON qs.id = smp.question_set_id
WHERE smp.sub_module_id = $1
AND smp.is_active = TRUE
AND qs.set_type = 'QUIZ'
ORDER BY smp.display_order ASC, smp.id ASC
` `
type GetSubModuleLessonsRow struct { func (q *Queries) GetSubModuleLessons(ctx context.Context, subModuleID int64) ([]SubModuleLesson, error) {
ID int64 `json:"id"`
SubModuleID int64 `json:"sub_module_id"`
QuestionSetID int64 `json:"question_set_id"`
IntroVideoUrl pgtype.Text `json:"intro_video_url"`
DisplayOrder int32 `json:"display_order"`
IsActive bool `json:"is_active"`
Title string `json:"title"`
Description pgtype.Text `json:"description"`
Status string `json:"status"`
SetType string `json:"set_type"`
QuestionCount int64 `json:"question_count"`
}
func (q *Queries) GetSubModuleLessons(ctx context.Context, subModuleID int64) ([]GetSubModuleLessonsRow, error) {
rows, err := q.db.Query(ctx, GetSubModuleLessons, subModuleID) rows, err := q.db.Query(ctx, GetSubModuleLessons, subModuleID)
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer rows.Close() defer rows.Close()
var items []GetSubModuleLessonsRow var items []SubModuleLesson
for rows.Next() { for rows.Next() {
var i GetSubModuleLessonsRow var i SubModuleLesson
if err := rows.Scan( if err := rows.Scan(
&i.ID, &i.ID,
&i.SubModuleID, &i.SubModuleID,
&i.QuestionSetID,
&i.IntroVideoUrl,
&i.DisplayOrder, &i.DisplayOrder,
&i.IsActive, &i.IsActive,
&i.CreatedAt,
&i.Title, &i.Title,
&i.Description, &i.Description,
&i.Status, &i.Thumbnail,
&i.SetType, &i.TeachingText,
&i.QuestionCount, &i.TeachingImageUrl,
&i.TeachingAudioUrl,
&i.TeachingVideoUrl,
); err != nil { ); err != nil {
return nil, err return nil, err
} }
@ -1242,28 +1221,43 @@ const UpdateSubModuleLesson = `-- name: UpdateSubModuleLesson :one
UPDATE sub_module_lessons UPDATE sub_module_lessons
SET SET
sub_module_id = $1, sub_module_id = $1,
question_set_id = $2, title = $2,
intro_video_url = $3, description = $3,
display_order = $4, thumbnail = $4,
is_active = $5 teaching_text = $5,
WHERE id = $6 teaching_image_url = $6,
RETURNING id, sub_module_id, question_set_id, intro_video_url, display_order, is_active, created_at teaching_audio_url = $7,
teaching_video_url = $8,
display_order = $9,
is_active = $10
WHERE id = $11
RETURNING id, sub_module_id, display_order, is_active, created_at, title, description, thumbnail, teaching_text, teaching_image_url, teaching_audio_url, teaching_video_url
` `
type UpdateSubModuleLessonParams struct { type UpdateSubModuleLessonParams struct {
SubModuleID int64 `json:"sub_module_id"` SubModuleID int64 `json:"sub_module_id"`
QuestionSetID int64 `json:"question_set_id"` Title string `json:"title"`
IntroVideoUrl pgtype.Text `json:"intro_video_url"` Description pgtype.Text `json:"description"`
DisplayOrder int32 `json:"display_order"` Thumbnail pgtype.Text `json:"thumbnail"`
IsActive bool `json:"is_active"` TeachingText pgtype.Text `json:"teaching_text"`
ID int64 `json:"id"` TeachingImageUrl pgtype.Text `json:"teaching_image_url"`
TeachingAudioUrl pgtype.Text `json:"teaching_audio_url"`
TeachingVideoUrl pgtype.Text `json:"teaching_video_url"`
DisplayOrder int32 `json:"display_order"`
IsActive bool `json:"is_active"`
ID int64 `json:"id"`
} }
func (q *Queries) UpdateSubModuleLesson(ctx context.Context, arg UpdateSubModuleLessonParams) (SubModuleLesson, error) { func (q *Queries) UpdateSubModuleLesson(ctx context.Context, arg UpdateSubModuleLessonParams) (SubModuleLesson, error) {
row := q.db.QueryRow(ctx, UpdateSubModuleLesson, row := q.db.QueryRow(ctx, UpdateSubModuleLesson,
arg.SubModuleID, arg.SubModuleID,
arg.QuestionSetID, arg.Title,
arg.IntroVideoUrl, arg.Description,
arg.Thumbnail,
arg.TeachingText,
arg.TeachingImageUrl,
arg.TeachingAudioUrl,
arg.TeachingVideoUrl,
arg.DisplayOrder, arg.DisplayOrder,
arg.IsActive, arg.IsActive,
arg.ID, arg.ID,
@ -1272,11 +1266,16 @@ func (q *Queries) UpdateSubModuleLesson(ctx context.Context, arg UpdateSubModule
err := row.Scan( err := row.Scan(
&i.ID, &i.ID,
&i.SubModuleID, &i.SubModuleID,
&i.QuestionSetID,
&i.IntroVideoUrl,
&i.DisplayOrder, &i.DisplayOrder,
&i.IsActive, &i.IsActive,
&i.CreatedAt, &i.CreatedAt,
&i.Title,
&i.Description,
&i.Thumbnail,
&i.TeachingText,
&i.TeachingImageUrl,
&i.TeachingAudioUrl,
&i.TeachingVideoUrl,
) )
return i, err return i, err
} }

View File

@ -356,13 +356,18 @@ type SubModule struct {
} }
type SubModuleLesson struct { type SubModuleLesson struct {
ID int64 `json:"id"` ID int64 `json:"id"`
SubModuleID int64 `json:"sub_module_id"` SubModuleID int64 `json:"sub_module_id"`
QuestionSetID int64 `json:"question_set_id"` DisplayOrder int32 `json:"display_order"`
IntroVideoUrl pgtype.Text `json:"intro_video_url"` IsActive bool `json:"is_active"`
DisplayOrder int32 `json:"display_order"` CreatedAt pgtype.Timestamptz `json:"created_at"`
IsActive bool `json:"is_active"` Title string `json:"title"`
CreatedAt pgtype.Timestamptz `json:"created_at"` Description pgtype.Text `json:"description"`
Thumbnail pgtype.Text `json:"thumbnail"`
TeachingText pgtype.Text `json:"teaching_text"`
TeachingImageUrl pgtype.Text `json:"teaching_image_url"`
TeachingAudioUrl pgtype.Text `json:"teaching_audio_url"`
TeachingVideoUrl pgtype.Text `json:"teaching_video_url"`
} }
type SubModulePractice struct { type SubModulePractice struct {

View File

@ -101,35 +101,30 @@ type updatePracticeReq struct {
IsActive *bool `json:"is_active"` IsActive *bool `json:"is_active"`
} }
type attachSubModuleLessonReq struct { type createSubModuleLessonReq struct {
SubModuleID int64 `json:"sub_module_id"` SubModuleID int64 `json:"sub_module_id"`
QuestionSetID int64 `json:"question_set_id"` Title string `json:"title"`
IntroVideoURL *string `json:"intro_video_url"` Description *string `json:"description"`
DisplayOrder *int32 `json:"display_order"` Thumbnail *string `json:"thumbnail"`
IsActive *bool `json:"is_active"` TeachingText *string `json:"teaching_text"`
} TeachingImageURL *string `json:"teaching_image_url"`
TeachingAudioURL *string `json:"teaching_audio_url"`
type updateLessonQuestionReq struct { TeachingVideoURL *string `json:"teaching_video_url"`
QuestionID int64 `json:"question_id"` DisplayOrder *int32 `json:"display_order"`
DisplayOrder *int32 `json:"display_order"` IsActive *bool `json:"is_active"`
} }
type updateSubModuleLessonReq struct { type updateSubModuleLessonReq struct {
SubModuleID *int64 `json:"sub_module_id"` SubModuleID *int64 `json:"sub_module_id"`
QuestionSetID *int64 `json:"question_set_id"` Title *string `json:"title"`
IntroVideoURL *string `json:"intro_video_url"` Description *string `json:"description"`
DisplayOrder *int32 `json:"display_order"` Thumbnail *string `json:"thumbnail"`
IsActive *bool `json:"is_active"` TeachingText *string `json:"teaching_text"`
Title *string `json:"title"` TeachingImageURL *string `json:"teaching_image_url"`
Description *string `json:"description"` TeachingAudioURL *string `json:"teaching_audio_url"`
BannerImage *string `json:"banner_image"` TeachingVideoURL *string `json:"teaching_video_url"`
Persona *string `json:"persona"` DisplayOrder *int32 `json:"display_order"`
TimeLimitMinutes *int32 `json:"time_limit_minutes"` IsActive *bool `json:"is_active"`
PassingScore *int32 `json:"passing_score"`
ShuffleQuestions *bool `json:"shuffle_questions"`
Status *string `json:"status"`
SubCourseVideoID *int64 `json:"sub_course_video_id"`
Questions []updateLessonQuestionReq `json:"questions"`
} }
type createSubModulePracticeReq struct { type createSubModulePracticeReq struct {
@ -159,6 +154,16 @@ func toText(v *string) pgtype.Text {
return pgtype.Text{String: *v, Valid: true} return pgtype.Text{String: *v, Valid: true}
} }
func mergeTextField(current pgtype.Text, req *string) pgtype.Text {
if req == nil {
return current
}
if *req == "" {
return pgtype.Text{Valid: false}
}
return pgtype.Text{String: *req, Valid: true}
}
func toInt4(v *int32) pgtype.Int4 { func toInt4(v *int32) pgtype.Int4 {
if v == nil { if v == nil {
return pgtype.Int4{Valid: false} return pgtype.Int4{Valid: false}
@ -1515,41 +1520,46 @@ func (h *Handler) CreateSubModuleVideo(c *fiber.Ctx) error {
return c.Status(fiber.StatusCreated).JSON(domain.Response{Message: "Sub-module video created", Data: created}) return c.Status(fiber.StatusCreated).JSON(domain.Response{Message: "Sub-module video created", Data: created})
} }
// AttachSubModuleLesson godoc // CreateSubModuleLesson godoc
// @Summary Attach lesson to sub-module // @Summary Create lesson under sub-module
// @Description Links a question set lesson to a sub-module // @Description Creates a sub-module lesson with teaching content (text, image, audio, video URLs) and optional thumbnail
// @Tags course-management // @Tags course-management
// @Accept json // @Accept json
// @Produce json // @Produce json
// @Param body body attachSubModuleLessonReq true "Attach lesson payload" // @Param body body createSubModuleLessonReq true "Create lesson payload"
// @Success 201 {object} domain.Response // @Success 201 {object} domain.Response
// @Failure 400 {object} domain.ErrorResponse // @Failure 400 {object} domain.ErrorResponse
// @Failure 500 {object} domain.ErrorResponse // @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/course-management/sub-module-lessons [post] // @Router /api/v1/course-management/sub-module-lessons [post]
func (h *Handler) AttachSubModuleLesson(c *fiber.Ctx) error { func (h *Handler) CreateSubModuleLesson(c *fiber.Ctx) error {
var req attachSubModuleLessonReq var req createSubModuleLessonReq
if err := c.BodyParser(&req); err != nil { if err := c.BodyParser(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "Invalid request body", Error: err.Error()}) return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "Invalid request body", Error: err.Error()})
} }
if req.SubModuleID <= 0 || req.QuestionSetID <= 0 { if req.SubModuleID <= 0 || strings.TrimSpace(req.Title) == "" {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "sub_module_id and question_set_id are required"}) return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "sub_module_id and title are required"})
} }
attached, err := h.analyticsDB.AttachQuestionSetLessonToSubModule(c.Context(), dbgen.AttachQuestionSetLessonToSubModuleParams{ created, err := h.analyticsDB.CreateSubModuleLesson(c.Context(), dbgen.CreateSubModuleLessonParams{
SubModuleID: req.SubModuleID, SubModuleID: req.SubModuleID,
QuestionSetID: req.QuestionSetID, Title: strings.TrimSpace(req.Title),
IntroVideoUrl: toText(req.IntroVideoURL), Description: toText(req.Description),
Column4: intOrNil(req.DisplayOrder), Thumbnail: toText(req.Thumbnail),
Column5: boolOrNil(req.IsActive), TeachingText: toText(req.TeachingText),
TeachingImageUrl: toText(req.TeachingImageURL),
TeachingAudioUrl: toText(req.TeachingAudioURL),
TeachingVideoUrl: toText(req.TeachingVideoURL),
Column9: intOrNil(req.DisplayOrder),
Column10: boolOrNil(req.IsActive),
}) })
if err != nil { if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to attach lesson", Error: err.Error()}) return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to create lesson", Error: err.Error()})
} }
return c.Status(fiber.StatusCreated).JSON(domain.Response{Message: "Lesson attached to sub-module", Data: attached}) return c.Status(fiber.StatusCreated).JSON(domain.Response{Message: "Lesson created", Data: created})
} }
// GetSubModuleLessons godoc // GetSubModuleLessons godoc
// @Summary Get lessons under sub-module // @Summary Get lessons under sub-module
// @Description Returns all active lessons attached to a sub-module with question-set details // @Description Returns all active lessons for a sub-module (teaching content metadata)
// @Tags course-management // @Tags course-management
// @Accept json // @Accept json
// @Produce json // @Produce json
@ -1618,7 +1628,7 @@ func (h *Handler) GetSubModuleLessonByID(c *fiber.Ctx) error {
// UpdateSubModuleLesson godoc // UpdateSubModuleLesson godoc
// @Summary Update lesson detail // @Summary Update lesson detail
// @Description Updates lesson metadata, linked question-set metadata, and optionally replaces lesson questions // @Description Updates lesson teaching content, thumbnail, ordering, and active flag
// @Tags course-management // @Tags course-management
// @Accept json // @Accept json
// @Produce json // @Produce json
@ -1662,18 +1672,21 @@ func (h *Handler) UpdateSubModuleLesson(c *fiber.Ctx) error {
targetSubModuleID = *req.SubModuleID targetSubModuleID = *req.SubModuleID
} }
targetQuestionSetID := currentLesson.QuestionSetID targetTitle := currentLesson.Title
if req.QuestionSetID != nil { if req.Title != nil {
if *req.QuestionSetID <= 0 { t := strings.TrimSpace(*req.Title)
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "question_set_id must be a positive integer"}) if t == "" {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "title cannot be empty"})
} }
targetQuestionSetID = *req.QuestionSetID targetTitle = t
} }
targetIntroVideoURL := currentLesson.IntroVideoUrl targetDescription := mergeTextField(currentLesson.Description, req.Description)
if req.IntroVideoURL != nil { targetThumbnail := mergeTextField(currentLesson.Thumbnail, req.Thumbnail)
targetIntroVideoURL = toText(req.IntroVideoURL) targetTeachingText := mergeTextField(currentLesson.TeachingText, req.TeachingText)
} targetTeachingImage := mergeTextField(currentLesson.TeachingImageUrl, req.TeachingImageURL)
targetTeachingAudio := mergeTextField(currentLesson.TeachingAudioUrl, req.TeachingAudioURL)
targetTeachingVideo := mergeTextField(currentLesson.TeachingVideoUrl, req.TeachingVideoURL)
targetDisplayOrder := currentLesson.DisplayOrder targetDisplayOrder := currentLesson.DisplayOrder
if req.DisplayOrder != nil { if req.DisplayOrder != nil {
@ -1686,12 +1699,17 @@ func (h *Handler) UpdateSubModuleLesson(c *fiber.Ctx) error {
} }
if _, err := h.analyticsDB.UpdateSubModuleLesson(c.Context(), dbgen.UpdateSubModuleLessonParams{ if _, err := h.analyticsDB.UpdateSubModuleLesson(c.Context(), dbgen.UpdateSubModuleLessonParams{
SubModuleID: targetSubModuleID, SubModuleID: targetSubModuleID,
QuestionSetID: targetQuestionSetID, Title: targetTitle,
IntroVideoUrl: targetIntroVideoURL, Description: targetDescription,
DisplayOrder: targetDisplayOrder, Thumbnail: targetThumbnail,
IsActive: targetIsActive, TeachingText: targetTeachingText,
ID: lessonID, TeachingImageUrl: targetTeachingImage,
TeachingAudioUrl: targetTeachingAudio,
TeachingVideoUrl: targetTeachingVideo,
DisplayOrder: targetDisplayOrder,
IsActive: targetIsActive,
ID: lessonID,
}); err != nil { }); err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Failed to update lesson", Message: "Failed to update lesson",
@ -1699,125 +1717,6 @@ func (h *Handler) UpdateSubModuleLesson(c *fiber.Ctx) error {
}) })
} }
currentSet, err := h.questionsSvc.GetQuestionSetByID(c.Context(), targetQuestionSetID)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Failed to load linked question set",
Error: err.Error(),
})
}
shouldUpdateSet := req.Title != nil || req.Description != nil || req.BannerImage != nil ||
req.Persona != nil || req.TimeLimitMinutes != nil || req.PassingScore != nil ||
req.ShuffleQuestions != nil || req.Status != nil || req.SubCourseVideoID != nil
if shouldUpdateSet {
title := currentSet.Title
if req.Title != nil {
title = *req.Title
}
input := domain.CreateQuestionSetInput{
Title: title,
Description: currentSet.Description,
BannerImage: currentSet.BannerImage,
Persona: currentSet.Persona,
TimeLimitMinutes: currentSet.TimeLimitMinutes,
PassingScore: currentSet.PassingScore,
SubCourseVideoID: currentSet.SubCourseVideoID,
IntroVideoURL: req.IntroVideoURL,
ShuffleQuestions: &currentSet.ShuffleQuestions,
}
currentStatus := currentSet.Status
input.Status = &currentStatus
if req.Description != nil {
input.Description = req.Description
}
if req.BannerImage != nil {
input.BannerImage = req.BannerImage
}
if req.Persona != nil {
input.Persona = req.Persona
}
if req.TimeLimitMinutes != nil {
input.TimeLimitMinutes = req.TimeLimitMinutes
}
if req.PassingScore != nil {
input.PassingScore = req.PassingScore
}
if req.ShuffleQuestions != nil {
input.ShuffleQuestions = req.ShuffleQuestions
}
if req.Status != nil {
input.Status = req.Status
}
if req.SubCourseVideoID != nil {
input.SubCourseVideoID = req.SubCourseVideoID
}
if err := h.questionsSvc.UpdateQuestionSet(c.Context(), targetQuestionSetID, input); err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Failed to update linked question set",
Error: err.Error(),
})
}
}
if req.Questions != nil {
seen := make(map[int64]struct{}, len(req.Questions))
for idx, q := range req.Questions {
if q.QuestionID <= 0 {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "Each question_id must be a positive integer"})
}
if _, exists := seen[q.QuestionID]; exists {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "Duplicate question_id values are not allowed"})
}
seen[q.QuestionID] = struct{}{}
order := q.DisplayOrder
if order == nil {
defaultOrder := int32(idx)
order = &defaultOrder
}
if _, err := h.questionsSvc.GetQuestionByID(c.Context(), q.QuestionID); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Invalid question_id in questions payload",
Error: err.Error(),
})
}
if _, err := h.questionsSvc.AddQuestionToSet(c.Context(), targetQuestionSetID, q.QuestionID, order); err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Failed to upsert lesson question",
Error: err.Error(),
})
}
}
existingItems, err := h.questionsSvc.GetQuestionSetItems(c.Context(), targetQuestionSetID)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Failed to load existing lesson questions",
Error: err.Error(),
})
}
for _, item := range existingItems {
if _, keep := seen[item.QuestionID]; keep {
continue
}
if err := h.questionsSvc.RemoveQuestionFromSet(c.Context(), targetQuestionSetID, item.QuestionID); err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Failed to remove question from lesson",
Error: err.Error(),
})
}
}
}
updatedLesson, err := h.analyticsDB.GetSubModuleLessonByID(c.Context(), lessonID) updatedLesson, err := h.analyticsDB.GetSubModuleLessonByID(c.Context(), lessonID)
if err != nil { if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
@ -1826,20 +1725,9 @@ func (h *Handler) UpdateSubModuleLesson(c *fiber.Ctx) error {
}) })
} }
updatedQuestions, err := h.questionsSvc.GetQuestionSetItems(c.Context(), updatedLesson.QuestionSetID)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Lesson updated but failed to fetch latest questions",
Error: err.Error(),
})
}
return c.Status(fiber.StatusOK).JSON(domain.Response{ return c.Status(fiber.StatusOK).JSON(domain.Response{
Message: "Lesson updated successfully", Message: "Lesson updated successfully",
Data: map[string]interface{}{ Data: updatedLesson,
"lesson": updatedLesson,
"questions": updatedQuestions,
},
}) })
} }

View File

@ -116,10 +116,10 @@ func (a *App) initAppRoutes() {
groupV1.Post("/course-management/sub-module-videos", a.authMiddleware, a.RequirePermission("videos.create"), h.CreateSubModuleVideo) 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.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) groupV1.Delete("/course-management/sub-module-videos/:videoId", a.authMiddleware, a.RequirePermission("videos.delete"), h.DeleteSubModuleVideo)
groupV1.Get("/course-management/sub-modules/:subModuleId/lessons", a.authMiddleware, a.RequirePermission("question_sets.list"), h.GetSubModuleLessons) groupV1.Get("/course-management/sub-modules/:subModuleId/lessons", a.authMiddleware, a.RequirePermission("learning_tree.get"), h.GetSubModuleLessons)
groupV1.Get("/course-management/sub-module-lessons/:lessonId", a.authMiddleware, a.RequirePermission("question_sets.get"), h.GetSubModuleLessonByID) groupV1.Get("/course-management/sub-module-lessons/:lessonId", a.authMiddleware, a.RequirePermission("learning_tree.get"), h.GetSubModuleLessonByID)
groupV1.Put("/course-management/sub-module-lessons/:lessonId", a.authMiddleware, a.RequirePermission("question_sets.update"), h.UpdateSubModuleLesson) groupV1.Put("/course-management/sub-module-lessons/:lessonId", a.authMiddleware, a.RequirePermission("subcourses.update"), h.UpdateSubModuleLesson)
groupV1.Post("/course-management/sub-module-lessons", a.authMiddleware, a.RequirePermission("question_sets.update"), h.AttachSubModuleLesson) groupV1.Post("/course-management/sub-module-lessons", a.authMiddleware, a.RequirePermission("subcourses.create"), h.CreateSubModuleLesson)
groupV1.Get("/course-management/sub-modules/:subModuleId/practices", a.authMiddleware, a.RequirePermission("question_sets.list"), h.GetSubModulePractices) 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.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.Post("/course-management/sub-module-practices", a.authMiddleware, a.RequirePermission("question_sets.update"), h.CreateSubModulePractice)