fix: Namensabgleich und Tab-Wechsel im Klassenlehrer-Bereich, Fehltag-Hervorhebung
Zwei Bugs behoben: - Gefilterte Listen blieben nach Klick auf eine Roster-Zeile leer: WebUntis liefert Schülernamen je nach Bericht in unterschiedlicher Reihenfolge (Schülerreport "Vorname Nachname", Fehlzeiten-/Klassenbuchberichte "Nachname Vorname"). Neuer reihenfolge-unabhängiger Namensvergleich (UntisNameMatching), betraf auch den Roster-Namensabgleich der Übersicht selbst (Ampel-Symbole/Badges waren ohne ExternKey vermutlich fast immer falsch). - Nach dem ersten Tab-Wechsel (Klick auf Roster-Zeile) blieb ein zweiter Klick wirkungslos, sobald man zwischendurch manuell zur Übersicht zurückgeklickt hatte: TabbedPage.SelectedIndex war ohne Mode=TwoWay gebunden. Explizit ergänzt. Zusätzlich: Fehlzeiten-Zeilen für einen kompletten Fehltag werden jetzt dezent hervorgehoben (Nutzer-Feedback, bisher nicht erkennbar). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -29,6 +29,10 @@ public sealed record ClassAbsenceDaySummaryRow(DateOnly Date, string StudentName
|
||||
public string PeriodsLabel => string.Join(", ", PeriodNumbers.Order());
|
||||
public string StatusLabel => string.Join(", ", Statuses);
|
||||
public string ReasonLabel => string.Join("; ", AbsenceReasons);
|
||||
/// Nutzer-Feedback: ein ganzer Fehltag ist in der Liste sonst nicht auf einen Blick erkennbar -
|
||||
/// dezente Hervorhebung statt einer weiteren Text-/Icon-Spalte.
|
||||
public string RowBackground => CountsAsFullDay ? "#1FFFA000" : "Transparent";
|
||||
public string? FullDayTooltip => CountsAsFullDay ? "Ganzer Fehltag" : null;
|
||||
|
||||
public static IReadOnlyList<ClassAbsenceDaySummaryRow> GroupByStudentAndDay(
|
||||
IEnumerable<UntisClassAbsenceEntryDto> entries) => entries
|
||||
@@ -138,13 +142,14 @@ public partial class ClassTeacherDetailsViewModel : ObservableObject
|
||||
finally { Busy = false; }
|
||||
}
|
||||
|
||||
// WebUntis liefert Namen je nach Bericht in anderer Reihenfolge als der Schülerreport, aus dem
|
||||
// StudentFilter beim Klick in der Übersicht gesetzt wird (siehe UntisNameMatching) - ein
|
||||
// exakter String-Vergleich hier ließ die gefilterten Listen fälschlich leer erscheinen.
|
||||
private bool MatchesStudentFilter(UntisForeignClassRegisterEventDto entry) =>
|
||||
string.IsNullOrWhiteSpace(StudentFilter) ||
|
||||
string.Equals(entry.StudentName, StudentFilter, StringComparison.OrdinalIgnoreCase);
|
||||
string.IsNullOrWhiteSpace(StudentFilter) || UntisNameMatching.NamesMatch(entry.StudentName, StudentFilter);
|
||||
|
||||
private bool MatchesStudentFilter(UntisClassAbsenceEntryDto entry) =>
|
||||
string.IsNullOrWhiteSpace(StudentFilter) ||
|
||||
string.Equals(entry.StudentName, StudentFilter, StringComparison.OrdinalIgnoreCase);
|
||||
string.IsNullOrWhiteSpace(StudentFilter) || UntisNameMatching.NamesMatch(entry.StudentName, StudentFilter);
|
||||
|
||||
private static bool TryDate(int value, out DateOnly date) =>
|
||||
DateOnly.TryParseExact(value.ToString(), "yyyyMMdd", out date);
|
||||
|
||||
@@ -17,8 +17,10 @@ public sealed record ClassTeacherRosterRow(string StudentName, int? ExternKey, b
|
||||
|
||||
/// <summary>Führt die WebUntis-Schülerliste der Klasse mit den heutigen Fehlzeiten und den
|
||||
/// jüngsten Klassenbucheinträgen zusammen. Zuordnung primär über <c>ExternKey</c> (beide
|
||||
/// stammen aus WebUntis, gleiche Kennung), Fallback auf Namensgleichheit für den
|
||||
/// Klassenbuch-Bericht, der keinen ExternKey liefert (siehe UntisForeignClassRegisterEventDto).</summary>
|
||||
/// stammen aus WebUntis, gleiche Kennung), Fallback auf reihenfolge-unabhängigen Namensabgleich
|
||||
/// (<see cref="UntisNameMatching"/>) für den Klassenbuch-Bericht, der keinen ExternKey liefert
|
||||
/// (siehe UntisForeignClassRegisterEventDto) — WebUntis liefert Namen dort in anderer
|
||||
/// Reihenfolge als im Schülerreport.</summary>
|
||||
public static IReadOnlyList<ClassTeacherRosterRow> Build(
|
||||
IReadOnlyList<UntisStudentDto> students,
|
||||
IReadOnlyList<ClassAbsenceDaySummaryRow> todayAbsences,
|
||||
@@ -29,20 +31,21 @@ public sealed record ClassTeacherRosterRow(string StudentName, int? ExternKey, b
|
||||
.GroupBy(a => a.ExternKey!.Value)
|
||||
.ToDictionary(g => g.Key, g => g.First());
|
||||
var absenceByName = todayAbsences
|
||||
.GroupBy(a => a.StudentName, StringComparer.OrdinalIgnoreCase)
|
||||
.ToDictionary(g => g.Key, g => g.First(), StringComparer.OrdinalIgnoreCase);
|
||||
.GroupBy(a => UntisNameMatching.NameKey(a.StudentName))
|
||||
.ToDictionary(g => g.Key, g => g.First());
|
||||
var recentNames = recentClassRegisterEntries
|
||||
.Select(e => e.StudentName)
|
||||
.ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||
.Select(e => UntisNameMatching.NameKey(e.StudentName))
|
||||
.ToHashSet();
|
||||
|
||||
return students
|
||||
.Select(s =>
|
||||
{
|
||||
var nameKey = UntisNameMatching.NameKey(s.DisplayName);
|
||||
var absence = (s.ExternKey is { } key ? absenceByKey.GetValueOrDefault(key) : null)
|
||||
?? absenceByName.GetValueOrDefault(s.DisplayName);
|
||||
?? absenceByName.GetValueOrDefault(nameKey);
|
||||
return new ClassTeacherRosterRow(s.DisplayName, s.ExternKey, absence is not null,
|
||||
absence is null ? null : $"{absence.TotalAbsentPeriods} Stunde(n) — {absence.StatusLabel}",
|
||||
recentNames.Contains(s.DisplayName));
|
||||
recentNames.Contains(nameKey));
|
||||
})
|
||||
.OrderBy(r => r.StudentName)
|
||||
.ToList();
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
namespace LehrerApp.Desktop.ViewModels.ClassTeacher;
|
||||
|
||||
/// <summary>
|
||||
/// WebUntis liefert Schülernamen je nach Bericht in unterschiedlicher Reihenfolge — der
|
||||
/// Schülerreport baut "Vorname Nachname" (<see cref="Services.UntisStudentDto.DisplayName"/>),
|
||||
/// die Fehlzeiten-/Klassenbuchberichte liefern typischerweise "Nachname Vorname". Ein exakter
|
||||
/// String-Vergleich zwischen diesen Quellen schlägt deshalb praktisch immer fehl (Bug: gefilterte
|
||||
/// Listen im Klassenlehrer-Bereich blieben leer, Roster-Symbole waren durchgängig falsch). Der
|
||||
/// Vergleich hier ist deshalb reihenfolge-unabhängig: beide Namen werden in Wörter zerlegt und als
|
||||
/// sortierte Menge verglichen (kein Abgleich über eine externe Schülerkennung möglich, da der
|
||||
/// Klassenbuch-Bericht keine liefert, siehe UntisForeignClassRegisterEventDto).
|
||||
/// </summary>
|
||||
public static class UntisNameMatching
|
||||
{
|
||||
public static bool NamesMatch(string? a, string? b) =>
|
||||
!string.IsNullOrWhiteSpace(a) && !string.IsNullOrWhiteSpace(b) && NameKey(a) == NameKey(b);
|
||||
|
||||
public static string NameKey(string value) => string.Join(' ',
|
||||
value.Split(' ', StringSplitOptions.RemoveEmptyEntries)
|
||||
.Select(w => w.Trim().ToLowerInvariant())
|
||||
.OrderBy(w => w, StringComparer.Ordinal));
|
||||
}
|
||||
Reference in New Issue
Block a user