From 44d9ce2b2981f08536cf8e463310db1afed30655 Mon Sep 17 00:00:00 2001 From: Nasser AlKawari <60303522@udst.edu.qa> Date: Sat, 11 Apr 2026 20:01:01 +0300 Subject: [PATCH] fix: resolve quiz score calculation and MongoDB validation errors --- backend/models/Result.js | 45 +++++--- backend/routes/quizRoutes.js | 92 ++++++++++++++++- backend/services/scoreService.js | 70 ++++--------- src/pages/DashboardPage.jsx | 170 +++++++++++++++++++++++-------- 4 files changed, 266 insertions(+), 111 deletions(-) diff --git a/backend/models/Result.js b/backend/models/Result.js index c540504..335e321 100644 --- a/backend/models/Result.js +++ b/backend/models/Result.js @@ -1,19 +1,34 @@ const mongoose = require('mongoose'); -const resultDetailSchema = new mongoose.Schema({ - question: { type: String, required: true }, - userAnswer: { type: String }, - correctAnswer: { type: String, required: true }, - isCorrect: { type: Boolean, required: true }, -}); - -const resultSchema = new mongoose.Schema({ - quizId: { type: mongoose.Schema.Types.ObjectId, ref: 'Quiz' }, - score: { type: Number, required: true }, - total: { type: Number, required: true }, - percentage: { type: Number, required: true }, - results: { type: [resultDetailSchema], required: true }, - submittedAt:{ type: Date, default: Date.now }, -}); +const resultSchema = new mongoose.Schema( + { + quizId: { + type: mongoose.Schema.Types.ObjectId, + ref: 'Quiz', + default: null, + }, + score: { + type: Number, + required: true, + }, + total: { + type: Number, + required: true, + }, + correctAnswers: { + type: Number, + required: true, + }, + wrongAnswers: { + type: Number, + required: true, + }, + answers: { + type: [String], + default: [], + }, + }, + { timestamps: true } +); module.exports = mongoose.model('Result', resultSchema); \ No newline at end of file diff --git a/backend/routes/quizRoutes.js b/backend/routes/quizRoutes.js index 5a77605..270923a 100644 --- a/backend/routes/quizRoutes.js +++ b/backend/routes/quizRoutes.js @@ -49,25 +49,107 @@ router.post('/submit', async (req, res) => { if (!questions || !Array.isArray(questions)) { return res.status(400).json({ error: 'Bad Request: "questions" must be an array' }); } + if (!answers || !Array.isArray(answers)) { return res.status(400).json({ error: 'Bad Request: "answers" must be an array' }); } + if (questions.length === 0) { - return res.status(400).json({ error: 'Bad Request: "questions" array cannot be empty' }); + return res.status(400).json({ error: 'Bad Request: "questions" cannot be empty' }); + } + + if (questions.length !== answers.length) { + return res.status(400).json({ error: 'Mismatch between questions and answers length' }); } const scoreResult = calculateScore(questions, answers); + // Ensure all required fields exist + const normalizedResult = { + score: scoreResult.score, + total: scoreResult.total, + correctAnswers: scoreResult.correctAnswers, + wrongAnswers: scoreResult.wrongAnswers, + }; + // Save result to MongoDB const result = await Result.create({ quizId: quizId || null, - ...scoreResult, + ...normalizedResult, + answers, + }); + + return res.status(200).json({ + resultId: result._id, + ...normalizedResult, }); - return res.status(200).json({ resultId: result._id, ...scoreResult }); } catch (error) { - const statusCode = error.message.includes('Mismatch') || error.message.includes('malformed') ? 400 : 500; - return res.status(statusCode).json({ error: 'Failed to calculate score', details: error.message }); + return res.status(500).json({ + error: 'Failed to calculate score', + details: error.message, + }); + } +}); + +router.get('/results', async (req, res) => { + try { + const results = await Result.find() + .populate('quizId') + .sort({ createdAt: -1 }); + + return res.status(200).json(results); + } catch (error) { + return res.status(500).json({ + error: 'Failed to fetch results', + details: error.message, + }); + } +}); + +router.get('/dashboard', async (req, res) => { + try { + const results = await Result.find() + .populate('quizId') + .sort({ createdAt: -1 }); + + const completed = results.length; + + const avgScore = completed + ? Math.round( + results.reduce((sum, item) => sum + (item.total ? (item.score / item.total) * 100 : 0), 0) / completed + ) + : 0; + + const recentQuizzes = results.slice(0, 5).map((item) => ({ + id: item._id, + topic: item.quizId?.topic || 'Untitled Quiz', + score: item.score, + total: item.total, + percentage: item.total ? Math.round((item.score / item.total) * 100) : 0, + date: item.createdAt, + })); + + const chartData = results + .slice(0, 7) + .reverse() + .map((item) => ({ + id: item._id, + percentage: item.total ? Math.round((item.score / item.total) * 100) : 0, + })); + + return res.status(200).json({ + completed, + avgScore, + recentQuizzes, + chartData, + latestQuiz: recentQuizzes[0] || null, + }); + } catch (error) { + return res.status(500).json({ + error: 'Failed to fetch dashboard data', + details: error.message, + }); } }); diff --git a/backend/services/scoreService.js b/backend/services/scoreService.js index 013ed8a..5a8da12 100644 --- a/backend/services/scoreService.js +++ b/backend/services/scoreService.js @@ -1,70 +1,40 @@ -// Calculate score from quiz submission -function calculateScore(questions, userAnswers) { - // Validate inputs - if (!Array.isArray(questions) || questions.length === 0) { - throw new Error('Questions array must be a non-empty array'); +function calculateScore(questions, answers) { + if (!Array.isArray(questions) || !Array.isArray(answers)) { + throw new Error("Questions and answers must be arrays"); } - if (!Array.isArray(userAnswers)) { - throw new Error('User answers must be an array'); + if (questions.length !== answers.length) { + throw new Error("Mismatch between questions and answers length"); } - if (questions.length !== userAnswers.length) { - throw new Error( - `Mismatch: ${questions.length} questions but ${userAnswers.length} answers provided` - ); - } - - let score = 0; - const results = []; + let correctAnswers = 0; for (let i = 0; i < questions.length; i++) { const question = questions[i]; - const userAnswer = userAnswers[i]; + const userAnswer = answers[i]; - // Validate question structure - if ( - !question || - typeof question !== 'object' || - !question.question || - !question.answer - ) { - throw new Error(`Question ${i + 1} is malformed`); + if (!question || typeof question !== "object") { + throw new Error(`Malformed question at index ${i}`); } - // Handle null/undefined answers - const normalizedUserAnswer = - userAnswer === null || userAnswer === undefined - ? null - : String(userAnswer).toUpperCase().trim(); - - const correctAnswer = String(question.answer).toUpperCase().trim(); - const isCorrect = - normalizedUserAnswer === correctAnswer && normalizedUserAnswer !== null; - - if (isCorrect) { - score++; + if (!question.answer) { + throw new Error(`Missing answer field in question at index ${i}`); } - results.push({ - question: question.question, - userAnswer: normalizedUserAnswer, - correctAnswer: correctAnswer, - isCorrect: isCorrect, - }); + if (userAnswer === question.answer) { + correctAnswers++; + } } const total = questions.length; - const percentage = Math.round((score / total) * 100); + const wrongAnswers = total - correctAnswers; return { - score: score, - total: total, - percentage: percentage, - results: results, + score: correctAnswers, + total, + correctAnswers, + wrongAnswers, }; } -module.exports = { - calculateScore, -}; +module.exports = { calculateScore }; \ No newline at end of file diff --git a/src/pages/DashboardPage.jsx b/src/pages/DashboardPage.jsx index 78dcb2c..bd7ed36 100644 --- a/src/pages/DashboardPage.jsx +++ b/src/pages/DashboardPage.jsx @@ -1,7 +1,72 @@ +import { useEffect, useState } from "react"; import { useNavigate } from "react-router-dom"; function DashboardPage() { const navigate = useNavigate(); + const [dashboardData, setDashboardData] = useState({ + completed: 0, + avgScore: 0, + recentQuizzes: [], + chartData: [], + latestQuiz: null, + }); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(""); + + useEffect(() => { + const fetchDashboardData = async () => { + try { + setLoading(true); + setError(""); + + const response = await fetch("http://localhost:5000/api/quiz/dashboard"); + const data = await response.json(); + + if (!response.ok) { + throw new Error(data.error || "Failed to fetch dashboard data."); + } + + setDashboardData(data); + } catch (err) { + setError(err.message || "Failed to load dashboard."); + } finally { + setLoading(false); + } + }; + + fetchDashboardData(); + }, []); + + const { completed, avgScore, recentQuizzes, chartData, latestQuiz } = dashboardData; + + const safeChartData = [...chartData]; + while (safeChartData.length < 7) { + safeChartData.unshift({ percentage: 0 }); + } + + if (loading) { + return ( +
+
+

Loading dashboard...

+
+
+ ); + } + + if (error) { + return ( +
+
+

Failed to load dashboard

+

{error}

+ +
+
+ ); + } return (
@@ -19,14 +84,20 @@ function DashboardPage() { - +

Welcome back, Alex.

-

Your cognitive flow is at 84% today.

+

+ {completed > 0 + ? `You have completed ${completed} quiz${completed > 1 ? "zes" : ""} so far.` + : "Start your first quiz to see your progress here."} +

Profile
@@ -34,15 +105,26 @@ function DashboardPage() {
-
IN PROGRESS
-

Quantum Mechanics 101

+
+ {latestQuiz ? "LATEST RESULT" : "NO QUIZ YET"} +
+ +

{latestQuiz ? latestQuiz.topic : "No quiz attempts yet"}

+

- You’ve completed 14 of 20 questions. Picking up where you left off - will boost your recall score. + {latestQuiz + ? `You scored ${latestQuiz.score}/${latestQuiz.total} on your latest quiz.` + : "Generate and complete a quiz to populate your dashboard with real data."}

- + + +
+ +
+ {latestQuiz ? `${latestQuiz.percentage}%` : "0%"}
-
70%
@@ -52,11 +134,11 @@ function DashboardPage() {
- 128 + {completed} Completed
- 88% + {avgScore}% Avg Score
@@ -64,47 +146,53 @@ function DashboardPage() {

Score History

-

Performance over the last 30 days

+

Performance over recent attempts

+
- - - - - - - + {safeChartData.map((item, index) => ( + x.percentage)) && + item.percentage > 0 + ? "active-bar" + : "" + } + style={{ height: `${Math.max(item.percentage, 15) * 2}px` }} + > + ))}

Recent Quizzes

- View All -
- -
-
- Modern History -

Oct 12, 2024

-
-
9/10
+ navigate("/generate")}> + New Quiz +
-
-
- Bio-Genetics -

Oct 10, 2024

+ {recentQuizzes.length === 0 ? ( +
+
+ No quizzes yet +

Complete a quiz to see history here.

+
+
0/0
-
7/10
-
- -
-
- React Fundamentals -

Oct 08, 2024

-
-
8/10
-
+ ) : ( + recentQuizzes.map((item) => ( +
+
+ {item.topic} +

{new Date(item.date).toLocaleDateString()}

+
+
+ {item.score}/{item.total} +
+
+ )) + )}