Merge pull request 'feat: add vacation day management to home view and persist changelog version in local storage' (#9) from multi-tenant-integration into main

Reviewed-on: #9
This commit was merged in pull request #9.
This commit is contained in:
2026-06-24 20:39:48 +00:00
2 changed files with 87 additions and 11 deletions
@@ -114,9 +114,17 @@
} }
} }
private void ClearBadge() private async Task ClearBadge()
{ {
_showVersionBadge = false; _showVersionBadge = false;
try
{
await JSRuntime.InvokeVoidAsync("localStorage.setItem", "lastSeenVersion", timetracker.Client.Components.Pages.Changelog.LatestVersion);
}
catch
{
// Ignored
}
} }
private string GetInitials(string? name) private string GetInitials(string? name)
+75 -7
View File
@@ -138,10 +138,31 @@ else
</MudChip> </MudChip>
} }
else if (!hasData) else if (!hasData)
{
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="1">
@if (day.IsVacation)
{
<MudChip T="string" Color="Color.Info" Variant="Variant.Filled" Size="Size.Small" Style="font-weight:700">
URLAUB
</MudChip>
<MudButton Size="Size.Small" Variant="Variant.Text" Color="Color.Error"
OnClick="@(async () => await RemoveVacation(day))"
Style="text-transform:none; font-size: 0.75rem;">
Urlaub entfernen
</MudButton>
}
else
{ {
<MudChip T="string" Color="Color.Default" Variant="Variant.Outlined" Size="Size.Small"> <MudChip T="string" Color="Color.Default" Variant="Variant.Outlined" Size="Size.Small">
Nicht erfasst Nicht erfasst
</MudChip> </MudChip>
<MudButton Size="Size.Small" Variant="Variant.Text" Color="Color.Info"
OnClick="@(async () => await MarkAsVacation(day))"
Style="text-transform:none; font-size: 0.75rem;">
Als Urlaub eintragen
</MudButton>
}
</MudStack>
} }
</MudStack> </MudStack>
</CardHeaderContent> </CardHeaderContent>
@@ -190,7 +211,7 @@ else
<MudDivider Vertical="true" FlexItem="true" Style="height:36px" /> <MudDivider Vertical="true" FlexItem="true" Style="height:36px" />
<MudStack Spacing="0"> <MudStack Spacing="0">
<MudText Typo="Typo.overline" Color="Color.Secondary">Soll</MudText> <MudText Typo="Typo.overline" Color="Color.Secondary">Soll</MudText>
<MudText Typo="Typo.body1">@FormatTs(TimeSpan.FromHours(_settings.DailyTargetHours))</MudText> <MudText Typo="Typo.body1">@FormatTs(TimeSpan.FromHours(GetDailyTargetHours(day)))</MudText>
</MudStack> </MudStack>
<MudDivider Vertical="true" FlexItem="true" Style="height:36px" /> <MudDivider Vertical="true" FlexItem="true" Style="height:36px" />
<MudStack Spacing="0"> <MudStack Spacing="0">
@@ -224,7 +245,7 @@ else
@* ── Fortschrittsbalken ── *@ @* ── Fortschrittsbalken ── *@
@if (progressPct >= 0) @if (progressPct >= 0)
{ {
<MudTooltip Text="@($"Netto {FormatTs(GetEffectiveNetWork(day) ?? TimeSpan.Zero)} von {FormatTs(TimeSpan.FromHours(_settings.DailyTargetHours))} Soll")"> <MudTooltip Text="@($"Netto {FormatTs(GetEffectiveNetWork(day) ?? TimeSpan.Zero)} von {FormatTs(TimeSpan.FromHours(GetDailyTargetHours(day)))} Soll")">
<MudProgressLinear Value="@Math.Min(progressPct, 100)" <MudProgressLinear Value="@Math.Min(progressPct, 100)"
Color="@(progressPct >= 100 ? Color.Success : Color.Primary)" Color="@(progressPct >= 100 ? Color.Success : Color.Primary)"
Rounded="true" Class="mt-3" Style="height:6px" /> Rounded="true" Class="mt-3" Style="height:6px" />
@@ -378,6 +399,7 @@ else
private Dictionary<DateOnly, string> _holidays = []; private Dictionary<DateOnly, string> _holidays = [];
private int _holidayYear = -1; private int _holidayYear = -1;
private bool _showOnboarding; private bool _showOnboarding;
private List<VacationDay> _vacations = [];
private bool IsCurrentWeek => _monday == GetMonday(DateOnly.FromDateTime(DateTime.Today)); private bool IsCurrentWeek => _monday == GetMonday(DateOnly.FromDateTime(DateTime.Today));
@@ -428,8 +450,9 @@ else
} }
var dbDaysTask = TrackerService.GetWeekAsync(_userId, _monday); var dbDaysTask = TrackerService.GetWeekAsync(_userId, _monday);
var vacationsTask = TrackerService.GetVacationDaysAsync(_userId, _monday.Year);
await Task.WhenAll(holidaysTask, dbDaysTask); await Task.WhenAll(holidaysTask, dbDaysTask, vacationsTask);
if (_monday.Year != _holidayYear) if (_monday.Year != _holidayYear)
{ {
@@ -438,11 +461,14 @@ else
_holidayYear = _monday.Year; _holidayYear = _monday.Year;
} }
_vacations = await vacationsTask;
var dbDays = await dbDaysTask; var dbDays = await dbDaysTask;
_days = Enumerable.Range(0, 7).Select(i => _days = Enumerable.Range(0, 7).Select(i =>
{ {
var date = _monday.AddDays(i); var date = _monday.AddDays(i);
return DayVm.From(dbDays.FirstOrDefault(d => d.Date == date), date, _userId); var dayVm = DayVm.From(dbDays.FirstOrDefault(d => d.Date == date), date, _userId);
dayVm.IsVacation = _vacations.Any(v => v.Date == date);
return dayVm;
}).ToList(); }).ToList();
BuildWeekLabels(); BuildWeekLabels();
} }
@@ -523,23 +549,36 @@ else
return day.GrossWork.Value - GetEffectiveBreakTime(day); return day.GrossWork.Value - GetEffectiveBreakTime(day);
} }
private double GetDailyTargetHours(DayVm day)
{
var isHoliday = _holidays.ContainsKey(day.Date);
var isWorkDay = _settings.IsWorkDay(day.Date.DayOfWeek);
if (!isWorkDay || isHoliday || day.IsVacation)
{
return 0.0;
}
return _settings.DailyTargetHours;
}
private TimeSpan? GetOvertime(DayVm day) private TimeSpan? GetOvertime(DayVm day)
{ {
var net = GetEffectiveNetWork(day); var net = GetEffectiveNetWork(day);
return net.HasValue ? net.Value - TimeSpan.FromHours(_settings.DailyTargetHours) : null; return net.HasValue ? net.Value - TimeSpan.FromHours(GetDailyTargetHours(day)) : null;
} }
private int GetProgressPercent(DayVm day) private int GetProgressPercent(DayVm day)
{ {
var net = GetEffectiveNetWork(day); var net = GetEffectiveNetWork(day);
if (!net.HasValue || _settings.DailyTargetHours <= 0) return -1; var target = GetDailyTargetHours(day);
return (int)(net.Value.TotalHours / _settings.DailyTargetHours * 100); if (!net.HasValue || target <= 0) return -1;
return (int)(net.Value.TotalHours / target * 100);
} }
private string GetBorderColor(DayVm day, bool isWorkDay, List<string> errors) private string GetBorderColor(DayVm day, bool isWorkDay, List<string> errors)
{ {
if (!isWorkDay) return "#CFD8DC"; if (!isWorkDay) return "#CFD8DC";
if (errors.Count > 0) return "#EF5350"; if (errors.Count > 0) return "#EF5350";
if (day.IsVacation) return "#0EA5E9"; // Cool blue for vacation
if (!day.Start.HasValue && !day.End.HasValue) return "#B0BEC5"; if (!day.Start.HasValue && !day.End.HasValue) return "#B0BEC5";
if (!GetEffectiveNetWork(day).HasValue) return "#B0BEC5"; if (!GetEffectiveNetWork(day).HasValue) return "#B0BEC5";
return "#4CAF50"; return "#4CAF50";
@@ -610,6 +649,34 @@ else
return day.Start.Value + target + finalBreak; return day.Start.Value + target + finalBreak;
} }
private async Task MarkAsVacation(DayVm day)
{
var vacation = new VacationDay
{
UserId = _userId,
Date = day.Date,
Note = "Über Wochenübersicht eingetragen"
};
await TrackerService.AddVacationDayAsync(vacation);
await LoadWeek();
_totalOvertime = await TrackerService.GetTotalOvertimeAsync(_userId, _settings);
Snackbar.Add($"{day.Date:dd.MM.yyyy} als Urlaub eingetragen.", Severity.Success);
}
private async Task RemoveVacation(DayVm day)
{
var item = _vacations.FirstOrDefault(v => v.Date == day.Date);
if (item != null)
{
await TrackerService.RemoveVacationDayAsync(_userId, item.Id);
await LoadWeek();
_totalOvertime = await TrackerService.GetTotalOvertimeAsync(_userId, _settings);
Snackbar.Add($"Urlaub für {day.Date:dd.MM.yyyy} entfernt.", Severity.Info);
}
}
private static string FormatTimeSpan(TimeSpan? ts) private static string FormatTimeSpan(TimeSpan? ts)
{ {
if (!ts.HasValue) return ""; if (!ts.HasValue) return "";
@@ -626,6 +693,7 @@ else
public TimeSpan? Start { get; set; } public TimeSpan? Start { get; set; }
public TimeSpan? End { get; set; } public TimeSpan? End { get; set; }
public List<BreakVm> Breaks { get; set; } = []; public List<BreakVm> Breaks { get; set; } = [];
public bool IsVacation { get; set; }
public TimeSpan? GrossWork => public TimeSpan? GrossWork =>
Start.HasValue && End.HasValue && End > Start ? End.Value - Start.Value : null; Start.HasValue && End.HasValue && End > Start ? End.Value - Start.Value : null;