import { Get, Body, Post, Route, Tags, SuccessResponse, Response, Security, Put, Path, Delete, Request, Example, FormField, UploadedFile, Query } from 'tsoa'; import { ValidationError } from '../middleware/errorHandler'; import { CoursesInstructorService } from '../services/CoursesInstructor.service'; import { createCourseResponse, ListMyCourseResponse, GetMyCourseResponse, UpdateMyCourse, UpdateMyCourseResponse, DeleteMyCourseResponse, submitCourseResponse, listinstructorCourseResponse, addinstructorCourseResponse, removeinstructorCourseResponse, setprimaryCourseInstructorResponse, GetEnrolledStudentsResponse, GetEnrolledStudentDetailResponse, GetQuizScoresResponse, GetQuizAttemptDetailResponse, GetCourseApprovalsResponse, SearchInstructorResponse, GetCourseApprovalHistoryResponse, setCourseDraftResponse, CloneCourseResponse, } from '../types/CoursesInstructor.types'; import { CreateCourseValidator, UpdateCourseValidator, CloneCourseValidator } from "../validators/CoursesInstructor.validator"; import jwt from 'jsonwebtoken'; import { config } from '../config'; @Route('api/instructors/courses') @Tags('CoursesInstructor') export class CoursesInstructorController { /** * ดึงรายการคอร์สทั้งหมดของผู้สอน * Get all courses where the authenticated user is an instructor */ @Get('') @Security('jwt', ['instructor']) @SuccessResponse('200', 'Courses retrieved successfully') @Response('401', 'Invalid or expired token') @Response('404', 'Courses not found') public async listMyCourses( @Request() request: any, @Query() status?: 'DRAFT' | 'PENDING' | 'APPROVED' | 'REJECTED' | 'ARCHIVED' ): Promise { const token = request.headers.authorization?.replace('Bearer ', ''); if (!token) { throw new ValidationError('No token provided'); } return await CoursesInstructorService.listMyCourses({ token, status }); } /** * ค้นหาผู้สอนทั้งหมดในระบบ (ไม่รวมตัวเองและคนที่อยู่ในคอร์สแล้ว) * Search all instructors in database (excluding self and existing course instructors) * @param courseId - รหัสคอร์ส / Course ID * @param query - คำค้นหา (email หรือ username) / Search query (email or username) */ @Get('{courseId}/search-instructors') @Security('jwt', ['instructor']) @SuccessResponse('200', 'Instructors found') @Response('401', 'Invalid or expired token') public async searchInstructors( @Request() request: any, @Path() courseId: number, @Query() query: string ): Promise { const token = request.headers.authorization?.replace('Bearer ', ''); if (!token) throw new ValidationError('No token provided'); return await CoursesInstructorService.searchInstructors({ token, query, course_id: courseId }); } /** * ดึงข้อมูลคอร์สเฉพาะของผู้สอน (พร้อมบทเรียนและเนื้อหา) * Get detailed course information including chapters, lessons, attachments, and quizzes * @param courseId - รหัสคอร์ส / Course ID */ @Get('{courseId}') @Security('jwt', ['instructor']) @SuccessResponse('200', 'Course retrieved successfully') @Response('401', 'Invalid or expired token') @Response('404', 'Course not found') public async getMyCourse(@Request() request: any, @Path() courseId: number): Promise { const token = request.headers.authorization?.replace('Bearer ', ''); if (!token) { throw new ValidationError('No token provided'); } return await CoursesInstructorService.getmyCourse({ token, course_id: courseId }); } /** * แก้ไขข้อมูลคอร์ส * Update course information (only for course instructors) * @param courseId - รหัสคอร์ส / Course ID */ @Put('{courseId}') @Security('jwt', ['instructor']) @SuccessResponse('200', 'Course updated successfully') @Response('401', 'Invalid or expired token') @Response('404', 'Course not found') public async updateCourse(@Request() request: any, @Path() courseId: number, @Body() body: UpdateMyCourse): Promise { const token = request.headers.authorization?.replace('Bearer ', ''); if (!token) throw new ValidationError('No token provided'); const { error } = UpdateCourseValidator.validate(body.data); if (error) throw new ValidationError(error.details[0].message); return await CoursesInstructorService.updateCourse(token, courseId, body.data); } /** * สร้างคอร์สใหม่ (พร้อมอัปโหลดรูป thumbnail) * Create a new course with optional thumbnail upload (status will be DRAFT by default) * @param data - JSON string containing course data * @param thumbnail - Optional thumbnail image file */ @Post('') @Security('jwt', ['instructor']) @SuccessResponse('201', 'Course created successfully') @Response('400', 'Validation error') @Response('500', 'Internal server error') public async createCourse( @Request() request: any, @FormField() data: string, @UploadedFile() thumbnail?: Express.Multer.File ): Promise { const token = request.headers.authorization?.replace('Bearer ', ''); if (!token) throw new ValidationError('No token provided'); const decoded = jwt.verify(token, config.jwt.secret) as { id: number }; const parsed = JSON.parse(data); const { error, value } = CreateCourseValidator.validate(parsed); if (error) throw new ValidationError(error.details[0].message); // Validate thumbnail file type if provided if (thumbnail && !thumbnail.mimetype?.startsWith('image/')) throw new ValidationError('Only image files are allowed for thumbnail'); return await CoursesInstructorService.createCourse(value, decoded.id, thumbnail); } /** * อัปโหลดรูป thumbnail ของคอร์ส * Upload course thumbnail image * @param courseId - รหัสคอร์ส / Course ID * @param file - ไฟล์รูปภาพ / Image file */ @Post('{courseId}/thumbnail') @Security('jwt', ['instructor']) @SuccessResponse('200', 'Thumbnail uploaded successfully') @Response('401', 'Invalid or expired token') @Response('400', 'Validation error') public async uploadThumbnail( @Request() request: any, @Path() courseId: number, @UploadedFile() file: Express.Multer.File ): Promise<{ code: number; message: string; data: { course_id: number; thumbnail_url: string } }> { const token = request.headers.authorization?.replace('Bearer ', ''); if (!token) throw new ValidationError('No token provided'); if (!file.mimetype?.startsWith('image/')) throw new ValidationError('Only image files are allowed'); return await CoursesInstructorService.uploadThumbnail(token, courseId, file); } /** * ลบคอร์ส (เฉพาะผู้สอนหลักเท่านั้น) * Delete a course (only primary instructor can delete) * @param courseId - รหัสคอร์ส / Course ID */ @Delete('{courseId}') @Security('jwt', ['instructor']) @SuccessResponse('200', 'Course deleted successfully') @Response('401', 'Invalid or expired token') @Response('404', 'Course not found') public async deleteCourse(@Request() request: any, @Path() courseId: number): Promise { const token = request.headers.authorization?.replace('Bearer ', ''); if (!token) throw new ValidationError('No token provided') return await CoursesInstructorService.deleteCourse(token, courseId); } /** * คัดลอกคอร์ส (Clone Course) * Clone an existing course to a new one with copied chapters, lessons, quizzes, and attachments * @param courseId - รหัสคอร์สต้นฉบับ / Source Course ID * @param body - ชื่อคอร์สใหม่ / New course title */ @Post('{courseId}/clone') @Security('jwt', ['instructor']) @SuccessResponse('201', 'Course cloned successfully') @Response('401', 'Invalid or expired token') @Response('403', 'Not an instructor of this course') @Response('404', 'Course not found') public async cloneCourse( @Request() request: any, @Path() courseId: number, @Body() body: { title: { th: string; en: string } } ): Promise { const token = request.headers.authorization?.replace('Bearer ', ''); if (!token) throw new ValidationError('No token provided'); const { error } = CloneCourseValidator.validate(body); if (error) throw new ValidationError(error.details[0].message); const result = await CoursesInstructorService.cloneCourse({ token, course_id: courseId, title: body.title }); return result; } /** * ส่งคอร์สเพื่อขออนุมัติจากแอดมิน * Submit course for admin review and approval * @param courseId - รหัสคอร์ส / Course ID */ @Post('send-review/{courseId}') @Security('jwt', ['instructor']) @SuccessResponse('200', 'Course submitted successfully') @Response('401', 'Invalid or expired token') @Response('404', 'Course not found') public async submitCourse(@Request() request: any, @Path() courseId: number): Promise { const token = request.headers.authorization?.replace('Bearer ', ''); if (!token) throw new ValidationError('No token provided') return await CoursesInstructorService.sendCourseForReview({ token, course_id: courseId }); } /** * ตั้งค่าคอร์สเป็น DRAFT * Set course to draft * @param courseId - รหัสคอร์ส / Course ID */ @Post('set-draft/{courseId}') @Security('jwt', ['instructor']) @SuccessResponse('200', 'Course set to draft successfully') @Response('401', 'Invalid or expired token') @Response('404', 'Course not found') public async setCourseDraft(@Request() request: any, @Path() courseId: number): Promise { const token = request.headers.authorization?.replace('Bearer ', ''); if (!token) throw new ValidationError('No token provided') return await CoursesInstructorService.setCourseDraft({ token, course_id: courseId }); } /** * ดึงประวัติการส่งอนุมัติคอร์สทั้งหมด * Get all course approval history * @param courseId - รหัสคอร์ส / Course ID */ @Get('{courseId}/approvals') @Security('jwt', ['instructor']) @SuccessResponse('200', 'Course approvals retrieved successfully') @Response('401', 'Invalid or expired token') @Response('403', 'You are not an instructor of this course') @Response('404', 'Course not found') public async getCourseApprovals(@Request() request: any, @Path() courseId: number): Promise { const token = request.headers.authorization?.replace('Bearer ', ''); if (!token) throw new ValidationError('No token provided') return await CoursesInstructorService.getCourseApprovals(token, courseId); } /** * ดึงรายชื่อผู้สอนทั้งหมดในคอร์ส * Get list of all instructors in a specific course * @param courseId - รหัสคอร์ส / Course ID */ @Get('listinstructor/{courseId}') @Security('jwt', ['instructor']) @SuccessResponse('200', 'Instructors retrieved successfully') @Response('401', 'Invalid or expired token') @Response('404', 'Instructors not found') public async listInstructorCourses(@Request() request: any, @Path() courseId: number): Promise { const token = request.headers.authorization?.replace('Bearer ', ''); if (!token) throw new ValidationError('No token provided') return await CoursesInstructorService.listInstructorsOfCourse({ token, course_id: courseId }); } /** * เพิ่มผู้สอนเข้าในคอร์ส (ใช้ email หรือ username) * Add a new instructor to the course (using email or username) * @param courseId - รหัสคอร์ส / Course ID * @param emailOrUsername - email หรือ username ของผู้สอน / Instructor's email or username */ @Post('add-instructor/{courseId}/{emailOrUsername}') @Security('jwt', ['instructor']) @SuccessResponse('200', 'Instructor added successfully') @Response('401', 'Invalid or expired token') @Response('404', 'Instructor not found') public async addInstructor(@Request() request: any, @Path() courseId: number, @Path() emailOrUsername: string): Promise { const token = request.headers.authorization?.replace('Bearer ', ''); if (!token) throw new ValidationError('No token provided') return await CoursesInstructorService.addInstructorToCourse({ token, course_id: courseId, email_or_username: emailOrUsername }); } /** * ลบผู้สอนออกจากคอร์ส * Remove an instructor from the course * @param courseId - รหัสคอร์ส / Course ID * @param userId - รหัสผู้ใช้ที่ต้องการลบออกจากผู้สอน / User ID to remove from instructors */ @Delete('remove-instructor/{courseId}/{userId}') @Security('jwt', ['instructor']) @SuccessResponse('200', 'Instructor removed successfully') @Response('401', 'Invalid or expired token') @Response('404', 'Instructor not found') public async removeInstructor(@Request() request: any, @Path() courseId: number, @Path() userId: number): Promise { const token = request.headers.authorization?.replace('Bearer ', ''); if (!token) throw new ValidationError('No token provided') return await CoursesInstructorService.removeInstructorFromCourse({ token, course_id: courseId, user_id: userId }); } /** * กำหนดผู้สอนหลักของคอร์ส * Set a user as the primary instructor of the course * @param courseId - รหัสคอร์ส / Course ID * @param userId - รหัสผู้ใช้ที่ต้องการตั้งเป็นผู้สอนหลัก / User ID to set as primary instructor */ @Put('set-primary-instructor/{courseId}/{userId}') @Security('jwt', ['instructor']) @SuccessResponse('200', 'Primary instructor set successfully') @Response('401', 'Invalid or expired token') @Response('404', 'Primary instructor not found') public async setPrimaryInstructor(@Request() request: any, @Path() courseId: number, @Path() userId: number): Promise { const token = request.headers.authorization?.replace('Bearer ', ''); if (!token) throw new ValidationError('No token provided') return await CoursesInstructorService.setPrimaryInstructor({ token, course_id: courseId, user_id: userId }); } /** * ดึงรายชื่อนักเรียนที่ลงทะเบียนในคอร์สพร้อม progress * Get all enrolled students with their progress * @param courseId - รหัสคอร์ส / Course ID * @param page - หน้าที่ต้องการ / Page number * @param limit - จำนวนต่อหน้า / Items per page * @param search - ค้นหาด้วย firstname, lastname, email, username * @param status - กรองตามสถานะ (ENROLLED, COMPLETED) */ @Get('{courseId}/students') @Security('jwt', ['instructor']) @SuccessResponse('200', 'Enrolled students retrieved successfully') @Response('401', 'Invalid or expired token') @Response('403', 'Not an instructor of this course') public async getEnrolledStudents( @Request() request: any, @Path() courseId: number, @Query() page?: number, @Query() limit?: number, @Query() search?: string, @Query() status?: 'ENROLLED' | 'COMPLETED' ): Promise { const token = request.headers.authorization?.replace('Bearer ', ''); if (!token) throw new ValidationError('No token provided'); return await CoursesInstructorService.getEnrolledStudents({ token, course_id: courseId, page, limit, search, status, }); } /** * ดึงรายละเอียดการเรียนของนักเรียนแต่ละคน (progress ของแต่ละ lesson) * Get enrolled student detail with lesson progress * @param courseId - รหัสคอร์ส / Course ID * @param studentId - รหัสนักเรียน / Student ID */ @Get('{courseId}/students/{studentId}') @Security('jwt', ['instructor']) @SuccessResponse('200', 'Enrolled student detail retrieved successfully') @Response('401', 'Invalid or expired token') @Response('403', 'Not an instructor of this course') @Response('404', 'Student not found or not enrolled') public async getEnrolledStudentDetail( @Request() request: any, @Path() courseId: number, @Path() studentId: number ): Promise { const token = request.headers.authorization?.replace('Bearer ', ''); if (!token) throw new ValidationError('No token provided'); return await CoursesInstructorService.getEnrolledStudentDetail({ token, course_id: courseId, student_id: studentId, }); } /** * ดึงคะแนนสอบของนักเรียนทุกคนในแต่ละ lesson quiz * Get quiz scores of all students for a specific lesson * @param courseId - รหัสคอร์ส / Course ID * @param lessonId - รหัสบทเรียน / Lesson ID * @param page - หน้าที่ต้องการ / Page number * @param limit - จำนวนต่อหน้า / Items per page * @param search - ค้นหาด้วย firstname, lastname, email, username * @param isPassed - กรองตามผลสอบ (true = ผ่าน, false = ไม่ผ่าน) */ @Get('{courseId}/lessons/{lessonId}/quiz/scores') @Security('jwt', ['instructor']) @SuccessResponse('200', 'Quiz scores retrieved successfully') @Response('401', 'Invalid or expired token') @Response('403', 'Not an instructor of this course') @Response('404', 'Lesson or quiz not found') public async getQuizScores( @Request() request: any, @Path() courseId: number, @Path() lessonId: number, @Query() page?: number, @Query() limit?: number, @Query() search?: string, @Query() isPassed?: boolean ): Promise { const token = request.headers.authorization?.replace('Bearer ', ''); if (!token) throw new ValidationError('No token provided'); return await CoursesInstructorService.getQuizScores({ token, course_id: courseId, lesson_id: lessonId, page, limit, search, is_passed: isPassed, }); } /** * ดูรายละเอียดการทำข้อสอบล่าสุดของนักเรียนแต่ละคน * Get latest quiz attempt detail for a specific student * @param courseId - รหัสคอร์ส / Course ID * @param lessonId - รหัสบทเรียน / Lesson ID * @param studentId - รหัสนักเรียน / Student ID */ @Get('{courseId}/lessons/{lessonId}/quiz/students/{studentId}') @Security('jwt', ['instructor']) @SuccessResponse('200', 'Quiz attempt detail retrieved successfully') @Response('401', 'Invalid or expired token') @Response('403', 'Not an instructor of this course') @Response('404', 'Student or quiz attempt not found') public async getQuizAttemptDetail( @Request() request: any, @Path() courseId: number, @Path() lessonId: number, @Path() studentId: number ): Promise { const token = request.headers.authorization?.replace('Bearer ', ''); if (!token) throw new ValidationError('No token provided'); return await CoursesInstructorService.getQuizAttemptDetail({ token, course_id: courseId, lesson_id: lessonId, student_id: studentId, }); } /** * ดึงประวัติการขออนุมัติคอร์ส * Get course approval history for instructor to see rejection reasons * @param courseId - รหัสคอร์ส / Course ID */ @Get('{courseId}/approval-history') @Security('jwt', ['instructor']) @SuccessResponse('200', 'Approval history retrieved successfully') @Response('401', 'Invalid or expired token') @Response('403', 'Not an instructor of this course') @Response('404', 'Course not found') public async getCourseApprovalHistory( @Request() request: any, @Path() courseId: number ): Promise { const token = request.headers.authorization?.replace('Bearer ', ''); if (!token) throw new ValidationError('No token provided'); return await CoursesInstructorService.getCourseApprovalHistory(token, courseId); } }