first commit

This commit is contained in:
Wieland, Marc
2026-05-22 09:18:01 +02:00
commit 88ac175190
346 changed files with 69358 additions and 0 deletions
+307
View File
@@ -0,0 +1,307 @@
@page "/month"
@rendermode InteractiveServer
@inject TimetrackerService TrackerService
@inject HolidayService HolidayService
<PageTitle>@_deCulture.DateTimeFormat.GetMonthName(_month) @_year Monatsübersicht Timetracker</PageTitle>
@if (_loading)
{
<MudStack AlignItems="AlignItems.Center" Class="mt-16" Spacing="3">
<MudProgressCircular Color="Color.Primary" Indeterminate="true" Size="Size.Large" />
<MudText Color="Color.Secondary">Lade Monatsdaten…</MudText>
</MudStack>
}
else
{
<MudStack Spacing="3">
@* ── Monats-Header ── *@
<MudPaper Elevation="4" Class="pa-5 rounded-xl"
Style="background: linear-gradient(135deg, #3F51B5 0%, #1A237E 100%); color: white;">
<MudStack Row="true" AlignItems="AlignItems.Center" Justify="Justify.SpaceBetween">
<MudIconButton Icon="@Icons.Material.Filled.ChevronLeft"
Style="color:white" Size="Size.Large" OnClick="PrevMonth" />
<MudStack Spacing="0" AlignItems="AlignItems.Center">
<MudText Typo="Typo.h5" Style="color:white; font-weight:700; letter-spacing:0.5px">
@_deCulture.DateTimeFormat.GetMonthName(_month) @_year
</MudText>
<MudText Typo="Typo.caption" Style="color:rgba(255,255,255,0.72)">
@_subLabel
</MudText>
</MudStack>
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="0">
@if (!IsCurrentMonth)
{
<MudButton Variant="Variant.Text" Style="color:white"
OnClick="GoToCurrentMonth" Size="Size.Small">
Heute
</MudButton>
}
<MudIconButton Icon="@Icons.Material.Filled.ChevronRight"
Style="color:white" Size="Size.Large" OnClick="NextMonth" />
</MudStack>
</MudStack>
</MudPaper>
@* ── Monatstabelle ── *@
<MudCard Elevation="3" Class="rounded-xl">
<MudCardContent Class="pa-0">
<MudSimpleTable Dense="true" Striped="false" Hover="true" Style="overflow-x:auto">
<thead>
<tr style="background: rgba(63,81,181,0.08);">
<th style="font-weight:700; padding:10px 16px">Tag</th>
<th style="font-weight:700; padding:10px 16px">Start</th>
<th style="font-weight:700; padding:10px 16px">Ende</th>
<th style="font-weight:700; padding:10px 16px">Netto</th>
<th style="font-weight:700; padding:10px 16px">Gleitzeit</th>
<th style="font-weight:700; padding:10px 16px">Status</th>
</tr>
</thead>
<tbody>
@foreach (var d in _days)
{
<tr style="@GetRowStyle(d)">
<td style="padding:8px 16px; white-space:nowrap">
<MudStack Spacing="0">
<MudText Typo="Typo.body2" Style="@($"font-weight:{(d.IsToday ? "700" : "500")}")">
@d.Date.ToString("ddd, dd. MMM", _deCulture)
</MudText>
</MudStack>
</td>
<td style="padding:8px 16px">
@(d.StartTime.HasValue ? d.StartTime.Value.ToString(@"HH\:mm") : "—")
</td>
<td style="padding:8px 16px">
@(d.EndTime.HasValue ? d.EndTime.Value.ToString(@"HH\:mm") : "—")
</td>
<td style="padding:8px 16px">
@(d.Net.HasValue ? FormatTs(d.Net.Value) : "—")
</td>
<td style="padding:8px 16px; color:@(d.Overtime >= TimeSpan.Zero ? "#4CAF50" : "#FF9800"); font-weight:600">
@(d.Net.HasValue ? FormatTs(d.Overtime, sign: true) : "—")
</td>
<td style="padding:8px 16px">
@GetStatusChip(d)
</td>
</tr>
}
</tbody>
</MudSimpleTable>
</MudCardContent>
</MudCard>
@* ── Monatszusammenfassung ── *@
<MudPaper Elevation="4" Class="pa-5 rounded-xl"
Style="background: linear-gradient(135deg, rgba(63,81,181,0.08) 0%, rgba(26,35,126,0.04) 100%); border-left: 6px solid #3F51B5;">
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2" Class="mb-4">
<MudIcon Icon="@Icons.Material.Filled.CalendarViewMonth" Color="Color.Primary" />
<MudText Typo="Typo.h6" Style="font-weight:700">Monatszusammenfassung</MudText>
</MudStack>
<MudGrid Spacing="3">
<MudItem xs="6" sm="4" md="2">
<MudStack Spacing="0" AlignItems="AlignItems.Center">
<MudText Typo="Typo.h5" Color="Color.Primary" Style="font-weight:700">
@FormatTs(_monthNet)
</MudText>
<MudText Typo="Typo.caption" Color="Color.Secondary">Netto gesamt</MudText>
</MudStack>
</MudItem>
<MudItem xs="6" sm="4" md="2">
<MudStack Spacing="0" AlignItems="AlignItems.Center">
<MudText Typo="Typo.h5"
Style="@($"font-weight:700; color:{(_monthOvertime >= TimeSpan.Zero ? "#4CAF50" : "#FF9800")}")">
@FormatTs(_monthOvertime, sign: true)
</MudText>
<MudText Typo="Typo.caption" Color="Color.Secondary">Gleitzeit</MudText>
</MudStack>
</MudItem>
<MudItem xs="6" sm="4" md="2">
<MudStack Spacing="0" AlignItems="AlignItems.Center">
<MudText Typo="Typo.h5" Color="Color.Default" Style="font-weight:700">
@_recordedWorkDays / @_totalWorkDays
</MudText>
<MudText Typo="Typo.caption" Color="Color.Secondary">Arbeitstage</MudText>
</MudStack>
</MudItem>
<MudItem xs="6" sm="4" md="2">
<MudStack Spacing="0" AlignItems="AlignItems.Center">
<MudText Typo="Typo.h5" Color="Color.Secondary" Style="font-weight:700">
@_vacationCount
</MudText>
<MudText Typo="Typo.caption" Color="Color.Secondary">Urlaubstage</MudText>
</MudStack>
</MudItem>
<MudItem xs="6" sm="4" md="2">
<MudStack Spacing="0" AlignItems="AlignItems.Center">
<MudText Typo="Typo.h5" Color="Color.Tertiary" Style="font-weight:700">
@_holidayCount
</MudText>
<MudText Typo="Typo.caption" Color="Color.Secondary">Feiertage</MudText>
</MudStack>
</MudItem>
</MudGrid>
</MudPaper>
</MudStack>
}
@code {
private static readonly System.Globalization.CultureInfo _deCulture = new("de-DE");
private bool _loading = true;
private int _year = DateTime.Today.Year;
private int _month = DateTime.Today.Month;
private List<MonthDayVm> _days = [];
private AppSettings _settings = new();
private string _subLabel = "";
private TimeSpan _monthNet;
private TimeSpan _monthOvertime;
private int _recordedWorkDays;
private int _totalWorkDays;
private int _vacationCount;
private int _holidayCount;
private bool IsCurrentMonth => _year == DateTime.Today.Year && _month == DateTime.Today.Month;
protected override async Task OnInitializedAsync()
{
_settings = await TrackerService.GetSettingsAsync();
await LoadMonth();
_loading = false;
}
private async Task LoadMonth()
{
var workDays = await TrackerService.GetMonthAsync(_year, _month);
var holidays = await HolidayService.GetHolidaysAsync(_year);
var vacations = await TrackerService.GetVacationDaysAsync(_year);
var holidayMap = holidays.ToDictionary(h => h.Date, h => h.Name);
var vacationSet = vacations.Select(v => v.Date).ToHashSet();
var daysInMonth = DateTime.DaysInMonth(_year, _month);
var today = DateOnly.FromDateTime(DateTime.Today);
_days = Enumerable.Range(1, daysInMonth).Select(day =>
{
var date = new DateOnly(_year, _month, day);
var wd = workDays.FirstOrDefault(w => w.Date == date);
var isWorkDay = _settings.IsWorkDay(date.DayOfWeek);
TimeSpan? net = null;
if (wd?.StartTime != null && wd.EndTime != null)
{
var gross = wd.EndTime.Value.ToTimeSpan() - wd.StartTime.Value.ToTimeSpan();
var breaks = 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()));
net = gross - breaks;
if (net < TimeSpan.Zero) net = null;
}
var target = isWorkDay ? TimeSpan.FromHours(_settings.DailyTargetHours) : TimeSpan.Zero;
var overtime = net.HasValue ? net.Value - target : TimeSpan.Zero;
return new MonthDayVm
{
Date = date,
StartTime = wd?.StartTime,
EndTime = wd?.EndTime,
Net = net,
Overtime = overtime,
HolidayName = holidayMap.GetValueOrDefault(date),
IsVacation = vacationSet.Contains(date),
IsWorkDay = isWorkDay,
IsToday = date == today
};
}).ToList();
// Summaries
_monthNet = _days.Where(d => d.Net.HasValue).Aggregate(TimeSpan.Zero, (s, d) => s + d.Net!.Value);
_monthOvertime = _days
.Where(d => d.IsWorkDay && d.Net.HasValue)
.Aggregate(TimeSpan.Zero, (s, d) => s + d.Overtime);
_recordedWorkDays = _days.Count(d => d.IsWorkDay && d.Net.HasValue);
_totalWorkDays = _days.Count(d => d.IsWorkDay && string.IsNullOrEmpty(d.HolidayName) && !d.IsVacation);
_vacationCount = _days.Count(d => d.IsVacation);
_holidayCount = _days.Count(d => !string.IsNullOrEmpty(d.HolidayName));
var recorded = _recordedWorkDays;
var total = _totalWorkDays;
_subLabel = recorded == 0 ? "Noch keine Einträge diesen Monat" : $"{recorded} von {total} Arbeitstagen erfasst";
}
private async Task PrevMonth()
{
var d = new DateOnly(_year, _month, 1).AddMonths(-1);
_year = d.Year; _month = d.Month;
await LoadMonth();
}
private async Task NextMonth()
{
var d = new DateOnly(_year, _month, 1).AddMonths(1);
_year = d.Year; _month = d.Month;
await LoadMonth();
}
private async Task GoToCurrentMonth()
{
_year = DateTime.Today.Year; _month = DateTime.Today.Month;
await LoadMonth();
}
private static string FormatTs(TimeSpan ts, bool sign = false)
{
var neg = ts < TimeSpan.Zero;
var abs = neg ? ts.Negate() : ts;
var h = (int)abs.TotalHours;
var m = abs.Minutes;
if (!sign) return $"{h}:{m:D2} h";
return neg ? $"{h}:{m:D2} h" : $"+{h}:{m:D2} h";
}
private string GetRowStyle(MonthDayVm d)
{
if (d.IsToday) return "background: rgba(63,81,181,0.10);";
if (!string.IsNullOrEmpty(d.HolidayName)) return "background: rgba(0,150,136,0.07);";
if (d.IsVacation) return "background: rgba(255,152,0,0.08);";
if (!d.IsWorkDay) return "background: rgba(0,0,0,0.03); color: #90A4AE;";
if (d.Net.HasValue) return "background: rgba(76,175,80,0.06);";
return "";
}
private RenderFragment GetStatusChip(MonthDayVm d) => builder =>
{
void Chip(string text, string color)
{
builder.OpenElement(0, "span");
builder.AddAttribute(1, "style",
$"display:inline-block; padding:2px 10px; border-radius:12px; font-size:0.75rem; font-weight:600; background:{color}20; color:{color}; border:1px solid {color}60;");
builder.AddContent(2, text);
builder.CloseElement();
}
if (d.IsToday && !d.Net.HasValue && d.IsWorkDay) Chip("Heute", "#3F51B5");
else if (!string.IsNullOrEmpty(d.HolidayName)) Chip(d.HolidayName, "#009688");
else if (d.IsVacation) Chip("Urlaub", "#FF9800");
else if (!d.IsWorkDay) Chip("Frei", "#90A4AE");
else if (d.Net.HasValue) Chip("Erfasst", "#4CAF50");
else Chip("Ausstehend", "#CFD8DC");
};
private sealed class MonthDayVm
{
public DateOnly Date { get; set; }
public TimeOnly? StartTime { get; set; }
public TimeOnly? EndTime { get; set; }
public TimeSpan? Net { get; set; }
public TimeSpan Overtime { get; set; }
public string? HolidayName { get; set; }
public bool IsVacation { get; set; }
public bool IsWorkDay { get; set; }
public bool IsToday { get; set; }
}
}