271 lines
7.0 KiB
TypeScript
271 lines
7.0 KiB
TypeScript
import { Component, inject, signal } from '@angular/core';
|
|
import { FormsModule } from '@angular/forms';
|
|
import { RouterLink } from '@angular/router';
|
|
import {
|
|
ApiClientService,
|
|
type AdminUserListItemDto,
|
|
type ApiErrorBody,
|
|
type RoleDto,
|
|
} from '@boilerplate/api-client';
|
|
|
|
type ActiveFilter = 'all' | 'active' | 'inactive';
|
|
type UserSort = 'name' | 'email' | 'lastLoginAt' | 'createdAt';
|
|
|
|
@Component({
|
|
standalone: true,
|
|
imports: [FormsModule, RouterLink],
|
|
template: `
|
|
<form class="toolbar" (ngSubmit)="load()">
|
|
<label>
|
|
Suche
|
|
<input name="search" [(ngModel)]="search" placeholder="Name oder E-Mail" />
|
|
</label>
|
|
<label>
|
|
Status
|
|
<select name="active" [(ngModel)]="active">
|
|
<option value="all">Alle</option>
|
|
<option value="active">Aktiv</option>
|
|
<option value="inactive">Deaktiviert</option>
|
|
</select>
|
|
</label>
|
|
<label>
|
|
Rolle
|
|
<select name="roleId" [(ngModel)]="roleId">
|
|
<option value="">Alle Rollen</option>
|
|
@for (role of roles(); track role.id) {
|
|
<option [value]="role.id">{{ role.name }}</option>
|
|
}
|
|
</select>
|
|
</label>
|
|
<label>
|
|
Sortierung
|
|
<select name="sort" [(ngModel)]="sort">
|
|
<option value="name">Name</option>
|
|
<option value="email">E-Mail</option>
|
|
<option value="lastLoginAt">Letzter Login</option>
|
|
<option value="createdAt">Erstellt</option>
|
|
</select>
|
|
</label>
|
|
<button type="submit">Suchen</button>
|
|
</form>
|
|
|
|
@if (error(); as currentError) {
|
|
<section class="notice error">
|
|
<strong>{{ currentError.message }}</strong>
|
|
@if (currentError.requestId) {
|
|
<small>Request-ID: {{ currentError.requestId }}</small>
|
|
}
|
|
</section>
|
|
}
|
|
|
|
@if (loading()) {
|
|
<section class="notice">Benutzer werden geladen.</section>
|
|
} @else if (users().length === 0) {
|
|
<section class="notice">Keine Benutzer gefunden.</section>
|
|
} @else {
|
|
<section class="list">
|
|
@for (user of users(); track user.id) {
|
|
<article [class.inactive]="!user.active">
|
|
<div class="summary">
|
|
<strong>{{ user.name }}</strong>
|
|
<span>{{ user.email || 'Keine E-Mail' }}</span>
|
|
<span>{{ user.active ? 'Aktiv' : 'Deaktiviert' }}</span>
|
|
</div>
|
|
<div class="chips" aria-label="Rollen">
|
|
@for (role of user.roles; track role.id) {
|
|
<span>{{ role.name }}</span>
|
|
}
|
|
</div>
|
|
<dl>
|
|
<div>
|
|
<dt>Sessions</dt>
|
|
<dd>{{ user.activeSessionCount }}</dd>
|
|
</div>
|
|
<div>
|
|
<dt>Letzter Login</dt>
|
|
<dd>{{ user.lastLoginAt || 'nie' }}</dd>
|
|
</div>
|
|
</dl>
|
|
<a class="button" [routerLink]="['/admin/users', user.id]">Details</a>
|
|
</article>
|
|
}
|
|
</section>
|
|
<nav class="pagination" aria-label="Seitennavigation">
|
|
<button type="button" [disabled]="page <= 1" (click)="previous()">Zurueck</button>
|
|
<span>Seite {{ page }} von {{ totalPages() }}</span>
|
|
<button type="button" [disabled]="page >= totalPages()" (click)="next()">Weiter</button>
|
|
</nav>
|
|
}
|
|
`,
|
|
styles: [
|
|
`
|
|
.toolbar {
|
|
display: grid;
|
|
gap: 12px;
|
|
margin-bottom: 16px;
|
|
}
|
|
label,
|
|
.summary,
|
|
dl {
|
|
display: grid;
|
|
gap: 6px;
|
|
}
|
|
input,
|
|
select,
|
|
button,
|
|
.button {
|
|
min-height: 44px;
|
|
}
|
|
input,
|
|
select {
|
|
border: 1px solid var(--color-border);
|
|
border-radius: 6px;
|
|
padding: 0 10px;
|
|
}
|
|
button,
|
|
.button {
|
|
border: 0;
|
|
border-radius: 6px;
|
|
background: var(--color-primary);
|
|
color: var(--color-surface);
|
|
padding: 0 14px;
|
|
}
|
|
.button {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
text-decoration: none;
|
|
}
|
|
.notice,
|
|
article {
|
|
background: var(--color-surface);
|
|
border: 1px solid var(--color-border);
|
|
border-radius: 8px;
|
|
padding: 16px;
|
|
}
|
|
.notice.error {
|
|
border-color: var(--color-danger);
|
|
color: var(--color-danger);
|
|
}
|
|
.list {
|
|
display: grid;
|
|
gap: 12px;
|
|
}
|
|
article {
|
|
display: grid;
|
|
gap: 12px;
|
|
}
|
|
article.inactive {
|
|
border-left: 4px solid var(--color-border-strong);
|
|
}
|
|
.chips {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 6px;
|
|
}
|
|
.chips span {
|
|
border: 1px solid var(--color-border);
|
|
border-radius: 999px;
|
|
padding: 4px 8px;
|
|
background: var(--color-background);
|
|
}
|
|
dl {
|
|
margin: 0;
|
|
}
|
|
dt {
|
|
color: var(--color-text-muted);
|
|
font-size: 0.88rem;
|
|
}
|
|
dd {
|
|
margin: 0;
|
|
}
|
|
.pagination {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 12px;
|
|
margin-top: 16px;
|
|
}
|
|
@media (min-width: 900px) {
|
|
.toolbar {
|
|
grid-template-columns: 2fr 1fr 1fr 1fr auto;
|
|
align-items: end;
|
|
}
|
|
article {
|
|
grid-template-columns: 1.4fr 1fr 1.2fr auto;
|
|
align-items: center;
|
|
}
|
|
}
|
|
`,
|
|
],
|
|
})
|
|
export class AdminUsersPageComponent {
|
|
private readonly api = inject(ApiClientService);
|
|
readonly users = signal<AdminUserListItemDto[]>([]);
|
|
readonly roles = signal<RoleDto[]>([]);
|
|
readonly loading = signal(false);
|
|
readonly error = signal<ApiErrorBody | null>(null);
|
|
search = '';
|
|
active: ActiveFilter = 'all';
|
|
roleId = '';
|
|
sort: UserSort = 'name';
|
|
page = 1;
|
|
pageSize = 25;
|
|
total = 0;
|
|
|
|
constructor() {
|
|
this.api.adminRoles().subscribe((roles) => this.roles.set(roles));
|
|
this.load();
|
|
}
|
|
|
|
load(): void {
|
|
this.loading.set(true);
|
|
this.error.set(null);
|
|
this.api
|
|
.adminUsers({
|
|
search: this.search,
|
|
active: this.active,
|
|
roleId: this.roleId,
|
|
sort: this.sort,
|
|
page: this.page,
|
|
pageSize: this.pageSize,
|
|
})
|
|
.subscribe({
|
|
next: (page) => {
|
|
this.users.set(page.items);
|
|
this.total = page.total;
|
|
this.page = page.page;
|
|
this.pageSize = page.pageSize;
|
|
this.loading.set(false);
|
|
},
|
|
error: (error: { error?: ApiErrorBody }) => {
|
|
this.error.set(error.error ?? this.genericError());
|
|
this.loading.set(false);
|
|
},
|
|
});
|
|
}
|
|
|
|
previous(): void {
|
|
this.page -= 1;
|
|
this.load();
|
|
}
|
|
|
|
next(): void {
|
|
this.page += 1;
|
|
this.load();
|
|
}
|
|
|
|
totalPages(): number {
|
|
return Math.max(1, Math.ceil(this.total / this.pageSize));
|
|
}
|
|
|
|
private genericError(): ApiErrorBody {
|
|
return {
|
|
status: 0,
|
|
code: 'UNKNOWN',
|
|
message: 'Benutzer konnten nicht geladen werden.',
|
|
requestId: '',
|
|
};
|
|
}
|
|
}
|