diff --git a/LehrerApp.Desktop/ViewModels/Groups/WebUntisHomeworkComparisonViewModel.cs b/LehrerApp.Desktop/ViewModels/Groups/WebUntisHomeworkComparisonViewModel.cs
new file mode 100644
index 0000000..73f303b
--- /dev/null
+++ b/LehrerApp.Desktop/ViewModels/Groups/WebUntisHomeworkComparisonViewModel.cs
@@ -0,0 +1,229 @@
+using System.Collections.ObjectModel;
+using System.Text.RegularExpressions;
+using CommunityToolkit.Mvvm.ComponentModel;
+using CommunityToolkit.Mvvm.Input;
+using LehrerApp.Core.Interfaces;
+using LehrerApp.Core.Models;
+using LehrerApp.Core.Services;
+using LehrerApp.Desktop.Services;
+
+namespace LehrerApp.Desktop.ViewModels.Groups;
+
+/// Eine Zeile bleibt auch ohne automatische Zuordnung sichtbar -
+/// kann manuell per Auswahlliste gesetzt werden, gleiches Muster wie
+/// und
+/// .
+public partial class WebUntisHomeworkRow : ObservableObject
+{
+ public required string ClassName { get; init; }
+ public required DateOnly Date { get; init; }
+ public required string UntisStudentName { get; init; }
+ public string? SubjectLabel { get; init; }
+ public string? Text { get; init; }
+ public required IReadOnlyList Candidates { get; init; }
+ internal Guid? MatchedSubjectId { get; init; }
+ internal Action? OnAssignmentChanged { get; init; }
+ public string DateLabel => Date.ToString("dd.MM.yyyy");
+ public bool CanApply => SessionId is not null;
+
+ [ObservableProperty] private Student? _assignedStudent;
+ [ObservableProperty] private string _localStatus = "ohne Zuordnung";
+ [ObservableProperty] private Guid? _sessionId;
+ [ObservableProperty] private bool _selected;
+
+ partial void OnAssignedStudentChanged(Student? value) => OnAssignmentChanged?.Invoke(this);
+}
+
+/// Abgleich negativer Klassenbucheinträge zum Stichwort "Hausaufgabe" (eigene, siehe
+/// ) gegen
+/// - Dashboard-weit statt pro Lerngruppe, aus demselben
+/// Grund wie beim Dokumentations-Abgleich (der WebUntis-"-alle-"-Bericht ist klassenübergreifend,
+/// siehe TODO.md). Nutzer-Feedback: WebUntis liefert für Klassenbucheinträge weder eine externe
+/// Schülerkennung noch eine feste Fach-/Lerngruppenzuordnung, aber der Lehrkraft-eigene Text enthält
+/// praktisch immer das Untis-Fachkürzel - darüber wird die passende Lerngruppe (SubjectId + aktive
+/// Mitgliedschaft am Eintragsdatum) aufgelöst, mehrdeutige Treffer bleiben unaufgelöst statt zu raten.
+/// Ein Eintrag setzt lokal ausschließlich vor, und auch nur,
+/// wenn dort noch gar kein Status hinterlegt ist - bereits vorhandene, feinere Erfassungen ("Teilweise
+/// angefertigt", "nachgereicht" usw.) werden nie automatisch überschrieben, sondern nur zum Vergleich
+/// danebengestellt (Nutzerwunsch).
+public partial class WebUntisHomeworkComparisonViewModel : ObservableObject
+{
+ private readonly WebUntisIntegrationService _untis;
+ private readonly IStudentRepository _students;
+ private readonly IGroupRepository _groups;
+ private readonly ISubjectRepository _subjects;
+ private readonly IGroupMembershipRepository _memberships;
+ private readonly IParticipationSessionRepository _sessions;
+ private readonly IParticipationRepository _participation;
+ private readonly SchoolYearService _schoolYears;
+
+ public ObservableCollection Rows { get; } = [];
+ [ObservableProperty] private DateTimeOffset? _startDate = DateTimeOffset.Now.AddDays(-7);
+ [ObservableProperty] private DateTimeOffset? _endDate = DateTimeOffset.Now;
+ [ObservableProperty] private string _status = "Zeitraum wählen und Klassenbucheinträge laden.";
+ [ObservableProperty] private bool _busy;
+
+ public WebUntisHomeworkComparisonViewModel(WebUntisIntegrationService untis, IStudentRepository students,
+ IGroupRepository groups, ISubjectRepository subjects, IGroupMembershipRepository memberships,
+ IParticipationSessionRepository sessions, IParticipationRepository participation,
+ SchoolYearService schoolYears)
+ {
+ _untis = untis; _students = students; _groups = groups; _subjects = subjects;
+ _memberships = memberships; _sessions = sessions; _participation = participation;
+ _schoolYears = schoolYears;
+ }
+
+ [RelayCommand]
+ private async Task Load()
+ {
+ var start = DateOnly.FromDateTime((StartDate ?? DateTimeOffset.Now).LocalDateTime);
+ var end = DateOnly.FromDateTime((EndDate ?? DateTimeOffset.Now).LocalDateTime);
+ if (end < start) { Status = "Das Enddatum darf nicht vor dem Startdatum liegen."; return; }
+ Busy = true; Rows.Clear();
+ try
+ {
+ var ownStudents = _students.GetAll();
+ var globalIndex = BuildNameIndex(ownStudents);
+ // Wie beim Dokumentations-Abgleich zusätzlich pro Klasse indiziert, um gleiche Namen in
+ // verschiedenen Klassen unterscheiden zu können.
+ var classIndexes = _groups.GetAll()
+ .Where(g => g.Type == GroupType.Class)
+ .GroupBy(g => g.Name, StringComparer.OrdinalIgnoreCase)
+ .ToDictionary(
+ g => g.Key,
+ g => BuildNameIndex(g.SelectMany(x => _students.GetByGroup(x.Id)).Distinct().ToList()),
+ StringComparer.OrdinalIgnoreCase);
+ var shortNameIndex = _subjects.GetAll()
+ .Where(s => !string.IsNullOrWhiteSpace(s.ShortName))
+ .GroupBy(s => s.ShortName.Trim().ToUpperInvariant())
+ .Where(g => g.Count() == 1) // mehrdeutiges Kürzel lieber nicht zuordnen als raten
+ .ToDictionary(g => g.Key, g => g.First());
+
+ var entries = await _untis.GetOwnClassRegisterEventsAsync(start, end);
+ var ordered = entries
+ .Where(IsMissingHomeworkEntry)
+ .Select(e => (Entry: e, Date: TryDate(e.Date, out var d) ? d : (DateOnly?)null))
+ .Where(x => x.Date is not null)
+ .OrderBy(x => x.Date).ThenBy(x => x.Entry.StudentName);
+
+ void ResolveLocalMatch(WebUntisHomeworkRow row)
+ {
+ if (row.AssignedStudent is not { } student)
+ {
+ row.SessionId = null; row.LocalStatus = "ohne Zuordnung"; row.Selected = false;
+ return;
+ }
+ var group = ResolveGroup(student, row.MatchedSubjectId, row.Date);
+ var session = group is null ? null
+ : _sessions.GetByGroup(group.Id).FirstOrDefault(s => s.Date == row.Date);
+ var entry = session is null ? null : _participation.GetBySessionAndStudent(session.Id, student.Id);
+ var current = entry is null ? null : HomeworkDisplay.Effective(entry);
+ row.SessionId = session?.Id;
+ row.LocalStatus = session is null
+ ? row.MatchedSubjectId is null ? "Fachkürzel nicht erkannt"
+ : group is null ? "kein passender Kurs gefunden" : "keine lokale Stunde"
+ : HomeworkDisplay.Label(current);
+ // Nur vorbelegen, wenn lokal noch überhaupt nichts erfasst ist - jeder vorhandene
+ // Status (auch ein bereits gesetztes "fehlt") bleibt unangetastet, siehe Klassenkommentar.
+ row.Selected = session is not null && current is null;
+ }
+
+ foreach (var (entry, date) in ordered)
+ {
+ var nameKey = NameKey(entry.StudentName);
+ var match = (classIndexes.TryGetValue(entry.ClassName, out var classIndex)
+ ? classIndex.GetValueOrDefault(nameKey)
+ : null)
+ ?? globalIndex.GetValueOrDefault(nameKey);
+ var subject = MatchSubject(entry.Text, shortNameIndex) ?? MatchSubject(entry.CategoryName, shortNameIndex);
+
+ var row = new WebUntisHomeworkRow
+ {
+ ClassName = entry.ClassName, Date = date!.Value, UntisStudentName = entry.StudentName,
+ SubjectLabel = subject?.ShortName, Text = entry.Text, Candidates = ownStudents,
+ MatchedSubjectId = subject?.Id, OnAssignmentChanged = ResolveLocalMatch,
+ };
+ Rows.Add(row);
+ row.AssignedStudent = match; // löst OnAssignedStudentChanged aus und setzt SessionId/LocalStatus/Selected
+ }
+
+ var unresolvedStudent = Rows.Count(x => x.AssignedStudent is null);
+ var unresolvedSubject = Rows.Count(x => x.MatchedSubjectId is null);
+ Status = $"{Rows.Count} Einträge \"fehlende Hausaufgabe\" erhalten" +
+ (unresolvedStudent > 0 ? $", {unresolvedStudent} bitte manuell zuordnen" : "") +
+ (unresolvedSubject > 0 ? $", bei {unresolvedSubject} kein Fachkürzel im Text erkannt" : "") +
+ $". {Rows.Count(x => x.CanApply)} einer lokalen Stunde zuordenbar.";
+ }
+ catch (WebUntisIntegrationException ex) { Status = ex.Message; }
+ finally { Busy = false; }
+ }
+
+ [RelayCommand]
+ private void Apply()
+ {
+ var selected = Rows.Where(x => x.Selected && x.SessionId is not null && x.AssignedStudent is not null).ToList();
+ foreach (var row in selected)
+ {
+ var studentId = row.AssignedStudent!.Id;
+ var entry = _participation.GetBySessionAndStudent(row.SessionId!.Value, studentId)
+ ?? new ParticipationEntry { SessionId = row.SessionId.Value, StudentId = studentId };
+ entry.Homework = HomeworkStatus.MissingOpen;
+ entry.HomeworkMissing = true;
+ entry.UpdatedAt = DateTime.UtcNow;
+ _participation.Save(entry);
+ row.LocalStatus = HomeworkDisplay.Label(HomeworkStatus.MissingOpen);
+ row.Selected = false;
+ }
+ Status = $"{selected.Count} Hausaufgaben-Status übernommen.";
+ }
+
+ /// Nur Lerngruppen (nicht die Klasse selbst), bei denen der/die Schüler*in am Eintragsdatum aktiv
+ /// Mitglied ist und deren Fach zum erkannten Kürzel passt - mehr als ein Treffer bleibt bewusst
+ /// unaufgelöst statt irgendeinen davon zu wählen.
+ private LearningGroup? ResolveGroup(Student student, Guid? subjectId, DateOnly date)
+ {
+ if (subjectId is null) return null;
+ var schoolYear = _schoolYears.CurrentSchoolYear(date);
+ var candidates = _memberships.GetByStudent(student.Id)
+ .Where(m => GroupMembershipService.IsActiveOn(m, date))
+ .Select(m => _groups.GetById(m.GroupId))
+ .Where(g => g is not null && g.SubjectId == subjectId && g.SchoolYear == schoolYear)
+ .Cast()
+ .ToList();
+ return candidates.Count == 1 ? candidates[0] : null;
+ }
+
+ // Gleiche Heuristik wie ClassTeacherDetailsViewModel.ContainsHomework, zusätzlich auf negative
+ // Einträge eingeschränkt (eine positive "Hausaufgabe"-Kategorie wäre kein Fehlen-Signal).
+ private static bool IsMissingHomeworkEntry(UntisClassRegisterEventDto entry) =>
+ string.Equals(entry.CategoryGroup, "Negativ", StringComparison.OrdinalIgnoreCase) &&
+ (entry.CategoryName?.Contains("Hausauf", StringComparison.OrdinalIgnoreCase) == true ||
+ entry.Text?.Contains("Hausauf", StringComparison.OrdinalIgnoreCase) == true);
+
+ private static Subject? MatchSubject(string? text, IReadOnlyDictionary shortNameIndex)
+ {
+ if (string.IsNullOrWhiteSpace(text)) return null;
+ foreach (var token in Regex.Split(text, @"[^\p{L}\p{Nd}]+"))
+ if (token.Length > 0 && shortNameIndex.TryGetValue(token.ToUpperInvariant(), out var subject))
+ return subject;
+ return null;
+ }
+
+ private static string NameKey(string value) => value.Trim().ToLowerInvariant();
+
+ // Wie bei den übrigen WebUntis-Abgleichen: beide Namensreihenfolgen registriert, aber nur falls
+ // innerhalb der Kandidaten eindeutig.
+ private static Dictionary BuildNameIndex(IReadOnlyList candidates) =>
+ candidates
+ .SelectMany(student => new[]
+ {
+ NameKey($"{student.LastName} {student.FirstName}"),
+ NameKey($"{student.FirstName} {student.LastName}"),
+ }.Select(key => (Key: key, Student: student)))
+ .GroupBy(x => x.Key)
+ .Where(group => group.Select(x => x.Student).Distinct().Count() == 1)
+ .ToDictionary(group => group.Key, group => group.First().Student);
+
+ private static bool TryDate(int value, out DateOnly date) =>
+ DateOnly.TryParseExact(value.ToString(), "yyyyMMdd", out date);
+}
diff --git a/LehrerApp.Desktop/ViewModels/MainWindowViewModel.cs b/LehrerApp.Desktop/ViewModels/MainWindowViewModel.cs
index 1790690..f328767 100644
--- a/LehrerApp.Desktop/ViewModels/MainWindowViewModel.cs
+++ b/LehrerApp.Desktop/ViewModels/MainWindowViewModel.cs
@@ -150,8 +150,11 @@ public partial class MainWindowViewModel : ObservableObject
private TimetableViewModel GetTimetable()
{
+ // WeekOffset bewusst nicht zurücksetzen: TimetableViewModel ist ein DI-Singleton, die
+ // zuletzt angezeigte Woche bleibt also automatisch über Tab-Wechsel hinweg erhalten
+ // (Nutzer-Feedback: beim Planen der nächsten Woche nervt es, nach jedem kurzen Abstecher
+ // in einen anderen Bereich wieder manuell dorthin zurückblättern zu müssen).
var timetable = _services.GetRequiredService();
- timetable.WeekOffset = 0;
timetable.Load();
timetable.ActiveTabIndex = 0;
return timetable;
diff --git a/LehrerApp.Desktop/Views/Dashboard/DashboardView.axaml b/LehrerApp.Desktop/Views/Dashboard/DashboardView.axaml
index 997b313..3c4122f 100644
--- a/LehrerApp.Desktop/Views/Dashboard/DashboardView.axaml
+++ b/LehrerApp.Desktop/Views/Dashboard/DashboardView.axaml
@@ -28,6 +28,8 @@
+
diff --git a/LehrerApp.Desktop/Views/Dashboard/DashboardView.axaml.cs b/LehrerApp.Desktop/Views/Dashboard/DashboardView.axaml.cs
index 1d22762..0656330 100644
--- a/LehrerApp.Desktop/Views/Dashboard/DashboardView.axaml.cs
+++ b/LehrerApp.Desktop/Views/Dashboard/DashboardView.axaml.cs
@@ -5,7 +5,9 @@ using LehrerApp.Core.Models;
using LehrerApp.Core.Services;
using LehrerApp.Desktop.Services;
using LehrerApp.Desktop.ViewModels;
+using LehrerApp.Desktop.ViewModels.Groups;
using LehrerApp.Desktop.ViewModels.Students;
+using LehrerApp.Desktop.Views.Groups;
using LehrerApp.Desktop.Views.Students;
using LehrerApp.Desktop.Views.Workload;
using Microsoft.Extensions.DependencyInjection;
@@ -42,4 +44,20 @@ public partial class DashboardView : UserControl
App.Services.GetRequiredService());
await new WebUntisDocumentationComparisonDialog { DataContext = dialogVm }.ShowDialog(owner);
}
+
+ private async void OnCompareWebUntisHomeworkClick(object? sender, RoutedEventArgs e)
+ {
+ var owner = TopLevel.GetTopLevel(this) as Window;
+ if (owner is null) return;
+ var dialogVm = new WebUntisHomeworkComparisonViewModel(
+ App.Services.GetRequiredService(),
+ App.Services.GetRequiredService(),
+ App.Services.GetRequiredService(),
+ App.Services.GetRequiredService(),
+ App.Services.GetRequiredService(),
+ App.Services.GetRequiredService(),
+ App.Services.GetRequiredService(),
+ App.Services.GetRequiredService());
+ await new WebUntisHomeworkComparisonDialog { DataContext = dialogVm }.ShowDialog(owner);
+ }
}
diff --git a/LehrerApp.Desktop/Views/Groups/WebUntisHomeworkComparisonDialog.axaml b/LehrerApp.Desktop/Views/Groups/WebUntisHomeworkComparisonDialog.axaml
new file mode 100644
index 0000000..842d84b
--- /dev/null
+++ b/LehrerApp.Desktop/Views/Groups/WebUntisHomeworkComparisonDialog.axaml
@@ -0,0 +1,58 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/LehrerApp.Desktop/Views/Groups/WebUntisHomeworkComparisonDialog.axaml.cs b/LehrerApp.Desktop/Views/Groups/WebUntisHomeworkComparisonDialog.axaml.cs
new file mode 100644
index 0000000..f3de237
--- /dev/null
+++ b/LehrerApp.Desktop/Views/Groups/WebUntisHomeworkComparisonDialog.axaml.cs
@@ -0,0 +1,11 @@
+using Avalonia.Controls;
+using Avalonia.Interactivity;
+
+namespace LehrerApp.Desktop.Views.Groups;
+
+public partial class WebUntisHomeworkComparisonDialog : Window
+{
+ public WebUntisHomeworkComparisonDialog() => InitializeComponent();
+
+ private void OnClose(object? sender, RoutedEventArgs e) => Close();
+}
diff --git a/TODO.md b/TODO.md
index 37b72b5..8b59b7e 100644
--- a/TODO.md
+++ b/TODO.md
@@ -982,7 +982,9 @@ angezeigten Woche geklebt, nicht nur in der einen Woche, zu der es gehört). Fü
berechnen `HolidayBadgeFor`/`IsLastBeforeExamFor` die Badges deshalb jetzt je Zelle am dort
gezeigten Datum statt an "heute" — das Bearbeiten-Raster (zeigt ohnehin nur das wiederkehrende
Muster ohne Datum) behält die alte, "heute"-verankerte Berechnung. Beim erneuten Navigieren in den
-Stundenplan (Sidebar-Klick) springt die Ansicht wieder auf die laufende Woche zurück.
+Stundenplan (Sidebar-Klick) sprang die Ansicht ursprünglich wieder auf die laufende Woche zurück —
+siehe "Nachtrag zu 4.3, Bugfix Wochennavigation (September 2026)" am Ende dieses Abschnitts, wo
+dieses Verhalten auf Nutzer-Feedback hin wieder entfernt wurde.
**Nachtrag zu 4.3, fünfte Iteration (Aufsichten + Vertretung):** Nutzer-Feedback: zwischen manchen
Stunden ist auch Pausenaufsicht zu erledigen, und gelegentlich kommen Sonderfälle vor — eine
@@ -1930,6 +1932,69 @@ eigenen Unterricht abfragt und deshalb mit den regulären Lehrkraft-Rechten funk
Vorgänge je Klassen-Roster-Namen über denselben Namensabgleich wie die übrigen `Build*`-Methoden
dieser Klasse auf (`IVorgangRepository.GetByStudent`).
+**Nachtrag zu 4.3, Bugfix Wochennavigation (September 2026):** Nutzer-Feedback: beim Planen der
+nächsten Woche musste nach jedem kurzen Abstecher in einen anderen Bereich der Sidebar wieder
+manuell zur zuvor betrachteten Woche zurückgeblättert werden ("man muss nicht nur einmal
+hinspringen, sondern jedes Mal wieder neu"). Ursache war das in der vierten Iteration dokumentierte
+Verhalten, dass `MainWindowViewModel.GetTimetable()` bei jedem erneuten Aufruf der Seitenleisten-
+Navigation `TimetableViewModel.WeekOffset` explizit auf 0 zurücksetzte — obwohl `TimetableViewModel`
+als DI-Singleton ohnehin über die gesamte Sitzung hinweg bestehen bleibt und `WeekOffset` daher ohne
+diese Zeile ganz von selbst erhalten bliebe. Der Reset wurde entfernt; ein separater Zustands-
+Singleton/eine statische Variable dafür war nicht nötig. Das erklärt vermutlich auch den zweiten
+gemeldeten Effekt ("der Zurück-Button springt manchmal zwei Wochen zurück"): wer z.B. in Woche +1
+plante, kurz wegnavigierte und danach einmal "‹" klickte, landete wegen des Resets in Woche -1 statt
+der erwarteten Woche 0 — gefühlt ein Sprung um zwei Wochen, obwohl `PreviousWeekCommand`/
+`NextWeekCommand` selbst korrekt je Klick nur um eine Woche verschieben (siehe
+`NextWeek_BadgeGehoertZurAngezeigtenWoche_NichtZuHeute`, das zwei aufeinanderfolgende Klicks bewusst
+als zwei Wochen erwartet). Der "Diese Woche"-Button existierte bereits seit der vierten Iteration
+(`CurrentWeekCommand`, nur sichtbar wenn `!IsCurrentWeek`).
+
+**Nachtrag zu 4.3, Abgleich fehlender Hausaufgaben (September 2026):** Nutzer-Feedback: negative
+Klassenbucheinträge zur Kategorie "fehlende HA" sind ein brauchbares Signal, ob ein/e Schüler*in die
+Hausaufgabe nicht gemacht hat, und sollen als Vorschlag mit der eigenen Erfassung
+(`ParticipationEntry.Homework`) abgeglichen werden können — ausdrücklich nicht die erteilte
+Hausaufgabe selbst (`Lesson.Homework`), das bleibt ein offener, separater Punkt.
+- Neuer Dashboard-weiter Dialog `WebUntisHomeworkComparisonViewModel`/`WebUntisHomeworkComparisonDialog`
+ ([LehrerApp.Desktop/ViewModels/Groups/WebUntisHomeworkComparisonViewModel.cs](LehrerApp.Desktop/ViewModels/Groups/WebUntisHomeworkComparisonViewModel.cs)),
+ Button "Fehlende Hausaufgaben abgleichen…" neben dem bereits vorhandenen "Klassenbuch
+ abgleichen…" auf dem Dashboard. Baut auf demselben Muster wie
+ `WebUntisDocumentationComparisonViewModel` (Dashboard-weit, da der WebUntis-"-alle-"-Bericht
+ klassenübergreifend ist) und `WebUntisLessonAbsenceComparisonViewModel` (schreibt wie dort über
+ `IParticipationRepository.GetBySessionAndStudent`/`Save`).
+- **Filter:** `CategoryGroup == "Negativ"` und `CategoryName`/`Text` enthält "Hausauf" — dieselbe
+ Substring-Heuristik wie `ClassTeacherDetailsViewModel.ContainsHomework`, nur zusätzlich auf
+ negative Einträge eingeschränkt.
+- **Fach-Erkennung:** Der Klassenbuch-Bericht liefert kein zuverlässiges Fach-Feld, aber laut Nutzer
+ enthält der mitgeschickte Text praktisch immer das Untis-Fachkürzel. Deshalb wird `Text` (Fallback
+ `CategoryName`) gegen `Subject.ShortName` alle Lerngruppen-Fächer abgeglichen (Wortgrenzen-Split,
+ nicht `Contains`, um kurze Kürzel nicht versehentlich in anderen Wörtern zu treffen); mehrdeutige
+ Kürzel (zwei Fächer mit demselben `ShortName`) werden bewusst nicht zugeordnet statt zu raten.
+- **Lerngruppe/Stunde auflösen:** aus dem per Namensabgleich gefundenen `Student` werden über
+ `IGroupMembershipRepository.GetByStudent` alle am Eintragsdatum aktiven Mitgliedschaften
+ (`GroupMembershipService.IsActiveOn`) genommen und auf das erkannte Fach + Schuljahr des
+ Eintragsdatums eingeschränkt; bleibt mehr als eine Lerngruppe übrig, bleibt die Zeile unaufgelöst
+ statt irgendeine zu wählen (gleiches Vorsichtsprinzip wie beim Namensabgleich). Die
+ `ParticipationSession` der Lerngruppe am Eintragsdatum liefert dann die Stunde für
+ `IParticipationRepository`.
+- **Kein Überschreiben (Kernanforderung):** eine Zeile wird nur vorausgewählt, wenn
+ `HomeworkDisplay.Effective(entry)` noch `null` ist, also lokal überhaupt noch nichts erfasst wurde
+ — jeder bereits vorhandene Status (auch ein bereits gesetztes "fehlt", nicht nur "Teilweise"/
+ "nachgereicht") bleibt unangetastet und wird nur zum Vergleich daneben angezeigt; die Checkbox
+ bleibt trotzdem bedienbar, falls doch bewusst überschrieben werden soll. Übernommen wird immer nur
+ `HomeworkStatus.MissingOpen` ("Nachreichen offen") — nie automatisch `MissingOverdue`, die weitere
+ Nachverfolgung bleibt laut Nutzer App-intern.
+- **Bewusst kein Caching:** `GetOwnClassRegisterEventsAsync` wird wie beim bereits bestehenden
+ `WebUntisDocumentationComparisonViewModel` direkt (ungecacht) aufgerufen statt über
+ `UntisReportCacheService`, dessen Cache-Tabellen aktuell klassen-/`GetForeignClassRegisterEventsAsync`-
+ spezifisch aufgebaut sind (ein Schema-Umbau für die eigenen, klassenübergreifenden Einträge wäre ein
+ eigenständiger, deutlich größerer Umbau gewesen, siehe Nutzer-Rückfrage). Beide "eigene Einträge"-
+ Dialoge lösen pro Öffnen einen eigenen Live-Abruf aus; falls das in der Praxis stört, wäre eine
+ gemeinsame Cache-Erweiterung ein sinnvoller separater Nachtrag.
+- Aus Konsistenz mit den beiden bestehenden, strukturell identischen Abgleichs-ViewModels bewusst
+ ungetestet gelassen: `WebUntisIntegrationService` ist `sealed` und nicht auf ein Interface
+ gemockt, `WebUntisLessonAbsenceComparisonViewModel`/`WebUntisDocumentationComparisonViewModel`
+ haben aus demselben Grund ebenfalls keine Tests.
+
### 4.4 Wochen-/Tagesansicht
- [x] **4.4.1** Kalenderansicht über alle Gruppen: Woche und Tag — siehe Nachtrag zu 4.3
("Heute"-Tab: Tagesliste unten angedockt, gruppenübergreifendes Wochenraster darüber, inkl.