import { HttpErrorResponse } from '@angular/common/http'; import { Component, DestroyRef, inject, signal } from '@angular/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { RouterLink } from '@angular/router'; import { MatButtonModule } from '@angular/material/button'; import { MatDialog } from '@angular/material/dialog'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatIconModule } from '@angular/material/icon'; import { MatInputModule } from '@angular/material/input'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { EMPTY, Subject, catchError, filter, finalize, of, startWith, switchMap, take, tap } from 'rxjs'; import { AuthStore } from '../../core/auth/auth-store'; import { AdminUsersApi } from '../../core/users/admin-users-api'; import { UsersApi } from '../../core/users/users-api'; import { AdminUserDirectorySummary, AdminUserStatusId, UserDirectoryPage, UserDirectoryRecord, } from '../../models/user-directory.model'; import { ConfirmDialog } from '../../shared/confirm-dialog/confirm-dialog'; import { PlayerAssignments } from './player-assignments'; import { UserEdit, UserEditValue } from './user-edit'; @Component({ selector: 'app-users', imports: [ RouterLink, MatButtonModule, MatFormFieldModule, MatIconModule, MatInputModule, MatProgressSpinnerModule, PlayerAssignments, UserEdit, ], templateUrl: './users.html', styleUrl: './users.scss', }) export class Users { private readonly usersApi = inject(UsersApi); private readonly adminUsersApi = inject(AdminUsersApi); private readonly authStore = inject(AuthStore); private readonly dialog = inject(MatDialog); private readonly destroyRef = inject(DestroyRef); private readonly directoryRequests = new Subject(); protected readonly isAdmin = this.authStore.isGlobalAdmin; protected readonly currentUser = this.authStore.currentUser; protected readonly directory = signal(null); protected readonly loading = signal(true); protected readonly loadError = signal(null); protected readonly mutationError = signal(null); protected readonly searchDraft = signal(''); protected readonly search = signal(''); protected readonly page = signal(1); protected readonly limit = 20; protected readonly editingUserId = signal(null); protected readonly assignmentUserId = signal(null); protected readonly assignmentBusyUserId = signal(null); protected readonly pendingUserId = signal(null); constructor() { this.directoryRequests .pipe( startWith(true), switchMap((showLoading) => { if (showLoading) this.loading.set(true); this.loadError.set(null); const search = this.search(); return this.usersApi .loadDirectory({ page: this.page(), limit: this.limit, ...(search ? { search } : {}) }) .pipe( tap((directory) => this.directory.set(directory)), catchError((error: HttpErrorResponse) => { this.directory.set(null); this.loadError.set(this.errorMessage(error, 'Benutzer konnten nicht geladen werden.')); return EMPTY; }), finalize(() => this.loading.set(false)), ); }), takeUntilDestroyed(this.destroyRef), ) .subscribe(); } protected loadDirectory(showLoading = true): void { this.directoryRequests.next(showLoading); } protected submitSearch(): void { if (this.assignmentBusyUserId() !== null) return; const search = this.searchDraft().trim(); if (this.loading() && search === this.search() && this.page() === 1) return; this.search.set(search); this.page.set(1); this.loadDirectory(); } protected previousPage(): void { if (this.page() <= 1 || this.loading() || this.assignmentBusyUserId() !== null) return; this.page.update((value) => value - 1); this.loadDirectory(); } protected nextPage(): void { if (!this.directory()?.hasNextPage || this.loading() || this.assignmentBusyUserId() !== null) return; this.page.update((value) => value + 1); this.loadDirectory(); } protected adminDetails(user: UserDirectoryRecord): AdminUserDirectorySummary | null { if (!this.isAdmin()) return null; return 'email' in user && 'role' in user ? user : null; } protected fullName(user: UserDirectoryRecord): string { return [user.firstName, user.lastName].filter(Boolean).join(' ') || `Benutzer ${user.id}`; } protected initials(user: UserDirectoryRecord): string { const value = `${user.firstName?.charAt(0) ?? ''}${user.lastName?.charAt(0) ?? ''}`.trim(); return value || '?'; } protected statusName(statusId?: number): string { if (statusId === 1) return 'Aktiv'; if (statusId === 2) return 'Inaktiv'; return 'Unbekannt'; } protected roleName(roleId?: number): string { if (roleId === 1) return 'Administrator'; if (roleId === 2) return 'Benutzer'; return 'Unbekannt'; } protected teamRoleName(name?: string): string { return ( { player: 'Spieler', scnd_treasurer: '2. Kassenwart', captain: 'Kapitän', treasurer: 'Kassenwart', coach: 'Trainer', }[name ?? ''] ?? 'Spieler' ); } protected toggleEdit(userId: number): void { if (this.assignmentBusyUserId() !== null) return; this.assignmentUserId.set(null); this.editingUserId.update((value) => (value === userId ? null : userId)); this.mutationError.set(null); } protected toggleAssignments(userId: number): void { if (this.assignmentBusyUserId() !== null) return; this.editingUserId.set(null); this.assignmentUserId.update((value) => (value === userId ? null : userId)); this.mutationError.set(null); } protected saveEdit(user: AdminUserDirectorySummary, value: UserEditValue): void { if (this.pendingUserId() !== null) return; this.pendingUserId.set(user.id); this.mutationError.set(null); const profileRequest = this.adminUsersApi.updateProfile(user.id, { firstName: value.firstName, lastName: value.lastName, }); const roleId = user.role?.id; let profileSaved = false; profileRequest .pipe( tap((updatedUser) => { profileSaved = true; const currentUser = this.currentUser(); if (currentUser?.id === updatedUser.id) { this.authStore.updateUser({ ...currentUser, firstName: updatedUser.firstName, lastName: updatedUser.lastName, }); } }), switchMap(() => roleId === value.role ? of(user) : this.adminUsersApi.updateRole(user.id, { role: value.role }), ), finalize(() => this.pendingUserId.set(null)), takeUntilDestroyed(this.destroyRef), ) .subscribe({ next: () => { this.editingUserId.set(null); this.loadDirectory(); }, error: (error: HttpErrorResponse) => { this.mutationError.set(this.errorMessage(error, 'Änderung fehlgeschlagen.')); if (profileSaved) this.loadDirectory(); }, }); } protected changeStatus(user: AdminUserDirectorySummary): void { if (this.isSelf(user) || this.pendingUserId() !== null) return; const isActive = user.status?.id !== 2; const status: AdminUserStatusId = isActive ? 2 : 1; this.dialog .open(ConfirmDialog, { data: { title: isActive ? 'Benutzer deaktivieren?' : 'Benutzer aktivieren?', message: `${this.fullName(user)} wird ${isActive ? 'deaktiviert' : 'aktiviert'}.`, confirmLabel: isActive ? 'Deaktivieren' : 'Aktivieren', }, restoreFocus: true, }) .afterClosed() .pipe(filter(Boolean), take(1), takeUntilDestroyed(this.destroyRef)) .subscribe(() => this.updateStatus(user.id, status)); } protected isSelf(user: UserDirectoryRecord): boolean { return user.id === this.currentUser()?.id; } protected assignmentsChanged(): void { this.loadDirectory(false); } protected assignmentBusyChanged(userId: number, busy: boolean): void { this.assignmentBusyUserId.set(busy ? userId : null); } private updateStatus(userId: number, status: AdminUserStatusId): void { this.pendingUserId.set(userId); this.mutationError.set(null); this.adminUsersApi .updateStatus(userId, { status }) .pipe(finalize(() => this.pendingUserId.set(null)), takeUntilDestroyed(this.destroyRef)) .subscribe({ next: () => this.loadDirectory(), error: (error: HttpErrorResponse) => this.mutationError.set(this.errorMessage(error, 'Status konnte nicht geändert werden.')), }); } private errorMessage(error: HttpErrorResponse, fallback: string): string { const detail = typeof error.error?.message === 'string' ? error.error.message : ''; if (error.status === 403) return `Keine Berechtigung. ${detail}`.trim(); return detail ? `${fallback} ${detail}` : fallback; } }