refactor: Dashboard-Handlungsbedarf zu einer Karte zusammenfassen

Fasst die sieben Kacheln, die tatsaechlich "wo muss ich reagieren" beantworten
(Entschuldigungen, Fehlzeiten-Warnung, Foerderplan-Wiedervorlage, Korrekturen,
ungeplante Stunden, Auffaelligkeiten, Unterrichtszeit-Nacherfassung), zu einer
Karte "Handlungsbedarf" mit Filter-Chips zusammen statt sieben eigener
Sichtbarkeits-Schalter.

Neues gemeinsames Modell AttentionItem/AttentionGroup/AttentionAction traegt nur
Anzeigedaten - die Datenbeschaffung bleibt unveraendert in den jeweiligen
DashboardViewModel.LoadXxx-Methoden, die am Ende ein AttentionItem statt ihrer
eigenen Item-Klasse erzeugen. RebuildAttention() gruppiert nach Art (feste
Reihenfolge) und wendet Filter an, ohne die Repos erneut abzufragen.

SupportPlanDueItem, CorrectionProgressItem, UnplannedLessonItem und
DashboardAlertItem entfallen (nur dashboard-intern verwendet); OpenExcuseItem
und AttendanceWarningItem bleiben bestehen, da GroupOverviewViewModel sie
weiterhin nutzt. Fuenf Navigations-Commands wurden durch ein einziges
OpenAttentionItemCommand ersetzt.

Bewusste Verhaltensaenderung: LoadAlerts dupliziert Fehlzeiten-Ueberschreitungen
nicht mehr in die Auffaelligkeiten-Gruppe, da dieselbe Zahl sonst zweimal in
derselben Karte erschiene (vorher durch zwei getrennte Kacheln nicht sichtbar).

Details und Testanpassungen siehe TODO.md, Abschnitt 9 (Dashboard).
This commit is contained in:
2026-09-11 00:40:58 +02:00
parent c3ce1a7204
commit 6af4bee1f0
6 changed files with 447 additions and 367 deletions
@@ -20,9 +20,7 @@ public sealed class DashboardViewModelTests
var vm = BuildVm(group, new Lesson { GroupId = group.Id, Date = today });
Assert.False(vm.ExcusesCard.EffectiveIsVisible);
Assert.False(vm.CorrectionsCard.EffectiveIsVisible);
Assert.False(vm.AlertsCard.EffectiveIsVisible);
Assert.False(vm.AttentionCard.EffectiveIsVisible);
Assert.Equal("0 offene Punkte", vm.AttentionSummary);
Assert.True(vm.TodayCard.EffectiveIsVisible);
Assert.True(vm.CalendarCard.EffectiveIsVisible);
@@ -40,7 +38,7 @@ public sealed class DashboardViewModelTests
var vm = BuildVm(group, new Lesson { GroupId = group.Id, Date = today });
// Ohne mindestens eine leer ausgeblendete Kachel wuerde der Test nichts pruefen.
Assert.False(vm.ExcusesCard.EffectiveIsVisible);
Assert.False(vm.AttentionCard.EffectiveIsVisible);
var belegtePlaetze = vm.DashboardCards.Where(c => c.EffectiveIsVisible)
.Select(c => c.Row * 2 + c.Column).OrderBy(slot => slot).ToList();
@@ -79,6 +77,18 @@ public sealed class DashboardViewModelTests
Assert.Equal(new Avalonia.Thickness(0, 0, 8, 8), vm.TasksCard.Margin);
}
/// Die frueheren sieben eigenen Listen (OpenExcuses, AttendanceWarnings, ...) sind zur
/// zusammengefassten Attention-Karte verschmolzen (siehe AttentionItem.cs) — Tests greifen
/// seither ueber Kind gefiltert zu statt ueber eine eigene Collection je Art.
private static IEnumerable<AttentionItem> Items(DashboardViewModel vm, AttentionKind kind) =>
vm.Attention.Where(g => g.Kind == kind).SelectMany(g => g.Items);
/// AttentionItem traegt fuer MissingTeachingTime kein eigenes Date-Feld (Title ist bereits der
/// formatierte Anzeigetext) — das genaue Datum steckt im mitgegebenen Action-Parameter
/// (derselbe MissingTeachingTimeItem, den auch der "Erfassen"-Dialog erhaelt).
private static DateOnly MissingTimeDate(AttentionItem item) =>
((MissingTeachingTimeItem)item.Actions.Single().Parameter!).Date;
private static PeriodScheduleService NewPeriodSchedule()
{
var tempPath = System.IO.Path.Combine(
@@ -204,9 +214,8 @@ public sealed class DashboardViewModelTests
var vm = BuildVm(group, new Lesson { GroupId = group.Id, Date = today }, slots: slots, periodSchedule: periodSchedule);
Assert.Contains(vm.MissingTeachingTimeEntries, i => i.Date == pastDay);
Assert.True(vm.MissingTeachingTimeCard.EffectiveIsVisible);
Assert.Equal(2, vm.AttentionCount); // fehlende Unterrichtszeit + bereits bestehende ungeplante Stunde
Assert.Contains(Items(vm, AttentionKind.MissingTeachingTime), i => MissingTimeDate(i) == pastDay);
Assert.True(vm.AttentionCard.EffectiveIsVisible);
}
[Fact]
@@ -226,7 +235,7 @@ public sealed class DashboardViewModelTests
var vm = BuildVm(group, new Lesson { GroupId = group.Id, Date = today },
slots: slots, periodSchedule: periodSchedule, timeEntries: timeEntries);
Assert.DoesNotContain(vm.MissingTeachingTimeEntries, i => i.Date == pastDay);
Assert.DoesNotContain(Items(vm, AttentionKind.MissingTeachingTime), i => MissingTimeDate(i) == pastDay);
}
[Fact]
@@ -246,7 +255,7 @@ public sealed class DashboardViewModelTests
var vm = BuildVm(group, new Lesson { GroupId = group.Id, Date = today },
slots: slots, periodSchedule: periodSchedule, substitutions: substitutions);
Assert.DoesNotContain(vm.MissingTeachingTimeEntries, i => i.Date == pastDay);
Assert.DoesNotContain(Items(vm, AttentionKind.MissingTeachingTime), i => MissingTimeDate(i) == pastDay);
}
[Fact]
@@ -266,7 +275,7 @@ public sealed class DashboardViewModelTests
var vm = BuildVm(group, new Lesson { GroupId = group.Id, Date = today },
slots: slots, periodSchedule: periodSchedule, schoolHolidays: schoolHolidays);
Assert.DoesNotContain(vm.MissingTeachingTimeEntries, i => i.Date == pastDay);
Assert.DoesNotContain(Items(vm, AttentionKind.MissingTeachingTime), i => MissingTimeDate(i) == pastDay);
}
[Fact]
@@ -284,7 +293,7 @@ public sealed class DashboardViewModelTests
var vm = BuildVm(group, new Lesson { GroupId = group.Id, Date = today }, slots: slots, periodSchedule: periodSchedule);
Assert.DoesNotContain(vm.MissingTeachingTimeEntries, i => i.Date == today);
Assert.DoesNotContain(Items(vm, AttentionKind.MissingTeachingTime), i => MissingTimeDate(i) == today);
}
[Fact]
@@ -302,7 +311,7 @@ public sealed class DashboardViewModelTests
var vm = BuildVm(group, new Lesson { GroupId = group.Id, Date = today }, slots: slots, periodSchedule: periodSchedule);
Assert.Contains(vm.MissingTeachingTimeEntries, i => i.Date == today);
Assert.Contains(Items(vm, AttentionKind.MissingTeachingTime), i => MissingTimeDate(i) == today);
}
[Fact]
@@ -429,10 +438,9 @@ public sealed class DashboardViewModelTests
exams: new FakeExams([exam]), results: results, memberships: memberships,
students: new FakeStudents([anna, ben]));
var correction = Assert.Single(vm.OpenCorrections);
Assert.Equal(1, correction.Completed);
Assert.Equal(2, correction.Total);
Assert.Equal(50, correction.Percent);
var correction = Assert.Single(Items(vm, AttentionKind.Correction));
Assert.Equal(50, correction.ProgressPercent);
Assert.Equal("1 von 2 Arbeiten bewertet", correction.ProgressLabel);
}
[Fact]
@@ -453,7 +461,7 @@ public sealed class DashboardViewModelTests
var vm = BuildVm(group, new Lesson { GroupId = group.Id, Date = today }, grades: grades,
memberships: memberships, students: new FakeStudents([student]));
Assert.Contains(vm.Alerts, a => a.StudentId == student.Id && a.KindLabel == "Notenabfall");
Assert.Contains(Items(vm, AttentionKind.Alert), a => a.Title == student.FullName && a.TrailingText == "Notenabfall");
}
[Fact]
@@ -612,9 +620,8 @@ public sealed class DashboardViewModelTests
var vm = BuildVm(group, pastLesson, slots: slots);
var item = Assert.Single(vm.UnplannedLessons);
Assert.Equal(group.Id, item.GroupId);
Assert.Equal(1, item.PeriodNumber);
var item = Assert.Single(Items(vm, AttentionKind.Unplanned));
Assert.Equal($"{group.Name} · 1. Stunde", item.Title);
Assert.Equal("Heute", item.DateDisplay);
}
@@ -629,7 +636,7 @@ public sealed class DashboardViewModelTests
var vm = BuildVm(group, lesson, slots: slots);
Assert.Empty(vm.UnplannedLessons);
Assert.Empty(Items(vm, AttentionKind.Unplanned));
}
[Fact]
@@ -643,7 +650,7 @@ public sealed class DashboardViewModelTests
var vm = BuildVm(group, pastLesson, slots: slots);
Assert.Empty(vm.UnplannedLessons);
Assert.Empty(Items(vm, AttentionKind.Unplanned));
}
[Fact]
@@ -659,7 +666,7 @@ public sealed class DashboardViewModelTests
var vm = BuildVm(group, pastLesson, slots: slots, schoolHolidays: schoolHolidays);
Assert.Empty(vm.UnplannedLessons);
Assert.Empty(Items(vm, AttentionKind.Unplanned));
}
[Fact]
@@ -674,7 +681,7 @@ public sealed class DashboardViewModelTests
var vm = BuildVm(group, lesson, slots: slots);
Assert.Empty(vm.UnplannedLessons);
Assert.Empty(Items(vm, AttentionKind.Unplanned));
}
[Fact]
@@ -689,9 +696,10 @@ public sealed class DashboardViewModelTests
var vm = BuildVm(group, lesson, slots: slots);
Assert.Equal(2, vm.UnplannedLessons.Count);
Assert.Contains(vm.UnplannedLessons, i => i.PeriodNumber == 3);
Assert.Contains(vm.UnplannedLessons, i => i.PeriodNumber == 4);
var unplanned = Items(vm, AttentionKind.Unplanned).ToList();
Assert.Equal(2, unplanned.Count);
Assert.Contains(unplanned, i => i.Title.Contains("3. Stunde"));
Assert.Contains(unplanned, i => i.Title.Contains("4. Stunde"));
}
[Fact]
@@ -709,7 +717,7 @@ public sealed class DashboardViewModelTests
var vm = BuildVm(group, pastLesson, slots: slots, substitutions: substitutions);
Assert.Empty(vm.UnplannedLessons);
Assert.Empty(Items(vm, AttentionKind.Unplanned));
}
[Fact]
@@ -732,7 +740,7 @@ public sealed class DashboardViewModelTests
var vm = BuildVm(group, new Lesson { GroupId = group.Id, Date = today },
students: new FakeStudents([student]), sessions: sessions, entries: entries);
Assert.Empty(vm.AttendanceWarnings);
Assert.Empty(Items(vm, AttentionKind.Attendance));
}
[Fact]
@@ -756,9 +764,9 @@ public sealed class DashboardViewModelTests
var vm = BuildVm(group, new Lesson { GroupId = group.Id, Date = today },
students: new FakeStudents([student]), sessions: sessions, entries: entries);
var item = Assert.Single(vm.AttendanceWarnings);
Assert.Equal(student.FullName, item.StudentName);
Assert.Equal(30.0, item.AbsenceRatePercent);
var item = Assert.Single(Items(vm, AttentionKind.Attendance));
Assert.Equal(student.FullName, item.Title);
Assert.Equal("30 %", item.TrailingText);
}
[Fact]