mirror of
https://github.com/rgrgogu/new_starr.git
synced 2026-09-27 00:12:54 +08:00
ready to test
Testing Signed-off-by: Kenneth Obsequio <k80308392@gmail.com>
This commit is contained in:
@@ -0,0 +1,843 @@
|
||||
# Courses Controller Documentation
|
||||
|
||||
**File:** `controllers/admin/courses.controller.js`
|
||||
**Base URL:** `/api/admin/courses`
|
||||
**Guards:** `authenticate → requireAdmin() → adminLimiter`
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
- [Courses](#courses)
|
||||
- [Get All Courses](#get-all-courses)
|
||||
- [Get Single Course](#get-single-course)
|
||||
- [Create Course](#create-course)
|
||||
- [Update Course](#update-course)
|
||||
- [Archive Course](#archive-course)
|
||||
- [Bulk Archive Courses](#bulk-archive-courses)
|
||||
- [Restore Course](#restore-course)
|
||||
- [Bulk Restore Courses](#bulk-restore-courses)
|
||||
- [Get Archived Courses](#get-archived-courses)
|
||||
- [Get Archived Course](#get-archived-course)
|
||||
- [Flat Lists (dropdowns)](#flat-lists)
|
||||
- [Get Course Field Values](#get-course-field-values)
|
||||
- [Course Instructors](#course-instructors)
|
||||
- [Get Instructors](#get-instructors)
|
||||
- [Sync Instructors](#sync-instructors)
|
||||
- [Course Prerequisites](#course-prerequisites)
|
||||
- [Get Prerequisites](#get-prerequisites)
|
||||
- [Sync Prerequisites](#sync-prerequisites)
|
||||
- [Course Assessment](#course-assessment)
|
||||
- [Get Assessment](#get-assessment)
|
||||
- [Create Assessment](#create-assessment)
|
||||
- [Update Assessment](#update-assessment)
|
||||
- [Archive/Restore Assessment](#archiverestore-assessment)
|
||||
- [Quiz Questions](#quiz-questions)
|
||||
- [Get Questions](#get-questions)
|
||||
- [Create Question](#create-question)
|
||||
- [Update Question](#update-question)
|
||||
- [Archive/Restore Question](#archiverestore-question)
|
||||
- [Bulk Archive/Restore Questions](#bulk-archiverestore-questions)
|
||||
- [Units](#units)
|
||||
- [Get All Units](#get-all-units)
|
||||
- [Get Single Unit](#get-single-unit)
|
||||
- [Create Unit](#create-unit)
|
||||
- [Update Unit](#update-unit)
|
||||
- [Archive/Restore Unit](#archiverestore-unit)
|
||||
- [Bulk Archive/Restore Units](#bulk-archiverestore-units)
|
||||
- [Get Unit Field Values](#get-unit-field-values)
|
||||
- [Unit Quiz](#unit-quiz)
|
||||
- [Get Quiz](#get-quiz)
|
||||
- [Create Quiz](#create-quiz)
|
||||
- [Update Quiz](#update-quiz)
|
||||
- [Archive/Restore Quiz](#archiverestore-quiz)
|
||||
- [Lessons](#lessons)
|
||||
- [Get All Lessons](#get-all-lessons)
|
||||
- [Get Single Lesson](#get-single-lesson)
|
||||
- [Create Lesson](#create-lesson)
|
||||
- [Update Lesson](#update-lesson)
|
||||
- [Archive/Restore Lesson](#archiverestore-lesson)
|
||||
- [Bulk Archive/Restore Lessons](#bulk-archiverestore-lessons)
|
||||
- [Get Lesson Field Values](#get-lesson-field-values)
|
||||
- [Lesson Page](#lesson-page)
|
||||
- [Get Lesson Page](#get-lesson-page)
|
||||
- [Upsert Lesson Page](#upsert-lesson-page)
|
||||
- [Course Reading Progress](#course-reading-progress)
|
||||
- [Get Course Reading Progress](#get-course-reading-progress)
|
||||
- [Get User Reading Progress](#get-user-reading-progress)
|
||||
|
||||
---
|
||||
|
||||
## Course Hierarchy
|
||||
|
||||
```
|
||||
Course
|
||||
├── CourseObjective[]
|
||||
├── CoursePrerequisite[]
|
||||
├── CourseAssessment (one)
|
||||
│ └── QuizQuestion[] → QuizOption[]
|
||||
├── CourseInstructor[]
|
||||
└── Unit[]
|
||||
├── UnitQuiz (one)
|
||||
│ └── QuizQuestion[] → QuizOption[]
|
||||
└── Lesson[]
|
||||
├── LessonObjective[]
|
||||
└── LessonPage (one) { blocks: [] }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Courses
|
||||
|
||||
### Get All Courses
|
||||
|
||||
**`GET /api/admin/courses`**
|
||||
|
||||
Returns paginated active courses.
|
||||
|
||||
### Query Parameters
|
||||
| Parameter | Type | Required | Description |
|
||||
|-----------|--------|----------|-------------|
|
||||
| `page` | number | No | Default: `1` |
|
||||
| `limit` | number | No | Default: `10` |
|
||||
| `filters` | array | No | JSON filter array |
|
||||
| `sort` | array | No | JSON sort array |
|
||||
|
||||
### Response `200`
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"message": "Courses retrieved.",
|
||||
"data": [...],
|
||||
"pagination": { "page": 1, "limit": 10, "total": 12, "totalPages": 2 }
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Get Single Course
|
||||
|
||||
**`GET /api/admin/courses/:courseId`**
|
||||
|
||||
Returns the full course tree: units (with lessons and quiz), objectives, prerequisites, and assessment.
|
||||
|
||||
### Response `200`
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"message": "Course retrieved.",
|
||||
"data": {
|
||||
"course_id": 1,
|
||||
"uuid": "...",
|
||||
"title": "Advanced JavaScript",
|
||||
"description": "...",
|
||||
"course_code": "JS-201",
|
||||
"order_index": 0,
|
||||
"level": "advanced",
|
||||
"subscription": "premium",
|
||||
"duration_seconds": 7200,
|
||||
"objectives": [{ "objective_id": 1, "text": "Understand closures", "order_index": 0 }],
|
||||
"prerequisites": [],
|
||||
"assessment": { ... },
|
||||
"units": [
|
||||
{
|
||||
"unit_id": 1, "title": "Closures", "order_index": 0,
|
||||
"lessons": [{ "lesson_id": 1, "title": "What is a closure?", "order_index": 0 }],
|
||||
"quiz": { ... }
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Create Course
|
||||
|
||||
**`POST /api/admin/courses`**
|
||||
|
||||
Creates a course with optional objectives and category assignments in a single transaction.
|
||||
|
||||
### Request Body
|
||||
| Field | Type | Required | Description |
|
||||
|-------|------|----------|-------------|
|
||||
| `title` | string | **Yes** | Course title |
|
||||
| `description` | string | No | |
|
||||
| `course_code` | string | No | Unique course identifier |
|
||||
| `order_index` | number | No | Default: `0` |
|
||||
| `level` | string | No | `beginner`, `intermediate`, `advanced` |
|
||||
| `subscription` | string | No | `free`, `premium`. Default: `free` |
|
||||
| `objectives` | array | No | `[{ text, order_index }]` |
|
||||
| `category_ids` | array | No | Category IDs to assign |
|
||||
| `createdBy` | number | No | Creator user ID |
|
||||
|
||||
### Response `201`
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"message": "Course created.",
|
||||
"data": { "course_id": 5, "title": "Advanced JavaScript", ... }
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Update Course
|
||||
|
||||
**`PUT /api/admin/courses/:courseId`**
|
||||
|
||||
Updates course fields. When `objectives` or `category_ids` are provided, they replace the existing sets.
|
||||
|
||||
### Request Body (all optional)
|
||||
`title`, `description`, `order_index`, `course_code`, `level`, `subscription`, `objectives`, `category_ids`, `updatedBy`
|
||||
|
||||
---
|
||||
|
||||
### Archive Course
|
||||
|
||||
**`DELETE /api/admin/courses/:courseId`**
|
||||
|
||||
Soft-deletes the course.
|
||||
|
||||
### Response `200`
|
||||
```json
|
||||
{ "status": "success", "message": "Course archived." }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Bulk Archive Courses
|
||||
|
||||
**`DELETE /api/admin/courses/bulk`**
|
||||
|
||||
### Request Body
|
||||
```json
|
||||
{ "ids": [1, 2, 3] }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Restore Course
|
||||
|
||||
**`PATCH /api/admin/courses/:courseId/restore`**
|
||||
|
||||
---
|
||||
|
||||
### Bulk Restore Courses
|
||||
|
||||
**`PATCH /api/admin/courses/restore/bulk`**
|
||||
|
||||
### Request Body
|
||||
```json
|
||||
{ "ids": [1, 2] }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Get Archived Courses
|
||||
|
||||
**`GET /api/admin/courses/archives`**
|
||||
|
||||
---
|
||||
|
||||
### Get Archived Course
|
||||
|
||||
**`GET /api/admin/courses/archives/:courseId`**
|
||||
|
||||
---
|
||||
|
||||
### Flat Lists
|
||||
|
||||
Lightweight endpoints that return `uuid + title` arrays (no pagination). Used by the task requirement builder dropdowns.
|
||||
|
||||
**`GET /api/admin/courses/flat`** — all active courses: `[{ uuid, title }]`
|
||||
|
||||
**`GET /api/admin/courses/units-flat`** — all active units: `[{ uuid, title, order_index, course_title }]`
|
||||
|
||||
**`GET /api/admin/courses/lessons-flat`** — all active lessons: `[{ uuid, title, order_index, unit_title, unit_order, course_title }]`
|
||||
|
||||
---
|
||||
|
||||
### Get Course Field Values
|
||||
|
||||
**`GET /api/admin/courses/field-values`**
|
||||
|
||||
---
|
||||
|
||||
## Course Instructors
|
||||
|
||||
### Get Instructors
|
||||
|
||||
**`GET /api/admin/courses/:courseId/instructors`**
|
||||
|
||||
Returns instructors ordered by `order_index`. Includes linked `users` (staff/admin accounts) when `user_id` is set.
|
||||
|
||||
### Response `200`
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"message": "Instructors retrieved.",
|
||||
"data": [
|
||||
{
|
||||
"id": 1,
|
||||
"course_id": 5,
|
||||
"user_id": 3,
|
||||
"display_name": "Dr. Jane Smith",
|
||||
"order_index": 0,
|
||||
"user": { "user_id": 3, "email": "jane@example.com", "acc_type": "staff", "personal_info": { ... } }
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Sync Instructors
|
||||
|
||||
**`PUT /api/admin/courses/:courseId/instructors`**
|
||||
|
||||
Replaces the full instructor list for a course in a single transaction.
|
||||
|
||||
- Validates any `user_id` values — they must be `staff` or `admin` accounts.
|
||||
- Passing an empty array clears all instructors.
|
||||
|
||||
### Request Body
|
||||
```json
|
||||
{
|
||||
"instructors": [
|
||||
{ "user_id": 3, "display_name": "Dr. Jane Smith", "order_index": 0 },
|
||||
{ "user_id": null, "display_name": "External Contributor", "order_index": 1 }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Course Prerequisites
|
||||
|
||||
### Get Prerequisites
|
||||
|
||||
**`GET /api/admin/courses/:courseId/prerequisites`**
|
||||
|
||||
Returns prerequisites ordered by `order_index`.
|
||||
|
||||
### Response `200`
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"message": "Prerequisites retrieved.",
|
||||
"data": [
|
||||
{ "prereq_id": 1, "course_id": 5, "ref_type": "course", "ref_id": 2, "order_index": 0 }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Sync Prerequisites
|
||||
|
||||
**`PUT /api/admin/courses/:courseId/prerequisites`**
|
||||
|
||||
Replaces the full prerequisite list. Valid `ref_type` values: `course`, `unit`, `lesson`.
|
||||
|
||||
### Request Body
|
||||
```json
|
||||
{
|
||||
"prerequisites": [
|
||||
{ "ref_type": "course", "ref_id": 2 },
|
||||
{ "ref_type": "unit", "ref_id": 7 }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Course Assessment
|
||||
|
||||
One assessment per course. Assessment questions are shared with unit quizzes via polymorphic `assessment_id` / `quiz_id` fields.
|
||||
|
||||
### Get Assessment
|
||||
|
||||
**`GET /api/admin/courses/:courseId/assessment`**
|
||||
|
||||
Returns the assessment with its questions and options.
|
||||
|
||||
### Response `200`
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"message": "Assessment retrieved.",
|
||||
"data": {
|
||||
"assessment_id": 1,
|
||||
"uuid": "...",
|
||||
"course_id": 5,
|
||||
"title": "Final Exam",
|
||||
"is_required": true,
|
||||
"passing_score": 80,
|
||||
"time_limit_minutes": 60,
|
||||
"max_questions": 20,
|
||||
"questions": [
|
||||
{
|
||||
"question_id": 1, "type": "multiple_choice", "question": "What is a closure?",
|
||||
"explanation": "...", "points": 2, "order_index": 0,
|
||||
"options": [
|
||||
{ "option_id": 1, "text": "A function + its outer scope", "is_correct": true, "order_index": 0 }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Create Assessment
|
||||
|
||||
**`POST /api/admin/courses/:courseId/assessment`**
|
||||
|
||||
Only one assessment per course. Returns `409` if one already exists.
|
||||
|
||||
### Request Body
|
||||
| Field | Type | Required | Description |
|
||||
|-------|------|----------|-------------|
|
||||
| `title` | string | No | |
|
||||
| `is_required` | boolean | No | Default: `false` |
|
||||
| `passing_score` | number | No | Default: `70` |
|
||||
| `time_limit_minutes` | number | No | `null` = no limit |
|
||||
| `max_questions` | number | No | `null` = show all |
|
||||
| `createdBy` | number | No | |
|
||||
|
||||
---
|
||||
|
||||
### Update Assessment
|
||||
|
||||
**`PATCH /api/admin/courses/:courseId/assessment/:assessmentId`**
|
||||
|
||||
---
|
||||
|
||||
### Archive/Restore Assessment
|
||||
|
||||
**`DELETE /api/admin/courses/:courseId/assessment/:assessmentId`** — archive
|
||||
**`PATCH /api/admin/courses/:courseId/assessment/:assessmentId/restore`** — restore
|
||||
**`GET /api/admin/courses/:courseId/assessment/archives`** — get archived assessment
|
||||
|
||||
---
|
||||
|
||||
## Quiz Questions
|
||||
|
||||
Shared by both **Unit Quizzes** and **Course Assessments**. The parent is determined by the route:
|
||||
|
||||
- Under a unit quiz: `/:courseId/units/:unitId/quiz/:quizId/questions`
|
||||
- Under an assessment: `/:courseId/assessment/:assessmentId/questions`
|
||||
|
||||
### Question Types
|
||||
|
||||
| Type | Options Required |
|
||||
|------|----------------|
|
||||
| `true_false` | Auto-generated `[True, False]` if `options` is empty |
|
||||
| `multiple_choice` | Exactly one `is_correct: true` option |
|
||||
| `multi_select` | One or more `is_correct: true` options |
|
||||
|
||||
### Get Questions
|
||||
|
||||
**`GET .../:parentId/questions`**
|
||||
|
||||
Returns questions with options, ordered by `order_index`.
|
||||
|
||||
---
|
||||
|
||||
### Create Question
|
||||
|
||||
**`POST .../:parentId/questions`**
|
||||
|
||||
Creates a question and its options in a transaction. `true_false` questions auto-generate `[True, False]` options if `options` is omitted.
|
||||
|
||||
### Request Body
|
||||
| Field | Type | Required | Description |
|
||||
|-------|------|----------|-------------|
|
||||
| `type` | string | **Yes** | `true_false`, `multiple_choice`, `multi_select` |
|
||||
| `question` | string | **Yes** | Question text |
|
||||
| `explanation` | string | No | Shown after answer |
|
||||
| `order_index` | number | No | Default: `0` |
|
||||
| `points` | number | No | Default: `1` |
|
||||
| `options` | array | No | `[{ text, is_correct, order_index }]` |
|
||||
| `createdBy` | number | No | |
|
||||
|
||||
### Response `201`
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"message": "Question created.",
|
||||
"data": { "question_id": 1, "type": "multiple_choice", "options": [...] }
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Update Question
|
||||
|
||||
**`PATCH .../:parentId/questions/:questionId`**
|
||||
|
||||
When `options` is provided, the full option set is replaced (destroy + re-insert).
|
||||
|
||||
---
|
||||
|
||||
### Archive/Restore Question
|
||||
|
||||
**`DELETE .../:parentId/questions/:questionId`** — archive
|
||||
**`PATCH .../:parentId/questions/:questionId/restore`** — restore
|
||||
**`GET .../:parentId/questions/archives/:questionId`** — get archived question
|
||||
|
||||
---
|
||||
|
||||
### Bulk Archive/Restore Questions
|
||||
|
||||
**`DELETE .../:parentId/questions/bulk`** — bulk archive
|
||||
**`PATCH .../:parentId/questions/restore/bulk`** — bulk restore
|
||||
|
||||
### Request Body
|
||||
```json
|
||||
{ "ids": [1, 2, 3], "deletedBy": 1 }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Units
|
||||
|
||||
### Get All Units
|
||||
|
||||
**`GET /api/admin/courses/:courseId/units`**
|
||||
|
||||
Returns paginated active units for a course, ordered by `order_index`.
|
||||
|
||||
---
|
||||
|
||||
### Get Single Unit
|
||||
|
||||
**`GET /api/admin/courses/:courseId/units/:unitId`**
|
||||
|
||||
Returns a unit with its lessons and quiz.
|
||||
|
||||
### Response `200`
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"message": "Unit retrieved.",
|
||||
"data": {
|
||||
"unit_id": 1, "uuid": "...", "course_id": 5,
|
||||
"title": "Introduction", "description": "...",
|
||||
"order_index": 0, "duration_seconds": 1800,
|
||||
"lessons": [...],
|
||||
"quiz": { ... }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Create Unit
|
||||
|
||||
**`POST /api/admin/courses/:courseId/units`**
|
||||
|
||||
### Request Body
|
||||
| Field | Type | Required | Description |
|
||||
|-------|------|----------|-------------|
|
||||
| `title` | string | **Yes** | |
|
||||
| `description` | string | No | |
|
||||
| `order` | number | No | Default: `0` |
|
||||
| `createdBy` | number | No | |
|
||||
|
||||
---
|
||||
|
||||
### Update Unit
|
||||
|
||||
**`PUT /api/admin/courses/:courseId/units/:unitId`**
|
||||
|
||||
### Request Body (all optional)
|
||||
`title`, `description`, `order`, `updatedBy`
|
||||
|
||||
---
|
||||
|
||||
### Archive/Restore Unit
|
||||
|
||||
**`DELETE /api/admin/courses/:courseId/units/:unitId`** — archive
|
||||
**`PATCH /api/admin/courses/:courseId/units/:unitId/restore`** — restore
|
||||
**`GET /api/admin/courses/:courseId/units/archives`** — list archived
|
||||
**`GET /api/admin/courses/:courseId/units/archives/:unitId`** — get one archived
|
||||
|
||||
---
|
||||
|
||||
### Bulk Archive/Restore Units
|
||||
|
||||
**`DELETE /api/admin/courses/:courseId/units/bulk`** — bulk archive
|
||||
**`PATCH /api/admin/courses/:courseId/units/restore/bulk`** — bulk restore
|
||||
|
||||
```json
|
||||
{ "ids": [1, 2] }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Get Unit Field Values
|
||||
|
||||
**`GET /api/admin/courses/:courseId/field-values`**
|
||||
|
||||
---
|
||||
|
||||
## Unit Quiz
|
||||
|
||||
One quiz per unit. Shares `QuizQuestion` / `QuizOption` with course assessments (via `quiz_id` FK).
|
||||
|
||||
### Get Quiz
|
||||
|
||||
**`GET /api/admin/courses/:courseId/units/:unitId/quiz`**
|
||||
|
||||
Returns the quiz with questions and options.
|
||||
|
||||
---
|
||||
|
||||
### Create Quiz
|
||||
|
||||
**`POST /api/admin/courses/:courseId/units/:unitId/quiz`**
|
||||
|
||||
Only one quiz per unit. Returns `409` if one already exists.
|
||||
|
||||
### Request Body
|
||||
| Field | Type | Required | Description |
|
||||
|-------|------|----------|-------------|
|
||||
| `title` | string | No | |
|
||||
| `is_required` | boolean | No | Default: `false` |
|
||||
| `passing_score` | number | No | Default: `70` |
|
||||
| `max_questions` | number | No | `null` = show all |
|
||||
| `createdBy` | number | No | |
|
||||
|
||||
---
|
||||
|
||||
### Update Quiz
|
||||
|
||||
**`PATCH /api/admin/courses/:courseId/units/:unitId/quiz/:quizId`**
|
||||
|
||||
### Request Body (all optional)
|
||||
`title`, `is_required`, `passing_score`, `max_questions`, `updatedBy`
|
||||
|
||||
---
|
||||
|
||||
### Archive/Restore Quiz
|
||||
|
||||
**`DELETE /api/admin/courses/:courseId/units/:unitId/quiz/:quizId`** — archive
|
||||
**`PATCH /api/admin/courses/:courseId/units/:unitId/quiz/:quizId/restore`** — restore
|
||||
**`GET /api/admin/courses/:courseId/units/:unitId/quiz/archives`** — get archived quiz
|
||||
|
||||
---
|
||||
|
||||
## Lessons
|
||||
|
||||
### Get All Lessons
|
||||
|
||||
**`GET /api/admin/courses/:courseId/units/:unitId/lessons`**
|
||||
|
||||
Returns paginated active lessons, ordered by `order_index`.
|
||||
|
||||
---
|
||||
|
||||
### Get Single Lesson
|
||||
|
||||
**`GET /api/admin/courses/:courseId/units/:unitId/lessons/:lessonId`**
|
||||
|
||||
Returns a lesson with its page (blocks) and objectives.
|
||||
|
||||
### Response `200`
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"message": "Lesson retrieved.",
|
||||
"data": {
|
||||
"lesson_id": 1, "uuid": "...", "unit_id": 1,
|
||||
"title": "What is a closure?", "description": "...",
|
||||
"duration_seconds": 900, "order_index": 0,
|
||||
"page": { "page_id": 1, "lesson_id": 1, "blocks": [...] },
|
||||
"objectives": [{ "objective_id": 1, "text": "Understand closures", "order_index": 0 }],
|
||||
"unit": { "unit_id": 1, "course_id": 5, ... }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Create Lesson
|
||||
|
||||
**`POST /api/admin/courses/:courseId/units/:unitId/lessons`**
|
||||
|
||||
Creates a lesson, an empty `LessonPage` (blocks: `[]`), and optional objectives in a single transaction.
|
||||
|
||||
### Request Body
|
||||
| Field | Type | Required | Description |
|
||||
|-------|------|----------|-------------|
|
||||
| `title` | string | **Yes** | |
|
||||
| `description` | string | No | |
|
||||
| `order` | number | No | Default: `0` |
|
||||
| `objectives` | array | No | `[{ text, order_index }]` |
|
||||
| `createdBy` | number | No | |
|
||||
|
||||
---
|
||||
|
||||
### Update Lesson
|
||||
|
||||
**`PUT /api/admin/courses/:courseId/units/:unitId/lessons/:lessonId`**
|
||||
|
||||
When `objectives` is provided, the full set is replaced.
|
||||
|
||||
### Request Body (all optional)
|
||||
`title`, `description`, `order`, `objectives`, `updatedBy`
|
||||
|
||||
---
|
||||
|
||||
### Archive/Restore Lesson
|
||||
|
||||
**`DELETE /api/admin/courses/:courseId/units/:unitId/lessons/:lessonId`** — archive
|
||||
**`PATCH /api/admin/courses/:courseId/units/:unitId/lessons/:lessonId/restore`** — restore
|
||||
**`GET /api/admin/courses/:courseId/units/:unitId/lessons/archives`** — list archived
|
||||
**`GET /api/admin/courses/:courseId/units/:unitId/lessons/archives/:lessonId`** — get one archived
|
||||
|
||||
---
|
||||
|
||||
### Bulk Archive/Restore Lessons
|
||||
|
||||
**`DELETE /api/admin/courses/:courseId/units/:unitId/lessons/bulk`** — bulk archive
|
||||
**`PATCH /api/admin/courses/:courseId/units/:unitId/lessons/restore/bulk`** — bulk restore
|
||||
|
||||
```json
|
||||
{ "ids": [1, 2] }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Get Lesson Field Values
|
||||
|
||||
**`GET /api/admin/courses/:courseId/units/:unitId/field-values`**
|
||||
|
||||
---
|
||||
|
||||
## Lesson Page
|
||||
|
||||
Each lesson has exactly **one** page. A page is created automatically when a lesson is created (with empty `blocks`).
|
||||
|
||||
### Get Lesson Page
|
||||
|
||||
**`GET /api/admin/courses/:courseId/units/:unitId/lessons/:lessonId/page`**
|
||||
|
||||
### Response `200`
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"message": "Lesson page retrieved.",
|
||||
"data": {
|
||||
"page_id": 1,
|
||||
"lesson_id": 3,
|
||||
"blocks": [
|
||||
{ "type": "text", "content": "A closure is..." },
|
||||
{ "type": "video", "asset_id": 12, "duration_seconds": 300 }
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Upsert Lesson Page
|
||||
|
||||
**`PUT /api/admin/courses/:courseId/units/:unitId/lessons/:lessonId/page`**
|
||||
|
||||
Creates or replaces the lesson page's block content. After saving, `duration_seconds` on the lesson (and its ancestor unit and course) is automatically recomputed from video blocks.
|
||||
|
||||
### Request Body
|
||||
| Field | Type | Required | Description |
|
||||
|-------|------|----------|-------------|
|
||||
| `blocks` | array | **Yes** | Block array |
|
||||
| `updatedBy` | number | No | |
|
||||
|
||||
### Response `200` (updated) / `201` (created)
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"message": "Lesson page updated.",
|
||||
"data": { ... }
|
||||
}
|
||||
```
|
||||
|
||||
### Error Responses
|
||||
| Status | Message |
|
||||
|--------|---------|
|
||||
| `400` | `blocks must be an array.` |
|
||||
| `404` | `Lesson not found.` |
|
||||
|
||||
---
|
||||
|
||||
## Course Reading Progress
|
||||
|
||||
> These endpoints are served from `controllers/admin/course_reading_progress.controller.js` but mounted on the `/api/admin/courses` router.
|
||||
|
||||
### Get Course Reading Progress
|
||||
|
||||
**`GET /api/admin/courses/:courseId/reading-progress`**
|
||||
|
||||
Returns one summary row per user who has touched the course. Includes lesson/unit completion counts derived from the live course structure (not from stored counters).
|
||||
|
||||
Sorted: in-progress users first (most recent access first), completed users last.
|
||||
|
||||
### Response `200`
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"message": "Course reading progress retrieved.",
|
||||
"data": [
|
||||
{
|
||||
"user_id": 42,
|
||||
"course_status": "in_progress",
|
||||
"last_accessed_at": "2026-06-21T09:00:00.000Z",
|
||||
"lessons_completed": 3,
|
||||
"units_completed": 1,
|
||||
"user": {
|
||||
"email": "user@example.com",
|
||||
"full_name": "Jane Doe",
|
||||
"avatar_url": "https://..."
|
||||
},
|
||||
"units_total": 4,
|
||||
"lessons_total": 12
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Get User Reading Progress
|
||||
|
||||
**`GET /api/admin/courses/:courseId/reading-progress/users/:userId`**
|
||||
|
||||
Loaded lazily when the admin expands a user row. Returns the full unit → lesson breakdown with progress status per item.
|
||||
|
||||
### Response `200`
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"message": "User reading progress retrieved.",
|
||||
"data": [
|
||||
{
|
||||
"unit_id": 1, "uuid": "...", "title": "Introduction",
|
||||
"status": "completed",
|
||||
"lessons": [
|
||||
{
|
||||
"lesson_id": 1, "uuid": "...", "title": "What is a closure?",
|
||||
"status": "completed",
|
||||
"completed_at": "2026-06-20T10:00:00.000Z"
|
||||
},
|
||||
{
|
||||
"lesson_id": 2, "uuid": "...", "title": "Closure examples",
|
||||
"status": null,
|
||||
"completed_at": null
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
Reference in New Issue
Block a user