feat: add API endpoint and service logic for reordering quiz questions.

This commit is contained in:
JakkrapartXD 2026-01-26 12:00:59 +07:00
parent 44c61c8fb2
commit 84e4d478c7
3 changed files with 125 additions and 0 deletions

View file

@ -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<ReorderQuestionResponse> {
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

View file

@ -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<ReorderQuestionResponse> {
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

View file

@ -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;