WIP (unstable): Klassenlehrer-Feature auf Einstellungen/eigenen Bereich umgestellt
Nutzer-Feedback: Klassenlehrer ist eine Rolle für die ganze Klasse, unabhängig vom eigenen Unterricht - die Anbindung an ein LearningGroup.IsClassTeacher-Flag mit Tab in der Gruppendetailansicht war deshalb der falsche Ort. Ersetzt durch: - Klassenauswahl in den WebUntis-Einstellungen (WebUntisSettingsService.HomeroomClassName), über den bestehenden WebUntis-Klassenpicker. - Eigener Sidebar-Bereich "Klassenlehrer" statt Gruppen-Tab. - Zwei Ebenen: kompakte Roster-Übersicht mit Ampel-Symbolen (Fehlzeiten heute, Klassenbuch-Badge) und Details (die bisherigen Rohdaten-Listen, jetzt gefiltert auf einzelne Schüler*innen anspringbar). Noch nicht mit echtem WebUntis-Zugang gegengeprüft (siehe TODO.md). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,135 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using LehrerApp.Desktop.Services;
|
||||
|
||||
namespace LehrerApp.Desktop.ViewModels.ClassTeacher;
|
||||
|
||||
/// <summary>Eine Zeile der Klassenlehrer-Übersicht ("auf einen Blick", Nutzer-Feedback) - pro
|
||||
/// Schüler*in ein Ampel-Symbol für "heute laut WebUntis keine gemeldete Fehlzeit" und ein Badge,
|
||||
/// falls in den letzten Tagen ein Klassenbucheintrag einer anderen Lehrkraft angelegt wurde.
|
||||
/// Bewusst kein Bezug zu eigenem Unterricht/eigener Anwesenheitserfassung - genau der Fall, den es
|
||||
/// abzudecken gilt, ist der Tag ohne eigenen Unterricht mit der Klasse.</summary>
|
||||
public sealed record ClassTeacherRosterRow(string StudentName, int? ExternKey, bool HasAbsenceToday,
|
||||
string? AbsenceTooltip, bool HasRecentClassRegisterEntry)
|
||||
{
|
||||
public string StatusIcon => HasAbsenceToday ? "⚠" : "✓";
|
||||
|
||||
/// <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>
|
||||
public static IReadOnlyList<ClassTeacherRosterRow> Build(
|
||||
IReadOnlyList<UntisStudentDto> students,
|
||||
IReadOnlyList<ClassAbsenceDaySummaryRow> todayAbsences,
|
||||
IReadOnlyList<UntisForeignClassRegisterEventDto> recentClassRegisterEntries)
|
||||
{
|
||||
var absenceByKey = todayAbsences
|
||||
.Where(a => a.ExternKey is not null)
|
||||
.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);
|
||||
var recentNames = recentClassRegisterEntries
|
||||
.Select(e => e.StudentName)
|
||||
.ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
return students
|
||||
.Select(s =>
|
||||
{
|
||||
var absence = absenceByKey.GetValueOrDefault(s.ExternKey)
|
||||
?? absenceByName.GetValueOrDefault(s.DisplayName);
|
||||
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));
|
||||
})
|
||||
.OrderBy(r => r.StudentName)
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Top-Level-Bereich "Klassenlehrer" (eigener Sidebar-Eintrag, siehe MainWindowViewModel) - bewusst
|
||||
/// nicht an eine LearningGroup gehängt, da man Klassenlehrer für eine ganze Klasse ist, unabhängig
|
||||
/// vom eigenen Unterricht (Nutzer-Feedback zur ursprünglichen Umsetzung als Gruppen-Tab). Die
|
||||
/// Klasse selbst kommt aus den WebUntis-Einstellungen (<see cref="WebUntisSettingsService.HomeroomClassName"/>).
|
||||
/// Zwei Ebenen: Übersicht (dieses ViewModel, kompaktes Roster) und Details
|
||||
/// (<see cref="ClassTeacherDetailsViewModel"/>, die bisherigen Rohdaten-Listen) - ein Klick auf
|
||||
/// eine Roster-Zeile springt gefiltert in die Details.
|
||||
/// </summary>
|
||||
public partial class ClassTeacherOverviewViewModel : ObservableObject
|
||||
{
|
||||
private readonly WebUntisSettingsService _settings;
|
||||
private readonly WebUntisIntegrationService _untis;
|
||||
|
||||
public ClassTeacherDetailsViewModel DetailsTab { get; }
|
||||
public ObservableCollection<ClassTeacherRosterRow> Roster { get; } = [];
|
||||
|
||||
[ObservableProperty] private string? _homeroomClassName;
|
||||
[ObservableProperty] private int _activeTabIndex;
|
||||
[ObservableProperty] private string _status = "";
|
||||
[ObservableProperty] private bool _busy;
|
||||
|
||||
public bool HomeroomClassConfigured => !string.IsNullOrWhiteSpace(HomeroomClassName);
|
||||
|
||||
/// Vom Code-Behind/MainWindowViewModel gesetzt: springt in die WebUntis-Einstellungen, wenn noch
|
||||
/// keine Klasse gewählt ist.
|
||||
public Func<Task>? OnNavigateToSettings { get; set; }
|
||||
|
||||
public ClassTeacherOverviewViewModel(WebUntisSettingsService settings, WebUntisIntegrationService untis,
|
||||
ClassTeacherDetailsViewModel detailsTab)
|
||||
{
|
||||
_settings = settings; _untis = untis; DetailsTab = detailsTab;
|
||||
}
|
||||
|
||||
partial void OnHomeroomClassNameChanged(string? value) => OnPropertyChanged(nameof(HomeroomClassConfigured));
|
||||
|
||||
[RelayCommand]
|
||||
private async Task Load()
|
||||
{
|
||||
HomeroomClassName = _settings.HomeroomClassName;
|
||||
ActiveTabIndex = 0;
|
||||
Roster.Clear();
|
||||
if (!HomeroomClassConfigured)
|
||||
{
|
||||
Status = "Noch keine Klasse ausgewählt.";
|
||||
return;
|
||||
}
|
||||
|
||||
var className = HomeroomClassName!;
|
||||
DetailsTab.Initialize(className);
|
||||
Busy = true;
|
||||
try
|
||||
{
|
||||
var today = DateOnly.FromDateTime(DateTime.Today);
|
||||
var studentsTask = _untis.GetStudentsAsync(className);
|
||||
var absencesTask = _untis.GetClassAbsencesAsync(className, today, today);
|
||||
var classRegisterTask = _untis.GetForeignClassRegisterEventsAsync(className, today.AddDays(-7), today);
|
||||
await Task.WhenAll(studentsTask, absencesTask, classRegisterTask);
|
||||
|
||||
var todayAbsences = ClassAbsenceDaySummaryRow.GroupByStudentAndDay(absencesTask.Result);
|
||||
foreach (var row in ClassTeacherRosterRow.Build(studentsTask.Result.Students, todayAbsences,
|
||||
classRegisterTask.Result))
|
||||
Roster.Add(row);
|
||||
Status = $"{Roster.Count} Schüler*innen · Stand heute.";
|
||||
}
|
||||
catch (WebUntisIntegrationException ex) { Status = ex.Message; }
|
||||
finally { Busy = false; }
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void ShowDetailsForStudent(ClassTeacherRosterRow? row)
|
||||
{
|
||||
if (row is null) return;
|
||||
DetailsTab.StudentFilter = row.StudentName;
|
||||
ActiveTabIndex = 1;
|
||||
DetailsTab.LoadCommand.Execute(null);
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task GoToSettings()
|
||||
{
|
||||
if (OnNavigateToSettings is not null) await OnNavigateToSettings();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user