add check quizz for student

This commit is contained in:
JakkrapartXD 2026-01-22 17:30:35 +07:00
parent 76f8ab120a
commit c982ab2c05
3 changed files with 232 additions and 0 deletions

View file

@ -11,6 +11,8 @@ import {
SaveVideoProgressBody,
GetVideoProgressResponse,
CompleteLessonResponse,
SubmitQuizResponse,
SubmitQuizBody,
} from '../types/CoursesStudent.types';
import { EnrollmentStatus } from '@prisma/client';
@ -202,4 +204,34 @@ export class CoursesStudentController {
}
return await this.service.completeLesson({ token, lesson_id: lessonId });
}
/**
* Quiz
* Submit quiz answers and get score
* @param courseId - / Course ID
* @param lessonId - / Lesson ID
*/
@Post('courses/{courseId}/lessons/{lessonId}/quiz/submit')
@Security('jwt', ['student'])
@SuccessResponse('200', 'Quiz submitted successfully')
@Response('401', 'Invalid or expired token')
@Response('403', 'Not enrolled in this course')
@Response('404', 'Lesson or quiz not found')
public async submitQuiz(
@Request() request: any,
@Path() courseId: number,
@Path() lessonId: number,
@Body() body: SubmitQuizBody
): Promise<SubmitQuizResponse> {
const token = request.headers.authorization?.replace('Bearer ', '');
if (!token) {
throw new ValidationError('No token provided');
}
return await this.service.submitQuiz({
token,
course_id: courseId,
lesson_id: lessonId,
answers: body.answers,
});
}
}

View file

@ -21,6 +21,8 @@ import {
GetVideoProgressResponse,
CompleteLessonInput,
CompleteLessonResponse,
SubmitQuizInput,
SubmitQuizResponse,
} from "../types/CoursesStudent.types";
import { getPresignedUrl, listObjects, getVideoFolder, getAttachmentsFolder } from '../config/minio';
@ -461,6 +463,7 @@ export class CoursesStudentService {
}
}
async checkAccessLesson(input: CheckLessonAccessInput): Promise<CheckLessonAccessResponse> {
try {
const { token, course_id, lesson_id } = input;
@ -993,4 +996,156 @@ export class CoursesStudentService {
throw error;
}
}
/**
* Quiz
* Submit quiz answers and calculate score
*/
async submitQuiz(input: SubmitQuizInput): Promise<SubmitQuizResponse> {
try {
const { token, course_id, lesson_id, answers } = input;
const decoded = jwt.verify(token, config.jwt.secret) as { id: number };
// Check enrollment
const enrollment = await prisma.enrollment.findUnique({
where: {
unique_enrollment: {
user_id: decoded.id,
course_id,
},
},
});
if (!enrollment) {
throw new ForbiddenError('You are not enrolled in this course');
}
// Get lesson and verify it's a QUIZ type
const lesson = await prisma.lesson.findUnique({
where: { id: lesson_id },
include: {
quiz: {
include: {
questions: {
include: {
choices: true,
},
},
},
},
chapter: true,
},
});
if (!lesson) {
throw new NotFoundError('Lesson not found');
}
if (lesson.type !== 'QUIZ') {
throw new ValidationError('This lesson is not a quiz');
}
if (!lesson.quiz) {
throw new NotFoundError('Quiz not found for this lesson');
}
// Verify lesson belongs to the course
if (lesson.chapter.course_id !== course_id) {
throw new NotFoundError('Lesson not found in this course');
}
const quiz = lesson.quiz;
// Get previous attempt count
const previousAttempts = await prisma.quizAttempt.count({
where: {
user_id: decoded.id,
quiz_id: quiz.id,
},
});
const attemptNumber = previousAttempts + 1;
// Calculate score
let totalScore = 0;
let earnedScore = 0;
let correctAnswers = 0;
const answersReview: {
question_id: number;
selected_choice_id: number;
correct_choice_id: number;
is_correct: boolean;
score: number;
}[] = [];
for (const question of quiz.questions) {
totalScore += question.score;
// Find the correct choice for this question
const correctChoice = question.choices.find(c => c.is_correct);
const correctChoiceId = correctChoice?.id ?? 0;
// Find student's answer for this question
const studentAnswer = answers.find(a => a.question_id === question.id);
const selectedChoiceId = studentAnswer?.choice_id ?? 0;
// Check if answer is correct
const isCorrect = selectedChoiceId === correctChoiceId;
if (isCorrect) {
earnedScore += question.score;
correctAnswers++;
}
answersReview.push({
question_id: question.id,
selected_choice_id: selectedChoiceId,
correct_choice_id: correctChoiceId,
is_correct: isCorrect,
score: isCorrect ? question.score : 0,
});
}
// Calculate percentage and check if passed
const scorePercentage = totalScore > 0 ? (earnedScore / totalScore) * 100 : 0;
const isPassed = scorePercentage >= quiz.passing_score;
// Create quiz attempt record
const now = new Date();
const quizAttempt = await prisma.quizAttempt.create({
data: {
user_id: decoded.id,
quiz_id: quiz.id,
score: earnedScore,
total_questions: quiz.questions.length,
correct_answers: correctAnswers,
is_passed: isPassed,
attempt_number: attemptNumber,
answers: answers as any, // Store student answers as JSON
started_at: now,
completed_at: now,
},
});
return {
code: 200,
message: isPassed ? 'Quiz passed!' : 'Quiz completed',
data: {
attempt_id: quizAttempt.id,
quiz_id: quiz.id,
score: earnedScore,
total_score: totalScore,
total_questions: quiz.questions.length,
correct_answers: correctAnswers,
is_passed: isPassed,
passing_score: quiz.passing_score,
attempt_number: attemptNumber,
started_at: quizAttempt.started_at,
completed_at: quizAttempt.completed_at!,
answers_review: quiz.show_answers_after_completion ? answersReview : undefined,
},
};
} catch (error) {
logger.error(error);
throw error;
}
}
}

View file

@ -319,3 +319,48 @@ export interface CompleteLessonResponse {
certificate_issued?: boolean;
};
}
// ============================================
// Quiz Submission Types
// ============================================
export interface QuizAnswerInput {
question_id: number;
choice_id: number; // For MULTIPLE_CHOICE and TRUE_FALSE
}
export interface SubmitQuizInput {
token: string;
course_id: number;
lesson_id: number;
answers: QuizAnswerInput[];
}
export interface SubmitQuizBody {
answers: QuizAnswerInput[];
}
export interface SubmitQuizResponse {
code: number;
message: string;
data?: {
attempt_id: number;
quiz_id: number;
score: number;
total_score: number;
total_questions: number;
correct_answers: number;
is_passed: boolean;
passing_score: number;
attempt_number: number;
started_at: Date;
completed_at: Date;
answers_review?: {
question_id: number;
selected_choice_id: number;
correct_choice_id: number;
is_correct: boolean;
score: number;
}[];
};
}