using System.Collections.ObjectModel; using System.Globalization; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using LehrerApp.Desktop.Services; using LehrerApp.Desktop.ViewModels.ClassTeacher; namespace LehrerApp.Desktop.ViewModels; public sealed record WarningDetailFigure(string Label, string Value); /// /// Inhalt des "Details"-Popups einer klassenübergreifenden Dashboard-Warnung (Nutzer-Feedback: im /// Eintrag mehr erfahren, ohne erst ins Schülerdetail zu springen). Recycelt das Monats-Widget der /// Klassenlehrer-Übersicht ("Monat auf einen Blick", + /// Views/Shared/CompactMonthCalendar) — dort aus WebUntis-Daten der eigenen Klasse, hier aus den /// lokal erfassten Mitarbeits-Einträgen aller Lerngruppen, mit denselben Kürzeln/Farben. /// Wird erst beim Öffnen des Popups erzeugt (AttentionItem.Detail ist lazy), damit das Dashboard /// nicht für jede Warnzeile vorab alle Belege zusammensucht. /// public partial class StudentWarningDetailViewModel : ObservableObject { private static readonly CultureInfo De = new("de-DE"); private const int MaxListedEvents = 10; private readonly IReadOnlyList _calendarEvents; private readonly DateOnly _firstMonth; private readonly DateOnly _lastMonth; private readonly DateOnly _today; public string Header { get; } public string Summary { get; } public IReadOnlyList Figures { get; } public string ListHeader { get; } public IReadOnlyList Events { get; } public string MoreEventsHint { get; } public bool HasMoreEvents => !string.IsNullOrEmpty(MoreEventsHint); public bool HasEvents => Events.Count > 0; public string Legend { get; } public ObservableCollection CalendarDays { get; } = []; [ObservableProperty] [NotifyPropertyChangedFor(nameof(MonthLabel))] [NotifyCanExecuteChangedFor(nameof(PreviousMonthCommand), nameof(NextMonthCommand))] private DateOnly _month; public string MonthLabel => Month.ToString("MMMM yyyy", De); public StudentWarningDetailViewModel(AttentionKind kind, StudentWarningEvidence e) { var b = e.Balance; Header = e.StudentName; switch (kind) { case AttentionKind.MissingHomework: Summary = $"{e.MissingHomework.Count} offene Hausaufgaben · Schuljahr {e.SchoolYear}"; Figures = [ new("Nicht gemacht", e.MissingHomework.Count(x => x.Label.StartsWith("Nicht", StringComparison.Ordinal)).ToString()), new("Teilweise", e.MissingHomework.Count(x => x.Label.StartsWith("Teilweise", StringComparison.Ordinal)).ToString()), ]; ListHeader = "Betroffene Termine"; _calendarEvents = e.MissingHomework; Events = e.MissingHomework; Legend = "H Hausaufgabe offen/unvollständig"; break; case AttentionKind.Lateness: Summary = $"{e.Lateness.Count}× verspätet · Schuljahr {e.SchoolYear}"; Figures = [ new("Verspätet", (e.Lateness.Count - e.SignificantlyLateCount).ToString()), new("Erheblich", e.SignificantlyLateCount.ToString()), new("Fehlquote", $"{b.AbsenceRatePercent.ToString("0.#", De)} %"), ]; ListHeader = "Verspätungen"; _calendarEvents = [.. e.Absences, .. e.Lateness]; Events = e.Lateness; Legend = "V verspätet · U unentsch. · A Entsch. offen · E entsch."; break; default: Summary = $"Fehlquote {b.AbsenceRatePercent.ToString("0.#", De)} % · {b.TotalChecked} Stunden erfasst · Schuljahr {e.SchoolYear}"; Figures = [ new("Entschuldigt", b.Excused.ToString()), new("Unentschuldigt", b.Unexcused.ToString()), new("Entsch. offen", b.ExcusePending.ToString()), new("Verspätet", e.Lateness.Count.ToString()), ]; ListHeader = "Fehltermine"; _calendarEvents = [.. e.Absences, .. e.Lateness]; Events = e.Absences; Legend = "U unentsch. · A Entsch. offen · E entsch. · V verspätet"; break; } if (Events.Count > MaxListedEvents) { MoreEventsHint = $"… und {Events.Count - MaxListedEvents} weitere"; Events = Events.Take(MaxListedEvents).ToList(); } else MoreEventsHint = ""; _firstMonth = FirstOfMonth(e.From); _lastMonth = FirstOfMonth(e.AsOf < e.To ? e.AsOf : e.To); if (_lastMonth < _firstMonth) _lastMonth = _firstMonth; // Startet beim Monat des jüngsten Belegs — der aktuelle Monat kann zu Monatsbeginn noch // leer sein, obwohl die Warnung gerade wegen der Vorwochen anschlägt. var latest = Events.Count > 0 ? FirstOfMonth(Events[0].Date) : _lastMonth; _today = e.AsOf; Month = latest < _firstMonth ? _firstMonth : latest > _lastMonth ? _lastMonth : latest; RebuildCalendar(); } private static DateOnly FirstOfMonth(DateOnly d) => new(d.Year, d.Month, 1); partial void OnMonthChanged(DateOnly value) => RebuildCalendar(); private bool CanGoPrevious() => Month > _firstMonth; private bool CanGoNext() => Month < _lastMonth; [RelayCommand(CanExecute = nameof(CanGoPrevious))] private void PreviousMonth() => Month = Month.AddMonths(-1); [RelayCommand(CanExecute = nameof(CanGoNext))] private void NextMonth() => Month = Month.AddMonths(1); private void RebuildCalendar() { CalendarDays.Clear(); foreach (var day in BuildMonth(Month, _calendarEvents, _today)) CalendarDays.Add(day); } /// Gleiche Verdichtung wie ClassTeacherOverviewViewModel.BuildCompactMonthDays: eine Zelle je /// Kalendertag, das stärkste Kürzel ersetzt die Tageszahl, alle Details stehen im Tooltip. public static IReadOnlyList BuildMonth(DateOnly month, IReadOnlyList events, DateOnly today) { var result = new List(); var byDate = events.Where(e => e.Date.Year == month.Year && e.Date.Month == month.Month) .GroupBy(e => e.Date).ToDictionary(g => g.Key, g => g.ToList()); for (var n = 1; n <= DateTime.DaysInMonth(month.Year, month.Month); n++) { var date = new DateOnly(month.Year, month.Month, n); if (!byDate.TryGetValue(date, out var dayEvents)) { result.Add(new ClassTeacherCompactCalendarDay(date, "", "#00000000", $"{date:dd.MM.yyyy}: unauffällig", date == today)); continue; } var strongest = dayEvents.OrderBy(e => Priority(e.Code)).First(); var tooltip = $"{date:dd.MM.yyyy}\n" + string.Join("\n", dayEvents.Select(e => $"{e.GroupName}: {e.Label}")); result.Add(new ClassTeacherCompactCalendarDay(date, strongest.Code.TrimEnd('!'), strongest.ColorHex, tooltip, date == today)); } return result; } private static int Priority(string code) => code switch { "U" => 0, "A" => 1, "E" => 2, "V!" => 3, "V" => 4, _ => 5, }; }