diff --git a/timetracker.Client/Components/Layout/MainLayout.razor b/timetracker.Client/Components/Layout/MainLayout.razor index ad1a0a7..5188fb9 100644 --- a/timetracker.Client/Components/Layout/MainLayout.razor +++ b/timetracker.Client/Components/Layout/MainLayout.razor @@ -48,6 +48,9 @@ @code { + [PersistentState(AllowUpdates = true)] + public TenantInfo? PersistentTenantInfo { get; set; } + private bool _drawerOpen = true; private bool _isDarkMode; @@ -55,44 +58,60 @@ { UserNotificationService.OnUserDeleted += HandleUserDeleted; - try + if (PersistentTenantInfo != null) { - var tenantInfo = await Http.GetFromJsonAsync("api/tenant/current"); - if (tenantInfo?.IsTenant == true && !string.IsNullOrEmpty(tenantInfo.PrimaryColor)) + ApplyTheme(PersistentTenantInfo); + } + else + { + try { - var primaryColor = tenantInfo.PrimaryColor; - var secondaryColor = tenantInfo.SecondaryColor ?? tenantInfo.PrimaryColor; - - _theme.PaletteLight.Primary = primaryColor; - _theme.PaletteLight.Secondary = secondaryColor; - _theme.PaletteDark.Primary = primaryColor; - _theme.PaletteDark.Secondary = secondaryColor; - - // Dynamically apply background colors to navbar - _theme.PaletteLight.AppbarBackground = primaryColor; - _theme.PaletteLight.DrawerBackground = primaryColor; - _theme.PaletteDark.AppbarBackground = primaryColor; - _theme.PaletteDark.DrawerBackground = primaryColor; - - // Adjust text/icon contrast based on background luminance - var isLight = IsLightColor(primaryColor); - var textCol = isLight ? "#0F172A" : "#FFFFFF"; - var subTextCol = isLight ? "rgba(15, 23, 42, 0.70)" : "rgba(255, 255, 255, 0.70)"; - - _theme.PaletteLight.AppbarText = textCol; - _theme.PaletteLight.DrawerText = subTextCol; - _theme.PaletteLight.DrawerIcon = subTextCol; - _theme.PaletteLight.PrimaryContrastText = textCol; - - _theme.PaletteDark.AppbarText = textCol; - _theme.PaletteDark.DrawerText = subTextCol; - _theme.PaletteDark.DrawerIcon = subTextCol; - _theme.PaletteDark.PrimaryContrastText = textCol; + var tenantInfo = await Http.GetFromJsonAsync("api/tenant/current"); + if (tenantInfo != null) + { + ApplyTheme(tenantInfo); + PersistentTenantInfo = tenantInfo; + } + } + catch + { + // Ignore background error } } - catch + } + + private void ApplyTheme(TenantInfo tenantInfo) + { + if (tenantInfo.IsTenant && !string.IsNullOrEmpty(tenantInfo.PrimaryColor)) { - // Ignore background error + var primaryColor = tenantInfo.PrimaryColor; + var secondaryColor = tenantInfo.SecondaryColor ?? tenantInfo.PrimaryColor; + + _theme.PaletteLight.Primary = primaryColor; + _theme.PaletteLight.Secondary = secondaryColor; + _theme.PaletteDark.Primary = primaryColor; + _theme.PaletteDark.Secondary = secondaryColor; + + // Dynamically apply background colors to navbar + _theme.PaletteLight.AppbarBackground = primaryColor; + _theme.PaletteLight.DrawerBackground = primaryColor; + _theme.PaletteDark.AppbarBackground = primaryColor; + _theme.PaletteDark.DrawerBackground = primaryColor; + + // Adjust text/icon contrast based on background luminance + var isLight = IsLightColor(primaryColor); + var textCol = isLight ? "#0F172A" : "#FFFFFF"; + var subTextCol = isLight ? "rgba(15, 23, 42, 0.70)" : "rgba(255, 255, 255, 0.70)"; + + _theme.PaletteLight.AppbarText = textCol; + _theme.PaletteLight.DrawerText = subTextCol; + _theme.PaletteLight.DrawerIcon = subTextCol; + _theme.PaletteLight.PrimaryContrastText = textCol; + + _theme.PaletteDark.AppbarText = textCol; + _theme.PaletteDark.DrawerText = subTextCol; + _theme.PaletteDark.DrawerIcon = subTextCol; + _theme.PaletteDark.PrimaryContrastText = textCol; } } diff --git a/timetracker.Client/Components/Layout/NavMenu.razor b/timetracker.Client/Components/Layout/NavMenu.razor index de9ac32..744c205 100644 --- a/timetracker.Client/Components/Layout/NavMenu.razor +++ b/timetracker.Client/Components/Layout/NavMenu.razor @@ -96,6 +96,9 @@ @code { + [PersistentState(AllowUpdates = true)] + public string? PersistentCompanyName { get; set; } + [Parameter] public EventCallback OnToggleDrawer { get; set; } private bool _showVersionBadge; @@ -103,17 +106,25 @@ protected override async Task OnInitializedAsync() { - try + if (PersistentCompanyName != null) { - var tenantInfo = await Http.GetFromJsonAsync("api/tenant/current"); - if (tenantInfo?.IsTenant == true && !string.IsNullOrEmpty(tenantInfo.Name)) - { - _companyName = tenantInfo.Name; - } + _companyName = PersistentCompanyName; } - catch + else { - // Ignore background error + try + { + var tenantInfo = await Http.GetFromJsonAsync("api/tenant/current"); + if (tenantInfo?.IsTenant == true && !string.IsNullOrEmpty(tenantInfo.Name)) + { + _companyName = tenantInfo.Name; + PersistentCompanyName = _companyName; + } + } + catch + { + // Ignore background error + } } } diff --git a/timetracker.Client/Components/Pages/AdminUsers.razor b/timetracker.Client/Components/Pages/AdminUsers.razor index 3033be3..01f4596 100644 --- a/timetracker.Client/Components/Pages/AdminUsers.razor +++ b/timetracker.Client/Components/Pages/AdminUsers.razor @@ -152,6 +152,12 @@ else } @code { + [PersistentState(AllowUpdates = true)] + public List? PersistentUsers { get; set; } + + [PersistentState(AllowUpdates = true)] + public List? PersistentTenants { get; set; } + private List _users = []; private List _tenants = []; private bool _loading = true; @@ -161,23 +167,35 @@ else protected override async Task OnInitializedAsync() { + if (PersistentUsers != null) + { + _users = PersistentUsers; + _tenants = PersistentTenants ?? []; + _loading = false; + } + var claim = (await AuthStateProvider.GetAuthenticationStateAsync()) .User.FindFirst(ClaimTypes.NameIdentifier); if (claim == null) return; UserNotificationService.OnUsersChanged += RefreshUsers; - try + if (PersistentUsers == null) { - _users = await AuthService.GetAllUsersAsync(); - _tenants = await Http.GetFromJsonAsync>("api/superadmin/tenants") ?? []; + try + { + _users = await AuthService.GetAllUsersAsync(); + _tenants = await Http.GetFromJsonAsync>("api/superadmin/tenants") ?? []; + + PersistentUsers = _users; + PersistentTenants = _tenants; + _loading = false; + } + catch (Exception ex) + { + Snackbar.Add($"Fehler beim Laden der Daten: {ex.Message}", Severity.Error); + } } - catch (Exception ex) - { - Snackbar.Add($"Fehler beim Laden der Daten: {ex.Message}", Severity.Error); - } - - _loading = false; } private async Task RefreshUsers() diff --git a/timetracker.Client/Components/Pages/Changelog.razor b/timetracker.Client/Components/Pages/Changelog.razor index 6a278e1..d2cf9d0 100644 --- a/timetracker.Client/Components/Pages/Changelog.razor +++ b/timetracker.Client/Components/Pages/Changelog.razor @@ -60,15 +60,19 @@ public static readonly List Releases = [ - new("1.5", "25.06.2026", true, + new("1.7", "27.06.2026", true, + [ + new("Neu", "Excel-Export"), + new("Upgrade", "Besseres Page-Loading") + ], Highlighted: true), + new("1.6", "25.06.2026", true, [ new("Neu", "Userprofil angelegt"), new("Upgrade", "Früheste Gehenszeit wird angezeigt"), new("Upgrade", "Pausen werden dynamisch berechnet"), - new("Neu", "Excel-Export"), new("Neu", "Multi-Tenant Integration"), new("Upgrade", "Verbesserte Single-User Ansicht") - ], Highlighted: true), + ]), new("1.4", "08.06.2026", true, [ new("Neu", "Timebot implementiert"), diff --git a/timetracker.Client/Components/Pages/Feiertage.razor b/timetracker.Client/Components/Pages/Feiertage.razor index 7f41223..32b2f86 100644 --- a/timetracker.Client/Components/Pages/Feiertage.razor +++ b/timetracker.Client/Components/Pages/Feiertage.razor @@ -183,6 +183,18 @@ else @code { private static readonly System.Globalization.CultureInfo _deCulture = new("de-DE"); + [PersistentState(AllowUpdates = true)] + public AppSettings? PersistentSettings { get; set; } + + [PersistentState(AllowUpdates = true)] + public List? PersistentHolidays { get; set; } + + [PersistentState(AllowUpdates = true)] + public string? PersistentSubLabel { get; set; } + + [PersistentState(AllowUpdates = true)] + public int? PersistentYear { get; set; } + private bool _loading = true; private int _year = DateTime.Today.Year; private List _holidays = []; @@ -193,15 +205,38 @@ else protected override async Task OnInitializedAsync() { + if (PersistentSettings != null && PersistentHolidays != null) + { + _settings = PersistentSettings; + _holidays = PersistentHolidays; + _subLabel = PersistentSubLabel ?? ""; + _year = PersistentYear ?? DateTime.Today.Year; + _loading = false; + } + var authState = await AuthStateProvider.GetAuthenticationStateAsync(); var claim = authState.User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier); if (claim != null) { _userId = int.Parse(claim.Value); - _settings = await TrackerService.GetSettingsAsync(_userId); + + if (PersistentSettings == null || PersistentHolidays == null) + { + _settings = await TrackerService.GetSettingsAsync(_userId); + await LoadHolidays(); + + PersistentSettings = _settings; + PersistentHolidays = _holidays; + PersistentSubLabel = _subLabel; + PersistentYear = _year; + _loading = false; + } + } + else + { + await LoadHolidays(); + _loading = false; } - await LoadHolidays(); - _loading = false; } private async Task LoadHolidays() diff --git a/timetracker.Client/Components/Pages/Home.razor b/timetracker.Client/Components/Pages/Home.razor index eb7cb23..b41f901 100644 --- a/timetracker.Client/Components/Pages/Home.razor +++ b/timetracker.Client/Components/Pages/Home.razor @@ -388,6 +388,30 @@ else @code { private static readonly System.Globalization.CultureInfo _deCulture = new("de-DE"); + [PersistentState(AllowUpdates = true)] + public AppSettings? PersistentSettings { get; set; } + + [PersistentState(AllowUpdates = true)] + public List? PersistentDays { get; set; } + + [PersistentState(AllowUpdates = true)] + public TimeSpan? PersistentTotalOvertime { get; set; } + + [PersistentState(AllowUpdates = true)] + public Dictionary? PersistentHolidays { get; set; } + + [PersistentState(AllowUpdates = true)] + public int? PersistentHolidayYear { get; set; } + + [PersistentState(AllowUpdates = true)] + public List? PersistentVacations { get; set; } + + [PersistentState(AllowUpdates = true)] + public string? PersistentWeekLabel { get; set; } + + [PersistentState(AllowUpdates = true)] + public string? PersistentWeekSubLabel { get; set; } + private bool _loading = true; private int _userId; private DateOnly _monday; @@ -409,18 +433,45 @@ else protected override async Task OnInitializedAsync() { + if (PersistentSettings != null && PersistentDays != null && PersistentTotalOvertime != null) + { + _settings = PersistentSettings; + _days = PersistentDays; + _totalOvertime = PersistentTotalOvertime.Value; + _holidays = PersistentHolidays ?? []; + _holidayYear = PersistentHolidayYear ?? -1; + _vacations = PersistentVacations ?? []; + _weekLabel = PersistentWeekLabel ?? ""; + _weekSubLabel = PersistentWeekSubLabel ?? ""; + _loading = false; + } + var authState = await AuthStateProvider.GetAuthenticationStateAsync(); var claim = authState.User.FindFirst(ClaimTypes.NameIdentifier); if (claim == null) return; // Prerender-Pass – Circuit noch nicht authentifiziert _userId = int.Parse(claim.Value); _monday = GetMonday(DateOnly.FromDateTime(DateTime.Today)); - _settings = await TrackerService.GetSettingsAsync(_userId); - var loadWeekTask = LoadWeek(); - var overtimeTask = TrackerService.GetTotalOvertimeAsync(_userId, _settings); - - await Task.WhenAll(loadWeekTask, overtimeTask); - _totalOvertime = await overtimeTask; + if (PersistentSettings == null || PersistentDays == null || PersistentTotalOvertime == null) + { + _settings = await TrackerService.GetSettingsAsync(_userId); + + var loadWeekTask = LoadWeek(); + var overtimeTask = TrackerService.GetTotalOvertimeAsync(_userId, _settings); + + await Task.WhenAll(loadWeekTask, overtimeTask); + _totalOvertime = await overtimeTask; + + PersistentSettings = _settings; + PersistentDays = _days; + PersistentTotalOvertime = _totalOvertime; + PersistentHolidays = _holidays; + PersistentHolidayYear = _holidayYear; + PersistentVacations = _vacations; + PersistentWeekLabel = _weekLabel; + PersistentWeekSubLabel = _weekSubLabel; + _loading = false; + } try { @@ -431,8 +482,6 @@ else { // Ignored during prerendering/SSR } - - _loading = false; } private async Task HandleOnboardingFinished() @@ -693,7 +742,7 @@ else } // ── ViewModels ──────────────────────────────────────────────── - private sealed class DayVm + public sealed class DayVm { public int Id { get; set; } public int UserId { get; set; } @@ -743,7 +792,7 @@ else }; } - private sealed class BreakVm + public sealed class BreakVm { public int Id { get; set; } public TimeSpan? Start { get; set; } diff --git a/timetracker.Client/Components/Pages/Login.razor b/timetracker.Client/Components/Pages/Login.razor index 5ab214b..c12f54d 100644 --- a/timetracker.Client/Components/Pages/Login.razor +++ b/timetracker.Client/Components/Pages/Login.razor @@ -9,7 +9,7 @@ @GetPageTitle() -@if (_tenantInfo == null) +@if (TenantInfo == null) { @* Loading indicator while resolving Tenant *@ @@ -17,7 +17,7 @@ Lade Timetracker… } -else if (_tenantInfo.IsTenant && !_tenantInfo.IsApproved) +else if (TenantInfo.IsTenant && !TenantInfo.IsApproved) { @* Pending Approval Screen *@ @@ -34,7 +34,7 @@ else if (_tenantInfo.IsTenant && !_tenantInfo.IsApproved) Class="mb-4" /> Freischaltung ausstehend - Die Firma @_tenantInfo.Name wurde erfolgreich registriert. + Die Firma @TenantInfo.Name wurde erfolgreich registriert. Der Zugang wird derzeit von unserem SuperAdmin geprüft und in Kürze freigeschaltet. @@ -64,9 +64,9 @@ else - @(_tenantInfo.IsTenant ? _tenantInfo.Name : "Timetracker SaaS") + @(TenantInfo.IsTenant ? TenantInfo.Name : "Timetracker SaaS") - @if (!_tenantInfo.IsTenant) + @if (!TenantInfo.IsTenant) { Zentrales Portal } @@ -168,7 +168,7 @@ else } else if (_activeTab == 1) { - @if (_tenantInfo.IsTenant) + @if (TenantInfo.IsTenant) { @* ── Employee Register Form ── *@ @@ -359,7 +359,7 @@ else } - @if (!_tenantInfo.IsTenant && _activeTab != 2) + @if (!TenantInfo.IsTenant && _activeTab != 2) { @@ -378,7 +378,9 @@ else } @code { - private TenantInfo? _tenantInfo; + [PersistentState(AllowUpdates = true)] + public TenantInfo? TenantInfo { get; set; } + private int _activeTab = 0; private string? _error; private bool _loading; @@ -399,15 +401,18 @@ else protected override async Task OnInitializedAsync() { - try + if (TenantInfo is null) { - _tenantInfo = await Http.GetFromJsonAsync("api/tenant/current"); - } - catch (Exception ex) - { - Console.WriteLine($"Fehler beim Abrufen der TenantInfo: {ex.Message}"); - // Fallback - _tenantInfo = new TenantInfo { IsTenant = false }; + try + { + TenantInfo = await Http.GetFromJsonAsync("api/tenant/current"); + } + catch (Exception ex) + { + Console.WriteLine($"Fehler beim Abrufen der TenantInfo: {ex.Message}"); + // Fallback + TenantInfo = new TenantInfo { IsTenant = false }; + } } } @@ -430,8 +435,8 @@ else private string GetPageTitle() { - if (_tenantInfo == null) return "Lade…"; - return _tenantInfo.IsTenant ? $"Anmelden bei {_tenantInfo.Name} – Timetracker" : "Anmelden – Timetracker SaaS"; + if (TenantInfo == null) return "Lade…"; + return TenantInfo.IsTenant ? $"Anmelden bei {TenantInfo.Name} – Timetracker" : "Anmelden – Timetracker SaaS"; } private string GetDomainSuffix() diff --git a/timetracker.Client/Components/Pages/Month.razor b/timetracker.Client/Components/Pages/Month.razor index 193750e..ef64dd2 100644 --- a/timetracker.Client/Components/Pages/Month.razor +++ b/timetracker.Client/Components/Pages/Month.razor @@ -155,6 +155,33 @@ else @code { private static readonly System.Globalization.CultureInfo _deCulture = new("de-DE"); + [PersistentState(AllowUpdates = true)] + public AppSettings? PersistentSettings { get; set; } + + [PersistentState(AllowUpdates = true)] + public List? PersistentDays { get; set; } + + [PersistentState(AllowUpdates = true)] + public string? PersistentSubLabel { get; set; } + + [PersistentState(AllowUpdates = true)] + public TimeSpan? PersistentMonthNet { get; set; } + + [PersistentState(AllowUpdates = true)] + public TimeSpan? PersistentMonthOvertime { get; set; } + + [PersistentState(AllowUpdates = true)] + public int? PersistentRecordedWorkDays { get; set; } + + [PersistentState(AllowUpdates = true)] + public int? PersistentTotalWorkDays { get; set; } + + [PersistentState(AllowUpdates = true)] + public int? PersistentVacationCount { get; set; } + + [PersistentState(AllowUpdates = true)] + public int? PersistentHolidayCount { get; set; } + private bool _loading = true; private int _userId; private int _year = DateTime.Today.Year; @@ -174,13 +201,41 @@ else protected override async Task OnInitializedAsync() { + if (PersistentSettings != null && PersistentDays != null) + { + _settings = PersistentSettings; + _days = PersistentDays; + _subLabel = PersistentSubLabel ?? ""; + _monthNet = PersistentMonthNet ?? TimeSpan.Zero; + _monthOvertime = PersistentMonthOvertime ?? TimeSpan.Zero; + _recordedWorkDays = PersistentRecordedWorkDays ?? 0; + _totalWorkDays = PersistentTotalWorkDays ?? 0; + _vacationCount = PersistentVacationCount ?? 0; + _holidayCount = PersistentHolidayCount ?? 0; + _loading = false; + } + var authState = await AuthStateProvider.GetAuthenticationStateAsync(); var claim = authState.User.FindFirst(ClaimTypes.NameIdentifier); if (claim == null) return; _userId = int.Parse(claim.Value); - _settings = await TrackerService.GetSettingsAsync(_userId); - await LoadMonth(); - _loading = false; + + if (PersistentSettings == null || PersistentDays == null) + { + _settings = await TrackerService.GetSettingsAsync(_userId); + await LoadMonth(); + + PersistentSettings = _settings; + PersistentDays = _days; + PersistentSubLabel = _subLabel; + PersistentMonthNet = _monthNet; + PersistentMonthOvertime = _monthOvertime; + PersistentRecordedWorkDays = _recordedWorkDays; + PersistentTotalWorkDays = _totalWorkDays; + PersistentVacationCount = _vacationCount; + PersistentHolidayCount = _holidayCount; + _loading = false; + } } private async Task LoadMonth() @@ -317,7 +372,7 @@ else else Chip("Ausstehend", "#CFD8DC"); }; - private sealed class MonthDayVm + public sealed class MonthDayVm { public DateOnly Date { get; set; } public TimeOnly? StartTime { get; set; } diff --git a/timetracker.Client/Components/Pages/Profile.razor b/timetracker.Client/Components/Pages/Profile.razor index 6e8c1fe..207adc6 100644 --- a/timetracker.Client/Components/Pages/Profile.razor +++ b/timetracker.Client/Components/Pages/Profile.razor @@ -163,6 +163,21 @@ else } @code { + [PersistentState(AllowUpdates = true)] + public AppSettings? PersistentSettings { get; set; } + + [PersistentState(AllowUpdates = true)] + public string? PersistentUsername { get; set; } + + [PersistentState(AllowUpdates = true)] + public string? PersistentNewUsername { get; set; } + + [PersistentState(AllowUpdates = true)] + public bool? PersistentIsAdmin { get; set; } + + [PersistentState(AllowUpdates = true)] + public TimeSpan? PersistentOvertime { get; set; } + private bool _loading = true; private int _userId; private string _username = ""; @@ -184,18 +199,38 @@ else protected override async Task OnInitializedAsync() { + if (PersistentSettings != null) + { + _settings = PersistentSettings; + _username = PersistentUsername ?? ""; + _newUsername = PersistentNewUsername ?? ""; + _isAdmin = PersistentIsAdmin ?? false; + _overtime = PersistentOvertime ?? TimeSpan.Zero; + _loading = false; + } + 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); + + if (PersistentSettings == null) + { + _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; + _settings = await TrackerService.GetSettingsAsync(_userId); + _overtime = await TrackerService.GetTotalOvertimeAsync(_userId, _settings); + + PersistentSettings = _settings; + PersistentUsername = _username; + PersistentNewUsername = _newUsername; + PersistentIsAdmin = _isAdmin; + PersistentOvertime = _overtime; + _loading = false; + } } private string GetInitials(string? name) diff --git a/timetracker.Client/Components/Pages/Settings.razor b/timetracker.Client/Components/Pages/Settings.razor index 85e7a4c..85fd8a5 100644 --- a/timetracker.Client/Components/Pages/Settings.razor +++ b/timetracker.Client/Components/Pages/Settings.razor @@ -511,6 +511,27 @@ else @code { private static readonly System.Globalization.CultureInfo _deCulture = new("de-DE"); + [PersistentState(AllowUpdates = true)] + public AppSettings? PersistentSettings { get; set; } + + [PersistentState(AllowUpdates = true)] + public TenantInfo? PersistentTenantInfo { get; set; } + + [PersistentState(AllowUpdates = true)] + public List? PersistentVacationDays { get; set; } + + [PersistentState(AllowUpdates = true)] + public List? PersistentHolHolidays { get; set; } + + [PersistentState(AllowUpdates = true)] + public string? PersistentTenantName { get; set; } + + [PersistentState(AllowUpdates = true)] + public string? PersistentTenantPrimaryColor { get; set; } + + [PersistentState(AllowUpdates = true)] + public string? PersistentTenantSecondaryColor { get; set; } + private AppSettings? _settings; private int _userId; private bool _isTenantAdmin; @@ -571,36 +592,63 @@ else protected override async Task OnInitializedAsync() { + if (PersistentSettings != null) + { + _settings = PersistentSettings; + _tenantInfo = PersistentTenantInfo; + _vacationDays = PersistentVacationDays ?? []; + _holHolidays = PersistentHolHolidays ?? []; + _tenantName = PersistentTenantName ?? ""; + _tenantPrimaryColor = PersistentTenantPrimaryColor ?? "#0EA5E9"; + _tenantSecondaryColor = PersistentTenantSecondaryColor ?? "#0EA5E9"; + } + var authState = await AuthStateProvider.GetAuthenticationStateAsync(); var claim = authState.User.FindFirst(ClaimTypes.NameIdentifier); if (claim == null) return; _userId = int.Parse(claim.Value); - _settings = await TrackerService.GetSettingsAsync(_userId); - _isTenantAdmin = authState.User.IsInRole("TenantAdmin") || authState.User.HasClaim("IsTenantAdmin", "true"); - if (_isTenantAdmin) + if (PersistentSettings != null) { - try + _isTenantAdmin = authState.User.IsInRole("TenantAdmin") || authState.User.HasClaim("IsTenantAdmin", "true"); + } + else + { + _isTenantAdmin = authState.User.IsInRole("TenantAdmin") || authState.User.HasClaim("IsTenantAdmin", "true"); + _settings = await TrackerService.GetSettingsAsync(_userId); + + if (_isTenantAdmin) { - _tenantInfo = await Http.GetFromJsonAsync("api/tenant/current"); - if (_tenantInfo != null) + try { - _tenantName = _tenantInfo.Name ?? ""; - _tenantPrimaryColor = _tenantInfo.PrimaryColor ?? "#0EA5E9"; - _tenantSecondaryColor = _tenantInfo.SecondaryColor ?? "#0EA5E9"; + _tenantInfo = await Http.GetFromJsonAsync("api/tenant/current"); + if (_tenantInfo != null) + { + _tenantName = _tenantInfo.Name ?? ""; + _tenantPrimaryColor = _tenantInfo.PrimaryColor ?? "#0EA5E9"; + _tenantSecondaryColor = _tenantInfo.SecondaryColor ?? "#0EA5E9"; + } + } + catch (Exception ex) + { + Snackbar.Add($"Fehler beim Laden der Firmen-Einstellungen: {ex.Message}", Severity.Error); } } - catch (Exception ex) - { - Snackbar.Add($"Fehler beim Laden der Firmen-Einstellungen: {ex.Message}", Severity.Error); - } + + var loadVacationsTask = LoadVacations(); + var loadHolidaysTask = HolidayService.GetHolidaysAsync(_holYear, _settings.GermanState); + + await Task.WhenAll(loadVacationsTask, loadHolidaysTask); + _holHolidays = await loadHolidaysTask; + + PersistentSettings = _settings; + PersistentTenantInfo = _tenantInfo; + PersistentVacationDays = _vacationDays; + PersistentHolHolidays = _holHolidays; + PersistentTenantName = _tenantName; + PersistentTenantPrimaryColor = _tenantPrimaryColor; + PersistentTenantSecondaryColor = _tenantSecondaryColor; } - - var loadVacationsTask = LoadVacations(); - var loadHolidaysTask = HolidayService.GetHolidaysAsync(_holYear, _settings.GermanState); - - await Task.WhenAll(loadVacationsTask, loadHolidaysTask); - _holHolidays = await loadHolidaysTask; } private async Task LoadVacations() diff --git a/timetracker.Client/Components/Pages/Stats.razor b/timetracker.Client/Components/Pages/Stats.razor index 0136ecb..d4a5db7 100644 --- a/timetracker.Client/Components/Pages/Stats.razor +++ b/timetracker.Client/Components/Pages/Stats.razor @@ -324,6 +324,27 @@ else } @code { + [PersistentState(AllowUpdates = true)] + public AppSettings? PersistentSettings { get; set; } + + [PersistentState(AllowUpdates = true)] + public List? PersistentDays { get; set; } + + [PersistentState(AllowUpdates = true)] + public TimeSpan? PersistentTotalOvertime { get; set; } + + [PersistentState(AllowUpdates = true)] + public double? PersistentWeekWorkedHours { get; set; } + + [PersistentState(AllowUpdates = true)] + public double? PersistentWeekTargetHours { get; set; } + + [PersistentState(AllowUpdates = true)] + public double? PersistentWeekOvertimeHours { get; set; } + + [PersistentState(AllowUpdates = true)] + public double? PersistentMaxHoursValue { get; set; } + private bool _loading = true; private int _userId; private AppSettings _settings = new(); @@ -349,84 +370,106 @@ else protected override async Task OnInitializedAsync() { + if (PersistentSettings != null && PersistentDays != null) + { + _settings = PersistentSettings; + _days = PersistentDays; + _totalOvertime = PersistentTotalOvertime ?? TimeSpan.Zero; + _weekWorkedHours = PersistentWeekWorkedHours ?? 0.0; + _weekTargetHours = PersistentWeekTargetHours ?? 0.0; + _weekOvertimeHours = PersistentWeekOvertimeHours ?? 0.0; + _maxHoursValue = PersistentMaxHoursValue ?? 10.0; + _loading = false; + } + var authState = await AuthStateProvider.GetAuthenticationStateAsync(); var claim = authState.User.FindFirst(ClaimTypes.NameIdentifier); if (claim == null) return; _userId = int.Parse(claim.Value); - _settings = await TrackerService.GetSettingsAsync(_userId); - - var today = DateOnly.FromDateTime(DateTime.Today); - var monday = GetMonday(today); - - var dbDays = await TrackerService.GetWeekAsync(_userId, monday); - _rawDbDaysCount = dbDays.Count; - _totalOvertime = await TrackerService.GetTotalOvertimeAsync(_userId, _settings); - - _weekTargetHours = 0; - _weekWorkedHours = 0; - _dbMatchStatus.Clear(); - - _days = Enumerable.Range(0, 7).Select(i => + if (PersistentSettings == null || PersistentDays == null) { - var date = monday.AddDays(i); - bool isWorkDay = _settings.IsWorkDay(date.DayOfWeek); - double target = isWorkDay ? _settings.DailyTargetHours : 0.0; + _settings = await TrackerService.GetSettingsAsync(_userId); - if (isWorkDay) - { - _weekTargetHours += _settings.DailyTargetHours; - } + var today = DateOnly.FromDateTime(DateTime.Today); + var monday = GetMonday(today); + + var dbDays = await TrackerService.GetWeekAsync(_userId, monday); + _rawDbDaysCount = dbDays.Count; + _totalOvertime = await TrackerService.GetTotalOvertimeAsync(_userId, _settings); - var wd = dbDays.FirstOrDefault(d => d.Date == date); - double worked = 0.0; - - if (wd != null) + _weekTargetHours = 0; + _weekWorkedHours = 0; + _dbMatchStatus.Clear(); + + _days = Enumerable.Range(0, 7).Select(i => { - _dbMatchStatus[date] = $"Ja (Id={wd.Id}, Start={wd.StartTime}, Ende={wd.EndTime})"; + var date = monday.AddDays(i); + bool isWorkDay = _settings.IsWorkDay(date.DayOfWeek); + double target = isWorkDay ? _settings.DailyTargetHours : 0.0; - if (wd.StartTime != null && wd.EndTime != null) + if (isWorkDay) { - var gross = wd.EndTime.Value.ToTimeSpan() - wd.StartTime.Value.ToTimeSpan(); - if (gross > TimeSpan.Zero) + _weekTargetHours += _settings.DailyTargetHours; + } + + var wd = dbDays.FirstOrDefault(d => d.Date == date); + double worked = 0.0; + + if (wd != null) + { + _dbMatchStatus[date] = $"Ja (Id={wd.Id}, Start={wd.StartTime}, Ende={wd.EndTime})"; + + if (wd.StartTime != null && wd.EndTime != null) { - var breakTotal = wd.Breaks - .Where(b => b.StartTime.HasValue && b.EndTime.HasValue && b.EndTime > b.StartTime) - .Aggregate(TimeSpan.Zero, (s, b) => - s + (b.EndTime!.Value.ToTimeSpan() - b.StartTime!.Value.ToTimeSpan())); - var minMinutes = gross.TotalHours >= 9.0 ? 45 : 30; - var minBreak = TimeSpan.FromMinutes(Math.Max(minMinutes, _settings.MinimumBreakMinutes)); - var finalBreak = breakTotal > minBreak ? breakTotal : minBreak; - worked = (gross - finalBreak).TotalHours; - if (worked < 0) worked = 0.0; + var gross = wd.EndTime.Value.ToTimeSpan() - wd.StartTime.Value.ToTimeSpan(); + if (gross > TimeSpan.Zero) + { + var breakTotal = wd.Breaks + .Where(b => b.StartTime.HasValue && b.EndTime.HasValue && b.EndTime > b.StartTime) + .Aggregate(TimeSpan.Zero, (s, b) => + s + (b.EndTime!.Value.ToTimeSpan() - b.StartTime!.Value.ToTimeSpan())); + var minMinutes = gross.TotalHours >= 9.0 ? 45 : 30; + var minBreak = TimeSpan.FromMinutes(Math.Max(minMinutes, _settings.MinimumBreakMinutes)); + var finalBreak = breakTotal > minBreak ? breakTotal : minBreak; + worked = (gross - finalBreak).TotalHours; + if (worked < 0) worked = 0.0; + } } } - } - else - { - _dbMatchStatus[date] = "Nein"; - } + else + { + _dbMatchStatus[date] = "Nein"; + } + + _weekWorkedHours += worked; + + return new DayStat + { + Date = date, + DayName = date.ToString("dddd", _deCulture), + DayShortName = date.ToString("ddd", _deCulture), + WorkedHours = worked, + TargetHours = target, + IsToday = date == today, + IsWorkDay = isWorkDay + }; + }).ToList(); + + _weekOvertimeHours = _weekWorkedHours - _weekTargetHours; - _weekWorkedHours += worked; + var maxWorked = _days.Max(d => d.WorkedHours); + _maxHoursValue = Math.Max(10.0, Math.Max(maxWorked, _settings.DailyTargetHours)); - return new DayStat - { - Date = date, - DayName = date.ToString("dddd", _deCulture), - DayShortName = date.ToString("ddd", _deCulture), - WorkedHours = worked, - TargetHours = target, - IsToday = date == today, - IsWorkDay = isWorkDay - }; - }).ToList(); - - _weekOvertimeHours = _weekWorkedHours - _weekTargetHours; - - var maxWorked = _days.Max(d => d.WorkedHours); - _maxHoursValue = Math.Max(10.0, Math.Max(maxWorked, _settings.DailyTargetHours)); - - _loading = false; + PersistentSettings = _settings; + PersistentDays = _days; + PersistentTotalOvertime = _totalOvertime; + PersistentWeekWorkedHours = _weekWorkedHours; + PersistentWeekTargetHours = _weekTargetHours; + PersistentWeekOvertimeHours = _weekOvertimeHours; + PersistentMaxHoursValue = _maxHoursValue; + _loading = false; + } } private static DateOnly GetMonday(DateOnly date) @@ -468,7 +511,7 @@ else NavigationManager.NavigateTo($"api/tracker/export/year?year={_exportYear}", forceLoad: true); } - private sealed class DayStat + public sealed class DayStat { public DateOnly Date { get; set; } public string DayName { get; set; } = ""; diff --git a/timetracker.Client/Components/Pages/SuperAdmin.razor b/timetracker.Client/Components/Pages/SuperAdmin.razor index 3109c7c..de82bc9 100644 --- a/timetracker.Client/Components/Pages/SuperAdmin.razor +++ b/timetracker.Client/Components/Pages/SuperAdmin.razor @@ -167,12 +167,23 @@ else } @code { + [PersistentState(AllowUpdates = true)] + public List? PersistentTenants { get; set; } + private bool _loading = true; private List _tenants = []; protected override async Task OnInitializedAsync() { - await LoadTenants(); + if (PersistentTenants != null) + { + _tenants = PersistentTenants; + _loading = false; + } + else + { + await LoadTenants(); + } } private async Task LoadTenants() @@ -181,6 +192,7 @@ else try { _tenants = await Http.GetFromJsonAsync>("api/superadmin/tenants") ?? []; + PersistentTenants = _tenants; } catch (Exception ex) { diff --git a/timetracker.Client/Components/Pages/UrlaubsMaximizer.razor b/timetracker.Client/Components/Pages/UrlaubsMaximizer.razor index 879743b..8b0a9a9 100644 --- a/timetracker.Client/Components/Pages/UrlaubsMaximizer.razor +++ b/timetracker.Client/Components/Pages/UrlaubsMaximizer.razor @@ -226,6 +226,27 @@ else @code { private static readonly System.Globalization.CultureInfo _deCulture = new("de-DE"); + [PersistentState(AllowUpdates = true)] + public AppSettings? PersistentSettings { get; set; } + + [PersistentState(AllowUpdates = true)] + public Dictionary? PersistentHolidays { get; set; } + + [PersistentState(AllowUpdates = true)] + public List? PersistentVacationList { get; set; } + + [PersistentState(AllowUpdates = true)] + public int? PersistentRemainingDays { get; set; } + + [PersistentState(AllowUpdates = true)] + public List? PersistentSuggestions { get; set; } + + [PersistentState(AllowUpdates = true)] + public string? PersistentSubLabel { get; set; } + + [PersistentState(AllowUpdates = true)] + public int? PersistentYear { get; set; } + private bool _loading = true; private int _year = DateTime.Today.Year; private AppSettings _settings = new(); @@ -238,13 +259,37 @@ else protected override async Task OnInitializedAsync() { + if (PersistentSettings != null && PersistentSuggestions != null) + { + _settings = PersistentSettings; + _holidays = PersistentHolidays ?? []; + _vacationSet = (PersistentVacationList ?? []).ToHashSet(); + _remainingDays = PersistentRemainingDays ?? 0; + _suggestions = PersistentSuggestions; + _subLabel = PersistentSubLabel ?? ""; + _year = PersistentYear ?? DateTime.Today.Year; + _loading = false; + } + var authState = await AuthStateProvider.GetAuthenticationStateAsync(); var claim = authState.User.FindFirst(ClaimTypes.NameIdentifier); if (claim == null) return; _userId = int.Parse(claim.Value); - _settings = await TrackerService.GetSettingsAsync(_userId); - await LoadYear(); - _loading = false; + + if (PersistentSettings == null || PersistentSuggestions == null) + { + _settings = await TrackerService.GetSettingsAsync(_userId); + await LoadYear(); + + PersistentSettings = _settings; + PersistentHolidays = _holidays; + PersistentVacationList = _vacationSet.ToList(); + PersistentRemainingDays = _remainingDays; + PersistentSuggestions = _suggestions; + PersistentSubLabel = _subLabel; + PersistentYear = _year; + _loading = false; + } } private async Task LoadYear() @@ -283,7 +328,7 @@ else // ── Algorithmus ────────────────────────────────────────────────────── private enum DayKind { Free, WorkAvailable, WorkTaken } - private sealed record Suggestion( + public sealed record Suggestion( DateOnly SpanStart, DateOnly SpanEnd, List VacationDaysToTake, diff --git a/timetracker.Client/Services/HostAuthenticationStateProvider.cs b/timetracker.Client/Services/HostAuthenticationStateProvider.cs index 7899a3a..313ac4b 100644 --- a/timetracker.Client/Services/HostAuthenticationStateProvider.cs +++ b/timetracker.Client/Services/HostAuthenticationStateProvider.cs @@ -1,5 +1,6 @@ using System.Security.Claims; using System.Net.Http.Json; +using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.Authorization; using timetracker.Shared; @@ -8,12 +9,14 @@ namespace timetracker.Client.Services; public class HostAuthenticationStateProvider : AuthenticationStateProvider { private readonly HttpClient _http; + private readonly PersistentComponentState _state; private static readonly ClaimsPrincipal Anonymous = new(new ClaimsIdentity()); private ClaimsPrincipal? _currentUser; - public HostAuthenticationStateProvider(HttpClient http) + public HostAuthenticationStateProvider(HttpClient http, PersistentComponentState state) { _http = http; + _state = state; } public override async Task GetAuthenticationStateAsync() @@ -23,20 +26,40 @@ public class HostAuthenticationStateProvider : AuthenticationStateProvider return new AuthenticationState(_currentUser); } + if (_state.TryTakeFromJson("UserInfoState", out var userInfo) && userInfo != null) + { + if (!userInfo.IsAuthenticated) + { + _currentUser = Anonymous; + return new AuthenticationState(_currentUser); + } + + var claims = new[] + { + new Claim(ClaimTypes.NameIdentifier, userInfo.Id.ToString()), + new Claim(ClaimTypes.Name, userInfo.Username), + new Claim("IsTenantAdmin", userInfo.IsTenantAdmin ? "true" : "false"), + new Claim(ClaimTypes.Role, userInfo.IsTenantAdmin ? "TenantAdmin" : "Employee") + }; + var identity = new ClaimsIdentity(claims, "Cookie"); + _currentUser = new ClaimsPrincipal(identity); + return new AuthenticationState(_currentUser); + } + try { var response = await _http.GetAsync("api/auth/me"); if (response.IsSuccessStatusCode) { - var userInfo = await response.Content.ReadFromJsonAsync(); - if (userInfo != null) + var apiUserInfo = await response.Content.ReadFromJsonAsync(); + if (apiUserInfo != null) { var claims = new[] { - new Claim(ClaimTypes.NameIdentifier, userInfo.Id.ToString()), - new Claim(ClaimTypes.Name, userInfo.Username), - new Claim("IsTenantAdmin", userInfo.IsTenantAdmin ? "true" : "false"), - new Claim(ClaimTypes.Role, userInfo.IsTenantAdmin ? "TenantAdmin" : "Employee") + new Claim(ClaimTypes.NameIdentifier, apiUserInfo.Id.ToString()), + new Claim(ClaimTypes.Name, apiUserInfo.Username), + new Claim("IsTenantAdmin", apiUserInfo.IsTenantAdmin ? "true" : "false"), + new Claim(ClaimTypes.Role, apiUserInfo.IsTenantAdmin ? "TenantAdmin" : "Employee") }; var identity = new ClaimsIdentity(claims, "Cookie"); _currentUser = new ClaimsPrincipal(identity); diff --git a/timetracker.Server/Components/App.razor b/timetracker.Server/Components/App.razor index 17fcf08..40a81cd 100644 --- a/timetracker.Server/Components/App.razor +++ b/timetracker.Server/Components/App.razor @@ -27,6 +27,7 @@ +