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:
@@ -73,8 +73,35 @@ 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).
|
||||
public int YearAbsenceDayCount { get; init; }
|
||||
public int YearUnexcusedDayCount { 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"/>.
|
||||
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
|
||||
/// einfließen — "wenn die negativen Einträge durch die Decke gehen"). Anders als
|
||||
/// <see cref="HasRecentClassRegisterEntry"/> (nur letzte 7 Tage, für die "Klassenbuch"-Badges)
|
||||
/// bewusst über das ganze Jahr, damit sich ein Muster über die Zeit zeigen kann.
|
||||
public int YearNegativeClassRegisterCount { get; init; }
|
||||
/// Mindestens ein Klassenbucheintrag mit dem Schlüsselwort "suspendier(t)" im Zeitraum — soll
|
||||
/// den Score unabhängig von allem anderen sofort deutlich nach oben treiben (Nutzer-Feedback:
|
||||
/// "Suspendiert sollte direkt hochschießen").
|
||||
public bool HasSuspensionEntry { get; init; }
|
||||
/// Gewichtete Summe aus <see cref="ClassRegisterEntryWeight"/> über alle Klassenbucheinträge
|
||||
/// des Schuljahres — fließt in <see cref="PatternScore"/> ein.
|
||||
public double ClassRegisterScoreComponent { get; init; }
|
||||
public int SchoolDaysElapsed { get; init; }
|
||||
/// Nutzer-Feedback: nachdem der Nenner zeitweise Ferientage mitzählte (siehe TODO.md), soll die
|
||||
/// Herkunft der Zahl nachvollziehbar bleiben, ohne dafür die WebUntis-Ferienliste separat
|
||||
@@ -85,19 +112,78 @@ public sealed record ClassTeacherRosterRow(string StudentName, int? ExternKey, b
|
||||
/// sichtbar — WebUntis' Ferienkalender deckt die Sommerferien selbst nicht ab (siehe TODO.md),
|
||||
/// die Korrektur passiert also am Startpunkt, nicht an abgezogenen Tagen mittendrin.
|
||||
public DateOnly TermStart { get; init; }
|
||||
public bool HasYearSummary => SchoolDaysElapsed > 0 && YearAbsenceDayCount > 0;
|
||||
public bool HasYearSummary => SchoolDaysElapsed > 0 &&
|
||||
(YearAbsenceDayCount > 0 || YearLateDayCount > 0 || YearNegativeClassRegisterCount > 0);
|
||||
public int YearAbsenceRatePercent =>
|
||||
SchoolDaysElapsed <= 0 ? 0 : (int)Math.Round(100d * YearAbsenceDayCount / SchoolDaysElapsed);
|
||||
public string YearSummaryLabel => HasYearSummary
|
||||
? $"{YearAbsenceRatePercent} % Fehlzeit seit Schuljahresbeginn" : "";
|
||||
public string YearSummaryLabel => !HasYearSummary ? "" :
|
||||
YearAbsenceDayCount > 0 ? $"{YearAbsenceRatePercent} % Fehlzeit seit Schuljahresbeginn"
|
||||
: YearLateDayCount > 0 ? $"{YearLateDayCount}× verspätet, keine Fehltage seit Schuljahresbeginn"
|
||||
: $"{YearNegativeClassRegisterCount} negative Klassenbucheinträge seit Schuljahresbeginn";
|
||||
public string? YearSummaryTooltip => !HasYearSummary ? null :
|
||||
$"{YearAbsenceDayCount} von {SchoolDaysElapsed} Schultagen seit {TermStart:dd.MM.} mit Fehlzeit" +
|
||||
(YearUnexcusedDayCount > 0 ? $" · {YearUnexcusedDayCount} unentschuldigt" : "") +
|
||||
(YearAbsenceDayCount > 0
|
||||
? $"{YearAbsenceDayCount} von {SchoolDaysElapsed} Schultagen seit {TermStart:dd.MM.} mit Fehlzeit" +
|
||||
(YearUnexcusedDayCount > 0 ? $" · {YearUnexcusedDayCount} unentschuldigt" : "")
|
||||
: $"Keine Fehltage seit {TermStart:dd.MM.}") +
|
||||
(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" : "");
|
||||
|
||||
// Feste Gewichte statt einstellbarer Werte (Nutzer-Entscheidung): unentschuldigt fällt am
|
||||
// stärksten ins Gewicht ("geschwänzt"), entschuldigte Abwesenheit mittel, reine Verspätung
|
||||
// ("verpennt") am wenigsten — genau die vom Nutzer gewünschte Trennung, nur als Zahl statt als
|
||||
// Statustext. Bei Bedarf später anpassbar, siehe TODO.md.
|
||||
private const double UnexcusedDayWeight = 3.0;
|
||||
private const double ExcusedDayWeight = 1.0;
|
||||
private const double LateDayWeight = 0.5;
|
||||
// Klassenbuch-Einträge anderer Lehrkräfte (Nutzer-Feedback): ein einzelner "Negativ"-Eintrag
|
||||
// wiegt ungefähr wie eine entschuldigte Fehlzeit — erst die Häufung ("durch die Decke gehen")
|
||||
// treibt den Score merklich. Die beiden genannten Muster (fehlende Hausaufgaben/schlechte
|
||||
// Mitarbeit) zählen zusätzlich etwas stärker, weil sie explizit als wiederkehrend relevant
|
||||
// genannt wurden. "Suspendiert" ist bewusst ein Ausreißer-Gewicht, das den Score sofort nach
|
||||
// oben reißt, unabhängig vom sonstigen Verlauf.
|
||||
private const double NegativeClassRegisterEntryWeight = 1.0;
|
||||
private const double ConcerningKeywordBonus = 1.5;
|
||||
private const double SuspensionKeywordWeight = 15.0;
|
||||
private static readonly string[] ConcerningKeywords = ["hausaufgabe", "mitarbeit"];
|
||||
private static readonly string[] SuspensionKeywords = ["suspendier"];
|
||||
|
||||
/// Auffälligkeits-Score über das ganze bisherige Schuljahr statt nur "heute" (Nutzer-Feedback:
|
||||
/// wer heute da ist, aber davor 5 Tage unentschuldigt fehlte, verschwand bisher komplett aus der
|
||||
/// Liste, während der Musterschüler beim ersten Fehltag des Jahres auf Rang eins landete, sobald
|
||||
/// er der/die einzige "heute Auffällige" war). Reine Summe ohne zeitliche Abklingung — für die
|
||||
/// vom Nutzer genannten Fälle (Muster über die letzten Tage/Wochen) reicht das, ohne die
|
||||
/// zusätzliche Komplexität einer Verfallskurve. Bezieht neben Fehlzeiten auch Klassenbucheinträge
|
||||
/// anderer Lehrkräfte mit ein (<see cref="ClassRegisterScoreComponent"/>) — beides sind
|
||||
/// unabhängige Auffälligkeits-Achsen (Anwesenheit vs. Verhalten/Leistung), die sich addieren
|
||||
/// statt sich gegenseitig zu verdrängen.
|
||||
public double PatternScore =>
|
||||
YearUnexcusedDayCount * UnexcusedDayWeight +
|
||||
YearExcusedDayCount * ExcusedDayWeight +
|
||||
YearLateDayCount * LateDayWeight +
|
||||
ClassRegisterScoreComponent;
|
||||
|
||||
private static bool IsLateReason(string reason) =>
|
||||
reason.Contains("verspät", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
private static bool IsNegativeClassRegisterEntry(UntisForeignClassRegisterEventDto e) =>
|
||||
e.CategoryGroup?.Contains("Negativ", StringComparison.OrdinalIgnoreCase) == true;
|
||||
|
||||
private static bool MatchesAnyKeyword(UntisForeignClassRegisterEventDto e, IReadOnlyList<string> keywords) =>
|
||||
keywords.Any(k =>
|
||||
e.CategoryName?.Contains(k, StringComparison.OrdinalIgnoreCase) == true ||
|
||||
e.Text?.Contains(k, StringComparison.OrdinalIgnoreCase) == true);
|
||||
|
||||
private static double ClassRegisterEntryWeight(UntisForeignClassRegisterEventDto e)
|
||||
{
|
||||
var weight = 0.0;
|
||||
if (IsNegativeClassRegisterEntry(e)) weight += NegativeClassRegisterEntryWeight;
|
||||
if (MatchesAnyKeyword(e, ConcerningKeywords)) weight += ConcerningKeywordBonus;
|
||||
if (MatchesAnyKeyword(e, SuspensionKeywords)) weight += SuspensionKeywordWeight;
|
||||
return weight;
|
||||
}
|
||||
|
||||
public static IReadOnlyList<ClassTeacherRosterRow> Build(
|
||||
IReadOnlyList<UntisStudentRosterCacheEntry> students,
|
||||
IReadOnlyList<ClassAbsenceDaySummaryRow> todayAbsences,
|
||||
@@ -106,7 +192,8 @@ public sealed record ClassTeacherRosterRow(string StudentName, int? ExternKey, b
|
||||
IReadOnlyList<ClassAbsenceDaySummaryRow>? yearAbsences = null,
|
||||
int schoolDaysElapsed = 0,
|
||||
int holidayWeekdaysExcluded = 0,
|
||||
DateOnly termStart = default)
|
||||
DateOnly termStart = default,
|
||||
IReadOnlyList<UntisForeignClassRegisterEventDto>? yearClassRegisterEntries = null)
|
||||
{
|
||||
var referenceDate = today ?? todayAbsences.FirstOrDefault()?.Date ?? DateOnly.FromDateTime(DateTime.Today);
|
||||
var absenceByKey = todayAbsences.Where(a => a.ExternKey is not null)
|
||||
@@ -115,6 +202,12 @@ public sealed record ClassTeacherRosterRow(string StudentName, int? ExternKey, b
|
||||
.ToDictionary(g => g.Key, g => g.First());
|
||||
var recentByName = recentClassRegisterEntries.GroupBy(e => UntisNameMatching.NameKey(e.StudentName))
|
||||
.ToDictionary(g => g.Key, g => g.ToList());
|
||||
// Für den Score bewusst über das ganze Schuljahr statt nur die letzten 7 Tage (anders als
|
||||
// recentClassRegisterEntries oben, das für HasRecentClassRegisterEntry/-Badges weiterhin nur
|
||||
// die aktuelle Woche abbildet) — fällt auf recentClassRegisterEntries zurück, wenn der
|
||||
// Aufrufer keine eigene Jahresliste mitgibt (z.B. bestehende Tests).
|
||||
var yearRegisterByName = (yearClassRegisterEntries ?? recentClassRegisterEntries)
|
||||
.GroupBy(e => UntisNameMatching.NameKey(e.StudentName)).ToDictionary(g => g.Key, g => g.ToList());
|
||||
var yearRows = yearAbsences ?? [];
|
||||
var yearByKey = yearRows.Where(a => a.ExternKey is not null)
|
||||
.GroupBy(a => a.ExternKey!.Value).ToDictionary(g => g.Key, g => g.ToList());
|
||||
@@ -129,14 +222,23 @@ public sealed record ClassTeacherRosterRow(string StudentName, int? ExternKey, b
|
||||
var registerEntries = recentByName.GetValueOrDefault(nameKey) ?? [];
|
||||
var yearEntries = (student.ExternKey is { } yearKey ? yearByKey.GetValueOrDefault(yearKey) : null)
|
||||
?? yearByName.GetValueOrDefault(nameKey) ?? [];
|
||||
var yearRegisterEntries = yearRegisterByName.GetValueOrDefault(nameKey) ?? [];
|
||||
return new ClassTeacherRosterRow(student.DisplayName, student.ExternKey, absence is not null,
|
||||
absence is null ? null : $"{absence.TotalAbsentPeriods} Stunde(n) — {absence.StatusLabel}",
|
||||
registerEntries.Count > 0)
|
||||
{
|
||||
TodayAbsence = absence,
|
||||
HasClassRegisterToday = registerEntries.Any(e => TryDate(e.Date, out var date) && date == referenceDate),
|
||||
YearAbsenceDayCount = yearEntries.Count,
|
||||
// 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),
|
||||
YearLateDayCount = yearEntries.Count(r => !r.IsUnexcused && r.IsLate),
|
||||
YearExcusedDayCount = yearEntries.Count(r => !r.IsUnexcused && !r.IsLate),
|
||||
YearNegativeClassRegisterCount = yearRegisterEntries.Count(IsNegativeClassRegisterEntry),
|
||||
HasSuspensionEntry = yearRegisterEntries.Any(e => MatchesAnyKeyword(e, SuspensionKeywords)),
|
||||
ClassRegisterScoreComponent = yearRegisterEntries.Sum(ClassRegisterEntryWeight),
|
||||
SchoolDaysElapsed = schoolDaysElapsed,
|
||||
HolidayWeekdaysExcluded = holidayWeekdaysExcluded,
|
||||
TermStart = termStart,
|
||||
@@ -232,7 +334,11 @@ public partial class ClassTeacherOverviewViewModel : ObservableObject
|
||||
[ObservableProperty] private string _status = "";
|
||||
[ObservableProperty] private bool _busy;
|
||||
[ObservableProperty] private string _searchText = "";
|
||||
[ObservableProperty] private int _selectedRosterFilter;
|
||||
/// Standardmäßig der Muster-Reiter (3) statt "Auffällig" (0) — Nutzer-Feedback: die reine
|
||||
/// Heute-Ansicht lässt Schüler*innen mit Vorgeschichte (z.B. 5 Tage unentschuldigt letzte
|
||||
/// Woche, heute aber da) komplett verschwinden, während ein erstmaliger Einzelfehltag ganz oben
|
||||
/// steht, sobald er der/die einzige "heute Auffällige" ist.
|
||||
[ObservableProperty] private int _selectedRosterFilter = 3;
|
||||
[ObservableProperty] private string _primarySectionTitle = "Heute auffällig";
|
||||
[ObservableProperty] private string _secondarySectionTitle = "Weitere Schüler*innen";
|
||||
[ObservableProperty] private int _studentCount;
|
||||
@@ -263,6 +369,7 @@ public partial class ClassTeacherOverviewViewModel : ObservableObject
|
||||
public bool AlertsFilterSelected => SelectedRosterFilter == 0;
|
||||
public bool ClassRegisterFilterSelected => SelectedRosterFilter == 1;
|
||||
public bool AllFilterSelected => SelectedRosterFilter == 2;
|
||||
public bool PatternScoreFilterSelected => SelectedRosterFilter == 3;
|
||||
public int TodayUnexcusedPercent => Percent(TodayUnexcusedCount);
|
||||
public int LatePercent => Percent(LateCount);
|
||||
public int PresentPercent => Percent(PresentCount);
|
||||
@@ -324,6 +431,7 @@ public partial class ClassTeacherOverviewViewModel : ObservableObject
|
||||
OnPropertyChanged(nameof(AlertsFilterSelected));
|
||||
OnPropertyChanged(nameof(ClassRegisterFilterSelected));
|
||||
OnPropertyChanged(nameof(AllFilterSelected));
|
||||
OnPropertyChanged(nameof(PatternScoreFilterSelected));
|
||||
ApplyRosterFilter();
|
||||
}
|
||||
|
||||
@@ -357,7 +465,14 @@ public partial class ClassTeacherOverviewViewModel : ObservableObject
|
||||
var yearStart = _schoolYear.SchoolYearStart(_schoolYear.CurrentSchoolYear(today));
|
||||
var studentsTask = _cache.GetStudentRosterAsync(className);
|
||||
var absencesTask = _cache.GetAbsencesAsync(className, yearStart, today);
|
||||
var classRegisterTask = _cache.GetClassRegisterEventsAsync(className, sevenDayStart, today);
|
||||
// Seit Schuljahresbeginn statt nur die letzte Woche (Nutzer-Feedback: Klassenbucheinträge
|
||||
// anderer Lehrkräfte — z.B. fehlende Hausaufgaben, schlechte Mitarbeit, im Extremfall eine
|
||||
// Suspendierung — sollen wie die Fehlzeiten in den Auffälligkeits-Score einfließen, nicht
|
||||
// nur die letzten 7 Tage). Dieselbe "kalte Historie bleibt gecacht"-Logik wie bei den
|
||||
// Fehlzeiten oben, kein zusätzliches Abruf-Risiko. Die "letzte 7 Tage"-Badges
|
||||
// (RecentClassRegisterCount, HasRecentClassRegisterEntry) werden weiterhin unten aus
|
||||
// genau diesem einen Abruf herausgefiltert statt separat erneut abgerufen.
|
||||
var classRegisterTask = _cache.GetClassRegisterEventsAsync(className, yearStart, today);
|
||||
var holidaysTask = GetHolidaysAsync();
|
||||
await Task.WhenAll(studentsTask, absencesTask, classRegisterTask, holidaysTask);
|
||||
|
||||
@@ -367,9 +482,11 @@ public partial class ClassTeacherOverviewViewModel : ObservableObject
|
||||
var rawWeekdaysElapsed = CountSchoolWeekdays(termStart, today, []);
|
||||
var schoolDaysElapsed = CountSchoolWeekdays(termStart, today, holidaysTask.Result);
|
||||
var holidayWeekdaysExcluded = rawWeekdaysElapsed - schoolDaysElapsed;
|
||||
var recentRegisterEntries = classRegisterTask.Result
|
||||
.Where(e => TryDate(e.Date, out var d) && d >= sevenDayStart).ToList();
|
||||
foreach (var row in ClassTeacherRosterRow.Build(studentsTask.Result, todayAbsences,
|
||||
classRegisterTask.Result, today, absenceDaysYear, schoolDaysElapsed,
|
||||
holidayWeekdaysExcluded, termStart)) Roster.Add(row);
|
||||
recentRegisterEntries, today, absenceDaysYear, schoolDaysElapsed,
|
||||
holidayWeekdaysExcluded, termStart, classRegisterTask.Result)) Roster.Add(row);
|
||||
|
||||
StudentCount = Roster.Count;
|
||||
TodayAlertCount = Roster.Count(r => r.HasAbsenceToday);
|
||||
@@ -378,7 +495,7 @@ public partial class ClassTeacherOverviewViewModel : ObservableObject
|
||||
PresentCount = Roster.Count(r => !r.HasAbsenceToday);
|
||||
ExcusedAbsenceCount = Roster.Count(r => r.HasAbsenceToday && !r.IsLate && !r.IsUnexcused);
|
||||
UnexcusedAbsenceCount = Roster.Count(r => r.HasAbsenceToday && !r.IsLate && r.IsUnexcused);
|
||||
RecentClassRegisterCount = classRegisterTask.Result.Count;
|
||||
RecentClassRegisterCount = recentRegisterEntries.Count;
|
||||
BuildTrend(absenceDaysYear, trendDays);
|
||||
BuildPatternNotices(absenceDaysYear, sevenDayStart);
|
||||
BuildWeekdayPatternNotices(absenceDaysYear);
|
||||
@@ -397,6 +514,7 @@ public partial class ClassTeacherOverviewViewModel : ObservableObject
|
||||
[RelayCommand] private void ShowAlerts() => SelectedRosterFilter = 0;
|
||||
[RelayCommand] private void ShowClassRegister() => SelectedRosterFilter = 1;
|
||||
[RelayCommand] private void ShowAll() => SelectedRosterFilter = 2;
|
||||
[RelayCommand] private void ShowPatternScore() => SelectedRosterFilter = 3;
|
||||
|
||||
[RelayCommand]
|
||||
private void OpenClassRegister()
|
||||
@@ -444,6 +562,18 @@ public partial class ClassTeacherOverviewViewModel : ObservableObject
|
||||
PrimarySectionTitle = "Klassenbucheinträge der letzten 7 Tage";
|
||||
foreach (var row in query.Where(r => r.HasRecentClassRegisterEntry)) PrimaryRoster.Add(row);
|
||||
}
|
||||
else if (SelectedRosterFilter == 3)
|
||||
{
|
||||
// Nutzer-Feedback: "heute auffällig" allein lässt Schüler*innen mit echter Vorgeschichte
|
||||
// verschwinden, sobald sie an einem Tag zufällig anwesend sind, und lässt einen
|
||||
// einmaligen Erstfehltag ganz oben stehen, sobald er der/die einzige "heute Auffällige"
|
||||
// ist. Deshalb eigene Sortierung nach dem kumulierten Score übers Schuljahr statt nach
|
||||
// dem heutigen Status — bewusst unabhängig von HasAbsenceToday.
|
||||
PrimarySectionTitle = "Nach Muster seit Schuljahresbeginn";
|
||||
foreach (var row in query.Where(r => r.PatternScore > 0)
|
||||
.OrderByDescending(r => r.PatternScore).ThenBy(r => r.StudentName))
|
||||
PrimaryRoster.Add(row);
|
||||
}
|
||||
else
|
||||
{
|
||||
PrimarySectionTitle = "Heute auffällig";
|
||||
@@ -476,6 +606,9 @@ public partial class ClassTeacherOverviewViewModel : ObservableObject
|
||||
(double)day.Unexcused / max, (double)day.LateExcused / max, (double)day.Excused / max));
|
||||
}
|
||||
|
||||
private static bool TryDate(int value, out DateOnly date) =>
|
||||
DateOnly.TryParseExact(value.ToString(), "yyyyMMdd", out date);
|
||||
|
||||
/// Letzte <paramref name="count"/> Werktage bis einschließlich <paramref name="end"/>, ohne
|
||||
/// Ferienkalender (bewusste Vereinfachung, siehe TODO.md 12.4-Nachtrag) — die App kennt keine
|
||||
/// Schulferien, nur Wochenenden.
|
||||
|
||||
@@ -227,14 +227,23 @@
|
||||
<Border Grid.Row="0" Background="{DynamicResource AppCardBackgroundBrush}"
|
||||
BorderBrush="{DynamicResource AppCardBorderBrush}" BorderThickness="1"
|
||||
CornerRadius="8" Padding="8" Margin="0,0,0,4">
|
||||
<Grid ColumnDefinitions="Auto,Auto,Auto,*,250" ColumnSpacing="8">
|
||||
<Grid ColumnDefinitions="Auto,Auto,Auto,Auto,*,250" ColumnSpacing="8">
|
||||
<Button Grid.Column="0" Classes="filter" Classes.active="{Binding AlertsFilterSelected}"
|
||||
Content="Auffällig" Command="{Binding ShowAlertsCommand}"/>
|
||||
<Button Grid.Column="1" Classes="filter" Classes.active="{Binding ClassRegisterFilterSelected}"
|
||||
Content="Klassenbuch" Command="{Binding ShowClassRegisterCommand}"/>
|
||||
<Button Grid.Column="2" Classes="filter" Classes.active="{Binding AllFilterSelected}"
|
||||
Content="Alle" Command="{Binding ShowAllCommand}"/>
|
||||
<TextBox Grid.Column="4" Text="{Binding SearchText, Mode=TwoWay}" PlaceholderText="Schüler*in suchen…"
|
||||
<!-- Nutzer-Feedback: "Auffällig" zeigt nur den Heute-Snapshot — wer heute zufällig
|
||||
da ist, aber zuvor mehrfach unentschuldigt fehlte, taucht dort gar nicht auf,
|
||||
während ein einmaliger Erstfehltag ganz oben landet. Neuer Reiter sortiert
|
||||
stattdessen nach dem kumulierten Score seit Schuljahresbeginn
|
||||
(ClassTeacherRosterRow.PatternScore: unentschuldigt > entschuldigt >
|
||||
Verspätung) und ist deshalb der beim Öffnen aktive Standard-Reiter. -->
|
||||
<Button Grid.Column="3" Classes="filter" Classes.active="{Binding PatternScoreFilterSelected}"
|
||||
Content="Gesamtbild" Command="{Binding ShowPatternScoreCommand}"
|
||||
ToolTip.Tip="Sortiert nach Auffälligkeits-Score seit Schuljahresbeginn (unentschuldigt zählt am stärksten, Verspätung am wenigsten) statt nur nach dem heutigen Status."/>
|
||||
<TextBox Grid.Column="5" Text="{Binding SearchText, Mode=TwoWay}" PlaceholderText="Schüler*in suchen…"
|
||||
FontSize="12" MinHeight="32"/>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
Reference in New Issue
Block a user