add changes
This commit is contained in:
@@ -0,0 +1,400 @@
|
||||
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']);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user