Files
Tasks/frontend/src/app/features/history/quiz-history.component.ts
2025-12-19 21:18:47 +02:00

313 lines
8.4 KiB
TypeScript

import { Component, OnInit, inject, signal, computed } from '@angular/core';
import { CommonModule } from '@angular/common';
import { Router, ActivatedRoute, RouterLink } from '@angular/router';
import { MatCardModule } from '@angular/material/card';
import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { MatTableModule } from '@angular/material/table';
import { MatPaginatorModule, PageEvent } from '@angular/material/paginator';
import { MatSelectModule } from '@angular/material/select';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatTooltipModule } from '@angular/material/tooltip';
import { MatChipsModule } from '@angular/material/chips';
import { UserService } from '../../core/services/user.service';
import { AuthService } from '../../core/services/auth.service';
import { CategoryService } from '../../core/services/category.service';
import { QuizHistoryResponse, PaginationInfo } from '../../core/models/dashboard.model';
import { QuizSession, QuizSessionHistory } from '../../core/models/quiz.model';
import { Category } from '../../core/models/category.model';
@Component({
selector: 'app-quiz-history',
standalone: true,
imports: [
CommonModule,
RouterLink,
MatCardModule,
MatButtonModule,
MatIconModule,
MatProgressSpinnerModule,
MatTableModule,
MatPaginatorModule,
MatSelectModule,
MatFormFieldModule,
MatTooltipModule,
MatChipsModule
],
templateUrl: './quiz-history.component.html',
styleUrls: ['./quiz-history.component.scss']
})
export class QuizHistoryComponent implements OnInit {
private userService = inject(UserService);
private authService = inject(AuthService);
private categoryService = inject(CategoryService);
private router = inject(Router);
private route = inject(ActivatedRoute);
// Signals
isLoading = signal<boolean>(true);
history = signal<QuizSessionHistory[]>([]);
pagination = signal<PaginationInfo | null>(null);
categories = signal<Category[]>([]);
error = signal<string | null>(null);
// Filter and sort state
currentPage = signal<number>(1);
pageSize = signal<number>(10);
selectedCategory = signal<string | null>(null);
sortBy = signal<'date' | 'score'>('date');
// Table columns
displayedColumns: string[] = ['date', 'category', 'score', 'time', 'status', 'actions'];
// Computed values
isEmpty = computed(() => this.history().length === 0 && !this.isLoading());
totalItems = computed(() => this.pagination()?.totalItems || 0);
ngOnInit(): void {
this.loadCategories();
this.loadHistoryFromRoute();
}
/**
* Load categories for filter
*/
loadCategories(): void {
this.categoryService.getCategories().subscribe({
next: (response: any) => {
this.categories.set(response.categories || []);
},
error: (err) => {
console.error('Error loading categories:', err);
}
});
}
/**
* Load history based on route query params
*/
loadHistoryFromRoute(): void {
this.route.queryParams.subscribe(params => {
const page = params['page'] ? parseInt(params['page'], 10) : 1;
const limit = params['limit'] ? parseInt(params['limit'], 10) : 10;
const category = params['category'] || null;
const sortBy = params['sortBy'] || 'date';
this.currentPage.set(page);
this.pageSize.set(limit);
this.selectedCategory.set(category);
this.sortBy.set(sortBy);
this.loadHistory();
});
}
/**
* Load quiz history
*/
loadHistory(): void {
const state: any = (this.authService as any).authState();
const user = state?.user;
if (!user || !user.id) {
this.router.navigate(['/login']);
return;
}
this.isLoading.set(true);
this.error.set(null);
(this.userService as any).getHistory(
user.id,
this.currentPage(),
this.pageSize(),
this.selectedCategory() || undefined,
this.sortBy()
).subscribe({
next: (response: QuizHistoryResponse) => {
this.history.set(response.data.sessions || []);
this.pagination.set(response.data.pagination);
this.isLoading.set(false);
},
error: (err: any) => {
console.error('History error:', err);
this.error.set('Failed to load quiz history');
this.isLoading.set(false);
}
});
}
/**
* Handle page change
*/
onPageChange(event: PageEvent): void {
this.currentPage.set(event.pageIndex + 1);
this.pageSize.set(event.pageSize);
this.updateUrlAndLoad();
}
/**
* Handle category filter change
*/
onCategoryChange(categoryId: string | null): void {
this.selectedCategory.set(categoryId);
this.currentPage.set(1); // Reset to first page
this.updateUrlAndLoad();
}
/**
* Handle sort change
*/
onSortChange(sortBy: 'date' | 'score'): void {
this.sortBy.set(sortBy);
this.currentPage.set(1); // Reset to first page
this.updateUrlAndLoad();
}
/**
* Update URL with query params and reload data
*/
updateUrlAndLoad(): void {
const queryParams: any = {
page: this.currentPage(),
limit: this.pageSize(),
sortBy: this.sortBy()
};
if (this.selectedCategory()) {
queryParams.category = this.selectedCategory();
}
this.router.navigate([], {
relativeTo: this.route,
queryParams,
queryParamsHandling: 'merge'
});
}
/**
* View quiz results
*/
viewResults(sessionId: string | undefined): void {
if (sessionId) {
this.router.navigate(['/quiz', sessionId, 'results']);
}
}
/**
* Review quiz
*/
reviewQuiz(sessionId: string | undefined): void {
if (sessionId) {
this.router.navigate(['/quiz', sessionId, 'review']);
}
}
/**
* Format date
*/
formatDate(dateString: string | undefined): string {
if (!dateString) return 'N/A';
const date = new Date(dateString);
return date.toLocaleDateString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
}
/**
* Format duration
*/
formatDuration(seconds: number | undefined): string {
if (!seconds) return '0s';
const minutes = Math.floor(seconds / 60);
const secs = seconds % 60;
if (minutes === 0) {
return `${secs}s`;
}
return `${minutes}m ${secs}s`;
}
/**
* Get score color
*/
getScoreColor(score: number, total: number): string {
const percentage = (score / total) * 100;
if (percentage >= 80) return 'success';
if (percentage >= 60) return 'warning';
return 'error';
}
/**
* Get status badge class
*/
getStatusClass(status: string): string {
switch (status) {
case 'completed':
return 'status-completed';
case 'in_progress':
return 'status-in-progress';
case 'abandoned':
return 'status-abandoned';
default:
return '';
}
}
/**
* Export to CSV
*/
exportToCSV(): void {
if (this.history().length === 0) {
return;
}
// Create CSV header
const headers = ['Date', 'Category', 'Score', 'Total Questions', 'Percentage', 'Time Spent', 'Status'];
const csvRows = [headers.join(',')];
// Add data rows
this.history().forEach(session => {
const percentage = ((session.score.earned / session.questions.total) * 100).toFixed(2);
const row = [
this.formatDate(session.completedAt || session.startedAt),
session.category?.name || 'Unknown',
session.score.earned.toString(),
session.questions.total.toString(),
`${percentage}%`,
this.formatDuration(session.time.spent),
session.status
];
csvRows.push(row.join(','));
});
// Create blob and download
const csvContent = csvRows.join('\n');
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
const link = document.createElement('a');
const url = URL.createObjectURL(blob);
link.setAttribute('href', url);
link.setAttribute('download', `quiz-history-${new Date().toISOString().split('T')[0]}.csv`);
link.style.visibility = 'hidden';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
/**
* Refresh history
*/
refresh(): void {
this.loadHistory();
}
}