Zurückgestellte Dashboard-Warnungen in eigener Gruppe sammeln
CI / build-and-test (push) Canceled after 0s
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:
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]
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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/
|
||||
|
||||
Reference in new issue
Block a user