diff --git a/LehrerApp.Desktop.Tests/SeatingPlanViewModelTests.cs b/LehrerApp.Desktop.Tests/SeatingPlanViewModelTests.cs index e3fcad4..5c9c11a 100644 --- a/LehrerApp.Desktop.Tests/SeatingPlanViewModelTests.cs +++ b/LehrerApp.Desktop.Tests/SeatingPlanViewModelTests.cs @@ -235,6 +235,65 @@ public sealed class SeatingPlanViewModelTests Assert.False(vm.Seats[1].IsOccupied); } + // ── Unterrichtsmodus (14.x): Sitzung mit einer konkreten Stunde verknüpfen ──────────────── + + [Fact] + public void SelectOrCreateSessionForLesson_BereitsVerknuepfteSitzungVorhanden_WaehltSieAus() + { + var groupId = Guid.NewGuid(); + var lesson = new Lesson { GroupId = groupId, Date = DateOnly.FromDateTime(DateTime.Today), Topic = "Redox" }; + var existingSession = new ParticipationSession + { GroupId = groupId, Date = lesson.Date, LessonId = lesson.Id, Comment = "Redox" }; + var sessions = new FakeSessions([existingSession]); + var plan = new SeatingPlan { GroupId = groupId, Name = "Standard", Rows = 1, Columns = 1 }; + var vm = new SeatingPlanTabViewModel(new FakeSeatingPlans([plan]), new FakeStudents([]), + new FakeMemberships([]), sessions, new FakeEntries(), new FakeAspects()); + vm.Initialize(groupId, isReadOnly: false); + + vm.SelectOrCreateSessionForLesson(lesson); + + Assert.Equal(existingSession.Id, vm.SelectedSession?.Id); + Assert.Single(sessions.GetByGroup(groupId)); + } + + [Fact] + public void SelectOrCreateSessionForLesson_KeineSitzungVorhanden_LegtVerknuepfteAnUndWaehltSieAus() + { + var groupId = Guid.NewGuid(); + var lesson = new Lesson + { GroupId = groupId, Date = DateOnly.FromDateTime(DateTime.Today), Topic = "Redox" }; + var sessions = new FakeSessions([]); + var plan = new SeatingPlan { GroupId = groupId, Name = "Standard", Rows = 1, Columns = 1 }; + var vm = new SeatingPlanTabViewModel(new FakeSeatingPlans([plan]), new FakeStudents([]), + new FakeMemberships([]), sessions, new FakeEntries(), new FakeAspects()); + vm.Initialize(groupId, isReadOnly: false); + + vm.SelectOrCreateSessionForLesson(lesson); + + var created = Assert.Single(sessions.GetByGroup(groupId)); + Assert.Equal(lesson.Id, created.LessonId); + Assert.Equal(lesson.Topic, created.Comment); + Assert.Equal(vm.SelectedSession?.Id, created.Id); + } + + [Fact] + public void SelectOrCreateSessionForLesson_ArchivierteGruppe_LegtKeineSitzungAn() + { + var groupId = Guid.NewGuid(); + var lesson = new Lesson + { GroupId = groupId, Date = DateOnly.FromDateTime(DateTime.Today), Topic = "Redox" }; + var sessions = new FakeSessions([]); + var plan = new SeatingPlan { GroupId = groupId, Name = "Standard", Rows = 1, Columns = 1 }; + var vm = new SeatingPlanTabViewModel(new FakeSeatingPlans([plan]), new FakeStudents([]), + new FakeMemberships([]), sessions, new FakeEntries(), new FakeAspects()); + vm.Initialize(groupId, isReadOnly: true); + + vm.SelectOrCreateSessionForLesson(lesson); + + Assert.Empty(sessions.GetByGroup(groupId)); + Assert.Null(vm.SelectedSession); + } + [Fact] public void ShowSeatUndCanToggleHidden_HaengenVonBearbeitungsmodusUndBelegungAb() { diff --git a/LehrerApp.Desktop.Tests/TeachingModeViewModelTests.cs b/LehrerApp.Desktop.Tests/TeachingModeViewModelTests.cs new file mode 100644 index 0000000..a3336ad --- /dev/null +++ b/LehrerApp.Desktop.Tests/TeachingModeViewModelTests.cs @@ -0,0 +1,82 @@ +using LehrerApp.Core.Models; +using LehrerApp.Desktop.ViewModels.Groups; +using Xunit; + +namespace LehrerApp.Desktop.Tests; + +public sealed class TeachingModeViewModelTests +{ + private static SeatingPlanTabViewModel BuildSeatingPlan(Guid groupId, FakeSessions? sessions = null, + List? students = null, List? memberships = null, + List? plans = null) => + new(new FakeSeatingPlans(plans ?? [new SeatingPlan { GroupId = groupId, Name = "Standard", Rows = 1, Columns = 1 }]), + new FakeStudents(students ?? []), new FakeMemberships(memberships ?? []), + sessions ?? new FakeSessions([]), new FakeEntries(), new FakeAspects()); + + [Fact] + public void Konstruktor_LaedtVerlaufsplanUndInitialisiertSitzplanFuerDieGruppe() + { + var group = new LearningGroup { Name = "Q1 Chemie" }; + var lesson = new Lesson + { + GroupId = group.Id, Date = DateOnly.FromDateTime(DateTime.Today), Topic = "Redox", + Phases = [new LessonPhaseStep { Name = "Einstieg", DurationMinutes = 10, Activity = "Impuls" }], + }; + var seatingPlan = BuildSeatingPlan(group.Id); + + var vm = new TeachingModeViewModel(lesson, group, new FakeAlternativeLessonPaths([]), seatingPlan); + + Assert.Equal("Q1 Chemie", vm.GroupName); + Assert.Equal("Redox", vm.LessonInfo.Topic); + Assert.Single(vm.LessonInfo.PhaseGroups.Single().Phases); + Assert.True(vm.SeatingPlan.HasSelectedPlan); + } + + [Fact] + public void Konstruktor_VerknuepftDieSitzungMitDieserStunde() + { + var group = new LearningGroup { Name = "Q1 Chemie" }; + var lesson = new Lesson + { GroupId = group.Id, Date = DateOnly.FromDateTime(DateTime.Today), Topic = "Redox" }; + var sessions = new FakeSessions([]); + var seatingPlan = BuildSeatingPlan(group.Id, sessions); + + var vm = new TeachingModeViewModel(lesson, group, new FakeAlternativeLessonPaths([]), seatingPlan); + + var created = Assert.Single(sessions.GetByGroup(group.Id)); + Assert.Equal(lesson.Id, created.LessonId); + Assert.Equal(vm.SeatingPlan.SelectedSession?.Id, created.Id); + } + + [Fact] + public void Konstruktor_BereitsExistierendeVerknuepfteSitzung_WirdWiederverwendet() + { + var group = new LearningGroup { Name = "Q1 Chemie" }; + var lesson = new Lesson + { GroupId = group.Id, Date = DateOnly.FromDateTime(DateTime.Today), Topic = "Redox" }; + var existing = new ParticipationSession + { GroupId = group.Id, Date = lesson.Date, LessonId = lesson.Id, Comment = "Redox" }; + var sessions = new FakeSessions([existing]); + var seatingPlan = BuildSeatingPlan(group.Id, sessions); + + var vm = new TeachingModeViewModel(lesson, group, new FakeAlternativeLessonPaths([]), seatingPlan); + + Assert.Single(sessions.GetByGroup(group.Id)); + Assert.Equal(existing.Id, vm.SeatingPlan.SelectedSession?.Id); + } + + /// Eine archivierte Gruppe darf im Unterrichtsmodus nicht bearbeitbar sein - genau wie in der + /// normalen Gruppenansicht (GroupDetailViewModel.IsReadOnly). + [Fact] + public void Konstruktor_ArchivierteGruppe_InitialisiertSitzplanSchreibgeschuetzt() + { + var group = new LearningGroup { Name = "Q1 Chemie", IsActive = false }; + var lesson = new Lesson + { GroupId = group.Id, Date = DateOnly.FromDateTime(DateTime.Today), Topic = "Redox" }; + var seatingPlan = BuildSeatingPlan(group.Id); + + var vm = new TeachingModeViewModel(lesson, group, new FakeAlternativeLessonPaths([]), seatingPlan); + + Assert.False(vm.SeatingPlan.IsEditable); + } +} diff --git a/LehrerApp.Desktop.Tests/TimetableViewModelTests.cs b/LehrerApp.Desktop.Tests/TimetableViewModelTests.cs index 697355b..c3a9545 100644 --- a/LehrerApp.Desktop.Tests/TimetableViewModelTests.cs +++ b/LehrerApp.Desktop.Tests/TimetableViewModelTests.cs @@ -275,6 +275,44 @@ public sealed class TimetableViewModelTests Assert.Equal("Zur Lerngruppe", vm.TodayItems[0].OpenButtonLabel); } + // ── Unterrichtsmodus (14.x) ──────────────────────────────────────────────── + + [Fact] + public async Task StartTeachingMode_MitVorhandenerLesson_OeffnetDenUnterrichtsmodus() + { + var today = DateOnly.FromDateTime(DateTime.Today); + var group = new LearningGroup { Name = "Q1 Chemie" }; + var slots = new FakeTimetableSlots(); + slots.Add(new TimetableSlot { GroupId = group.Id, Weekday = today.DayOfWeek, PeriodNumber = 1 }); + var lesson = new Lesson { GroupId = group.Id, Date = today, LessonNumber = 1, Topic = "Redox" }; + var lessons = new FakeLessons(); + lessons.Add(lesson); + var vm = BuildViewModel(slots, new FakeGroups([group]), lessons: lessons); + Lesson? openedLesson = null; + vm.OnOpenTeachingMode = l => { openedLesson = l; return Task.CompletedTask; }; + + await vm.StartTeachingModeCommand.ExecuteAsync(vm.TodayItems[0]); + + Assert.Equal(lesson.Id, openedLesson?.Id); + } + + [Fact] + public async Task StartTeachingMode_OhneVorhandeneLesson_TutNichts() + { + var today = DateOnly.FromDateTime(DateTime.Today); + var group = new LearningGroup { Name = "Q1 Chemie" }; + var slots = new FakeTimetableSlots(); + slots.Add(new TimetableSlot { GroupId = group.Id, Weekday = today.DayOfWeek, PeriodNumber = 1 }); + var vm = BuildViewModel(slots, new FakeGroups([group])); + var opened = false; + vm.OnOpenTeachingMode = _ => { opened = true; return Task.CompletedTask; }; + + await vm.StartTeachingModeCommand.ExecuteAsync(vm.TodayItems[0]); + + Assert.False(opened); + Assert.False(vm.TodayItems[0].HasLesson); + } + [Fact] public async Task OpenWeekCell_MitVorhandenerLesson_OeffnetDenViewer() { diff --git a/LehrerApp.Desktop/ViewModels/Groups/SeatingPlanViewModels.cs b/LehrerApp.Desktop/ViewModels/Groups/SeatingPlanViewModels.cs index 8f5493b..7d7652c 100644 --- a/LehrerApp.Desktop/ViewModels/Groups/SeatingPlanViewModels.cs +++ b/LehrerApp.Desktop/ViewModels/Groups/SeatingPlanViewModels.cs @@ -109,6 +109,32 @@ public partial class SeatingPlanTabViewModel : ObservableObject return option; } + /// + /// Für den Unterrichtsmodus (14.x): wählt eine bereits mit dieser Stunde verknüpfte Sitzung + /// aus (angelegt z.B. über PlanningTabViewModel.CreateParticipationSession, 3.3.1) oder legt + /// bei Bedarf eine neue, EXPLIZIT verknüpfte an (). + /// Anders als (anonyme "Sitzplan"-Sitzung, erst bei der + /// ersten tatsächlichen Aktion) ist das hier bewusst sofort beim Start des Unterrichtsmodus + /// erlaubt: welche Stunde gemeint ist, steht durch die explizite Auswahl der Lehrkraft + /// (Klick auf "Unterrichtsmodus starten" für genau diese Stunde) bereits unzweideutig fest - + /// keine Geistersitzungs-Gefahr wie beim bloßen Öffnen eines Tabs. + /// + public void SelectOrCreateSessionForLesson(Lesson lesson) + { + var existing = TodaySessions.FirstOrDefault(s => s.LessonId == lesson.Id); + if (existing is not null) { SelectedSession = existing; return; } + if (!IsEditable) return; + + var created = new ParticipationSession + { + GroupId = _groupId, Date = lesson.Date, LessonId = lesson.Id, Comment = lesson.Topic, + }; + _sessions.Save(created); + var option = new ParticipationSessionOption(created); + TodaySessions.Add(option); + SelectedSession = option; + } + partial void OnSelectedSessionChanged(ParticipationSessionOption? value) => RefreshSeatLessonData(); private void LoadStudentOptions() diff --git a/LehrerApp.Desktop/ViewModels/Groups/TeachingModeViewModel.cs b/LehrerApp.Desktop/ViewModels/Groups/TeachingModeViewModel.cs new file mode 100644 index 0000000..ba3a9d2 --- /dev/null +++ b/LehrerApp.Desktop/ViewModels/Groups/TeachingModeViewModel.cs @@ -0,0 +1,33 @@ +using LehrerApp.Core.Interfaces; +using LehrerApp.Core.Models; + +namespace LehrerApp.Desktop.ViewModels.Groups; + +/// +/// Unterrichtsmodus (14.x): konsolidierte Ansicht für eine konkrete, heute stattfindende Stunde - +/// Verlaufsplan und Sitzplan mit Schnellbewertung auf einem Bildschirm, statt zwischen den Tabs +/// der Gruppe hin- und herzuspringen. Wird über TimetableViewModel.StartTeachingModeCommand aus +/// 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. +/// +public class TeachingModeViewModel +{ + public string GroupName { get; } + public LessonViewerViewModel LessonInfo { get; } + public SeatingPlanTabViewModel SeatingPlan { get; } + + public TeachingModeViewModel(Lesson lesson, LearningGroup group, + IAlternativeLessonPathRepository alternativePaths, SeatingPlanTabViewModel seatingPlan) + { + GroupName = group.Name; + LessonInfo = new LessonViewerViewModel(lesson, alternativePaths); + SeatingPlan = seatingPlan; + SeatingPlan.Initialize(group.Id, !group.IsActive); + SeatingPlan.SelectOrCreateSessionForLesson(lesson); + } +} diff --git a/LehrerApp.Desktop/ViewModels/Planning/TimetableViewModel.cs b/LehrerApp.Desktop/ViewModels/Planning/TimetableViewModel.cs index e5ab89f..fe3bf26 100644 --- a/LehrerApp.Desktop/ViewModels/Planning/TimetableViewModel.cs +++ b/LehrerApp.Desktop/ViewModels/Planning/TimetableViewModel.cs @@ -91,6 +91,7 @@ public partial class TimetableViewModel : ObservableObject public Func? OnAddSubstitution { get; set; } public Action? OnNavigateToSettings { get; set; } public Func? OnOpenLessonViewer { get; set; } + public Func? OnOpenTeachingMode { get; set; } public TimetableViewModel(ITimetableSlotRepository slots, IGroupRepository groups, ISubjectRepository subjects, ILessonRepository lessons, IExamRepository exams, @@ -259,6 +260,16 @@ public partial class TimetableViewModel : ObservableObject else OnNavigateToGroup?.Invoke(item.GroupId); } + /// Startet den Unterrichtsmodus (14.x) für eine konkrete, heute stattfindende Stunde — + /// bewusst nur aus der "Heute"-Tagesliste heraus erreichbar (nicht dem Wochenraster), da der + /// Modus für das aktive Unterrichten HEUTE gedacht ist, nicht zum Durchblättern anderer Tage. + [RelayCommand] + private async Task StartTeachingMode(TodayLessonItem? item) + { + if (item?.Lesson is not { } lesson || OnOpenTeachingMode is null) return; + await OnOpenTeachingMode(lesson); + } + [RelayCommand] private async Task OpenWeekCell(WeekCellItem? item) { @@ -784,8 +795,9 @@ public class TodayLessonItem public bool HasGroupId => GroupId != Guid.Empty; public bool HasUnhandledHomework { get; private init; } /// Nur gesetzt, wenn für diesen Slot/Tag bereits eine Lesson existiert — Grundlage für den - /// Direktsprung in den Verlaufsplan-Viewer (4.5.2). + /// Direktsprung in den Verlaufsplan-Viewer (4.5.2) und den Unterrichtsmodus (14.x). public Lesson? Lesson { get; private init; } + public bool HasLesson => Lesson is not null; public string OpenButtonLabel => Lesson is not null ? "Verlaufsplan ansehen" : "Zur Lerngruppe"; public TodayLessonItem(Guid groupId, int periodNumber, string groupName, string room, diff --git a/LehrerApp.Desktop/Views/Groups/TeachingModeWindow.axaml b/LehrerApp.Desktop/Views/Groups/TeachingModeWindow.axaml new file mode 100644 index 0000000..fd7a0d8 --- /dev/null +++ b/LehrerApp.Desktop/Views/Groups/TeachingModeWindow.axaml @@ -0,0 +1,99 @@ + + + + + + + + + + + + + + + + + + + + + +