first commit
This commit is contained in:
@@ -0,0 +1,226 @@
|
||||
<div class="admin-questions-container">
|
||||
<!-- Header -->
|
||||
<div class="page-header">
|
||||
<div class="header-content">
|
||||
<div class="title-section">
|
||||
<mat-icon class="header-icon">quiz</mat-icon>
|
||||
<div>
|
||||
<h1>Question Management</h1>
|
||||
<p class="subtitle">Create, edit, and manage quiz questions</p>
|
||||
</div>
|
||||
</div>
|
||||
<button mat-raised-button color="primary" (click)="createQuestion()">
|
||||
<mat-icon>add</mat-icon>
|
||||
Create Question
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Filters Card -->
|
||||
<mat-card class="filters-card">
|
||||
<mat-card-content>
|
||||
<form [formGroup]="filterForm" class="filters-form">
|
||||
<!-- Search -->
|
||||
<mat-form-field appearance="outline" class="search-field">
|
||||
<mat-label>Search Questions</mat-label>
|
||||
<input matInput formControlName="search" placeholder="Search by question text...">
|
||||
<mat-icon matPrefix>search</mat-icon>
|
||||
</mat-form-field>
|
||||
|
||||
<!-- Category Filter -->
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Category</mat-label>
|
||||
<mat-select formControlName="category">
|
||||
<mat-option value="all">All Categories</mat-option>
|
||||
@for (category of categories(); track category.id) {
|
||||
<mat-option [value]="category.id">{{ category.name }}</mat-option>
|
||||
}
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
<!-- Difficulty Filter -->
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Difficulty</mat-label>
|
||||
<mat-select formControlName="difficulty">
|
||||
<mat-option value="all">All Difficulties</mat-option>
|
||||
<mat-option value="easy">Easy</mat-option>
|
||||
<mat-option value="medium">Medium</mat-option>
|
||||
<mat-option value="hard">Hard</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
<!-- Type Filter -->
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Type</mat-label>
|
||||
<mat-select formControlName="type">
|
||||
<mat-option value="all">All Types</mat-option>
|
||||
<mat-option value="multiple">Multiple Choice</mat-option>
|
||||
<mat-option value="trueFalse">True/False</mat-option>
|
||||
<mat-option value="written">Written</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
<!-- Sort By -->
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Sort By</mat-label>
|
||||
<mat-select formControlName="sortBy">
|
||||
<mat-option value="createdAt">Date Created</mat-option>
|
||||
<mat-option value="questionText">Question Text</mat-option>
|
||||
<mat-option value="difficulty">Difficulty</mat-option>
|
||||
<mat-option value="points">Points</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
<!-- Sort Order -->
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Order</mat-label>
|
||||
<mat-select formControlName="sortOrder">
|
||||
<mat-option value="asc">Ascending</mat-option>
|
||||
<mat-option value="desc">Descending</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</form>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
|
||||
<!-- Results Card -->
|
||||
<mat-card class="results-card">
|
||||
<!-- Loading State -->
|
||||
@if (isLoading()) {
|
||||
<div class="loading-container">
|
||||
<mat-spinner diameter="50"></mat-spinner>
|
||||
<p>Loading questions...</p>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Error State -->
|
||||
@else if (error()) {
|
||||
<div class="error-container">
|
||||
<mat-icon color="warn">error</mat-icon>
|
||||
<p>{{ error() }}</p>
|
||||
<button mat-raised-button color="primary" (click)="loadQuestions()">
|
||||
<mat-icon>refresh</mat-icon>
|
||||
Retry
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Empty State -->
|
||||
@else if (questions().length === 0) {
|
||||
<div class="empty-container">
|
||||
<mat-icon>quiz</mat-icon>
|
||||
<h3>No Questions Found</h3>
|
||||
<p>No questions match your current filters. Try adjusting your search criteria.</p>
|
||||
<button mat-raised-button color="primary" (click)="createQuestion()">
|
||||
<mat-icon>add</mat-icon>
|
||||
Create First Question
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Questions Table (Desktop) -->
|
||||
@else {
|
||||
<div class="table-container">
|
||||
<table mat-table [dataSource]="questions()" class="questions-table">
|
||||
<!-- Question Text Column -->
|
||||
<ng-container matColumnDef="questionText">
|
||||
<th mat-header-cell *matHeaderCellDef>Question</th>
|
||||
<td mat-cell *matCellDef="let question">
|
||||
<div class="question-text-cell">
|
||||
{{ question.questionText.substring(0, 100) }}{{ question.questionText.length > 100 ? '...' : '' }}
|
||||
</div>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Type Column -->
|
||||
<ng-container matColumnDef="type">
|
||||
<th mat-header-cell *matHeaderCellDef>Type</th>
|
||||
<td mat-cell *matCellDef="let question">
|
||||
<mat-chip>
|
||||
@if (question.questionType === 'multiple') {
|
||||
<mat-icon>radio_button_checked</mat-icon>
|
||||
<span class="px-5"> MCQ</span>
|
||||
} @else if (question.questionType === 'trueFalse') {
|
||||
<mat-icon>check_circle</mat-icon>
|
||||
<span> T/F</span>
|
||||
} @else {
|
||||
<mat-icon>edit_note</mat-icon>
|
||||
<span> Written</span>
|
||||
}
|
||||
</mat-chip>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Category Column -->
|
||||
<ng-container matColumnDef="category">
|
||||
<th mat-header-cell *matHeaderCellDef>Category</th>
|
||||
<td mat-cell *matCellDef="let question">
|
||||
{{ getCategoryName(question) }}
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Difficulty Column -->
|
||||
<ng-container matColumnDef="difficulty">
|
||||
<th mat-header-cell *matHeaderCellDef>Difficulty</th>
|
||||
<td mat-cell *matCellDef="let question">
|
||||
<mat-chip [color]="getDifficultyColor(question.difficulty)">
|
||||
{{ question.difficulty }}
|
||||
</mat-chip>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Points Column -->
|
||||
<ng-container matColumnDef="points">
|
||||
<th mat-header-cell *matHeaderCellDef>Points</th>
|
||||
<td mat-cell *matCellDef="let question">
|
||||
<span class="points-badge">{{ question.points }}</span>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Status Column -->
|
||||
<ng-container matColumnDef="status">
|
||||
<th mat-header-cell *matHeaderCellDef>Status</th>
|
||||
<td mat-cell *matCellDef="let question">
|
||||
<mat-chip [color]="getStatusColor(question.isActive)">
|
||||
{{ question.isActive ? 'Active' : 'Inactive' }}
|
||||
</mat-chip>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Actions Column -->
|
||||
<ng-container matColumnDef="actions">
|
||||
<th mat-header-cell *matHeaderCellDef>Actions</th>
|
||||
<td mat-cell *matCellDef="let question">
|
||||
<div class="action-buttons">
|
||||
<button mat-icon-button color="primary"
|
||||
(click)="editQuestion(question)"
|
||||
matTooltip="Edit Question">
|
||||
<mat-icon>edit</mat-icon>
|
||||
</button>
|
||||
<button mat-icon-button color="warn"
|
||||
(click)="deleteQuestion(question)"
|
||||
matTooltip="Delete Question">
|
||||
<mat-icon>delete</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
||||
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
<app-pagination
|
||||
[state]="paginationState()"
|
||||
[pageNumbers]="pageNumbers()"
|
||||
[pageSizeOptions]="[10, 25, 50, 100]"
|
||||
[showFirstLast]="true"
|
||||
[itemLabel]="'questions'"
|
||||
(pageChange)="goToPage($event)"
|
||||
(pageSizeChange)="onPageSizeChange($event)">
|
||||
</app-pagination>
|
||||
}
|
||||
</mat-card>
|
||||
</div>
|
||||
@@ -0,0 +1,341 @@
|
||||
.admin-questions-container {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
padding: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
// Page Header
|
||||
.page-header {
|
||||
margin-bottom: 2rem;
|
||||
|
||||
.header-content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
}
|
||||
|
||||
.title-section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
|
||||
.header-icon {
|
||||
font-size: 2.5rem;
|
||||
width: 2.5rem;
|
||||
height: 2.5rem;
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 0;
|
||||
font-size: 2rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
margin: 0.25rem 0 0 0;
|
||||
font-size: 0.95rem;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
}
|
||||
|
||||
button {
|
||||
height: 42px;
|
||||
padding: 0 1.5rem;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
mat-icon {
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Filters Card
|
||||
.filters-card {
|
||||
margin-bottom: 1.5rem;
|
||||
|
||||
.filters-form {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 1rem;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.search-field {
|
||||
grid-column: span 2;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
grid-column: span 1;
|
||||
}
|
||||
}
|
||||
|
||||
mat-form-field {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Results Card
|
||||
.results-card {
|
||||
min-height: 400px;
|
||||
}
|
||||
|
||||
// Loading State
|
||||
.loading-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 4rem 2rem;
|
||||
gap: 1.5rem;
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
font-size: 1rem;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
}
|
||||
|
||||
// Error State
|
||||
.error-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 4rem 2rem;
|
||||
gap: 1rem;
|
||||
|
||||
mat-icon {
|
||||
font-size: 4rem;
|
||||
width: 4rem;
|
||||
height: 4rem;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
font-size: 1rem;
|
||||
color: var(--text-secondary);
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
// Empty State
|
||||
.empty-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 4rem 2rem;
|
||||
gap: 1rem;
|
||||
|
||||
mat-icon {
|
||||
font-size: 5rem;
|
||||
width: 5rem;
|
||||
height: 5rem;
|
||||
color: var(--text-disabled);
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin: 0;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 500;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
font-size: 1rem;
|
||||
color: var(--text-secondary);
|
||||
text-align: center;
|
||||
max-width: 500px;
|
||||
}
|
||||
|
||||
button {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
// Questions Table
|
||||
.table-container {
|
||||
overflow-x: auto;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
margin: -1rem;
|
||||
padding: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.questions-table {
|
||||
width: 100%;
|
||||
background: transparent;
|
||||
|
||||
th {
|
||||
font-weight: 600;
|
||||
font-size: 0.875rem;
|
||||
text-transform: uppercase;
|
||||
color: var(--text-secondary);
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
td {
|
||||
padding: 1rem;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
tr {
|
||||
border-bottom: 1px solid var(--divider-color);
|
||||
|
||||
&:hover {
|
||||
background-color: var(--hover-background);
|
||||
}
|
||||
}
|
||||
|
||||
.question-text-cell {
|
||||
max-width: 400px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
mat-chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
font-size: 0.875rem;
|
||||
|
||||
mat-icon {
|
||||
font-size: 1rem;
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.points-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 2rem;
|
||||
height: 1.5rem;
|
||||
padding: 0 0.5rem;
|
||||
background-color: var(--primary-color);
|
||||
color: white;
|
||||
border-radius: 12px;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
gap: 0.25rem;
|
||||
|
||||
button {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
|
||||
mat-icon {
|
||||
font-size: 1.25rem;
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Pagination
|
||||
.pagination-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 1rem;
|
||||
border-top: 1px solid var(--divider-color);
|
||||
margin-top: 1rem;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.pagination-info {
|
||||
font-size: 0.875rem;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.pagination-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
button {
|
||||
min-width: 40px;
|
||||
height: 40px;
|
||||
|
||||
&.active {
|
||||
background-color: var(--primary-color);
|
||||
color: white;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.4;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
.ellipsis {
|
||||
padding: 0 0.5rem;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Dark Mode Support
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.admin-questions-container {
|
||||
--text-primary: #e0e0e0;
|
||||
--text-secondary: #a0a0a0;
|
||||
--text-disabled: #606060;
|
||||
--divider-color: #404040;
|
||||
--hover-background: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.questions-table {
|
||||
tr:hover {
|
||||
background-color: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Light Mode Support
|
||||
@media (prefers-color-scheme: light) {
|
||||
.admin-questions-container {
|
||||
--text-primary: #212121;
|
||||
--text-secondary: #757575;
|
||||
--text-disabled: #bdbdbd;
|
||||
--divider-color: #e0e0e0;
|
||||
--hover-background: rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.questions-table {
|
||||
tr:hover {
|
||||
background-color: rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,327 @@
|
||||
import { Component, OnInit, inject, signal, computed } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormBuilder, FormGroup, ReactiveFormsModule } from '@angular/forms';
|
||||
import { Router } from '@angular/router';
|
||||
import { debounceTime, distinctUntilChanged, finalize } from 'rxjs/operators';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatTableModule } from '@angular/material/table';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatSelectModule } from '@angular/material/select';
|
||||
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
||||
import { MatTooltipModule } from '@angular/material/tooltip';
|
||||
import { MatChipsModule } from '@angular/material/chips';
|
||||
import { MatMenuModule } from '@angular/material/menu';
|
||||
import { MatDialog, MatDialogModule } from '@angular/material/dialog';
|
||||
import { AdminService } from '../../../core/services/admin.service';
|
||||
import { CategoryService } from '../../../core/services/category.service';
|
||||
import { Question } from '../../../core/models/question.model';
|
||||
import { Category } from '../../../core/models/category.model';
|
||||
import { DeleteConfirmDialogComponent } from '../delete-confirm-dialog/delete-confirm-dialog.component';
|
||||
import { PaginationService, PaginationState } from '../../../core/services/pagination.service';
|
||||
import { PaginationComponent } from '../../../shared/components/pagination/pagination.component';
|
||||
|
||||
/**
|
||||
* AdminQuestionsComponent
|
||||
*
|
||||
* Displays and manages all questions with pagination, filtering, and sorting.
|
||||
*
|
||||
* Features:
|
||||
* - Question table with key columns
|
||||
* - Search by question text
|
||||
* - Filter by category, difficulty, and type
|
||||
* - Sort by various fields
|
||||
* - Pagination controls
|
||||
* - Action buttons (Edit, Delete, View)
|
||||
* - Delete confirmation dialog
|
||||
* - Responsive design (cards on mobile)
|
||||
* - Loading and error states
|
||||
*/
|
||||
@Component({
|
||||
selector: 'app-admin-questions',
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
ReactiveFormsModule,
|
||||
MatCardModule,
|
||||
MatButtonModule,
|
||||
MatIconModule,
|
||||
MatTableModule,
|
||||
MatInputModule,
|
||||
MatFormFieldModule,
|
||||
MatSelectModule,
|
||||
MatProgressSpinnerModule,
|
||||
MatTooltipModule,
|
||||
MatChipsModule,
|
||||
MatMenuModule,
|
||||
MatDialogModule,
|
||||
PaginationComponent
|
||||
],
|
||||
templateUrl: './admin-questions.component.html',
|
||||
styleUrl: './admin-questions.component.scss'
|
||||
})
|
||||
export class AdminQuestionsComponent implements OnInit {
|
||||
private readonly adminService = inject(AdminService);
|
||||
private readonly categoryService = inject(CategoryService);
|
||||
private readonly router = inject(Router);
|
||||
private readonly fb = inject(FormBuilder);
|
||||
private readonly dialog = inject(MatDialog);
|
||||
private readonly paginationService = inject(PaginationService);
|
||||
|
||||
// State signals
|
||||
readonly questions = signal<Question[]>([]);
|
||||
readonly isLoading = signal<boolean>(false);
|
||||
readonly error = signal<string | null>(null);
|
||||
readonly categories = this.categoryService.categories;
|
||||
|
||||
// Pagination
|
||||
readonly currentPage = signal<number>(1);
|
||||
readonly pageSize = signal<number>(10);
|
||||
readonly totalQuestions = signal<number>(0);
|
||||
readonly totalPages = computed(() => Math.ceil(this.totalQuestions() / this.pageSize()));
|
||||
|
||||
// Computed pagination state for reusable component
|
||||
readonly paginationState = computed<PaginationState>(() => {
|
||||
return this.paginationService.calculatePaginationState({
|
||||
currentPage: this.currentPage(),
|
||||
pageSize: this.pageSize(),
|
||||
totalItems: this.totalQuestions()
|
||||
});
|
||||
});
|
||||
|
||||
// Computed page numbers
|
||||
readonly pageNumbers = computed(() => {
|
||||
return this.paginationService.calculatePageNumbers(
|
||||
this.currentPage(),
|
||||
this.totalPages(),
|
||||
5
|
||||
);
|
||||
});
|
||||
|
||||
// Table configuration
|
||||
displayedColumns: string[] = ['questionText', 'type', 'category', 'difficulty', 'points', 'status', 'actions'];
|
||||
|
||||
// Filter form
|
||||
filterForm!: FormGroup;
|
||||
|
||||
// Expose Math for template
|
||||
Math = Math;
|
||||
|
||||
ngOnInit(): void {
|
||||
this.initializeFilterForm();
|
||||
this.setupSearchDebounce();
|
||||
this.loadCategories();
|
||||
this.loadQuestions();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize filter form
|
||||
*/
|
||||
private initializeFilterForm(): void {
|
||||
this.filterForm = this.fb.group({
|
||||
search: [''],
|
||||
category: ['all'],
|
||||
difficulty: ['all'],
|
||||
type: ['all'],
|
||||
sortBy: ['createdAt'],
|
||||
sortOrder: ['desc']
|
||||
});
|
||||
|
||||
// Subscribe to filter changes (except search which is debounced)
|
||||
this.filterForm.valueChanges
|
||||
.pipe(
|
||||
debounceTime(300),
|
||||
distinctUntilChanged()
|
||||
)
|
||||
.subscribe(() => {
|
||||
this.currentPage.set(1);
|
||||
this.loadQuestions();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup search field debounce
|
||||
*/
|
||||
private setupSearchDebounce(): void {
|
||||
this.filterForm.get('search')?.valueChanges
|
||||
.pipe(
|
||||
debounceTime(500),
|
||||
distinctUntilChanged()
|
||||
)
|
||||
.subscribe(() => {
|
||||
this.currentPage.set(1);
|
||||
this.loadQuestions();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Load categories for filter dropdown
|
||||
*/
|
||||
private loadCategories(): void {
|
||||
if (this.categories().length === 0) {
|
||||
this.categoryService.getCategories().subscribe();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load questions with current filters
|
||||
*/
|
||||
loadQuestions(): void {
|
||||
this.isLoading.set(true);
|
||||
this.error.set(null);
|
||||
|
||||
const filters = this.filterForm.value;
|
||||
const params: any = {
|
||||
page: this.currentPage(),
|
||||
limit: this.pageSize(),
|
||||
search: filters.search || undefined,
|
||||
category: filters.category !== 'all' ? filters.category : undefined,
|
||||
difficulty: filters.difficulty !== 'all' ? filters.difficulty : undefined,
|
||||
sortBy: filters.sortBy,
|
||||
order: filters.sortOrder
|
||||
};
|
||||
|
||||
// Remove undefined values
|
||||
Object.keys(params).forEach(key => params[key] === undefined && delete params[key]);
|
||||
|
||||
this.adminService.getAllQuestions(params)
|
||||
.pipe(finalize(() => this.isLoading.set(false)))
|
||||
.subscribe({
|
||||
next: (response) => {
|
||||
this.questions.set(response.data);
|
||||
this.totalQuestions.set(response.total);
|
||||
this.currentPage.set(response.page);
|
||||
},
|
||||
error: (error) => {
|
||||
this.error.set(error.message || 'Failed to load questions');
|
||||
this.questions.set([]);
|
||||
this.totalQuestions.set(0);
|
||||
console.error('Load questions error:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigate to create question page
|
||||
*/
|
||||
createQuestion(): void {
|
||||
this.router.navigate(['/admin/questions/new']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigate to edit question page
|
||||
*/
|
||||
editQuestion(question: Question): void {
|
||||
this.router.navigate(['/admin/questions', question.id, 'edit']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Open delete confirmation dialog
|
||||
*/
|
||||
deleteQuestion(question: Question): void {
|
||||
const dialogRef = this.dialog.open(DeleteConfirmDialogComponent, {
|
||||
width: '500px',
|
||||
data: {
|
||||
title: 'Delete Question',
|
||||
message: 'Are you sure you want to delete this question? This action cannot be undone.',
|
||||
itemName: question.questionText.substring(0, 100) + (question.questionText.length > 100 ? '...' : ''),
|
||||
confirmText: 'Delete',
|
||||
cancelText: 'Cancel'
|
||||
}
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe(confirmed => {
|
||||
if (confirmed && question.id) {
|
||||
this.performDelete(question.id);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform delete operation
|
||||
*/
|
||||
private performDelete(id: string): void {
|
||||
this.isLoading.set(true);
|
||||
|
||||
this.adminService.deleteQuestion(id)
|
||||
.pipe(finalize(() => this.isLoading.set(false)))
|
||||
.subscribe({
|
||||
next: () => {
|
||||
// Reload questions after deletion
|
||||
this.loadQuestions();
|
||||
},
|
||||
error: (error) => {
|
||||
this.error.set('Failed to delete question');
|
||||
console.error('Delete error:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get category name from question
|
||||
* The API returns a nested category object with the question
|
||||
*/
|
||||
getCategoryName(question: Question): string {
|
||||
// First try to get from nested category object (API response)
|
||||
if (question.category?.name) {
|
||||
return question.category.name;
|
||||
}
|
||||
|
||||
// Fallback: try to find by categoryId in loaded categories
|
||||
if (question.categoryId) {
|
||||
const category = this.categories().find(
|
||||
c => c.id === question.categoryId || c.id === question.categoryId.toString()
|
||||
);
|
||||
if (category) {
|
||||
return category.name;
|
||||
}
|
||||
}
|
||||
|
||||
// Last fallback: use categoryName property if available
|
||||
return question.categoryName || 'Unknown';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get status chip color
|
||||
*/
|
||||
getStatusColor(isActive: boolean): string {
|
||||
return isActive ? 'primary' : 'warn';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get difficulty chip color
|
||||
*/
|
||||
getDifficultyColor(difficulty: string): string {
|
||||
switch (difficulty.toLowerCase()) {
|
||||
case 'easy':
|
||||
return 'primary';
|
||||
case 'medium':
|
||||
return 'accent';
|
||||
case 'hard':
|
||||
return 'warn';
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Go to specific page
|
||||
*/
|
||||
goToPage(page: number): void {
|
||||
if (page >= 1 && page <= this.totalPages()) {
|
||||
this.currentPage.set(page);
|
||||
this.loadQuestions();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle page size change
|
||||
*/
|
||||
onPageSizeChange(pageSize: number): void {
|
||||
this.pageSize.set(pageSize);
|
||||
this.currentPage.set(1); // Reset to first page
|
||||
this.loadQuestions();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user