import { Component, OnInit, signal } from "@angular/core"; import { DatePipe } from "@angular/common"; import { FormsModule } from "@angular/forms"; import { Router } from "@angular/router"; import { MatSnackBar } from "@angular/material/snack-bar"; import { ApiService } from "../core/api.service"; import { MATERIAL } from "../shared/material"; @Component({ standalone: true, imports: [FormsModule, DatePipe, ...MATERIAL], template: `

Administration

Benutzer, Daten, Sicherungen und Ausbildungsstellen-Watcher

@for (item of sections; track item.id) { }
@if (section() === "database") {
Datenbank sichern

Aktuellen PostgreSQL-Dump als ZIP herunterladen.

PgHero

Datenbankabfragen, Speicher und Performance analysieren.

monitoring PgHero öffnen
Datenbank wiederherstellen

Eine Sicherung überschreibt den aktuellen Datenbestand. Vorher unbedingt ein Backup herunterladen.

} @if (section() === "users") { E-MailStartpasswortSofort bestätigen
E-Mail {{ u.email }} @if (u.is_admin) { Admin } Bestätigt Einträge {{ u.entries_count }}
} @if (section() === "entries") {
Datum {{ e.date | date: "dd.MM.yyyy" }} Benutzer {{ e.user_email }} Art {{ e.praktikums_typ }} / {{ e.entry_art }} Zeit {{ e.hours }} h {{ e.minutes }} min
} @if (section() === "watcher") {

Ausbildungsstellen-Watcher

NameURLTypHTMLRSSAktiv
@for (s of sources(); track s.id) { {{ s.name }}{{ s.url }}Zuletzt geprüft: {{ s.last_checked_at ? (s.last_checked_at | date: "dd.MM.yyyy HH:mm") : "–" }}
}

Gefundene Treffer

@for (h of hits(); track h.id) { {{ h.title }}{{ h.source_name }}

{{ h.snippet }}

Öffnen
} }
`, styles: [ ` .admin-nav { display: flex; flex-wrap: wrap; gap: 10px; margin-bottom: 22px; } .selected { background: #dce9ff !important; } .danger { border: 1px solid #e1a4a4; } .danger input { margin: 10px 0 20px; } .source-form { margin-bottom: 22px; } .source-form mat-card-content { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; } .source-form mat-form-field { min-width: 220px; } .hit { margin-bottom: 10px; } .hit mat-card-content, .cards mat-card-content { display: flex; flex-direction: column; gap: 8px; } table { width: 100%; } `, ], }) export class AdminDatabaseComponent implements OnInit { readonly sections = [ { id: "database", label: "Backup & Restore", icon: "database" }, { id: "users", label: "Benutzer", icon: "group" }, { id: "entries", label: "Alle Einträge", icon: "list_alt" }, { id: "watcher", label: "Ausbildungs-Watcher", icon: "travel_explore" }, ]; section = signal("database"); restoreFile = signal(null); busy = signal(false); users = signal([]); entries = signal([]); sources = signal([]); hits = signal([]); userColumns = ["email", "confirmed", "entries", "actions"]; entryColumns = ["date", "user", "art", "time", "actions"]; sourceDraft: any = { name: "", url: "", kind: "html", enabled: true }; newUser: any = { email: "", password: "", confirmed: true }; constructor( private api: ApiService, private snack: MatSnackBar, router: Router, ) { if (api.user() && !api.user()!.is_admin) router.navigate(["/dashboard"]); } ngOnInit() { this.reload(); } reload() { this.api.adminUsers().subscribe((v) => this.users.set(v)); this.api.adminEntries().subscribe((v) => this.entries.set(v)); this.api.trainingSources().subscribe((v) => this.sources.set(v)); this.api.trainingHits().subscribe((v) => this.hits.set(v)); } selectFile(event: Event) { this.restoreFile.set((event.target as HTMLInputElement).files?.[0] || null); } backup() { this.api.databaseBackup().subscribe((blob) => { const a = document.createElement("a"); a.href = URL.createObjectURL(blob); a.download = `praktikum-backup-${new Date().toISOString().slice(0, 10)}.zip`; a.click(); URL.revokeObjectURL(a.href); }); } restore() { const file = this.restoreFile(); if ( !file || !confirm( "Aktuelle Datenbank wirklich mit dieser Sicherung überschreiben?", ) ) return; this.busy.set(true); this.api.databaseRestore(file).subscribe({ next: (r) => { this.busy.set(false); this.snack.open(r.message, "OK"); this.reload(); }, error: (e) => { this.busy.set(false); this.snack.open(e.error?.error || "Restore fehlgeschlagen", "OK"); }, }); } setConfirmed(u: any, value: boolean) { this.api .updateAdminUser(u.id, { confirmed: value }) .subscribe(() => this.reload()); } createUser(){this.api.createAdminUser(this.newUser).subscribe({next:()=>{this.newUser={email:"",password:"",confirmed:true};this.reload();},error:e=>this.snack.open(e.error?.errors?.join(", ")||"Benutzer konnte nicht angelegt werden","OK")});} removeUser(u: any) { if (confirm(`${u.email} wirklich löschen?`)) this.api.deleteAdminUser(u.id).subscribe(() => this.reload()); } removeEntry(e: any) { if (confirm("Eintrag wirklich löschen?")) this.api.deleteAdminEntry(e.id).subscribe(() => this.reload()); } editSource(s: any) { this.sourceDraft = { ...s }; } saveSource() { this.api.saveTrainingSource(this.sourceDraft).subscribe(() => { this.sourceDraft = { name: "", url: "", kind: "html", enabled: true }; this.reload(); }); } removeSource(s: any) { if (confirm("Quelle und zugehörige Treffer löschen?")) this.api.deleteTrainingSource(s.id).subscribe(() => this.reload()); } removeHit(h: any) { this.api.deleteTrainingHit(h.id).subscribe(() => this.reload()); } runWatcher() { this.api.runTrainingWatch().subscribe((r) => { this.snack.open(`${r.new_hits} neue Treffer`, "OK"); this.reload(); }); } }