diff --git a/eeg_frontend/src/app/pages/admin-user-approval/admin-user-approval.html b/eeg_frontend/src/app/pages/admin-user-approval/admin-user-approval.html
index 262b6b6..08921d0 100644
--- a/eeg_frontend/src/app/pages/admin-user-approval/admin-user-approval.html
+++ b/eeg_frontend/src/app/pages/admin-user-approval/admin-user-approval.html
@@ -4,6 +4,36 @@
Prüfung der ausstehenden Anmeldungen und EDA-Netz-Checks.
+
+
{{ errorMessage() }}
@@ -12,60 +42,126 @@
@if (isLoading()) {
-
Lade ausstehende Anträge...
+
Lade Daten...
}
- @if (!isLoading() && pendingUsers().length === 0) {
-
-
Aktuell gibt es keine neuen User im Status PENDING.
-
- }
-
- @if (!isLoading() && pendingUsers().length > 0) {
-
-
-
-
-
- | Benutzer |
- Status |
- Status |
- Aktionen |
-
-
-
- @for (user of pendingUsers(); track user.id) {
-
-
- |
- {{ user.firstName }} {{ user.lastName }}
- {{ user.email }}
- |
-
-
-
- {{ user.status }}
-
- |
-
-
-
-
-
- |
-
-
- }
-
-
+ @if (!isLoading() && activeTab() === 'pending') {
+ @if (pendingUsers().length === 0) {
+
+
Aktuell gibt es keine neuen User im Status PENDING.
-
+ } @else {
+
+
+
+
+
+ |
+ Benutzer
+ |
+
+ Status
+ |
+
+ Aktionen
+ |
+
+
+
+ @for (user of pendingUsers(); track user.id) {
+
+ |
+
+ {{ user.firstName }} {{ user.lastName }}
+
+ {{ user.email }}
+ |
+
+
+ {{ getStatusLabel(user.status || 'PENDING') }}
+
+ |
+
+
+
+ |
+
+ }
+
+
+
+
+ }
+ }
+
+ @if (!isLoading() && activeTab() === 'all') {
+ @if (allUsers().length === 0) {
+
+
Keine Benutzer vorhanden.
+
+ } @else {
+
+
+
+
+
+ |
+ Benutzer
+ |
+
+ Status
+ |
+
+ Rolle
+ |
+
+
+
+ @for (user of allUsers(); track user.id) {
+
+ |
+
+ {{ user.firstName }} {{ user.lastName }}
+
+ {{ user.email }}
+ |
+
+
+ {{ getStatusLabel(user.status || 'UNKNOWN') }}
+
+ |
+
+ {{ user.participantType === 'COMPANY' ? 'Unternehmen' : 'Privatperson' }}
+ |
+
+ }
+
+
+
+
+ }
}
diff --git a/eeg_frontend/src/app/pages/admin-user-approval/admin-user-approval.spec.ts b/eeg_frontend/src/app/pages/admin-user-approval/admin-user-approval.spec.ts
index 95b1f7c..827f08d 100644
--- a/eeg_frontend/src/app/pages/admin-user-approval/admin-user-approval.spec.ts
+++ b/eeg_frontend/src/app/pages/admin-user-approval/admin-user-approval.spec.ts
@@ -1,22 +1,230 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
-
+import { provideHttpClient } from '@angular/common/http';
+import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing';
+import { vi } from 'vitest';
import { AdminUserApprovalComponent } from './admin-user-approval';
+import { UserProfileResponse } from '../../api';
-describe('AdminUserApproval', () => {
+describe('AdminUserApprovalComponent', () => {
let component: AdminUserApprovalComponent;
let fixture: ComponentFixture
;
+ let httpMock: HttpTestingController;
+
+ const mockPendingUser: UserProfileResponse = {
+ id: '1',
+ firstName: 'Max',
+ lastName: 'Mustermann',
+ email: 'max@test.at',
+ status: 'PENDING',
+ participantType: UserProfileResponse.ParticipantTypeEnum.Private,
+ };
+
+ const mockApprovedUser: UserProfileResponse = {
+ id: '2',
+ firstName: 'Anna',
+ lastName: 'Schmidt',
+ email: 'anna@test.at',
+ status: 'APPROVED',
+ participantType: UserProfileResponse.ParticipantTypeEnum.Company,
+ };
+
+ const mockRejectedUser: UserProfileResponse = {
+ id: '3',
+ firstName: 'Tom',
+ lastName: 'Berger',
+ email: 'tom@test.at',
+ status: 'REJECTED',
+ participantType: UserProfileResponse.ParticipantTypeEnum.Private,
+ };
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [AdminUserApprovalComponent],
+ providers: [provideHttpClient(), provideHttpClientTesting()],
}).compileComponents();
fixture = TestBed.createComponent(AdminUserApprovalComponent);
component = fixture.componentInstance;
- await fixture.whenStable();
+ httpMock = TestBed.inject(HttpTestingController);
+ });
+
+ afterEach(() => {
+ httpMock.verify();
});
it('should create', () => {
+ fixture.detectChanges();
+ const req = httpMock.expectOne((r) => r.url.includes('/admin/users/pending'));
+ req.flush([]);
expect(component).toBeTruthy();
});
+
+ it('should default activeTab to pending', () => {
+ fixture.detectChanges();
+ const req = httpMock.expectOne((r) => r.url.includes('/admin/users/pending'));
+ req.flush([]);
+ expect(component.activeTab()).toBe('pending');
+ });
+
+ it('should initialize allUsers as empty array', () => {
+ fixture.detectChanges();
+ const req = httpMock.expectOne((r) => r.url.includes('/admin/users/pending'));
+ req.flush([]);
+ expect(component.allUsers()).toEqual([]);
+ });
+
+ it('should load pending users on init', () => {
+ fixture.detectChanges();
+ const req = httpMock.expectOne((r) => r.url.includes('/admin/users/pending'));
+ expect(req.request.method).toBe('GET');
+ req.flush([mockPendingUser]);
+ expect(component.pendingUsers().length).toBe(1);
+ expect(component.pendingUsers()[0].firstName).toBe('Max');
+ });
+
+ it('should load all users when switching to all tab', () => {
+ fixture.detectChanges();
+ const pendingReq = httpMock.expectOne((r) => r.url.includes('/admin/users/pending'));
+ pendingReq.flush([]);
+
+ component.switchTab('all');
+
+ const allReq = httpMock.expectOne(
+ (r) => r.url.includes('/admin/users') && !r.url.includes('/pending'),
+ );
+ expect(allReq.request.method).toBe('GET');
+ allReq.flush([mockPendingUser, mockApprovedUser, mockRejectedUser]);
+
+ expect(component.allUsers().length).toBe(3);
+ });
+
+ it('should switch activeTab signal', () => {
+ fixture.detectChanges();
+ const req = httpMock.expectOne((r) => r.url.includes('/admin/users/pending'));
+ req.flush([]);
+
+ component.switchTab('all');
+ const allReq = httpMock.expectOne(
+ (r) => r.url.includes('/admin/users') && !r.url.includes('/pending'),
+ );
+ allReq.flush([]);
+ expect(component.activeTab()).toBe('all');
+
+ component.switchTab('pending');
+ expect(component.activeTab()).toBe('pending');
+ });
+
+ describe('getStatusLabel', () => {
+ it('should return "Ausstehend" for PENDING', () => {
+ expect(component.getStatusLabel('PENDING')).toBe('Ausstehend');
+ });
+
+ it('should return "Aktiv" for ACTIVE', () => {
+ expect(component.getStatusLabel('ACTIVE')).toBe('Aktiv');
+ });
+
+ it('should return "Freigeschaltet" for APPROVED', () => {
+ expect(component.getStatusLabel('APPROVED')).toBe('Freigeschaltet');
+ });
+
+ it('should return "Inaktiv" for INACTIVE', () => {
+ expect(component.getStatusLabel('INACTIVE')).toBe('Inaktiv');
+ });
+
+ it('should return "Abgelehnt" for REJECTED', () => {
+ expect(component.getStatusLabel('REJECTED')).toBe('Abgelehnt');
+ });
+
+ it('should return the original status for unknown values', () => {
+ expect(component.getStatusLabel('UNKNOWN')).toBe('UNKNOWN');
+ });
+ });
+
+ describe('getStatusBadgeClass', () => {
+ it('should return yellow badge for PENDING', () => {
+ expect(component.getStatusBadgeClass('PENDING')).toContain('yellow');
+ });
+
+ it('should return green badge for APPROVED', () => {
+ expect(component.getStatusBadgeClass('APPROVED')).toContain('green');
+ });
+
+ it('should return green badge for ACTIVE', () => {
+ expect(component.getStatusBadgeClass('ACTIVE')).toContain('green');
+ });
+
+ it('should return red badge for REJECTED', () => {
+ expect(component.getStatusBadgeClass('REJECTED')).toContain('red');
+ });
+
+ it('should return red badge for INACTIVE', () => {
+ expect(component.getStatusBadgeClass('INACTIVE')).toContain('red');
+ });
+ });
+
+ it('should approve user and remove from pending list', () => {
+ fixture.detectChanges();
+ const pendingReq = httpMock.expectOne((r) => r.url.includes('/admin/users/pending'));
+ pendingReq.flush([mockPendingUser]);
+
+ vi.spyOn(window, 'confirm').mockReturnValue(true);
+ component.approveUser(mockPendingUser.id);
+
+ const approveReq = httpMock.expectOne((r) => r.url.includes('/approve'));
+ approveReq.flush({});
+ expect(component.pendingUsers().length).toBe(0);
+ });
+
+ it('should reject user and remove from pending list', () => {
+ fixture.detectChanges();
+ const pendingReq = httpMock.expectOne((r) => r.url.includes('/admin/users/pending'));
+ pendingReq.flush([mockPendingUser]);
+
+ vi.spyOn(window, 'confirm').mockReturnValue(true);
+ component.rejectUser(mockPendingUser);
+
+ const rejectReq = httpMock.expectOne((r) => r.url.includes('/reject'));
+ rejectReq.flush({});
+ expect(component.pendingUsers().length).toBe(0);
+ });
+
+ describe('template', () => {
+ it('should render tab navigation', () => {
+ fixture.detectChanges();
+ const req = httpMock.expectOne((r) => r.url.includes('/admin/users/pending'));
+ req.flush([]);
+ fixture.detectChanges();
+
+ const tabs = fixture.nativeElement.querySelectorAll('[role="tab"]');
+ expect(tabs.length).toBe(2);
+ expect(tabs[0].textContent).toContain('Ausstehende Anträge');
+ expect(tabs[1].textContent).toContain('Alle Benutzer');
+ });
+
+ it('should show pending tab as active by default', () => {
+ fixture.detectChanges();
+ const req = httpMock.expectOne((r) => r.url.includes('/admin/users/pending'));
+ req.flush([]);
+ fixture.detectChanges();
+
+ const activeTab = fixture.nativeElement.querySelector('[role="tab"][aria-selected="true"]');
+ expect(activeTab?.textContent).toContain('Ausstehende Anträge');
+ });
+
+ it('should show only one Status column header (no duplicate)', () => {
+ fixture.detectChanges();
+ const req = httpMock.expectOne((r) => r.url.includes('/admin/users/pending'));
+ req.flush([mockPendingUser]);
+ fixture.detectChanges();
+
+ const allHeaders = fixture.nativeElement.querySelectorAll('th');
+ let statusCount = 0;
+ for (let i = 0; i < allHeaders.length; i++) {
+ if (allHeaders[i].textContent?.trim() === 'Status') {
+ statusCount++;
+ }
+ }
+ expect(statusCount).toBe(1);
+ });
+ });
});
diff --git a/eeg_frontend/src/app/pages/admin-user-approval/admin-user-approval.ts b/eeg_frontend/src/app/pages/admin-user-approval/admin-user-approval.ts
index ca0bcc4..6e5faa2 100644
--- a/eeg_frontend/src/app/pages/admin-user-approval/admin-user-approval.ts
+++ b/eeg_frontend/src/app/pages/admin-user-approval/admin-user-approval.ts
@@ -1,18 +1,19 @@
-import {Component, DestroyRef, inject, OnInit, signal} from '@angular/core';
-import {takeUntilDestroyed} from '@angular/core/rxjs-interop';
-import {AdminIAMService, UserProfileResponse} from '../../api';
-import {ToastService} from '../../services/toast';
+import { Component, DestroyRef, inject, OnInit, signal } from '@angular/core';
+import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
+import { AdminIAMService, UserProfileResponse } from '../../api';
+import { ToastService } from '../../services/toast';
@Component({
selector: 'app-admin-user-approval',
standalone: true,
imports: [],
templateUrl: './admin-user-approval.html',
- styleUrls: ['./admin-user-approval.scss']
+ styleUrls: ['./admin-user-approval.scss'],
})
export class AdminUserApprovalComponent implements OnInit {
- // Signals für reaktives State-Management (perfekt für den Zoneless-Modus)
+ public readonly activeTab = signal<'pending' | 'all'>('pending');
public readonly pendingUsers = signal([]);
+ public readonly allUsers = signal([]);
public readonly isLoading = signal(true);
public readonly errorMessage = signal('');
@@ -24,11 +25,53 @@ export class AdminUserApprovalComponent implements OnInit {
this.loadPendingUsers();
}
+ public switchTab(tab: 'pending' | 'all'): void {
+ this.activeTab.set(tab);
+ if (tab === 'all' && this.allUsers().length === 0) {
+ this.loadAllUsers();
+ }
+ }
+
+ public getStatusLabel(status: string): string {
+ switch (status) {
+ case 'PENDING':
+ return 'Ausstehend';
+ case 'ACTIVE':
+ return 'Aktiv';
+ case 'APPROVED':
+ return 'Freigeschaltet';
+ case 'INACTIVE':
+ return 'Inaktiv';
+ case 'REJECTED':
+ return 'Abgelehnt';
+ default:
+ return status;
+ }
+ }
+
+ public getStatusBadgeClass(status: string): string {
+ switch (status) {
+ case 'PENDING':
+ return 'bg-yellow-100 text-yellow-800';
+ case 'ACTIVE':
+ return 'bg-green-100 text-green-800';
+ case 'APPROVED':
+ return 'bg-green-100 text-green-800';
+ case 'REJECTED':
+ return 'bg-red-100 text-red-800';
+ case 'INACTIVE':
+ return 'bg-red-100 text-red-800';
+ default:
+ return 'bg-gray-100 text-gray-800';
+ }
+ }
+
private loadPendingUsers(): void {
this.isLoading.set(true);
this.errorMessage.set('');
- this.adminService.getPendingUsers()
+ this.adminService
+ .getPendingUsers()
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe({
next: (users: UserProfileResponse[]) => {
@@ -38,7 +81,26 @@ export class AdminUserApprovalComponent implements OnInit {
error: () => {
this.errorMessage.set('Fehler beim Laden der Daten aus dem EDA-Netz-Check.');
this.isLoading.set(false);
- }
+ },
+ });
+ }
+
+ private loadAllUsers(): void {
+ this.isLoading.set(true);
+ this.errorMessage.set('');
+
+ this.adminService
+ .getAllUsers()
+ .pipe(takeUntilDestroyed(this.destroyRef))
+ .subscribe({
+ next: (users: UserProfileResponse[]) => {
+ this.allUsers.set(users);
+ this.isLoading.set(false);
+ },
+ error: () => {
+ this.errorMessage.set('Fehler beim Laden aller Benutzer.');
+ this.isLoading.set(false);
+ },
});
}
@@ -46,29 +108,34 @@ export class AdminUserApprovalComponent implements OnInit {
if (!userId) return;
this.errorMessage.set('');
- this.adminService.approveUser(userId)
+ this.adminService
+ .approveUser(userId)
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe({
next: () => {
- this.pendingUsers.update(users => users.filter(user => user.id !== userId));
+ this.pendingUsers.update((users) => users.filter((user) => user.id !== userId));
},
error: () => {
this.errorMessage.set('Der User konnte nicht freigegeben werden.');
- }
+ },
});
}
public rejectUser(user: UserProfileResponse): void {
if (!user.id) return;
- if (confirm(`Möchtest du die Registrierung von ${user.firstName} ${user.lastName} wirklich ablehnen?`)) {
+ if (
+ confirm(
+ `Möchtest du die Registrierung von ${user.firstName} ${user.lastName} wirklich ablehnen?`,
+ )
+ ) {
this.adminService.rejectUser(user.id).subscribe({
next: () => {
- this.pendingUsers.update(users => users.filter(u => u.id !== user.id));
+ this.pendingUsers.update((users) => users.filter((u) => u.id !== user.id));
},
error: (err) => {
this.toastService.error(err.error?.message || 'Der User konnte nicht abgelehnt werden.');
- }
+ },
});
}
}