@page "/month" @rendermode InteractiveServer @attribute [Authorize] @inject TimetrackerService TrackerService @inject HolidayService HolidayService @inject AuthenticationStateProvider AuthStateProvider @_deCulture.DateTimeFormat.GetMonthName(_month) @_year – Monatsübersicht – Timetracker @if (_loading) { Lade Monatsdaten… } else { @* ── Monats-Header ── *@ @_deCulture.DateTimeFormat.GetMonthName(_month) @_year @_subLabel @if (!IsCurrentMonth) { Heute } @* ── Monatstabelle ── *@ Tag Start Ende Netto Gleitzeit Status @foreach (var d in _days) { @d.Date.ToString("ddd, dd. MMM", _deCulture) @(d.StartTime.HasValue ? d.StartTime.Value.ToString(@"HH\:mm") : "—") @(d.EndTime.HasValue ? d.EndTime.Value.ToString(@"HH\:mm") : "—") @(d.Net.HasValue ? FormatTs(d.Net.Value) : "—") @(d.Net.HasValue ? FormatTs(d.Overtime, sign: true) : "—") @GetStatusChip(d) } @* ── Monatszusammenfassung ── *@ Monatszusammenfassung @FormatTs(_monthNet) Netto gesamt = TimeSpan.Zero ? "#4CAF50" : "#FF9800")}")"> @FormatTs(_monthOvertime, sign: true) Gleitzeit @_recordedWorkDays / @_totalWorkDays Arbeitstage @_vacationCount Urlaubstage @_holidayCount Feiertage } @code { private static readonly System.Globalization.CultureInfo _deCulture = new("de-DE"); private bool _loading = true; private int _userId; private int _year = DateTime.Today.Year; private int _month = DateTime.Today.Month; private List _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() { 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; } private async Task LoadMonth() { var workDays = await TrackerService.GetMonthAsync(_userId, _year, _month); var holidays = await HolidayService.GetHolidaysAsync(_year, _settings.GermanState); var vacations = await TrackerService.GetVacationDaysAsync(_userId, _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; } } }