Files
timetracker/timetracker.Client/Components/Pages/Profile.razor
T
2026-06-24 23:48:04 +02:00

281 lines
13 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
@page "/profile"
@rendermode InteractiveWebAssembly
@attribute [Authorize]
@using System.Security.Claims
@inject IAuthService AuthService
@inject ITimetrackerService TrackerService
@inject ISnackbar Snackbar
@inject AuthenticationStateProvider AuthStateProvider
@inject NavigationManager Nav
<PageTitle>Benutzerprofil Timetracker</PageTitle>
@if (_loading)
{
<MudStack AlignItems="AlignItems.Center" Class="mt-16">
<MudProgressCircular Color="Color.Primary" Indeterminate="true" Size="Size.Large" />
<MudText Color="Color.Secondary">Lade Profil…</MudText>
</MudStack>
}
else
{
<MudStack Spacing="4">
@* ── Header ── *@
<MudPaper Elevation="4" Class="pa-5 rounded-xl mud-theme-primary">
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="3">
<MudIcon Icon="@Icons.Material.Filled.AccountCircle" Style="color: var(--mud-palette-primary-text); font-size:2.5rem" />
<MudStack Spacing="0">
<MudText Typo="Typo.h5" Style="color: var(--mud-palette-primary-text); font-weight:700">Mein Profil</MudText>
<MudText Typo="Typo.caption" Style="color: var(--mud-palette-primary-text); opacity: 0.72;">
Persönliche Details verwalten und Account sichern
</MudText>
</MudStack>
</MudStack>
</MudPaper>
<MudGrid Spacing="4">
@* ── Übersicht & Details ── *@
<MudItem xs="12" md="4">
<MudCard Elevation="3" Class="rounded-xl h-100">
<MudCardContent>
<MudStack AlignItems="AlignItems.Center" Class="py-6" Spacing="3">
<MudAvatar Style="width:96px; height:96px; font-size:2rem; font-weight:700; background: linear-gradient(135deg, #0EA5E9 0%, #0284C7 100%); color: white;">
@GetInitials(_username)
</MudAvatar>
<MudStack AlignItems="AlignItems.Center" Spacing="0">
<MudText Typo="Typo.h6" Style="font-weight:700">@_username</MudText>
<MudText Typo="Typo.caption" Color="Color.Secondary">@(_isAdmin ? "Administrator" : "Standardbenutzer")</MudText>
</MudStack>
</MudStack>
<MudDivider Class="my-3" />
<MudStack Spacing="2" Class="pa-2">
<MudStack Row="true" Justify="Justify.SpaceBetween">
<MudText Typo="Typo.body2" Color="Color.Secondary">Benutzer-ID</MudText>
<MudText Typo="Typo.body2"><b>#@_userId</b></MudText>
</MudStack>
<MudStack Row="true" Justify="Justify.SpaceBetween">
<MudText Typo="Typo.body2" Color="Color.Secondary">Gleitzeit-Saldo</MudText>
<MudText Typo="Typo.body2" Color="@(_overtime >= TimeSpan.Zero ? Color.Success : Color.Warning)">
<b>@FormatTs(_overtime, sign: true)</b>
</MudText>
</MudStack>
<MudStack Row="true" Justify="Justify.SpaceBetween">
<MudText Typo="Typo.body2" Color="Color.Secondary">Sollstunden / Tag</MudText>
<MudText Typo="Typo.body2"><b>@_settings.DailyTargetHours h</b></MudText>
</MudStack>
</MudStack>
</MudCardContent>
</MudCard>
</MudItem>
@* ── Einstellungen verwalten ── *@
<MudItem xs="12" md="8">
<MudStack Spacing="4">
@* Benutzername ändern *@
<MudCard Elevation="3" Class="rounded-xl">
<MudCardHeader>
<CardHeaderContent>
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">
<MudIcon Icon="@Icons.Material.Filled.Badge" Color="Color.Primary" />
<MudText Typo="Typo.h6" Style="font-weight:600">Benutzername ändern</MudText>
</MudStack>
</CardHeaderContent>
</MudCardHeader>
<MudCardContent>
<MudForm @ref="_renameForm" @bind-IsValid="_renameIsValid">
<MudStack Spacing="3">
<MudTextField @bind-Value="_newUsername"
Label="Neuer Benutzername"
Variant="Variant.Outlined"
Required="true"
RequiredError="Bitte gib einen Benutzernamen ein."
Validation="@(new Func<string, string?>(ValidateUsername))" />
<MudButton Variant="Variant.Filled"
Color="Color.Primary"
OnClick="HandleRename"
Disabled="!_renameIsValid || _newUsername == _username"
Class="align-self-start rounded-lg py-2 px-4"
StartIcon="@Icons.Material.Filled.Save">
Speichern
</MudButton>
</MudStack>
</MudForm>
</MudCardContent>
</MudCard>
@* Passwort ändern *@
<MudCard Elevation="3" Class="rounded-xl">
<MudCardHeader>
<CardHeaderContent>
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">
<MudIcon Icon="@Icons.Material.Filled.Lock" Color="Color.Primary" />
<MudText Typo="Typo.h6" Style="font-weight:600">Passwort ändern</MudText>
</MudStack>
</CardHeaderContent>
</MudCardHeader>
<MudCardContent>
<MudForm @ref="_passwordForm" @bind-IsValid="_passwordIsValid">
<MudStack Spacing="3">
<MudTextField @bind-Value="_currentPassword"
Label="Aktuelles Passwort"
InputType="InputType.Password"
Variant="Variant.Outlined"
Required="true"
RequiredError="Bitte gib dein aktuelles Passwort ein." />
<MudTextField @bind-Value="_newPassword"
Label="Neues Passwort"
InputType="InputType.Password"
Variant="Variant.Outlined"
Required="true"
RequiredError="Bitte gib dein neues Passwort ein."
Validation="@(new Func<string, string?>(ValidatePassword))" />
<MudTextField @bind-Value="_confirmPassword"
Label="Neues Passwort bestätigen"
InputType="InputType.Password"
Variant="Variant.Outlined"
Required="true"
RequiredError="Bitte bestätige dein neues Passwort."
Validation="@(new Func<string, string?>(ValidateConfirmPassword))" />
<MudButton Variant="Variant.Filled"
Color="Color.Primary"
OnClick="HandleChangePassword"
Disabled="!_passwordIsValid"
Class="align-self-start rounded-lg py-2 px-4"
StartIcon="@Icons.Material.Filled.VpnKey">
Passwort ändern
</MudButton>
</MudStack>
</MudForm>
</MudCardContent>
</MudCard>
</MudStack>
</MudItem>
</MudGrid>
</MudStack>
}
@code {
private bool _loading = true;
private int _userId;
private string _username = "";
private string _newUsername = "";
private bool _isAdmin;
private AppSettings _settings = new();
private TimeSpan _overtime;
// Formulare
private MudForm? _renameForm;
private bool _renameIsValid;
private MudForm? _passwordForm;
private bool _passwordIsValid;
// Passwörter
private string _currentPassword = "";
private string _newPassword = "";
private string _confirmPassword = "";
protected override async Task OnInitializedAsync()
{
var authState = await AuthStateProvider.GetAuthenticationStateAsync();
var claim = authState.User.FindFirst(ClaimTypes.NameIdentifier);
if (claim == null) return;
_userId = int.Parse(claim.Value);
_username = authState.User.Identity?.Name ?? "";
_newUsername = _username;
_isAdmin = _username.Equals("marc", StringComparison.OrdinalIgnoreCase);
_settings = await TrackerService.GetSettingsAsync(_userId);
_overtime = await TrackerService.GetTotalOvertimeAsync(_userId, _settings);
_loading = false;
}
private string GetInitials(string? name)
{
if (string.IsNullOrWhiteSpace(name)) return "U";
var parts = name.Split(' ', StringSplitOptions.RemoveEmptyEntries);
if (parts.Length == 1) return parts[0][..Math.Min(2, parts[0].Length)].ToUpper();
return $"{parts[0][0]}{parts[1][0]}".ToUpper();
}
private static string FormatTs(TimeSpan ts, bool sign = false)
{
if (ts == TimeSpan.Zero && sign) return "±0:00";
var prefix = sign ? (ts >= TimeSpan.Zero ? "+" : "") : (ts < TimeSpan.Zero ? "" : "");
var abs = ts.Duration();
return $"{prefix}{(int)abs.TotalHours}:{abs.Minutes:D2}";
}
// Validierungen
private string? ValidateUsername(string val)
{
if (string.IsNullOrWhiteSpace(val) || val.Length < 3)
return "Benutzername muss mindestens 3 Zeichen lang sein.";
return null;
}
private string? ValidatePassword(string val)
{
if (string.IsNullOrWhiteSpace(val) || val.Length < 6)
return "Passwort muss mindestens 6 Zeichen lang sein.";
return null;
}
private string? ValidateConfirmPassword(string val)
{
if (val != _newPassword)
return "Die Passwörter stimmen nicht überein.";
return null;
}
// Aktionen
private async Task HandleRename()
{
if (_renameForm == null) return;
await _renameForm.ValidateAsync();
if (!_renameForm.IsValid) return;
var error = await AuthService.RenameUserAsync(_userId, _newUsername);
if (error != null)
{
Snackbar.Add(error, Severity.Error);
}
else
{
Snackbar.Add("Benutzername erfolgreich geändert. Bitte melde dich ggf. neu an, um alle Anzeigen zu aktualisieren.", Severity.Success);
_username = _newUsername;
// Umleitung zur Startseite, um die neue Identity zu laden
Nav.NavigateTo("/", forceLoad: true);
}
}
private async Task HandleChangePassword()
{
if (_passwordForm == null) return;
await _passwordForm.ValidateAsync();
if (!_passwordForm.IsValid) return;
var error = await AuthService.ChangePasswordAsync(_userId, _currentPassword, _newPassword);
if (error != null)
{
Snackbar.Add(error, Severity.Error);
}
else
{
Snackbar.Add("Passwort erfolgreich geändert.", Severity.Success);
_currentPassword = "";
_newPassword = "";
_confirmPassword = "";
if (_passwordForm != null) await _passwordForm.ResetAsync();
}
}
}