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(true); history = signal([]); pagination = signal(null); categories = signal([]); error = signal(null); // Filter and sort state currentPage = signal(1); pageSize = signal(10); selectedCategory = signal(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(); } }