This commit is contained in:
Bastian Wagner
2026-06-29 14:34:34 +02:00
parent ecb902565d
commit 9a721f9e86
18 changed files with 884 additions and 7 deletions

View File

@@ -17,6 +17,44 @@
{{ auth.user()?.onboardingCompleted ? 'Onboarding abgeschlossen' : 'Onboarding offen' }}
</span>
</div>
<section class="settings-section" aria-label="Task-Mail Einstellungen">
<div class="settings-heading">
<mat-icon aria-hidden="true">mark_email_unread</mat-icon>
<div>
<h2>Task-Mail</h2>
<p>Offene Tasks von heute und ueberfaellige Tasks.</p>
</div>
</div>
<mat-form-field appearance="outline">
<mat-label>Taegliche Benachrichtigung</mat-label>
<mat-select
[value]="auth.user()?.taskDigestPreference ?? 'both'"
[disabled]="savingTaskDigestPreference"
(selectionChange)="updateTaskDigestPreference($event.value)"
>
@for (option of taskDigestPreferenceOptions; track option.value) {
<mat-option [value]="option.value">
{{ option.label }}
</mat-option>
}
</mat-select>
</mat-form-field>
@for (option of taskDigestPreferenceOptions; track option.value) {
@if ((auth.user()?.taskDigestPreference ?? 'both') === option.value) {
<p class="settings-description">{{ option.description }}</p>
}
}
@if (savingTaskDigestPreference) {
<div class="saving-row">
<mat-progress-spinner mode="indeterminate" diameter="18" />
<span>Speichert...</span>
</div>
}
</section>
</mat-card-content>
<mat-card-actions align="end">

View File

@@ -30,6 +30,56 @@
color: var(--mat-sys-primary);
}
.settings-section {
display: grid;
gap: 0.8rem;
margin-top: 1rem;
padding: 0.9rem;
border: 1px solid color-mix(in srgb, var(--mat-sys-outline-variant) 72%, transparent);
border-radius: 8px;
background: color-mix(in srgb, var(--mat-sys-surface-container-low) 36%, var(--mat-sys-surface));
}
.settings-heading {
display: flex;
gap: 0.75rem;
min-width: 0;
}
.settings-heading mat-icon {
flex: 0 0 auto;
color: var(--mat-sys-primary);
}
.settings-heading h2,
.settings-heading p,
.settings-description {
margin: 0;
}
.settings-heading h2 {
font-size: 1rem;
font-weight: 600;
}
.settings-heading p,
.settings-description {
color: var(--mat-sys-on-surface-variant);
line-height: 1.45;
}
.settings-section mat-form-field {
width: 100%;
}
.saving-row {
display: inline-flex;
align-items: center;
gap: 0.5rem;
color: var(--mat-sys-on-surface-variant);
font-size: 0.9rem;
}
mat-card-actions {
flex-wrap: wrap;
gap: 0.5rem;

View File

@@ -4,12 +4,24 @@ import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
import { MatIconModule } from '@angular/material/icon';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { MatSelectModule } from '@angular/material/select';
import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar';
import { finalize } from 'rxjs';
import { getAuthErrorMessage } from '../auth/error-message';
import { AuthService } from '../auth/auth.service';
import { TaskDigestPreference } from '../auth/auth.models';
import { OnboardingService } from '../onboarding/onboarding.service';
@Component({
selector: 'app-account',
imports: [MatButtonModule, MatCardModule, MatIconModule, MatProgressSpinnerModule],
imports: [
MatButtonModule,
MatCardModule,
MatIconModule,
MatProgressSpinnerModule,
MatSelectModule,
MatSnackBarModule,
],
templateUrl: './account.component.html',
styleUrl: './account.component.scss',
})
@@ -17,11 +29,57 @@ export class AccountComponent {
protected readonly auth = inject(AuthService);
protected readonly onboarding = inject(OnboardingService);
private readonly router = inject(Router);
private readonly snackBar = inject(MatSnackBar);
protected savingTaskDigestPreference = false;
protected readonly taskDigestPreferenceOptions: ReadonlyArray<{
value: TaskDigestPreference;
label: string;
description: string;
}> = [
{
value: 'both',
label: 'Morgens und nachmittags',
description: 'E-Mail um 9:00 und 15:00 Uhr, wenn Tasks offen sind.',
},
{
value: 'morning',
label: 'Nur morgens',
description: 'E-Mail um 9:00 Uhr, wenn Tasks offen sind.',
},
{
value: 'none',
label: 'Keine Task-Mail',
description: 'Es werden keine taeglichen Task-Mails gesendet.',
},
];
resetOnboarding(): void {
this.onboarding.resetForCurrentUser();
}
updateTaskDigestPreference(preference: TaskDigestPreference): void {
if (this.savingTaskDigestPreference || preference === this.auth.user()?.taskDigestPreference) {
return;
}
this.savingTaskDigestPreference = true;
this.auth
.updateTaskDigestPreference(preference)
.pipe(finalize(() => (this.savingTaskDigestPreference = false)))
.subscribe({
next: () => {
this.snackBar.open('Task-Mail-Einstellung gespeichert.', 'OK', {
duration: 3000,
});
},
error: (error: unknown) => {
this.snackBar.open(getAuthErrorMessage(error), 'OK', {
duration: 5000,
});
},
});
}
logout(): void {
this.auth.logout();
void this.router.navigateByUrl('/login');

View File

@@ -1,9 +1,12 @@
export type TaskDigestPreference = 'none' | 'morning' | 'both';
export interface PublicUser {
id: string;
email: string;
name?: string;
verified: boolean;
onboardingCompleted: boolean;
taskDigestPreference: TaskDigestPreference;
}
export interface PublicUserSearchResult {

View File

@@ -9,6 +9,7 @@ import {
RegisterRequest,
RegisterResponse,
ResendVerificationResponse,
TaskDigestPreference,
VerifyEmailResponse,
} from './auth.models';
@@ -64,6 +65,12 @@ export class AuthService {
.pipe(tap((user) => this.storeUser(user)));
}
updateTaskDigestPreference(preference: TaskDigestPreference): Observable<PublicUser> {
return this.http
.patch<PublicUser>(`${this.apiUrl}/me/task-digest`, { preference })
.pipe(tap((user) => this.storeUser(user)));
}
accessToken(): string | null {
return this.storage?.getItem(ACCESS_TOKEN_KEY) ?? null;
}