Loading angepasst

This commit is contained in:
MarcWieland
2026-06-26 21:35:23 +02:00
parent 6ff8743dad
commit 0c3eb3a323
19 changed files with 1991 additions and 188 deletions
@@ -48,6 +48,9 @@
</MudLayout>
@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<TenantInfo>("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<TenantInfo>("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;
}
}
@@ -96,6 +96,9 @@
</div>
@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<TenantInfo>("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<TenantInfo>("api/tenant/current");
if (tenantInfo?.IsTenant == true && !string.IsNullOrEmpty(tenantInfo.Name))
{
_companyName = tenantInfo.Name;
PersistentCompanyName = _companyName;
}
}
catch
{
// Ignore background error
}
}
}
@@ -152,6 +152,12 @@ else
}
@code {
[PersistentState(AllowUpdates = true)]
public List<User>? PersistentUsers { get; set; }
[PersistentState(AllowUpdates = true)]
public List<TenantStats>? PersistentTenants { get; set; }
private List<User> _users = [];
private List<TenantStats> _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<List<TenantStats>>("api/superadmin/tenants") ?? [];
try
{
_users = await AuthService.GetAllUsersAsync();
_tenants = await Http.GetFromJsonAsync<List<TenantStats>>("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()
@@ -60,15 +60,19 @@
public static readonly List<Release> 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"),
@@ -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<PublicHoliday>? 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<PublicHoliday> _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()
+59 -10
View File
@@ -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<DayVm>? PersistentDays { get; set; }
[PersistentState(AllowUpdates = true)]
public TimeSpan? PersistentTotalOvertime { get; set; }
[PersistentState(AllowUpdates = true)]
public Dictionary<DateOnly, string>? PersistentHolidays { get; set; }
[PersistentState(AllowUpdates = true)]
public int? PersistentHolidayYear { get; set; }
[PersistentState(AllowUpdates = true)]
public List<VacationDay>? 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; }
+23 -18
View File
@@ -9,7 +9,7 @@
<PageTitle>@GetPageTitle()</PageTitle>
@if (_tenantInfo == null)
@if (TenantInfo == null)
{
@* Loading indicator while resolving Tenant *@
<MudContainer MaxWidth="MaxWidth.Small" Class="mt-16 text-center">
@@ -17,7 +17,7 @@
<MudText Class="mt-4" Color="Color.Secondary">Lade Timetracker…</MudText>
</MudContainer>
}
else if (_tenantInfo.IsTenant && !_tenantInfo.IsApproved)
else if (TenantInfo.IsTenant && !TenantInfo.IsApproved)
{
@* Pending Approval Screen *@
<MudContainer MaxWidth="MaxWidth.Small" Class="mt-16">
@@ -34,7 +34,7 @@ else if (_tenantInfo.IsTenant && !_tenantInfo.IsApproved)
Class="mb-4" />
<MudText Typo="Typo.h5" Style="font-weight: 700;" Class="mb-2">Freischaltung ausstehend</MudText>
<MudText Typo="Typo.body1" Color="Color.Secondary" Class="mb-6">
Die Firma <strong>@_tenantInfo.Name</strong> wurde erfolgreich registriert.
Die Firma <strong>@TenantInfo.Name</strong> wurde erfolgreich registriert.
Der Zugang wird derzeit von unserem SuperAdmin geprüft und in Kürze freigeschaltet.
</MudText>
<MudDivider Class="my-4" />
@@ -64,9 +64,9 @@ else
<MudIcon Icon="@Icons.Material.Filled.AccessTime"
Style="font-size:4rem; color:#0EA5E9" />
<MudText Typo="Typo.h4" Style="font-weight:700; color:#0EA5E9">
@(_tenantInfo.IsTenant ? _tenantInfo.Name : "Timetracker SaaS")
@(TenantInfo.IsTenant ? TenantInfo.Name : "Timetracker SaaS")
</MudText>
@if (!_tenantInfo.IsTenant)
@if (!TenantInfo.IsTenant)
{
<MudText Typo="Typo.caption" Color="Color.Secondary">Zentrales Portal</MudText>
}
@@ -168,7 +168,7 @@ else
}
else if (_activeTab == 1)
{
@if (_tenantInfo.IsTenant)
@if (TenantInfo.IsTenant)
{
@* ── Employee Register Form ── *@
<MudStack Spacing="3">
@@ -359,7 +359,7 @@ else
</MudStack>
}
@if (!_tenantInfo.IsTenant && _activeTab != 2)
@if (!TenantInfo.IsTenant && _activeTab != 2)
{
<MudDivider Class="my-4" />
<MudStack AlignItems="AlignItems.Center">
@@ -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<TenantInfo>("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<TenantInfo>("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()
@@ -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<MonthDayVm>? 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; }
@@ -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)
@@ -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<VacationDay>? PersistentVacationDays { get; set; }
[PersistentState(AllowUpdates = true)]
public List<PublicHoliday>? 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<TenantInfo>("api/tenant/current");
if (_tenantInfo != null)
try
{
_tenantName = _tenantInfo.Name ?? "";
_tenantPrimaryColor = _tenantInfo.PrimaryColor ?? "#0EA5E9";
_tenantSecondaryColor = _tenantInfo.SecondaryColor ?? "#0EA5E9";
_tenantInfo = await Http.GetFromJsonAsync<TenantInfo>("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()
+106 -63
View File
@@ -324,6 +324,27 @@ else
}
@code {
[PersistentState(AllowUpdates = true)]
public AppSettings? PersistentSettings { get; set; }
[PersistentState(AllowUpdates = true)]
public List<DayStat>? 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; } = "";
@@ -167,12 +167,23 @@ else
}
@code {
[PersistentState(AllowUpdates = true)]
public List<TenantStats>? PersistentTenants { get; set; }
private bool _loading = true;
private List<TenantStats> _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<List<TenantStats>>("api/superadmin/tenants") ?? [];
PersistentTenants = _tenants;
}
catch (Exception ex)
{
@@ -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<DateOnly, string>? PersistentHolidays { get; set; }
[PersistentState(AllowUpdates = true)]
public List<DateOnly>? PersistentVacationList { get; set; }
[PersistentState(AllowUpdates = true)]
public int? PersistentRemainingDays { get; set; }
[PersistentState(AllowUpdates = true)]
public List<Suggestion>? 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<DateOnly> VacationDaysToTake,
@@ -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<AuthenticationState> GetAuthenticationStateAsync()
@@ -23,20 +26,40 @@ public class HostAuthenticationStateProvider : AuthenticationStateProvider
return new AuthenticationState(_currentUser);
}
if (_state.TryTakeFromJson<UserInfo>("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<UserInfo>();
if (userInfo != null)
var apiUserInfo = await response.Content.ReadFromJsonAsync<UserInfo>();
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);
+1
View File
@@ -27,6 +27,7 @@
<body>
<Routes />
<persist-component-state />
<script src="@Assets["_framework/blazor.web.js"]"></script>
<script src="_content/MudBlazor/MudBlazor.min.js"></script>
<script>
@@ -0,0 +1,59 @@
using System.Diagnostics;
using System.Security.Claims;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.AspNetCore.Components.Server;
using Microsoft.AspNetCore.Components.Web;
using timetracker.Shared;
namespace timetracker.Server;
public class PersistingServerAuthenticationStateProvider : ServerAuthenticationStateProvider, IDisposable
{
private readonly PersistentComponentState _state;
private readonly PersistingComponentStateSubscription _subscription;
public PersistingServerAuthenticationStateProvider(PersistentComponentState state)
{
_state = state;
_subscription = state.RegisterOnPersisting(PersistAuthenticationState);
}
private async Task PersistAuthenticationState()
{
var authState = await GetAuthenticationStateAsync();
var user = authState.User;
if (user.Identity?.IsAuthenticated == true)
{
var idClaim = user.FindFirst(ClaimTypes.NameIdentifier)?.Value;
var name = user.Identity.Name;
var isAdminClaim = user.FindFirst("IsTenantAdmin")?.Value;
if (idClaim != null && name != null)
{
var userInfo = new UserInfo
{
Id = int.Parse(idClaim),
Username = name,
IsTenantAdmin = isAdminClaim == "true",
IsAuthenticated = true
};
_state.PersistAsJson("UserInfoState", userInfo);
}
}
else
{
var userInfo = new UserInfo
{
IsAuthenticated = false
};
_state.PersistAsJson("UserInfoState", userInfo);
}
}
public void Dispose()
{
_subscription.Dispose();
}
}
+3
View File
@@ -7,8 +7,10 @@ using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.HttpOverrides;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.AspNetCore.Components.Authorization;
using MudBlazor.Services;
using Serilog;
using timetracker.Server;
using timetracker.Server.Components;
using timetracker.Data;
using timetracker.Shared;
@@ -68,6 +70,7 @@ builder.Services.AddAuthorization(options =>
});
builder.Services.AddCascadingAuthenticationState();
builder.Services.AddScoped<AuthenticationStateProvider, PersistingServerAuthenticationStateProvider>();
builder.Services.AddHttpContextAccessor();
builder.Services.AddTransient<ITenantProvider, TenantProvider>();
File diff suppressed because it is too large Load Diff
+1
View File
@@ -5,4 +5,5 @@ public class UserInfo
public int Id { get; set; }
public string Username { get; set; } = "";
public bool IsTenantAdmin { get; set; }
public bool IsAuthenticated { get; set; }
}