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,
};
}