Klassenlehrer: faire Fehlquote, Muster-Score statt Heute-Snapshot
CI / build-and-test (push) Canceled after 0s
CI / build-and-test (push) Canceled after 0s
Verspätung zählt in der Fehlquote nicht mehr wie ein voller Fehltag, sondern separat mit reduziertem Gewicht. Neuer "Gesamtbild"-Reiter (Standardansicht) sortiert die Klasse nach einem kumulierten Auffälligkeits-Score übers Schuljahr statt nur nach dem heutigen Status, und bezieht dabei auch Klassenbucheinträge anderer Lehrkräfte ein (Schlüsselwörter für Hausaufgaben/Mitarbeit, "suspendiert" schießt sofort deutlich hoch). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -228,6 +228,198 @@ public sealed class ClassTeacherViewModelsTests
|
||||
Assert.Null(row.YearSummaryTooltip);
|
||||
}
|
||||
|
||||
// ── Fehlquote behandelt Verspätung nicht mehr wie einen ganzen Fehltag (Nutzer-Feedback) ────
|
||||
|
||||
[Fact]
|
||||
public void RosterBuild_ReineVerspaetungZaehltNichtInDieFehlquote()
|
||||
{
|
||||
// Nutzer-Feedback: "10× 5 Min. verspätet" wurde bisher exakt wie "10× unentschuldigt
|
||||
// gefehlt" behandelt. Reine (nicht unentschuldigte) Verspätung darf die Quote nicht mehr
|
||||
// treiben, taucht aber weiterhin als eigene Zahl auf.
|
||||
var students = new[] { Student(1001, "Ada Müller") };
|
||||
var yearAbsences = Enumerable.Range(0, 3)
|
||||
.Select(i => new ClassAbsenceDaySummaryRow(new DateOnly(2026, 8, 10 + i), "Müller Ada", 1001, 0, 5,
|
||||
["Deu"], [1], ["entsch."], ["Verspätung"], null, null, false))
|
||||
.ToArray();
|
||||
|
||||
var row = Assert.Single(ClassTeacherRosterRow.Build(students, [], [],
|
||||
new DateOnly(2026, 8, 26), yearAbsences, schoolDaysElapsed: 20, termStart: new DateOnly(2026, 8, 1)));
|
||||
|
||||
Assert.Equal(0, row.YearAbsenceDayCount);
|
||||
Assert.Equal(3, row.YearLateDayCount);
|
||||
Assert.Equal(0, row.YearAbsenceRatePercent);
|
||||
Assert.True(row.HasYearSummary);
|
||||
Assert.Contains("verspätet", row.YearSummaryLabel);
|
||||
Assert.DoesNotContain("%", row.YearSummaryLabel);
|
||||
Assert.Contains("3 Tage verspätet", row.YearSummaryTooltip);
|
||||
Assert.Contains("zählt nicht zur Quote", row.YearSummaryTooltip);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RosterBuild_UnentschuldigtUndVerspaetetZaehltAlsUnentschuldigtNichtAlsVerspaetung()
|
||||
{
|
||||
// Wer unentschuldigt UND verspätet ist, bleibt der schwerwiegendere Fall (dieselbe
|
||||
// Rangfolge wie AttentionRank/StatusKind/BuildTrend: Unentschuldigt vor Verspätet) — zählt
|
||||
// deshalb weiter voll in die Quote statt in YearLateDayCount zu verschwinden.
|
||||
var students = new[] { Student(1001, "Ada Müller") };
|
||||
var yearAbsences = new[]
|
||||
{
|
||||
new ClassAbsenceDaySummaryRow(new DateOnly(2026, 8, 10), "Müller Ada", 1001, 0, 60,
|
||||
["Deu"], [1], ["nicht entsch."], ["Verspätung"], null, null, false),
|
||||
};
|
||||
|
||||
var row = Assert.Single(ClassTeacherRosterRow.Build(students, [], [],
|
||||
new DateOnly(2026, 8, 26), yearAbsences, schoolDaysElapsed: 20, termStart: new DateOnly(2026, 8, 1)));
|
||||
|
||||
Assert.Equal(1, row.YearAbsenceDayCount);
|
||||
Assert.Equal(1, row.YearUnexcusedDayCount);
|
||||
Assert.Equal(0, row.YearLateDayCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PatternScore_GewichtetUnentschuldigtDeutlichStaerkerAlsVerspaetung()
|
||||
{
|
||||
// Kernfall aus dem Nutzer-Feedback: 10× 5 Minuten verspätet darf nicht denselben Score
|
||||
// ergeben wie 10× ganztägig unentschuldigt gefehlt.
|
||||
var lateStudent = Student(1001, "Verpennt Timo");
|
||||
var truantStudent = Student(1002, "Geschwaenzt Cem");
|
||||
var yearAbsences = Enumerable.Range(0, 10)
|
||||
.SelectMany(i => new[]
|
||||
{
|
||||
new ClassAbsenceDaySummaryRow(new DateOnly(2026, 8, 3 + i), "Verpennt Timo", 1001, 0, 5,
|
||||
["Deu"], [1], ["entsch."], ["Verspätung"], null, null, false),
|
||||
new ClassAbsenceDaySummaryRow(new DateOnly(2026, 8, 3 + i), "Geschwaenzt Cem", 1002, 6, 270,
|
||||
["Deu"], [1, 2, 3, 4, 5, 6], ["nicht entsch."], ["Absent"], null, null, true),
|
||||
})
|
||||
.ToArray();
|
||||
|
||||
var roster = ClassTeacherRosterRow.Build([lateStudent, truantStudent], [], [],
|
||||
new DateOnly(2026, 8, 26), yearAbsences, schoolDaysElapsed: 20, termStart: new DateOnly(2026, 8, 1));
|
||||
|
||||
var timo = Assert.Single(roster, r => r.StudentName == "Verpennt Timo");
|
||||
var cem = Assert.Single(roster, r => r.StudentName == "Geschwaenzt Cem");
|
||||
Assert.Equal(5.0, timo.PatternScore);
|
||||
Assert.Equal(30.0, cem.PatternScore);
|
||||
Assert.True(cem.PatternScore > timo.PatternScore * 5);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PatternScore_ErfasstMusterAuchWennHeuteAnwesend()
|
||||
{
|
||||
// Nutzer-Feedback: ein/e Schüler*in, die/der heute da ist, aber zuvor mehrfach unentschuldigt
|
||||
// fehlte, soll nicht spurlos aus der Musterauswertung verschwinden — anders als bei der
|
||||
// reinen "heute auffällig"-Sortierung (HasAbsenceToday).
|
||||
var student = Student(1001, "Ada Müller");
|
||||
var yearAbsences = Enumerable.Range(0, 5)
|
||||
.Select(i => new ClassAbsenceDaySummaryRow(new DateOnly(2026, 8, 10 + i), "Müller Ada", 1001, 6, 270,
|
||||
["Deu"], [1, 2, 3, 4, 5, 6], ["nicht entsch."], ["Absent"], null, null, true))
|
||||
.ToArray();
|
||||
|
||||
// Kein todayAbsences-Eintrag: heute anwesend.
|
||||
var row = Assert.Single(ClassTeacherRosterRow.Build([student], [], [],
|
||||
new DateOnly(2026, 8, 26), yearAbsences, schoolDaysElapsed: 20, termStart: new DateOnly(2026, 8, 1)));
|
||||
|
||||
Assert.False(row.HasAbsenceToday);
|
||||
Assert.True(row.PatternScore > 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PatternScore_IstNullOhneJahresdaten()
|
||||
{
|
||||
var students = new[] { Student(1001, "Ada Müller") };
|
||||
|
||||
var row = Assert.Single(ClassTeacherRosterRow.Build(students, [], [], new DateOnly(2026, 8, 26)));
|
||||
|
||||
Assert.Equal(0.0, row.PatternScore);
|
||||
}
|
||||
|
||||
// ── Klassenbucheinträge anderer Lehrkräfte fließen ebenfalls in den Score ein ────────────────
|
||||
|
||||
[Fact]
|
||||
public void PatternScore_NegativeKlassenbucheintraegeErhoehenDenScoreAuchOhneFehlzeiten()
|
||||
{
|
||||
var student = Student(1001, "Ada Müller");
|
||||
var yearRegister = Enumerable.Range(0, 4)
|
||||
.Select(i => new UntisForeignClassRegisterEventDto("6a", 20260810 + i, "Deu", "Müller Ada", "test",
|
||||
"Unterrichtsstörung", "Negativ", "Gestört"))
|
||||
.ToArray();
|
||||
|
||||
var row = Assert.Single(ClassTeacherRosterRow.Build([student], [], [],
|
||||
new DateOnly(2026, 8, 26), schoolDaysElapsed: 20, termStart: new DateOnly(2026, 8, 1),
|
||||
yearClassRegisterEntries: yearRegister));
|
||||
|
||||
Assert.Equal(4, row.YearNegativeClassRegisterCount);
|
||||
Assert.Equal(4.0, row.PatternScore);
|
||||
Assert.True(row.HasYearSummary);
|
||||
Assert.Contains("negative Klassenbucheinträge", row.YearSummaryLabel);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PatternScore_HausaufgabenUndMitarbeitSchluesselwoerterZaehlenStaerkerAlsPlainNegativ()
|
||||
{
|
||||
var withKeyword = Student(1001, "Keyword Kim");
|
||||
var plain = Student(1002, "Plain Priya");
|
||||
var yearRegister = new[]
|
||||
{
|
||||
new UntisForeignClassRegisterEventDto("6a", 20260810, "Deu", "Keyword Kim", "test",
|
||||
"Hausaufgaben fehlen", "Negativ", "HA nicht dabei"),
|
||||
new UntisForeignClassRegisterEventDto("6a", 20260810, "Deu", "Plain Priya", "test",
|
||||
"Sonstiges", "Negativ", "Gestört"),
|
||||
};
|
||||
|
||||
var roster = ClassTeacherRosterRow.Build([withKeyword, plain], [], [],
|
||||
new DateOnly(2026, 8, 26), schoolDaysElapsed: 20, termStart: new DateOnly(2026, 8, 1),
|
||||
yearClassRegisterEntries: yearRegister);
|
||||
|
||||
var kim = Assert.Single(roster, r => r.StudentName == "Keyword Kim");
|
||||
var priya = Assert.Single(roster, r => r.StudentName == "Plain Priya");
|
||||
Assert.True(kim.PatternScore > priya.PatternScore);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PatternScore_SuspendiertSchiesstDenScoreWeitUeberVieleAndereEintraegeHinaus()
|
||||
{
|
||||
var suspended = Student(1001, "Suspendiert Sam");
|
||||
var chronic = Student(1002, "Viele Vera");
|
||||
var yearRegister = new List<UntisForeignClassRegisterEventDto>
|
||||
{
|
||||
new("6a", 20260810, "Deu", "Suspendiert Sam", "test", "Ordnungsmaßnahme", "Negativ",
|
||||
"Vom Unterricht suspendiert"),
|
||||
};
|
||||
yearRegister.AddRange(Enumerable.Range(0, 6)
|
||||
.Select(i => new UntisForeignClassRegisterEventDto("6a", 20260810 + i, "Deu", "Viele Vera", "test",
|
||||
"Sonstiges", "Negativ", "Gestört")));
|
||||
|
||||
var roster = ClassTeacherRosterRow.Build([suspended, chronic], [], [],
|
||||
new DateOnly(2026, 8, 26), schoolDaysElapsed: 20, termStart: new DateOnly(2026, 8, 1),
|
||||
yearClassRegisterEntries: yearRegister);
|
||||
|
||||
var sam = Assert.Single(roster, r => r.StudentName == "Suspendiert Sam");
|
||||
var vera = Assert.Single(roster, r => r.StudentName == "Viele Vera");
|
||||
Assert.True(sam.HasSuspensionEntry);
|
||||
Assert.True(sam.PatternScore > vera.PatternScore);
|
||||
Assert.Contains("Suspendierung vermerkt", sam.YearSummaryTooltip);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PatternScore_OhneEigeneJahresklassenbuchlisteFaelltAufDenUebergebenenParameterZurueck()
|
||||
{
|
||||
// Rückwärtskompatibilität: Aufrufer, die (wie die bestehenden Tests) nur den ursprünglichen
|
||||
// recentClassRegisterEntries-Parameter befüllen, sollen trotzdem einen Score aus diesen
|
||||
// Einträgen bekommen statt stillschweigend leer auszugehen.
|
||||
var student = Student(1001, "Ada Müller");
|
||||
var entries = new[]
|
||||
{
|
||||
new UntisForeignClassRegisterEventDto("6a", 20260810, "Deu", "Müller Ada", "test",
|
||||
"Sonstiges", "Negativ", "Gestört"),
|
||||
};
|
||||
|
||||
var row = Assert.Single(ClassTeacherRosterRow.Build([student], [], entries,
|
||||
new DateOnly(2026, 8, 26)));
|
||||
|
||||
Assert.True(row.PatternScore > 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RosterBuild_OrdnetJahresfehlzeitenAuchOhneExternKeyUeberNamenZu()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user