mails
This commit is contained in:
@@ -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">
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user