add changes

This commit is contained in:
AD2025
2025-11-14 21:48:47 +02:00
parent 6f23890407
commit 37b4d565b1
72 changed files with 17104 additions and 246 deletions

View File

@@ -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']);
}
}