diff --git a/LehrerApp.Desktop.Tests/TeachingModeViewModelTests.cs b/LehrerApp.Desktop.Tests/TeachingModeViewModelTests.cs index a3336ad..9873012 100644 --- a/LehrerApp.Desktop.Tests/TeachingModeViewModelTests.cs +++ b/LehrerApp.Desktop.Tests/TeachingModeViewModelTests.cs @@ -13,6 +13,17 @@ public sealed class TeachingModeViewModelTests new FakeStudents(students ?? []), new FakeMemberships(memberships ?? []), sessions ?? new FakeSessions([]), new FakeEntries(), new FakeAspects()); + /// Dieselben Fakes wie , aber für ParticipationTabViewModel — + /// beide brauchen eigene IParticipationSessionRepository/IParticipationRepository-Instanzen + /// im Produktivcode (siehe TeachingModeWindow-Kommentar zu ReloadSeatBadgesFromRepository), + /// hier reicht für die Tests aber eine gemeinsame FakeSessions-Instanz, damit beide + /// ViewModels dieselbe verknüpfte Sitzung sehen. + private static ParticipationTabViewModel BuildParticipation(LearningGroup group, + FakeSessions? sessions = null) => + new(sessions ?? new FakeSessions([]), new FakeEntries(), new FakeAspects(), + new FakeStudents([]), new FakeMemberships([]), new FakeGroups([group]), + new FakeCompetencyDomains()); + [Fact] public void Konstruktor_LaedtVerlaufsplanUndInitialisiertSitzplanFuerDieGruppe() { @@ -24,7 +35,8 @@ public sealed class TeachingModeViewModelTests }; var seatingPlan = BuildSeatingPlan(group.Id); - var vm = new TeachingModeViewModel(lesson, group, new FakeAlternativeLessonPaths([]), seatingPlan); + var vm = new TeachingModeViewModel(lesson, group, new FakeAlternativeLessonPaths([]), + new FakeLessons(), seatingPlan, BuildParticipation(group)); Assert.Equal("Q1 Chemie", vm.GroupName); Assert.Equal("Redox", vm.LessonInfo.Topic); @@ -41,13 +53,37 @@ public sealed class TeachingModeViewModelTests var sessions = new FakeSessions([]); var seatingPlan = BuildSeatingPlan(group.Id, sessions); - var vm = new TeachingModeViewModel(lesson, group, new FakeAlternativeLessonPaths([]), seatingPlan); + var vm = new TeachingModeViewModel(lesson, group, new FakeAlternativeLessonPaths([]), + new FakeLessons(), seatingPlan, BuildParticipation(group, sessions)); var created = Assert.Single(sessions.GetByGroup(group.Id)); Assert.Equal(lesson.Id, created.LessonId); Assert.Equal(vm.SeatingPlan.SelectedSession?.Id, created.Id); } + /// Nutzer-Feedback: Schnellbewertungs-Dialoge (Mitarbeit, Anwesenheit/Hausaufgabe) sollen sich + /// im Unterrichtsmodus direkt öffnen lassen — das setzt voraus, dass die dafür verwendete + /// ParticipationTabViewModel-Instanz von Anfang an auf der zu dieser Stunde gehörenden + /// Sitzung steht, nicht auf einer beliebigen anderen Sitzung der Gruppe. + [Fact] + public void Konstruktor_SelektiertDieselbeSitzungAuchInDerParticipationViewModel() + { + var group = new LearningGroup { Name = "Q1 Chemie" }; + var lesson = new Lesson + { GroupId = group.Id, Date = DateOnly.FromDateTime(DateTime.Today), Topic = "Redox" }; + var otherSession = new ParticipationSession + { GroupId = group.Id, Date = lesson.Date.AddDays(-1), Comment = "Andere Stunde" }; + var sessions = new FakeSessions([otherSession]); + var seatingPlan = BuildSeatingPlan(group.Id, sessions); + + var vm = new TeachingModeViewModel(lesson, group, new FakeAlternativeLessonPaths([]), + new FakeLessons(), seatingPlan, BuildParticipation(group, sessions)); + + Assert.NotNull(vm.SeatingPlan.SelectedSession); + Assert.Equal(vm.SeatingPlan.SelectedSession!.Id, vm.Participation.SelectedSession?.Id); + Assert.NotEqual(otherSession.Id, vm.Participation.SelectedSession?.Id); + } + [Fact] public void Konstruktor_BereitsExistierendeVerknuepfteSitzung_WirdWiederverwendet() { @@ -59,7 +95,8 @@ public sealed class TeachingModeViewModelTests var sessions = new FakeSessions([existing]); var seatingPlan = BuildSeatingPlan(group.Id, sessions); - var vm = new TeachingModeViewModel(lesson, group, new FakeAlternativeLessonPaths([]), seatingPlan); + var vm = new TeachingModeViewModel(lesson, group, new FakeAlternativeLessonPaths([]), + new FakeLessons(), seatingPlan, BuildParticipation(group, sessions)); Assert.Single(sessions.GetByGroup(group.Id)); Assert.Equal(existing.Id, vm.SeatingPlan.SelectedSession?.Id); @@ -75,8 +112,92 @@ public sealed class TeachingModeViewModelTests { GroupId = group.Id, Date = DateOnly.FromDateTime(DateTime.Today), Topic = "Redox" }; var seatingPlan = BuildSeatingPlan(group.Id); - var vm = new TeachingModeViewModel(lesson, group, new FakeAlternativeLessonPaths([]), seatingPlan); + var vm = new TeachingModeViewModel(lesson, group, new FakeAlternativeLessonPaths([]), + new FakeLessons(), seatingPlan, BuildParticipation(group)); Assert.False(vm.SeatingPlan.IsEditable); } + + // ── Hausaufgabe: letzte Stunde ansehen/abhaken, aktuelle einsehen/ändern ───── + + [Fact] + public void Konstruktor_FindetHausaufgabeDerVorherigenStunde() + { + var group = new LearningGroup { Name = "Q1 Chemie" }; + var today = DateOnly.FromDateTime(DateTime.Today); + var previous = new Lesson + { + GroupId = group.Id, Date = today.AddDays(-7), Topic = "Säuren", + Homework = "S. 42, Aufgabe 3", HomeworkChecked = false, + }; + var lesson = new Lesson { GroupId = group.Id, Date = today, Topic = "Redox" }; + var lessons = new FakeLessons(); + lessons.Add(previous); + var seatingPlan = BuildSeatingPlan(group.Id); + + var vm = new TeachingModeViewModel(lesson, group, new FakeAlternativeLessonPaths([]), + lessons, seatingPlan, BuildParticipation(group)); + + Assert.True(vm.Homework.HasPreviousHomework); + Assert.Equal("S. 42, Aufgabe 3", vm.Homework.PreviousHomeworkText); + Assert.Contains("Säuren", vm.Homework.PreviousLessonLabel); + Assert.False(vm.Homework.PreviousHomeworkChecked); + } + + [Fact] + public void Konstruktor_IgnoriertVorherigeStundeOhneHausaufgabe() + { + var group = new LearningGroup { Name = "Q1 Chemie" }; + var today = DateOnly.FromDateTime(DateTime.Today); + var previous = new Lesson { GroupId = group.Id, Date = today.AddDays(-7), Topic = "Säuren" }; + var lesson = new Lesson { GroupId = group.Id, Date = today, Topic = "Redox" }; + var lessons = new FakeLessons(); + lessons.Add(previous); + var seatingPlan = BuildSeatingPlan(group.Id); + + var vm = new TeachingModeViewModel(lesson, group, new FakeAlternativeLessonPaths([]), + lessons, seatingPlan, BuildParticipation(group)); + + Assert.False(vm.Homework.HasPreviousHomework); + } + + [Fact] + public void PreviousHomeworkChecked_SpeichertSofortAnDerVorherigenStunde() + { + var group = new LearningGroup { Name = "Q1 Chemie" }; + var today = DateOnly.FromDateTime(DateTime.Today); + var previous = new Lesson + { GroupId = group.Id, Date = today.AddDays(-7), Topic = "Säuren", Homework = "S. 42" }; + var lesson = new Lesson { GroupId = group.Id, Date = today, Topic = "Redox" }; + var lessons = new FakeLessons(); + lessons.Add(previous); + var seatingPlan = BuildSeatingPlan(group.Id); + var vm = new TeachingModeViewModel(lesson, group, new FakeAlternativeLessonPaths([]), + lessons, seatingPlan, BuildParticipation(group)); + + vm.Homework.PreviousHomeworkChecked = true; + + var saved = Assert.Single(lessons.GetByGroupAndRange(group.Id, today.AddDays(-120), today.AddDays(-1))); + Assert.True(saved.HomeworkChecked); + } + + [Fact] + public void SaveCurrentHomework_SchreibtAufDieAktuelleStunde() + { + var group = new LearningGroup { Name = "Q1 Chemie" }; + var lesson = new Lesson + { GroupId = group.Id, Date = DateOnly.FromDateTime(DateTime.Today), Topic = "Redox" }; + var lessons = new FakeLessons(); + lessons.Add(lesson); + var seatingPlan = BuildSeatingPlan(group.Id); + var vm = new TeachingModeViewModel(lesson, group, new FakeAlternativeLessonPaths([]), + lessons, seatingPlan, BuildParticipation(group)); + + vm.Homework.CurrentHomework = "AB 7 fertigstellen"; + vm.Homework.SaveCurrentHomeworkCommand.Execute(null); + + var saved = Assert.Single(lessons.GetByGroupAndRange(group.Id, lesson.Date, lesson.Date)); + Assert.Equal("AB 7 fertigstellen", saved.Homework); + Assert.Equal("Gespeichert.", vm.Homework.SaveStatus); + } } diff --git a/LehrerApp.Desktop/ViewModels/Groups/SeatingPlanViewModels.cs b/LehrerApp.Desktop/ViewModels/Groups/SeatingPlanViewModels.cs index ac15337..b76ebc6 100644 --- a/LehrerApp.Desktop/ViewModels/Groups/SeatingPlanViewModels.cs +++ b/LehrerApp.Desktop/ViewModels/Groups/SeatingPlanViewModels.cs @@ -236,6 +236,13 @@ public partial class SeatingPlanTabViewModel : ObservableObject NotifyCommands(); } + /// Öffentlich statt intern (kein InternalsVisibleTo in dieser Codebasis, siehe + /// UntisSyncService-Kommentar): der Unterrichtsmodus bewertet über eine zweite, unabhängige + /// ParticipationTabViewModel-Instanz (Schnellbewertungs-Dialoge, siehe TeachingModeWindow) — + /// diese Instanz hier bekommt davon nichts automatisch mit und muss nach jedem Dialog explizit + /// neu aus dem Repository laden, damit die Sitzplatz-Badges nicht veraltet bleiben. + public void ReloadSeatBadgesFromRepository() => RefreshSeatLessonData(); + private void RefreshSeatLessonData() { if (SelectedSession is null) diff --git a/LehrerApp.Desktop/ViewModels/Groups/TeachingModeViewModel.cs b/LehrerApp.Desktop/ViewModels/Groups/TeachingModeViewModel.cs index ba3a9d2..e8de90f 100644 --- a/LehrerApp.Desktop/ViewModels/Groups/TeachingModeViewModel.cs +++ b/LehrerApp.Desktop/ViewModels/Groups/TeachingModeViewModel.cs @@ -1,3 +1,5 @@ +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; using LehrerApp.Core.Interfaces; using LehrerApp.Core.Models; @@ -10,24 +12,104 @@ namespace LehrerApp.Desktop.ViewModels.Groups; /// der "Heute"-Tagesliste heraus gestartet. /// /// Bewusst keine eigene DI-Registrierung als Singleton/Transient: wie LessonViewerViewModel wird -/// diese ViewModel direkt vom Code-Behind konstruiert, das dafür nötige SeatingPlanTabViewModel -/// aber weiterhin über DI aufgelöst (transient, siehe AppBootstrapper) und hier per Konstruktor -/// entgegengenommen statt selbst aus App.Services zu ziehen - ViewModels greifen in diesem Code -/// nicht selbst auf den Service-Container zu, das bleibt Aufgabe des Code-Behind. +/// diese ViewModel direkt vom Code-Behind konstruiert, das dafür nötige SeatingPlanTabViewModel/ +/// ParticipationTabViewModel aber weiterhin über DI aufgelöst (transient, siehe AppBootstrapper) +/// und hier per Konstruktor entgegengenommen statt selbst aus App.Services zu ziehen - +/// ViewModels greifen in diesem Code nicht selbst auf den Service-Container zu, das bleibt +/// Aufgabe des Code-Behind. /// public class TeachingModeViewModel { public string GroupName { get; } public LessonViewerViewModel LessonInfo { get; } public SeatingPlanTabViewModel SeatingPlan { get; } + /// Nutzer-Feedback: die Schnellbewertungs-Dialoge für Mitarbeit/Anwesenheit-Hausaufgabe + /// gab es bislang nur über den Mitarbeit-Tab der Gruppe — im Unterrichtsmodus musste man dafür + /// erst wieder raus. Hier dieselbe wie im Mitarbeit-Tab, + /// aber auf die zu dieser Stunde gehörende Sitzung vorselektiert (siehe Konstruktor), damit die + /// über OnQuickInput/OnStatusQuickInput geöffneten Dialoge (Wiring in + /// TeachingModeWindow.axaml.cs, gleiches Muster wie ParticipationTabView.axaml.cs) direkt auf + /// der richtigen Sitzung starten. + public ParticipationTabViewModel Participation { get; } + public TeachingModeHomeworkViewModel Homework { get; } public TeachingModeViewModel(Lesson lesson, LearningGroup group, - IAlternativeLessonPathRepository alternativePaths, SeatingPlanTabViewModel seatingPlan) + IAlternativeLessonPathRepository alternativePaths, ILessonRepository lessons, + SeatingPlanTabViewModel seatingPlan, ParticipationTabViewModel participation) { GroupName = group.Name; LessonInfo = new LessonViewerViewModel(lesson, alternativePaths); + SeatingPlan = seatingPlan; SeatingPlan.Initialize(group.Id, !group.IsActive); SeatingPlan.SelectOrCreateSessionForLesson(lesson); + + Participation = participation; + Participation.Initialize(group.Id, group.SchoolYear, !group.IsActive); + if (SeatingPlan.SelectedSession is { } linkedSession) + Participation.SelectedSession = + Participation.Sessions.FirstOrDefault(s => s.Id == linkedSession.Id); + + // 4.5.4: dieselbe "letzte Stunde vor dieser"-Suche wie das Stundenplan-Badge + // "Hausaufgabe kontrollieren" (TimetableViewModel.HasUnhandledHomework) - 120 Tage + // Lookback deckt auch längere Ferienpausen ab, ohne unbegrenzt weit zurückzuscannen. + var previousLesson = lessons.GetByGroupAndRange(group.Id, lesson.Date.AddDays(-120), lesson.Date.AddDays(-1)) + .OrderByDescending(l => l.Date).ThenByDescending(l => l.LessonNumber ?? 0) + .FirstOrDefault(); + Homework = new TeachingModeHomeworkViewModel(lesson, previousLesson, lessons); + } +} + +/// +/// Nutzer-Feedback: im Unterrichtsmodus sollte man die Hausaufgabe aus der letzten Stunde als +/// kontrolliert abhaken und die neue Hausaufgabe eintragen/ändern können, ohne dafür den vollen +/// -Editor zu öffnen (der zusätzlich Phasen/Reflexion usw. zeigt) +/// - manchmal ergibt sich die Hausaufgabe erst während der Stunde. Bewusst eine eigene, kleine +/// statt Felder direkt auf , da +/// diese Klasse (wie ) selbst keine Bindable-Basisklasse hat. +/// +public partial class TeachingModeHomeworkViewModel : ObservableObject +{ + private readonly ILessonRepository _lessons; + private readonly Lesson _lesson; + private readonly Lesson? _previousLesson; + + public bool HasPreviousHomework => + _previousLesson is not null && !string.IsNullOrWhiteSpace(_previousLesson.Homework); + public string PreviousHomeworkText => _previousLesson?.Homework ?? ""; + public string PreviousLessonLabel => _previousLesson is null ? "" : + $"{_previousLesson.Date:dd.MM.yyyy}" + (string.IsNullOrWhiteSpace(_previousLesson.Topic) + ? "" : $" · {_previousLesson.Topic}"); + + [ObservableProperty] private bool _previousHomeworkChecked; + [ObservableProperty] private string _currentHomework; + [ObservableProperty] private string _saveStatus = ""; + + public TeachingModeHomeworkViewModel(Lesson lesson, Lesson? previousLesson, ILessonRepository lessons) + { + _lesson = lesson; + _previousLesson = previousLesson; + _lessons = lessons; + _previousHomeworkChecked = previousLesson?.HomeworkChecked ?? false; + _currentHomework = lesson.Homework ?? ""; + } + + /// Sofort beim Umschalten gespeichert (kein separater Speichern-Klick nötig) - dieselbe + /// Checkbox-Semantik wie im LessonDialog (4.5.4): "kontrolliert" und "nicht kontrollieren" + /// schließen sich aus. + partial void OnPreviousHomeworkCheckedChanged(bool value) + { + if (_previousLesson is null) return; + _previousLesson.HomeworkChecked = value; + if (value) _previousLesson.HomeworkCheckDismissed = false; + _lessons.Save(_previousLesson); + } + + [RelayCommand] + private void SaveCurrentHomework() + { + _lesson.Homework = CurrentHomework; + _lessons.Save(_lesson); + SaveStatus = "Gespeichert."; } } diff --git a/LehrerApp.Desktop/Views/Groups/TeachingModeWindow.axaml b/LehrerApp.Desktop/Views/Groups/TeachingModeWindow.axaml index fd7a0d8..1578210 100644 --- a/LehrerApp.Desktop/Views/Groups/TeachingModeWindow.axaml +++ b/LehrerApp.Desktop/Views/Groups/TeachingModeWindow.axaml @@ -27,6 +27,13 @@ + +