bump
This commit is contained in:
270
apps/frontend/src/app/features/admin/admin-users.page.ts
Normal file
270
apps/frontend/src/app/features/admin/admin-users.page.ts
Normal file
@@ -0,0 +1,270 @@
|
||||
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: '',
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user