diff --git a/Backend/src/controllers/ChaptersLessonInstructorController.ts b/Backend/src/controllers/ChaptersLessonInstructorController.ts index 56a9fe46..64a4d2b4 100644 --- a/Backend/src/controllers/ChaptersLessonInstructorController.ts +++ b/Backend/src/controllers/ChaptersLessonInstructorController.ts @@ -22,6 +22,8 @@ import { ReorderLessonsBody, AddQuestionBody, UpdateQuestionBody, + ReorderQuestionResponse, + ReorderQuestionBody, } from '../types/ChaptersLesson.typs'; const chaptersLessonService = new ChaptersLessonService(); @@ -305,6 +307,28 @@ export class ChaptersLessonInstructorController { }); } + @Put('chapters/{chapterId}/lessons/{lessonId}/questions/{questionId}/reorder') + @Security('jwt', ['instructor']) + @SuccessResponse('200', 'Question reordered successfully') + public async reorderQuestion( + @Request() request: any, + @Path() courseId: number, + @Path() chapterId: number, + @Path() lessonId: number, + @Path() questionId: number, + @Body() body: ReorderQuestionBody + ): Promise { + const token = request.headers.authorization?.replace('Bearer ', ''); + if (!token) throw new ValidationError('No token provided'); + return await chaptersLessonService.reorderQuestion({ + token, + course_id: courseId, + lesson_id: lessonId, + question_id: questionId, + sort_order: body.sort_order, + }); + } + /** * ลบคำถาม * Delete a question diff --git a/Backend/src/services/ChaptersLesson.service.ts b/Backend/src/services/ChaptersLesson.service.ts index 57462d69..2d508017 100644 --- a/Backend/src/services/ChaptersLesson.service.ts +++ b/Backend/src/services/ChaptersLesson.service.ts @@ -37,6 +37,8 @@ import { DeleteQuestionInput, DeleteQuestionResponse, QuizQuestionData, + ReorderQuestionInput, + ReorderQuestionResponse, } from "../types/ChaptersLesson.typs"; import { CoursesInstructorService } from './CoursesInstructor.service'; @@ -832,6 +834,87 @@ export class ChaptersLessonService { } } + async reorderQuestion(request: ReorderQuestionInput): Promise { + try { + const { token, course_id, lesson_id, question_id, 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 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 reorder 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'); + } + + const oldSortOrder = existingQuestion.sort_order; + const quizId = lesson.quiz.id; + + // If same position, no need to reorder + if (oldSortOrder === sort_order) { + const questions = await prisma.question.findMany({ + where: { quiz_id: quizId }, + orderBy: { sort_order: 'asc' }, + include: { choices: { orderBy: { sort_order: 'asc' } } } + }); + return { code: 200, message: 'Question reordered successfully', data: questions as QuizQuestionData[] }; + } + + // Shift other questions to make room for the insert + if (oldSortOrder > sort_order) { + // Moving up: shift questions between newSortOrder and oldSortOrder-1 down by 1 + await prisma.question.updateMany({ + where: { + quiz_id: quizId, + sort_order: { gte: sort_order, lt: oldSortOrder } + }, + data: { sort_order: { increment: 1 } } + }); + } else { + // Moving down: shift questions between oldSortOrder+1 and newSortOrder up by 1 + await prisma.question.updateMany({ + where: { + quiz_id: quizId, + sort_order: { gt: oldSortOrder, lte: sort_order } + }, + data: { sort_order: { decrement: 1 } } + }); + } + + // Update the question's sort order + await prisma.question.update({ + where: { id: question_id }, + data: { sort_order } + }); + + // Fetch all questions with updated order + const questions = await prisma.question.findMany({ + where: { quiz_id: quizId }, + orderBy: { sort_order: 'asc' }, + include: { choices: { orderBy: { sort_order: 'asc' } } } + }); + + return { code: 200, message: 'Question reordered successfully', data: questions as QuizQuestionData[] }; + } catch (error) { + logger.error(`Error reordering question: ${error}`); + throw error; + } + } + /** * ลบคำถาม * Delete a question and all its choices diff --git a/Backend/src/types/ChaptersLesson.typs.ts b/Backend/src/types/ChaptersLesson.typs.ts index b2936a6a..6a46f59d 100644 --- a/Backend/src/types/ChaptersLesson.typs.ts +++ b/Backend/src/types/ChaptersLesson.typs.ts @@ -477,6 +477,20 @@ export interface DeleteQuestionInput { question_id: number; } +export interface ReorderQuestionInput { + token: string; + course_id: number; + lesson_id: number; + question_id: number; + sort_order: number; +} + +export interface ReorderQuestionResponse { + code: number; + message: string; + data: QuizQuestionData[]; +} + /** * Response for deleting a question */ @@ -526,6 +540,10 @@ export interface ReorderLessonsBody { sort_order: number; } +export interface ReorderQuestionBody { + sort_order: number; +} + export interface AddQuestionBody { question: MultiLanguageText; explanation?: MultiLanguageText;