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>
136 lines
6.1 KiB
C#
136 lines
6.1 KiB
C#
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();
|
|
}
|
|
}
|