add changes

This commit is contained in:
AD2025
2025-12-20 00:14:28 +02:00
parent 665919c1e2
commit 079c10e843
6 changed files with 381 additions and 273 deletions

View File

@@ -3,12 +3,12 @@ import { Question } from './question.model';
export interface QuizSessionHistory { export interface QuizSessionHistory {
"time": { time: {
"spent": number, spent: number,
"limit": number | null, limit: number | null,
"percentage": number percentage: number
}, },
"createdAt": "2025-12-19T18:49:58.000Z" createdAt: string;
id: string; id: string;
category?: { category?: {
@@ -111,7 +111,7 @@ export interface QuizStartResponse {
*/ */
export interface QuizAnswerSubmission { export interface QuizAnswerSubmission {
questionId: string; questionId: string;
answer: string | string[]; userAnswer: string | string[];
quizSessionId: string; quizSessionId: string;
timeSpent?: number; timeSpent?: number;
} }
@@ -146,18 +146,122 @@ export interface QuizResults {
questions: QuizQuestionResult[]; 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 * Quiz Question Result
*/ */
export interface QuizQuestionResult { export interface QuizQuestionResult {
questionId: string; id: string;
questionText: string; questionText: string;
questionType: string; questionType: string;
userAnswer: string | string[]; options: any;
correctAnswer: string | string[]; difficulty: string;
isCorrect: boolean;
explanation: string;
points: number; 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; timeSpent?: number;
} }

View File

@@ -10,7 +10,11 @@ import {
QuizAnswerSubmission, QuizAnswerSubmission,
QuizAnswerResponse, QuizAnswerResponse,
QuizResults, QuizResults,
QuizStartFormRequest QuizStartFormRequest,
CompletedQuizResult,
CompletedQuizResponse,
QuizReviewResult,
QuizReviewResponse
} from '../models/quiz.model'; } from '../models/quiz.model';
import { ToastService } from './toast.service'; import { ToastService } from './toast.service';
import { StorageService } from './storage.service'; import { StorageService } from './storage.service';
@@ -37,7 +41,7 @@ export class QuizService {
readonly questions = this._questions.asReadonly(); readonly questions = this._questions.asReadonly();
// Quiz results state // Quiz results state
private readonly _quizResults = signal<QuizResults | null>(null); private readonly _quizResults = signal<CompletedQuizResult | QuizReviewResult | QuizResults | null>(null);
readonly quizResults = this._quizResults.asReadonly(); readonly quizResults = this._quizResults.asReadonly();
// Loading states // Loading states
@@ -131,7 +135,22 @@ export class QuizService {
submitAnswer(submission: QuizAnswerSubmission): Observable<QuizAnswerResponse> { submitAnswer(submission: QuizAnswerSubmission): Observable<QuizAnswerResponse> {
this._isSubmittingAnswer.set(true); this._isSubmittingAnswer.set(true);
return this.http.post<QuizAnswerResponse>(`${this.apiUrl}/submit`, submission).pipe( return this.http.post<any>(`${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 => { tap(response => {
if (response.success) { if (response.success) {
// Update session state // Update session state
@@ -163,13 +182,13 @@ export class QuizService {
/** /**
* Complete the quiz session * Complete the quiz session
*/ */
completeQuiz(sessionId: string): Observable<QuizResults> { completeQuiz(sessionId: string): Observable<CompletedQuizResponse> {
this._isCompletingQuiz.set(true); this._isCompletingQuiz.set(true);
return this.http.post<QuizResults>(`${this.apiUrl}/complete`, { sessionId }).pipe( return this.http.post<CompletedQuizResponse>(`${this.apiUrl}/complete`, { sessionId }).pipe(
tap(results => { tap(results => {
if (results.success) { if (results.success) {
this._quizResults.set(results); this._quizResults.set(results.data);
// Update session status // Update session status
const currentSession = this._activeSession(); const currentSession = this._activeSession();
@@ -220,11 +239,11 @@ export class QuizService {
/** /**
* Get quiz review data * Get quiz review data
*/ */
reviewQuiz(sessionId: string): Observable<QuizResults> { reviewQuiz(sessionId: string): Observable<QuizReviewResponse> {
return this.http.get<QuizResults>(`${this.apiUrl}/review/${sessionId}`).pipe( return this.http.get<QuizReviewResponse>(`${this.apiUrl}/review/${sessionId}`).pipe(
tap(results => { tap(results => {
if (results.success) { if (results.success) {
this._quizResults.set(results); this._quizResults.set(results.data);
} }
}), }),
catchError(error => { catchError(error => {

View File

@@ -102,10 +102,10 @@
</h3> </h3>
</div> </div>
@if (!answerResult()?.isCorrect) { @if (answerResult() && !answerResult()!.isCorrect) {
<div class="correct-answer"> <div class="correct-answer">
<strong>Correct Answer:</strong> <strong>Correct Answer:</strong>
<p>{{ answerResult()?.correctAnswer }}</p> <p>{{ answerResult()!.correctAnswer }}</p>
</div> </div>
} }
@@ -123,9 +123,6 @@
</div> </div>
} }
{{this.answerForm?.valid }}
{{ !this.answerSubmitted()}}
{{ !this.isSubmittingAnswer()}}
<!-- Action Buttons --> <!-- Action Buttons -->
<div class="action-buttons"> <div class="action-buttons">
@if (!answerSubmitted()) { @if (!answerSubmitted()) {

View File

@@ -269,7 +269,7 @@ export class QuizQuestionComponent implements OnInit, OnDestroy {
const submission: QuizAnswerSubmission = { const submission: QuizAnswerSubmission = {
questionId: question.id, questionId: question.id,
answer: answer, userAnswer: answer,
quizSessionId: this.sessionId quizSessionId: this.sessionId
}; };

View File

@@ -65,7 +65,7 @@
<mat-icon>emoji_events</mat-icon> <mat-icon>emoji_events</mat-icon>
</div> </div>
<div class="card-info"> <div class="card-info">
<div class="card-value">{{ results()!.percentage }}%</div> <div class="card-value">{{ scorePercentage() }}%</div>
<div class="card-label">Score</div> <div class="card-label">Score</div>
</div> </div>
</mat-card-content> </mat-card-content>
@@ -74,29 +74,17 @@
<!-- Filter Tabs --> <!-- Filter Tabs -->
<div class="filter-tabs"> <div class="filter-tabs">
<button <button mat-stroked-button [class.active]="filterType() === 'all'" (click)="setFilter('all')">
mat-stroked-button
[class.active]="filterType() === 'all'"
(click)="setFilter('all')"
>
<mat-icon>list</mat-icon> <mat-icon>list</mat-icon>
All Questions ({{ allQuestions().length }}) All Questions ({{ allQuestions().length }})
</button> </button>
<button <button mat-stroked-button [class.active]="filterType() === 'correct'" (click)="setFilter('correct')"
mat-stroked-button class="correct-tab">
[class.active]="filterType() === 'correct'"
(click)="setFilter('correct')"
class="correct-tab"
>
<mat-icon>check_circle</mat-icon> <mat-icon>check_circle</mat-icon>
Correct ({{ correctCount() }}) Correct ({{ correctCount() }})
</button> </button>
<button <button mat-stroked-button [class.active]="filterType() === 'incorrect'" (click)="setFilter('incorrect')"
mat-stroked-button class="incorrect-tab">
[class.active]="filterType() === 'incorrect'"
(click)="setFilter('incorrect')"
class="incorrect-tab"
>
<mat-icon>cancel</mat-icon> <mat-icon>cancel</mat-icon>
Incorrect ({{ incorrectCount() }}) Incorrect ({{ incorrectCount() }})
</button> </button>
@@ -104,7 +92,7 @@
<!-- Questions List --> <!-- Questions List -->
<div class="questions-list"> <div class="questions-list">
@for (question of paginatedQuestions(); track question.questionId; let i = $index) { @for (question of paginatedQuestions(); track question.id; let i = $index) {
<mat-card class="question-card" [class.incorrect]="!question.isCorrect"> <mat-card class="question-card" [class.incorrect]="!question.isCorrect">
<mat-card-header> <mat-card-header>
<div class="question-header-content"> <div class="question-header-content">
@@ -123,15 +111,11 @@
</div> </div>
@if (isAuthenticated()) { @if (isAuthenticated()) {
<button <button mat-icon-button class="bookmark-btn" [class.bookmarked]="isBookmarked(question.id)"
mat-icon-button (click)="toggleBookmark(question.id)"
class="bookmark-btn" [matTooltip]="isBookmarked(question.id) ? 'Remove bookmark' : 'Bookmark question'">
[class.bookmarked]="isBookmarked(question.questionId)"
(click)="toggleBookmark(question.questionId)"
[matTooltip]="isBookmarked(question.questionId) ? 'Remove bookmark' : 'Bookmark question'"
>
<mat-icon> <mat-icon>
{{ isBookmarked(question.questionId) ? 'bookmark' : 'bookmark_border' }} {{ isBookmarked(question.id) ? 'bookmark' : 'bookmark_border' }}
</mat-icon> </mat-icon>
</button> </button>
} }
@@ -147,7 +131,7 @@
<div class="answer-row"> <div class="answer-row">
<div class="answer-label">Your Answer:</div> <div class="answer-label">Your Answer:</div>
<div class="answer-value" [class.incorrect]="!question.isCorrect"> <div class="answer-value" [class.incorrect]="!question.isCorrect">
{{ formatAnswer(question.userAnswer) || 'Not answered' }} {{ formatAnswer(question.userAnswer ?? '') || 'Not answered' }}
@if (!question.isCorrect) { @if (!question.isCorrect) {
<mat-icon class="answer-icon error">close</mat-icon> <mat-icon class="answer-icon error">close</mat-icon>
} @else { } @else {
@@ -197,43 +181,24 @@
<!-- Pagination --> <!-- Pagination -->
@if (totalQuestions() > pageSize()) { @if (totalQuestions() > pageSize()) {
<mat-paginator <mat-paginator [length]="totalQuestions()" [pageSize]="pageSize()" [pageIndex]="pageIndex()"
[length]="totalQuestions()" [pageSizeOptions]="[5, 10, 20, 50]" (page)="onPageChange($event)" showFirstLastButtons>
[pageSize]="pageSize()"
[pageIndex]="pageIndex()"
[pageSizeOptions]="[5, 10, 20, 50]"
(page)="onPageChange($event)"
showFirstLastButtons
>
</mat-paginator> </mat-paginator>
} }
<!-- Action Buttons --> <!-- Action Buttons -->
<div class="action-buttons"> <div class="action-buttons">
<button <button mat-raised-button (click)="backToResults()" class="action-btn">
mat-raised-button
(click)="backToResults()"
class="action-btn"
>
<mat-icon>arrow_back</mat-icon> <mat-icon>arrow_back</mat-icon>
Back to Results Back to Results
</button> </button>
<button <button mat-raised-button color="primary" (click)="retakeQuiz()" class="action-btn">
mat-raised-button
color="primary"
(click)="retakeQuiz()"
class="action-btn"
>
<mat-icon>refresh</mat-icon> <mat-icon>refresh</mat-icon>
Retake Quiz Retake Quiz
</button> </button>
<button <button mat-raised-button (click)="goToDashboard()" class="action-btn">
mat-raised-button
(click)="goToDashboard()"
class="action-btn"
>
<mat-icon>dashboard</mat-icon> <mat-icon>dashboard</mat-icon>
Dashboard Dashboard
</button> </button>

View File

@@ -12,7 +12,7 @@ import { MatPaginatorModule, PageEvent } from '@angular/material/paginator';
import { Subject, takeUntil } from 'rxjs'; import { Subject, takeUntil } from 'rxjs';
import { QuizService } from '../../../core/services/quiz.service'; import { QuizService } from '../../../core/services/quiz.service';
import { StorageService } from '../../../core/services/storage.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({ @Component({
selector: 'app-quiz-review', selector: 'app-quiz-review',
@@ -49,8 +49,15 @@ export class QuizReviewComponent implements OnInit, OnDestroy {
// Filter // Filter
readonly filterType = signal<'all' | 'correct' | 'incorrect'>('all'); readonly filterType = signal<'all' | 'correct' | 'incorrect'>('all');
// Computed values // Computed values - handle QuizReviewResult structure
readonly allQuestions = computed(() => this.results()?.questions ?? []); 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(() => { readonly filteredQuestions = computed(() => {
const questions = this.allQuestions(); const questions = this.allQuestions();
@@ -80,6 +87,22 @@ export class QuizReviewComponent implements OnInit, OnDestroy {
this.allQuestions().filter(q => !q.isCorrect).length this.allQuestions().filter(q => !q.isCorrect).length
); );
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(() => readonly isAuthenticated = computed(() =>
this.storageService.isAuthenticated() this.storageService.isAuthenticated()
); );