diff --git a/LehrerApp.Core/Models/LearningGroup.cs b/LehrerApp.Core/Models/LearningGroup.cs
index 6bc830f..c2afd80 100644
--- a/LehrerApp.Core/Models/LearningGroup.cs
+++ b/LehrerApp.Core/Models/LearningGroup.cs
@@ -14,13 +14,6 @@ public class LearningGroup
public bool IsOwnClass { get; set; }
public bool IsDifferentiated { get; set; }
///
- /// Ich bin Klassenlehrer/-in dieser Klasse. Anders als (rein
- /// kalenderfarbliche Markierung, mehrfach setzbar für Klasse + zugehörige Kurse) schaltet
- /// dieses Flag Zugriff auf Daten anderer Lehrkräfte zu dieser Klasse frei (Klassenlehrer-Tab)
- /// und sollte deshalb nur an genau einer Klasse-Gruppe gesetzt werden.
- ///
- public bool IsClassTeacher { get; set; }
- ///
/// Default true (alte Datensätze ohne dieses Feld verhalten sich damit unverändert wie bisher
/// - LiteDB liefert den C#-Default für fehlende Felder). Für Gruppen wie Klassenrat oder
/// Willkommenskreis, die keinen inhaltlichen Verlaufsplan brauchen, in den Stammdaten
diff --git a/LehrerApp.Desktop.Tests/ClassTeacherViewModelsTests.cs b/LehrerApp.Desktop.Tests/ClassTeacherViewModelsTests.cs
index 4b46583..b3505bd 100644
--- a/LehrerApp.Desktop.Tests/ClassTeacherViewModelsTests.cs
+++ b/LehrerApp.Desktop.Tests/ClassTeacherViewModelsTests.cs
@@ -1,5 +1,5 @@
using LehrerApp.Desktop.Services;
-using LehrerApp.Desktop.ViewModels.Groups;
+using LehrerApp.Desktop.ViewModels.ClassTeacher;
using Xunit;
namespace LehrerApp.Desktop.Tests;
@@ -62,4 +62,48 @@ public sealed class ClassTeacherViewModelsTests
Assert.Equal(new DateOnly(2026, 8, 25), rows[0].Date);
Assert.Equal(new DateOnly(2026, 8, 24), rows[1].Date);
}
+
+ [Fact]
+ public void RosterBuild_OrdnetFehlzeitPerExternKeyUndKlassenbuchPerNameZu()
+ {
+ var students = new[]
+ {
+ Student(1001, "Ada Müller"),
+ Student(1002, "Ben Schmidt"),
+ Student(1003, "Cem Yilmaz"),
+ };
+ var todayAbsences = new[]
+ {
+ new ClassAbsenceDaySummaryRow(new DateOnly(2026, 8, 25), "Ada Müller", 1001, 2, 90,
+ ["Che"], [3, 4], ["nicht entsch."], ["Absent"], null, null, false),
+ };
+ var recentClassRegister = new[]
+ {
+ new UntisForeignClassRegisterEventDto("6a", 20260824, "Deu", "Ben Schmidt", "mueller",
+ "Vergessen", "Organisation", "Buch vergessen"),
+ };
+
+ var roster = ClassTeacherRosterRow.Build(students, todayAbsences, recentClassRegister);
+
+ Assert.Equal(3, roster.Count);
+ var ada = Assert.Single(roster, r => r.StudentName == "Ada Müller");
+ Assert.True(ada.HasAbsenceToday);
+ Assert.Contains("nicht entsch.", ada.AbsenceTooltip);
+ Assert.False(ada.HasRecentClassRegisterEntry);
+
+ var ben = Assert.Single(roster, r => r.StudentName == "Ben Schmidt");
+ Assert.False(ben.HasAbsenceToday);
+ Assert.True(ben.HasRecentClassRegisterEntry);
+
+ var cem = Assert.Single(roster, r => r.StudentName == "Cem Yilmaz");
+ Assert.False(cem.HasAbsenceToday);
+ Assert.False(cem.HasRecentClassRegisterEntry);
+ }
+
+ private static UntisStudentDto Student(int externKey, string displayName) => new(
+ UntisId: externKey, ExternKey: externKey, ClassName: "6a", Name: displayName, LongName: null,
+ ForeName: null, DisplayName: displayName, Gender: null, BirthDate: null, BirthDateRaw: null,
+ EntryDate: null, EntryDateRaw: null, ExitDate: null, ExitDateRaw: null, Text: null,
+ MedicalReportDuty: null, Schulpflicht: null, Majority: null,
+ Address: new UntisStudentAddressDto(null, null, null, null, null, null), AttributeIL: null);
}
diff --git a/LehrerApp.Desktop.Tests/GroupDetailViewModelTests.cs b/LehrerApp.Desktop.Tests/GroupDetailViewModelTests.cs
index 8637a6e..6847b14 100644
--- a/LehrerApp.Desktop.Tests/GroupDetailViewModelTests.cs
+++ b/LehrerApp.Desktop.Tests/GroupDetailViewModelTests.cs
@@ -1,6 +1,5 @@
using LehrerApp.Core.Models;
using LehrerApp.Core.Services;
-using LehrerApp.Desktop.Services;
using LehrerApp.Desktop.ViewModels.Groups;
using Xunit;
@@ -30,9 +29,7 @@ public sealed class GroupDetailViewModelTests
new FakeCompetencyDomains(), students, new CompetencyAnalysisService()),
new SeatingPlanTabViewModel(new FakeSeatingPlans(), students, memberships,
new FakeSessions([]), new FakeEntries(), new FakeAspects()),
- new GroupDocumentationTabViewModel(new FakeDocumentation(), students, groups),
- new ClassTeacherTabViewModel(
- new WebUntisIntegrationService(new HttpClient(), TestSupport.BuildWebUntisSettingsService()), groups));
+ new GroupDocumentationTabViewModel(new FakeDocumentation(), students, groups));
vm.LoadGroup(group.Id);
vm.SelectedExam = vm.Exams.First(e => e.Id == exam.Id);
diff --git a/LehrerApp.Desktop/AppBootstrapper.cs b/LehrerApp.Desktop/AppBootstrapper.cs
index d9db3a1..91b181f 100644
--- a/LehrerApp.Desktop/AppBootstrapper.cs
+++ b/LehrerApp.Desktop/AppBootstrapper.cs
@@ -5,6 +5,7 @@ using LehrerApp.Data;
using LehrerApp.Data.Repositories;
using LehrerApp.Desktop.Services;
using LehrerApp.Desktop.ViewModels;
+using LehrerApp.Desktop.ViewModels.ClassTeacher;
using LehrerApp.Desktop.ViewModels.Groups;
using LehrerApp.Desktop.ViewModels.Planning;
using LehrerApp.Desktop.ViewModels.Settings;
@@ -297,6 +298,8 @@ public static class AppBootstrapper
services.AddSingleton();
services.AddSingleton();
services.AddSingleton();
+ services.AddSingleton();
+ services.AddSingleton();
// Transient: neue Instanz pro Navigation (für Detailseiten)
services.AddTransient();
@@ -308,7 +311,6 @@ public static class AppBootstrapper
services.AddTransient();
services.AddTransient();
services.AddTransient();
- services.AddTransient();
services.AddTransient();
services.AddTransient();
services.AddTransient();
diff --git a/LehrerApp.Desktop/Services/WebUntisSettingsService.cs b/LehrerApp.Desktop/Services/WebUntisSettingsService.cs
index 1af427e..b9eebe5 100644
--- a/LehrerApp.Desktop/Services/WebUntisSettingsService.cs
+++ b/LehrerApp.Desktop/Services/WebUntisSettingsService.cs
@@ -11,6 +11,8 @@ internal class WebUntisSettingsConfig
public string LastSyncStatus { get; set; } = "";
public string? EncryptedApiCredentials { get; set; }
public int? TeacherUntisId { get; set; }
+ public int? HomeroomClassUntisId { get; set; }
+ public string? HomeroomClassName { get; set; }
}
public sealed record WebUntisCredentials(string School, string Host, string Username, string Password);
@@ -36,6 +38,8 @@ public class WebUntisSettingsService
public bool IsConfigured => _config.EncryptedIcalUrl is not null;
public bool ApiIsConfigured => _config.EncryptedApiCredentials is not null;
public int? TeacherUntisId => _config.TeacherUntisId;
+ public int? HomeroomClassUntisId => _config.HomeroomClassUntisId;
+ public string? HomeroomClassName => _config.HomeroomClassName;
public DateTime? LastSyncAt => _config.LastSyncAt;
public string LastSyncStatus => _config.LastSyncStatus;
@@ -83,6 +87,8 @@ public class WebUntisSettingsService
{
_config.EncryptedApiCredentials = null;
_config.TeacherUntisId = null;
+ _config.HomeroomClassUntisId = null;
+ _config.HomeroomClassName = null;
Save();
}
@@ -92,6 +98,16 @@ public class WebUntisSettingsService
Save();
}
+ // Die Klasse, deren Klassenlehrer/-in man ist - unabhängig von jeder LearningGroup (siehe
+ // TODO.md "Klassenlehrer"-Feature): man ist es für die ganze Klasse, nicht für einen einzelnen
+ // Unterricht/Kurs. HomeroomClassName ist wie TeacherUntisId unverschlüsselt, da kein Geheimnis.
+ public void SetHomeroomClass(int? untisId, string? name)
+ {
+ _config.HomeroomClassUntisId = untisId;
+ _config.HomeroomClassName = name;
+ Save();
+ }
+
public void SetLastSync(DateTime at, string status)
{
_config.LastSyncAt = at;
diff --git a/LehrerApp.Desktop/ViewModels/Groups/ClassTeacherViewModels.cs b/LehrerApp.Desktop/ViewModels/ClassTeacher/ClassTeacherDetailsViewModel.cs
similarity index 76%
rename from LehrerApp.Desktop/ViewModels/Groups/ClassTeacherViewModels.cs
rename to LehrerApp.Desktop/ViewModels/ClassTeacher/ClassTeacherDetailsViewModel.cs
index 0434592..aff4767 100644
--- a/LehrerApp.Desktop/ViewModels/Groups/ClassTeacherViewModels.cs
+++ b/LehrerApp.Desktop/ViewModels/ClassTeacher/ClassTeacherDetailsViewModel.cs
@@ -1,10 +1,9 @@
using System.Collections.ObjectModel;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
-using LehrerApp.Core.Interfaces;
using LehrerApp.Desktop.Services;
-namespace LehrerApp.Desktop.ViewModels.Groups;
+namespace LehrerApp.Desktop.ViewModels.ClassTeacher;
/// Eine Zeile aus dem "-alle-"-Klassenbuchbericht, die eine andere Lehrkraft angelegt hat.
/// Rein zur Ansicht - anders als beim Dokumentations-Abgleich (eigene Einträge) gibt es hier kein
@@ -57,19 +56,19 @@ public sealed record ClassAbsenceDaySummaryRow(DateOnly Date, string StudentName
}
///
-/// Klassenlehrer-Tab: zeigt Klassenbucheinträge, die andere Lehrkräfte zu Schülern dieser Klasse
-/// angelegt haben (WebUntis "-alle-"-Bericht, gefiltert auf fremde statt der eigenen
-/// Benutzerkennung wie beim bestehenden Dokumentations-Abgleich, siehe
+/// Details-Ansicht des Klassenlehrer-Bereichs: Klassenbucheinträge, die andere Lehrkräfte zu
+/// Schülern der Klasse angelegt haben (WebUntis "-alle-"-Bericht, gefiltert auf fremde statt der
+/// eigenen Benutzerkennung wie beim bestehenden Dokumentations-Abgleich, siehe
/// ), sowie die
/// Fehlzeiten der Klasse über alle Fächer ().
-/// Beides rein zur Ansicht, kein Übernehmen in lokale Daten. Nur für Gruppen mit
-/// sichtbar (siehe
-/// GroupDetailViewModel/GroupDetailView).
+/// Beides rein zur Ansicht, kein Übernehmen in lokale Daten. Bewusst unabhängig von jeder
+/// — Klassenlehrer ist man für die ganze Klasse, nicht für
+/// einen einzelnen Unterricht; die Klasse kommt aus
+/// (letztlich aus den WebUntis-Einstellungen), nicht aus einer Gruppen-ID.
///
-public partial class ClassTeacherTabViewModel : ObservableObject
+public partial class ClassTeacherDetailsViewModel : ObservableObject
{
private readonly WebUntisIntegrationService _untis;
- private readonly IGroupRepository _groups;
private string _className = "";
public ObservableCollection Entries { get; } = [];
@@ -79,15 +78,17 @@ public partial class ClassTeacherTabViewModel : ObservableObject
[ObservableProperty] private DateTimeOffset _endDate = DateTimeOffset.Now;
[ObservableProperty] private string _status = "Zeitraum wählen und laden.";
[ObservableProperty] private bool _busy;
+ /// Von der Übersicht gesetzt (Klick auf eine Roster-Zeile) - leer zeigt alle Schüler*innen.
+ [ObservableProperty] private string _studentFilter = "";
- public ClassTeacherTabViewModel(WebUntisIntegrationService untis, IGroupRepository groups)
+ public ClassTeacherDetailsViewModel(WebUntisIntegrationService untis)
{
- _untis = untis; _groups = groups;
+ _untis = untis;
}
- public void Initialize(Guid groupId)
+ public void Initialize(string className)
{
- _className = _groups.GetById(groupId)?.Name ?? "";
+ _className = className;
Entries.Clear();
AbsenceEntries.Clear();
Status = "Zeitraum wählen und laden.";
@@ -99,7 +100,7 @@ public partial class ClassTeacherTabViewModel : ObservableObject
var start = DateOnly.FromDateTime(StartDate.LocalDateTime);
var end = DateOnly.FromDateTime(EndDate.LocalDateTime);
if (end < start) { Status = "Das Enddatum darf nicht vor dem Startdatum liegen."; return; }
- if (string.IsNullOrWhiteSpace(_className)) { Status = "Dieser Gruppe fehlt ein Klassenname."; return; }
+ if (string.IsNullOrWhiteSpace(_className)) { Status = "Keine Klasse ausgewählt."; return; }
Busy = true; Entries.Clear(); AbsenceEntries.Clear();
try
@@ -109,6 +110,7 @@ public partial class ClassTeacherTabViewModel : ObservableObject
await Task.WhenAll(classRegisterTask, absencesTask);
var ordered = classRegisterTask.Result
+ .Where(MatchesStudentFilter)
.Select(e => (Entry: e, Date: TryDate(e.Date, out var d) ? d : (DateOnly?)null))
.Where(x => x.Date is not null)
.OrderByDescending(x => x.Date).ThenBy(x => x.Entry.StudentName);
@@ -118,7 +120,8 @@ public partial class ClassTeacherTabViewModel : ObservableObject
entry.TeacherUsername, entry.CategoryName, entry.CategoryGroup, entry.Text));
}
- foreach (var row in ClassAbsenceDaySummaryRow.GroupByStudentAndDay(absencesTask.Result))
+ var absences = absencesTask.Result.Where(MatchesStudentFilter);
+ foreach (var row in ClassAbsenceDaySummaryRow.GroupByStudentAndDay(absences))
AbsenceEntries.Add(row);
Status = $"{Entries.Count} Klassenbucheinträge anderer Lehrkräfte, " +
@@ -128,6 +131,14 @@ public partial class ClassTeacherTabViewModel : ObservableObject
finally { Busy = false; }
}
+ private bool MatchesStudentFilter(UntisForeignClassRegisterEventDto entry) =>
+ string.IsNullOrWhiteSpace(StudentFilter) ||
+ string.Equals(entry.StudentName, StudentFilter, StringComparison.OrdinalIgnoreCase);
+
+ private bool MatchesStudentFilter(UntisClassAbsenceEntryDto entry) =>
+ string.IsNullOrWhiteSpace(StudentFilter) ||
+ string.Equals(entry.StudentName, StudentFilter, StringComparison.OrdinalIgnoreCase);
+
private static bool TryDate(int value, out DateOnly date) =>
DateOnly.TryParseExact(value.ToString(), "yyyyMMdd", out date);
}
diff --git a/LehrerApp.Desktop/ViewModels/ClassTeacher/ClassTeacherOverviewViewModel.cs b/LehrerApp.Desktop/ViewModels/ClassTeacher/ClassTeacherOverviewViewModel.cs
new file mode 100644
index 0000000..048f1d8
--- /dev/null
+++ b/LehrerApp.Desktop/ViewModels/ClassTeacher/ClassTeacherOverviewViewModel.cs
@@ -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;
+
+/// 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.
+public sealed record ClassTeacherRosterRow(string StudentName, int? ExternKey, bool HasAbsenceToday,
+ string? AbsenceTooltip, bool HasRecentClassRegisterEntry)
+{
+ public string StatusIcon => HasAbsenceToday ? "⚠" : "✓";
+
+ /// Führt die WebUntis-Schülerliste der Klasse mit den heutigen Fehlzeiten und den
+ /// jüngsten Klassenbucheinträgen zusammen. Zuordnung primär über ExternKey (beide
+ /// stammen aus WebUntis, gleiche Kennung), Fallback auf Namensgleichheit für den
+ /// Klassenbuch-Bericht, der keinen ExternKey liefert (siehe UntisForeignClassRegisterEventDto).
+ public static IReadOnlyList Build(
+ IReadOnlyList students,
+ IReadOnlyList todayAbsences,
+ IReadOnlyList 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();
+ }
+}
+
+///
+/// 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 ().
+/// Zwei Ebenen: Übersicht (dieses ViewModel, kompaktes Roster) und Details
+/// (, die bisherigen Rohdaten-Listen) - ein Klick auf
+/// eine Roster-Zeile springt gefiltert in die Details.
+///
+public partial class ClassTeacherOverviewViewModel : ObservableObject
+{
+ private readonly WebUntisSettingsService _settings;
+ private readonly WebUntisIntegrationService _untis;
+
+ public ClassTeacherDetailsViewModel DetailsTab { get; }
+ public ObservableCollection 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? 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();
+ }
+}
diff --git a/LehrerApp.Desktop/ViewModels/Groups/GroupViewModels.cs b/LehrerApp.Desktop/ViewModels/Groups/GroupViewModels.cs
index 3b2c599..265dad6 100644
--- a/LehrerApp.Desktop/ViewModels/Groups/GroupViewModels.cs
+++ b/LehrerApp.Desktop/ViewModels/Groups/GroupViewModels.cs
@@ -251,7 +251,6 @@ public partial class GroupDetailViewModel : ObservableObject
// bevor LoadGroup() läuft (siehe MainWindowViewModel.NavigateToGroupDetail), Group ist dann
// kurzzeitig null — ein verschachtelter Pfad würde dafür jedes Mal einen Binding-Fehler loggen.
public bool IsDifferentiated => Group?.IsDifferentiated ?? false;
- public bool IsClassTeacher => Group?.IsClassTeacher ?? false;
public bool IsReadOnly => Group is { IsActive: false };
public bool IsEditable => Group is { IsActive: true };
public string SubjectName { get; private set; } = "";
@@ -259,7 +258,6 @@ public partial class GroupDetailViewModel : ObservableObject
partial void OnGroupChanged(LearningGroup? value)
{
OnPropertyChanged(nameof(IsDifferentiated));
- OnPropertyChanged(nameof(IsClassTeacher));
OnPropertyChanged(nameof(IsReadOnly));
OnPropertyChanged(nameof(IsEditable));
NotifyWriteCommands();
@@ -281,7 +279,6 @@ public partial class GroupDetailViewModel : ObservableObject
public CompetencyOverviewTabViewModel CompetencyOverviewTab { get; }
public SeatingPlanTabViewModel SeatingPlanTab { get; }
public GroupDocumentationTabViewModel GroupDocumentationTab { get; }
- public ClassTeacherTabViewModel ClassTeacherTab { get; }
public Func>? OnAddStudent { get; set; }
public Func>? OnWithdrawStudent { get; set; }
public Func>? OnAddExam { get; set; }
@@ -298,8 +295,7 @@ public partial class GroupDetailViewModel : ObservableObject
GroupOverviewViewModel overviewTab, ParticipationTabViewModel participationTab,
GradeOverviewTabViewModel gradeOverviewTab,
PlanningTabViewModel planningTab, CompetencyOverviewTabViewModel competencyOverviewTab,
- SeatingPlanTabViewModel seatingPlanTab, GroupDocumentationTabViewModel groupDocumentationTab,
- ClassTeacherTabViewModel classTeacherTab)
+ SeatingPlanTabViewModel seatingPlanTab, GroupDocumentationTabViewModel groupDocumentationTab)
{
_groups = groups; _students = students; _memberships = memberships; _subjects = subjects;
_exams = exams; _grades = grades; _tasks = tasks;
@@ -310,7 +306,6 @@ public partial class GroupDetailViewModel : ObservableObject
CompetencyOverviewTab = competencyOverviewTab;
SeatingPlanTab = seatingPlanTab;
GroupDocumentationTab = groupDocumentationTab;
- ClassTeacherTab = classTeacherTab;
SeatingPlanTab.OnAssessmentChanged = () =>
{
ParticipationTab.LoadSessions();
@@ -340,7 +335,6 @@ public partial class GroupDetailViewModel : ObservableObject
CompetencyOverviewTab.Initialize(Group);
SeatingPlanTab.Initialize(Group.Id, IsReadOnly);
GroupDocumentationTab.Initialize(Group.Id);
- ClassTeacherTab.Initialize(Group.Id);
}
private void ReloadExams()
@@ -883,7 +877,6 @@ public partial class AddGroupDialogViewModel : ObservableObject
[ObservableProperty] private int? _hoursPerWeek;
[ObservableProperty] private bool _isOwnClass;
[ObservableProperty] private bool _isDifferentiated;
- [ObservableProperty] private bool _isClassTeacher;
[ObservableProperty] private bool _requiresLessonPlanning = true;
[ObservableProperty] private int? _webUntisLessonId;
[ObservableProperty] private string _nameError = "";
@@ -924,7 +917,6 @@ public partial class AddGroupDialogViewModel : ObservableObject
HoursPerWeek = group.HoursPerWeek;
IsOwnClass = group.IsOwnClass;
IsDifferentiated = group.IsDifferentiated;
- IsClassTeacher = group.IsClassTeacher;
RequiresLessonPlanning = group.RequiresLessonPlanning;
WebUntisLessonId = group.WebUntisLessonId;
OnPropertyChanged(nameof(DialogTitle));
@@ -972,7 +964,6 @@ public partial class AddGroupDialogViewModel : ObservableObject
Result.HoursPerWeek = HoursPerWeek;
Result.IsOwnClass = IsOwnClass;
Result.IsDifferentiated = IsDifferentiated;
- Result.IsClassTeacher = IsClassTeacher;
Result.RequiresLessonPlanning = RequiresLessonPlanning;
Result.WebUntisLessonId = WebUntisLessonId;
_groups.Save(Result);
diff --git a/LehrerApp.Desktop/ViewModels/MainWindowViewModel.cs b/LehrerApp.Desktop/ViewModels/MainWindowViewModel.cs
index 3007bfa..2b57169 100644
--- a/LehrerApp.Desktop/ViewModels/MainWindowViewModel.cs
+++ b/LehrerApp.Desktop/ViewModels/MainWindowViewModel.cs
@@ -3,6 +3,7 @@ using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using LehrerApp.Core.Services;
using LehrerApp.Desktop.Services;
+using LehrerApp.Desktop.ViewModels.ClassTeacher;
using LehrerApp.Desktop.ViewModels.Groups;
using LehrerApp.Desktop.ViewModels.Planning;
using LehrerApp.Desktop.ViewModels.Settings;
@@ -31,6 +32,7 @@ public partial class MainWindowViewModel : ObservableObject
public bool IsExamsActive => ActiveNavItem == NavItem.Exams;
public bool IsPlannerActive => ActiveNavItem == NavItem.Planner;
public bool IsWorkloadActive => ActiveNavItem == NavItem.Workload;
+ public bool IsClassTeacherActive => ActiveNavItem == NavItem.ClassTeacher;
public bool IsSettingsActive => ActiveNavItem == NavItem.Settings;
public MainWindowViewModel(IServiceProvider services,
@@ -71,6 +73,7 @@ public partial class MainWindowViewModel : ObservableObject
case TimetableViewModel vm: vm.Load(); break;
case WorkloadViewModel vm:
vm.Tasks.Load(); vm.TimeTracking.Load(); vm.Evaluation.Load(); break;
+ case ClassTeacherOverviewViewModel vm: vm.LoadCommand.Execute(null); break;
case GroupDetailViewModel { Group: { } group } vm: vm.LoadGroup(group.Id); break;
// Inline-Bearbeitung (IsEditing) nicht überschreiben - anders als die Gruppenansicht
// laufen Namens-/Geschlechtsänderungen hier nicht über einen Dialog.
@@ -87,6 +90,7 @@ public partial class MainWindowViewModel : ObservableObject
OnPropertyChanged(nameof(IsExamsActive));
OnPropertyChanged(nameof(IsPlannerActive));
OnPropertyChanged(nameof(IsWorkloadActive));
+ OnPropertyChanged(nameof(IsClassTeacherActive));
OnPropertyChanged(nameof(IsSettingsActive));
}
@@ -102,6 +106,7 @@ public partial class MainWindowViewModel : ObservableObject
NavItem.Exams => new PlaceholderViewModel { Title = "Klausuren", Icon = "📝" },
NavItem.Planner => GetTimetable(),
NavItem.Workload => GetWorkload(),
+ NavItem.ClassTeacher => GetClassTeacherOverview(),
NavItem.Settings => _services.GetRequiredService(),
_ => CurrentPage,
};
@@ -141,6 +146,14 @@ public partial class MainWindowViewModel : ObservableObject
return workload;
}
+ private ClassTeacherOverviewViewModel GetClassTeacherOverview()
+ {
+ var vm = _services.GetRequiredService();
+ vm.OnNavigateToSettings = () => { NavigateToSettings(SettingsTab.WebUntis); return Task.CompletedTask; };
+ vm.LoadCommand.Execute(null);
+ return vm;
+ }
+
public void NavigateToGroupDetail(Guid groupId, int initialTab = 0)
{
ActiveNavItem = NavItem.Groups;
@@ -175,7 +188,7 @@ public partial class MainWindowViewModel : ObservableObject
public void NavigateToWorkload() => NavigateTo(NavItem.Workload);
}
-public enum NavItem { Dashboard, Groups, Students, Exams, Planner, Workload, Settings }
+public enum NavItem { Dashboard, Groups, Students, Exams, Planner, Workload, ClassTeacher, Settings }
public partial class PlaceholderViewModel : ObservableObject
{
diff --git a/LehrerApp.Desktop/ViewModels/Settings/SettingsViewModel.cs b/LehrerApp.Desktop/ViewModels/Settings/SettingsViewModel.cs
index fb23578..8d77b88 100644
--- a/LehrerApp.Desktop/ViewModels/Settings/SettingsViewModel.cs
+++ b/LehrerApp.Desktop/ViewModels/Settings/SettingsViewModel.cs
@@ -217,7 +217,14 @@ public partial class SettingsViewModel : ObservableObject
[ObservableProperty] private string _untisPassword = "";
[ObservableProperty] private string _untisApiStatus = "";
[ObservableProperty] private bool _untisApiBusy;
+ [ObservableProperty] private string? _untisHomeroomClassName;
+ public string UntisHomeroomClassDisplay => UntisHomeroomClassName is { Length: > 0 } name
+ ? $"Ausgewählt: {name}"
+ : "Keine Klasse ausgewählt.";
+ partial void OnUntisHomeroomClassNameChanged(string? value) => OnPropertyChanged(nameof(UntisHomeroomClassDisplay));
public Func? OnReviewUntisMapping { get; set; }
+ /// Vom Code-Behind gesetzt: öffnet den WebUntis-Klassenauswahldialog, liefert null bei Abbruch.
+ public Func>? OnPickHomeroomClass { get; set; }
// ── Schulweiter Jahresplan (ClassyPlan-iCal) ─────────────────────────────
@@ -559,6 +566,7 @@ public partial class SettingsViewModel : ObservableObject
UntisUsername = credentials.Username;
UntisApiStatus = $"API-Zugang für {credentials.Username} ist lokal verschlüsselt gespeichert.";
}
+ UntisHomeroomClassName = _untisSettings.HomeroomClassName;
}
[RelayCommand]
@@ -647,6 +655,23 @@ public partial class SettingsViewModel : ObservableObject
if (OnReviewUntisMapping is not null) await OnReviewUntisMapping();
}
+ [RelayCommand]
+ private async Task UntisPickHomeroomClass()
+ {
+ if (OnPickHomeroomClass is null) return;
+ var result = await OnPickHomeroomClass();
+ if (result is null) return;
+ _untisSettings.SetHomeroomClass(result.Value.UntisId, result.Value.Name);
+ LoadUntisSettings();
+ }
+
+ [RelayCommand]
+ private void UntisClearHomeroomClass()
+ {
+ _untisSettings.SetHomeroomClass(null, null);
+ LoadUntisSettings();
+ }
+
// ── Schulweiter Jahresplan: Laden / Speichern / Entfernen / Jetzt abrufen ─
private void LoadAnnualPlanSettings()
diff --git a/LehrerApp.Desktop/Views/Groups/ClassTeacherTabView.axaml b/LehrerApp.Desktop/Views/ClassTeacher/ClassTeacherDetailsView.axaml
similarity index 82%
rename from LehrerApp.Desktop/Views/Groups/ClassTeacherTabView.axaml
rename to LehrerApp.Desktop/Views/ClassTeacher/ClassTeacherDetailsView.axaml
index 10245ab..b992d2d 100644
--- a/LehrerApp.Desktop/Views/Groups/ClassTeacherTabView.axaml
+++ b/LehrerApp.Desktop/Views/ClassTeacher/ClassTeacherDetailsView.axaml
@@ -1,10 +1,10 @@
+ xmlns:vm="clr-namespace:LehrerApp.Desktop.ViewModels.ClassTeacher"
+ x:Class="LehrerApp.Desktop.Views.ClassTeacher.ClassTeacherDetailsView"
+ x:DataType="vm:ClassTeacherDetailsViewModel">
-
+
@@ -15,8 +15,14 @@
-
-
+
+
+
+
+
+
+
@@ -24,7 +30,7 @@
-
+
@@ -44,9 +50,9 @@
-
-
+
@@ -56,7 +62,7 @@
-
+
@@ -79,6 +85,6 @@
-
+
diff --git a/LehrerApp.Desktop/Views/ClassTeacher/ClassTeacherDetailsView.axaml.cs b/LehrerApp.Desktop/Views/ClassTeacher/ClassTeacherDetailsView.axaml.cs
new file mode 100644
index 0000000..877ea08
--- /dev/null
+++ b/LehrerApp.Desktop/Views/ClassTeacher/ClassTeacherDetailsView.axaml.cs
@@ -0,0 +1,17 @@
+using Avalonia.Controls;
+using Avalonia.Interactivity;
+using LehrerApp.Desktop.ViewModels.ClassTeacher;
+
+namespace LehrerApp.Desktop.Views.ClassTeacher;
+
+public partial class ClassTeacherDetailsView : UserControl
+{
+ public ClassTeacherDetailsView() => InitializeComponent();
+
+ private void OnClearStudentFilterClick(object? sender, RoutedEventArgs e)
+ {
+ if (DataContext is not ClassTeacherDetailsViewModel vm) return;
+ vm.StudentFilter = "";
+ vm.LoadCommand.Execute(null);
+ }
+}
diff --git a/LehrerApp.Desktop/Views/ClassTeacher/ClassTeacherOverviewView.axaml b/LehrerApp.Desktop/Views/ClassTeacher/ClassTeacherOverviewView.axaml
new file mode 100644
index 0000000..d75743e
--- /dev/null
+++ b/LehrerApp.Desktop/Views/ClassTeacher/ClassTeacherOverviewView.axaml
@@ -0,0 +1,59 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/LehrerApp.Desktop/Views/ClassTeacher/ClassTeacherOverviewView.axaml.cs b/LehrerApp.Desktop/Views/ClassTeacher/ClassTeacherOverviewView.axaml.cs
new file mode 100644
index 0000000..4de2158
--- /dev/null
+++ b/LehrerApp.Desktop/Views/ClassTeacher/ClassTeacherOverviewView.axaml.cs
@@ -0,0 +1,8 @@
+using Avalonia.Controls;
+
+namespace LehrerApp.Desktop.Views.ClassTeacher;
+
+public partial class ClassTeacherOverviewView : UserControl
+{
+ public ClassTeacherOverviewView() => InitializeComponent();
+}
diff --git a/LehrerApp.Desktop/Views/Groups/AddGroupDialog.axaml b/LehrerApp.Desktop/Views/Groups/AddGroupDialog.axaml
index d4070c7..fd63de9 100644
--- a/LehrerApp.Desktop/Views/Groups/AddGroupDialog.axaml
+++ b/LehrerApp.Desktop/Views/Groups/AddGroupDialog.axaml
@@ -71,9 +71,6 @@
-
-
diff --git a/LehrerApp.Desktop/Views/Groups/ClassTeacherTabView.axaml.cs b/LehrerApp.Desktop/Views/Groups/ClassTeacherTabView.axaml.cs
deleted file mode 100644
index dfbf55c..0000000
--- a/LehrerApp.Desktop/Views/Groups/ClassTeacherTabView.axaml.cs
+++ /dev/null
@@ -1,8 +0,0 @@
-using Avalonia.Controls;
-
-namespace LehrerApp.Desktop.Views.Groups;
-
-public partial class ClassTeacherTabView : UserControl
-{
- public ClassTeacherTabView() => InitializeComponent();
-}
diff --git a/LehrerApp.Desktop/Views/Groups/GroupDetailView.axaml b/LehrerApp.Desktop/Views/Groups/GroupDetailView.axaml
index dd9b41c..085241a 100644
--- a/LehrerApp.Desktop/Views/Groups/GroupDetailView.axaml
+++ b/LehrerApp.Desktop/Views/Groups/GroupDetailView.axaml
@@ -205,11 +205,6 @@
-
-
-
-
-
diff --git a/LehrerApp.Desktop/Views/MainWindow.axaml b/LehrerApp.Desktop/Views/MainWindow.axaml
index 88d42fe..e1fc74d 100644
--- a/LehrerApp.Desktop/Views/MainWindow.axaml
+++ b/LehrerApp.Desktop/Views/MainWindow.axaml
@@ -13,6 +13,8 @@
xmlns:vmp="clr-namespace:LehrerApp.Desktop.ViewModels.Planning"
xmlns:vw="clr-namespace:LehrerApp.Desktop.Views.Workload"
xmlns:vmw="clr-namespace:LehrerApp.Desktop.ViewModels.Workload"
+ xmlns:vct="clr-namespace:LehrerApp.Desktop.Views.ClassTeacher"
+ xmlns:vmct="clr-namespace:LehrerApp.Desktop.ViewModels.ClassTeacher"
xmlns:svc="clr-namespace:LehrerApp.Desktop.Services"
x:Class="LehrerApp.Desktop.Views.MainWindow"
x:DataType="vm:MainWindowViewModel"
@@ -58,6 +60,9 @@
+
+
+
@@ -209,6 +214,16 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/LehrerApp.Desktop/Views/Settings/SettingsView.axaml.cs b/LehrerApp.Desktop/Views/Settings/SettingsView.axaml.cs
index c4607ac..3ef74aa 100644
--- a/LehrerApp.Desktop/Views/Settings/SettingsView.axaml.cs
+++ b/LehrerApp.Desktop/Views/Settings/SettingsView.axaml.cs
@@ -4,8 +4,10 @@ using Avalonia.Platform.Storage;
using LehrerApp.Core.Interfaces;
using LehrerApp.Desktop.Services;
using LehrerApp.Desktop.ViewModels;
+using LehrerApp.Desktop.ViewModels.Groups;
using LehrerApp.Desktop.ViewModels.Planning;
using LehrerApp.Desktop.ViewModels.Settings;
+using LehrerApp.Desktop.Views.Groups;
using LehrerApp.Desktop.Views.Planning;
using LehrerApp.Desktop.Views.Shared;
using Microsoft.Extensions.DependencyInjection;
@@ -31,6 +33,7 @@ public partial class SettingsView : UserControl
vm.OnConfirmRecoveryRestore = ShowRecoveryRestoreConfirmDialog;
vm.OnThemeChanged = App.ApplyTheme;
vm.OnReviewUntisMapping = ShowUntisMappingReviewDialog;
+ vm.OnPickHomeroomClass = PickHomeroomClassAsync;
_ = vm.LoadSchoolLocationCommand.ExecuteAsync(null);
}
}
@@ -51,6 +54,20 @@ public partial class SettingsView : UserControl
await dialog.ShowDialog(owner);
}
+ private async Task<(int UntisId, string Name)?> PickHomeroomClassAsync()
+ {
+ var owner = TopLevel.GetTopLevel(this) as Window;
+ if (owner is null) return null;
+
+ var untis = App.Services.GetRequiredService();
+ var selectionVm = new WebUntisClassSelectionViewModel(untis);
+ var selection = new WebUntisClassSelectionDialog { DataContext = selectionVm };
+ await selectionVm.InitializeAsync();
+ if (!await selection.ShowDialog(owner) || selectionVm.SelectedClass is null) return null;
+
+ return (selectionVm.SelectedClass.UntisId, selectionVm.SelectedClass.Name);
+ }
+
private async Task SaveRecoveryFile(string content)
{
var topLevel = TopLevel.GetTopLevel(this);
diff --git a/TODO.md b/TODO.md
index 612a6cc..ce0eb56 100644
--- a/TODO.md
+++ b/TODO.md
@@ -1277,53 +1277,57 @@ eigenen Unterricht abfragt und deshalb mit den regulären Lehrkraft-Rechten funk
Auch hier keine externe Schülerkennung, nur der Name — Namensabgleich soll deshalb zusätzlich auf
Klasse+Fach aus der Zeile scopen (→ genau eine lokale Lerngruppe), nicht global über alle Schüler.
Abgleichsdialog (Diff gegen `Documentation`, Übernehmen in beide Richtungen) noch nicht gebaut.
-- [x] **"Klassenlehrer"-Feature, Schritt 1 (Klassenbuch fremder Einträge, August 2026):** Der
- "-alle-"-Bericht enthält für die eigene Klasse auch Einträge, die andere Lehrkräfte in deren
- Fächern angelegt haben (`Benutzer` ≠ eigener Login) — für den reinen Dokumentations-Abgleich
- (eigene Einträge synchron halten) werden die herausgefiltert. Neues Flag
- `LearningGroup.IsClassTeacher` ("Klassenlehrer/-in dieser Klasse", Stammdaten-Checkbox, keine
- Migration nötig) blendet einen neuen Tab "Klassenlehrer" in der Gruppendetailansicht ein —
- bewusst ein eigenes Flag statt `IsOwnClass` (das ist rein die Kalenderfarb-Markierung und
- mehrfach setzbar für Klasse + zugehörige Kurse, würde den Tab sonst versehentlich auch bei
- Kursen zeigen). `WebUntisIntegrationService.GetForeignClassRegisterEventsAsync` ist das
- Spiegelbild von `GetOwnClassRegisterEventsAsync`: derselbe "-alle-"-Rohbericht, aber Einträge
- anderer Lehrkräfte statt der eigenen, zusätzlich auf die übergebene Klasse eingeschränkt (kein
- neuer WebUntis-Bericht nötig). `ClassTeacherTabViewModel`/`ClassTeacherTabView` zeigen die
- Liste rein lesend an (Datum, Schüler*in, Fach/Lehrkraft, Kategorie, Text) — kein
- "Übernehmen"-Command wie beim Dokumentations-Abgleich, da es fremde Einträge sind, keine
- eigenen zum lokalen Synchronisieren.
- **Nicht Teil dieses Schritts:** Fehlzeiten der Klasse aus fremdem Unterricht — dafür fehlt noch
- ein per Netzwerk-Mitschnitt zu ermittelnder klassenbezogener Fehlzeiten-Bericht ohne `lessonId`
- (vermutlich `klasseId`/`teacherId` statt `lsid`); folgt als eigener Schritt, sobald die
- Query-Parameter bekannt sind. Ebenfalls zurückgestellt: Dokumentation/Behördenschriftverkehr als
- eigenständiger, WebUntis-unabhängiger Baustein.
-- [x] **"Klassenlehrer"-Feature, Schritt 2 (Fehlzeiten aus fremdem Unterricht, August 2026):** Der
- gesuchte Bericht ist `reports.do?name=AbsencePerStudent&klasseOrStudentgroupId=KL&studentId=-1`
- (per Netzwerk-Mitschnitt gefunden) — Query-Parameter sind bis auf `name`/`klasseOrStudentgroupId`/
- `studentId` identisch mit `ClassregEventPerStudent`. `klasseOrStudentgroupId` ist `"KL"` +
- die `getKlassen`-interne Klassen-ID (dasselbe Element-Kürzel-Schema wie `kl`/`te`/`su`/`ro` bei
- anderen Berichten) — **kein manuelles Feld nötig**, `WebUntisIntegrationService.ResolveClassIdAsync`
- löst sie pro Aufruf über `getSchoolyears`+`getKlassen` und den Klassennamen auf (analog zum
- bestehenden Klassen-Auswahldialog). Das CSV-Format ist **nicht** identisch mit `AbsencePerLesson`
- (ursprüngliche Vermutung anhand echter Daten widerlegt) — eigenes Spaltenschema, eine Zeile pro
- Fehlstunde, neuer Parser `WebUntisClassAbsenceParser`/Modell `UntisClassAbsenceEntry`. Anders als
- `AbsencePerLesson` liefert `Status` hier einen Klartext-Wert direkt (im Sample nur `"nicht
- entsch."` beobachtet, andere Werte nicht bestätigt), es braucht also keine Status-Heuristik.
- Der Header `Text` kommt zweimal vor (eine immer-leere Spalte, dann die eigentliche Tagesnotiz) —
- der bestehende Dictionary-per-Header-Parse-Stil überschreibt das zufällig richtig, im Parser
- kommentiert statt stillschweigend vorausgesetzt. `Fehltage` (0/1, `CountsAsFullDay`) ist laut
- Nutzer-Angabe der auf eine ganze Zahl abgerundete Fehlzeit-Tagesanteil: bleibt 0, bis auch die
+- [x] **"Klassenlehrer"-Feature — Datenschicht (August 2026):** Zwei WebUntis-Berichte liefern, was
+ für einen Klassenlehrer-Überblick fehlte. `WebUntisIntegrationService.GetForeignClassRegisterEventsAsync`
+ ist das Spiegelbild von `GetOwnClassRegisterEventsAsync`: derselbe "-alle-"-Rohbericht
+ (`ClassregEventPerStudent`), aber Einträge anderer Lehrkräfte (`Benutzer` ≠ eigener Login) statt
+ der eigenen, auf eine Klasse eingeschränkt — kein neuer Bericht nötig.
+ `GetClassAbsencesAsync`/`reports.do?name=AbsencePerStudent&klasseOrStudentgroupId=KL&studentId=-1`
+ (per Netzwerk-Mitschnitt gefunden) liefert Fehlzeiten der ganzen Klasse über alle Fächer, nicht nur
+ eigenen Unterricht. `klasseOrStudentgroupId` ist `"KL"` + die `getKlassen`-interne Klassen-ID
+ (dasselbe Element-Kürzel-Schema wie `kl`/`te`/`su`/`ro` bei anderen Berichten) —
+ `WebUntisIntegrationService.ResolveClassIdAsync` löst sie pro Aufruf über
+ `getSchoolyears`+`getKlassen` und den Klassennamen auf. Das CSV-Format ist **nicht** identisch mit
+ `AbsencePerLesson` (ursprüngliche Vermutung anhand echter Daten widerlegt) — eigenes Spaltenschema,
+ eine Zeile pro Fehlstunde, eigener Parser `WebUntisClassAbsenceParser`/Modell
+ `UntisClassAbsenceEntry`. Anders als `AbsencePerLesson` liefert `Status` hier einen Klartext-Wert
+ direkt (im Sample nur `"nicht entsch."` beobachtet, andere Werte nicht bestätigt), es braucht also
+ keine Status-Heuristik. Der Header `Text` kommt zweimal vor (eine immer-leere Spalte, dann die
+ eigentliche Tagesnotiz) — der Dictionary-per-Header-Parse-Stil überschreibt das zufällig richtig,
+ im Parser kommentiert statt stillschweigend vorausgesetzt. `Fehltage` (0/1, `CountsAsFullDay`) ist
+ laut Nutzer-Angabe der auf eine ganze Zahl abgerundete Fehlzeit-Tagesanteil: bleibt 0, bis auch die
letzte Unterrichtsstunde des Tages als versäumt gilt, erst deren Zeile trägt dann den Wert 1.
- Im Klassenlehrer-Tab (Nutzer-Feedback) **pro Schüler*in und Tag gruppiert** dargestellt statt
- einer Zeile pro Fehlstunde — `ClassAbsenceDaySummaryRow.GroupByStudentAndDay` fasst Fehlstunden/
- -minuten zusammen und sammelt Fächer/Status/Gründe als distinkte Listen. Rein Ansicht, kein
- Abgleich/Übernehmen in lokale Anwesenheitsdaten; eigene und fremde Fächer erscheinen bewusst
- beide ungefiltert.
- **Zurückgestellte Idee:** ein Detail-Drill-down pro Schüler*in (z. B. Heatmap der gefehlten
- Einzelstunden, Wochenansicht mit Verspätung/vorzeitigem Verlassen) — noch offen, ob im selben Tab
- oder über einen eigenen Tab/Dialog "in die Tiefe" erreichbar; die Rohdaten
- (`UntisClassAbsenceEntryDto`) sind dafür schon vollständig vorhanden, nur noch nicht in der UI
- genutzt.
+ `ClassAbsenceDaySummaryRow.GroupByStudentAndDay` fasst die Fehlstunden pro Schüler*in/Tag zusammen
+ (Fächer/Status/Gründe als distinkte Listen) statt eine Zeile pro Fehlstunde zu zeigen.
+- [x] **"Klassenlehrer"-Feature — Neuausrichtung (August 2026):** Die erste Umsetzung hing das
+ Feature an ein `LearningGroup.IsClassTeacher`-Flag mit Tab in der Gruppendetailansicht.
+ Nutzer-Feedback: konzeptionell falsch — Klassenlehrer ist man für eine ganze Klasse, unabhängig
+ vom eigenen Unterricht (die Checkbox saß z. B. auch an "SOL"/"Klassenrat"), und die Übersicht wird
+ gerade an Tagen gebraucht, an denen man selbst keinen Unterricht mit der Klasse hat.
+ `LearningGroup.IsClassTeacher` wieder entfernt. Stattdessen:
+ - **Klassenauswahl in den Einstellungen** statt an einer Gruppe:
+ `WebUntisSettingsService.HomeroomClassUntisId`/`HomeroomClassName` (unverschlüsselt, gleiches
+ Muster wie `TeacherUntisId`), Auswahl im "Untis-Einbettung"-Tab über den bestehenden
+ `WebUntisClassSelectionViewModel`/`WebUntisClassSelectionDialog` (bisher nur für den
+ WebUntis-Klassenimport in der Kursansicht genutzt).
+ - **Eigener Top-Level-Bereich** "Klassenlehrer" in der Seitenleiste (`NavItem.ClassTeacher`,
+ `ClassTeacherOverviewViewModel`) statt Tab in einer `LearningGroup`-Detailansicht — sichtbar und
+ ladbar unabhängig davon, ob heute eigener Unterricht mit der Klasse stattfindet. Zeigt bei
+ fehlender Klassenauswahl einen Hinweis mit Sprung in die Einstellungen.
+ - **Zwei Ebenen** (Nutzer-Feedback: "auf die Schnelle" statt Rohdaten-Listen als Erstes):
+ "Übersicht" — kompaktes Roster (`ClassTeacherRosterRow.Build`, Zuordnung primär über `ExternKey`
+ für die Fehlzeiten, Namens-Fallback für den Klassenbuch-Bericht ohne `ExternKey`) mit
+ Ampel-Symbol "keine gemeldete Fehlzeit heute laut WebUntis" (unabhängig von eigener
+ Anwesenheitserfassung) und Badge bei Klassenbucheinträgen anderer Lehrkräfte in den letzten 7
+ Tagen; "Details" — die bisherigen Rohdaten-Listen (jetzt `ClassTeacherDetailsViewModel`/
+ `ClassTeacherDetailsView`, aus `ViewModels/Groups`/`Views/Groups` nach
+ `ViewModels/ClassTeacher`/`Views/ClassTeacher` verschoben, `Initialize(string className)` statt
+ `Initialize(Guid groupId)`), aus der Übersicht per Klick auf eine Zeile gefiltert auf
+ eine/n Schüler*in anspringbar (`ClassTeacherDetailsViewModel.StudentFilter`).
+ **Zurückgestellte Ideen:** Verknüpfung Roster-Zeile ↔ lokaler `Student`/Navigation zum
+ Schülerdetail; ein Detail-Drill-down pro Schüler*in (Heatmap gefehlter Einzelstunden,
+ Verspätung/vorzeitiges Verlassen je Woche); eine KI-gestützte Zusammenfassung/Bewertung der
+ Einträge (pro Schüler*in oder gesamt) mit Namens-Anonymisierung/Synonymen vor dem Versand.
### 4.4 Wochen-/Tagesansicht
- [x] **4.4.1** Kalenderansicht über alle Gruppen: Woche und Tag — siehe Nachtrag zu 4.3