const { QuizSession, Question, Category, GuestSession, QuizSessionQuestion, QuizAnswer, sequelize } = require('../models'); const { v4: uuidv4 } = require('uuid'); const { Op } = require('sequelize'); /** * Start a new quiz session * POST /api/quiz/start */ exports.startQuizSession = async (req, res) => { const transaction = await sequelize.transaction(); try { const { categoryId, questionCount = 10, difficulty = 'mixed', quizType = 'practice' } = req.body; const userId = req.user?.userId; const guestSessionId = req.guestSessionId; // UUID from guest middleware for foreign key const guestId = req.guestId; // String ID for guest session lookup // Validate: Must be either authenticated user or guest if (!userId && !guestSessionId) { await transaction.rollback(); return res.status(401).json({ success: false, message: 'Authentication required. Please login or start a guest session.' }); } // Validate categoryId is provided if (!categoryId) { await transaction.rollback(); return res.status(400).json({ success: false, message: 'Category ID is required' }); } // Validate UUID format const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; if (!uuidRegex.test(categoryId)) { await transaction.rollback(); return res.status(400).json({ success: false, message: 'Invalid category ID format' }); } // Validate questionCount const count = parseInt(questionCount); if (isNaN(count) || count < 1 || count > 50) { await transaction.rollback(); return res.status(400).json({ success: false, message: 'Question count must be between 1 and 50' }); } // Validate difficulty const validDifficulties = ['easy', 'medium', 'hard', 'mixed']; if (!validDifficulties.includes(difficulty)) { await transaction.rollback(); return res.status(400).json({ success: false, message: 'Invalid difficulty. Must be: easy, medium, hard, or mixed' }); } // Validate quizType const validQuizTypes = ['practice', 'timed', 'exam']; if (!validQuizTypes.includes(quizType)) { await transaction.rollback(); return res.status(400).json({ success: false, message: 'Invalid quiz type. Must be: practice, timed, or exam' }); } // Check if category exists and is active const category = await Category.findByPk(categoryId, { transaction }); if (!category) { await transaction.rollback(); return res.status(404).json({ success: false, message: 'Category not found' }); } if (!category.isActive) { await transaction.rollback(); return res.status(400).json({ success: false, message: 'Category is not active' }); } // Check guest access permissions if (guestSessionId && !category.guestAccessible) { await transaction.rollback(); return res.status(403).json({ success: false, message: 'This category is not accessible to guest users. Please register to access all categories.' }); } // For guest users: check quiz limit if (guestSessionId) { const guestSession = await GuestSession.findOne({ where: { guestId: guestId }, // Use the string guestId to lookup transaction }); if (!guestSession) { await transaction.rollback(); return res.status(404).json({ success: false, message: 'Guest session not found' }); } // Check if guest session is expired if (new Date() > new Date(guestSession.expiresAt)) { await transaction.rollback(); return res.status(410).json({ success: false, message: 'Guest session has expired. Please start a new session.' }); } // Check if guest session is converted if (guestSession.isConverted) { await transaction.rollback(); return res.status(410).json({ success: false, message: 'This guest session has been converted to a user account. Please login.' }); } // Check quiz limit if (guestSession.quizzesAttempted >= guestSession.maxQuizzes) { await transaction.rollback(); return res.status(403).json({ success: false, message: 'Quiz limit reached. Please register to continue taking quizzes.', quizLimit: { maxQuizzes: guestSession.maxQuizzes, quizzesAttempted: guestSession.quizzesAttempted } }); } // Increment guest quizzes_attempted await guestSession.increment('quizzesAttempted', { transaction }); } // Build question query based on difficulty const questionWhere = { categoryId: categoryId, isActive: true }; if (difficulty !== 'mixed') { questionWhere.difficulty = difficulty; } // Get random questions from the category const availableQuestions = await Question.findAll({ where: questionWhere, order: sequelize.random(), limit: count, attributes: ['id', 'questionText', 'questionType', 'options', 'difficulty', 'points', 'tags'], transaction }); // Check if we have enough questions if (availableQuestions.length === 0) { await transaction.rollback(); return res.status(404).json({ success: false, message: `No ${difficulty !== 'mixed' ? difficulty : ''} questions available in this category`.trim() }); } if (availableQuestions.length < count) { // Continue with available questions but inform user console.log(`Only ${availableQuestions.length} questions available, requested ${count}`); } const actualQuestionCount = availableQuestions.length; // Calculate total points const totalPoints = availableQuestions.reduce((sum, q) => sum + q.points, 0); // Set time limit for timed/exam quizzes (2 minutes per question, stored in seconds) let timeLimit = null; if (quizType === 'timed' || quizType === 'exam') { timeLimit = actualQuestionCount * 2 * 60; // 2 minutes per question = 120 seconds per question } // Create quiz session const quizSession = await QuizSession.create({ id: uuidv4(), userId: userId || null, guestSessionId: guestSessionId || null, categoryId: categoryId, quizType: quizType, difficulty: difficulty, totalQuestions: actualQuestionCount, questionsAnswered: 0, correctAnswers: 0, score: 0, totalPoints: totalPoints, timeLimit: timeLimit, status: 'in_progress', startedAt: new Date() }, { transaction }); // Create quiz_session_questions records (junction table) const quizSessionQuestions = availableQuestions.map((question, index) => ({ id: uuidv4(), quizSessionId: quizSession.id, questionId: question.id, questionOrder: index + 1 })); await QuizSessionQuestion.bulkCreate(quizSessionQuestions, { transaction }); // Commit transaction await transaction.commit(); // Prepare questions response (exclude correctAnswer) const questionsResponse = availableQuestions.map((q, index) => ({ id: q.id, questionText: q.questionText, questionType: q.questionType, options: q.options, difficulty: q.difficulty, points: q.points, tags: q.tags, order: index + 1 })); // Response res.status(201).json({ success: true, data: { sessionId: quizSession.id, category: { id: category.id, name: category.name, slug: category.slug, icon: category.icon, color: category.color }, quizType: quizSession.quizType, difficulty: quizSession.difficulty, totalQuestions: quizSession.totalQuestions, totalPoints: quizSession.totalPoints, timeLimit: quizSession.timeLimit ? Math.floor(quizSession.timeLimit / 60) : null, // Convert seconds to minutes for response status: quizSession.status, startedAt: quizSession.startedAt, questions: questionsResponse }, message: 'Quiz session started successfully' }); } catch (error) { await transaction.rollback(); console.error('Error starting quiz session:', error); res.status(500).json({ success: false, message: 'An error occurred while starting the quiz session', error: process.env.NODE_ENV === 'development' ? error.message : undefined }); } }; /** * Submit an answer for a quiz question * POST /api/quiz/submit */ exports.submitAnswer = async (req, res) => { const transaction = await sequelize.transaction(); try { const { quizSessionId, questionId, userAnswer, timeSpent = 0 } = req.body; const userId = req.user?.userId; const guestSessionId = req.guestSessionId; // Validate required fields if (!quizSessionId) { await transaction.rollback(); return res.status(400).json({ success: false, message: 'Quiz session ID is required' }); } if (!questionId) { await transaction.rollback(); return res.status(400).json({ success: false, message: 'Question ID is required' }); } if (userAnswer === undefined || userAnswer === null || userAnswer === '') { await transaction.rollback(); return res.status(400).json({ success: false, message: 'User answer is required' }); } // Validate UUID format for quizSessionId const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; if (!uuidRegex.test(quizSessionId)) { await transaction.rollback(); return res.status(400).json({ success: false, message: 'Invalid quiz session ID format' }); } // Validate UUID format for questionId if (!uuidRegex.test(questionId)) { await transaction.rollback(); return res.status(400).json({ success: false, message: 'Invalid question ID format' }); } // Find quiz session const quizSession = await QuizSession.findByPk(quizSessionId, { transaction }); if (!quizSession) { await transaction.rollback(); return res.status(404).json({ success: false, message: 'Quiz session not found' }); } // Check authorization: session must belong to current user or guest if (userId && quizSession.userId !== userId) { await transaction.rollback(); return res.status(403).json({ success: false, message: 'You are not authorized to submit answers for this quiz session' }); } if (guestSessionId && quizSession.guestSessionId !== guestSessionId) { await transaction.rollback(); return res.status(403).json({ success: false, message: 'You are not authorized to submit answers for this quiz session' }); } // Check if session is in progress if (quizSession.status !== 'in_progress') { await transaction.rollback(); return res.status(400).json({ success: false, message: `Cannot submit answer for a quiz session with status: ${quizSession.status}` }); } // Check if question belongs to this quiz session const questionInSession = await QuizSessionQuestion.findOne({ where: { quizSessionId: quizSessionId, questionId: questionId }, transaction }); if (!questionInSession) { await transaction.rollback(); return res.status(400).json({ success: false, message: 'Question does not belong to this quiz session' }); } // Check if already answered const existingAnswer = await QuizAnswer.findOne({ where: { quizSessionId: quizSessionId, questionId: questionId }, transaction }); if (existingAnswer) { await transaction.rollback(); return res.status(400).json({ success: false, message: 'Question has already been answered in this quiz session' }); } // Get the question with correct answer const question = await Question.findByPk(questionId, { include: [{ model: Category, as: 'category', attributes: ['id', 'name', 'slug', 'icon', 'color'] }], transaction }); if (!question) { await transaction.rollback(); return res.status(404).json({ success: false, message: 'Question not found' }); } // Parse correct answer (may be JSON array for multiple choice) let correctAnswer = question.correctAnswer; try { // Try to parse as JSON (for multiple choice questions with array format) const parsed = JSON.parse(correctAnswer); if (Array.isArray(parsed)) { correctAnswer = parsed[0]; // Take first element if array } } catch (e) { // Not JSON, use as is } // Compare answer with correct answer (case-insensitive) const isCorrect = userAnswer.toString().toLowerCase().trim() === correctAnswer.toString().toLowerCase().trim(); // Calculate points earned const pointsEarned = isCorrect ? question.points : 0; // Create quiz answer record const quizAnswer = await QuizAnswer.create({ id: uuidv4(), quizSessionId: quizSessionId, questionId: questionId, selectedOption: userAnswer, isCorrect: isCorrect, pointsEarned: pointsEarned, timeTaken: timeSpent, answeredAt: new Date() }, { transaction }); // Update quiz session stats if (isCorrect) { await quizSession.increment('correctAnswers', { by: 1, transaction }); await quizSession.increment('score', { by: pointsEarned, transaction }); } await quizSession.increment('questionsAnswered', { by: 1, transaction }); await quizSession.increment('timeSpent', { by: timeSpent, transaction }); // Update question stats await question.increment('timesAttempted', { by: 1, transaction }); if (isCorrect) { await question.increment('timesCorrect', { by: 1, transaction }); } // Commit transaction await transaction.commit(); // Return response with feedback (exclude correct answer if incorrect) const response = { success: true, data: { answerId: quizAnswer.id, questionId: question.id, isCorrect: isCorrect, pointsEarned: pointsEarned, timeTaken: timeSpent, feedback: { explanation: question.explanation, questionText: question.questionText, userAnswer: userAnswer, difficulty: question.difficulty, category: question.category }, sessionProgress: { questionsAnswered: quizSession.questionsAnswered + 1, totalQuestions: quizSession.totalQuestions, currentScore: quizSession.score + pointsEarned, correctAnswers: quizSession.correctAnswers + (isCorrect ? 1 : 0) } }, message: isCorrect ? 'Correct answer!' : 'Incorrect answer' }; // Only include correct answer in response if user got it wrong (for learning) if (!isCorrect) { response.data.feedback.correctAnswer = question.correctAnswer; } res.status(201).json(response); } catch (error) { await transaction.rollback(); console.error('Error submitting answer:', error); res.status(500).json({ success: false, message: 'An error occurred while submitting the answer', error: process.env.NODE_ENV === 'development' ? error.message : undefined }); } }; /** * Complete Quiz Session * Finalize a quiz session, calculate final score and results * POST /api/quiz/complete */ exports.completeQuizSession = async (req, res) => { const transaction = await sequelize.transaction(); try { const { sessionId } = req.body; // Validate required fields if (!sessionId) { return res.status(400).json({ success: false, message: 'Session ID is required' }); } // Validate UUID format const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; if (!uuidRegex.test(sessionId)) { return res.status(400).json({ success: false, message: 'Invalid session ID format' }); } // Find the quiz session const quizSession = await QuizSession.findByPk(sessionId, { include: [ { model: Category, as: 'category', attributes: ['id', 'name', 'slug', 'icon', 'color'] } ], transaction }); if (!quizSession) { await transaction.rollback(); return res.status(404).json({ success: false, message: 'Quiz session not found' }); } // Check authorization - user must own the session const isUser = req.user && quizSession.userId === req.user.userId; const isGuest = req.guestSessionId && quizSession.guestSessionId === req.guestSessionId; if (!isUser && !isGuest) { await transaction.rollback(); return res.status(403).json({ success: false, message: 'You do not have permission to complete this quiz session' }); } // Check if session is already completed if (quizSession.status === 'completed') { await transaction.rollback(); return res.status(400).json({ success: false, message: 'Quiz session is already completed' }); } // Check if session is in progress if (quizSession.status !== 'in_progress') { await transaction.rollback(); return res.status(400).json({ success: false, message: `Cannot complete quiz session with status: ${quizSession.status}` }); } // Get all answers for this session const answers = await QuizAnswer.findAll({ where: { quizSessionId: sessionId }, transaction }); // Calculate final score (already tracked in session) const finalScore = parseFloat(quizSession.score) || 0; const totalQuestions = quizSession.totalQuestions; const questionsAnswered = quizSession.questionsAnswered; const correctAnswers = quizSession.correctAnswers; // Calculate percentage const percentage = totalQuestions > 0 ? Math.round((finalScore / quizSession.totalPoints) * 100) : 0; // Determine pass/fail (70% is passing) const isPassed = percentage >= 70; // Calculate time taken (in seconds) const endTime = new Date(); const startTime = new Date(quizSession.startedAt); const timeTaken = Math.floor((endTime - startTime) / 1000); // seconds // Check if timed quiz exceeded time limit let isTimeout = false; if (quizSession.timeLimit && timeTaken > quizSession.timeLimit) { isTimeout = true; } // Update quiz session await quizSession.update({ status: isTimeout ? 'timeout' : 'completed', score: finalScore, percentage, isPassed, timeSpent: timeTaken, endTime, completedAt: endTime }, { transaction }); // Update user stats if registered user if (quizSession.userId) { const { User } = require('../models'); const user = await User.findByPk(quizSession.userId, { transaction }); if (user) { await user.increment('totalQuizzes', { by: 1, transaction }); if (isPassed) { await user.increment('quizzesPassed', { by: 1, transaction }); } // Update accuracy - use correct field names await user.increment('totalQuestionsAnswered', { by: questionsAnswered, transaction }); await user.increment('correctAnswers', { by: correctAnswers, transaction }); // Calculate streak (simplified - can be enhanced later) const today = new Date().toDateString(); const lastActive = user.lastActiveDate ? new Date(user.lastActiveDate).toDateString() : null; if (lastActive === today) { // Already counted for today } else { const yesterday = new Date(); yesterday.setDate(yesterday.getDate() - 1); const yesterdayStr = yesterday.toDateString(); if (lastActive === yesterdayStr) { // Continue streak await user.increment('currentStreak', { by: 1, transaction }); if (user.currentStreak + 1 > user.longestStreak) { await user.update({ longestStreak: user.currentStreak + 1 }, { transaction }); } } else { // Reset streak await user.update({ currentStreak: 1 }, { transaction }); // Update longest streak if necessary if (user.longestStreak < 1) { await user.update({ longestStreak: 1 }, { transaction }); } } } await user.update({ lastActiveDate: new Date() }, { transaction }); } } // Commit transaction await transaction.commit(); // Prepare detailed results const results = { sessionId: quizSession.id, status: quizSession.status, category: { id: quizSession.category.id, name: quizSession.category.name, slug: quizSession.category.slug, icon: quizSession.category.icon, color: quizSession.category.color }, quizType: quizSession.quizType, difficulty: quizSession.difficulty, score: { earned: finalScore, total: parseFloat(quizSession.totalPoints) || 0, percentage }, questions: { total: totalQuestions, answered: questionsAnswered, correct: correctAnswers, incorrect: questionsAnswered - correctAnswers, unanswered: totalQuestions - questionsAnswered }, accuracy: questionsAnswered > 0 ? Math.round((correctAnswers / questionsAnswered) * 100) : 0, isPassed, time: { started: quizSession.startedAt, completed: quizSession.completedAt, taken: timeTaken, limit: quizSession.timeLimit, isTimeout } }; res.status(200).json({ success: true, data: results, message: isPassed ? 'Congratulations! Quiz completed successfully' : 'Quiz completed. Keep practicing to improve!' }); } catch (error) { await transaction.rollback(); console.error('Error completing quiz session:', error); res.status(500).json({ success: false, message: 'An error occurred while completing the quiz session', error: process.env.NODE_ENV === 'development' ? error.message : undefined }); } }; /** * Get session details with questions and answers * GET /api/quiz/session/:sessionId */ exports.getSessionDetails = async (req, res) => { try { const { sessionId } = req.params; const userId = req.user?.userId; const guestSessionId = req.guestSessionId; // Validate: Must be either authenticated user or guest if (!userId && !guestSessionId) { return res.status(401).json({ success: false, message: 'Authentication required. Please login or start a guest session.' }); } // Validate sessionId is provided if (!sessionId) { return res.status(400).json({ success: false, message: 'Session ID is required' }); } // Validate UUID format const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; if (!uuidRegex.test(sessionId)) { return res.status(400).json({ success: false, message: 'Invalid session ID format' }); } // Find quiz session with all associations const quizSession = await QuizSession.findByPk(sessionId, { include: [ { model: Category, as: 'category', attributes: ['id', 'name', 'slug', 'icon', 'color'] } ] }); // Check if session exists if (!quizSession) { return res.status(404).json({ success: false, message: 'Quiz session not found' }); } // Authorization: Check if session belongs to current user/guest const isOwner = userId ? quizSession.userId === userId : quizSession.guestSessionId === guestSessionId; if (!isOwner) { return res.status(403).json({ success: false, message: 'You do not have permission to access this quiz session' }); } // Get questions for this session with their answers const sessionQuestions = await QuizSessionQuestion.findAll({ where: { quizSessionId: sessionId }, include: [ { model: Question, as: 'question', attributes: [ 'id', 'questionText', 'questionType', 'options', 'difficulty', 'points', 'explanation', 'tags', 'correctAnswer' // Include correct answer for review ], include: [ { model: Category, as: 'category', attributes: ['id', 'name', 'slug', 'icon', 'color'] } ] } ], order: [['questionOrder', 'ASC']] }); // Get all answers for this session const answers = await QuizAnswer.findAll({ where: { quizSessionId: sessionId }, attributes: ['questionId', 'selectedOption', 'isCorrect', 'pointsEarned', 'timeTaken', 'answeredAt'] }); // Create a map of answers by questionId for quick lookup const answerMap = {}; answers.forEach(answer => { answerMap[answer.questionId] = { userAnswer: answer.selectedOption, isCorrect: answer.isCorrect, pointsEarned: parseFloat(answer.pointsEarned) || 0, timeTaken: answer.timeTaken, answeredAt: answer.answeredAt }; }); // Build questions array with answers const questionsWithAnswers = sessionQuestions.map(sq => { const question = sq.question; const answer = answerMap[question.id]; return { id: question.id, questionText: question.questionText, questionType: question.questionType, options: question.options, difficulty: question.difficulty, points: question.points, explanation: question.explanation, tags: question.tags, order: sq.questionOrder, // Only show correct answer if session is completed or if already answered correctAnswer: (quizSession.status === 'completed' || quizSession.status === 'timeout' || answer) ? question.correctAnswer : undefined, userAnswer: answer?.userAnswer || null, isCorrect: answer ? answer.isCorrect : null, pointsEarned: answer?.pointsEarned || 0, timeTaken: answer?.timeTaken || null, answeredAt: answer?.answeredAt || null, isAnswered: !!answer }; }); // Calculate progress const totalQuestions = sessionQuestions.length; const answeredQuestions = answers.length; const correctAnswers = answers.filter(a => a.isCorrect).length; const incorrectAnswers = answers.filter(a => !a.isCorrect).length; const unansweredQuestions = totalQuestions - answeredQuestions; const progressPercentage = totalQuestions > 0 ? Math.round((answeredQuestions / totalQuestions) * 100) : 0; // Calculate time tracking const startedAt = quizSession.startedAt; const endTime = quizSession.endTime || new Date(); const timeSpent = Math.floor((endTime - startedAt) / 1000); // seconds const timeLimit = quizSession.timeLimit; // already in seconds const timeRemaining = timeLimit ? Math.max(0, timeLimit - timeSpent) : null; // Build response const response = { success: true, data: { session: { id: quizSession.id, status: quizSession.status, quizType: quizSession.quizType, difficulty: quizSession.difficulty, category: { id: quizSession.category.id, name: quizSession.category.name, slug: quizSession.category.slug, icon: quizSession.category.icon, color: quizSession.category.color }, score: { earned: parseFloat(quizSession.score) || 0, total: parseFloat(quizSession.totalPoints) || 0, percentage: quizSession.percentage || 0 }, isPassed: quizSession.isPassed || false, startedAt, completedAt: quizSession.completedAt, timeSpent, // seconds timeLimit, // seconds timeRemaining // seconds (null if no limit) }, progress: { totalQuestions, answeredQuestions, correctAnswers, incorrectAnswers, unansweredQuestions, progressPercentage }, questions: questionsWithAnswers }, message: 'Quiz session details retrieved successfully' }; return res.status(200).json(response); } catch (error) { console.error('Error getting session details:', error); return res.status(500).json({ success: false, message: 'An error occurred while retrieving session details', error: process.env.NODE_ENV === 'development' ? error.message : undefined }); } }; /** * Review completed quiz session with all answers and explanations * GET /api/quiz/review/:sessionId */ exports.reviewQuizSession = async (req, res) => { try { const { sessionId } = req.params; const userId = req.user?.userId; const guestSessionId = req.guestSessionId; // Validate: Must be either authenticated user or guest if (!userId && !guestSessionId) { return res.status(401).json({ success: false, message: 'Authentication required. Please login or start a guest session.' }); } // Validate sessionId is provided if (!sessionId) { return res.status(400).json({ success: false, message: 'Session ID is required' }); } // Validate UUID format const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; if (!uuidRegex.test(sessionId)) { return res.status(400).json({ success: false, message: 'Invalid session ID format' }); } // Find quiz session with category const quizSession = await QuizSession.findByPk(sessionId, { include: [ { model: Category, as: 'category', attributes: ['id', 'name', 'slug', 'icon', 'color'] } ] }); // Check if session exists if (!quizSession) { return res.status(404).json({ success: false, message: 'Quiz session not found' }); } // Authorization: Check if session belongs to current user/guest const isOwner = userId ? quizSession.userId === userId : quizSession.guestSessionId === guestSessionId; if (!isOwner) { return res.status(403).json({ success: false, message: 'You do not have permission to access this quiz session' }); } // Check if session is completed or timed out if (quizSession.status !== 'completed' && quizSession.status !== 'timeout') { return res.status(400).json({ success: false, message: 'Can only review completed or timed out quiz sessions', currentStatus: quizSession.status }); } // Get questions for this session with their details const sessionQuestions = await QuizSessionQuestion.findAll({ where: { quizSessionId: sessionId }, include: [ { model: Question, as: 'question', attributes: [ 'id', 'questionText', 'questionType', 'options', 'difficulty', 'points', 'explanation', 'tags', 'correctAnswer' // Include for review ], include: [ { model: Category, as: 'category', attributes: ['id', 'name', 'slug'] } ] } ], order: [['questionOrder', 'ASC']] }); // Get all answers for this session const answers = await QuizAnswer.findAll({ where: { quizSessionId: sessionId }, attributes: ['questionId', 'selectedOption', 'isCorrect', 'pointsEarned', 'timeTaken', 'answeredAt'] }); // Create a map of answers by questionId const answerMap = {}; answers.forEach(answer => { answerMap[answer.questionId] = { userAnswer: answer.selectedOption, isCorrect: answer.isCorrect, pointsEarned: parseFloat(answer.pointsEarned) || 0, timeTaken: answer.timeTaken, answeredAt: answer.answeredAt }; }); // Build questions array with complete review information const reviewQuestions = sessionQuestions.map(sq => { const question = sq.question; const answer = answerMap[question.id]; // Determine result status for visual feedback let resultStatus = 'unanswered'; if (answer) { resultStatus = answer.isCorrect ? 'correct' : 'incorrect'; } // For multiple choice, mark which option is correct and which was selected let optionsWithFeedback = null; if (question.questionType === 'multiple' && question.options) { optionsWithFeedback = question.options.map(option => ({ id: option.id, text: option.text, isCorrect: option.id === question.correctAnswer, isSelected: answer ? option.id === answer.userAnswer : false, feedback: option.id === question.correctAnswer ? 'correct-answer' : (answer && option.id === answer.userAnswer ? 'user-selected-wrong' : null) })); } return { id: question.id, questionText: question.questionText, questionType: question.questionType, options: optionsWithFeedback || question.options, difficulty: question.difficulty, points: question.points, explanation: question.explanation, tags: question.tags, order: sq.questionOrder, correctAnswer: question.correctAnswer, userAnswer: answer?.userAnswer || null, isCorrect: answer ? answer.isCorrect : null, resultStatus, // 'correct', 'incorrect', 'unanswered' pointsEarned: answer?.pointsEarned || 0, pointsPossible: question.points, timeTaken: answer?.timeTaken || null, answeredAt: answer?.answeredAt || null, // Visual feedback helpers showExplanation: true, wasAnswered: !!answer }; }); // Calculate summary statistics const totalQuestions = sessionQuestions.length; const answeredQuestions = answers.length; const correctAnswers = answers.filter(a => a.isCorrect).length; const incorrectAnswers = answers.filter(a => !a.isCorrect).length; const unansweredQuestions = totalQuestions - answeredQuestions; const finalScore = parseFloat(quizSession.score) || 0; const totalPoints = parseFloat(quizSession.totalPoints) || 0; const accuracy = answeredQuestions > 0 ? Math.round((correctAnswers / answeredQuestions) * 100) : 0; // Calculate time statistics const totalTimeTaken = answers.reduce((sum, a) => sum + (a.timeTaken || 0), 0); const averageTimePerQuestion = answeredQuestions > 0 ? Math.round(totalTimeTaken / answeredQuestions) : 0; // Build response const response = { success: true, data: { session: { id: quizSession.id, status: quizSession.status, quizType: quizSession.quizType, difficulty: quizSession.difficulty, category: { id: quizSession.category.id, name: quizSession.category.name, slug: quizSession.category.slug, icon: quizSession.category.icon, color: quizSession.category.color }, startedAt: quizSession.startedAt, completedAt: quizSession.completedAt, timeSpent: quizSession.timeSpent }, summary: { score: { earned: finalScore, total: totalPoints, percentage: quizSession.percentage || 0 }, questions: { total: totalQuestions, answered: answeredQuestions, correct: correctAnswers, incorrect: incorrectAnswers, unanswered: unansweredQuestions }, accuracy, isPassed: quizSession.isPassed || false, timeStatistics: { totalTime: totalTimeTaken, averageTimePerQuestion, timeLimit: quizSession.timeLimit, wasTimedOut: quizSession.status === 'timeout' } }, questions: reviewQuestions }, message: 'Quiz review retrieved successfully' }; return res.status(200).json(response); } catch (error) { console.error('Error reviewing quiz session:', error); return res.status(500).json({ success: false, message: 'An error occurred while reviewing quiz session', error: process.env.NODE_ENV === 'development' ? error.message : undefined }); } };