Stundenweise Fehlzeiten getrennt auswerten
This commit is contained in:
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",
|
||||
|
||||
Reference in new issue
Block a user