Klassenlehrer-Tab: vier Feature-Ideen umgesetzt (ohne Elterngesprächs-PDF)

Offene Entschuldigungen mit Frist (neue Seitenspalten-Karte, sortiert nach
Tagen offen), aggregierte Klassenbuch-Kategorien (Chip-Reihe im
Klassenbuch-Tab), Wochentags-Häufung als dritte Mustererkennungsregel, und
eine Wiedervorlagen-Verknüpfung: ein "+"-Button legt direkt aus der Zeile
eine WorkTask "Eltern kontaktieren" an, und eine dritte Musterregel
verknüpft hohe Jahresfehlquote mit fallendem Mitarbeit-Trend.

ContextMenu auf den Zeilen-Buttons scheiterte an Avalonias
$parent-Vorfahren-Bindings (Popup hängt nicht im normalen visuellen Baum) —
stattdessen zwei Geschwister-Buttons pro Zeile. Elterngesprächs-Blatt als
PDF bewusst zurückgestellt, Konzept noch nicht geschärft.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-27 13:57:09 +02:00
co-authored by Claude Sonnet 5
parent 3f8dab4abd
commit 3f484ce3c8
6 changed files with 537 additions and 7 deletions
@@ -264,6 +264,149 @@ public sealed class ClassTeacherViewModelsTests
Assert.Equal(row.StatusText, row.StatusTextWithGlyph);
}
// ── Offene Entschuldigungen (Feature-Idee 2) ─────────────────────────────
[Fact]
public void OpenExcuseBuild_SortiertNachTagenOffenAbsteigendUndIgnoriertEntschuldigte()
{
var absences = new[]
{
new ClassAbsenceDaySummaryRow(new DateOnly(2026, 8, 20), "Alt Ada", 1001, 2, 90,
["Che"], [1, 2], ["nicht entsch."], ["Absent"], null, null, false),
new ClassAbsenceDaySummaryRow(new DateOnly(2026, 8, 25), "Neu Ben", 1002, 1, 45,
["Deu"], [1], ["nicht entsch."], ["Absent"], null, null, false),
new ClassAbsenceDaySummaryRow(new DateOnly(2026, 8, 24), "Entschuldigt Cem", 1003, 1, 45,
["Deu"], [1], ["entsch."], ["Absent"], null, null, false),
};
var rows = ClassTeacherOpenExcuseRow.Build(absences, new DateOnly(2026, 8, 26));
// StudentName kommt aus StudentDisplayName (WebUntis liefert "Nachname Vorname",
// hier für die Anzeige auf "Vorname Nachname" gedreht - siehe ClassAbsenceDaySummaryRow).
Assert.Equal(["Ada Alt", "Ben Neu"], rows.Select(r => r.StudentName));
Assert.Equal(6, rows[0].DaysOpen);
Assert.True(rows[0].IsOverdue);
Assert.Equal(1, rows[1].DaysOpen);
Assert.False(rows[1].IsOverdue);
}
// ── Aggregierte Klassenbuch-Kategorien (Feature-Idee 5) ──────────────────
[Fact]
public void CategoryAggregateBuild_ZaehltProKategorieUndNenntTopSchueler()
{
var entries = new[]
{
new ClassTeacherClassRegisterRow(new DateOnly(2026, 8, 20), "Deu", "Schmidt Ben", "test",
"Hausaufgaben fehlen", "Negativ", "HA fehlt"),
new ClassTeacherClassRegisterRow(new DateOnly(2026, 8, 21), "Deu", "Schmidt Ben", "test",
"Hausaufgaben fehlen", "Negativ", "HA fehlt"),
new ClassTeacherClassRegisterRow(new DateOnly(2026, 8, 22), "Che", "Müller Ada", "test",
"Hausaufgaben fehlen", "Negativ", "HA fehlt"),
new ClassTeacherClassRegisterRow(new DateOnly(2026, 8, 23), "Che", "Müller Ada", "test",
"Lob", "Positiv", "Gute Mitarbeit"),
};
var rows = ClassTeacherCategoryAggregateRow.Build(entries);
Assert.Equal(["Hausaufgaben fehlen", "Lob"], rows.Select(r => r.CategoryName));
var homework = rows[0];
Assert.Equal(3, homework.Count);
Assert.Contains("2× Ben Schmidt", homework.TopStudentsLabel);
Assert.Contains("1× Ada Müller", homework.TopStudentsLabel);
}
// ── Erweiterte Mustererkennung: Wochentags-Häufung (Feature-Idee 4) ──────
[Fact]
public void DetectWeekdayPatterns_ErkenntDeutlicheHaeufungAnEinemWochentag()
{
// 24.08./31.08.2026 und 07.09.2026 sind allesamt Montage.
var absences = new[]
{
new ClassAbsenceDaySummaryRow(new DateOnly(2026, 8, 24), "Muster Mona", 1001, 1, 45,
["Deu"], [1], ["entsch."], ["Absent"], null, null, false),
new ClassAbsenceDaySummaryRow(new DateOnly(2026, 8, 31), "Muster Mona", 1001, 1, 45,
["Deu"], [1], ["entsch."], ["Absent"], null, null, false),
new ClassAbsenceDaySummaryRow(new DateOnly(2026, 9, 7), "Muster Mona", 1001, 1, 45,
["Deu"], [1], ["entsch."], ["Absent"], null, null, false),
};
var names = new Dictionary<string, string> { [UntisNameMatching.NameKey("Muster Mona")] = "Mona Muster" };
var notices = ClassTeacherOverviewViewModel.DetectWeekdayPatterns(
absences, names, new HashSet<string>());
var notice = Assert.Single(notices);
Assert.Equal("Mona Muster", notice.StudentName);
Assert.Contains("Montag", notice.Message);
Assert.Equal(ClassTeacherStatusKind.Info, notice.Kind);
}
[Fact]
public void DetectWeekdayPatterns_IgnoriertZuKleineStichprobe()
{
var absences = new[]
{
new ClassAbsenceDaySummaryRow(new DateOnly(2026, 8, 24), "Muster Mona", 1001, 1, 45,
["Deu"], [1], ["entsch."], ["Absent"], null, null, false),
new ClassAbsenceDaySummaryRow(new DateOnly(2026, 8, 31), "Muster Mona", 1001, 1, 45,
["Deu"], [1], ["entsch."], ["Absent"], null, null, false),
};
var notices = ClassTeacherOverviewViewModel.DetectWeekdayPatterns(
absences, new Dictionary<string, string>(), new HashSet<string>());
Assert.Empty(notices);
}
[Fact]
public void DetectWeekdayPatterns_LaesstBereitsGemeldeteSchuelerAus()
{
var absences = new[]
{
new ClassAbsenceDaySummaryRow(new DateOnly(2026, 8, 24), "Muster Mona", 1001, 1, 45,
["Deu"], [1], ["entsch."], ["Absent"], null, null, false),
new ClassAbsenceDaySummaryRow(new DateOnly(2026, 8, 31), "Muster Mona", 1001, 1, 45,
["Deu"], [1], ["entsch."], ["Absent"], null, null, false),
new ClassAbsenceDaySummaryRow(new DateOnly(2026, 9, 7), "Muster Mona", 1001, 1, 45,
["Deu"], [1], ["entsch."], ["Absent"], null, null, false),
};
var names = new Dictionary<string, string> { [UntisNameMatching.NameKey("Muster Mona")] = "Mona Muster" };
var notices = ClassTeacherOverviewViewModel.DetectWeekdayPatterns(
absences, names, new HashSet<string> { "Mona Muster" });
Assert.Empty(notices);
}
// ── Noten-/Mitarbeit-Brücke: Namensabgleich (Feature-Idee 7) ─────────────
[Fact]
public void MatchStudent_FindetUeberVorUndNachnameTrotzKommaInFullName()
{
// Student.FullName liefert "Nachname, Vorname" (mit Komma) - ein Abgleich darüber würde
// UntisNameMatching.NameKey verfälschen ("Müller," bliebe eigenes Wort). MatchStudent
// muss deshalb FirstName/LastName getrennt verwenden, nicht FullName.
var students = new List<Student>
{
new() { FirstName = "Ada", LastName = "Müller" },
new() { FirstName = "Ben", LastName = "Schmidt" },
};
var match = ClassTeacherOverviewViewModel.MatchStudent("Müller Ada", students);
Assert.NotNull(match);
Assert.Equal("Ada", match!.FirstName);
}
[Fact]
public void MatchStudent_LiefertNullOhneTreffer()
{
var students = new List<Student> { new() { FirstName = "Ada", LastName = "Müller" } };
Assert.Null(ClassTeacherOverviewViewModel.MatchStudent("Unbekannt Xyz", students));
}
private static UntisStudentRosterCacheEntry Student(int? externKey, string displayName) =>
new() { ClassName = "6a", ExternKey = externKey, DisplayName = displayName };
}