feat: add vacation day management to home view and persist changelog version in local storage #9
@@ -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)
|
||||||
|
|||||||
@@ -139,9 +139,30 @@ else
|
|||||||
}
|
}
|
||||||
else if (!hasData)
|
else if (!hasData)
|
||||||
{
|
{
|
||||||
<MudChip T="string" Color="Color.Default" Variant="Variant.Outlined" Size="Size.Small">
|
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="1">
|
||||||
Nicht erfasst
|
@if (day.IsVacation)
|
||||||
</MudChip>
|
{
|
||||||
|
<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">
|
||||||
|
Nicht erfasst
|
||||||
|
</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;
|
||||||
|
|||||||
Reference in New Issue
Block a user