diff --git a/LehrerApp.Core/Services/DashboardSettingsService.cs b/LehrerApp.Core/Services/DashboardSettingsService.cs
index 2476f04..67fe3d0 100644
--- a/LehrerApp.Core/Services/DashboardSettingsService.cs
+++ b/LehrerApp.Core/Services/DashboardSettingsService.cs
@@ -12,10 +12,13 @@ public sealed class DashboardCardSetting
/// Speichert Sichtbarkeit und Reihenfolge der Dashboard-Kacheln lokal.
public sealed class DashboardSettingsService
{
+ // "attention" fasst die frueheren sieben Kacheln missingteachingtime/excuses/corrections/
+ // unplanned/alerts/attendance/support zusammen (siehe AttentionItem.cs im Desktop-Projekt).
+ // Alte gespeicherte dashboardsettings.json-Dateien mit den frueheren Keys sind unproblematisch:
+ // Load() unten verwirft unbekannte Keys ohnehin stillschweigend und ergaenzt neue als sichtbar.
public static readonly string[] DefaultCardOrder =
[
- "today", "tasks", "missingteachingtime", "calendar", "excuses", "upcoming",
- "corrections", "unplanned", "alerts", "attendance", "support", "groups", "examload",
+ "today", "tasks", "attention", "calendar", "upcoming", "groups", "examload",
];
private readonly string _configPath;
diff --git a/LehrerApp.Desktop.Tests/DashboardViewModelTests.cs b/LehrerApp.Desktop.Tests/DashboardViewModelTests.cs
index 5e8fcf9..869e221 100644
--- a/LehrerApp.Desktop.Tests/DashboardViewModelTests.cs
+++ b/LehrerApp.Desktop.Tests/DashboardViewModelTests.cs
@@ -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 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]
diff --git a/LehrerApp.Desktop/ViewModels/AttentionItem.cs b/LehrerApp.Desktop/ViewModels/AttentionItem.cs
new file mode 100644
index 0000000..da08f01
--- /dev/null
+++ b/LehrerApp.Desktop/ViewModels/AttentionItem.cs
@@ -0,0 +1,106 @@
+using CommunityToolkit.Mvvm.ComponentModel;
+using System.Windows.Input;
+
+namespace LehrerApp.Desktop.ViewModels;
+
+/// Die sieben Dashboard-Kacheln, die tatsächlich "wo muss ich reagieren" beantworten
+/// (siehe ), zusammengefasst zu einer Karte mit
+/// Filter-Chips statt sieben eigenen Sichtbarkeits-Schaltern. Reihenfolge hier ist die Reihenfolge
+/// der Gruppen in der zusammengefassten Ansicht (siehe DashboardViewModel.RebuildAttention) — sie
+/// übernimmt bewusst die frühere Sortierung aus DashboardSettingsService.DefaultCardOrder, damit
+/// sich nichts, was Nutzer bereits kennen, unvorhersehbar umsortiert.
+/// "Klausurwochen" (examload) bleibt bewusst außen vor: die Einträge dort sind Wochen-Aggregate,
+/// keine Einzelvorgänge, die sich abhaken oder anklicken lassen — sie passen nicht ins Item-Modell.
+public enum AttentionKind { MissingTeachingTime, Excuse, Correction, Unplanned, Alert, Attendance, SupportPlan }
+
+/// Eine Inline-Aktion auf einem Handlungsbedarf-Eintrag (z.B. "Entschuldigt"/"Unentschuldigt",
+/// "Erfassen"). Verwendet ICommand statt Action, damit die bereits vorhandenen generierten
+/// RelayCommands (OpenExcuseItem.MarkExcusedCommand, DashboardViewModel.AddMissingTeachingTimeCommand)
+/// direkt weiterverwendet werden können, statt sie in einen zweiten Delegate-Typ zu verpacken.
+public sealed record AttentionAction(string Label, ICommand Command, object? Parameter = null);
+
+/// Gemeinsame Projektion für die sieben Handlungsbedarf-Arten. Trägt nur Anzeigedaten —
+/// die eigentliche Beschaffung bleibt unverändert in den jeweiligen DashboardViewModel.LoadXxx-
+/// Methoden; sie erzeugen am Ende ein AttentionItem statt (wie vorher) ihre eigene Item-Klasse in
+/// ihre eigene ObservableCollection einzutragen.
+public sealed class AttentionItem
+{
+ public AttentionKind Kind { get; }
+ public string Title { get; }
+ public string Subtitle { get; }
+ /// Kurztext rechts neben dem Titel — Fehlzeitenquote, Alert-Kind-Label. Meist leer.
+ public string TrailingText { get; }
+ public string DateDisplay { get; }
+ public bool IsOverdue { get; }
+ public AlertSeverity? Severity { get; }
+ /// Nur bei Korrekturen gesetzt (0–100); steuert die ProgressBar. Eigenes HasProgress statt
+ /// Nullability, damit die ProgressBar-Bindung ein einfaches int (kompatibel mit Value:double) bleibt.
+ public int ProgressPercent { get; }
+ public bool HasProgress { get; }
+ /// Text unter der ProgressBar, z.B. "3 von 5 Arbeiten bewertet". Nur bei Korrekturen gesetzt.
+ public string ProgressLabel { get; }
+ /// Färbt TrailingText wie die frühere Fehlzeiten-Warnung (fest "Foreground=Red") ein —
+ /// nur bei Attendance gesetzt, weil ein AttentionItem dieser Art per Konstruktion nur entsteht,
+ /// wenn die Fehlzeitenquote den Schwellenwert bereits überschreitet.
+ public bool IsWarningTrailing { get; }
+ public IReadOnlyList Actions { get; }
+ public Action? Navigate { get; }
+
+ public AttentionItem(AttentionKind kind, string title, string subtitle = "", string trailingText = "",
+ string dateDisplay = "", bool isOverdue = false, AlertSeverity? severity = null,
+ int progressPercent = 0, string progressLabel = "", bool hasProgress = false,
+ bool isWarningTrailing = false, IReadOnlyList? actions = null, Action? navigate = null)
+ {
+ Kind = kind;
+ Title = title;
+ Subtitle = subtitle;
+ TrailingText = trailingText;
+ DateDisplay = dateDisplay;
+ IsOverdue = isOverdue;
+ Severity = severity;
+ ProgressPercent = progressPercent;
+ ProgressLabel = progressLabel;
+ HasProgress = hasProgress;
+ IsWarningTrailing = isWarningTrailing;
+ Actions = actions ?? [];
+ Navigate = navigate;
+ }
+
+ public bool HasSubtitle => !string.IsNullOrWhiteSpace(Subtitle);
+ public bool HasTrailingText => !string.IsNullOrWhiteSpace(TrailingText);
+ public bool HasDate => !string.IsNullOrWhiteSpace(DateDisplay);
+ public bool HasActions => Actions.Count > 0;
+ public bool HasSeverity => Severity is not null;
+ public bool IsHighSeverity => Severity == AlertSeverity.High;
+ public bool IsMediumSeverity => Severity == AlertSeverity.Medium;
+}
+
+/// Eine Kind-Gruppe innerhalb der zusammengefassten Handlungsbedarf-Liste. Items behalten
+/// die Sortierung, die ihre jeweilige LoadXxx-Methode schon immer produziert hat (z.B. Fehlzeiten
+/// nach Quote absteigend, Korrekturen nach Datum) — ein zweites, generisches "Importance"-Sortieren
+/// quer über alle Arten würde diese bewusst gewählten Reihenfolgen wieder zerstören.
+public sealed class AttentionGroup(AttentionKind kind, string header, IReadOnlyList items)
+{
+ public AttentionKind Kind { get; } = kind;
+ public string Header { get; } = header;
+ public IReadOnlyList Items { get; } = items;
+ public int Count => Items.Count;
+}
+
+/// Ein Filter-Chip für eine Handlungsbedarf-Art in der zusammengefassten Karte — ersetzt
+/// die frühere Sichtbarkeit je Einzelkachel (siehe DashboardViewModel.LoadAttentionFilters).
+public partial class AttentionFilterOption : ObservableObject
+{
+ [ObservableProperty] private bool _isActive = true;
+ public AttentionKind Kind { get; }
+ public string Label { get; }
+ public Action? OnChanged { get; set; }
+
+ public AttentionFilterOption(AttentionKind kind, string label)
+ {
+ Kind = kind;
+ Label = label;
+ }
+
+ partial void OnIsActiveChanged(bool value) => OnChanged?.Invoke();
+}
diff --git a/LehrerApp.Desktop/ViewModels/DashboardViewModel.cs b/LehrerApp.Desktop/ViewModels/DashboardViewModel.cs
index 70ddf85..d20ca5f 100644
--- a/LehrerApp.Desktop/ViewModels/DashboardViewModel.cs
+++ b/LehrerApp.Desktop/ViewModels/DashboardViewModel.cs
@@ -89,20 +89,43 @@ public partial class DashboardViewModel : ObservableObject
public ObservableCollection OpenTasks { get; } = [];
public ObservableCollection CurrentGroups { get; } = [];
public ObservableCollection CalendarDays { get; } = [];
- public ObservableCollection OpenExcuses { get; } = [];
- public ObservableCollection AttendanceWarnings { get; } = [];
public ObservableCollection ExamWeekLoads { get; } = [];
- public ObservableCollection MissingTeachingTimeEntries { get; } = [];
- public ObservableCollection SupportPlanReviews { get; } = [];
public ObservableCollection UpcomingDates { get; } = [];
- public ObservableCollection OpenCorrections { get; } = [];
- public ObservableCollection UnplannedLessons { get; } = [];
- public ObservableCollection Alerts { get; } = [];
public ObservableCollection SelectedDayEvents { get; } = [];
public ObservableCollection DashboardCards { get; } = [];
public ObservableCollection WeatherWarnings { get; } = [];
+ // Zusammengefasste "Handlungsbedarf"-Karte (vormals sieben eigene Kacheln/Collections:
+ // Entschuldigungen, Fehlzeiten, Förderplan, Korrekturen, ungeplante Stunden, Auffälligkeiten,
+ // Unterrichtszeit-Nacherfassung — siehe AttentionItem.cs). Attention traegt nur, was nach
+ // Filterung (AttentionFilters) tatsaechlich angezeigt wird; die Rohdaten je Art liegen in den
+ // privaten _xyzItems-Feldern und werden von RebuildAttention() neu zusammengesetzt, sobald sich
+ // Daten oder Filter aendern — ohne die Repos erneut abzufragen.
+ public ObservableCollection Attention { get; } = [];
+ public ObservableCollection AttentionFilters { get; } = [];
public static string[] CalendarWeekdayHeaders { get; } = ["Mo", "Di", "Mi", "Do", "Fr", "Sa", "So"];
+ private readonly List _excuses = [];
+ private readonly List _attendanceItems = [];
+ private readonly List _supportItems = [];
+ private readonly List _correctionItems = [];
+ private readonly List _unplannedItems = [];
+ private readonly List _alertItems = [];
+ private readonly List _missingTimeItems = [];
+
+ /// Reihenfolge und Überschriften der Gruppen in der zusammengefassten Handlungsbedarf-Karte —
+ /// übernimmt die frühere Kachel-Reihenfolge aus DashboardSettingsService.DefaultCardOrder,
+ /// damit sich für Nutzer nichts unvorhersehbar umsortiert.
+ private static readonly (AttentionKind Kind, string Header)[] AttentionGroupOrder =
+ [
+ (AttentionKind.MissingTeachingTime, "Unterrichtszeit nacherfassen"),
+ (AttentionKind.Excuse, "Offene Entschuldigungen"),
+ (AttentionKind.Correction, "Offene Korrekturen"),
+ (AttentionKind.Unplanned, "Ungeplante Stunden"),
+ (AttentionKind.Alert, "Auffälligkeiten"),
+ (AttentionKind.Attendance, "Fehlzeiten-Warnung"),
+ (AttentionKind.SupportPlan, "Förderplan-Wiedervorlage"),
+ ];
+
// Navigation-Callback – wird von App.axaml.cs verdrahtet
public Action? OnNavigateToGroup { get; set; }
public Action? OnNavigateToStudent { get; set; }
@@ -125,21 +148,15 @@ public partial class DashboardViewModel : ObservableObject
public DashboardCardOption TodayCard => Card("today");
public DashboardCardOption TasksCard => Card("tasks");
public DashboardCardOption CalendarCard => Card("calendar");
- public DashboardCardOption ExcusesCard => Card("excuses");
public DashboardCardOption UpcomingCard => Card("upcoming");
- public DashboardCardOption CorrectionsCard => Card("corrections");
- public DashboardCardOption UnplannedCard => Card("unplanned");
- public DashboardCardOption AlertsCard => Card("alerts");
- public DashboardCardOption AttendanceCard => Card("attendance");
public DashboardCardOption ExamLoadCard => Card("examload");
- public DashboardCardOption MissingTeachingTimeCard => Card("missingteachingtime");
- public DashboardCardOption SupportCard => Card("support");
public DashboardCardOption GroupsCard => Card("groups");
+ public DashboardCardOption AttentionCard => Card("attention");
public int TodayLessonCount => TodaysLessons.Count;
public int OpenTaskCount => OpenTasks.Count;
public int UpcomingCount => UpcomingDates.Count;
- public int AttentionCount => OpenExcuses.Count + AttendanceWarnings.Count + SupportPlanReviews.Count
- + OpenCorrections.Count + UnplannedLessons.Count + Alerts.Count + MissingTeachingTimeEntries.Count;
+ public int AttentionCount => _excuses.Count + _attendanceItems.Count + _supportItems.Count
+ + _correctionItems.Count + _unplannedItems.Count + _alertItems.Count + _missingTimeItems.Count;
public string TodayLessonSummary => TodayLessonCount == 1 ? "1 Stunde" : $"{TodayLessonCount} Stunden";
public string OpenTaskSummary => OpenTaskCount == 1 ? "1 Aufgabe" : $"{OpenTaskCount} Aufgaben";
public string AttentionSummary => AttentionCount == 1 ? "1 offener Punkt" : $"{AttentionCount} offene Punkte";
@@ -181,6 +198,7 @@ public partial class DashboardViewModel : ObservableObject
annualPlanSync.DataChanged += () => Avalonia.Threading.Dispatcher.UIThread.Post(LoadCalendar);
}
LoadDashboardCards();
+ LoadAttentionFilters();
Load();
RefreshWebUntisHealth();
}
@@ -265,6 +283,7 @@ public partial class DashboardViewModel : ObservableObject
LoadOpenCorrections(groups, today);
LoadUnplannedLessons(groups, today);
LoadAlerts(groups, today);
+ RebuildAttention();
UpdateDashboardSummary();
}
@@ -273,16 +292,10 @@ public partial class DashboardViewModel : ObservableObject
TodayCard.IsEmpty = TodaysLessons.Count == 0;
TasksCard.IsEmpty = OpenTasks.Count == 0;
CalendarCard.IsEmpty = false;
- ExcusesCard.IsEmpty = OpenExcuses.Count == 0;
UpcomingCard.IsEmpty = UpcomingDates.Count == 0;
- CorrectionsCard.IsEmpty = OpenCorrections.Count == 0;
- UnplannedCard.IsEmpty = UnplannedLessons.Count == 0;
- AlertsCard.IsEmpty = Alerts.Count == 0;
- AttendanceCard.IsEmpty = AttendanceWarnings.Count == 0;
ExamLoadCard.IsEmpty = ExamWeekLoads.Count == 0;
- MissingTeachingTimeCard.IsEmpty = MissingTeachingTimeEntries.Count == 0;
- SupportCard.IsEmpty = SupportPlanReviews.Count == 0;
GroupsCard.IsEmpty = CurrentGroups.Count == 0;
+ AttentionCard.IsEmpty = AttentionCount == 0;
// Erst nachdem alle IsEmpty-Werte stehen: welche Kachel tatsaechlich gerendert wird, haengt
// ueber EffectiveIsVisible daran, und davon wiederum die Zeilen-/Spaltenzuordnung.
@@ -353,7 +366,7 @@ public partial class DashboardViewModel : ObservableObject
private void LoadAttendanceWarnings(DateOnly today)
{
- AttendanceWarnings.Clear();
+ _attendanceItems.Clear();
var schoolYear = _sy.CurrentSchoolYear();
var from = _sy.SchoolYearStart(schoolYear);
var to = _sy.SchoolYearEnd(schoolYear);
@@ -376,9 +389,13 @@ public partial class DashboardViewModel : ObservableObject
items.Add(new AttendanceWarningItem(student.Id, student.FullName, balance.AbsenceRatePercent));
}
foreach (var item in items.OrderByDescending(i => i.AbsenceRatePercent))
- AttendanceWarnings.Add(item);
+ _attendanceItems.Add(ToAttentionItem(item));
}
+ private AttentionItem ToAttentionItem(AttendanceWarningItem w) => new(
+ AttentionKind.Attendance, w.StudentName, trailingText: $"{w.AbsenceRatePercent:0.#} %",
+ isWarningTrailing: true, navigate: () => OnNavigateToStudent?.Invoke(w.StudentId));
+
// ── Klausurwochen (Nutzer-Feedback) ───────────────────────────────────────
//
// Persönliche Klausurlast über alle Kurse hinweg — anders als die klassenbezogene
@@ -410,7 +427,7 @@ public partial class DashboardViewModel : ObservableObject
private void LoadMissingTeachingTime(DateOnly today)
{
- MissingTeachingTimeEntries.Clear();
+ _missingTimeItems.Clear();
var firstDay = today.AddDays(-MissingTeachingTimeLookbackDays);
var publicHolidayDates = Enumerable.Range(firstDay.Year, today.Year - firstDay.Year + 1)
.SelectMany(y => _publicHolidays.GetHolidays(y, _calendarSettings.State))
@@ -445,14 +462,15 @@ public partial class DashboardViewModel : ObservableObject
items.Add(new MissingTeachingTimeItem(date, windowStart, windowEnd));
}
foreach (var item in items.OrderBy(i => i.Date))
- MissingTeachingTimeEntries.Add(item);
+ _missingTimeItems.Add(new AttentionItem(AttentionKind.MissingTeachingTime, item.DateDisplay,
+ actions: [new AttentionAction("Erfassen", AddMissingTeachingTimeCommand, item)]));
}
// ── Förderplan-Wiedervorlage (5.3.2) ──────────────────────────────────────
private void LoadSupportPlanReviews(DateOnly today)
{
- SupportPlanReviews.Clear();
+ _supportItems.Clear();
var dueBy = today.AddDays(SupportPlanDueWithinDays);
var due = _documentation.GetAll()
@@ -465,8 +483,11 @@ public partial class DashboardViewModel : ObservableObject
{
var student = _students.GetById(d.StudentId);
if (student is null) continue;
- SupportPlanReviews.Add(new SupportPlanDueItem(
- d.StudentId, student.FullName, d.Title, d.SupportData!.ReviewDate!.Value, today));
+ var reviewDate = d.SupportData!.ReviewDate!.Value;
+ var studentId = d.StudentId;
+ _supportItems.Add(new AttentionItem(AttentionKind.SupportPlan, student.FullName, subtitle: d.Title,
+ dateDisplay: reviewDate.ToString("dd.MM.yyyy"), isOverdue: reviewDate < today,
+ navigate: () => OnNavigateToStudent?.Invoke(studentId)));
}
}
@@ -511,7 +532,7 @@ public partial class DashboardViewModel : ObservableObject
private void LoadOpenCorrections(IReadOnlyDictionary groups, DateOnly today)
{
- OpenCorrections.Clear();
+ _correctionItems.Clear();
foreach (var group in groups.Values)
foreach (var exam in _exams.GetByGroup(group.Id)
.Where(e => e.Status is ExamStatus.Conducted or ExamStatus.Graded)
@@ -519,8 +540,13 @@ public partial class DashboardViewModel : ObservableObject
{
var (expected, evaluated) = ExamCorrectionCounter.Count(exam,
_memberships.GetByGroup(group.Id), _examResults.GetByExam(exam.Id));
- OpenCorrections.Add(new CorrectionProgressItem(exam.Id, group.Id, exam.Title,
- group.Name, exam.Date, evaluated, expected, today));
+ var percent = expected == 0 ? 0 : (int)Math.Round(evaluated * 100.0 / expected);
+ var groupId = group.Id;
+ _correctionItems.Add(new AttentionItem(AttentionKind.Correction, exam.Title, subtitle: group.Name,
+ dateDisplay: exam.Date.ToString("dd.MM.yyyy"),
+ isOverdue: exam.Date < today.AddDays(-7) && evaluated < expected,
+ progressPercent: percent, progressLabel: $"{evaluated} von {expected} Arbeiten bewertet", hasProgress: true,
+ navigate: () => OnNavigateToExam?.Invoke(groupId)));
}
}
@@ -536,14 +562,14 @@ public partial class DashboardViewModel : ObservableObject
private void LoadUnplannedLessons(IReadOnlyDictionary groups, DateOnly today)
{
- UnplannedLessons.Clear();
+ _unplannedItems.Clear();
var lastDay = today.AddDays(UnplannedLessonsLookaheadDays);
var publicHolidayDates = Enumerable.Range(today.Year, lastDay.Year - today.Year + 1)
.SelectMany(y => _publicHolidays.GetHolidays(y, _calendarSettings.State))
.Select(h => h.Date).ToHashSet();
var schoolHolidays = _schoolHolidays.GetAll();
- var items = new List();
+ var items = new List<(DateOnly Date, int PeriodNumber, LearningGroup Group)>();
foreach (var group in groups.Values.Where(g => g.RequiresLessonPlanning))
{
var slots = _timetableSlots.GetByGroup(group.Id);
@@ -572,12 +598,19 @@ public partial class DashboardViewModel : ObservableObject
{
continue;
}
- items.Add(new UnplannedLessonItem(group.Id, group.Name, date, slot.PeriodNumber, today));
+ items.Add((date, slot.PeriodNumber, group));
}
}
}
- foreach (var item in items.OrderBy(i => i.Date).ThenBy(i => i.PeriodNumber).ThenBy(i => i.GroupName))
- UnplannedLessons.Add(item);
+ foreach (var (date, periodNumber, group) in items.OrderBy(i => i.Date).ThenBy(i => i.PeriodNumber)
+ .ThenBy(i => i.Group.Name))
+ {
+ var dateDisplay = date == today ? "Heute" : date == today.AddDays(1) ? "Morgen" : date.ToString("dd.MM.");
+ var groupId = group.Id;
+ _unplannedItems.Add(new AttentionItem(AttentionKind.Unplanned,
+ $"{group.Name} · {periodNumber}. Stunde", dateDisplay: dateDisplay,
+ navigate: () => OnNavigateToUnplannedLesson?.Invoke(groupId)));
+ }
}
/// Erkennt, ob eine Stunde ohne eigene Lesson bereits Teil einer Doppelstunde ist, die
@@ -609,11 +642,11 @@ public partial class DashboardViewModel : ObservableObject
private void LoadAlerts(IReadOnlyDictionary groups, DateOnly today)
{
- Alerts.Clear();
- foreach (var warning in AttendanceWarnings)
- Alerts.Add(new DashboardAlertItem(warning.StudentId, null, warning.StudentName,
- "Fehlzeiten", $"Fehlzeitenquote {warning.AbsenceRatePercent:0.#} %", AlertSeverity.High));
-
+ _alertItems.Clear();
+ // Fehlzeiten-Auffälligkeiten erscheinen in der zusammengefassten Handlungsbedarf-Karte
+ // bereits als eigene Gruppe "Fehlzeiten-Warnung" (LoadAttendanceWarnings) — eine weitere
+ // Kopie hier wäre jetzt eine sichtbare Dopplung derselben Schüler/Zahl, die vor dem Merge
+ // durch zwei getrennte Kacheln (Auffälligkeiten vs. Fehlzeiten-Warnung) nicht auffiel.
foreach (var group in groups.Values)
{
var groupReportGrades = _reportGrades.GetByGroup(group.Id);
@@ -622,6 +655,7 @@ public partial class DashboardViewModel : ObservableObject
{
var student = _students.GetById(membership.StudentId);
if (student is null) continue;
+ var studentId = student.Id;
var values = _grades.GetByStudentAndGroup(student.Id, group.Id)
.OrderBy(g => g.Date)
.Select(g => int.TryParse(g.Value, out var value) ? (int?)value : null)
@@ -635,9 +669,10 @@ public partial class DashboardViewModel : ObservableObject
? recent - previous >= 1.0
: previous - recent >= 3.0;
if (declined)
- Alerts.Add(new DashboardAlertItem(student.Id, group.Id, student.FullName,
- "Notenabfall", $"{group.Name}: zuletzt {recent:0.0}, zuvor {previous:0.0}",
- AlertSeverity.Medium));
+ _alertItems.Add(new AttentionItem(AttentionKind.Alert, student.FullName,
+ subtitle: $"{group.Name}: zuletzt {recent:0.0}, zuvor {previous:0.0}",
+ trailingText: "Notenabfall", severity: AlertSeverity.Medium,
+ navigate: () => OnNavigateToStudent?.Invoke(studentId)));
}
var latestReport = groupReportGrades
@@ -646,9 +681,10 @@ public partial class DashboardViewModel : ObservableObject
var effective = latestReport?.OverrideValue ?? latestReport?.CalculatedValue;
if (int.TryParse(effective, out var reportValue)
&& (group.GradingSystem == GradingSystem.Grades1To6 ? reportValue >= 5 : reportValue <= 4))
- Alerts.Add(new DashboardAlertItem(student.Id, group.Id, student.FullName,
- "Versetzungsgefährdung", $"{group.Name}: aktueller Stand {reportValue}",
- AlertSeverity.High));
+ _alertItems.Add(new AttentionItem(AttentionKind.Alert, student.FullName,
+ subtitle: $"{group.Name}: aktueller Stand {reportValue}",
+ trailingText: "Versetzungsgefährdung", severity: AlertSeverity.High,
+ navigate: () => OnNavigateToStudent?.Invoke(studentId)));
}
}
}
@@ -670,13 +706,65 @@ public partial class DashboardViewModel : ObservableObject
private static string CardTitle(string key) => key switch
{
"today" => "Heute", "tasks" => "Offene Aufgaben", "calendar" => "Kalender",
- "excuses" => "Offene Entschuldigungen", "upcoming" => "Anstehende Termine",
- "corrections" => "Offene Korrekturen", "unplanned" => "Ungeplante Stunden", "alerts" => "Auffälligkeiten",
- "attendance" => "Fehlzeiten-Warnung", "support" => "Förderplan-Wiedervorlage",
- "groups" => "Meine Lerngruppen", "examload" => "Klausurwochen",
- "missingteachingtime" => "Unterrichtszeit nacherfassen", _ => key,
+ "upcoming" => "Anstehende Termine", "groups" => "Meine Lerngruppen", "examload" => "Klausurwochen",
+ "attention" => "Handlungsbedarf", _ => key,
};
+ // ── Handlungsbedarf: Filter-Chips ─────────────────────────────────────────
+ //
+ // Ersetzt die frühere Sichtbarkeit je Einzelkachel (sieben Schalter im "Bereiche anpassen"-
+ // Panel) durch Filter-Chips innerhalb der zusammengefassten Karte — dichter, und der
+ // naheliegende Ort, weil alle sieben jetzt eine Karte sind. Bewusst nur für die Dauer der
+ // Sitzung (keine Persistenz über DashboardSettingsService): das JSON-Format dort ist eine
+ // flache Liste von Kachel-Einstellungen, eine zweite Objektform nur für diese sieben Filter
+ // hätte das Dateiformat aufgespalten, ohne dass "welche Handlungsbedarf-Art blende ich
+ // dauerhaft aus" bisher als Bedürfnis geäußert wurde.
+
+ private void LoadAttentionFilters()
+ {
+ AttentionFilters.Clear();
+ foreach (var (kind, header) in AttentionGroupOrder)
+ {
+ var option = new AttentionFilterOption(kind, header) { OnChanged = RebuildAttention };
+ AttentionFilters.Add(option);
+ }
+ }
+
+ private bool IsAttentionFilterActive(AttentionKind kind) =>
+ AttentionFilters.FirstOrDefault(f => f.Kind == kind)?.IsActive ?? true;
+
+ /// Setzt Attention aus den bereits geladenen _xyzItems-Feldern neu zusammen — reine
+ /// Umsortierung/Filterung im Speicher, kein Repo-Zugriff. Wird nach jedem Load() sowie nach
+ /// jeder punktuellen Änderung (Entschuldigung aufgelöst, Zeit nacherfasst, Filter-Chip
+ /// umgeschaltet) aufgerufen.
+ private void RebuildAttention()
+ {
+ var byKind = new Dictionary>
+ {
+ [AttentionKind.MissingTeachingTime] = _missingTimeItems,
+ [AttentionKind.Excuse] = _excuses.Select(ToAttentionItem).ToList(),
+ [AttentionKind.Correction] = _correctionItems,
+ [AttentionKind.Unplanned] = _unplannedItems,
+ [AttentionKind.Alert] = _alertItems,
+ [AttentionKind.Attendance] = _attendanceItems,
+ [AttentionKind.SupportPlan] = _supportItems,
+ };
+
+ Attention.Clear();
+ foreach (var (kind, header) in AttentionGroupOrder)
+ {
+ if (!IsAttentionFilterActive(kind)) continue;
+ var items = byKind[kind];
+ if (items.Count == 0) continue;
+ Attention.Add(new AttentionGroup(kind, header, items));
+ }
+ }
+
+ private static AttentionItem ToAttentionItem(OpenExcuseItem e) => new(
+ AttentionKind.Excuse, e.StudentName, subtitle: $"{e.GroupName} · {e.DateDisplay}",
+ actions: [new AttentionAction("Entschuldigt", e.MarkExcusedCommand),
+ new AttentionAction("Unentschuldigt", e.MarkUnexcusedCommand)]);
+
// Zaehlt bewusst EffectiveIsVisible, nicht IsVisible: eine eingeschaltete, aber gerade leere
// HideWhenEmpty-Kachel wird nicht gerendert und darf deshalb auch keinen Rasterplatz belegen,
// sonst bleibt an ihrer Stelle eine Luecke im zweispaltigen Grid.
@@ -721,11 +809,10 @@ public partial class DashboardViewModel : ObservableObject
SaveAndApplyCardLayout();
}
- [RelayCommand] private void OpenStudentAttendance(AttendanceWarningItem? item)
- { if (item is not null) OnNavigateToStudent?.Invoke(item.StudentId); }
-
- [RelayCommand] private void OpenStudentSupportPlan(SupportPlanDueItem? item)
- { if (item is not null) OnNavigateToStudent?.Invoke(item.StudentId); }
+ /// Ersetzt die früheren fünf eigenen Navigations-Commands (OpenStudentAttendance,
+ /// OpenStudentSupportPlan, OpenCorrection, OpenUnplannedLesson, OpenAlert) — jedes AttentionItem
+ /// trägt sein Sprungziel bereits als Closure in Navigate.
+ [RelayCommand] private void OpenAttentionItem(AttentionItem? item) => item?.Navigate?.Invoke();
[RelayCommand]
private async Task AddMissingTeachingTime(MissingTeachingTimeItem? item)
@@ -733,12 +820,13 @@ public partial class DashboardViewModel : ObservableObject
if (item is null || OnAddMissingTeachingTime is null) return;
await OnAddMissingTeachingTime(item);
LoadMissingTeachingTime(DateOnly.FromDateTime(DateTime.Today));
+ RebuildAttention();
UpdateDashboardSummary();
}
private void LoadOpenExcuses(List groups, DateOnly today)
{
- OpenExcuses.Clear();
+ _excuses.Clear();
var cutoff = today.AddDays(-OpenExcuseMaxAgeDays);
var items = new List();
@@ -757,8 +845,7 @@ public partial class DashboardViewModel : ObservableObject
}
}
}
- foreach (var item in items.OrderBy(i => i.Date))
- OpenExcuses.Add(item);
+ _excuses.AddRange(items.OrderBy(i => i.Date));
}
private void ResolveExcuse(OpenExcuseItem item, AttendanceStatus status)
@@ -767,7 +854,8 @@ public partial class DashboardViewModel : ObservableObject
if (entry is null) return;
entry.Attendance = status;
_participationEntries.Save(entry);
- OpenExcuses.Remove(item);
+ _excuses.Remove(item);
+ RebuildAttention();
UpdateDashboardSummary();
}
@@ -968,12 +1056,6 @@ public partial class DashboardViewModel : ObservableObject
else OnNavigateToGroup?.Invoke(groupId);
}
}
- [RelayCommand] private void OpenCorrection(CorrectionProgressItem? item)
- { if (item is not null) OnNavigateToExam?.Invoke(item.GroupId); }
- [RelayCommand] private void OpenUnplannedLesson(UnplannedLessonItem? item)
- { if (item is not null) OnNavigateToUnplannedLesson?.Invoke(item.GroupId); }
- [RelayCommand] private void OpenAlert(DashboardAlertItem? item)
- { if (item is not null) OnNavigateToStudent?.Invoke(item.StudentId); }
[RelayCommand] private void Refresh() => Load();
[RelayCommand] private Task AddTask() => AddTaskInternal(startAsReminder: false);
@@ -1122,26 +1204,6 @@ public class MissingTeachingTimeItem(DateOnly date, TimeOnly windowStart, TimeOn
public string DateDisplay { get; } = date.ToString("dddd, dd.MM.", De);
}
-// ── Förderplan-Wiedervorlage (5.3.2) ──────────────────────────────────────────
-
-public class SupportPlanDueItem
-{
- public Guid StudentId { get; }
- public string StudentName { get; }
- public string Title { get; }
- public string ReviewDateDisplay { get; }
- public bool IsOverdue { get; }
-
- public SupportPlanDueItem(Guid studentId, string studentName, string title, DateOnly reviewDate, DateOnly today)
- {
- StudentId = studentId;
- StudentName = studentName;
- Title = title;
- ReviewDateDisplay = reviewDate.ToString("dd.MM.yyyy");
- IsOverdue = reviewDate < today;
- }
-}
-
public partial class CalendarDayCell : ObservableObject
{
[ObservableProperty] private bool _isSelected;
@@ -1217,46 +1279,8 @@ public sealed class UpcomingDateItem(UpcomingDateKind kind, DateOnly date, strin
};
}
-public sealed class CorrectionProgressItem(Guid examId, Guid groupId, string title, string groupName,
- DateOnly date, int completed, int total, DateOnly today)
-{
- public Guid ExamId { get; } = examId;
- public Guid GroupId { get; } = groupId;
- public string Title { get; } = title;
- public string GroupName { get; } = groupName;
- public DateOnly Date { get; } = date;
- public int Completed { get; } = completed;
- public int Total { get; } = total;
- public int Percent => Total == 0 ? 0 : (int)Math.Round(Completed * 100.0 / Total);
- public string ProgressDisplay => $"{Completed} von {Total} Arbeiten bewertet";
- public string DateDisplay => Date.ToString("dd.MM.yyyy");
- public bool IsOverdue => Date < today.AddDays(-7) && Completed < Total;
-}
-
-public sealed class UnplannedLessonItem(Guid groupId, string groupName, DateOnly date, int periodNumber, DateOnly today)
-{
- public Guid GroupId { get; } = groupId;
- public string GroupName { get; } = groupName;
- public DateOnly Date { get; } = date;
- public int PeriodNumber { get; } = periodNumber;
- public string DateDisplay => Date == today ? "Heute" : Date == today.AddDays(1) ? "Morgen" : Date.ToString("dd.MM.");
- public string Display => $"{GroupName} · {PeriodNumber}. Stunde";
-}
-
public enum AlertSeverity { Medium, High }
-public sealed class DashboardAlertItem(Guid studentId, Guid? groupId, string studentName,
- string kindLabel, string detail, AlertSeverity severity)
-{
- public Guid StudentId { get; } = studentId;
- public Guid? GroupId { get; } = groupId;
- public string StudentName { get; } = studentName;
- public string KindLabel { get; } = kindLabel;
- public string Detail { get; } = detail;
- public AlertSeverity Severity { get; } = severity;
- public string SeverityColor => Severity == AlertSeverity.High ? "#D32F2F" : "#F59E0B";
-}
-
public partial class DashboardCardOption : ObservableObject
{
[ObservableProperty] private bool _isVisible;
@@ -1281,8 +1305,7 @@ public partial class DashboardCardOption : ObservableObject
{
Key = key;
Title = title;
- HideWhenEmpty = key is "excuses" or "upcoming" or "corrections" or "unplanned"
- or "alerts" or "attendance" or "support";
+ HideWhenEmpty = key is "upcoming" or "attention";
_isVisible = isVisible;
}
diff --git a/LehrerApp.Desktop/Views/Dashboard/DashboardView.axaml b/LehrerApp.Desktop/Views/Dashboard/DashboardView.axaml
index a62b98e..452c79e 100644
--- a/LehrerApp.Desktop/Views/Dashboard/DashboardView.axaml
+++ b/LehrerApp.Desktop/Views/Dashboard/DashboardView.axaml
@@ -20,6 +20,28 @@
+
+
+
+
+
+
@@ -142,7 +164,8 @@
-
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Item)
+ hindurch, ohne $parent[ItemsControl] mit Ancestor-Level zaehlen zu muessen. -->
+
-
-
-
-
-
-
-
-
-
-
-
-
+
+
-
-
-
-
-
+
+
+
+
-
-
+
+
-
-
-
-
-
-
-
-
+
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
diff --git a/TODO.md b/TODO.md
index 18a3630..643bcb6 100644
--- a/TODO.md
+++ b/TODO.md
@@ -3228,6 +3228,43 @@ tatsächlichen Lesson oder einem an diesem Tag aktiven Stundenplan-Slot zuerst a
einem Akzentpunkt markiert; innerhalb dieser Gruppe sowie für den Rest gilt alphabetische Sortierung.
Ferien, Feiertage und vollständig ausgefallene Stunden werden dabei berücksichtigt.
+**Nachtrag Kachelraster-Bugfix (September 2026):** `ApplyCardLayout()` zählte bislang `IsVisible`
+statt `EffectiveIsVisible` für die Zeilen-/Spaltenzuordnung — eine eingeschaltete, aber gerade leere
+`HideWhenEmpty`-Kachel (im Alltag der Normalfall, da meist mehrere Hinweiskacheln leer sind) belegte
+dadurch weiterhin einen Rasterplatz und riss eine Lücke ins zweispaltige Grid. Zusätzlich hing der
+Grid-Margin jeder Kachel fest im XAML an einer Spalte (links/rechts), obwohl Spalte und Zeile erst
+zur Laufzeit aus Sichtbarkeit und Reihenfolge berechnet werden — beim Aus-/Einblenden einer Kachel
+wanderten die Nachbarn in die andere Spalte, der Rinnstein saß dann auf der falschen Seite.
+`DashboardCardOption.Margin` leitet den Wert jetzt aus `Column` ab, das XAML bindet darauf statt
+fixer Werte, und `UpdateDashboardSummary()` ruft `ApplyCardLayout()` erst auf, nachdem alle
+`IsEmpty`-Werte für den aktuellen Ladevorgang feststehen.
+
+**Nachtrag Handlungsbedarf-Zusammenfassung (September 2026, Nutzer-Feedback):** Die sieben Kacheln,
+die tatsächlich "wo muss ich reagieren" beantworten — Offene Entschuldigungen (9), Fehlzeiten-
+Warnung (5.2.3), Förderplan-Wiedervorlage (5.3.2), Offene Korrekturen (9.4), Ungeplante Stunden
+(9.9), Auffälligkeiten (9.5), Unterrichtszeit nacherfassen — sind zu einer Karte "Handlungsbedarf"
+mit Filter-Chips zusammengefasst. Neues gemeinsames Modell `AttentionItem`/`AttentionGroup`/
+`AttentionAction` ([AttentionItem.cs](LehrerApp.Desktop/ViewModels/AttentionItem.cs)) trägt nur
+Anzeigedaten; die Beschaffung bleibt unverändert in den jeweiligen `DashboardViewModel.LoadXxx`-
+Methoden, die am Ende ein `AttentionItem` statt ihrer eigenen Item-Klasse erzeugen.
+`DashboardViewModel.RebuildAttention()` gruppiert nach Art (feste Reihenfolge, keine
+kachel-übergreifende Neusortierung nach Dringlichkeit — das hätte die je Art bewusst gewählte
+Sortierung, z.B. Fehlzeiten nach Quote absteigend, wieder zerstört) und wendet die Filter-Chips an,
+ohne die Repos erneut abzufragen. `SupportPlanDueItem`, `CorrectionProgressItem`,
+`UnplannedLessonItem` und `DashboardAlertItem` entfallen (nur dashboard-intern verwendet);
+`OpenExcuseItem` und `AttendanceWarningItem` bleiben bestehen, da `GroupOverviewViewModel`
+(Kurs-Dashboard) sie weiterhin direkt nutzt. Fünf Navigations-Commands (`OpenStudentAttendance`,
+`OpenStudentSupportPlan`, `OpenCorrection`, `OpenUnplannedLesson`, `OpenAlert`) wurden durch ein
+einziges `OpenAttentionItemCommand` ersetzt, das das im `AttentionItem` mitgegebene `Navigate`-
+Delegate aufruft. **Bewusste Verhaltensänderung:** die "Auffälligkeiten"-Kachel zeigte bisher
+Fehlzeiten-Überschreitungen zusätzlich zu Notenabfall/Versetzungsgefährdung — in der
+zusammengefassten Karte wäre das dieselbe Zahl doppelt (einmal als eigene Gruppe "Fehlzeiten-
+Warnung", einmal als "Auffälligkeit"), was vor dem Merge durch zwei getrennte Kacheln nicht auffiel;
+`LoadAlerts` erzeugt diese Kopie deshalb nicht mehr. Die Filter-Chips sind bewusst nur
+Sitzungszustand (keine Persistenz in `dashboardsettings.json`, das dortige Format ist eine flache
+Kachel-Liste ohne Platz für Sub-Filter je Art). "Klausurwochen" bleibt weiterhin eine eigene Kachel
+(Wochen-Aggregat, kein Einzelvorgang zum Abhaken/Anklicken).
+
---
## 10. Sync & Server