feat: build admin user management UI
This commit is contained in:
@@ -1,13 +1,215 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { HttpErrorResponse } from '@angular/common/http';
|
||||
import { Component, inject, signal } from '@angular/core';
|
||||
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 { filter, finalize, of, switchMap, take } 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',
|
||||
template: `
|
||||
<header>
|
||||
<p class="eyebrow">Organisation</p>
|
||||
<h1>Benutzer</h1>
|
||||
<p>Benutzerverzeichnis wird vorbereitet.</p>
|
||||
</header>
|
||||
`,
|
||||
imports: [
|
||||
RouterLink,
|
||||
MatButtonModule,
|
||||
MatFormFieldModule,
|
||||
MatIconModule,
|
||||
MatInputModule,
|
||||
MatProgressSpinnerModule,
|
||||
PlayerAssignments,
|
||||
UserEdit,
|
||||
],
|
||||
templateUrl: './users.html',
|
||||
styleUrl: './users.scss',
|
||||
})
|
||||
export class Users {}
|
||||
export class Users {
|
||||
private readonly usersApi = inject(UsersApi);
|
||||
private readonly adminUsersApi = inject(AdminUsersApi);
|
||||
private readonly authStore = inject(AuthStore);
|
||||
private readonly dialog = inject(MatDialog);
|
||||
|
||||
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 pendingUserId = signal<number | null>(null);
|
||||
|
||||
constructor() {
|
||||
this.loadDirectory();
|
||||
}
|
||||
|
||||
protected loadDirectory(): void {
|
||||
this.loading.set(true);
|
||||
this.loadError.set(null);
|
||||
const search = this.search();
|
||||
this.usersApi
|
||||
.loadDirectory({ page: this.page(), limit: this.limit, ...(search ? { search } : {}) })
|
||||
.pipe(finalize(() => this.loading.set(false)))
|
||||
.subscribe({
|
||||
next: (directory) => this.directory.set(directory),
|
||||
error: (error: HttpErrorResponse) => {
|
||||
this.directory.set(null);
|
||||
this.loadError.set(this.errorMessage(error, 'Benutzer konnten nicht geladen werden.'));
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
protected submitSearch(): void {
|
||||
this.search.set(this.searchDraft().trim());
|
||||
this.page.set(1);
|
||||
this.loadDirectory();
|
||||
}
|
||||
|
||||
protected previousPage(): void {
|
||||
if (this.page() <= 1 || this.loading()) return;
|
||||
this.page.update((value) => value - 1);
|
||||
this.loadDirectory();
|
||||
}
|
||||
|
||||
protected nextPage(): void {
|
||||
if (!this.directory()?.hasNextPage || this.loading()) 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 {
|
||||
return statusId === 2 ? 'Inaktiv' : 'Aktiv';
|
||||
}
|
||||
|
||||
protected roleName(roleId?: number): string {
|
||||
return roleId === 1 ? 'Administrator' : 'Benutzer';
|
||||
}
|
||||
|
||||
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 {
|
||||
this.assignmentUserId.set(null);
|
||||
this.editingUserId.update((value) => (value === userId ? null : userId));
|
||||
this.mutationError.set(null);
|
||||
}
|
||||
|
||||
protected toggleAssignments(userId: number): void {
|
||||
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;
|
||||
profileRequest
|
||||
.pipe(
|
||||
switchMap(() =>
|
||||
roleId === value.role ? of(user) : this.adminUsersApi.updateRole(user.id, { role: value.role }),
|
||||
),
|
||||
finalize(() => this.pendingUserId.set(null)),
|
||||
)
|
||||
.subscribe({
|
||||
next: () => {
|
||||
this.editingUserId.set(null);
|
||||
this.loadDirectory();
|
||||
},
|
||||
error: (error: HttpErrorResponse) => {
|
||||
this.mutationError.set(this.errorMessage(error, 'Änderung fehlgeschlagen.'));
|
||||
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))
|
||||
.subscribe(() => this.updateStatus(user.id, status));
|
||||
}
|
||||
|
||||
protected isSelf(user: UserDirectoryRecord): boolean {
|
||||
return user.id === this.currentUser()?.id;
|
||||
}
|
||||
|
||||
protected assignmentsChanged(): void {
|
||||
this.loadDirectory();
|
||||
}
|
||||
|
||||
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)))
|
||||
.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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user