feat: add instructor endpoints for student progress tracking and quiz score management

Add four new instructor endpoints: getEnrolledStudents to view all enrolled students with progress, getQuizScores to view quiz scores for all students in a lesson, getQuizAttemptDetail to view detailed quiz attempt for a specific student, and searchStudents to search enrolled students by name/email/username. Add getQuizAttempts endpoint for students to retrieve their own quiz attempt history. All endpoints include
This commit is contained in:
JakkrapartXD 2026-02-02 18:02:19 +07:00
parent a648c41b72
commit 80d7372dfa
6 changed files with 832 additions and 1 deletions

View file

@ -23,6 +23,8 @@ import {
CompleteLessonResponse,
SubmitQuizInput,
SubmitQuizResponse,
GetQuizAttemptsInput,
GetQuizAttemptsResponse,
} from "../types/CoursesStudent.types";
import { getPresignedUrl, listObjects, getVideoFolder, getAttachmentsFolder } from '../config/minio';
@ -1260,4 +1262,109 @@ export class CoursesStudentService {
throw error;
}
}
/**
* Quiz
* Get student's quiz attempts for a lesson
*/
async getQuizAttempts(input: GetQuizAttemptsInput): Promise<GetQuizAttemptsResponse> {
try {
const { token, course_id, lesson_id } = 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: 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');
}
// Get all quiz attempts for this user
const attempts = await prisma.quizAttempt.findMany({
where: {
user_id: decoded.id,
quiz_id: lesson.quiz.id,
},
orderBy: { attempt_number: 'desc' },
});
// Calculate total score from questions
const totalScore = lesson.quiz.questions.reduce((sum, q) => sum + q.score, 0);
// Format attempts data
const attemptsData = attempts.map(attempt => ({
id: attempt.id,
quiz_id: attempt.quiz_id,
score: attempt.score,
total_score: totalScore,
total_questions: attempt.total_questions,
correct_answers: attempt.correct_answers,
is_passed: attempt.is_passed,
attempt_number: attempt.attempt_number,
started_at: attempt.started_at,
completed_at: attempt.completed_at,
}));
// Find best score and latest attempt
const bestScore = attempts.length > 0
? Math.max(...attempts.map(a => a.score))
: null;
const latestAttempt = attemptsData.length > 0 ? attemptsData[0] : null;
return {
code: 200,
message: 'Quiz attempts retrieved successfully',
data: {
lesson_id: lesson.id,
lesson_title: lesson.title as { th: string; en: string },
quiz_id: lesson.quiz.id,
quiz_title: lesson.quiz.title as { th: string; en: string },
passing_score: lesson.quiz.passing_score,
attempts: attemptsData,
best_score: bestScore,
latest_attempt: latestAttempt,
},
};
} catch (error) {
logger.error(error);
throw error;
}
}
}