255 lines
9.1 KiB
TypeScript
255 lines
9.1 KiB
TypeScript
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<boolean>();
|
|
|
|
protected readonly isAdmin = this.authStore.isGlobalAdmin;
|
|
protected readonly currentUser = this.authStore.currentUser;
|
|
protected readonly directory = signal<UserDirectoryPage | null>(null);
|
|
protected readonly loading = signal(true);
|
|
protected readonly loadError = signal<string | null>(null);
|
|
protected readonly mutationError = signal<string | null>(null);
|
|
protected readonly searchDraft = signal('');
|
|
protected readonly search = signal('');
|
|
protected readonly page = signal(1);
|
|
protected readonly limit = 20;
|
|
protected readonly editingUserId = signal<number | null>(null);
|
|
protected readonly assignmentUserId = signal<number | null>(null);
|
|
protected readonly assignmentBusyUserId = signal<number | null>(null);
|
|
protected readonly pendingUserId = signal<number | null>(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;
|
|
}
|
|
}
|