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
@@ -109,6 +109,32 @@ public partial class SeatingPlanTabViewModel : ObservableObject
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();
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 Action<SettingsTab>? OnNavigateToSettings { get; set; }
public Func<Lesson, Task>? OnOpenLessonViewer { get; set; }
public Func<Lesson, Task>? 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,
@@ -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"
IsVisible="{Binding HasUnhandledHomework}"/>
</StackPanel>
<Button Grid.Column="3" Content="{Binding OpenButtonLabel}" FontSize="11" Padding="9,4"
VerticalAlignment="Center" IsVisible="{Binding HasGroupId}"
Command="{Binding $parent[ItemsControl].((vm:TimetableViewModel)DataContext).OpenTodayLessonCommand}"
CommandParameter="{Binding}"/>
<StackPanel Grid.Column="3" Orientation="Horizontal" Spacing="6" VerticalAlignment="Center">
<Button Content="▶ Unterricht" FontSize="11" Padding="9,4"
IsVisible="{Binding HasLesson}"
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>
</Border>
</DataTemplate>
@@ -21,9 +21,23 @@ public partial class TimetableView : UserControl
vm.OnEditSlot = ShowSlotDialog;
vm.OnAddSubstitution = ShowSubstitutionDialog;
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)
{
var owner = TopLevel.GetTopLevel(this) as Window;