401 lines
10 KiB
TypeScript
401 lines
10 KiB
TypeScript
import { Component, OnInit, inject, DestroyRef, computed } from '@angular/core';
|
|
import { CommonModule } from '@angular/common';
|
|
import { FormBuilder, FormGroup, ReactiveFormsModule } from '@angular/forms';
|
|
import { Router, ActivatedRoute } from '@angular/router';
|
|
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
|
import { debounceTime, distinctUntilChanged } 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 { AdminUser, UserListParams } from '../../../core/models/admin.model';
|
|
import { RoleUpdateDialogComponent } from '../role-update-dialog/role-update-dialog.component';
|
|
import { StatusUpdateDialogComponent } from '../status-update-dialog/status-update-dialog.component';
|
|
import { PaginationService, PaginationState } from '../../../core/services/pagination.service';
|
|
import { PaginationComponent } from '../../../shared/components/pagination/pagination.component';
|
|
|
|
/**
|
|
* AdminUsersComponent
|
|
*
|
|
* Displays and manages all users with pagination, filtering, and sorting.
|
|
*
|
|
* Features:
|
|
* - User table with key columns
|
|
* - Search by username/email
|
|
* - Filter by role and status
|
|
* - Sort by username, email, or date
|
|
* - Pagination controls
|
|
* - Action buttons for each user
|
|
* - Responsive design (cards on mobile)
|
|
* - Loading and error states
|
|
*/
|
|
@Component({
|
|
selector: 'app-admin-users',
|
|
standalone: true,
|
|
imports: [
|
|
CommonModule,
|
|
ReactiveFormsModule,
|
|
MatCardModule,
|
|
MatButtonModule,
|
|
MatIconModule,
|
|
MatTableModule,
|
|
MatInputModule,
|
|
MatFormFieldModule,
|
|
MatSelectModule,
|
|
MatProgressSpinnerModule,
|
|
MatTooltipModule,
|
|
MatChipsModule,
|
|
MatMenuModule,
|
|
MatDialogModule,
|
|
PaginationComponent
|
|
],
|
|
templateUrl: './admin-users.component.html',
|
|
styleUrl: './admin-users.component.scss'
|
|
})
|
|
export class AdminUsersComponent implements OnInit {
|
|
private readonly adminService = inject(AdminService);
|
|
private readonly router = inject(Router);
|
|
private readonly route = inject(ActivatedRoute);
|
|
private readonly fb = inject(FormBuilder);
|
|
private readonly destroyRef = inject(DestroyRef);
|
|
private readonly dialog = inject(MatDialog);
|
|
private readonly paginationService = inject(PaginationService);
|
|
|
|
// Service signals
|
|
readonly users = this.adminService.adminUsersState;
|
|
readonly isLoading = this.adminService.isLoadingUsers;
|
|
readonly error = this.adminService.usersError;
|
|
readonly pagination = this.adminService.usersPagination;
|
|
|
|
// Computed pagination state for reusable component
|
|
readonly paginationState = computed<PaginationState | null>(() => {
|
|
const pag = this.pagination();
|
|
if (!pag) return null;
|
|
|
|
return this.paginationService.calculatePaginationState({
|
|
currentPage: pag.currentPage,
|
|
pageSize: pag.limit,
|
|
totalItems: pag.totalUsers
|
|
});
|
|
});
|
|
|
|
// Computed page numbers
|
|
readonly pageNumbers = computed(() => {
|
|
const state = this.paginationState();
|
|
if (!state) return [];
|
|
|
|
return this.paginationService.calculatePageNumbers(
|
|
state.currentPage,
|
|
state.totalPages,
|
|
5
|
|
);
|
|
});
|
|
|
|
// Table configuration
|
|
displayedColumns: string[] = ['username', 'email', 'role', 'status', 'joinedDate', 'lastLogin', 'actions'];
|
|
|
|
// Filter form
|
|
filterForm!: FormGroup;
|
|
|
|
// Current params
|
|
currentParams: UserListParams = {
|
|
page: 1,
|
|
limit: 10,
|
|
role: 'all',
|
|
isActive: 'all',
|
|
sortBy: 'createdAt',
|
|
sortOrder: 'desc',
|
|
search: ''
|
|
};
|
|
|
|
// Expose Math for template
|
|
Math = Math;
|
|
|
|
ngOnInit(): void {
|
|
this.initializeFilterForm();
|
|
this.setupSearchDebounce();
|
|
this.loadUsersFromRoute();
|
|
}
|
|
|
|
/**
|
|
* Initialize filter form
|
|
*/
|
|
private initializeFilterForm(): void {
|
|
this.filterForm = this.fb.group({
|
|
search: [''],
|
|
role: ['all'],
|
|
isActive: ['all'],
|
|
sortBy: ['createdAt'],
|
|
sortOrder: ['desc']
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Setup search field debounce
|
|
*/
|
|
private setupSearchDebounce(): void {
|
|
this.filterForm.get('search')?.valueChanges
|
|
.pipe(
|
|
debounceTime(500),
|
|
distinctUntilChanged(),
|
|
takeUntilDestroyed(this.destroyRef)
|
|
)
|
|
.subscribe(() => {
|
|
this.applyFilters();
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Load users based on route query params
|
|
*/
|
|
private loadUsersFromRoute(): void {
|
|
this.route.queryParams
|
|
.pipe(takeUntilDestroyed(this.destroyRef))
|
|
.subscribe(params => {
|
|
this.currentParams = {
|
|
page: +(params['page'] || 1),
|
|
limit: +(params['limit'] || 10),
|
|
role: params['role'] || 'all',
|
|
isActive: params['isActive'] || 'all',
|
|
sortBy: params['sortBy'] || 'createdAt',
|
|
sortOrder: params['sortOrder'] || 'desc',
|
|
search: params['search'] || ''
|
|
};
|
|
|
|
// Update form with current params
|
|
this.filterForm.patchValue({
|
|
search: this.currentParams.search,
|
|
role: this.currentParams.role,
|
|
isActive: this.currentParams.isActive,
|
|
sortBy: this.currentParams.sortBy,
|
|
sortOrder: this.currentParams.sortOrder
|
|
}, { emitEvent: false });
|
|
|
|
this.loadUsers();
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Load users from API
|
|
*/
|
|
private loadUsers(): void {
|
|
this.adminService.getUsers(this.currentParams)
|
|
.pipe(takeUntilDestroyed(this.destroyRef))
|
|
.subscribe();
|
|
}
|
|
|
|
/**
|
|
* Apply filters and reset to page 1
|
|
*/
|
|
applyFilters(): void {
|
|
const formValue = this.filterForm.value;
|
|
this.currentParams = {
|
|
...this.currentParams,
|
|
page: 1, // Reset to first page
|
|
search: formValue.search || '',
|
|
role: formValue.role || 'all',
|
|
isActive: formValue.isActive || 'all',
|
|
sortBy: formValue.sortBy || 'createdAt',
|
|
sortOrder: formValue.sortOrder || 'desc'
|
|
};
|
|
|
|
this.updateRouteParams();
|
|
}
|
|
|
|
/**
|
|
* Change page
|
|
*/
|
|
goToPage(page: number): void {
|
|
if (page < 1 || page > (this.pagination()?.totalPages ?? 1)) return;
|
|
|
|
this.currentParams = {
|
|
...this.currentParams,
|
|
page
|
|
};
|
|
|
|
this.updateRouteParams();
|
|
}
|
|
|
|
/**
|
|
* Handle page size change
|
|
*/
|
|
onPageSizeChange(pageSize: number): void {
|
|
this.currentParams = {
|
|
...this.currentParams,
|
|
page: 1,
|
|
limit: pageSize
|
|
};
|
|
|
|
this.updateRouteParams();
|
|
}
|
|
|
|
/**
|
|
* Update route query parameters
|
|
*/
|
|
private updateRouteParams(): void {
|
|
this.router.navigate([], {
|
|
relativeTo: this.route,
|
|
queryParams: this.currentParams,
|
|
queryParamsHandling: 'merge'
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Refresh users list
|
|
*/
|
|
refreshUsers(): void {
|
|
this.loadUsers();
|
|
}
|
|
|
|
/**
|
|
* Reset all filters
|
|
*/
|
|
resetFilters(): void {
|
|
this.filterForm.reset({
|
|
search: '',
|
|
role: 'all',
|
|
isActive: 'all',
|
|
sortBy: 'createdAt',
|
|
sortOrder: 'desc'
|
|
});
|
|
this.applyFilters();
|
|
}
|
|
|
|
/**
|
|
* View user details
|
|
*/
|
|
viewUserDetails(userId: string): void {
|
|
this.router.navigate(['/admin/users', userId]);
|
|
}
|
|
|
|
/**
|
|
* Edit user role - Opens role update dialog
|
|
*/
|
|
editUserRole(user: AdminUser): void {
|
|
const dialogRef = this.dialog.open(RoleUpdateDialogComponent, {
|
|
width: '600px',
|
|
maxWidth: '95vw',
|
|
data: { user },
|
|
disableClose: false
|
|
});
|
|
|
|
dialogRef.afterClosed().subscribe(newRole => {
|
|
if (newRole && newRole !== user.role) {
|
|
this.adminService.updateUserRole(user.id, newRole).subscribe({
|
|
next: () => {
|
|
// User list is automatically updated in the service
|
|
},
|
|
error: () => {
|
|
// Error is handled by service
|
|
}
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Toggle user active status
|
|
*/
|
|
toggleUserStatus(user: AdminUser): void {
|
|
const action = user.isActive ? 'deactivate' : 'activate';
|
|
|
|
const dialogRef = this.dialog.open(StatusUpdateDialogComponent, {
|
|
width: '500px',
|
|
data: {
|
|
user: user,
|
|
action: action
|
|
},
|
|
disableClose: false,
|
|
autoFocus: true
|
|
});
|
|
|
|
dialogRef.afterClosed()
|
|
.pipe(takeUntilDestroyed(this.destroyRef))
|
|
.subscribe((confirmed: boolean) => {
|
|
if (!confirmed) return;
|
|
|
|
// Call appropriate service method based on action
|
|
const serviceCall = action === 'activate'
|
|
? this.adminService.activateUser(user.id)
|
|
: this.adminService.deactivateUser(user.id);
|
|
|
|
serviceCall
|
|
.pipe(takeUntilDestroyed(this.destroyRef))
|
|
.subscribe({
|
|
next: () => {
|
|
// Signal update happens automatically in service
|
|
// No need to manually refresh the list
|
|
},
|
|
error: (error) => {
|
|
console.error('Error updating user status:', error);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Get role chip color
|
|
*/
|
|
getRoleColor(role: string): string {
|
|
return role === 'admin' ? 'primary' : 'accent';
|
|
}
|
|
|
|
/**
|
|
* Get status chip color
|
|
*/
|
|
getStatusColor(isActive: boolean): string {
|
|
return isActive ? 'primary' : 'warn';
|
|
}
|
|
|
|
/**
|
|
* Get status text
|
|
*/
|
|
getStatusText(isActive: boolean): string {
|
|
return isActive ? 'Active' : 'Inactive';
|
|
}
|
|
|
|
/**
|
|
* Format date for display
|
|
*/
|
|
formatDate(date: string | undefined): string {
|
|
if (!date) return 'Never';
|
|
const d = new Date(date);
|
|
return d.toLocaleDateString('en-US', {
|
|
year: 'numeric',
|
|
month: 'short',
|
|
day: 'numeric'
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Format date with time for display
|
|
*/
|
|
formatDateTime(date: string | undefined): string {
|
|
if (!date) return 'Never';
|
|
const d = new Date(date);
|
|
return d.toLocaleString('en-US', {
|
|
year: 'numeric',
|
|
month: 'short',
|
|
day: 'numeric',
|
|
hour: '2-digit',
|
|
minute: '2-digit'
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Navigate back to admin dashboard
|
|
*/
|
|
goBack(): void {
|
|
this.router.navigate(['/admin']);
|
|
}
|
|
}
|