@page "/stats" @rendermode InteractiveWebAssembly @attribute [Authorize] @inject ITimetrackerService TrackerService @inject AuthenticationStateProvider AuthStateProvider @using System.Security.Claims @using timetracker.Shared Statistiken – Timetracker @if (_loading) { Lade Statistiken… } else { @* ── Header ── *@ Statistiken Auswertung deiner Arbeitsleistung und Gleitzeitkonto @* ── Card 1: Wochen-Fortschritt ── *@ Wochenfortschritt @{ var pct = _weekTargetHours > 0 ? (int)Math.Min((_weekWorkedHours / _weekTargetHours) * 100, 100) : 0; var displayPct = _weekTargetHours > 0 ? (int)Math.Round((_weekWorkedHours / _weekTargetHours) * 100) : 0; } @displayPct% Erreicht Arbeitszeit (Ist): @FormatHours(_weekWorkedHours) Std. Wochensoll (Soll): @FormatHours(_weekTargetHours) Std. @* ── Card 2: Wochensaldo ── *@ Wochensaldo = 0 ? "#10B981" : "#EF4444")}; font-size:3rem")" /> = 0 ? "#10B981" : "#EF4444")};")"> @FormatTs(TimeSpan.FromHours(_weekOvertimeHours), sign: true) Überstunden-Veränderung diese Woche @* ── Card 3: Gesamtsaldo ── *@ = TimeSpan.Zero ? "#10B981" : "#EF4444")};")"> Gleitzeitkonto = TimeSpan.Zero ? "#10B981" : "#EF4444")}; font-size:3rem")" /> = TimeSpan.Zero ? "#10B981" : "#EF4444")};")"> @FormatTs(_totalOvertime, sign: true) Gesamtsaldo aller erfassten Tage @* ── Säulendiagramm (Tagesverteilung) ── *@ Arbeitszeitverteilung diese Woche Geleistete Nettoarbeitsstunden pro Wochentag @{ double plotLeft = 50; double plotRight = 580; double plotTop = 30; double plotBottom = 270; double plotWidth = plotRight - plotLeft; double plotHeight = plotBottom - plotTop; double barWidth = 36; double spacing = plotWidth / 7; } @for (int i = 0; i <= 4; i++) { double val = (_maxHoursValue / 4.0) * i; double yPos = plotBottom - (val / _maxHoursValue) * plotHeight; @((MarkupString)$"{val:F1}h") } @for (int i = 0; i < 7; i++) { var d = _days[i]; double xPos = plotLeft + (i * spacing) + (spacing - barWidth) / 2; double bHeight = (d.WorkedHours / _maxHoursValue) * plotHeight; double yPos = plotBottom - bHeight; string fillCol = "#94A3B8"; // Default light slate if (d.IsToday) { fillCol = "#0EA5E9"; // Sky Blue for today } else if (d.WorkedHours >= d.TargetHours && d.TargetHours > 0) { fillCol = "#475569"; // Slate Blue for normal completed target } else if (d.WorkedHours > 0 && d.TargetHours == 0) { fillCol = "#10B981"; // Emerald Green for weekend work (pure overtime) } else if (d.WorkedHours > 0) { fillCol = "#64748B"; // Slate Medium for incomplete target } else { fillCol = "#E2E8F0"; // Very light grey for zero hours } @d.DayName: @FormatHours(d.WorkedHours) Std. (Soll: @FormatHours(d.TargetHours) Std.) @if (d.WorkedHours > 0) { @((MarkupString)$"{FormatHours(d.WorkedHours)}") } @((MarkupString)$"{d.DayShortName}") } @if (_settings.DailyTargetHours > 0) { double yTarget = plotBottom - (_settings.DailyTargetHours / _maxHoursValue) * plotHeight; @((MarkupString)$"Soll ({_settings.DailyTargetHours} h)") } @* ── Diagnostics Panel ── *@ Benutzer-ID: @_userId Wochensoll: @_weekTargetHours Std. | Netto-Arbeitszeit: @_weekWorkedHours Std. | Wochensaldo: @_weekOvertimeHours Std. Anzahl erfasste Tage aus DB: @_rawDbDaysCount Datum Tag Ist (Std) Soll (Std) Ist > 0 Arbeitstag (Einst.) Aus DB zugeordnet @foreach (var d in _days) { @d.Date.ToString("yyyy-MM-dd") @d.DayName @d.WorkedHours @d.TargetHours @(d.WorkedHours > 0 ? "Ja" : "Nein") @(d.IsWorkDay ? "Ja" : "Nein") @(_dbMatchStatus.GetValueOrDefault(d.Date, "Nein")) } } @code { private bool _loading = true; private int _userId; private AppSettings _settings = new(); private List _days = []; private TimeSpan _totalOvertime; private double _weekWorkedHours; private double _weekTargetHours; private double _weekOvertimeHours; private double _maxHoursValue = 10.0; // Debug variables private int _rawDbDaysCount = 0; private Dictionary _dbMatchStatus = new(); private static readonly System.Globalization.CultureInfo _deCulture = new("de-DE"); 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); 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 => { var date = monday.AddDays(i); bool isWorkDay = _settings.IsWorkDay(date.DayOfWeek); double target = isWorkDay ? _settings.DailyTargetHours : 0.0; if (isWorkDay) { _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 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())); worked = (gross - breakTotal).TotalHours; if (worked < 0) worked = 0.0; } } } 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; var maxWorked = _days.Max(d => d.WorkedHours); _maxHoursValue = Math.Max(10.0, Math.Max(maxWorked, _settings.DailyTargetHours)); _loading = false; } private static DateOnly GetMonday(DateOnly date) { int diff = ((int)date.DayOfWeek - (int)DayOfWeek.Monday + 7) % 7; return date.AddDays(-diff); } private string FormatHours(double hours) { var ts = TimeSpan.FromHours(hours); return $"{(int)ts.TotalHours}:{ts.Minutes:D2}"; } private string FormatTs(TimeSpan ts, bool sign = false) { if (ts == TimeSpan.Zero && sign) return "±0:00"; var prefix = sign ? (ts >= TimeSpan.Zero ? "+" : "−") : (ts < TimeSpan.Zero ? "−" : ""); var abs = ts.Duration(); return $"{prefix}{(int)abs.TotalHours}:{abs.Minutes:D2}"; } private sealed class DayStat { public DateOnly Date { get; set; } public string DayName { get; set; } = ""; public string DayShortName { get; set; } = ""; public double WorkedHours { get; set; } public double TargetHours { get; set; } public bool IsToday { get; set; } public bool IsWorkDay { get; set; } } }