import { prisma } from '../config/database'; import { Prisma } from '@prisma/client'; import { config } from '../config'; import { logger } from '../config/logger'; import { deleteFile, generateFilePath, uploadFile } from '../config/minio'; import { UnauthorizedError, ValidationError, ForbiddenError, NotFoundError } from '../middleware/errorHandler'; import jwt from 'jsonwebtoken'; import { LessonData, ChapterData, CreateLessonInput, CreateChapterInput, UpdateChapterInput, ChaptersRequest, DeleteChapterRequest, ReorderChapterRequest, ListChaptersResponse, CreateChapterResponse, UpdateChapterResponse, DeleteChapterResponse, ReorderChapterResponse, GetLessonRequest, UpdateLessonRequest, DeleteLessonRequest, ReorderLessonsRequest, GetLessonResponse, CreateLessonResponse, UpdateLessonResponse, DeleteLessonResponse, ReorderLessonsResponse, AddVideoToLessonInput, UpdateVideoLessonInput, AddQuestionInput, AddQuestionResponse, UpdateQuestionInput, UpdateQuestionResponse, DeleteQuestionInput, DeleteQuestionResponse, QuizQuestionData, } from "../types/ChaptersLesson.typs"; import { CoursesInstructorService } from './CoursesInstructor.service'; /** * ตรวจสอบสิทธิ์เข้าถึง Course (สำหรับทั้ง Instructor และ Student) * Returns: { hasAccess: boolean, role: 'INSTRUCTOR' | 'STUDENT' | null, userId: number } */ async function validateCourseAccess(token: string, course_id: number): Promise<{ hasAccess: boolean; role: 'INSTRUCTOR' | 'STUDENT' | null; userId: number; }> { const decodedToken = jwt.verify(token, config.jwt.secret) as { id: number }; const userId = decodedToken.id; const user = await prisma.user.findUnique({ where: { id: userId } }); if (!user) { throw new UnauthorizedError('Invalid token'); } // Check if user is an instructor for this course const courseInstructor = await prisma.courseInstructor.findFirst({ where: { course_id, user_id: userId } }); if (courseInstructor) { return { hasAccess: true, role: 'INSTRUCTOR', userId }; } // Check if user is enrolled in this course const enrollment = await prisma.enrollment.findFirst({ where: { course_id, user_id: userId, status: { in: ['ENROLLED', 'IN_PROGRESS', 'COMPLETED'] } } }); if (enrollment) { return { hasAccess: true, role: 'STUDENT', userId }; } return { hasAccess: false, role: null, userId }; } export class ChaptersLessonService { async listChapters(request: ChaptersRequest): Promise { try { const { token, course_id } = request; const decodedToken = jwt.verify(token, config.jwt.secret) as { id: number }; const user = await prisma.user.findUnique({ where: { id: decodedToken.id } }); if (!user) { throw new UnauthorizedError('Invalid token'); } const chapters = await prisma.chapter.findMany({ where: { course_id }, orderBy: { sort_order: 'asc' }, include: { lessons: { orderBy: { sort_order: 'asc' } } } }); return { code: 200, message: 'Chapters fetched successfully', data: chapters as ChapterData[], total: chapters.length }; } catch (error) { logger.error(`Error fetching chapters: ${error}`); throw error; } } async createChapter(request: CreateChapterInput): Promise { try { const { token, course_id, title, description, sort_order } = request; const decodedToken = jwt.verify(token, config.jwt.secret) as { id: number }; await CoursesInstructorService.validateCourseStatus(course_id); const user = await prisma.user.findUnique({ where: { id: decodedToken.id } }); if (!user) { throw new UnauthorizedError('Invalid token'); } const courseInstructor = await CoursesInstructorService.validateCourseInstructor(token, course_id); if (!courseInstructor) { throw new ForbiddenError('You are not permitted to create chapter'); } const chapter = await prisma.chapter.create({ data: { course_id, title, description, sort_order } }); return { code: 200, message: 'Chapter created successfully', data: chapter as ChapterData }; } catch (error) { logger.error(`Error creating chapter: ${error}`); throw error; } } async updateChapter(request: UpdateChapterInput): Promise { try { const { token, course_id, chapter_id, title, description, sort_order } = request; const decodedToken = jwt.verify(token, config.jwt.secret) as { id: number }; await CoursesInstructorService.validateCourseStatus(course_id); const user = await prisma.user.findUnique({ where: { id: decodedToken.id } }); if (!user) { throw new UnauthorizedError('Invalid token'); } const courseInstructor = await CoursesInstructorService.validateCourseInstructor(token, course_id); if (!courseInstructor) { throw new ForbiddenError('You are not permitted to update chapter'); } const chapter = await prisma.chapter.update({ where: { id: chapter_id }, data: { title, description, sort_order } }); return { code: 200, message: 'Chapter updated successfully', data: chapter as ChapterData }; } catch (error) { logger.error(`Error updating chapter: ${error}`); throw error; } } async deleteChapter(request: DeleteChapterRequest): Promise { try { const { token, course_id, chapter_id } = request; const decodedToken = jwt.verify(token, config.jwt.secret) as { id: number }; await CoursesInstructorService.validateCourseStatus(course_id); const user = await prisma.user.findUnique({ where: { id: decodedToken.id } }); if (!user) { throw new UnauthorizedError('Invalid token'); } const courseInstructor = await CoursesInstructorService.validateCourseInstructor(token, course_id); if (!courseInstructor) { throw new ForbiddenError('You are not permitted to delete chapter'); } await prisma.chapter.delete({ where: { id: chapter_id } }); return { code: 200, message: 'Chapter deleted successfully' }; } catch (error) { logger.error(`Error deleting chapter: ${error}`); throw error; } } async reorderChapter(request: ReorderChapterRequest): Promise { try { const { token, course_id, chapter_id, sort_order: newSortOrder } = request; const decodedToken = jwt.verify(token, config.jwt.secret) as { id: number }; await CoursesInstructorService.validateCourseStatus(course_id); const user = await prisma.user.findUnique({ where: { id: decodedToken.id } }); if (!user) { throw new UnauthorizedError('Invalid token'); } const courseInstructor = await CoursesInstructorService.validateCourseInstructor(token, course_id); if (!courseInstructor) { throw new ForbiddenError('You are not permitted to reorder chapter'); } // Get current chapter to find its current sort_order const currentChapter = await prisma.chapter.findUnique({ where: { id: chapter_id } }); if (!currentChapter) { throw new NotFoundError('Chapter not found'); } // Validate chapter belongs to the specified course if (currentChapter.course_id !== course_id) { throw new NotFoundError('Chapter not found in this course'); } const oldSortOrder = currentChapter.sort_order; // If same position, no need to reorder if (oldSortOrder === newSortOrder) { const chapters = await prisma.chapter.findMany({ where: { course_id }, orderBy: { sort_order: 'asc' } }); return { code: 200, message: 'Chapter reordered successfully', data: chapters as ChapterData[] }; } // Shift other chapters to make room for the insert if (oldSortOrder > newSortOrder) { // Moving up: shift chapters between newSortOrder and oldSortOrder-1 down by 1 await prisma.chapter.updateMany({ where: { course_id, sort_order: { gte: newSortOrder, lt: oldSortOrder } }, data: { sort_order: { increment: 1 } } }); } else { // Moving down: shift chapters between oldSortOrder+1 and newSortOrder up by 1 await prisma.chapter.updateMany({ where: { course_id, sort_order: { gt: oldSortOrder, lte: newSortOrder } }, data: { sort_order: { decrement: 1 } } }); } // Update the target chapter to the new position await prisma.chapter.update({ where: { id: chapter_id }, data: { sort_order: newSortOrder } }); // Fetch all chapters with updated order const chapters = await prisma.chapter.findMany({ where: { course_id }, orderBy: { sort_order: 'asc' } }); return { code: 200, message: 'Chapter reordered successfully', data: chapters as ChapterData[] }; } catch (error) { logger.error(`Error reordering chapter: ${error}`); throw error; } } /** * สร้างบทเรียนเปล่า (ยังไม่มีเนื้อหา) * Create an empty lesson with basic information (no content yet) * สำหรับ QUIZ type จะสร้าง Quiz shell ไว้ด้วย แต่ยังไม่มีคำถาม */ async createLesson(request: CreateLessonInput): Promise { try { const { token, course_id, chapter_id, title, content, type, sort_order } = request; const decodedToken = jwt.verify(token, config.jwt.secret) as { id: number }; await CoursesInstructorService.validateCourseStatus(course_id); const user = await prisma.user.findUnique({ where: { id: decodedToken.id } }); if (!user) { throw new UnauthorizedError('Invalid token'); } const courseInstructor = await CoursesInstructorService.validateCourseInstructor(token, course_id); if (!courseInstructor) { throw new ForbiddenError('You are not permitted to create lesson'); } // Create the lesson const lesson = await prisma.lesson.create({ data: { chapter_id, title, content, type, sort_order } }); // If QUIZ type, create empty Quiz shell if (type === 'QUIZ') { const userId = decodedToken.id; await prisma.quiz.create({ data: { lesson_id: lesson.id, title: title, // Use lesson title as quiz title passing_score: 60, shuffle_questions: false, shuffle_choices: false, show_answers_after_completion: true, created_by: userId, } }); // Fetch complete lesson with quiz const completeLesson = await prisma.lesson.findUnique({ where: { id: lesson.id }, include: { quiz: true } }); return { code: 200, message: 'Lesson created successfully', data: completeLesson as LessonData }; } return { code: 200, message: 'Lesson created successfully', data: lesson as LessonData }; } catch (error) { logger.error(`Error creating lesson: ${error}`); throw error; } } /** * ดึงข้อมูลบทเรียนพร้อม attachments และ quiz * Get lesson with attachments and quiz (if QUIZ type) */ async getLesson(request: GetLessonRequest): Promise { try { const { token, course_id, lesson_id } = request; // Check access for both instructor and enrolled student const access = await validateCourseAccess(token, course_id); if (!access.hasAccess) { throw new ForbiddenError('You do not have access to this course'); } const lesson = await prisma.lesson.findUnique({ where: { id: lesson_id }, include: { attachments: { orderBy: { sort_order: 'asc' } }, quiz: { include: { questions: { orderBy: { sort_order: 'asc' }, include: { choices: { orderBy: { sort_order: 'asc' } } } } } } } }); if (!lesson) throw new NotFoundError('Lesson not found'); // Verify lesson belongs to the course const chapter = await prisma.chapter.findUnique({ where: { id: lesson.chapter_id } }); if (!chapter || chapter.course_id !== course_id) { throw new NotFoundError('Lesson not found in this course'); } // For students, check if lesson is published if (access.role === 'STUDENT' && !lesson.is_published) { throw new ForbiddenError('This lesson is not available yet'); } return { code: 200, message: 'Lesson fetched successfully', data: lesson as LessonData }; } catch (error) { logger.error(`Error fetching lesson: ${error}`); throw error; } } async updateLesson(request: UpdateLessonRequest): Promise { try { const { token, course_id, lesson_id, data } = request; const decodedToken = jwt.verify(token, config.jwt.secret) as { id: number }; await CoursesInstructorService.validateCourseStatus(course_id); const user = await prisma.user.findUnique({ where: { id: decodedToken.id } }); if (!user) { throw new UnauthorizedError('Invalid token'); } const courseInstructor = await CoursesInstructorService.validateCourseInstructor(token, course_id); if (!courseInstructor) { throw new ForbiddenError('You are not permitted to update lesson'); } const lesson = await prisma.lesson.update({ where: { id: lesson_id }, data }); return { code: 200, message: 'Lesson updated successfully', data: lesson as LessonData }; } catch (error) { logger.error(`Error updating lesson: ${error}`); throw error; } } /** * เรียงลำดับบทเรียนใหม่ * Reorder lessons within a chapter */ async reorderLessons(request: ReorderLessonsRequest): Promise { try { const { token, course_id, chapter_id, lesson_id, sort_order: newSortOrder } = request; const decodedToken = jwt.verify(token, config.jwt.secret) as { id: number }; await CoursesInstructorService.validateCourseStatus(course_id); const user = await prisma.user.findUnique({ where: { id: decodedToken.id } }); if (!user) throw new UnauthorizedError('Invalid token'); const courseInstructor = await CoursesInstructorService.validateCourseInstructor(token, course_id); if (!courseInstructor) throw new ForbiddenError('You are not permitted to reorder lessons'); // Verify chapter exists and belongs to the course const chapter = await prisma.chapter.findUnique({ where: { id: chapter_id } }); if (!chapter || chapter.course_id !== course_id) { throw new NotFoundError('Chapter not found'); } // Get current lesson to find its current sort_order const currentLesson = await prisma.lesson.findUnique({ where: { id: lesson_id } }); if (!currentLesson) { throw new NotFoundError('Lesson not found'); } if (currentLesson.chapter_id !== chapter_id) { throw new NotFoundError('Lesson not found in this chapter'); } const oldSortOrder = currentLesson.sort_order; // If same position, no need to reorder if (oldSortOrder === newSortOrder) { const lessons = await prisma.lesson.findMany({ where: { chapter_id }, orderBy: { sort_order: 'asc' } }); return { code: 200, message: 'Lessons reordered successfully', data: lessons as LessonData[] }; } // Shift other lessons to make room for the insert if (oldSortOrder > newSortOrder) { // Moving up: shift lessons between newSortOrder and oldSortOrder-1 down by 1 await prisma.lesson.updateMany({ where: { chapter_id, sort_order: { gte: newSortOrder, lt: oldSortOrder } }, data: { sort_order: { increment: 1 } } }); } else { // Moving down: shift lessons between oldSortOrder+1 and newSortOrder up by 1 await prisma.lesson.updateMany({ where: { chapter_id, sort_order: { gt: oldSortOrder, lte: newSortOrder } }, data: { sort_order: { decrement: 1 } } }); } // Update the target lesson to the new position await prisma.lesson.update({ where: { id: lesson_id }, data: { sort_order: newSortOrder } }); // Fetch all lessons with updated order const lessons = await prisma.lesson.findMany({ where: { chapter_id }, orderBy: { sort_order: 'asc' } }); return { code: 200, message: 'Lessons reordered successfully', data: lessons as LessonData[] }; } catch (error) { logger.error(`Error reordering lessons: ${error}`); throw error; } } /** * ลบบทเรียนพร้อมข้อมูลที่เกี่ยวข้องทั้งหมด * Delete lesson with all related data (quiz, questions, choices, attachments) * ไม่สามารถลบได้ถ้าบทเรียนถูก publish แล้ว */ async deleteLesson(request: DeleteLessonRequest): Promise { try { const { token, course_id, lesson_id } = request; const decodedToken = jwt.verify(token, config.jwt.secret) as { id: number }; await CoursesInstructorService.validateCourseStatus(course_id); const user = await prisma.user.findUnique({ where: { id: decodedToken.id } }); if (!user) throw new UnauthorizedError('Invalid token'); const courseInstructor = await CoursesInstructorService.validateCourseInstructor(token, course_id); if (!courseInstructor) throw new ForbiddenError('You are not permitted to delete this lesson'); // Fetch lesson with all related data const lesson = await prisma.lesson.findUnique({ where: { id: lesson_id }, include: { attachments: true, quiz: { include: { questions: { include: { choices: true } } } } } }); if (!lesson) throw new NotFoundError('Lesson not found'); // Check if lesson is published if (lesson.is_published) { throw new ValidationError('Cannot delete published lesson. Please unpublish first.'); } // Delete attachment files from MinIO if (lesson.attachments && lesson.attachments.length > 0) { for (const attachment of lesson.attachments) { try { await deleteFile(attachment.file_path); } catch (err) { logger.warn(`Failed to delete file from MinIO: ${attachment.file_path}`); } } } // Delete lesson (CASCADE will delete: attachments, quiz, questions, choices) // Based on Prisma schema: onDelete: Cascade await prisma.lesson.delete({ where: { id: lesson_id } }); return { code: 200, message: 'Lesson deleted successfully' }; } catch (error) { logger.error(`Error deleting lesson: ${error}`); throw error; } } /** * เพิ่มวิดีโอและไฟล์แนบให้บทเรียนประเภท VIDEO * Add video and attachments to an existing VIDEO type lesson */ async addVideoLesson(request: AddVideoToLessonInput): Promise { try { const { token, course_id, lesson_id, video, attachments } = request; const decodedToken = jwt.verify(token, config.jwt.secret) as { id: number }; await CoursesInstructorService.validateCourseStatus(course_id); const user = await prisma.user.findUnique({ where: { id: decodedToken.id } }); if (!user) { throw new UnauthorizedError('Invalid token'); } const courseInstructor = await CoursesInstructorService.validateCourseInstructor(token, course_id); if (!courseInstructor) { throw new ForbiddenError('You are not permitted to modify this lesson'); } // Verify lesson exists and is VIDEO type const lesson = await prisma.lesson.findUnique({ where: { id: lesson_id } }); if (!lesson) { throw new NotFoundError('Lesson not found'); } if (lesson.type !== 'VIDEO') { throw new ValidationError('Cannot add video to non-VIDEO type lesson'); } // Upload video to MinIO const videoPath = generateFilePath(course_id, lesson_id, 'video', video.originalname); await uploadFile(videoPath, video.buffer, video.mimetype); // Save video as attachment await prisma.lessonAttachment.create({ data: { lesson_id: lesson_id, file_name: video.originalname, file_size: video.size, mime_type: video.mimetype, file_path: videoPath, sort_order: 0, } }); // Handle additional attachments (PDFs, documents, etc.) if (attachments && attachments.length > 0) { for (let i = 0; i < attachments.length; i++) { const attachment = attachments[i]; const attachmentPath = generateFilePath(course_id, lesson_id, 'attachment', attachment.originalname); await uploadFile(attachmentPath, attachment.buffer, attachment.mimetype); await prisma.lessonAttachment.create({ data: { lesson_id: lesson_id, file_name: attachment.originalname, file_size: attachment.size, mime_type: attachment.mimetype, file_path: attachmentPath, sort_order: i + 1, } }); } } // Fetch the complete lesson with attachments const completeLesson = await prisma.lesson.findUnique({ where: { id: lesson_id }, include: { attachments: { orderBy: { sort_order: 'asc' } } } }); return { code: 200, message: 'Video added to lesson successfully', data: completeLesson as LessonData }; } catch (error) { logger.error(`Error adding video to lesson: ${error}`); throw error; } } async updateVideoLesson(request: UpdateVideoLessonInput): Promise { try { const { token, course_id, lesson_id, video, attachments } = request; const decodedToken = jwt.verify(token, config.jwt.secret) as { id: number }; await CoursesInstructorService.validateCourseStatus(course_id); const user = await prisma.user.findUnique({ where: { id: decodedToken.id } }); logger.info(`User: ${user}`); if (!user) throw new UnauthorizedError('Invalid token'); const courseInstructor = await CoursesInstructorService.validateCourseInstructor(token, course_id); if (!courseInstructor) throw new ForbiddenError('You are not permitted to modify this lesson'); const lesson = await prisma.lesson.findUnique({ where: { id: lesson_id } }); if (!lesson) throw new NotFoundError('Lesson not found'); if (lesson.type !== 'VIDEO') throw new ValidationError('Cannot update video for non-VIDEO type lesson'); // Update video if provided if (video) { // Find existing video attachment (sort_order = 0 is video) const existingVideo = await prisma.lessonAttachment.findFirst({ where: { lesson_id: lesson_id, sort_order: 0 } }); if (existingVideo) { // Delete old video from MinIO await deleteFile(existingVideo.file_path); // Upload new video to MinIO const newVideoPath = generateFilePath(course_id, lesson_id, 'video', video.originalname); await uploadFile(newVideoPath, video.buffer, video.mimetype); // Update lessonAttachment with new video info await prisma.lessonAttachment.update({ where: { id: existingVideo.id }, data: { file_name: video.originalname, file_size: video.size, mime_type: video.mimetype, file_path: newVideoPath, } }); } else { // No existing video, create new one const videoPath = generateFilePath(course_id, lesson_id, 'video', video.originalname); await uploadFile(videoPath, video.buffer, video.mimetype); await prisma.lessonAttachment.create({ data: { lesson_id: lesson_id, file_name: video.originalname, file_size: video.size, mime_type: video.mimetype, file_path: videoPath, sort_order: 0, } }); } } // Update attachments if provided if (attachments && attachments.length > 0) { // Find and delete existing attachments (sort_order > 0) const existingAttachments = await prisma.lessonAttachment.findMany({ where: { lesson_id: lesson_id, sort_order: { gt: 0 } } }); // Delete old attachment files from MinIO for (const att of existingAttachments) { await deleteFile(att.file_path); } // Delete old attachment records from database await prisma.lessonAttachment.deleteMany({ where: { lesson_id: lesson_id, sort_order: { gt: 0 } } }); // Upload new attachments for (let i = 0; i < attachments.length; i++) { const attachment = attachments[i]; const attachmentPath = generateFilePath(course_id, lesson_id, 'attachment', attachment.originalname); await uploadFile(attachmentPath, attachment.buffer, attachment.mimetype); await prisma.lessonAttachment.create({ data: { lesson_id: lesson_id, file_name: attachment.originalname, file_size: attachment.size, mime_type: attachment.mimetype, file_path: attachmentPath, sort_order: i + 1, } }); } } // Fetch updated lesson with attachments const updatedLesson = await prisma.lesson.findUnique({ where: { id: lesson_id }, include: { attachments: { orderBy: { sort_order: 'asc' } } } }); return { code: 200, message: 'Video lesson updated successfully', data: updatedLesson as LessonData }; } catch (error) { logger.error(`Error updating video lesson: ${error}`); throw error; } } /** * เพิ่มคำถามทีละข้อให้ Quiz Lesson * Add a single question with choices to an existing QUIZ lesson */ async addQuestion(request: AddQuestionInput): Promise { try { const { token, course_id, lesson_id, question, explanation, question_type, score, sort_order, choices } = request; const decodedToken = jwt.verify(token, config.jwt.secret) as { id: number }; await CoursesInstructorService.validateCourseStatus(course_id); const user = await prisma.user.findUnique({ where: { id: decodedToken.id } }); if (!user) throw new UnauthorizedError('Invalid token'); const courseInstructor = await CoursesInstructorService.validateCourseInstructor(token, course_id); if (!courseInstructor) throw new ForbiddenError('You are not permitted to modify this lesson'); // Verify lesson exists and is QUIZ type const lesson = await prisma.lesson.findUnique({ where: { id: lesson_id }, include: { quiz: true } }); if (!lesson) throw new NotFoundError('Lesson not found'); if (lesson.type !== 'QUIZ') throw new ValidationError('Cannot add question to non-QUIZ type lesson'); if (!lesson.quiz) throw new NotFoundError('Quiz not found for this lesson'); // Get current max sort_order const maxSortOrder = await prisma.question.aggregate({ where: { quiz_id: lesson.quiz.id }, _max: { sort_order: true } }); const nextSortOrder = sort_order ?? ((maxSortOrder._max.sort_order ?? -1) + 1); // Create the question const newQuestion = await prisma.question.create({ data: { quiz_id: lesson.quiz.id, question: question, explanation: explanation, question_type: question_type, score: score ?? 1, sort_order: nextSortOrder, } }); // Create choices if provided if (choices && choices.length > 0) { for (let i = 0; i < choices.length; i++) { const choiceInput = choices[i]; await prisma.choice.create({ data: { question_id: newQuestion.id, text: choiceInput.text, is_correct: choiceInput.is_correct, sort_order: choiceInput.sort_order ?? i, } }); } } // Fetch complete question with choices const completeQuestion = await prisma.question.findUnique({ where: { id: newQuestion.id }, include: { choices: { orderBy: { sort_order: 'asc' } } } }); return { code: 200, message: 'Question added successfully', data: completeQuestion as QuizQuestionData }; } catch (error) { logger.error(`Error adding question: ${error}`); throw error; } } /** * อัปเดตคำถาม * Update a question and optionally replace all choices */ async updateQuestion(request: UpdateQuestionInput): Promise { try { const { token, course_id, lesson_id, question_id, question, explanation, question_type, score, sort_order, choices } = request; const decodedToken = jwt.verify(token, config.jwt.secret) as { id: number }; await CoursesInstructorService.validateCourseStatus(course_id); const user = await prisma.user.findUnique({ where: { id: decodedToken.id } }); if (!user) throw new UnauthorizedError('Invalid token'); const courseInstructor = await CoursesInstructorService.validateCourseInstructor(token, course_id); if (!courseInstructor) throw new ForbiddenError('You are not permitted to modify this lesson'); // Verify lesson exists and is QUIZ type const lesson = await prisma.lesson.findUnique({ where: { id: lesson_id }, include: { quiz: true } }); if (!lesson) throw new NotFoundError('Lesson not found'); if (lesson.type !== 'QUIZ') throw new ValidationError('Cannot update question in non-QUIZ type lesson'); if (!lesson.quiz) throw new NotFoundError('Quiz not found for this lesson'); // Verify question exists and belongs to this quiz const existingQuestion = await prisma.question.findUnique({ where: { id: question_id } }); if (!existingQuestion || existingQuestion.quiz_id !== lesson.quiz.id) { throw new NotFoundError('Question not found in this quiz'); } // Update the question await prisma.question.update({ where: { id: question_id }, data: { question: (question ?? existingQuestion.question) as Prisma.InputJsonValue, explanation: (explanation ?? existingQuestion.explanation) as Prisma.InputJsonValue, question_type: question_type ?? existingQuestion.question_type, score: score ?? existingQuestion.score, sort_order: sort_order ?? existingQuestion.sort_order, } }); // If choices provided, replace all choices if (choices && choices.length > 0) { // Delete existing choices await prisma.choice.deleteMany({ where: { question_id: question_id } }); // Create new choices for (let i = 0; i < choices.length; i++) { const choiceInput = choices[i]; await prisma.choice.create({ data: { question_id: question_id, text: choiceInput.text, is_correct: choiceInput.is_correct, sort_order: choiceInput.sort_order ?? i, } }); } } // Fetch complete question with choices const completeQuestion = await prisma.question.findUnique({ where: { id: question_id }, include: { choices: { orderBy: { sort_order: 'asc' } } } }); return { code: 200, message: 'Question updated successfully', data: completeQuestion as QuizQuestionData }; } catch (error) { logger.error(`Error updating question: ${error}`); throw error; } } /** * ลบคำถาม * Delete a question and all its choices */ async deleteQuestion(request: DeleteQuestionInput): Promise { try { const { token, course_id, lesson_id, question_id } = request; const decodedToken = jwt.verify(token, config.jwt.secret) as { id: number }; await CoursesInstructorService.validateCourseStatus(course_id); const user = await prisma.user.findUnique({ where: { id: decodedToken.id } }); if (!user) throw new UnauthorizedError('Invalid token'); const courseInstructor = await CoursesInstructorService.validateCourseInstructor(token, course_id); if (!courseInstructor) throw new ForbiddenError('You are not permitted to modify this lesson'); // Verify lesson exists and is QUIZ type const lesson = await prisma.lesson.findUnique({ where: { id: lesson_id }, include: { quiz: true } }); if (!lesson) throw new NotFoundError('Lesson not found'); if (lesson.type !== 'QUIZ') throw new ValidationError('Cannot delete question from non-QUIZ type lesson'); if (!lesson.quiz) throw new NotFoundError('Quiz not found for this lesson'); // Verify question exists and belongs to this quiz const existingQuestion = await prisma.question.findUnique({ where: { id: question_id } }); if (!existingQuestion || existingQuestion.quiz_id !== lesson.quiz.id) { throw new NotFoundError('Question not found in this quiz'); } // Delete the question (CASCADE will delete choices) await prisma.question.delete({ where: { id: question_id } }); return { code: 200, message: 'Question deleted successfully' }; } catch (error) { logger.error(`Error deleting question: ${error}`); throw error; } } }