313 lines
8.4 KiB
TypeScript
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();
|
|
}
|
|
}
|