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: `
@if (error(); as currentError) {
{{ currentError.message }}
@if (currentError.requestId) {
Request-ID: {{ currentError.requestId }}
}
}
@if (loading()) {
} @else if (users().length === 0) {
} @else {
@for (user of users(); track user.id) {
{{ user.name }}
{{ user.email || 'Keine E-Mail' }}
{{ user.active ? 'Aktiv' : 'Deaktiviert' }}
@for (role of user.roles; track role.id) {
{{ role.name }}
}
- Sessions
- {{ user.activeSessionCount }}
- Letzter Login
- {{ user.lastLoginAt || 'nie' }}
Details
}
}
`,
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([]);
readonly roles = signal([]);
readonly loading = signal(false);
readonly error = signal(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: '',
};
}
}