+
+
+
+
+
{{ authService.currentUserRole() === 'ADMIN' ? 'Verwaltung' : 'Mein Bereich' }}
-
- Rolle: {{ authService.currentUserRole() }}
-
+
+ Rolle: {{ authService.currentUserRole() }}
+
-
+
diff --git a/eeg_frontend/src/app/layout/dashboard-layout/dashboard-layout.ts b/eeg_frontend/src/app/layout/dashboard-layout/dashboard-layout.ts
index 4ef2ade..ea1d46a 100644
--- a/eeg_frontend/src/app/layout/dashboard-layout/dashboard-layout.ts
+++ b/eeg_frontend/src/app/layout/dashboard-layout/dashboard-layout.ts
@@ -1,4 +1,4 @@
-import {Component, computed, inject} from '@angular/core';
+import {Component, computed, inject, signal} from '@angular/core';
import {Router, RouterLink, RouterLinkActive, RouterOutlet} from '@angular/router';
import {AuthService} from '../../services/auth';
import {NAV_ITEMS} from './dashboard-nav-items';
@@ -14,11 +14,21 @@ export class DashboardLayout {
authService = inject(AuthService);
private router = inject(Router);
+ isMobileMenuOpen = signal(false);
+
visibleNavItems = computed(() => {
const role = this.authService.currentUserRole();
return NAV_ITEMS.filter(item => role !== null && item.roles.includes(role));
});
+ toggleMobileMenu() {
+ this.isMobileMenuOpen.update(v => !v);
+ }
+
+ closeMobileMenu() {
+ this.isMobileMenuOpen.set(false);
+ }
+
onLogout() {
this.authService.logout();
this.router.navigate(['/']);
diff --git a/eeg_frontend/src/app/pages/profile/profile.html b/eeg_frontend/src/app/pages/profile/profile.html
index b9464ef..d9a41b3 100644
--- a/eeg_frontend/src/app/pages/profile/profile.html
+++ b/eeg_frontend/src/app/pages/profile/profile.html
@@ -1,106 +1,115 @@
-
+
Mein Profil
@if (isLoading()) {
-
Lade Profil...
+
} @else if (profile()) {
-
- @if (successMessage()) {
-
- {{ successMessage() }}
-
- }
- @if (errorMessage()) {
-
- {{ errorMessage() }}
-
- }
-
Persönliche Daten
-
-
-
-
-
-
-
-
-
-
-
-
-
- @if (editParticipantType === 'COMPANY') {
+
-
-
-
{{ profile()!.email }}
-
+
+
+
{{ profile()!.email }}
+
-
+
+
Passwort ändern
-
-
-
+
+
E-Mail-Adresse ändern
-
-
-
-
+
}
diff --git a/eeg_frontend/src/app/pages/profile/profile.ts b/eeg_frontend/src/app/pages/profile/profile.ts
index 25e0c97..737dc8b 100644
--- a/eeg_frontend/src/app/pages/profile/profile.ts
+++ b/eeg_frontend/src/app/pages/profile/profile.ts
@@ -1,32 +1,39 @@
import {Component, inject, OnInit, signal} from '@angular/core';
-import {FormsModule} from '@angular/forms';
+import {ReactiveFormsModule, FormBuilder, FormGroup, Validators} from '@angular/forms';
import {UserService, UserProfile, UpdateProfileRequest} from '../../services/user';
+import {ToastService} from '../../services/toast';
@Component({
selector: 'app-profile',
standalone: true,
- imports: [FormsModule],
+ imports: [ReactiveFormsModule],
templateUrl: './profile.html'
})
export class ProfileComponent implements OnInit {
private userService = inject(UserService);
+ private fb = inject(FormBuilder);
+ private toastService = inject(ToastService);
profile = signal
(null);
isLoading = signal(true);
isSaving = signal(false);
- successMessage = signal('');
- errorMessage = signal('');
- editFirstName = '';
- editLastName = '';
- editOrganizationName = '';
- editParticipantType = 'PRIVATE';
+ profileForm: FormGroup = this.fb.group({
+ firstName: ['', [Validators.required, Validators.minLength(2)]],
+ lastName: ['', [Validators.required, Validators.minLength(2)]],
+ organizationName: [''],
+ participantType: ['PRIVATE']
+ });
- currentPassword = '';
- newPassword = '';
- newPasswordConfirm = '';
+ passwordForm: FormGroup = this.fb.group({
+ currentPassword: ['', [Validators.required]],
+ newPassword: ['', [Validators.required, Validators.minLength(8)]],
+ newPasswordConfirm: ['', [Validators.required]]
+ });
- newEmail = '';
+ emailForm: FormGroup = this.fb.group({
+ newEmail: ['', [Validators.required, Validators.email]]
+ });
ngOnInit() {
this.loadProfile();
@@ -36,10 +43,12 @@ export class ProfileComponent implements OnInit {
this.userService.getProfile().subscribe({
next: profile => {
this.profile.set(profile);
- this.editFirstName = profile.firstName || '';
- this.editLastName = profile.lastName || '';
- this.editOrganizationName = profile.organizationName || '';
- this.editParticipantType = profile.participantType;
+ this.profileForm.patchValue({
+ firstName: profile.firstName || '',
+ lastName: profile.lastName || '',
+ organizationName: profile.organizationName || '',
+ participantType: profile.participantType
+ });
this.isLoading.set(false);
},
error: () => this.isLoading.set(false)
@@ -47,76 +56,69 @@ export class ProfileComponent implements OnInit {
}
updateProfile() {
- if (this.isSaving()) return;
+ if (this.isSaving() || this.profileForm.invalid) return;
this.isSaving.set(true);
- this.successMessage.set('');
- this.errorMessage.set('');
- const request: UpdateProfileRequest = {
- firstName: this.editFirstName,
- lastName: this.editLastName,
- organizationName: this.editOrganizationName || undefined,
- participantType: this.editParticipantType as 'PRIVATE' | 'COMPANY'
- };
+ const request: UpdateProfileRequest = this.profileForm.value;
this.userService.updateProfile(request).subscribe({
next: profile => {
this.profile.set(profile);
this.isSaving.set(false);
- this.successMessage.set('Profil erfolgreich aktualisiert.');
+ this.toastService.success('Profil erfolgreich aktualisiert.');
},
error: (err) => {
this.isSaving.set(false);
- this.errorMessage.set(err.error?.message || 'Fehler beim Aktualisieren.');
+ this.toastService.error(err.error?.message || 'Fehler beim Aktualisieren.');
}
});
}
changePassword() {
- if (this.isSaving() || !this.currentPassword || !this.newPassword) return;
- if (this.newPassword !== this.newPasswordConfirm) {
- this.errorMessage.set('Passwörter stimmen nicht überein.');
+ if (this.isSaving() || this.passwordForm.invalid) return;
+
+ if (this.passwordForm.value.newPassword !== this.passwordForm.value.newPasswordConfirm) {
+ this.toastService.warning('Passwörter stimmen nicht überein.');
return;
}
this.isSaving.set(true);
- this.successMessage.set('');
- this.errorMessage.set('');
this.userService.changePassword({
- currentPassword: this.currentPassword,
- newPassword: this.newPassword
+ currentPassword: this.passwordForm.value.currentPassword,
+ newPassword: this.passwordForm.value.newPassword
}).subscribe({
next: () => {
this.isSaving.set(false);
- this.successMessage.set('Passwort erfolgreich geändert.');
- this.currentPassword = '';
- this.newPassword = '';
- this.newPasswordConfirm = '';
+ this.toastService.success('Passwort erfolgreich geändert.');
+ this.passwordForm.reset();
},
error: (err) => {
this.isSaving.set(false);
- this.errorMessage.set(err.error?.message || 'Fehler beim Ändern des Passworts.');
+ this.toastService.error(err.error?.message || 'Fehler beim Ändern des Passworts.');
}
});
}
changeEmail() {
- if (this.isSaving() || !this.newEmail) return;
+ if (this.isSaving() || this.emailForm.invalid) return;
this.isSaving.set(true);
- this.successMessage.set('');
- this.errorMessage.set('');
- this.userService.changeEmail({newEmail: this.newEmail}).subscribe({
+ this.userService.changeEmail({newEmail: this.emailForm.value.newEmail}).subscribe({
next: () => {
this.isSaving.set(false);
- this.successMessage.set('E-Mail-Adresse geändert. Bitte neue Adresse verifizieren.');
- this.newEmail = '';
+ this.toastService.success('E-Mail-Adresse geändert. Bitte neue Adresse verifizieren.');
+ this.emailForm.reset();
},
error: (err) => {
this.isSaving.set(false);
- this.errorMessage.set(err.error?.message || 'Fehler beim Ändern der E-Mail.');
+ this.toastService.error(err.error?.message || 'Fehler beim Ändern der E-Mail.');
}
});
}
+
+ isFieldInvalid(form: FormGroup, fieldName: string): boolean {
+ const field = form.get(fieldName);
+ return !!(field && field.invalid && (field.dirty || field.touched));
+ }
}
diff --git a/eeg_frontend/src/app/services/toast.ts b/eeg_frontend/src/app/services/toast.ts
new file mode 100644
index 0000000..6db456e
--- /dev/null
+++ b/eeg_frontend/src/app/services/toast.ts
@@ -0,0 +1,50 @@
+import { Injectable, signal } from '@angular/core';
+
+export interface Toast {
+ id: number;
+ message: string;
+ type: 'success' | 'error' | 'warning' | 'info';
+}
+
+@Injectable({ providedIn: 'root' })
+export class ToastService {
+ private toasts = signal([]);
+ private nextId = 0;
+
+ get toasts$() {
+ return this.toasts.asReadonly();
+ }
+
+ show(message: string, type: Toast['type'] = 'info', duration = 3000) {
+ const id = this.nextId++;
+ const toast: Toast = { id, message, type };
+
+ this.toasts.update(toasts => [...toasts, toast]);
+
+ if (duration > 0) {
+ setTimeout(() => this.dismiss(id), duration);
+ }
+
+ return id;
+ }
+
+ success(message: string, duration = 3000) {
+ return this.show(message, 'success', duration);
+ }
+
+ error(message: string, duration = 5000) {
+ return this.show(message, 'error', duration);
+ }
+
+ warning(message: string, duration = 4000) {
+ return this.show(message, 'warning', duration);
+ }
+
+ dismiss(id: number) {
+ this.toasts.update(toasts => toasts.filter(t => t.id !== id));
+ }
+
+ clear() {
+ this.toasts.set([]);
+ }
+}