275 lines
7.6 KiB
TypeScript
275 lines
7.6 KiB
TypeScript
import { Component, computed, inject, signal } from '@angular/core';
|
|
import { FormsModule } from '@angular/forms';
|
|
import { ActivatedRoute, Router, RouterLink } from '@angular/router';
|
|
import {
|
|
ApiClientService,
|
|
type ApiErrorBody,
|
|
type Permission,
|
|
type RoleDto,
|
|
} from '@boilerplate/api-client';
|
|
import { adminPermissionGroups } from './admin-permissions';
|
|
|
|
@Component({
|
|
standalone: true,
|
|
imports: [FormsModule, RouterLink],
|
|
template: `
|
|
<a routerLink="/admin/roles">Zurueck zur Rollenliste</a>
|
|
|
|
@if (error(); as currentError) {
|
|
<section class="notice error">
|
|
<strong>{{ messageFor(currentError) }}</strong>
|
|
@if (currentError.requestId) {
|
|
<small>Request-ID: {{ currentError.requestId }}</small>
|
|
}
|
|
</section>
|
|
}
|
|
|
|
@if (!loading()) {
|
|
<form class="editor" (ngSubmit)="save()">
|
|
<section>
|
|
<h2>{{ isNew() ? 'Rolle anlegen' : 'Rolle bearbeiten' }}</h2>
|
|
@if (role()?.system) {
|
|
<span class="system">Systemrolle</span>
|
|
}
|
|
<label>
|
|
Name
|
|
<input
|
|
name="name"
|
|
[(ngModel)]="name"
|
|
[readonly]="role()?.system"
|
|
required
|
|
maxlength="80"
|
|
/>
|
|
</label>
|
|
<label>
|
|
Beschreibung
|
|
<textarea name="description" [(ngModel)]="description" maxlength="255"></textarea>
|
|
</label>
|
|
</section>
|
|
|
|
<section>
|
|
<h3>Permissions</h3>
|
|
<div class="groups">
|
|
@for (group of groups; track group.title) {
|
|
<fieldset>
|
|
<legend>{{ group.title }}</legend>
|
|
@for (permission of group.permissions; track permission.id) {
|
|
<label>
|
|
<input
|
|
type="checkbox"
|
|
[checked]="selectedPermissions().has(permission.id)"
|
|
[disabled]="role()?.name === 'admin'"
|
|
(change)="toggle(permission.id)"
|
|
/>
|
|
<span>
|
|
<code>{{ permission.id }}</code>
|
|
{{ permission.label }}
|
|
</span>
|
|
</label>
|
|
}
|
|
</fieldset>
|
|
}
|
|
</div>
|
|
</section>
|
|
|
|
<div class="actions">
|
|
<button type="submit" [disabled]="!name.trim()">Speichern</button>
|
|
@if (role(); as currentRole) {
|
|
@if (!currentRole.system) {
|
|
<button type="button" class="danger" (click)="delete(currentRole)">Loeschen</button>
|
|
}
|
|
}
|
|
</div>
|
|
</form>
|
|
} @else {
|
|
<section class="notice">Rolle wird geladen.</section>
|
|
}
|
|
`,
|
|
styles: [
|
|
`
|
|
a {
|
|
display: inline-flex;
|
|
margin-bottom: 16px;
|
|
color: var(--color-primary-hover);
|
|
}
|
|
.notice,
|
|
.editor {
|
|
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);
|
|
margin-bottom: 16px;
|
|
}
|
|
.editor,
|
|
section,
|
|
.groups,
|
|
fieldset,
|
|
.actions {
|
|
display: grid;
|
|
gap: 14px;
|
|
}
|
|
h2,
|
|
h3 {
|
|
margin: 0;
|
|
}
|
|
label {
|
|
display: grid;
|
|
gap: 6px;
|
|
}
|
|
input,
|
|
textarea,
|
|
button {
|
|
min-height: 44px;
|
|
}
|
|
input,
|
|
textarea {
|
|
border: 1px solid var(--color-border);
|
|
border-radius: 6px;
|
|
padding: 8px 10px;
|
|
}
|
|
textarea {
|
|
min-height: 96px;
|
|
resize: vertical;
|
|
}
|
|
button {
|
|
border: 0;
|
|
border-radius: 6px;
|
|
background: var(--color-primary);
|
|
color: var(--color-surface);
|
|
padding: 0 14px;
|
|
}
|
|
button.danger {
|
|
background: var(--color-danger);
|
|
}
|
|
button:disabled {
|
|
opacity: 0.55;
|
|
}
|
|
fieldset {
|
|
border: 1px solid var(--color-border);
|
|
border-radius: 8px;
|
|
padding: 12px;
|
|
}
|
|
fieldset label {
|
|
grid-template-columns: 24px 1fr;
|
|
align-items: start;
|
|
}
|
|
code {
|
|
display: block;
|
|
margin-bottom: 2px;
|
|
}
|
|
.system {
|
|
width: fit-content;
|
|
border: 1px solid var(--color-border);
|
|
border-radius: 999px;
|
|
padding: 4px 8px;
|
|
color: var(--color-text-secondary);
|
|
}
|
|
@media (min-width: 900px) {
|
|
.groups {
|
|
grid-template-columns: 1fr 1fr;
|
|
}
|
|
.actions {
|
|
grid-template-columns: auto auto;
|
|
justify-content: start;
|
|
}
|
|
}
|
|
`,
|
|
],
|
|
})
|
|
export class AdminRoleDetailPageComponent {
|
|
private readonly api = inject(ApiClientService);
|
|
private readonly route = inject(ActivatedRoute);
|
|
private readonly router = inject(Router);
|
|
readonly role = signal<RoleDto | null>(null);
|
|
readonly loading = signal(false);
|
|
readonly error = signal<ApiErrorBody | null>(null);
|
|
readonly selectedPermissions = signal(new Set<Permission>());
|
|
readonly groups = adminPermissionGroups;
|
|
readonly isNew = computed(() => this.route.snapshot.paramMap.get('id') === 'new');
|
|
name = '';
|
|
description = '';
|
|
|
|
constructor() {
|
|
if (!this.isNew()) this.load();
|
|
}
|
|
|
|
load(): void {
|
|
const id = this.route.snapshot.paramMap.get('id');
|
|
if (!id) return;
|
|
this.loading.set(true);
|
|
this.error.set(null);
|
|
this.api.adminRole(id).subscribe({
|
|
next: (role) => {
|
|
this.role.set(role);
|
|
this.name = role.name;
|
|
this.description = role.description;
|
|
this.selectedPermissions.set(new Set(role.permissions.map((permission) => permission.id)));
|
|
this.loading.set(false);
|
|
},
|
|
error: (error: { error?: ApiErrorBody }) => {
|
|
this.error.set(error.error ?? this.genericError());
|
|
this.loading.set(false);
|
|
},
|
|
});
|
|
}
|
|
|
|
toggle(permission: Permission): void {
|
|
if (this.role()?.name === 'admin') return;
|
|
const next = new Set(this.selectedPermissions());
|
|
if (next.has(permission)) next.delete(permission);
|
|
else next.add(permission);
|
|
this.selectedPermissions.set(next);
|
|
}
|
|
|
|
save(): void {
|
|
const body = {
|
|
name: this.name,
|
|
description: this.description,
|
|
permissions: Array.from(this.selectedPermissions()),
|
|
};
|
|
const request = this.isNew()
|
|
? this.api.createAdminRole(body)
|
|
: this.api.updateAdminRole(this.role()?.id ?? '', body);
|
|
request.subscribe({
|
|
next: (role) => {
|
|
void this.router.navigate(['/admin/roles', role.id]);
|
|
},
|
|
error: (error: { error?: ApiErrorBody }) =>
|
|
this.error.set(error.error ?? this.genericError()),
|
|
});
|
|
}
|
|
|
|
delete(role: RoleDto): void {
|
|
if (!confirm('Rolle wirklich loeschen?')) return;
|
|
this.api.deleteAdminRole(role.id).subscribe({
|
|
next: () => {
|
|
void this.router.navigate(['/admin/roles']);
|
|
},
|
|
error: (error: { error?: ApiErrorBody }) =>
|
|
this.error.set(error.error ?? this.genericError()),
|
|
});
|
|
}
|
|
|
|
messageFor(error: ApiErrorBody): string {
|
|
if (error.code === 'ROLE_STILL_ASSIGNED') return 'Diese Rolle ist noch Benutzern zugewiesen.';
|
|
if (error.code === 'SYSTEM_ROLE_PROTECTED') return 'Diese Systemrolle ist geschuetzt.';
|
|
if (error.code === 'LAST_ACTIVE_ADMIN_REQUIRED') {
|
|
return 'Mindestens ein aktiver Administrator muss erhalten bleiben.';
|
|
}
|
|
return error.message;
|
|
}
|
|
|
|
private genericError(): ApiErrorBody {
|
|
return {
|
|
status: 0,
|
|
code: 'UNKNOWN',
|
|
message: 'Die Rolle konnte nicht verarbeitet werden.',
|
|
requestId: '',
|
|
};
|
|
}
|
|
}
|