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:
parent
a648c41b72
commit
80d7372dfa
6 changed files with 832 additions and 1 deletions
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue