feat: Unterrichtsmodus - konsolidierte Ansicht für die laufende Stunde

Verlaufsplan und Sitzplan bisher über mehrere Tabs verteilt, obwohl im Unterricht selbst beides
gleichzeitig gebraucht wird. Neuer Button "▶ Unterricht" in der "Heute"-Tagesliste des
Stundenplans (nur bei vorhandener Lesson) öffnet ein maximiertes Fenster: links der
schreibgeschützte Verlaufsplan als kompakte Kartenliste, rechts der volle, unverändert
wiederverwendete SeatingPlanTabView (Drag&Drop, Schnellbewertung, Situations-Tags, PDF-Export).
TeachingModeViewModel baut auf dem bestehenden LessonViewerViewModel auf, dadurch kommen "Zur
Mitarbeit"/"Zu den Noten" ohne Duplizierung mit.

Neue SeatingPlanTabViewModel.SelectOrCreateSessionForLesson verknüpft die Mitarbeitssitzung
automatisch mit der konkreten Stunde - anders als die bestehende EnsureTodaySession darf das hier
sofort beim Start passieren, da durch den expliziten Klick für genau diese Stunde eindeutig
feststeht, worum es geht (keine Geistersitzungs-Gefahr wie beim bloßen Tab-Öffnen).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-20 14:53:16 +02:00
co-authored by Claude Sonnet 5
parent 7365947edc
commit d9d007cd4b
11 changed files with 437 additions and 7 deletions
@@ -235,6 +235,65 @@ public sealed class SeatingPlanViewModelTests
Assert.False(vm.Seats[1].IsOccupied); 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] [Fact]
public void ShowSeatUndCanToggleHidden_HaengenVonBearbeitungsmodusUndBelegungAb() public void ShowSeatUndCanToggleHidden_HaengenVonBearbeitungsmodusUndBelegungAb()
{ {
@@ -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<Student>? students = null, List<GroupMembership>? memberships = null,
List<SeatingPlan>? 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);
}
}
@@ -275,6 +275,44 @@ public sealed class TimetableViewModelTests
Assert.Equal("Zur Lerngruppe", vm.TodayItems[0].OpenButtonLabel); 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] [Fact]
public async Task OpenWeekCell_MitVorhandenerLesson_OeffnetDenViewer() public async Task OpenWeekCell_MitVorhandenerLesson_OeffnetDenViewer()
{ {
@@ -109,6 +109,32 @@ public partial class SeatingPlanTabViewModel : ObservableObject
return option; return option;
} }
/// <summary>
/// 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 (<see cref="ParticipationSession.LessonId"/>).
/// Anders als <see cref="EnsureTodaySession"/> (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.
/// </summary>
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(); partial void OnSelectedSessionChanged(ParticipationSessionOption? value) => RefreshSeatLessonData();
private void LoadStudentOptions() private void LoadStudentOptions()
@@ -0,0 +1,33 @@
using LehrerApp.Core.Interfaces;
using LehrerApp.Core.Models;
namespace LehrerApp.Desktop.ViewModels.Groups;
/// <summary>
/// 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.
/// </summary>
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);
}
}
@@ -91,6 +91,7 @@ public partial class TimetableViewModel : ObservableObject
public Func<Task>? OnAddSubstitution { get; set; } public Func<Task>? OnAddSubstitution { get; set; }
public Action<SettingsTab>? OnNavigateToSettings { get; set; } public Action<SettingsTab>? OnNavigateToSettings { get; set; }
public Func<Lesson, Task>? OnOpenLessonViewer { get; set; } public Func<Lesson, Task>? OnOpenLessonViewer { get; set; }
public Func<Lesson, Task>? OnOpenTeachingMode { get; set; }
public TimetableViewModel(ITimetableSlotRepository slots, IGroupRepository groups, public TimetableViewModel(ITimetableSlotRepository slots, IGroupRepository groups,
ISubjectRepository subjects, ILessonRepository lessons, IExamRepository exams, ISubjectRepository subjects, ILessonRepository lessons, IExamRepository exams,
@@ -259,6 +260,16 @@ public partial class TimetableViewModel : ObservableObject
else OnNavigateToGroup?.Invoke(item.GroupId); 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] [RelayCommand]
private async Task OpenWeekCell(WeekCellItem? item) private async Task OpenWeekCell(WeekCellItem? item)
{ {
@@ -784,8 +795,9 @@ public class TodayLessonItem
public bool HasGroupId => GroupId != Guid.Empty; public bool HasGroupId => GroupId != Guid.Empty;
public bool HasUnhandledHomework { get; private init; } public bool HasUnhandledHomework { get; private init; }
/// Nur gesetzt, wenn für diesen Slot/Tag bereits eine Lesson existiert — Grundlage für den /// 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 Lesson? Lesson { get; private init; }
public bool HasLesson => Lesson is not null;
public string OpenButtonLabel => Lesson is not null ? "Verlaufsplan ansehen" : "Zur Lerngruppe"; public string OpenButtonLabel => Lesson is not null ? "Verlaufsplan ansehen" : "Zur Lerngruppe";
public TodayLessonItem(Guid groupId, int periodNumber, string groupName, string room, public TodayLessonItem(Guid groupId, int periodNumber, string groupName, string room,
@@ -0,0 +1,99 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="clr-namespace:LehrerApp.Desktop.ViewModels.Groups"
xmlns:views="clr-namespace:LehrerApp.Desktop.Views.Groups"
x:Class="LehrerApp.Desktop.Views.Groups.TeachingModeWindow"
x:DataType="vm:TeachingModeViewModel"
Title="{Binding LessonInfo.Topic, StringFormat='Unterrichtsmodus · {0}'}"
Width="1400" Height="850" MinWidth="1000" MinHeight="600"
WindowState="Maximized" CanResize="True" WindowStartupLocation="CenterScreen">
<Grid RowDefinitions="Auto,*" Margin="20">
<Grid Grid.Row="0" ColumnDefinitions="*,Auto" Margin="0,0,0,14">
<StackPanel Grid.Column="0" Spacing="3">
<TextBlock FontSize="20" FontWeight="SemiBold">
<Run Text="{Binding GroupName}"/><Run Text=" · "/><Run Text="{Binding LessonInfo.Topic}"/>
</TextBlock>
<StackPanel Orientation="Horizontal" Spacing="16">
<TextBlock FontSize="12" Opacity="0.65">
<Run Text="Datum: "/><Run Text="{Binding LessonInfo.DateDisplay}"/>
</TextBlock>
<TextBlock FontSize="12" Opacity="0.65">
<Run Text="Beginn: "/><Run Text="{Binding LessonInfo.StartTimeDisplay}"/>
</TextBlock>
<TextBlock FontSize="12" Opacity="0.65">
<Run Text="Status: "/><Run Text="{Binding LessonInfo.StatusLabel}"/>
</TextBlock>
</StackPanel>
</StackPanel>
<StackPanel Grid.Column="1" Orientation="Horizontal" Spacing="8" VerticalAlignment="Center">
<Button Content="Zur Mitarbeit" Command="{Binding LessonInfo.NavigateToParticipationCommand}"
ToolTip.Tip="Schließt den Unterrichtsmodus und springt zum Tab 'Mitarbeit' der Lerngruppe."/>
<Button Content="Zu den Noten" Command="{Binding LessonInfo.NavigateToGradesCommand}"/>
<Button Content="Unterrichtsmodus beenden" Click="OnClose" Margin="16,0,0,0"/>
</StackPanel>
</Grid>
<Grid Grid.Row="1" ColumnDefinitions="360,16,*">
<Border Grid.Column="0" BorderBrush="{DynamicResource SystemControlForegroundBaseLowBrush}"
BorderThickness="0,0,1,0" Padding="0,0,16,0">
<ScrollViewer>
<StackPanel Spacing="14">
<TextBlock Text="Verlaufsplan" FontSize="14" FontWeight="SemiBold"/>
<ItemsControl ItemsSource="{Binding LessonInfo.PhaseGroups}">
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="vm:PhaseGroupViewItem">
<StackPanel Margin="0,0,0,10">
<StackPanel IsVisible="{Binding $parent[ItemsControl].((vm:LessonViewerViewModel)DataContext).HasAlternatives}">
<TextBlock Text="{Binding Label}" FontSize="12" FontWeight="SemiBold" Opacity="0.75" Margin="0,6,0,2"/>
<TextBlock Text="{Binding Description}" FontSize="11" Opacity="0.55" TextWrapping="Wrap" Margin="0,0,0,6"
IsVisible="{Binding Description, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
</StackPanel>
<ItemsControl ItemsSource="{Binding Phases}">
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="vm:PhaseViewItem">
<Border Background="{DynamicResource SystemControlBackgroundAltHighBrush}"
CornerRadius="6" Padding="10,8" Margin="0,0,0,6">
<StackPanel Spacing="2">
<Grid ColumnDefinitions="*,Auto">
<TextBlock Grid.Column="0" Text="{Binding Name}" FontSize="13" FontWeight="SemiBold"
TextWrapping="Wrap"/>
<TextBlock Grid.Column="1" FontSize="11" Opacity="0.6">
<Run Text="{Binding TimeDisplay}"/><Run Text=" · "/>
<Run Text="{Binding DurationMinutes, StringFormat='{}{0} Min.'}"/>
</TextBlock>
</Grid>
<TextBlock Text="{Binding Activity}" FontSize="12" TextWrapping="Wrap"
IsVisible="{Binding Activity, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
<TextBlock FontSize="11" Opacity="0.55" TextWrapping="Wrap"
IsVisible="{Binding Material, Converter={x:Static StringConverters.IsNotNullOrEmpty}}">
<Run Text="Material: "/><Run Text="{Binding Material}"/>
</TextBlock>
</StackPanel>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<StackPanel Spacing="4" IsVisible="{Binding LessonInfo.Homework, Converter={x:Static StringConverters.IsNotNullOrEmpty}}">
<TextBlock Text="Hausaufgabe" FontSize="12" FontWeight="SemiBold" Opacity="0.6"/>
<TextBlock Text="{Binding LessonInfo.Homework}" FontSize="13" TextWrapping="Wrap"/>
</StackPanel>
<StackPanel Spacing="4" IsVisible="{Binding LessonInfo.Reflection, Converter={x:Static StringConverters.IsNotNullOrEmpty}}">
<TextBlock Text="Reflexion" FontSize="12" FontWeight="SemiBold" Opacity="0.6"/>
<TextBlock Text="{Binding LessonInfo.Reflection}" FontSize="13" TextWrapping="Wrap"/>
</StackPanel>
</StackPanel>
</ScrollViewer>
</Border>
<views:SeatingPlanTabView Grid.Column="2" DataContext="{Binding SeatingPlan}"/>
</Grid>
</Grid>
</Window>
@@ -0,0 +1,28 @@
using Avalonia.Controls;
using Avalonia.Interactivity;
using LehrerApp.Desktop.ViewModels;
using LehrerApp.Desktop.ViewModels.Groups;
using Microsoft.Extensions.DependencyInjection;
namespace LehrerApp.Desktop.Views.Groups;
public partial class TeachingModeWindow : Window
{
public TeachingModeWindow() => InitializeComponent();
protected override void OnDataContextChanged(EventArgs e)
{
base.OnDataContextChanged(e);
// Gleiches Muster wie LessonViewerDialog (4.5.3): Fenster schließt zuerst, dann Sprung
// über die MainWindowViewModel-Singleton-Navigation in den Ziel-Tab der Gruppe.
if (DataContext is TeachingModeViewModel vm)
vm.LessonInfo.OnNavigateToTab = tabIndex =>
{
Close();
App.Services.GetRequiredService<MainWindowViewModel>()
.NavigateToGroupDetail(vm.LessonInfo.GroupId, tabIndex);
};
}
private void OnClose(object? sender, RoutedEventArgs e) => Close();
}
@@ -100,10 +100,17 @@
Foreground="#8E6C00" FontWeight="SemiBold" Foreground="#8E6C00" FontWeight="SemiBold"
IsVisible="{Binding HasUnhandledHomework}"/> IsVisible="{Binding HasUnhandledHomework}"/>
</StackPanel> </StackPanel>
<Button Grid.Column="3" Content="{Binding OpenButtonLabel}" FontSize="11" Padding="9,4" <StackPanel Grid.Column="3" Orientation="Horizontal" Spacing="6" VerticalAlignment="Center">
VerticalAlignment="Center" IsVisible="{Binding HasGroupId}" <Button Content="▶ Unterricht" FontSize="11" Padding="9,4"
Command="{Binding $parent[ItemsControl].((vm:TimetableViewModel)DataContext).OpenTodayLessonCommand}" IsVisible="{Binding HasLesson}"
CommandParameter="{Binding}"/> ToolTip.Tip="Unterrichtsmodus: Verlaufsplan und Sitzplan mit Schnellbewertung auf einem Bildschirm."
Command="{Binding $parent[ItemsControl].((vm:TimetableViewModel)DataContext).StartTeachingModeCommand}"
CommandParameter="{Binding}"/>
<Button Content="{Binding OpenButtonLabel}" FontSize="11" Padding="9,4"
IsVisible="{Binding HasGroupId}"
Command="{Binding $parent[ItemsControl].((vm:TimetableViewModel)DataContext).OpenTodayLessonCommand}"
CommandParameter="{Binding}"/>
</StackPanel>
</Grid> </Grid>
</Border> </Border>
</DataTemplate> </DataTemplate>
@@ -21,9 +21,23 @@ public partial class TimetableView : UserControl
vm.OnEditSlot = ShowSlotDialog; vm.OnEditSlot = ShowSlotDialog;
vm.OnAddSubstitution = ShowSubstitutionDialog; vm.OnAddSubstitution = ShowSubstitutionDialog;
vm.OnOpenLessonViewer = ShowLessonViewerDialog; vm.OnOpenLessonViewer = ShowLessonViewerDialog;
vm.OnOpenTeachingMode = ShowTeachingMode;
} }
} }
private async Task ShowTeachingMode(Lesson lesson)
{
var owner = TopLevel.GetTopLevel(this) as Window;
var group = App.Services.GetRequiredService<IGroupRepository>().GetById(lesson.GroupId);
if (owner is null || group is null) return;
var teachingModeVm = new TeachingModeViewModel(lesson, group,
App.Services.GetRequiredService<IAlternativeLessonPathRepository>(),
App.Services.GetRequiredService<SeatingPlanTabViewModel>());
var window = new TeachingModeWindow { DataContext = teachingModeVm };
await window.ShowDialog(owner);
}
private async Task ShowLessonViewerDialog(Lesson lesson) private async Task ShowLessonViewerDialog(Lesson lesson)
{ {
var owner = TopLevel.GetTopLevel(this) as Window; var owner = TopLevel.GetTopLevel(this) as Window;
+34 -2
View File
@@ -1041,10 +1041,42 @@ folgenden Punkte gehören direkt in `LehrerApp.Desktop`:
angewendeten KI-Änderung automatisch (`LessonDialogViewModel.MarkAppliedExternally`, lädt den angewendeten KI-Änderung automatisch (`LessonDialogViewModel.MarkAppliedExternally`, lädt den
frischen Stand statt der eigenen Felder), die aufrufende Liste aktualisiert sich wie nach frischen Stand statt der eigenen Felder), die aufrufende Liste aktualisiert sich wie nach
einem normalen Speichern. einem normalen Speichern.
- [x] **4.5.23** Unterrichtsmodus: eine konsolidierte Ansicht für die gerade laufende Stunde
(Nutzer-Idee — im Unterricht selbst bisher zu viele Klicks zwischen Verlaufsplan-Viewer,
Sitzplan-Tab und Mitarbeit-Tab einer Gruppe verteilt).
--- **Umsetzung:** Neuer Button "▶ Unterricht" in der "Heute"-Tagesliste des Stundenplans
(`TimetableView.axaml`, nur sichtbar wenn für den Slot bereits eine `Lesson` existiert —
bewusst NICHT im Wochenraster, der Modus ist für das aktive Unterrichten HEUTE gedacht,
nicht zum Durchblättern anderer Tage) öffnet ein neues, maximiertes Fenster
`TeachingModeWindow`: links der schreibgeschützte Verlaufsplan (Phasen, Hausaufgabe,
Reflexion — kompakte Kartenliste statt der breiten Tabellenansicht des bestehenden
`LessonViewerDialog`, da der Platz hier schmaler ist), rechts der volle, unverändert
wiederverwendete `SeatingPlanTabView` (Drag&Drop, Schnellbewertung per Sitzplatz-Klick,
Situations-Tags, Sitzplan-PDF-Export — alles bereits vorhanden, keine Duplizierung). Die
beiden bereits bestehenden Sprungziele aus dem Verlaufsplan-Viewer ("Zur Mitarbeit"/
"Zu den Noten", 4.5.3) sind identisch mitgekommen, da `TeachingModeViewModel` das
bestehende `LessonViewerViewModel` unverändert für die linke Spalte wiederverwendet statt
die Phasenanzeige zu duplizieren.
## 5. Schülerdokumentation **Sitzung automatisch mit der Stunde verknüpft:** Neue Methode
`SeatingPlanTabViewModel.SelectOrCreateSessionForLesson(Lesson)` — anders als die
bestehende `EnsureTodaySession()` (anonyme "Sitzplan"-Sitzung, erst bei der ersten
tatsächlichen Aktion, um das Geistersitzungs-Problem aus 14.12/7.1.5-Nachtrag zu vermeiden)
darf hier sofort beim Start des Unterrichtsmodus eine mit `Lesson.Id` verknüpfte Sitzung
angelegt werden: welche Stunde gemeint ist, steht durch den expliziten Klick auf
"▶ Unterricht" für genau diese Stunde bereits unzweideutig fest, keine passive
Tab-Öffnung wie beim ursprünglichen Bug. Existiert bereits eine verknüpfte Sitzung (z.B.
über "Sitzung aus der Stunde erstellen", 3.3.1, aus der Planung heraus angelegt), wird
diese wiederverwendet statt einer zweiten.
**Bewusst zurückgestellt:** kein Entry-Point aus dem Dashboard (dessen "Heute"-Karte
navigiert bisher direkt zum Mitarbeit-Tab, ein anderer Anzeigetyp als
`TodayLessonItem`) — der Stundenplan-Tab ist der naheliegendere, bereits etablierte Ort für
"heutige Stunden mit Lesson-Bezug" (4.5.2 nutzt denselben Ort). Kein eigenes
Vollbild-/Kiosk-Fenster (kein `WindowState.FullScreen`, kein Precedent dafür im Code) —
stattdessen ein maximiertes, normal schließbares Fenster, um kein neues, ungetestetes
UI-Verhalten einzuführen.
**Wichtige Abweichung von der ursprünglichen Planung (5.2):** Vor der Umsetzung zeigte sich, **Wichtige Abweichung von der ursprünglichen Planung (5.2):** Vor der Umsetzung zeigte sich,
dass 5.2 wie ursprünglich beschrieben eine zweite, parallele Fehlzeiten-Erfassung neben dem dass 5.2 wie ursprünglich beschrieben eine zweite, parallele Fehlzeiten-Erfassung neben dem