add changes
This commit is contained in:
815
frontend/src/app/core/services/admin.service.ts
Normal file
815
frontend/src/app/core/services/admin.service.ts
Normal file
@@ -0,0 +1,815 @@
|
||||
import { Injectable, signal, computed, inject } from '@angular/core';
|
||||
import { HttpClient, HttpErrorResponse } from '@angular/common/http';
|
||||
import { Observable, throwError } from 'rxjs';
|
||||
import { catchError, tap, map } from 'rxjs/operators';
|
||||
import { Router } from '@angular/router';
|
||||
import { environment } from '../../../environments/environment';
|
||||
import {
|
||||
AdminStatistics,
|
||||
AdminStatisticsResponse,
|
||||
AdminCacheEntry,
|
||||
DateRangeFilter,
|
||||
GuestAnalytics,
|
||||
GuestAnalyticsResponse,
|
||||
GuestSettings,
|
||||
GuestSettingsResponse,
|
||||
AdminUser,
|
||||
AdminUserListResponse,
|
||||
UserListParams,
|
||||
AdminUserDetail,
|
||||
AdminUserDetailResponse
|
||||
} from '../models/admin.model';
|
||||
import { Question, QuestionFormData } from '../models/question.model';
|
||||
import { ToastService } from './toast.service';
|
||||
|
||||
/**
|
||||
* AdminService
|
||||
*
|
||||
* Handles all admin-related API operations including:
|
||||
* - System-wide statistics
|
||||
* - User analytics
|
||||
* - Guest analytics
|
||||
* - User management
|
||||
* - Question management
|
||||
* - Settings management
|
||||
*
|
||||
* Features:
|
||||
* - Signal-based state management
|
||||
* - 5-minute caching for statistics
|
||||
* - Automatic authorization error handling
|
||||
* - Admin role verification
|
||||
*/
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class AdminService {
|
||||
private readonly http = inject(HttpClient);
|
||||
private readonly router = inject(Router);
|
||||
private readonly toastService = inject(ToastService);
|
||||
private readonly apiUrl = `${environment.apiUrl}/admin`;
|
||||
|
||||
// Cache storage for admin data
|
||||
private readonly cache = new Map<string, AdminCacheEntry<any>>();
|
||||
private readonly STATS_CACHE_TTL = 5 * 60 * 1000; // 5 minutes
|
||||
private readonly ANALYTICS_CACHE_TTL = 10 * 60 * 1000; // 10 minutes
|
||||
|
||||
// State signals - Statistics
|
||||
readonly adminStatsState = signal<AdminStatistics | null>(null);
|
||||
readonly isLoadingStats = signal<boolean>(false);
|
||||
readonly statsError = signal<string | null>(null);
|
||||
|
||||
// State signals - Guest Analytics
|
||||
readonly guestAnalyticsState = signal<GuestAnalytics | null>(null);
|
||||
readonly isLoadingAnalytics = signal<boolean>(false);
|
||||
readonly analyticsError = signal<string | null>(null);
|
||||
|
||||
// State signals - Guest Settings
|
||||
readonly guestSettingsState = signal<GuestSettings | null>(null);
|
||||
readonly isLoadingSettings = signal<boolean>(false);
|
||||
readonly settingsError = signal<string | null>(null);
|
||||
|
||||
// State signals - User Management
|
||||
readonly adminUsersState = signal<AdminUser[]>([]);
|
||||
readonly isLoadingUsers = signal<boolean>(false);
|
||||
readonly usersError = signal<string | null>(null);
|
||||
readonly usersPagination = signal<{
|
||||
currentPage: number;
|
||||
totalPages: number;
|
||||
totalUsers: number;
|
||||
limit: number;
|
||||
hasNext: boolean;
|
||||
hasPrev: boolean;
|
||||
} | null>(null);
|
||||
readonly currentUserFilters = signal<UserListParams>({});
|
||||
|
||||
// State signals - User Detail
|
||||
readonly selectedUserDetail = signal<AdminUserDetail | null>(null);
|
||||
readonly isLoadingUserDetail = signal<boolean>(false);
|
||||
readonly userDetailError = signal<string | null>(null);
|
||||
|
||||
// Date range filter
|
||||
readonly dateRangeFilter = signal<DateRangeFilter>({
|
||||
startDate: null,
|
||||
endDate: null
|
||||
});
|
||||
|
||||
// Computed signals - Statistics
|
||||
readonly hasStats = computed(() => this.adminStatsState() !== null);
|
||||
readonly totalUsers = computed(() => this.adminStatsState()?.totalUsers ?? 0);
|
||||
readonly activeUsers = computed(() => this.adminStatsState()?.activeUsers ?? 0);
|
||||
readonly totalQuizSessions = computed(() => this.adminStatsState()?.totalQuizSessions ?? 0);
|
||||
readonly totalQuestions = computed(() => this.adminStatsState()?.totalQuestions ?? 0);
|
||||
readonly averageScore = computed(() => this.adminStatsState()?.averageQuizScore ?? 0);
|
||||
|
||||
// Computed signals - Guest Analytics
|
||||
readonly hasAnalytics = computed(() => this.guestAnalyticsState() !== null);
|
||||
readonly totalGuestSessions = computed(() => this.guestAnalyticsState()?.totalGuestSessions ?? 0);
|
||||
readonly activeGuestSessions = computed(() => this.guestAnalyticsState()?.activeGuestSessions ?? 0);
|
||||
readonly conversionRate = computed(() => this.guestAnalyticsState()?.conversionRate ?? 0);
|
||||
readonly avgQuizzesPerGuest = computed(() => this.guestAnalyticsState()?.averageQuizzesPerGuest ?? 0);
|
||||
|
||||
// Computed signals - Guest Settings
|
||||
readonly hasSettings = computed(() => this.guestSettingsState() !== null);
|
||||
readonly isGuestAccessEnabled = computed(() => this.guestSettingsState()?.guestAccessEnabled ?? false);
|
||||
readonly maxQuizzesPerDay = computed(() => this.guestSettingsState()?.maxQuizzesPerDay ?? 0);
|
||||
readonly maxQuestionsPerQuiz = computed(() => this.guestSettingsState()?.maxQuestionsPerQuiz ?? 0);
|
||||
|
||||
// Computed signals - User Management
|
||||
readonly hasUsers = computed(() => this.adminUsersState().length > 0);
|
||||
readonly totalUsersCount = computed(() => this.usersPagination()?.totalUsers ?? 0);
|
||||
readonly currentPage = computed(() => this.usersPagination()?.currentPage ?? 1);
|
||||
readonly totalPages = computed(() => this.usersPagination()?.totalPages ?? 1);
|
||||
|
||||
// Computed signals - User Detail
|
||||
readonly hasUserDetail = computed(() => this.selectedUserDetail() !== null);
|
||||
readonly userFullName = computed(() => {
|
||||
const user = this.selectedUserDetail();
|
||||
return user ? user.username : '';
|
||||
});
|
||||
readonly userTotalQuizzes = computed(() => this.selectedUserDetail()?.statistics.totalQuizzes ?? 0);
|
||||
readonly userAverageScore = computed(() => this.selectedUserDetail()?.statistics.averageScore ?? 0);
|
||||
readonly userAccuracy = computed(() => this.selectedUserDetail()?.statistics.accuracy ?? 0);
|
||||
|
||||
/**
|
||||
* Get system-wide statistics
|
||||
* Implements 5-minute caching
|
||||
*/
|
||||
getStatistics(forceRefresh: boolean = false): Observable<AdminStatistics> {
|
||||
const cacheKey = 'admin-statistics';
|
||||
|
||||
// Check cache first
|
||||
if (!forceRefresh) {
|
||||
const cached = this.getFromCache<AdminStatistics>(cacheKey);
|
||||
if (cached) {
|
||||
this.adminStatsState.set(cached);
|
||||
return new Observable(observer => {
|
||||
observer.next(cached);
|
||||
observer.complete();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
this.isLoadingStats.set(true);
|
||||
this.statsError.set(null);
|
||||
|
||||
return this.http.get<AdminStatisticsResponse>(`${this.apiUrl}/statistics`).pipe(
|
||||
map(response => response.data),
|
||||
tap(data => {
|
||||
this.adminStatsState.set(data);
|
||||
this.setCache(cacheKey, data);
|
||||
this.isLoadingStats.set(false);
|
||||
}),
|
||||
catchError(error => {
|
||||
this.isLoadingStats.set(false);
|
||||
return this.handleError(error, 'Failed to load statistics');
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get statistics with date range filter
|
||||
*/
|
||||
getStatisticsWithDateRange(startDate: Date, endDate: Date): Observable<AdminStatistics> {
|
||||
this.isLoadingStats.set(true);
|
||||
this.statsError.set(null);
|
||||
|
||||
const params = {
|
||||
startDate: startDate.toISOString(),
|
||||
endDate: endDate.toISOString()
|
||||
};
|
||||
|
||||
return this.http.get<AdminStatisticsResponse>(`${this.apiUrl}/statistics`, { params }).pipe(
|
||||
map(response => response.data),
|
||||
tap(data => {
|
||||
this.adminStatsState.set(data);
|
||||
this.isLoadingStats.set(false);
|
||||
this.dateRangeFilter.set({ startDate, endDate });
|
||||
}),
|
||||
catchError(error => {
|
||||
this.isLoadingStats.set(false);
|
||||
return this.handleError(error, 'Failed to load filtered statistics');
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear date range filter and reload all-time statistics
|
||||
*/
|
||||
clearDateFilter(): void {
|
||||
this.dateRangeFilter.set({ startDate: null, endDate: null });
|
||||
this.getStatistics(true).subscribe();
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh statistics (force cache invalidation)
|
||||
*/
|
||||
refreshStatistics(): Observable<AdminStatistics> {
|
||||
this.invalidateCache('admin-statistics');
|
||||
return this.getStatistics(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get guest user analytics
|
||||
* Implements 10-minute caching
|
||||
*/
|
||||
getGuestAnalytics(forceRefresh: boolean = false): Observable<GuestAnalytics> {
|
||||
const cacheKey = 'guest-analytics';
|
||||
|
||||
// Check cache first
|
||||
if (!forceRefresh) {
|
||||
const cached = this.getFromCache<GuestAnalytics>(cacheKey);
|
||||
if (cached) {
|
||||
this.guestAnalyticsState.set(cached);
|
||||
return new Observable(observer => {
|
||||
observer.next(cached);
|
||||
observer.complete();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
this.isLoadingAnalytics.set(true);
|
||||
this.analyticsError.set(null);
|
||||
|
||||
return this.http.get<GuestAnalyticsResponse>(`${this.apiUrl}/guest-analytics`).pipe(
|
||||
map(response => response.data),
|
||||
tap(data => {
|
||||
this.guestAnalyticsState.set(data);
|
||||
this.setCache(cacheKey, data, this.ANALYTICS_CACHE_TTL);
|
||||
this.isLoadingAnalytics.set(false);
|
||||
}),
|
||||
catchError(error => {
|
||||
this.isLoadingAnalytics.set(false);
|
||||
return this.handleError(error, 'Failed to load guest analytics');
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh guest analytics (force cache invalidation)
|
||||
*/
|
||||
refreshGuestAnalytics(): Observable<GuestAnalytics> {
|
||||
this.invalidateCache('guest-analytics');
|
||||
return this.getGuestAnalytics(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get data from cache if not expired
|
||||
*/
|
||||
private getFromCache<T>(key: string): T | null {
|
||||
const entry = this.cache.get(key);
|
||||
if (!entry) return null;
|
||||
|
||||
const now = Date.now();
|
||||
if (now > entry.expiresAt) {
|
||||
this.cache.delete(key);
|
||||
return null;
|
||||
}
|
||||
|
||||
return entry.data as T;
|
||||
}
|
||||
|
||||
/**
|
||||
* Store data in cache with TTL
|
||||
*/
|
||||
private setCache<T>(key: string, data: T, ttl: number = this.STATS_CACHE_TTL): void {
|
||||
const now = Date.now();
|
||||
const entry: AdminCacheEntry<T> = {
|
||||
data,
|
||||
timestamp: now,
|
||||
expiresAt: now + ttl
|
||||
};
|
||||
this.cache.set(key, entry);
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidate specific cache entry
|
||||
*/
|
||||
private invalidateCache(key: string): void {
|
||||
this.cache.delete(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all cache entries
|
||||
*/
|
||||
clearCache(): void {
|
||||
this.cache.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle HTTP errors with proper messaging
|
||||
*/
|
||||
private handleError(error: HttpErrorResponse, defaultMessage: string): Observable<never> {
|
||||
let errorMessage = defaultMessage;
|
||||
|
||||
if (error.status === 401) {
|
||||
errorMessage = 'Unauthorized. Please login again.';
|
||||
this.toastService.error(errorMessage);
|
||||
this.router.navigate(['/login']);
|
||||
} else if (error.status === 403) {
|
||||
errorMessage = 'Access denied. Admin privileges required.';
|
||||
this.toastService.error(errorMessage);
|
||||
this.router.navigate(['/dashboard']);
|
||||
} else if (error.status === 404) {
|
||||
errorMessage = 'Resource not found.';
|
||||
this.toastService.error(errorMessage);
|
||||
} else if (error.status === 500) {
|
||||
errorMessage = 'Server error. Please try again later.';
|
||||
this.toastService.error(errorMessage);
|
||||
} else if (error.error?.message) {
|
||||
errorMessage = error.error.message;
|
||||
this.toastService.error(errorMessage);
|
||||
} else {
|
||||
this.toastService.error(errorMessage);
|
||||
}
|
||||
|
||||
this.statsError.set(errorMessage);
|
||||
return throwError(() => new Error(errorMessage));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get guest access settings
|
||||
* Implements 10-minute caching
|
||||
*/
|
||||
getGuestSettings(forceRefresh: boolean = false): Observable<GuestSettings> {
|
||||
const cacheKey = 'guest-settings';
|
||||
|
||||
// Check cache first
|
||||
if (!forceRefresh) {
|
||||
const cached = this.getFromCache<GuestSettings>(cacheKey);
|
||||
if (cached) {
|
||||
this.guestSettingsState.set(cached);
|
||||
return new Observable(observer => {
|
||||
observer.next(cached);
|
||||
observer.complete();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
this.isLoadingSettings.set(true);
|
||||
this.settingsError.set(null);
|
||||
|
||||
return this.http.get<GuestSettingsResponse>(`${this.apiUrl}/guest-settings`).pipe(
|
||||
map(response => response.data),
|
||||
tap(data => {
|
||||
this.guestSettingsState.set(data);
|
||||
this.setCache(cacheKey, data, this.ANALYTICS_CACHE_TTL);
|
||||
this.isLoadingSettings.set(false);
|
||||
}),
|
||||
catchError(error => {
|
||||
this.isLoadingSettings.set(false);
|
||||
return this.handleSettingsError(error, 'Failed to load guest settings');
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh guest settings (force reload)
|
||||
*/
|
||||
refreshGuestSettings(): Observable<GuestSettings> {
|
||||
this.invalidateCache('guest-settings');
|
||||
return this.getGuestSettings(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update guest access settings
|
||||
* Invalidates cache and updates state
|
||||
*/
|
||||
updateGuestSettings(data: Partial<GuestSettings>): Observable<GuestSettings> {
|
||||
this.isLoadingSettings.set(true);
|
||||
this.settingsError.set(null);
|
||||
|
||||
return this.http.put<GuestSettingsResponse>(`${this.apiUrl}/guest-settings`, data).pipe(
|
||||
map(response => response.data),
|
||||
tap(updatedSettings => {
|
||||
this.guestSettingsState.set(updatedSettings);
|
||||
this.invalidateCache('guest-settings');
|
||||
this.isLoadingSettings.set(false);
|
||||
this.toastService.success('Guest settings updated successfully');
|
||||
}),
|
||||
catchError(error => {
|
||||
this.isLoadingSettings.set(false);
|
||||
return this.handleSettingsError(error, 'Failed to update guest settings');
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle HTTP errors for guest settings
|
||||
*/
|
||||
private handleSettingsError(error: HttpErrorResponse, defaultMessage: string): Observable<never> {
|
||||
let errorMessage = defaultMessage;
|
||||
|
||||
if (error.status === 401) {
|
||||
errorMessage = 'Unauthorized. Please login again.';
|
||||
this.toastService.error(errorMessage);
|
||||
this.router.navigate(['/login']);
|
||||
} else if (error.status === 403) {
|
||||
errorMessage = 'Access denied. Admin privileges required.';
|
||||
this.toastService.error(errorMessage);
|
||||
this.router.navigate(['/dashboard']);
|
||||
} else if (error.status === 404) {
|
||||
errorMessage = 'Settings not found.';
|
||||
this.toastService.error(errorMessage);
|
||||
} else if (error.error?.message) {
|
||||
errorMessage = error.error.message;
|
||||
this.toastService.error(errorMessage);
|
||||
} else {
|
||||
this.toastService.error(errorMessage);
|
||||
}
|
||||
|
||||
this.settingsError.set(errorMessage);
|
||||
return throwError(() => new Error(errorMessage));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get users with pagination, filtering, and sorting
|
||||
*/
|
||||
getUsers(params: UserListParams = {}): Observable<AdminUserListResponse> {
|
||||
this.isLoadingUsers.set(true);
|
||||
this.usersError.set(null);
|
||||
|
||||
// Build query parameters
|
||||
const queryParams: any = {
|
||||
page: params.page ?? 1,
|
||||
limit: params.limit ?? 10
|
||||
};
|
||||
|
||||
if (params.role && params.role !== 'all') {
|
||||
queryParams.role = params.role;
|
||||
}
|
||||
|
||||
if (params.isActive && params.isActive !== 'all') {
|
||||
queryParams.isActive = params.isActive === 'active';
|
||||
}
|
||||
|
||||
if (params.sortBy) {
|
||||
queryParams.sortBy = params.sortBy;
|
||||
queryParams.sortOrder = params.sortOrder ?? 'asc';
|
||||
}
|
||||
|
||||
if (params.search) {
|
||||
queryParams.search = params.search;
|
||||
}
|
||||
|
||||
return this.http.get<AdminUserListResponse>(`${this.apiUrl}/users`, { params: queryParams }).pipe(
|
||||
tap(response => {
|
||||
this.adminUsersState.set(response.data.users);
|
||||
this.usersPagination.set(response.data.pagination);
|
||||
this.currentUserFilters.set(params);
|
||||
this.isLoadingUsers.set(false);
|
||||
}),
|
||||
catchError(error => {
|
||||
this.isLoadingUsers.set(false);
|
||||
return this.handleUsersError(error, 'Failed to load users');
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh users list with current filters
|
||||
*/
|
||||
refreshUsers(): Observable<AdminUserListResponse> {
|
||||
const currentFilters = this.currentUserFilters();
|
||||
return this.getUsers(currentFilters);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get detailed user profile by ID
|
||||
* Fetches comprehensive user data including statistics, quiz history, and activity timeline
|
||||
*/
|
||||
getUserDetails(userId: string): Observable<AdminUserDetail> {
|
||||
this.isLoadingUserDetail.set(true);
|
||||
this.userDetailError.set(null);
|
||||
|
||||
return this.http.get<AdminUserDetailResponse>(`${this.apiUrl}/users/${userId}`).pipe(
|
||||
map(response => response.data),
|
||||
tap(data => {
|
||||
this.selectedUserDetail.set(data);
|
||||
this.isLoadingUserDetail.set(false);
|
||||
}),
|
||||
catchError(error => {
|
||||
this.isLoadingUserDetail.set(false);
|
||||
return this.handleUserDetailError(error, 'Failed to load user details');
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear selected user detail
|
||||
*/
|
||||
clearUserDetail(): void {
|
||||
this.selectedUserDetail.set(null);
|
||||
this.userDetailError.set(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update user role (User <-> Admin)
|
||||
* Updates the role in both the users list and detail view if loaded
|
||||
*/
|
||||
updateUserRole(userId: string, role: 'user' | 'admin'): Observable<{ success: boolean; message: string; data: AdminUser }> {
|
||||
return this.http.put<{ success: boolean; message: string; data: AdminUser }>(`${this.apiUrl}/users/${userId}/role`, { role }).pipe(
|
||||
tap(response => {
|
||||
// Update user in the users list if present
|
||||
const currentUsers = this.adminUsersState();
|
||||
const updatedUsers = currentUsers.map(user =>
|
||||
user.id === userId ? { ...user, role } : user
|
||||
);
|
||||
this.adminUsersState.set(updatedUsers);
|
||||
|
||||
// Update user detail if currently viewing this user
|
||||
const currentDetail = this.selectedUserDetail();
|
||||
if (currentDetail && currentDetail.id === userId) {
|
||||
this.selectedUserDetail.set({ ...currentDetail, role });
|
||||
}
|
||||
|
||||
this.toastService.success(response.message || 'User role updated successfully');
|
||||
}),
|
||||
catchError(error => {
|
||||
let errorMessage = 'Failed to update user role';
|
||||
|
||||
if (error.status === 401) {
|
||||
errorMessage = 'Unauthorized. Please login again.';
|
||||
this.toastService.error(errorMessage);
|
||||
this.router.navigate(['/login']);
|
||||
} else if (error.status === 403) {
|
||||
errorMessage = 'Access denied. Admin privileges required.';
|
||||
this.toastService.error(errorMessage);
|
||||
} else if (error.status === 404) {
|
||||
errorMessage = 'User not found.';
|
||||
this.toastService.error(errorMessage);
|
||||
} else if (error.status === 400 && error.error?.message) {
|
||||
errorMessage = error.error.message;
|
||||
this.toastService.error(errorMessage);
|
||||
} else if (error.error?.message) {
|
||||
errorMessage = error.error.message;
|
||||
this.toastService.error(errorMessage);
|
||||
} else {
|
||||
this.toastService.error(errorMessage);
|
||||
}
|
||||
|
||||
return throwError(() => new Error(errorMessage));
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Activate user account
|
||||
* Updates the user status in both the users list and detail view if loaded
|
||||
*/
|
||||
activateUser(userId: string): Observable<{ success: boolean; message: string; data: AdminUser }> {
|
||||
return this.http.put<{ success: boolean; message: string; data: AdminUser }>(`${this.apiUrl}/users/${userId}/activate`, {}).pipe(
|
||||
tap(response => {
|
||||
// Update user in the users list if present
|
||||
const currentUsers = this.adminUsersState();
|
||||
const updatedUsers = currentUsers.map(user =>
|
||||
user.id === userId ? { ...user, isActive: true } : user
|
||||
);
|
||||
this.adminUsersState.set(updatedUsers);
|
||||
|
||||
// Update user detail if currently viewing this user
|
||||
const currentDetail = this.selectedUserDetail();
|
||||
if (currentDetail && currentDetail.id === userId) {
|
||||
this.selectedUserDetail.set({ ...currentDetail, isActive: true });
|
||||
}
|
||||
|
||||
this.toastService.success(response.message || 'User activated successfully');
|
||||
}),
|
||||
catchError(error => {
|
||||
let errorMessage = 'Failed to activate user';
|
||||
|
||||
if (error.status === 401) {
|
||||
errorMessage = 'Unauthorized. Please login again.';
|
||||
this.toastService.error(errorMessage);
|
||||
this.router.navigate(['/login']);
|
||||
} else if (error.status === 403) {
|
||||
errorMessage = 'Access denied. Admin privileges required.';
|
||||
this.toastService.error(errorMessage);
|
||||
} else if (error.status === 404) {
|
||||
errorMessage = 'User not found.';
|
||||
this.toastService.error(errorMessage);
|
||||
} else if (error.error?.message) {
|
||||
errorMessage = error.error.message;
|
||||
this.toastService.error(errorMessage);
|
||||
} else {
|
||||
this.toastService.error(errorMessage);
|
||||
}
|
||||
|
||||
return throwError(() => new Error(errorMessage));
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deactivate user account (soft delete)
|
||||
* Updates the user status in both the users list and detail view if loaded
|
||||
*/
|
||||
deactivateUser(userId: string): Observable<{ success: boolean; message: string; data: AdminUser }> {
|
||||
return this.http.delete<{ success: boolean; message: string; data: AdminUser }>(`${this.apiUrl}/users/${userId}`).pipe(
|
||||
tap(response => {
|
||||
// Update user in the users list if present
|
||||
const currentUsers = this.adminUsersState();
|
||||
const updatedUsers = currentUsers.map(user =>
|
||||
user.id === userId ? { ...user, isActive: false } : user
|
||||
);
|
||||
this.adminUsersState.set(updatedUsers);
|
||||
|
||||
// Update user detail if currently viewing this user
|
||||
const currentDetail = this.selectedUserDetail();
|
||||
if (currentDetail && currentDetail.id === userId) {
|
||||
this.selectedUserDetail.set({ ...currentDetail, isActive: false });
|
||||
}
|
||||
|
||||
this.toastService.success(response.message || 'User deactivated successfully');
|
||||
}),
|
||||
catchError(error => {
|
||||
let errorMessage = 'Failed to deactivate user';
|
||||
|
||||
if (error.status === 401) {
|
||||
errorMessage = 'Unauthorized. Please login again.';
|
||||
this.toastService.error(errorMessage);
|
||||
this.router.navigate(['/login']);
|
||||
} else if (error.status === 403) {
|
||||
errorMessage = 'Access denied. Admin privileges required.';
|
||||
this.toastService.error(errorMessage);
|
||||
} else if (error.status === 404) {
|
||||
errorMessage = 'User not found.';
|
||||
this.toastService.error(errorMessage);
|
||||
} else if (error.error?.message) {
|
||||
errorMessage = error.error.message;
|
||||
this.toastService.error(errorMessage);
|
||||
} else {
|
||||
this.toastService.error(errorMessage);
|
||||
}
|
||||
|
||||
return throwError(() => new Error(errorMessage));
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle HTTP errors for user detail
|
||||
*/
|
||||
private handleUserDetailError(error: HttpErrorResponse, defaultMessage: string): Observable<never> {
|
||||
let errorMessage = defaultMessage;
|
||||
|
||||
if (error.status === 401) {
|
||||
errorMessage = 'Unauthorized. Please login again.';
|
||||
this.toastService.error(errorMessage);
|
||||
this.router.navigate(['/login']);
|
||||
} else if (error.status === 403) {
|
||||
errorMessage = 'Access denied. Admin privileges required.';
|
||||
this.toastService.error(errorMessage);
|
||||
this.router.navigate(['/dashboard']);
|
||||
} else if (error.status === 404) {
|
||||
errorMessage = 'User not found.';
|
||||
this.toastService.error(errorMessage);
|
||||
} else if (error.error?.message) {
|
||||
errorMessage = error.error.message;
|
||||
this.toastService.error(errorMessage);
|
||||
} else {
|
||||
this.toastService.error(errorMessage);
|
||||
}
|
||||
|
||||
this.userDetailError.set(errorMessage);
|
||||
return throwError(() => new Error(errorMessage));
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle HTTP errors for user management
|
||||
*/
|
||||
private handleUsersError(error: HttpErrorResponse, defaultMessage: string): Observable<never> {
|
||||
let errorMessage = defaultMessage;
|
||||
|
||||
if (error.status === 401) {
|
||||
errorMessage = 'Unauthorized. Please login again.';
|
||||
this.toastService.error(errorMessage);
|
||||
this.router.navigate(['/login']);
|
||||
} else if (error.status === 403) {
|
||||
errorMessage = 'Access denied. Admin privileges required.';
|
||||
this.toastService.error(errorMessage);
|
||||
this.router.navigate(['/dashboard']);
|
||||
} else if (error.status === 404) {
|
||||
errorMessage = 'Users not found.';
|
||||
this.toastService.error(errorMessage);
|
||||
} else if (error.error?.message) {
|
||||
errorMessage = error.error.message;
|
||||
this.toastService.error(errorMessage);
|
||||
} else {
|
||||
this.toastService.error(errorMessage);
|
||||
}
|
||||
|
||||
this.usersError.set(errorMessage);
|
||||
return throwError(() => new Error(errorMessage));
|
||||
}
|
||||
|
||||
// ===========================
|
||||
// Question Management Methods
|
||||
// ===========================
|
||||
|
||||
/**
|
||||
* Get question by ID
|
||||
*/
|
||||
getQuestion(id: string): Observable<{ success: boolean; data: Question; message?: string }> {
|
||||
return this.http.get<{ success: boolean; data: Question; message?: string }>(
|
||||
`${this.apiUrl}/questions/${id}`
|
||||
).pipe(
|
||||
catchError((error: HttpErrorResponse) => this.handleQuestionError(error, 'Failed to load question'))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create new question
|
||||
*/
|
||||
createQuestion(data: QuestionFormData): Observable<{ success: boolean; data: Question; message?: string }> {
|
||||
return this.http.post<{ success: boolean; data: Question; message?: string }>(
|
||||
`${this.apiUrl}/questions`,
|
||||
data
|
||||
).pipe(
|
||||
tap((response) => {
|
||||
this.toastService.success('Question created successfully');
|
||||
}),
|
||||
catchError((error: HttpErrorResponse) => this.handleQuestionError(error, 'Failed to create question'))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update existing question
|
||||
*/
|
||||
updateQuestion(id: string, data: QuestionFormData): Observable<{ success: boolean; data: Question; message?: string }> {
|
||||
return this.http.put<{ success: boolean; data: Question; message?: string }>(
|
||||
`${this.apiUrl}/questions/${id}`,
|
||||
data
|
||||
).pipe(
|
||||
tap((response) => {
|
||||
this.toastService.success('Question updated successfully');
|
||||
}),
|
||||
catchError((error: HttpErrorResponse) => this.handleQuestionError(error, 'Failed to update question'))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete question (soft delete)
|
||||
*/
|
||||
deleteQuestion(id: string): Observable<{ success: boolean; message?: string }> {
|
||||
return this.http.delete<{ success: boolean; message?: string }>(
|
||||
`${this.apiUrl}/questions/${id}`
|
||||
).pipe(
|
||||
tap((response) => {
|
||||
this.toastService.success('Question deleted successfully');
|
||||
}),
|
||||
catchError((error: HttpErrorResponse) => this.handleQuestionError(error, 'Failed to delete question'))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle question-related errors
|
||||
*/
|
||||
private handleQuestionError(error: HttpErrorResponse, defaultMessage: string): Observable<never> {
|
||||
let errorMessage = defaultMessage;
|
||||
|
||||
if (error.status === 401) {
|
||||
errorMessage = 'Unauthorized. Please login again.';
|
||||
this.toastService.error(errorMessage);
|
||||
this.router.navigate(['/login']);
|
||||
} else if (error.status === 403) {
|
||||
errorMessage = 'Access denied. Admin privileges required.';
|
||||
this.toastService.error(errorMessage);
|
||||
this.router.navigate(['/dashboard']);
|
||||
} else if (error.status === 400) {
|
||||
errorMessage = error.error?.message || 'Invalid question data. Please check all fields.';
|
||||
this.toastService.error(errorMessage);
|
||||
} else if (error.error?.message) {
|
||||
errorMessage = error.error.message;
|
||||
this.toastService.error(errorMessage);
|
||||
} else {
|
||||
this.toastService.error(errorMessage);
|
||||
}
|
||||
|
||||
return throwError(() => new Error(errorMessage));
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset all admin state
|
||||
*/
|
||||
resetState(): void {
|
||||
this.adminStatsState.set(null);
|
||||
this.isLoadingStats.set(false);
|
||||
this.statsError.set(null);
|
||||
this.guestAnalyticsState.set(null);
|
||||
this.isLoadingAnalytics.set(false);
|
||||
this.analyticsError.set(null);
|
||||
this.guestSettingsState.set(null);
|
||||
this.isLoadingSettings.set(false);
|
||||
this.settingsError.set(null);
|
||||
this.adminUsersState.set([]);
|
||||
this.isLoadingUsers.set(false);
|
||||
this.usersError.set(null);
|
||||
this.usersPagination.set(null);
|
||||
this.currentUserFilters.set({});
|
||||
this.selectedUserDetail.set(null);
|
||||
this.isLoadingUserDetail.set(false);
|
||||
this.userDetailError.set(null);
|
||||
this.dateRangeFilter.set({ startDate: null, endDate: null });
|
||||
this.clearCache();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user