diff --git a/frontend/src/app/core/models/quiz.model.ts b/frontend/src/app/core/models/quiz.model.ts index be1aa03..543f9c1 100644 --- a/frontend/src/app/core/models/quiz.model.ts +++ b/frontend/src/app/core/models/quiz.model.ts @@ -3,12 +3,12 @@ import { Question } from './question.model'; export interface QuizSessionHistory { - "time": { - "spent": number, - "limit": number | null, - "percentage": number + time: { + spent: number, + limit: number | null, + percentage: number }, - "createdAt": "2025-12-19T18:49:58.000Z" + createdAt: string; id: string; category?: { @@ -111,7 +111,7 @@ export interface QuizStartResponse { */ export interface QuizAnswerSubmission { questionId: string; - answer: string | string[]; + userAnswer: string | string[]; quizSessionId: string; timeSpent?: number; } @@ -146,18 +146,122 @@ export interface QuizResults { questions: QuizQuestionResult[]; } + +// Response from /complete endpoint - questions are statistics +export interface CompletedQuizResult { + sessionId: string; + status: string; + category: { + id: string; + name: string; + slug: string; + icon: string; + color: string + }, + quizType: string + difficulty: string + score: { + earned: number, + total: number, + percentage: number + }, + questions: { + total: number, + answered: number, + correct: number, + incorrect: number, + unanswered: number + }, + accuracy: number, + isPassed: boolean, + time: { + started: string, + completed: string, + taken: number, + limit: number, + isTimeout: boolean + } +} + +export interface CompletedQuizResponse { + success: boolean + data: CompletedQuizResult +} + +// Response from /review endpoint - questions are detailed array +export interface QuizReviewResult { + session: { + id: string; + status: string; + quizType: string; + difficulty: string; + category: { + id: string; + name: string; + slug: string; + icon: string; + color: string; + }; + startedAt: string; + completedAt: string; + timeSpent: number; + }; + summary: { + score: { + earned: number; + total: number; + percentage: number; + }; + questions: { + total: number; + answered: number; + correct: number; + incorrect: number; + unanswered: number; + }; + accuracy: number; + isPassed: boolean; + timeStatistics: { + totalTime: number; + averageTimePerQuestion: number; + timeLimit: number | null; + wasTimedOut: boolean; + }; + }; + questions: QuizQuestionResult[]; +} + +export interface QuizReviewResponse { + success: boolean; + data: QuizReviewResult; + message?: string; +} + /** * Quiz Question Result */ export interface QuizQuestionResult { - questionId: string; + id: string; questionText: string; questionType: string; - userAnswer: string | string[]; - correctAnswer: string | string[]; - isCorrect: boolean; - explanation: string; + options: any; + difficulty: string; points: number; + explanation: string; + tags: string[]; + order: number; + correctAnswer: string | string[]; + userAnswer: string | string[] | null; + isCorrect: boolean | null; + resultStatus: 'correct' | 'incorrect' | 'unanswered'; + pointsEarned: number; + pointsPossible: number; + timeTaken: number | null; + answeredAt: string | null; + showExplanation: boolean; + wasAnswered: boolean; + // Legacy support + questionId?: string; timeSpent?: number; } diff --git a/frontend/src/app/core/services/quiz.service.ts b/frontend/src/app/core/services/quiz.service.ts index 33391b5..7f8c278 100644 --- a/frontend/src/app/core/services/quiz.service.ts +++ b/frontend/src/app/core/services/quiz.service.ts @@ -10,7 +10,11 @@ import { QuizAnswerSubmission, QuizAnswerResponse, QuizResults, - QuizStartFormRequest + QuizStartFormRequest, + CompletedQuizResult, + CompletedQuizResponse, + QuizReviewResult, + QuizReviewResponse } from '../models/quiz.model'; import { ToastService } from './toast.service'; import { StorageService } from './storage.service'; @@ -37,7 +41,7 @@ export class QuizService { readonly questions = this._questions.asReadonly(); // Quiz results state - private readonly _quizResults = signal(null); + private readonly _quizResults = signal(null); readonly quizResults = this._quizResults.asReadonly(); // Loading states @@ -131,7 +135,22 @@ export class QuizService { submitAnswer(submission: QuizAnswerSubmission): Observable { this._isSubmittingAnswer.set(true); - return this.http.post(`${this.apiUrl}/submit`, submission).pipe( + return this.http.post(`${this.apiUrl}/submit`, submission).pipe( + map(response => { + // Backend returns: { success, data: { isCorrect, pointsEarned, feedback: { explanation, correctAnswer }, sessionProgress: { currentScore } } } + // Frontend expects: { success, isCorrect, correctAnswer, explanation, points, score } + const backendData = response.data; + const mappedResponse: QuizAnswerResponse = { + success: response.success, + isCorrect: backendData.isCorrect, + correctAnswer: backendData.feedback?.correctAnswer || '', + explanation: backendData.feedback?.explanation || '', + points: backendData.pointsEarned || 0, + score: backendData.sessionProgress?.currentScore || 0, + message: response.message + }; + return mappedResponse; + }), tap(response => { if (response.success) { // Update session state @@ -163,13 +182,13 @@ export class QuizService { /** * Complete the quiz session */ - completeQuiz(sessionId: string): Observable { + completeQuiz(sessionId: string): Observable { this._isCompletingQuiz.set(true); - return this.http.post(`${this.apiUrl}/complete`, { sessionId }).pipe( + return this.http.post(`${this.apiUrl}/complete`, { sessionId }).pipe( tap(results => { if (results.success) { - this._quizResults.set(results); + this._quizResults.set(results.data); // Update session status const currentSession = this._activeSession(); @@ -220,11 +239,11 @@ export class QuizService { /** * Get quiz review data */ - reviewQuiz(sessionId: string): Observable { - return this.http.get(`${this.apiUrl}/review/${sessionId}`).pipe( + reviewQuiz(sessionId: string): Observable { + return this.http.get(`${this.apiUrl}/review/${sessionId}`).pipe( tap(results => { if (results.success) { - this._quizResults.set(results); + this._quizResults.set(results.data); } }), catchError(error => { diff --git a/frontend/src/app/features/quiz/quiz-question/quiz-question.html b/frontend/src/app/features/quiz/quiz-question/quiz-question.html index 5932abf..1bba3a4 100644 --- a/frontend/src/app/features/quiz/quiz-question/quiz-question.html +++ b/frontend/src/app/features/quiz/quiz-question/quiz-question.html @@ -102,10 +102,10 @@ - @if (!answerResult()?.isCorrect) { + @if (answerResult() && !answerResult()!.isCorrect) {
Correct Answer: -

{{ answerResult()?.correctAnswer }}

+

{{ answerResult()!.correctAnswer }}

} @@ -123,9 +123,6 @@ } - {{this.answerForm?.valid }} - {{ !this.answerSubmitted()}} - {{ !this.isSubmittingAnswer()}}
@if (!answerSubmitted()) { diff --git a/frontend/src/app/features/quiz/quiz-question/quiz-question.ts b/frontend/src/app/features/quiz/quiz-question/quiz-question.ts index ade4cb4..cfecf7e 100644 --- a/frontend/src/app/features/quiz/quiz-question/quiz-question.ts +++ b/frontend/src/app/features/quiz/quiz-question/quiz-question.ts @@ -269,7 +269,7 @@ export class QuizQuestionComponent implements OnInit, OnDestroy { const submission: QuizAnswerSubmission = { questionId: question.id, - answer: answer, + userAnswer: answer, quizSessionId: this.sessionId }; diff --git a/frontend/src/app/features/quiz/quiz-review/quiz-review.html b/frontend/src/app/features/quiz/quiz-review/quiz-review.html index ca92985..64f6d19 100644 --- a/frontend/src/app/features/quiz/quiz-review/quiz-review.html +++ b/frontend/src/app/features/quiz/quiz-review/quiz-review.html @@ -1,243 +1,208 @@
@if (isLoading()) { -
- -

Loading review...

-
+
+ +

Loading review...

+
} @if (!isLoading() && results()) { -
- -
- -
-

Quiz Review

-

Review your answers and learn from mistakes

-
-
- - -
- - -
- quiz -
-
-
{{ allQuestions().length }}
-
Total Questions
-
-
-
- - - -
- check_circle -
-
-
{{ correctCount() }}
-
Correct
-
-
-
- - - -
- cancel -
-
-
{{ incorrectCount() }}
-
Incorrect
-
-
-
- - - -
- emoji_events -
-
-
{{ results()!.percentage }}%
-
Score
-
-
-
-
- - -
- - - -
- - -
- @for (question of paginatedQuestions(); track question.questionId; let i = $index) { - - -
-
- {{ (pageIndex() * pageSize()) + i + 1 }} - - {{ question.isCorrect ? 'check_circle' : 'cancel' }} - -
- -
- - {{ getQuestionTypeText(question.questionType) }} - - {{ question.points }} pts -
- - @if (isAuthenticated()) { - - } -
-
- - -
{{ question.questionText }}
- - - -
-
-
Your Answer:
-
- {{ formatAnswer(question.userAnswer) || 'Not answered' }} - @if (!question.isCorrect) { - close - } @else { - check - } -
-
- - @if (!question.isCorrect) { -
-
Correct Answer:
-
- {{ formatAnswer(question.correctAnswer) }} - check -
-
- } -
- - @if (question.explanation) { -
-
- lightbulb - Explanation -
-

{{ question.explanation }}

-
- } - - @if (question.timeSpent) { -
- schedule - Time spent: {{ question.timeSpent }}s -
- } -
-
- } - - @if (paginatedQuestions().length === 0) { -
- info -

No questions match the selected filter

-
- } -
- - - @if (totalQuestions() > pageSize()) { - - - } - - -
- - - - - +
+ +
+ +
+

Quiz Review

+

Review your answers and learn from mistakes

+ + +
+ + +
+ quiz +
+
+
{{ allQuestions().length }}
+
Total Questions
+
+
+
+ + + +
+ check_circle +
+
+
{{ correctCount() }}
+
Correct
+
+
+
+ + + +
+ cancel +
+
+
{{ incorrectCount() }}
+
Incorrect
+
+
+
+ + + +
+ emoji_events +
+
+
{{ scorePercentage() }}%
+
Score
+
+
+
+
+ + +
+ + + +
+ + +
+ @for (question of paginatedQuestions(); track question.id; let i = $index) { + + +
+
+ {{ (pageIndex() * pageSize()) + i + 1 }} + + {{ question.isCorrect ? 'check_circle' : 'cancel' }} + +
+ +
+ + {{ getQuestionTypeText(question.questionType) }} + + {{ question.points }} pts +
+ + @if (isAuthenticated()) { + + } +
+
+ + +
{{ question.questionText }}
+ + + +
+
+
Your Answer:
+
+ {{ formatAnswer(question.userAnswer ?? '') || 'Not answered' }} + @if (!question.isCorrect) { + close + } @else { + check + } +
+
+ + @if (!question.isCorrect) { +
+
Correct Answer:
+
+ {{ formatAnswer(question.correctAnswer) }} + check +
+
+ } +
+ + @if (question.explanation) { +
+
+ lightbulb + Explanation +
+

{{ question.explanation }}

+
+ } + + @if (question.timeSpent) { +
+ schedule + Time spent: {{ question.timeSpent }}s +
+ } +
+
+ } + + @if (paginatedQuestions().length === 0) { +
+ info +

No questions match the selected filter

+
+ } +
+ + + @if (totalQuestions() > pageSize()) { + + + } + + +
+ + + + + +
+
} -
+
\ No newline at end of file diff --git a/frontend/src/app/features/quiz/quiz-review/quiz-review.ts b/frontend/src/app/features/quiz/quiz-review/quiz-review.ts index e7527e2..c35e087 100644 --- a/frontend/src/app/features/quiz/quiz-review/quiz-review.ts +++ b/frontend/src/app/features/quiz/quiz-review/quiz-review.ts @@ -12,7 +12,7 @@ import { MatPaginatorModule, PageEvent } from '@angular/material/paginator'; import { Subject, takeUntil } from 'rxjs'; import { QuizService } from '../../../core/services/quiz.service'; import { StorageService } from '../../../core/services/storage.service'; -import { QuizResults, QuizQuestionResult } from '../../../core/models/quiz.model'; +import { QuizReviewResult, QuizQuestionResult } from '../../../core/models/quiz.model'; @Component({ selector: 'app-quiz-review', @@ -41,25 +41,32 @@ export class QuizReviewComponent implements OnInit, OnDestroy { readonly sessionId = signal(''); readonly results = this.quizService.quizResults; readonly isLoading = signal(true); - + // Pagination readonly pageSize = signal(10); readonly pageIndex = signal(0); - + // Filter readonly filterType = signal<'all' | 'correct' | 'incorrect'>('all'); - // Computed values - readonly allQuestions = computed(() => this.results()?.questions ?? []); - + // Computed values - handle QuizReviewResult structure + readonly allQuestions = computed(() => { + const res = this.results(); + // Check if it's QuizReviewResult (has questions array) + if (res && 'questions' in res && Array.isArray(res.questions)) { + return res.questions; + } + return []; + }); + readonly filteredQuestions = computed(() => { const questions = this.allQuestions(); const filter = this.filterType(); - + if (filter === 'all') return questions; if (filter === 'correct') return questions.filter(q => q.isCorrect); if (filter === 'incorrect') return questions.filter(q => !q.isCorrect); - + return questions; }); @@ -72,15 +79,31 @@ export class QuizReviewComponent implements OnInit, OnDestroy { readonly totalQuestions = computed(() => this.filteredQuestions().length); - readonly correctCount = computed(() => + readonly correctCount = computed(() => this.allQuestions().filter(q => q.isCorrect).length ); - readonly incorrectCount = computed(() => + readonly incorrectCount = computed(() => this.allQuestions().filter(q => !q.isCorrect).length ); - readonly isAuthenticated = computed(() => + readonly scorePercentage = computed(() => { + const res = this.results(); + if (res && 'summary' in res) { + return res.summary.score.percentage; + } + return 0; + }); + + readonly sessionInfo = computed(() => { + const res = this.results(); + if (res && 'session' in res) { + return res.session; + } + return null; + }); + + readonly isAuthenticated = computed(() => this.storageService.isAuthenticated() ); @@ -95,7 +118,7 @@ export class QuizReviewComponent implements OnInit, OnDestroy { const id = params['sessionId']; if (id) { this.sessionId.set(id); - + // Check for filter query param this.route.queryParams .pipe(takeUntil(this.destroy$)) @@ -174,7 +197,7 @@ export class QuizReviewComponent implements OnInit, OnDestroy { const bookmarks = this.bookmarkedQuestions(); const newBookmarks = new Set(bookmarks); - + if (newBookmarks.has(questionId)) { newBookmarks.delete(questionId); // TODO: Call bookmark service to remove bookmark @@ -184,7 +207,7 @@ export class QuizReviewComponent implements OnInit, OnDestroy { // TODO: Call bookmark service to add bookmark console.log('Add bookmark:', questionId); } - + this.bookmarkedQuestions.set(newBookmarks); }