Stundenweise Fehlzeiten getrennt auswerten

This commit is contained in:
admin committed 2026-09-28 08:39:40 +02:00
1 parent aa326cf8b6
commit e091b5c8de
5 files changed
+136 -44

No files matched your search

@@ -509,11 +509,14 @@ public sealed class ClassTeacherViewModelsTests
termStart: new DateOnly(2026, 8, 1)));
Assert.True(row.HasYearSummary);
Assert.Equal(2, row.YearAbsenceDayCount);
Assert.Equal(1, row.YearAbsenceDayCount);
Assert.Equal(1, row.YearUnexcusedDayCount);
Assert.Equal(10, row.YearAbsenceRatePercent);
Assert.Contains("10 %", row.YearSummaryLabel);
Assert.Contains("2 von 20", row.YearSummaryTooltip);
Assert.Equal(2, row.YearAbsencePeriodCount);
Assert.Equal(2, row.YearExcusedPeriodCount);
Assert.Equal(5, row.YearAbsenceRatePercent);
Assert.Contains("5 %", row.YearSummaryLabel);
Assert.Contains("1 von 20", row.YearSummaryTooltip);
Assert.Contains("2 Fehlstunden", row.YearSummaryTooltip);
Assert.Contains("seit 01.08.", row.YearSummaryTooltip);
Assert.Contains("1 unentschuldigt", row.YearSummaryTooltip);
}
@@ -577,24 +580,30 @@ public sealed class ClassTeacherViewModelsTests
}
[Fact]
public void RosterBuild_UnentschuldigtUndVerspaetetZaehltAlsUnentschuldigtNichtAlsVerspaetung()
public void RosterBuild_UnentschuldigteFehlstundeZaehltNichtAlsGanzerFehltag()
{
// 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.
// Kernfall: Eine Schülerin fehlt aus Protest eine Unterrichtsstunde. Das bleibt
// unentschuldigt und score-relevant, darf im Zeugnis-/Jahresstand aber nicht als ganzer
// unentschuldigter Fehltag erscheinen.
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),
new ClassAbsenceDaySummaryRow(new DateOnly(2026, 8, 10), "Müller Ada", 1001, 1, 45,
["Deu"], [1], ["nicht entsch."], ["Absent"], "Aus Protest gegangen", 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.YearAbsenceDayCount);
Assert.Equal(0, row.YearUnexcusedDayCount);
Assert.Equal(1, row.YearAbsencePeriodCount);
Assert.Equal(1, row.YearUnexcusedPeriodCount);
Assert.Equal(0, row.YearLateDayCount);
Assert.Equal(0, row.YearAbsenceRatePercent);
Assert.Contains("1 Fehlstunde", row.YearSummaryLabel);
Assert.Contains("1 unentschuldigt", row.YearSummaryTooltip);
Assert.Equal(0.5, row.PatternScore, precision: 10); // 1 von typischerweise 6 Stunden × Tagesgewicht 3
}
[Fact]
@@ -754,7 +763,8 @@ public sealed class ClassTeacherViewModelsTests
var row = Assert.Single(ClassTeacherRosterRow.Build(students, [], [], new DateOnly(2026, 8, 26),
yearAbsences, schoolDaysElapsed: 10));
Assert.Equal(1, row.YearAbsenceDayCount);
Assert.Equal(0, row.YearAbsenceDayCount);
Assert.Equal(2, row.YearAbsencePeriodCount);
}
[Fact]
@@ -963,9 +973,9 @@ public sealed class ClassTeacherViewModelsTests
var absences = new[]
{
new ClassAbsenceDaySummaryRow(new DateOnly(2026, 8, 24), "Fehlt Cem", 1001, 1, 45,
["Deu"], [1], ["nicht entsch."], ["Absent"], null, null, false),
["Deu"], [1], ["nicht entsch."], ["Absent"], null, null, true),
new ClassAbsenceDaySummaryRow(new DateOnly(2026, 8, 25), "Fehlt Cem", 1001, 1, 45,
["Deu"], [1], ["nicht entsch."], ["Absent"], null, null, false),
["Deu"], [1], ["nicht entsch."], ["Absent"], null, null, true),
};
var names = new Dictionary<string, string> { [UntisNameMatching.NameKey("Fehlt Cem")] = "Cem Fehlt" };
@@ -978,6 +988,26 @@ public sealed class ClassTeacherViewModelsTests
Assert.False(notice.CanCreateReminder);
}
[Fact]
public void DetectLatePatterns_StundenweisesUnentschuldigtesFehlenBleibtEigeneWarnstufe()
{
var absences = new[]
{
new ClassAbsenceDaySummaryRow(new DateOnly(2026, 9, 21), "Protest Ada", 1001, 1, 45,
["Deu"], [2], ["nicht entsch."], ["Absent"], null, null, false),
new ClassAbsenceDaySummaryRow(new DateOnly(2026, 9, 24), "Protest Ada", 1001, 1, 45,
["Mat"], [4], ["nicht entsch."], ["Absent"], null, null, false),
};
var names = new Dictionary<string, string>
{ [UntisNameMatching.NameKey("Protest Ada")] = "Ada Protest" };
var notice = Assert.Single(ClassTeacherOverviewViewModel.DetectLatePatterns(
absences, names, sevenDayStart: new DateOnly(2026, 9, 20)));
Assert.Contains("stundenweise unentschuldigte Fehlzeiten", notice.Message);
Assert.Equal(ClassTeacherStatusKind.Warning, notice.Kind);
}
// ── Aggregierte Klassenbuch-Kategorien (Feature-Idee 5) ──────────────────
[Fact]
@@ -83,10 +83,12 @@ public sealed class VorgangBausteineTests
Assert.Equal(8, facts.AbsentPeriods);
Assert.Equal(1, facts.UnexcusedDays);
Assert.Equal(2, facts.PartialAbsentPeriods);
Assert.Equal(0, facts.UnexcusedPartialPeriods);
Assert.Single(facts.LateDays);
Assert.Equal(1, facts.NegativeRegisterCount);
Assert.Single(facts.HomeworkEntries);
Assert.Contains("8 Fehlstunden an 2 Tagen", facts.AbsenceShort);
Assert.Contains("1 ganzer Fehltag und 2 weitere Fehlstunden", facts.AbsenceShort);
var labels = facts.QuickBlocks(withName: true).Select(b => b.Label).ToList();
Assert.Contains("Fehlzeiten · Beispiel Anna", labels);
@@ -98,6 +100,23 @@ public sealed class VorgangBausteineTests
Assert.StartsWith("Sehr geehrte Erziehungsberechtigte von Anna Beispiel", letter.ParentLetterData!.DraftContent);
}
[Fact]
public void KlassenlehrerStand_UnentschuldigteFehlstundeWirdNichtAlsFehltagBezeichnet()
{
var student = new StudentOption(Guid.NewGuid(), "Beispiel Anna");
var today = new DateOnly(2026, 9, 28);
var facts = ClassTeacherStudentFacts.Build(student, "Anna Beispiel", "2026/27", today,
[Absence(today.AddDays(-4), "Anna Beispiel", 1, unexcused: true)], []);
Assert.Equal(0, facts.FullDays);
Assert.Equal(0, facts.UnexcusedDays);
Assert.Equal(1, facts.PartialAbsentPeriods);
Assert.Equal(1, facts.UnexcusedPartialPeriods);
Assert.Contains("0 ganze Fehltage und 1 weitere Fehlstunde", facts.AbsenceShort);
Assert.Contains("unentschuldigt: 0 ganze Tage, 1 Fehlstunde", facts.AbsenceShort);
Assert.Contains("1 Fehlstunde · Unentschuldigt", facts.AbsenceBlock);
}
[Fact]
public void Vorlage_KlassenleitungInformieren()
{
@@ -22,7 +22,10 @@ public sealed record ClassTeacherStudentFacts(StudentOption Student, string Natu
public int AbsentPeriods => AbsenceDays.Sum(d => d.TotalAbsentPeriods);
public int FullDays => AbsenceDays.Count(d => d.CountsAsFullDay);
public int UnexcusedDays => AbsenceDays.Count(d => d.IsUnexcused);
public int UnexcusedDays => AbsenceDays.Count(d => d.CountsAsFullDay && d.IsUnexcused);
public int PartialAbsentPeriods => AbsenceDays.Where(d => !d.CountsAsFullDay).Sum(d => d.TotalAbsentPeriods);
public int UnexcusedPartialPeriods => AbsenceDays
.Where(d => !d.CountsAsFullDay && d.IsUnexcused).Sum(d => d.TotalAbsentPeriods);
public int NegativeRegisterCount => RegisterEntries.Count(r => r.IsDangerStatus);
public IReadOnlyList<ClassTeacherClassRegisterRow> HomeworkEntries => RegisterEntries
.Where(r => ($"{r.CategoryName} {r.Text}").Contains("Hausauf", StringComparison.OrdinalIgnoreCase)).ToList();
@@ -30,15 +33,17 @@ public sealed record ClassTeacherStudentFacts(StudentOption Student, string Natu
private string Stand => $"WebUntis-Stand {AsOf:dd.MM.yyyy}";
public string AbsenceShort =>
$"{AbsentPeriods} Fehlstunde{(AbsentPeriods == 1 ? "" : "n")} an {AbsenceDays.Count} Tag{(AbsenceDays.Count == 1 ? "" : "en")} " +
$"im Schuljahr {SchoolYear} (davon {UnexcusedDays} Tag{(UnexcusedDays == 1 ? "" : "e")} unentschuldigt, " +
$"{FullDays} ganze Fehltag{(FullDays == 1 ? "" : "e")})";
$"{FullDayLabel(FullDays)} und " +
$"{PartialAbsentPeriods} weitere Fehlstunde{(PartialAbsentPeriods == 1 ? "" : "n")} " +
$"an {AbsenceDays.Count} Tag{(AbsenceDays.Count == 1 ? "" : "en")} im Schuljahr {SchoolYear} " +
$"(unentschuldigt: {FullDayCountLabel(UnexcusedDays)}, " +
$"{UnexcusedPartialPeriods} Fehlstunde{(UnexcusedPartialPeriods == 1 ? "" : "n")})";
public string LatenessShort => $"{LateDays.Count} Verspätung{(LateDays.Count == 1 ? "" : "en")} im Schuljahr {SchoolYear}";
public string RegisterShort =>
$"{RegisterEntries.Count} Klassenbucheintr{(RegisterEntries.Count == 1 ? "ag" : "äge")} im Schuljahr {SchoolYear}, davon {NegativeRegisterCount} negativ";
public string HomeworkShort => $"{HomeworkEntries.Count} Klassenbucheintr{(HomeworkEntries.Count == 1 ? "ag" : "äge")} zu Hausaufgaben im Schuljahr {SchoolYear}";
public string AbsenceBlock => $"{Stand}: {AbsenceShort}.\n" + List("Fehltage", AbsenceDays.Select(d =>
public string AbsenceBlock => $"{Stand}: {AbsenceShort}.\n" + List("Fehlzeiten", AbsenceDays.Select(d =>
$"{d.DateLabel}: {d.KindLabel} · {d.FriendlyStatusLabel}" +
(string.IsNullOrWhiteSpace(d.DetailLabel) ? "" : $" · {d.DetailLabel}")).ToList());
public string LatenessBlock => $"{Stand}: {LatenessShort}.\n" + List("Termine", LateDays.Select(d =>
@@ -50,6 +55,12 @@ public sealed record ClassTeacherStudentFacts(StudentOption Student, string Natu
$"{r.DateLabel}" + (string.IsNullOrWhiteSpace(r.Subject) ? "" : $" · {r.Subject}") + ": " +
string.Join(" – ", new[] { r.CategoryName, r.Text }.Where(v => !string.IsNullOrWhiteSpace(v)));
private static string FullDayLabel(int count) =>
count == 1 ? "1 ganzer Fehltag" : $"{count} ganze Fehltage";
private static string FullDayCountLabel(int count) =>
count == 1 ? "1 ganzer Tag" : $"{count} ganze Tage";
private static string List(string header, IReadOnlyList<string> lines)
{
var sb = new StringBuilder($"{header}:");
@@ -66,6 +66,11 @@ public sealed record ClassAbsenceDaySummaryRow(DateOnly Date, string StudentName
IReadOnlyList<int> PeriodNumbers, IReadOnlyList<string> Statuses, IReadOnlyList<string> AbsenceReasons,
string? Note, string? ExcuseNote, bool CountsAsFullDay)
{
/// Nur für die Gewichtung von stundenweisen Fehlzeiten im Auffälligkeitsscore. Zeugnis- und
/// Anzeigezahlen verwenden weiterhin die echten WebUntis-Felder CountsAsFullDay bzw.
/// TotalAbsentPeriods und rechnen keine Stunden künstlich in Fehltage um.
public const int TypicalPeriodsPerSchoolDayForScoring = 6;
public string DateLabel => Date.ToString("dd.MM.yyyy");
public string StudentDisplayName
{
@@ -83,6 +88,11 @@ public sealed record ClassAbsenceDaySummaryRow(DateOnly Date, string StudentName
r.Contains("verspät", StringComparison.OrdinalIgnoreCase));
public bool IsUnexcused => Statuses.Any(s =>
s.Contains("nicht entsch", StringComparison.OrdinalIgnoreCase));
public bool IsPartialAbsence => !CountsAsFullDay && (!IsLate || IsUnexcused);
public double AbsenceDayEquivalentForScoring => IsLate && !IsUnexcused ? 0d : CountsAsFullDay ? 1d :
Math.Min(1d, (TotalAbsentPeriods > 0
? TotalAbsentPeriods
: TotalAbsentMinutes / 45d) / TypicalPeriodsPerSchoolDayForScoring);
public string KindLabel => IsLate ? $"{TotalAbsentMinutes} Min. verspätet" :
CountsAsFullDay ? "Ganzer Fehltag" : $"{TotalAbsentPeriods} Fehlstunde{(TotalAbsentPeriods == 1 ? "" : "n")}";
public string FriendlyStatusLabel => IsUnexcused ? "Unentschuldigt" :
@@ -81,21 +81,22 @@ public sealed record ClassTeacherRosterRow(string StudentName, int? ExternKey, b
/// Kumulierte Fehlzeiten seit Schuljahresbeginn (Nutzer-Feedback: der Heute-Snapshot allein
/// sagt für Zeugnis/Attestpflicht wenig aus). <see cref="SchoolDaysElapsed"/> zählt Werktage
/// abzüglich der über WebUntis geladenen Ferien (<see cref="ClassTeacherOverviewViewModel.CountSchoolWeekdays"/>).
/// Zählt bewusst NUR echte Fehltage (unentschuldigt oder entschuldigt abwesend), keine reinen
/// Verspätungstage mehr — Nutzer-Feedback: die Quote behandelte "10× 5 Min. verspätet" bisher
/// exakt wie "10× ganztägig unentschuldigt gefehlt", obwohl beides für die eigene Reaktion
/// (Elterngespräch vs. Achselzucken) grundverschieden ist. Reine Verspätung landet stattdessen
/// separat in <see cref="YearLateDayCount"/> (gleiche Kategorisierung wie schon länger im
/// Wochentrend, siehe <see cref="ClassTeacherOverviewViewModel.BuildTrend"/>: Unentschuldigt vor
/// Verspätet vor Entschuldigt, überschneidungsfrei).
/// Zählt bewusst NUR von WebUntis als ganze Fehltage gekennzeichnete Tage. Stundenweises
/// Fehlen bleibt davon getrennt in <see cref="YearAbsencePeriodCount"/> — auch wenn es
/// unentschuldigt ist. Reine Verspätung landet separat in <see cref="YearLateDayCount"/>.
public int YearAbsenceDayCount { get; init; }
public int YearUnexcusedDayCount { get; init; }
/// Fehlstunden aus nicht ganztägigen Fehlzeiten. Ganze Fehltage werden nicht zusätzlich noch
/// einmal als ihre enthaltenen Unterrichtsstunden gezählt.
public int YearAbsencePeriodCount { get; init; }
public int YearUnexcusedPeriodCount { get; init; }
public int YearExcusedPeriodCount { get; init; }
/// Tage, die WebUntis als Verspätung führt (ohne Unentschuldigt-Status) — zählen bewusst nicht
/// in <see cref="YearAbsenceDayCount"/>/<see cref="YearAbsenceRatePercent"/>, tragen aber mit
/// reduziertem Gewicht zu <see cref="PatternScore"/> bei.
public int YearLateDayCount { get; init; }
/// Entschuldigt abwesende Tage (weder unentschuldigt noch nur verspätet) — Kehrwert von
/// <see cref="YearUnexcusedDayCount"/> innerhalb von <see cref="YearAbsenceDayCount"/>.
/// Entschuldigte ganze Fehltage — Gegenstück zu <see cref="YearUnexcusedDayCount"/> innerhalb
/// von <see cref="YearAbsenceDayCount"/>. Stunden stehen getrennt in den Period-Eigenschaften.
public int YearExcusedDayCount { get; init; }
/// Klassenbucheinträge anderer Lehrkräfte über das ganze bisherige Schuljahr mit
/// <c>CategoryGroup</c> "Negativ" (Nutzer-Feedback: sollen ebenfalls ins Auffälligkeitsbild
@@ -121,23 +122,33 @@ public sealed record ClassTeacherRosterRow(string StudentName, int? ExternKey, b
/// die Korrektur passiert also am Startpunkt, nicht an abgezogenen Tagen mittendrin.
public DateOnly TermStart { get; init; }
public bool HasYearSummary => SchoolDaysElapsed > 0 &&
(YearAbsenceDayCount > 0 || YearLateDayCount > 0 || YearNegativeClassRegisterCount > 0);
(YearAbsenceDayCount > 0 || YearAbsencePeriodCount > 0 || YearLateDayCount > 0 ||
YearNegativeClassRegisterCount > 0);
public int YearAbsenceRatePercent =>
SchoolDaysElapsed <= 0 ? 0 : (int)Math.Round(100d * YearAbsenceDayCount / SchoolDaysElapsed);
public string YearSummaryLabel => !HasYearSummary ? "" :
YearAbsenceDayCount > 0 ? $"{YearAbsenceRatePercent} % Fehlzeit seit Schuljahresbeginn"
YearAbsenceDayCount > 0 ? $"{YearAbsenceRatePercent} % ganze Fehltage" +
(YearAbsencePeriodCount > 0 ? $" · {PeriodLabel(YearAbsencePeriodCount)}" : "")
: YearAbsencePeriodCount > 0 ? $"{PeriodLabel(YearAbsencePeriodCount)} seit Schuljahresbeginn"
: YearLateDayCount > 0 ? $"{YearLateDayCount}× verspätet, keine Fehltage seit Schuljahresbeginn"
: $"{YearNegativeClassRegisterCount} negative Klassenbucheinträge seit Schuljahresbeginn";
public string? YearSummaryTooltip => !HasYearSummary ? null :
(YearAbsenceDayCount > 0
? $"{YearAbsenceDayCount} von {SchoolDaysElapsed} Schultagen seit {TermStart:dd.MM.} mit Fehlzeit" +
(YearUnexcusedDayCount > 0 ? $" · {YearUnexcusedDayCount} unentschuldigt" : "")
: $"Keine Fehltage seit {TermStart:dd.MM.}") +
? $"{YearAbsenceDayCount} von {SchoolDaysElapsed} Schultagen seit {TermStart:dd.MM.} als ganzer Fehltag" +
(YearUnexcusedDayCount > 0 ? $" · davon {YearUnexcusedDayCount} unentschuldigt" : "")
: $"Keine ganzen Fehltage seit {TermStart:dd.MM.}") +
(YearAbsencePeriodCount > 0
? $" · {PeriodLabel(YearAbsencePeriodCount)}" +
(YearUnexcusedPeriodCount > 0 ? $" ({YearUnexcusedPeriodCount} unentschuldigt)" : "")
: "") +
(YearLateDayCount > 0 ? $" · {YearLateDayCount} Tage verspätet (zählt nicht zur Quote)" : "") +
(YearNegativeClassRegisterCount > 0 ? $" · {YearNegativeClassRegisterCount} negative Klassenbucheinträge" : "") +
(HasSuspensionEntry ? " · Suspendierung vermerkt" : "") +
(HolidayWeekdaysExcluded > 0 ? $" · {HolidayWeekdaysExcluded} Ferientage abgezogen" : "");
private static string PeriodLabel(int count) =>
$"{count} Fehlstunde{(count == 1 ? "" : "n")}";
private static readonly string[] ConcerningKeywords = ["hausaufgabe", "mitarbeit"];
private static readonly string[] SuspensionKeywords = ["suspendier"];
@@ -238,8 +249,10 @@ public sealed record ClassTeacherRosterRow(string StudentName, int? ExternKey, b
var attendanceCutoff = cutoffFor(VorgangScoreDomain.Attendance);
var latenessCutoff = cutoffFor(VorgangScoreDomain.Lateness);
var classbookCutoff = cutoffFor(VorgangScoreDomain.Classbook);
var unexcused = yearEntries.Count(r => r.IsUnexcused && r.Date >= attendanceCutoff);
var excused = yearEntries.Count(r => !r.IsUnexcused && !r.IsLate && r.Date >= attendanceCutoff);
var unexcused = yearEntries.Where(r => r.IsUnexcused && r.Date >= attendanceCutoff)
.Sum(r => r.AbsenceDayEquivalentForScoring);
var excused = yearEntries.Where(r => !r.IsUnexcused && !r.IsLate && r.Date >= attendanceCutoff)
.Sum(r => r.AbsenceDayEquivalentForScoring);
var late = yearEntries.Count(r => !r.IsUnexcused && r.IsLate && r.Date >= latenessCutoff);
var classRegister = yearRegisterEntries
.Where(e => TryDate(e.Date, out var d) && d >= classbookCutoff)
@@ -314,13 +327,17 @@ public sealed record ClassTeacherRosterRow(string StudentName, int? ExternKey, b
{
TodayAbsence = absence,
HasClassRegisterToday = registerEntries.Any(e => TryDate(e.Date, out var date) && date == referenceDate),
// Unentschuldigt vor Verspätet vor Entschuldigt, überschneidungsfrei — dieselbe
// Kategorisierung wie im Wochentrend (BuildTrend). Nur Verspätung (nicht auch
// unentschuldigt) fällt bewusst aus YearAbsenceDayCount heraus, siehe dort.
YearAbsenceDayCount = yearEntries.Count(r => r.IsUnexcused || !r.IsLate),
YearUnexcusedDayCount = yearEntries.Count(r => r.IsUnexcused),
// Ganze Fehltage und stundenweises Fehlen bleiben getrennt. Insbesondere macht
// "unentschuldigt" aus einer einzelnen Fehlstunde keinen ganzen Fehltag.
YearAbsenceDayCount = yearEntries.Count(r => !r.IsLate && r.CountsAsFullDay),
YearUnexcusedDayCount = yearEntries.Count(r => !r.IsLate && r.CountsAsFullDay && r.IsUnexcused),
YearAbsencePeriodCount = yearEntries.Where(r => r.IsPartialAbsence).Sum(r => r.TotalAbsentPeriods),
YearUnexcusedPeriodCount = yearEntries.Where(r => r.IsPartialAbsence && r.IsUnexcused)
.Sum(r => r.TotalAbsentPeriods),
YearExcusedPeriodCount = yearEntries.Where(r => r.IsPartialAbsence && !r.IsUnexcused)
.Sum(r => r.TotalAbsentPeriods),
YearLateDayCount = yearEntries.Count(r => !r.IsUnexcused && r.IsLate),
YearExcusedDayCount = yearEntries.Count(r => !r.IsUnexcused && !r.IsLate),
YearExcusedDayCount = yearEntries.Count(r => !r.IsUnexcused && !r.IsLate && r.CountsAsFullDay),
YearNegativeClassRegisterCount = yearRegisterEntries.Count(IsNegativeClassRegisterEntry),
HasSuspensionEntry = yearRegisterEntries.Any(e => MatchesAnyKeyword(e, SuspensionKeywords)),
ClassRegisterScoreComponent = yearRegisterEntries.Sum(e => ClassRegisterEntryWeight(e, effectiveWeights)),
@@ -1017,12 +1034,17 @@ public partial class ClassTeacherOverviewViewModel : ObservableObject
var displayName = displayNamesByKey.GetValueOrDefault(group.Key) ?? rows[0].StudentDisplayName;
var weekRows = rows.Where(row => row.Date >= sevenDayStart).ToList();
var lateDaysWeek = weekRows.Count(row => row.IsLate);
var unexcusedDaysWeek = weekRows.Count(row => row.IsUnexcused && !row.IsLate);
var unexcusedDaysWeek = weekRows.Count(row => row.IsUnexcused && !row.IsLate && row.CountsAsFullDay);
var unexcusedPartialWeek = weekRows.Count(row => row.IsUnexcused && row.IsPartialAbsence);
var lateDaysYear = rows.Count(row => row.IsLate);
if (unexcusedDaysWeek >= 2)
notices.Add(new ClassTeacherPatternNotice(displayName,
$"{unexcusedDaysWeek} unentschuldigte Fehltage in 7 Tagen", ClassTeacherStatusKind.Danger));
else if (unexcusedPartialWeek >= 2)
notices.Add(new ClassTeacherPatternNotice(displayName,
$"{unexcusedPartialWeek} stundenweise unentschuldigte Fehlzeiten in 7 Tagen",
ClassTeacherStatusKind.Warning));
else if (lateDaysYear >= lateYearEscalationThreshold)
notices.Add(new ClassTeacherPatternNotice(displayName,
$"{lateDaysYear} Verspätungen seit Schuljahresbeginn – Elterngespräch oder Brief erwägen",