Zurückgestellte Dashboard-Warnungen in eigener Gruppe sammeln
CI / build-and-test (push) Canceled after 0s

Entschärfte Warnungen (zurückgestellt oder Vorgang in Bearbeitung) liefen
bisher grau am Ende ihrer Art-Gruppe mit und zählten weiter zu den offenen
Punkten. Sie stehen jetzt in einer eingeklappten "Zurückgestellt"-Gruppe
ganz unten, mit dem Grund als Untertitel, und zählen nicht mehr mit. Nach
Fristablauf kehren sie automatisch in ihre Gruppe und die Zählung zurück.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Opus 5.5 committed 2026-09-28 09:20:16 +02:00
1 parent 7d8deaf79c
commit 770a1833c8
5 files changed
+184 -47

No files matched your search

@@ -84,6 +84,11 @@ public sealed class DashboardViewModelTests
private static IEnumerable<AttentionItem> Items(DashboardViewModel vm, AttentionKind kind) =>
vm.Attention.Where(g => g.Kind == kind).SelectMany(g => g.Items);
/// Entschärfte Warnungen stehen nicht in ihrer Art-Gruppe, sondern gesammelt in der
/// "Zurückgestellt"-Gruppe am Ende.
private static IEnumerable<AttentionItem> DeferredItems(DashboardViewModel vm) =>
vm.Attention.Where(g => g.IsDeferred).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).
@@ -973,7 +978,7 @@ public sealed class DashboardViewModelTests
}
[Fact]
public void AttendanceWarnings_MitKuerzlichBearbeitetemVorgang_IstEntschaerftUndAmEndeSortiert()
public void AttendanceWarnings_MitKuerzlichBearbeitetemVorgang_WandertInZurueckgestelltGruppe()
{
var today = DateOnly.FromDateTime(DateTime.Today);
var group = new LearningGroup { Name = "9c" };
@@ -992,12 +997,13 @@ public sealed class DashboardViewModelTests
students: new FakeStudents([handled, active]), sessions: sessions, entries: entries,
vorgaenge: vorgaenge);
var items = Items(vm, AttentionKind.Attendance).ToList();
Assert.Equal(2, items.Count);
Assert.False(items[0].IsDefused);
Assert.Equal(active.FullName, items[0].Title);
Assert.True(items[1].IsDefused);
Assert.Equal(handled.FullName, items[1].Title);
var item = Assert.Single(Items(vm, AttentionKind.Attendance));
Assert.False(item.IsDefused);
Assert.Equal(active.FullName, item.Title);
var deferred = Assert.Single(DeferredItems(vm));
Assert.True(deferred.IsDefused);
Assert.Equal(handled.FullName, deferred.Title);
Assert.Equal("Fehlzeiten · Vorgang in Bearbeitung", deferred.Subtitle);
}
[Fact]
@@ -1040,9 +1046,63 @@ public sealed class DashboardViewModelTests
students: new FakeStudents([handled, active]), sessions: sessions, entries: entries,
warningSnoozes: snoozes);
var byName = Items(vm, AttentionKind.Attendance).ToDictionary(i => i.Title);
Assert.True(byName[handled.FullName].IsDefused);
Assert.False(byName[active.FullName].IsDefused);
Assert.Equal(active.FullName, Assert.Single(Items(vm, AttentionKind.Attendance)).Title);
var deferred = Assert.Single(DeferredItems(vm));
Assert.Equal(handled.FullName, deferred.Title);
Assert.Equal($"Fehlzeiten · zurückgestellt bis {today.AddDays(7):dd.MM.}", deferred.Subtitle);
}
[Fact]
public void ZurueckgestelltGruppe_StehtEingeklapptAmEndeUndZaehltNichtMit()
{
var today = DateOnly.FromDateTime(DateTime.Today);
var group = new LearningGroup { Name = "9c" };
var sessions = new FakeSessions([]);
var entries = new FakeEntries();
var (handled, active) = BuildTwoOverThresholdStudents(group, today, sessions, entries);
var snoozes = new FakeDashboardWarningSnoozeRepository();
snoozes.Save(new DashboardWarningSnooze { StudentId = handled.Id, WarningTag = "Absentismus", SnoozedUntil = today.AddDays(7) });
var vm = BuildVm(group, new Lesson { GroupId = group.Id, Date = today },
students: new FakeStudents([handled, active]), sessions: sessions, entries: entries,
warningSnoozes: snoozes);
var last = vm.Attention.Last();
Assert.True(last.IsDeferred);
Assert.Null(last.Kind);
Assert.False(last.IsExpanded);
Assert.Equal(1, vm.AttentionCount);
Assert.True(vm.HasOpenAttention);
}
[Fact]
public void ZurueckgestelltGruppe_AufklappzustandUeberlebtNeuaufbauUndFilterBlendetAus()
{
var today = DateOnly.FromDateTime(DateTime.Today);
var group = new LearningGroup { Name = "9c" };
var sessions = new FakeSessions([]);
var entries = new FakeEntries();
var (handled, active) = BuildTwoOverThresholdStudents(group, today, sessions, entries);
var snoozes = new FakeDashboardWarningSnoozeRepository();
snoozes.Save(new DashboardWarningSnooze { StudentId = handled.Id, WarningTag = "Absentismus", SnoozedUntil = today.AddDays(7) });
snoozes.Save(new DashboardWarningSnooze { StudentId = active.Id, WarningTag = "Absentismus", SnoozedUntil = today.AddDays(7) });
var vm = BuildVm(group, new Lesson { GroupId = group.Id, Date = today },
students: new FakeStudents([handled, active]), sessions: sessions, entries: entries,
warningSnoozes: snoozes);
// Nur noch zurückgestellte Warnungen: kein offener Punkt, Kachel bleibt aber erreichbar.
Assert.Equal(0, vm.AttentionCount);
Assert.False(vm.HasOpenAttention);
Assert.False(vm.AttentionCard.IsEmpty);
vm.Attention.Single(g => g.IsDeferred).ToggleExpandedCommand.Execute(null);
var filter = vm.AttentionFilters.Single(f => f.Kind == AttentionKind.Attendance);
filter.IsActive = false;
Assert.DoesNotContain(vm.Attention, g => g.IsDeferred);
filter.IsActive = true;
Assert.True(vm.Attention.Single(g => g.IsDeferred).IsExpanded);
}
[Fact]
@@ -1078,7 +1138,9 @@ public sealed class DashboardViewModelTests
Assert.Equal(student.Id, saved.StudentId);
Assert.Equal("Absentismus", saved.WarningTag);
Assert.Equal(today.AddDays(14), saved.SnoozedUntil);
Assert.True(Assert.Single(Items(vm, AttentionKind.Attendance)).IsDefused);
Assert.Empty(Items(vm, AttentionKind.Attendance));
Assert.True(Assert.Single(DeferredItems(vm)).IsDefused);
Assert.Equal(0, vm.AttentionCount);
}
[Fact]
+29 -6
View File
@@ -1,4 +1,5 @@
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using System.Windows.Input;
namespace LehrerApp.Desktop.ViewModels;
@@ -53,7 +54,8 @@ public sealed class AttentionItem
/// passendem Tag wurde kürzlich bearbeitet, oder die Warnung wurde manuell zurückgestellt (siehe
/// DashboardViewModel.SnoozeWarningAsync) — beides zeitlich befristet, damit eine ausbleibende
/// Besserung nach Ablauf der Frist wieder auffällt statt dauerhaft entschärft zu bleiben. Steuert
/// nur die Optik (grau statt rot) und die Sortierung (ans Ende der Gruppe); der Schwellenwert-
/// die Optik (grau statt rot), die Einordnung in die eingeklappte "Zurückgestellt"-Gruppe und
/// das Herausnehmen aus DashboardViewModel.AttentionCount; der Schwellenwert-
/// Vergleich selbst bleibt unverändert, die Warnung verschwindet also ganz, sobald sich die
/// zugrunde liegende Zahl tatsächlich bessert.</summary>
public bool IsDefused { get; }
@@ -109,13 +111,34 @@ public sealed class AttentionItem
/// <summary>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.</summary>
public sealed class AttentionGroup(AttentionKind kind, string header, IReadOnlyList<AttentionItem> items)
/// quer über alle Arten würde diese bewusst gewählten Reihenfolgen wieder zerstören.
/// Sonderfall IsDeferred: die "Zurückgestellt"-Gruppe am Ende sammelt entschärfte Warnungen aller
/// drei klassenübergreifenden Arten (Kind deshalb null) und ist ein-/ausklappbar.</summary>
public sealed partial class AttentionGroup : ObservableObject
{
public AttentionKind Kind { get; } = kind;
public string Header { get; } = header;
public IReadOnlyList<AttentionItem> Items { get; } = items;
public AttentionKind? Kind { get; }
public string Header { get; }
public IReadOnlyList<AttentionItem> Items { get; }
public int Count => Items.Count;
public bool IsDeferred { get; }
[ObservableProperty] [NotifyPropertyChangedFor(nameof(ExpandGlyph))] private bool _isExpanded;
public string ExpandGlyph => IsExpanded ? "▾" : "▸";
private readonly Action<bool>? _onExpandedChanged;
public AttentionGroup(AttentionKind? kind, string header, IReadOnlyList<AttentionItem> items,
bool isDeferred = false, bool isExpanded = true, Action<bool>? onExpandedChanged = null)
{
Kind = kind;
Header = header;
Items = items;
IsDeferred = isDeferred;
_isExpanded = isExpanded;
_onExpandedChanged = onExpandedChanged;
}
partial void OnIsExpandedChanged(bool value) => _onExpandedChanged?.Invoke(value);
[RelayCommand] private void ToggleExpanded() => IsExpanded = !IsExpanded;
}
/// <summary>Ein Filter-Chip für eine Handlungsbedarf-Art in der zusammengefassten Karte — ersetzt
@@ -66,8 +66,8 @@ public partial class DashboardViewModel : ObservableObject
/// (alle Gruppen) wird gewarnt.
private const int LatenessWarningThreshold = 8;
/// Nutzer-Feedback: eine Warnung, zu der bereits ein offener Vorgang mit passendem Tag existiert
/// ODER die manuell zurückgestellt wurde, erscheint "entschärft" (grau, ans Ende der Gruppe
/// einsortiert) statt weiter rot hervorgehoben — aber nur befristet: bessert sich die zugrunde
/// ODER die manuell zurückgestellt wurde, erscheint "entschärft" (grau, in der eingeklappten
/// "Zurückgestellt"-Gruppe, nicht mitgezählt) statt weiter rot hervorgehoben — aber nur befristet: bessert sich die zugrunde
/// liegende Zahl innerhalb dieser Frist nicht, soll die Warnung wieder auffallen, statt durch
/// einen dauerhaft offenen Vorgang für immer entschärft zu bleiben. Für den Vorgang-Fall zählt
/// dessen UpdatedAt (jede Interaktion setzt die Frist zurück); für "Zurückstellen" wählt die
@@ -194,9 +194,16 @@ public partial class DashboardViewModel : ObservableObject
public int TodayLessonCount => TodaysLessons.Count;
public int OpenTaskCount => OpenTasks.Count;
public int UpcomingCount => UpcomingDates.Count;
public int AttentionCount => _excuses.Count + _attendanceItems.Count + _supportItems.Count
/// Zurückgestellte/entschärfte Warnungen zählen bewusst nicht mit (Nutzer-Feedback) — sie stehen
/// eingeklappt in der eigenen "Zurückgestellt"-Gruppe, bis ihre Frist abläuft.
public int AttentionCount => _excuses.Count + _supportItems.Count
+ _correctionItems.Count + _unplannedItems.Count + _alertItems.Count + _missingTimeItems.Count
+ _missingHomeworkItems.Count + _latenessItems.Count;
+ CrossClassWarnings.Count(i => !i.IsDefused);
/// Steuert den Hinweis "Keine offenen Punkte." — die eingeklappte "Zurückgestellt"-Gruppe allein
/// gilt nicht als offener Punkt.
public bool HasOpenAttention => Attention.Any(g => !g.IsDeferred);
private IEnumerable<AttentionItem> CrossClassWarnings =>
_attendanceItems.Concat(_latenessItems).Concat(_missingHomeworkItems);
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";
@@ -350,7 +357,9 @@ public partial class DashboardViewModel : ObservableObject
UpcomingCard.IsEmpty = UpcomingDates.Count == 0;
ExamLoadCard.IsEmpty = ExamWeekLoads.Count == 0;
GroupsCard.IsEmpty = CurrentGroups.Count == 0;
AttentionCard.IsEmpty = AttentionCount == 0;
// Nicht über AttentionCount: die Kachel muss auch dann stehen bleiben, wenn nur noch
// zurückgestellte Warnungen übrig sind, sonst wären diese bis zum Fristablauf unerreichbar.
AttentionCard.IsEmpty = AttentionCount == 0 && !CrossClassWarnings.Any();
// Erst nachdem alle IsEmpty-Werte stehen: welche Kachel tatsaechlich gerendert wird, haengt
// ueber EffectiveIsVisible daran, und davon wiederum die Zeilen-/Spaltenzuordnung.
@@ -427,8 +436,8 @@ public partial class DashboardViewModel : ObservableObject
// ── Entschärfen (Nutzer-Feedback) ─────────────────────────────────────────
//
// Eine der drei klassenübergreifenden Warnungen gilt als "entschärft" (grau, ans Ende ihrer
// Gruppe sortiert), solange entweder ein offener Vorgang mit passendem Tag kürzlich bearbeitet
// Eine der drei klassenübergreifenden Warnungen gilt als "entschärft" (grau, in der
// eingeklappten "Zurückgestellt"-Gruppe, siehe RebuildAttention), solange entweder ein offener Vorgang mit passendem Tag kürzlich bearbeitet
// wurde, oder eine manuelle Rückstellung noch läuft — beides zeitlich befristet (siehe
// VorgangDefusalGraceDays-Kommentar), damit ausbleibende Besserung nach Fristablauf wieder
// auffällt statt dauerhaft entschärft zu bleiben. Beide Nachschlage-Mengen bulk-geladen wie
@@ -451,15 +460,24 @@ public partial class DashboardViewModel : ObservableObject
return set;
}
private HashSet<(Guid StudentId, string Tag)> BuildActiveSnoozeSet(DateOnly today) =>
// Bei doppelten Einträgen für dasselbe Paar (z.B. Altbestand vor der abgeleiteten Id) zählt die
// längste noch laufende Frist.
private Dictionary<(Guid StudentId, string Tag), DateOnly> BuildActiveSnoozeSet(DateOnly today) =>
_warningSnoozes.GetAll().Where(s => s.SnoozedUntil >= today)
.Select(s => (s.StudentId, s.WarningTag.ToLowerInvariant())).ToHashSet();
.GroupBy(s => (s.StudentId, s.WarningTag.ToLowerInvariant()))
.ToDictionary(g => g.Key, g => g.Max(s => s.SnoozedUntil));
private static bool IsDefused(Guid studentId, string tag,
HashSet<(Guid StudentId, string Tag)> recentVorgaenge, HashSet<(Guid StudentId, string Tag)> activeSnoozes)
/// Grund, warum eine Warnung entschärft ist, als Untertitel für die "Zurückgestellt"-Gruppe
/// (dort stehen alle drei Warnarten gemischt, deshalb mit Art-Präfix) — null, wenn sie scharf ist.
/// Eine manuelle Rückstellung hat Vorrang, weil sie ein konkretes Ablaufdatum nennen kann.
private static string? DefusalReason(Guid studentId, string tag, string kindLabel,
HashSet<(Guid StudentId, string Tag)> recentVorgaenge,
Dictionary<(Guid StudentId, string Tag), DateOnly> activeSnoozes)
{
var key = (studentId, tag.ToLowerInvariant());
return recentVorgaenge.Contains(key) || activeSnoozes.Contains(key);
if (activeSnoozes.TryGetValue(key, out var until))
return $"{kindLabel} · zurückgestellt bis {until:dd.MM.}";
return recentVorgaenge.Contains(key) ? $"{kindLabel} · Vorgang in Bearbeitung" : null;
}
/// Von der "Zurückstellen"-Aktion einer Warnung aufgerufen (siehe ToAttentionItem-Überladungen
@@ -514,15 +532,15 @@ public partial class DashboardViewModel : ObservableObject
items.Add(new AttendanceWarningItem(student.Id, student.FullName, balance.AbsenceRatePercent));
}
var ordered = items
.Select(i => (Item: i, Defused: IsDefused(i.StudentId, AbsentismusTag, recentVorgaenge, activeSnoozes)))
.OrderBy(x => x.Defused).ThenByDescending(x => x.Item.AbsenceRatePercent);
.Select(i => (Item: i, Defused: DefusalReason(i.StudentId, AbsentismusTag, "Fehlzeiten", recentVorgaenge, activeSnoozes)))
.OrderByDescending(x => x.Item.AbsenceRatePercent);
foreach (var (item, defused) in ordered)
_attendanceItems.Add(ToAttentionItem(item, defused));
}
private AttentionItem ToAttentionItem(AttendanceWarningItem w, bool isDefused) => new(
private AttentionItem ToAttentionItem(AttendanceWarningItem w, string? defusedReason) => new(
AttentionKind.Attendance, w.StudentName, trailingText: $"{w.AbsenceRatePercent:0.#} %",
isWarningTrailing: true, isDefused: isDefused,
subtitle: defusedReason ?? "", isWarningTrailing: true, isDefused: defusedReason is not null,
actions: [SnoozeAction(w.StudentId, AbsentismusTag, $"Fehlzeiten – {w.StudentName}")],
navigate: () => OnNavigateToStudentCase?.Invoke(w.StudentId, AbsentismusTag, "Fehlzeiten"),
detailFactory: () => new StudentWarningDetailViewModel(AttentionKind.Attendance, _evidence.Build(w.StudentId)));
@@ -549,15 +567,15 @@ public partial class DashboardViewModel : ObservableObject
items.Add(new MissingHomeworkWarningItem(student.Id, student.FullName, count));
}
var ordered = items
.Select(i => (Item: i, Defused: IsDefused(i.StudentId, HausaufgabenTag, recentVorgaenge, activeSnoozes)))
.OrderBy(x => x.Defused).ThenByDescending(x => x.Item.MissingCount);
.Select(i => (Item: i, Defused: DefusalReason(i.StudentId, HausaufgabenTag, "Hausaufgaben", recentVorgaenge, activeSnoozes)))
.OrderByDescending(x => x.Item.MissingCount);
foreach (var (item, defused) in ordered)
_missingHomeworkItems.Add(ToAttentionItem(item, defused));
}
private AttentionItem ToAttentionItem(MissingHomeworkWarningItem w, bool isDefused) => new(
private AttentionItem ToAttentionItem(MissingHomeworkWarningItem w, string? defusedReason) => new(
AttentionKind.MissingHomework, w.StudentName, trailingText: $"{w.MissingCount} offen",
isWarningTrailing: true, isDefused: isDefused,
subtitle: defusedReason ?? "", isWarningTrailing: true, isDefused: defusedReason is not null,
actions: [SnoozeAction(w.StudentId, HausaufgabenTag, $"Hausaufgaben – {w.StudentName}")],
navigate: () => OnNavigateToStudentCase?.Invoke(w.StudentId, HausaufgabenTag, "Hausaufgaben"),
detailFactory: () => new StudentWarningDetailViewModel(AttentionKind.MissingHomework, _evidence.Build(w.StudentId)));
@@ -588,15 +606,15 @@ public partial class DashboardViewModel : ObservableObject
items.Add(new LatenessWarningItem(student.Id, student.FullName, count));
}
var ordered = items
.Select(i => (Item: i, Defused: IsDefused(i.StudentId, VerspaetungenTag, recentVorgaenge, activeSnoozes)))
.OrderBy(x => x.Defused).ThenByDescending(x => x.Item.LateCount);
.Select(i => (Item: i, Defused: DefusalReason(i.StudentId, VerspaetungenTag, "Verspätungen", recentVorgaenge, activeSnoozes)))
.OrderByDescending(x => x.Item.LateCount);
foreach (var (item, defused) in ordered)
_latenessItems.Add(ToAttentionItem(item, defused));
}
private AttentionItem ToAttentionItem(LatenessWarningItem w, bool isDefused) => new(
private AttentionItem ToAttentionItem(LatenessWarningItem w, string? defusedReason) => new(
AttentionKind.Lateness, w.StudentName, trailingText: $"{w.LateCount}× verspätet",
isWarningTrailing: true, isDefused: isDefused,
subtitle: defusedReason ?? "", isWarningTrailing: true, isDefused: defusedReason is not null,
actions: [SnoozeAction(w.StudentId, VerspaetungenTag, $"Verspätungen – {w.StudentName}")],
navigate: () => OnNavigateToStudentCase?.Invoke(w.StudentId, VerspaetungenTag, "Verspätungen"),
detailFactory: () => new StudentWarningDetailViewModel(AttentionKind.Lateness, _evidence.Build(w.StudentId)));
@@ -958,15 +976,27 @@ public partial class DashboardViewModel : ObservableObject
};
Attention.Clear();
var deferred = new List<AttentionItem>();
foreach (var (kind, header) in AttentionGroupOrder)
{
if (!IsAttentionFilterActive(kind)) continue;
var items = byKind[kind];
var items = byKind[kind].Where(i => !i.IsDefused).ToList();
deferred.AddRange(byKind[kind].Where(i => i.IsDefused));
if (items.Count == 0) continue;
Attention.Add(new AttentionGroup(kind, header, items));
}
// Entschärfte Warnungen aller drei klassenübergreifenden Arten gesammelt ganz unten und
// eingeklappt (Nutzer-Feedback), statt grau in ihrer jeweiligen Gruppe mitzulaufen. Der
// Aufklapp-Zustand überlebt das Neuaufbauen der Gruppen (z.B. nach einem Zurückstellen).
if (deferred.Count > 0)
Attention.Add(new AttentionGroup(null, "Zurückgestellt", deferred,
isDeferred: true, isExpanded: _deferredGroupExpanded,
onExpandedChanged: expanded => _deferredGroupExpanded = expanded));
OnPropertyChanged(nameof(HasOpenAttention));
}
private bool _deferredGroupExpanded;
private static AttentionItem ToAttentionItem(OpenExcuseItem e) => new(
AttentionKind.Excuse, e.StudentName, subtitle: $"{e.GroupName} · {e.DateDisplay}",
actions: [new AttentionAction("Entschuldigt", e.MarkExcusedCommand),
@@ -40,8 +40,8 @@
</Style>
<!-- Entschärft (Nutzer-Feedback): ein offener Vorgang mit passendem Tag wurde kürzlich
bearbeitet, oder die Warnung wurde zurückgestellt (siehe AttentionItem.IsDefused) — grau
statt rot, damit sie nicht mehr die gleiche Dringlichkeit signalisiert, aber als Erinnerung
sichtbar bleibt. Nach ".attendance" deklariert, damit sie bei gleicher Klassenanzahl gewinnt. -->
statt rot in der eingeklappten "Zurückgestellt"-Gruppe, damit sie nicht mehr die gleiche
Dringlichkeit signalisiert, aber als Erinnerung erreichbar bleibt. Nach ".attendance" deklariert, damit sie bei gleicher Klassenanzahl gewinnt. -->
<Style Selector="Border.severitybar.defused">
<Setter Property="Width" Value="5"/>
<Setter Property="Background" Value="{DynamicResource SystemControlForegroundBaseMediumLowBrush}"/>
@@ -498,13 +498,25 @@
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="vm:AttentionGroup">
<StackPanel Margin="0,6,0,0" Spacing="4">
<TextBlock FontSize="11" FontWeight="SemiBold" Opacity="0.7">
<TextBlock FontSize="11" FontWeight="SemiBold" Opacity="0.7" IsVisible="{Binding !IsDeferred}">
<Run Text="{Binding Header}"/>
<Run Text=" ("/>
<Run Text="{Binding Count}"/>
<Run Text=")"/>
</TextBlock>
<ItemsControl ItemsSource="{Binding Items}">
<!-- "Zurückgestellt"-Gruppe (Nutzer-Feedback): entschärfte Warnungen gesammelt
am Ende, standardmäßig eingeklappt, zählen nicht zu den offenen Punkten. -->
<Button IsVisible="{Binding IsDeferred}" Command="{Binding ToggleExpandedCommand}"
Background="Transparent" BorderThickness="0" Padding="0" Margin="0,6,0,0">
<TextBlock FontSize="11" FontWeight="SemiBold" Opacity="0.7">
<Run Text="{Binding ExpandGlyph}"/>
<Run Text="{Binding Header}"/>
<Run Text=" ("/>
<Run Text="{Binding Count}"/>
<Run Text=")"/>
</TextBlock>
</Button>
<ItemsControl ItemsSource="{Binding Items}" IsVisible="{Binding IsExpanded}">
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="vm:AttentionItem">
<StackPanel Margin="0,4">
@@ -581,7 +593,7 @@
</ItemsControl.ItemTemplate>
</ItemsControl>
<TextBlock Text="Keine offenen Punkte." Classes="emptyhint"
IsVisible="{Binding !Attention.Count}"/>
IsVisible="{Binding !HasOpenAttention}"/>
</StackPanel>
</Border>
+10
View File
@@ -2018,6 +2018,16 @@ eigenen Unterricht abfragt und deshalb mit den regulären Lehrkraft-Rechten funk
Datensatz beschreiben (Last-Write-Wins). Schema-Migration 7 schlüsselt bereits lokal
gespeicherte Einträge auf die abgeleitete Id um. Bestehende lokale Zurückstellungen werden
erst beim nächsten erneuten Zurückstellen übertragen (kein automatischer Nach-Push).
**Nachtrag, eigene "Zurückgestellt"-Gruppe (2026-09-28):** Nutzer-Feedback: entschärfte
Warnungen liefen bisher grau am Ende ihrer Art-Gruppe mit und zählten weiter zu den "offenen
Punkten". `RebuildAttention` sortiert sie jetzt in eine eigene, standardmäßig eingeklappte
Gruppe "Zurückgestellt" ganz unten (`AttentionGroup.IsDeferred`, `Kind` dort null, weil alle drei
Warnarten gemischt stehen; Aufklapp-Zustand überlebt den Neuaufbau). Jeder Eintrag trägt den
Grund als Untertitel ("Fehlzeiten · zurückgestellt bis 12.10." bzw. "… · Vorgang in
Bearbeitung"). `AttentionCount` zählt entschärfte Warnungen nicht mehr mit; die Kachel bleibt
trotzdem sichtbar, solange zurückgestellte Einträge existieren (sonst wären sie bis zum
Fristablauf unerreichbar), und "Keine offenen Punkte." richtet sich nach `HasOpenAttention`.
Die Art-Filter-Chips gelten auch für die Einträge der "Zurückgestellt"-Gruppe.
- **Nachtrag, Detail-Popup und Schnellbausteine (September 2026):** Nutzer-Feedback: (a) im
Dashboard-Eintrag mehr erfahren, (b) beim Anlegen von Vorgang/Dokumentation Unterstützung statt
alles von Hand. Gemeinsame Grundlage ist der neue `StudentWarningEvidenceService` (Desktop/