From 518c3ee75142b67513903931907752d17b7a1562 Mon Sep 17 00:00:00 2001 From: Yared Yemane Date: Fri, 17 Apr 2026 08:27:40 -0700 Subject: [PATCH] added more structure to lessons --- ...b_module_lessons_teaching_content.down.sql | 18 + ...sub_module_lessons_teaching_content.up.sql | 37 + db/query/hierarchy.sql | 83 +- docs/docs.go | 975 +++++++++++++++++- docs/swagger.json | 975 +++++++++++++++++- docs/swagger.yaml | 647 +++++++++++- gen/db/hierarchy.sql.go | 269 +++-- gen/db/models.go | 19 +- .../web_server/handlers/hierarchy_handler.go | 268 ++--- internal/web_server/routes.go | 8 +- 10 files changed, 2854 insertions(+), 445 deletions(-) create mode 100644 db/migrations/000033_sub_module_lessons_teaching_content.down.sql create mode 100644 db/migrations/000033_sub_module_lessons_teaching_content.up.sql diff --git a/db/migrations/000033_sub_module_lessons_teaching_content.down.sql b/db/migrations/000033_sub_module_lessons_teaching_content.down.sql new file mode 100644 index 0000000..cf61232 --- /dev/null +++ b/db/migrations/000033_sub_module_lessons_teaching_content.down.sql @@ -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; diff --git a/db/migrations/000033_sub_module_lessons_teaching_content.up.sql b/db/migrations/000033_sub_module_lessons_teaching_content.up.sql new file mode 100644 index 0000000..d604a84 --- /dev/null +++ b/db/migrations/000033_sub_module_lessons_teaching_content.up.sql @@ -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'; diff --git a/db/query/hierarchy.sql b/db/query/hierarchy.sql index b2d7087..e3da128 100644 --- a/db/query/hierarchy.sql +++ b/db/query/hierarchy.sql @@ -83,43 +83,17 @@ WHERE sub_module_id = $1 ORDER BY display_order ASC, id ASC; -- name: GetSubModuleLessons :many -SELECT - smp.id, - smp.sub_module_id, - smp.question_set_id, - 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.sub_module_id = $1 - AND smp.is_active = TRUE - AND qs.set_type = 'QUIZ' -ORDER BY smp.display_order ASC, smp.id ASC; +SELECT * +FROM sub_module_lessons +WHERE sub_module_id = $1 + AND is_active = TRUE +ORDER BY display_order ASC, id ASC; -- name: GetSubModuleLessonByID :one -SELECT - smp.id, - smp.sub_module_id, - smp.question_set_id, - 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'; +SELECT * +FROM sub_module_lessons +WHERE id = $1 + AND is_active = TRUE; -- name: GetSubModulePractices :many SELECT @@ -289,26 +263,47 @@ VALUES ( ) RETURNING *; --- name: AttachQuestionSetLessonToSubModule :one +-- name: CreateSubModuleLesson :one INSERT INTO sub_module_lessons ( sub_module_id, - question_set_id, - intro_video_url, + title, + description, + thumbnail, + teaching_text, + teaching_image_url, + teaching_audio_url, + teaching_video_url, display_order, 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 *; -- name: UpdateSubModuleLesson :one UPDATE sub_module_lessons SET sub_module_id = $1, - question_set_id = $2, - intro_video_url = $3, - display_order = $4, - is_active = $5 -WHERE id = $6 + title = $2, + description = $3, + thumbnail = $4, + teaching_text = $5, + teaching_image_url = $6, + teaching_audio_url = $7, + teaching_video_url = $8, + display_order = $9, + is_active = $10 +WHERE id = $11 RETURNING *; -- name: CreateSubModulePractice :one diff --git a/docs/docs.go b/docs/docs.go index 0ed88fd..776ddf3 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -837,6 +837,44 @@ const docTemplate = `{ } }, "/api/v1/course-management/courses": { + "get": { + "description": "Returns all courses with pagination", + "produces": [ + "application/json" + ], + "tags": [ + "course-management" + ], + "summary": "List all courses", + "parameters": [ + { + "type": "integer", + "description": "Offset", + "name": "offset", + "in": "query" + }, + { + "type": "integer", + "description": "Limit", + "name": "limit", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.Response" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + }, "post": { "description": "Legacy-compatible endpoint for creating a course", "consumes": [ @@ -883,6 +921,51 @@ const docTemplate = `{ } }, "/api/v1/course-management/courses/{courseId}": { + "get": { + "description": "Returns one course by ID", + "produces": [ + "application/json" + ], + "tags": [ + "course-management" + ], + "summary": "Get course detail", + "parameters": [ + { + "type": "integer", + "description": "Course ID", + "name": "courseId", + "in": "path", + "required": true + } + ], + "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" + } + } + } + }, "put": { "description": "Legacy-compatible endpoint for updating a course", "consumes": [ @@ -1056,6 +1139,47 @@ const docTemplate = `{ } } }, + "/api/v1/course-management/courses/{courseId}/levels": { + "get": { + "description": "Returns all active levels for one course", + "produces": [ + "application/json" + ], + "tags": [ + "course-management" + ], + "summary": "List levels by course", + "parameters": [ + { + "type": "integer", + "description": "Course ID", + "name": "courseId", + "in": "path", + "required": true + } + ], + "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" + } + } + } + } + }, "/api/v1/course-management/courses/{courseId}/thumbnail": { "post": { "description": "Legacy-compatible endpoint for updating course thumbnail", @@ -1135,7 +1259,125 @@ const docTemplate = `{ } } }, + "/api/v1/course-management/human-language/courses": { + "get": { + "description": "Returns all courses under Human Language category", + "produces": [ + "application/json" + ], + "tags": [ + "course-management" + ], + "summary": "List Human Language courses", + "parameters": [ + { + "type": "integer", + "description": "Offset", + "name": "offset", + "in": "query" + }, + { + "type": "integer", + "description": "Limit", + "name": "limit", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.Response" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, + "/api/v1/course-management/human-language/sub-categories": { + "get": { + "description": "Returns active sub-categories under Human Language category", + "produces": [ + "application/json" + ], + "tags": [ + "course-management" + ], + "summary": "List Human Language sub-categories", + "parameters": [ + { + "type": "integer", + "description": "Offset", + "name": "offset", + "in": "query" + }, + { + "type": "integer", + "description": "Limit", + "name": "limit", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.Response" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, "/api/v1/course-management/levels": { + "get": { + "description": "Returns all levels with pagination", + "produces": [ + "application/json" + ], + "tags": [ + "course-management" + ], + "summary": "List all levels", + "parameters": [ + { + "type": "integer", + "description": "Offset", + "name": "offset", + "in": "query" + }, + { + "type": "integer", + "description": "Limit", + "name": "limit", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.Response" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + }, "post": { "description": "Creates a CEFR level under a course", "consumes": [ @@ -1181,7 +1423,133 @@ const docTemplate = `{ } } }, + "/api/v1/course-management/levels/{levelId}": { + "get": { + "description": "Returns one level by ID", + "produces": [ + "application/json" + ], + "tags": [ + "course-management" + ], + "summary": "Get level detail", + "parameters": [ + { + "type": "integer", + "description": "Level ID", + "name": "levelId", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, + "/api/v1/course-management/levels/{levelId}/modules": { + "get": { + "description": "Returns all active modules for one level", + "produces": [ + "application/json" + ], + "tags": [ + "course-management" + ], + "summary": "List modules by level", + "parameters": [ + { + "type": "integer", + "description": "Level ID", + "name": "levelId", + "in": "path", + "required": true + } + ], + "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" + } + } + } + } + }, "/api/v1/course-management/modules": { + "get": { + "description": "Returns all modules with pagination", + "produces": [ + "application/json" + ], + "tags": [ + "course-management" + ], + "summary": "List all modules", + "parameters": [ + { + "type": "integer", + "description": "Offset", + "name": "offset", + "in": "query" + }, + { + "type": "integer", + "description": "Limit", + "name": "limit", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.Response" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + }, "post": { "description": "Creates a module under a level", "consumes": [ @@ -1227,7 +1595,183 @@ const docTemplate = `{ } } }, + "/api/v1/course-management/modules/{moduleId}": { + "get": { + "description": "Returns one module by ID", + "produces": [ + "application/json" + ], + "tags": [ + "course-management" + ], + "summary": "Get module detail", + "parameters": [ + { + "type": "integer", + "description": "Module ID", + "name": "moduleId", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, + "/api/v1/course-management/modules/{moduleId}/sub-modules": { + "get": { + "description": "Returns all active sub-modules for one module", + "produces": [ + "application/json" + ], + "tags": [ + "course-management" + ], + "summary": "List sub-modules by module", + "parameters": [ + { + "type": "integer", + "description": "Module ID", + "name": "moduleId", + "in": "path", + "required": true + } + ], + "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" + } + } + } + } + }, + "/api/v1/course-management/practices/{practiceId}": { + "get": { + "description": "Returns one active practice by practice ID", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "course-management" + ], + "summary": "Get practice detail", + "parameters": [ + { + "type": "integer", + "description": "Practice ID", + "name": "practiceId", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, "/api/v1/course-management/sub-categories": { + "get": { + "description": "Returns all active course sub-categories", + "produces": [ + "application/json" + ], + "tags": [ + "course-management" + ], + "summary": "List course sub-categories", + "parameters": [ + { + "type": "integer", + "description": "Offset", + "name": "offset", + "in": "query" + }, + { + "type": "integer", + "description": "Limit", + "name": "limit", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.Response" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + }, "post": { "description": "Creates a sub-category under a course category", "consumes": [ @@ -1273,9 +1817,62 @@ const docTemplate = `{ } } }, + "/api/v1/course-management/sub-categories/{subCategoryId}/courses": { + "get": { + "description": "Returns courses for one sub-category", + "produces": [ + "application/json" + ], + "tags": [ + "course-management" + ], + "summary": "List courses by sub-category", + "parameters": [ + { + "type": "integer", + "description": "Sub-category ID", + "name": "subCategoryId", + "in": "path", + "required": true + }, + { + "type": "integer", + "description": "Offset", + "name": "offset", + "in": "query" + }, + { + "type": "integer", + "description": "Limit", + "name": "limit", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, "/api/v1/course-management/sub-module-lessons": { "post": { - "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", "consumes": [ "application/json" ], @@ -1285,15 +1882,15 @@ const docTemplate = `{ "tags": [ "course-management" ], - "summary": "Attach lesson to sub-module", + "summary": "Create lesson under sub-module", "parameters": [ { - "description": "Attach lesson payload", + "description": "Create lesson payload", "name": "body", "in": "body", "required": true, "schema": { - "$ref": "#/definitions/handlers.attachSubModuleLessonReq" + "$ref": "#/definitions/handlers.createSubModuleLessonReq" } } ], @@ -1319,6 +1916,113 @@ const docTemplate = `{ } } }, + "/api/v1/course-management/sub-module-lessons/{lessonId}": { + "get": { + "description": "Returns one active lesson detail by lesson ID", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "course-management" + ], + "summary": "Get lesson detail", + "parameters": [ + { + "type": "integer", + "description": "Lesson ID", + "name": "lessonId", + "in": "path", + "required": true + } + ], + "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" + } + } + } + }, + "put": { + "description": "Updates lesson teaching content, thumbnail, ordering, and active flag", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "course-management" + ], + "summary": "Update lesson detail", + "parameters": [ + { + "type": "integer", + "description": "Lesson ID", + "name": "lessonId", + "in": "path", + "required": true + }, + { + "description": "Update lesson payload", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handlers.updateSubModuleLessonReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, "/api/v1/course-management/sub-module-practices": { "post": { "description": "Creates a sub-module practice with metadata and linked question set", @@ -1412,6 +2116,44 @@ const docTemplate = `{ } }, "/api/v1/course-management/sub-modules": { + "get": { + "description": "Returns all sub-modules with pagination", + "produces": [ + "application/json" + ], + "tags": [ + "course-management" + ], + "summary": "List all sub-modules", + "parameters": [ + { + "type": "integer", + "description": "Offset", + "name": "offset", + "in": "query" + }, + { + "type": "integer", + "description": "Limit", + "name": "limit", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.Response" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + }, "post": { "description": "Creates a sub-module under a module", "consumes": [ @@ -1457,6 +2199,141 @@ const docTemplate = `{ } } }, + "/api/v1/course-management/sub-modules/{subModuleId}": { + "get": { + "description": "Returns one sub-module by ID", + "produces": [ + "application/json" + ], + "tags": [ + "course-management" + ], + "summary": "Get sub-module detail", + "parameters": [ + { + "type": "integer", + "description": "Sub-module ID", + "name": "subModuleId", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, + "/api/v1/course-management/sub-modules/{subModuleId}/lessons": { + "get": { + "description": "Returns all active lessons for a sub-module (teaching content metadata)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "course-management" + ], + "summary": "Get lessons under sub-module", + "parameters": [ + { + "type": "integer", + "description": "Sub-module ID", + "name": "subModuleId", + "in": "path", + "required": true + } + ], + "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" + } + } + } + } + }, + "/api/v1/course-management/sub-modules/{subModuleId}/practices": { + "get": { + "description": "Returns all active practices attached to a sub-module", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "course-management" + ], + "summary": "Get practices under sub-module", + "parameters": [ + { + "type": "integer", + "description": "Sub-module ID", + "name": "subModuleId", + "in": "path", + "required": true + } + ], + "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" + } + } + } + } + }, "/api/v1/files/audio": { "post": { "consumes": [ @@ -9358,26 +10235,6 @@ const docTemplate = `{ } } }, - "handlers.attachSubModuleLessonReq": { - "type": "object", - "properties": { - "display_order": { - "type": "integer" - }, - "intro_video_url": { - "type": "string" - }, - "is_active": { - "type": "boolean" - }, - "question_set_id": { - "type": "integer" - }, - "sub_module_id": { - "type": "integer" - } - } - }, "handlers.autoRenewReq": { "type": "object", "properties": { @@ -9685,6 +10542,41 @@ const docTemplate = `{ } } }, + "handlers.createSubModuleLessonReq": { + "type": "object", + "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" + } + } + }, "handlers.createSubModulePracticeReq": { "type": "object", "properties": { @@ -10217,6 +11109,41 @@ const docTemplate = `{ } } }, + "handlers.updateSubModuleLessonReq": { + "type": "object", + "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" + } + } + }, "handlers.verifyOTPReq": { "type": "object", "required": [ diff --git a/docs/swagger.json b/docs/swagger.json index f856779..34359ee 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -829,6 +829,44 @@ } }, "/api/v1/course-management/courses": { + "get": { + "description": "Returns all courses with pagination", + "produces": [ + "application/json" + ], + "tags": [ + "course-management" + ], + "summary": "List all courses", + "parameters": [ + { + "type": "integer", + "description": "Offset", + "name": "offset", + "in": "query" + }, + { + "type": "integer", + "description": "Limit", + "name": "limit", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.Response" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + }, "post": { "description": "Legacy-compatible endpoint for creating a course", "consumes": [ @@ -875,6 +913,51 @@ } }, "/api/v1/course-management/courses/{courseId}": { + "get": { + "description": "Returns one course by ID", + "produces": [ + "application/json" + ], + "tags": [ + "course-management" + ], + "summary": "Get course detail", + "parameters": [ + { + "type": "integer", + "description": "Course ID", + "name": "courseId", + "in": "path", + "required": true + } + ], + "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" + } + } + } + }, "put": { "description": "Legacy-compatible endpoint for updating a course", "consumes": [ @@ -1048,6 +1131,47 @@ } } }, + "/api/v1/course-management/courses/{courseId}/levels": { + "get": { + "description": "Returns all active levels for one course", + "produces": [ + "application/json" + ], + "tags": [ + "course-management" + ], + "summary": "List levels by course", + "parameters": [ + { + "type": "integer", + "description": "Course ID", + "name": "courseId", + "in": "path", + "required": true + } + ], + "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" + } + } + } + } + }, "/api/v1/course-management/courses/{courseId}/thumbnail": { "post": { "description": "Legacy-compatible endpoint for updating course thumbnail", @@ -1127,7 +1251,125 @@ } } }, + "/api/v1/course-management/human-language/courses": { + "get": { + "description": "Returns all courses under Human Language category", + "produces": [ + "application/json" + ], + "tags": [ + "course-management" + ], + "summary": "List Human Language courses", + "parameters": [ + { + "type": "integer", + "description": "Offset", + "name": "offset", + "in": "query" + }, + { + "type": "integer", + "description": "Limit", + "name": "limit", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.Response" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, + "/api/v1/course-management/human-language/sub-categories": { + "get": { + "description": "Returns active sub-categories under Human Language category", + "produces": [ + "application/json" + ], + "tags": [ + "course-management" + ], + "summary": "List Human Language sub-categories", + "parameters": [ + { + "type": "integer", + "description": "Offset", + "name": "offset", + "in": "query" + }, + { + "type": "integer", + "description": "Limit", + "name": "limit", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.Response" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, "/api/v1/course-management/levels": { + "get": { + "description": "Returns all levels with pagination", + "produces": [ + "application/json" + ], + "tags": [ + "course-management" + ], + "summary": "List all levels", + "parameters": [ + { + "type": "integer", + "description": "Offset", + "name": "offset", + "in": "query" + }, + { + "type": "integer", + "description": "Limit", + "name": "limit", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.Response" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + }, "post": { "description": "Creates a CEFR level under a course", "consumes": [ @@ -1173,7 +1415,133 @@ } } }, + "/api/v1/course-management/levels/{levelId}": { + "get": { + "description": "Returns one level by ID", + "produces": [ + "application/json" + ], + "tags": [ + "course-management" + ], + "summary": "Get level detail", + "parameters": [ + { + "type": "integer", + "description": "Level ID", + "name": "levelId", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, + "/api/v1/course-management/levels/{levelId}/modules": { + "get": { + "description": "Returns all active modules for one level", + "produces": [ + "application/json" + ], + "tags": [ + "course-management" + ], + "summary": "List modules by level", + "parameters": [ + { + "type": "integer", + "description": "Level ID", + "name": "levelId", + "in": "path", + "required": true + } + ], + "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" + } + } + } + } + }, "/api/v1/course-management/modules": { + "get": { + "description": "Returns all modules with pagination", + "produces": [ + "application/json" + ], + "tags": [ + "course-management" + ], + "summary": "List all modules", + "parameters": [ + { + "type": "integer", + "description": "Offset", + "name": "offset", + "in": "query" + }, + { + "type": "integer", + "description": "Limit", + "name": "limit", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.Response" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + }, "post": { "description": "Creates a module under a level", "consumes": [ @@ -1219,7 +1587,183 @@ } } }, + "/api/v1/course-management/modules/{moduleId}": { + "get": { + "description": "Returns one module by ID", + "produces": [ + "application/json" + ], + "tags": [ + "course-management" + ], + "summary": "Get module detail", + "parameters": [ + { + "type": "integer", + "description": "Module ID", + "name": "moduleId", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, + "/api/v1/course-management/modules/{moduleId}/sub-modules": { + "get": { + "description": "Returns all active sub-modules for one module", + "produces": [ + "application/json" + ], + "tags": [ + "course-management" + ], + "summary": "List sub-modules by module", + "parameters": [ + { + "type": "integer", + "description": "Module ID", + "name": "moduleId", + "in": "path", + "required": true + } + ], + "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" + } + } + } + } + }, + "/api/v1/course-management/practices/{practiceId}": { + "get": { + "description": "Returns one active practice by practice ID", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "course-management" + ], + "summary": "Get practice detail", + "parameters": [ + { + "type": "integer", + "description": "Practice ID", + "name": "practiceId", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, "/api/v1/course-management/sub-categories": { + "get": { + "description": "Returns all active course sub-categories", + "produces": [ + "application/json" + ], + "tags": [ + "course-management" + ], + "summary": "List course sub-categories", + "parameters": [ + { + "type": "integer", + "description": "Offset", + "name": "offset", + "in": "query" + }, + { + "type": "integer", + "description": "Limit", + "name": "limit", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.Response" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + }, "post": { "description": "Creates a sub-category under a course category", "consumes": [ @@ -1265,9 +1809,62 @@ } } }, + "/api/v1/course-management/sub-categories/{subCategoryId}/courses": { + "get": { + "description": "Returns courses for one sub-category", + "produces": [ + "application/json" + ], + "tags": [ + "course-management" + ], + "summary": "List courses by sub-category", + "parameters": [ + { + "type": "integer", + "description": "Sub-category ID", + "name": "subCategoryId", + "in": "path", + "required": true + }, + { + "type": "integer", + "description": "Offset", + "name": "offset", + "in": "query" + }, + { + "type": "integer", + "description": "Limit", + "name": "limit", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, "/api/v1/course-management/sub-module-lessons": { "post": { - "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", "consumes": [ "application/json" ], @@ -1277,15 +1874,15 @@ "tags": [ "course-management" ], - "summary": "Attach lesson to sub-module", + "summary": "Create lesson under sub-module", "parameters": [ { - "description": "Attach lesson payload", + "description": "Create lesson payload", "name": "body", "in": "body", "required": true, "schema": { - "$ref": "#/definitions/handlers.attachSubModuleLessonReq" + "$ref": "#/definitions/handlers.createSubModuleLessonReq" } } ], @@ -1311,6 +1908,113 @@ } } }, + "/api/v1/course-management/sub-module-lessons/{lessonId}": { + "get": { + "description": "Returns one active lesson detail by lesson ID", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "course-management" + ], + "summary": "Get lesson detail", + "parameters": [ + { + "type": "integer", + "description": "Lesson ID", + "name": "lessonId", + "in": "path", + "required": true + } + ], + "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" + } + } + } + }, + "put": { + "description": "Updates lesson teaching content, thumbnail, ordering, and active flag", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "course-management" + ], + "summary": "Update lesson detail", + "parameters": [ + { + "type": "integer", + "description": "Lesson ID", + "name": "lessonId", + "in": "path", + "required": true + }, + { + "description": "Update lesson payload", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handlers.updateSubModuleLessonReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, "/api/v1/course-management/sub-module-practices": { "post": { "description": "Creates a sub-module practice with metadata and linked question set", @@ -1404,6 +2108,44 @@ } }, "/api/v1/course-management/sub-modules": { + "get": { + "description": "Returns all sub-modules with pagination", + "produces": [ + "application/json" + ], + "tags": [ + "course-management" + ], + "summary": "List all sub-modules", + "parameters": [ + { + "type": "integer", + "description": "Offset", + "name": "offset", + "in": "query" + }, + { + "type": "integer", + "description": "Limit", + "name": "limit", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.Response" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + }, "post": { "description": "Creates a sub-module under a module", "consumes": [ @@ -1449,6 +2191,141 @@ } } }, + "/api/v1/course-management/sub-modules/{subModuleId}": { + "get": { + "description": "Returns one sub-module by ID", + "produces": [ + "application/json" + ], + "tags": [ + "course-management" + ], + "summary": "Get sub-module detail", + "parameters": [ + { + "type": "integer", + "description": "Sub-module ID", + "name": "subModuleId", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, + "/api/v1/course-management/sub-modules/{subModuleId}/lessons": { + "get": { + "description": "Returns all active lessons for a sub-module (teaching content metadata)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "course-management" + ], + "summary": "Get lessons under sub-module", + "parameters": [ + { + "type": "integer", + "description": "Sub-module ID", + "name": "subModuleId", + "in": "path", + "required": true + } + ], + "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" + } + } + } + } + }, + "/api/v1/course-management/sub-modules/{subModuleId}/practices": { + "get": { + "description": "Returns all active practices attached to a sub-module", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "course-management" + ], + "summary": "Get practices under sub-module", + "parameters": [ + { + "type": "integer", + "description": "Sub-module ID", + "name": "subModuleId", + "in": "path", + "required": true + } + ], + "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" + } + } + } + } + }, "/api/v1/files/audio": { "post": { "consumes": [ @@ -9350,26 +10227,6 @@ } } }, - "handlers.attachSubModuleLessonReq": { - "type": "object", - "properties": { - "display_order": { - "type": "integer" - }, - "intro_video_url": { - "type": "string" - }, - "is_active": { - "type": "boolean" - }, - "question_set_id": { - "type": "integer" - }, - "sub_module_id": { - "type": "integer" - } - } - }, "handlers.autoRenewReq": { "type": "object", "properties": { @@ -9677,6 +10534,41 @@ } } }, + "handlers.createSubModuleLessonReq": { + "type": "object", + "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" + } + } + }, "handlers.createSubModulePracticeReq": { "type": "object", "properties": { @@ -10209,6 +11101,41 @@ } } }, + "handlers.updateSubModuleLessonReq": { + "type": "object", + "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" + } + } + }, "handlers.verifyOTPReq": { "type": "object", "required": [ diff --git a/docs/swagger.yaml b/docs/swagger.yaml index ed61ae4..68ad4e7 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -911,19 +911,6 @@ definitions: required: - user_id 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: properties: auto_renew: @@ -1133,6 +1120,29 @@ definitions: - set_type - title 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: properties: description: @@ -1488,6 +1498,29 @@ definitions: title: type: string 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: properties: otp: @@ -2447,6 +2480,31 @@ paths: tags: - course-management /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: consumes: - application/json @@ -2503,6 +2561,36 @@ paths: summary: Delete course tags: - 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: consumes: - application/json @@ -2591,6 +2679,33 @@ paths: summary: Get course learning path tags: - 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: post: consumes: @@ -2643,7 +2758,84 @@ paths: summary: Get unified course hierarchy tags: - 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: + 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: consumes: - application/json @@ -2673,7 +2865,90 @@ paths: summary: Create level tags: - 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: + 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: consumes: - application/json @@ -2703,7 +2978,123 @@ paths: summary: Create module tags: - 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: + 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: consumes: - application/json @@ -2733,18 +3124,54 @@ paths: summary: Create course sub-category tags: - 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: post: consumes: - 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: - - description: Attach lesson payload + - description: Create lesson payload in: body name: body required: true schema: - $ref: '#/definitions/handlers.attachSubModuleLessonReq' + $ref: '#/definitions/handlers.createSubModuleLessonReq' produces: - application/json responses: @@ -2760,7 +3187,79 @@ paths: description: Internal Server Error schema: $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: - course-management /api/v1/course-management/sub-module-practices: @@ -2825,6 +3324,31 @@ paths: tags: - course-management /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: consumes: - application/json @@ -2854,6 +3378,95 @@ paths: summary: Create sub-module tags: - 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: post: consumes: diff --git a/gen/db/hierarchy.sql.go b/gen/db/hierarchy.sql.go index 66a3bf9..3b4899c 100644 --- a/gen/db/hierarchy.sql.go +++ b/gen/db/hierarchy.sql.go @@ -11,47 +11,6 @@ import ( "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 INSERT INTO course_sub_categories ( category_id, @@ -213,6 +172,78 @@ func (q *Queries) CreateSubModule(ctx context.Context, arg CreateSubModuleParams 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 INSERT INTO sub_module_practices ( sub_module_id, @@ -907,114 +938,62 @@ func (q *Queries) GetSubModuleByID(ctx context.Context, id int64) (SubModule, er } const GetSubModuleLessonByID = `-- name: GetSubModuleLessonByID :one -SELECT - smp.id, - smp.sub_module_id, - smp.question_set_id, - 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' +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 +FROM sub_module_lessons +WHERE id = $1 + AND is_active = TRUE ` -type GetSubModuleLessonByIDRow struct { - 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) { +func (q *Queries) GetSubModuleLessonByID(ctx context.Context, id int64) (SubModuleLesson, error) { row := q.db.QueryRow(ctx, GetSubModuleLessonByID, id) - var i GetSubModuleLessonByIDRow + var i SubModuleLesson err := row.Scan( &i.ID, &i.SubModuleID, - &i.QuestionSetID, - &i.IntroVideoUrl, &i.DisplayOrder, &i.IsActive, + &i.CreatedAt, &i.Title, &i.Description, - &i.Status, - &i.SetType, - &i.QuestionCount, + &i.Thumbnail, + &i.TeachingText, + &i.TeachingImageUrl, + &i.TeachingAudioUrl, + &i.TeachingVideoUrl, ) return i, err } const GetSubModuleLessons = `-- name: GetSubModuleLessons :many -SELECT - smp.id, - smp.sub_module_id, - smp.question_set_id, - 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.sub_module_id = $1 - AND smp.is_active = TRUE - AND qs.set_type = 'QUIZ' -ORDER BY smp.display_order ASC, smp.id ASC +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 +FROM sub_module_lessons +WHERE sub_module_id = $1 + AND is_active = TRUE +ORDER BY display_order ASC, id ASC ` -type GetSubModuleLessonsRow struct { - 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) { +func (q *Queries) GetSubModuleLessons(ctx context.Context, subModuleID int64) ([]SubModuleLesson, error) { rows, err := q.db.Query(ctx, GetSubModuleLessons, subModuleID) if err != nil { return nil, err } defer rows.Close() - var items []GetSubModuleLessonsRow + var items []SubModuleLesson for rows.Next() { - var i GetSubModuleLessonsRow + var i SubModuleLesson if err := rows.Scan( &i.ID, &i.SubModuleID, - &i.QuestionSetID, - &i.IntroVideoUrl, &i.DisplayOrder, &i.IsActive, + &i.CreatedAt, &i.Title, &i.Description, - &i.Status, - &i.SetType, - &i.QuestionCount, + &i.Thumbnail, + &i.TeachingText, + &i.TeachingImageUrl, + &i.TeachingAudioUrl, + &i.TeachingVideoUrl, ); err != nil { return nil, err } @@ -1242,28 +1221,43 @@ const UpdateSubModuleLesson = `-- name: UpdateSubModuleLesson :one UPDATE sub_module_lessons SET sub_module_id = $1, - question_set_id = $2, - intro_video_url = $3, - display_order = $4, - is_active = $5 -WHERE id = $6 -RETURNING id, sub_module_id, question_set_id, intro_video_url, display_order, is_active, created_at + title = $2, + description = $3, + thumbnail = $4, + teaching_text = $5, + teaching_image_url = $6, + 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 { - 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"` - ID int64 `json:"id"` + 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"` + 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) { row := q.db.QueryRow(ctx, UpdateSubModuleLesson, arg.SubModuleID, - arg.QuestionSetID, - arg.IntroVideoUrl, + arg.Title, + arg.Description, + arg.Thumbnail, + arg.TeachingText, + arg.TeachingImageUrl, + arg.TeachingAudioUrl, + arg.TeachingVideoUrl, arg.DisplayOrder, arg.IsActive, arg.ID, @@ -1272,11 +1266,16 @@ func (q *Queries) UpdateSubModuleLesson(ctx context.Context, arg UpdateSubModule err := row.Scan( &i.ID, &i.SubModuleID, - &i.QuestionSetID, - &i.IntroVideoUrl, &i.DisplayOrder, &i.IsActive, &i.CreatedAt, + &i.Title, + &i.Description, + &i.Thumbnail, + &i.TeachingText, + &i.TeachingImageUrl, + &i.TeachingAudioUrl, + &i.TeachingVideoUrl, ) return i, err } diff --git a/gen/db/models.go b/gen/db/models.go index 6a1cdaa..3015806 100644 --- a/gen/db/models.go +++ b/gen/db/models.go @@ -356,13 +356,18 @@ type SubModule struct { } type SubModuleLesson struct { - 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"` - CreatedAt pgtype.Timestamptz `json:"created_at"` + ID int64 `json:"id"` + SubModuleID int64 `json:"sub_module_id"` + DisplayOrder int32 `json:"display_order"` + IsActive bool `json:"is_active"` + CreatedAt pgtype.Timestamptz `json:"created_at"` + 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"` } type SubModulePractice struct { diff --git a/internal/web_server/handlers/hierarchy_handler.go b/internal/web_server/handlers/hierarchy_handler.go index fe5be6b..d1f11e1 100644 --- a/internal/web_server/handlers/hierarchy_handler.go +++ b/internal/web_server/handlers/hierarchy_handler.go @@ -101,35 +101,30 @@ type updatePracticeReq struct { IsActive *bool `json:"is_active"` } -type attachSubModuleLessonReq struct { - SubModuleID int64 `json:"sub_module_id"` - QuestionSetID int64 `json:"question_set_id"` - IntroVideoURL *string `json:"intro_video_url"` - DisplayOrder *int32 `json:"display_order"` - IsActive *bool `json:"is_active"` -} - -type updateLessonQuestionReq struct { - QuestionID int64 `json:"question_id"` - DisplayOrder *int32 `json:"display_order"` +type createSubModuleLessonReq struct { + SubModuleID int64 `json:"sub_module_id"` + Title string `json:"title"` + Description *string `json:"description"` + Thumbnail *string `json:"thumbnail"` + TeachingText *string `json:"teaching_text"` + TeachingImageURL *string `json:"teaching_image_url"` + TeachingAudioURL *string `json:"teaching_audio_url"` + TeachingVideoURL *string `json:"teaching_video_url"` + DisplayOrder *int32 `json:"display_order"` + IsActive *bool `json:"is_active"` } type updateSubModuleLessonReq struct { - SubModuleID *int64 `json:"sub_module_id"` - QuestionSetID *int64 `json:"question_set_id"` - IntroVideoURL *string `json:"intro_video_url"` - DisplayOrder *int32 `json:"display_order"` - IsActive *bool `json:"is_active"` - Title *string `json:"title"` - Description *string `json:"description"` - BannerImage *string `json:"banner_image"` - Persona *string `json:"persona"` - TimeLimitMinutes *int32 `json:"time_limit_minutes"` - 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"` + SubModuleID *int64 `json:"sub_module_id"` + Title *string `json:"title"` + Description *string `json:"description"` + Thumbnail *string `json:"thumbnail"` + TeachingText *string `json:"teaching_text"` + TeachingImageURL *string `json:"teaching_image_url"` + TeachingAudioURL *string `json:"teaching_audio_url"` + TeachingVideoURL *string `json:"teaching_video_url"` + DisplayOrder *int32 `json:"display_order"` + IsActive *bool `json:"is_active"` } type createSubModulePracticeReq struct { @@ -159,6 +154,16 @@ func toText(v *string) pgtype.Text { 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 { if v == nil { 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}) } -// AttachSubModuleLesson godoc -// @Summary Attach lesson to sub-module -// @Description Links a question set lesson to a sub-module +// CreateSubModuleLesson godoc +// @Summary Create lesson under sub-module +// @Description Creates a sub-module lesson with teaching content (text, image, audio, video URLs) and optional thumbnail // @Tags course-management // @Accept json // @Produce json -// @Param body body attachSubModuleLessonReq true "Attach lesson payload" +// @Param body body createSubModuleLessonReq true "Create lesson payload" // @Success 201 {object} domain.Response // @Failure 400 {object} domain.ErrorResponse // @Failure 500 {object} domain.ErrorResponse // @Router /api/v1/course-management/sub-module-lessons [post] -func (h *Handler) AttachSubModuleLesson(c *fiber.Ctx) error { - var req attachSubModuleLessonReq +func (h *Handler) CreateSubModuleLesson(c *fiber.Ctx) error { + var req createSubModuleLessonReq if err := c.BodyParser(&req); err != nil { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "Invalid request body", Error: err.Error()}) } - if req.SubModuleID <= 0 || req.QuestionSetID <= 0 { - return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "sub_module_id and question_set_id are required"}) + if req.SubModuleID <= 0 || strings.TrimSpace(req.Title) == "" { + 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{ - SubModuleID: req.SubModuleID, - QuestionSetID: req.QuestionSetID, - IntroVideoUrl: toText(req.IntroVideoURL), - Column4: intOrNil(req.DisplayOrder), - Column5: boolOrNil(req.IsActive), + created, err := h.analyticsDB.CreateSubModuleLesson(c.Context(), dbgen.CreateSubModuleLessonParams{ + SubModuleID: req.SubModuleID, + Title: strings.TrimSpace(req.Title), + Description: toText(req.Description), + Thumbnail: toText(req.Thumbnail), + 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 { - 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 // @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 // @Accept json // @Produce json @@ -1618,7 +1628,7 @@ func (h *Handler) GetSubModuleLessonByID(c *fiber.Ctx) error { // UpdateSubModuleLesson godoc // @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 // @Accept json // @Produce json @@ -1662,18 +1672,21 @@ func (h *Handler) UpdateSubModuleLesson(c *fiber.Ctx) error { targetSubModuleID = *req.SubModuleID } - targetQuestionSetID := currentLesson.QuestionSetID - if req.QuestionSetID != nil { - if *req.QuestionSetID <= 0 { - return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "question_set_id must be a positive integer"}) + targetTitle := currentLesson.Title + if req.Title != nil { + t := strings.TrimSpace(*req.Title) + if t == "" { + return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "title cannot be empty"}) } - targetQuestionSetID = *req.QuestionSetID + targetTitle = t } - targetIntroVideoURL := currentLesson.IntroVideoUrl - if req.IntroVideoURL != nil { - targetIntroVideoURL = toText(req.IntroVideoURL) - } + targetDescription := mergeTextField(currentLesson.Description, req.Description) + targetThumbnail := mergeTextField(currentLesson.Thumbnail, req.Thumbnail) + 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 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{ - SubModuleID: targetSubModuleID, - QuestionSetID: targetQuestionSetID, - IntroVideoUrl: targetIntroVideoURL, - DisplayOrder: targetDisplayOrder, - IsActive: targetIsActive, - ID: lessonID, + SubModuleID: targetSubModuleID, + Title: targetTitle, + Description: targetDescription, + Thumbnail: targetThumbnail, + TeachingText: targetTeachingText, + TeachingImageUrl: targetTeachingImage, + TeachingAudioUrl: targetTeachingAudio, + TeachingVideoUrl: targetTeachingVideo, + DisplayOrder: targetDisplayOrder, + IsActive: targetIsActive, + ID: lessonID, }); err != nil { return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ 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: ¤tSet.ShuffleQuestions, - } - - currentStatus := currentSet.Status - input.Status = ¤tStatus - - 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) if err != nil { 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{ Message: "Lesson updated successfully", - Data: map[string]interface{}{ - "lesson": updatedLesson, - "questions": updatedQuestions, - }, + Data: updatedLesson, }) } diff --git a/internal/web_server/routes.go b/internal/web_server/routes.go index 7225cec..5636fc3 100644 --- a/internal/web_server/routes.go +++ b/internal/web_server/routes.go @@ -116,10 +116,10 @@ func (a *App) initAppRoutes() { groupV1.Post("/course-management/sub-module-videos", a.authMiddleware, a.RequirePermission("videos.create"), h.CreateSubModuleVideo) groupV1.Put("/course-management/sub-module-videos/:videoId", a.authMiddleware, a.RequirePermission("videos.update"), h.UpdateSubModuleVideo) groupV1.Delete("/course-management/sub-module-videos/:videoId", a.authMiddleware, a.RequirePermission("videos.delete"), h.DeleteSubModuleVideo) - groupV1.Get("/course-management/sub-modules/:subModuleId/lessons", a.authMiddleware, a.RequirePermission("question_sets.list"), h.GetSubModuleLessons) - groupV1.Get("/course-management/sub-module-lessons/:lessonId", a.authMiddleware, a.RequirePermission("question_sets.get"), h.GetSubModuleLessonByID) - groupV1.Put("/course-management/sub-module-lessons/:lessonId", a.authMiddleware, a.RequirePermission("question_sets.update"), h.UpdateSubModuleLesson) - groupV1.Post("/course-management/sub-module-lessons", a.authMiddleware, a.RequirePermission("question_sets.update"), h.AttachSubModuleLesson) + groupV1.Get("/course-management/sub-modules/:subModuleId/lessons", a.authMiddleware, a.RequirePermission("learning_tree.get"), h.GetSubModuleLessons) + 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("subcourses.update"), h.UpdateSubModuleLesson) + 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/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)