Klassenlehrer-Tab: vier Feature-Ideen umgesetzt (ohne Elterngesprächs-PDF)
Offene Entschuldigungen mit Frist (neue Seitenspalten-Karte, sortiert nach Tagen offen), aggregierte Klassenbuch-Kategorien (Chip-Reihe im Klassenbuch-Tab), Wochentags-Häufung als dritte Mustererkennungsregel, und eine Wiedervorlagen-Verknüpfung: ein "+"-Button legt direkt aus der Zeile eine WorkTask "Eltern kontaktieren" an, und eine dritte Musterregel verknüpft hohe Jahresfehlquote mit fallendem Mitarbeit-Trend. ContextMenu auf den Zeilen-Buttons scheiterte an Avalonias $parent-Vorfahren-Bindings (Popup hängt nicht im normalen visuellen Baum) — stattdessen zwei Geschwister-Buttons pro Zeile. Elterngesprächs-Blatt als PDF bewusst zurückgestellt, Konzept noch nicht geschärft. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -26,6 +26,33 @@ public sealed record ClassTeacherClassRegisterRow(DateOnly Date, string? Subject
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Zusammenfassung "wie oft welche Klassenbuch-Kategorie im Zeitraum, und bei wem am
|
||||
/// häufigsten" — Nutzer-Feedback: die reine Gesamtzahl ("12 Klassenbucheinträge") sagt wenig,
|
||||
/// eine Aufschlüsselung nach Kategorie ist die eigentlich interessante Information ("Hausaufgaben
|
||||
/// fehlen: 12×, davon 5× Ben"). Rein clientseitig aus den bereits geladenen <see cref="Entries"/>
|
||||
/// berechnet, kein zusätzlicher WebUntis-Abruf.</summary>
|
||||
public sealed record ClassTeacherCategoryAggregateRow(string CategoryName, int Count, string TopStudentsLabel)
|
||||
{
|
||||
public string SummaryLabel => string.IsNullOrEmpty(TopStudentsLabel)
|
||||
? $"{Count}×" : $"{Count}× — {TopStudentsLabel}";
|
||||
|
||||
public static IReadOnlyList<ClassTeacherCategoryAggregateRow> Build(
|
||||
IReadOnlyList<ClassTeacherClassRegisterRow> entries, int topStudentsPerCategory = 2) =>
|
||||
entries.Where(e => !string.IsNullOrWhiteSpace(e.CategoryName))
|
||||
.GroupBy(e => e.CategoryName!)
|
||||
.Select(g =>
|
||||
{
|
||||
var topStudents = g.GroupBy(e => e.StudentDisplayName)
|
||||
.Select(sg => (Name: sg.Key, Count: sg.Count()))
|
||||
.OrderByDescending(sg => sg.Count).ThenBy(sg => sg.Name)
|
||||
.Take(topStudentsPerCategory)
|
||||
.Select(sg => $"{sg.Count}× {sg.Name}");
|
||||
return new ClassTeacherCategoryAggregateRow(g.Key, g.Count(), string.Join(", ", topStudents));
|
||||
})
|
||||
.OrderByDescending(r => r.Count).ThenBy(r => r.CategoryName)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
/// <summary>Fehlzeiten eines/einer Schüler*in an einem Tag, über alle Fächer zusammengefasst -
|
||||
/// der WebUntis-Bericht liefert eine Zeile pro Fehlstunde, was bei einer ganzen Klasse über mehrere
|
||||
/// Wochen zu unübersichtlich wäre (Nutzer-Feedback: kompakte "auf einen Blick"-Übersicht statt
|
||||
@@ -107,6 +134,7 @@ public partial class ClassTeacherDetailsViewModel : ObservableObject
|
||||
|
||||
public ObservableCollection<ClassTeacherClassRegisterRow> Entries { get; } = [];
|
||||
public ObservableCollection<ClassAbsenceDaySummaryRow> AbsenceEntries { get; } = [];
|
||||
public ObservableCollection<ClassTeacherCategoryAggregateRow> CategoryAggregates { get; } = [];
|
||||
|
||||
[ObservableProperty] private DateTimeOffset _startDate = DateTimeOffset.Now.AddDays(-6);
|
||||
[ObservableProperty] private DateTimeOffset _endDate = DateTimeOffset.Now;
|
||||
@@ -118,6 +146,7 @@ public partial class ClassTeacherDetailsViewModel : ObservableObject
|
||||
|
||||
public bool HasEntries => Entries.Count > 0;
|
||||
public bool HasAbsenceEntries => AbsenceEntries.Count > 0;
|
||||
public bool HasCategoryAggregates => CategoryAggregates.Count > 0;
|
||||
public string ActiveFilterLabel => string.IsNullOrWhiteSpace(StudentFilter)
|
||||
? "Alle Schüler*innen" : StudentFilter;
|
||||
|
||||
@@ -132,6 +161,7 @@ public partial class ClassTeacherDetailsViewModel : ObservableObject
|
||||
StudentFilter = "";
|
||||
Entries.Clear();
|
||||
AbsenceEntries.Clear();
|
||||
CategoryAggregates.Clear();
|
||||
Status = "Zeitraum wählen und laden.";
|
||||
NotifyListState();
|
||||
}
|
||||
@@ -170,7 +200,7 @@ public partial class ClassTeacherDetailsViewModel : ObservableObject
|
||||
if (end < start) { Status = "Das Enddatum darf nicht vor dem Startdatum liegen."; return; }
|
||||
if (string.IsNullOrWhiteSpace(_className)) { Status = "Keine Klasse ausgewählt."; return; }
|
||||
|
||||
Busy = true; Entries.Clear(); AbsenceEntries.Clear(); NotifyListState();
|
||||
Busy = true; Entries.Clear(); AbsenceEntries.Clear(); CategoryAggregates.Clear(); NotifyListState();
|
||||
try
|
||||
{
|
||||
var classRegisterTask = _cache.GetClassRegisterEventsAsync(_className, start, end, forceRefresh);
|
||||
@@ -187,6 +217,7 @@ public partial class ClassTeacherDetailsViewModel : ObservableObject
|
||||
Entries.Add(new ClassTeacherClassRegisterRow(date!.Value, entry.Subject, entry.StudentName,
|
||||
entry.TeacherUsername, entry.CategoryName, entry.CategoryGroup, entry.Text));
|
||||
}
|
||||
foreach (var row in ClassTeacherCategoryAggregateRow.Build(Entries)) CategoryAggregates.Add(row);
|
||||
|
||||
var absences = absencesTask.Result.Where(MatchesStudentFilter);
|
||||
foreach (var row in ClassAbsenceDaySummaryRow.GroupByStudentAndDay(absences))
|
||||
@@ -216,5 +247,6 @@ public partial class ClassTeacherDetailsViewModel : ObservableObject
|
||||
{
|
||||
OnPropertyChanged(nameof(HasEntries));
|
||||
OnPropertyChanged(nameof(HasAbsenceEntries));
|
||||
OnPropertyChanged(nameof(HasCategoryAggregates));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user