Stundenverlaufsplan: Viewer und Alternativpfad-Katalog (Kapitel 4.2 Nachtrag)

Schreibgeschützter LessonViewerDialog (größere Schrift, ohne Bearbeitungs-/Verlängern-/
Verschieben-Funktion) für den Einsatz während des Unterrichtens, erreichbar über "Anzeigen"
im Stunden-Toolbar.

Alternative Unterrichtsabläufe (z.B. Kurzversion bei Zeitmangel) laufen jetzt über einen
echten Katalog (neues Modell AlternativeLessonPath: Name + Beschreibung) statt Freitext direkt
an der Phase: im Verlaufsplan-Editor eine kompakte, farbig unterstützte Checkbox statt einer
durchgehend sichtbaren Eingabespalte, Zuordnung/Neuanlage über einen eigenen Dialog. Der Viewer
gruppiert Phasen entsprechend und zeigt die hinterlegte Beschreibung. Schema-Migration v3→v4
führt bestehende Freitextwerte verlustfrei in Katalogeinträge über.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-14 01:53:12 +02:00
co-authored by Claude Sonnet 5
parent de6ea001e7
commit ee1a47641e
22 changed files with 904 additions and 33 deletions
@@ -0,0 +1,57 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="clr-namespace:LehrerApp.Desktop.ViewModels.Groups"
xmlns:models="clr-namespace:LehrerApp.Core.Models;assembly=LehrerApp.Core"
x:Class="LehrerApp.Desktop.Views.Groups.AlternativePathDialog"
x:DataType="vm:AlternativePathDialogViewModel"
Title="Alternativer Ablauf"
Width="440" Height="500" MinWidth="380" MinHeight="380"
CanResize="True" WindowStartupLocation="CenterOwner">
<Grid RowDefinitions="*,Auto" Margin="24">
<ScrollViewer Grid.Row="0">
<StackPanel Spacing="14">
<TextBlock Text="Alternativer Ablauf" Classes="dialogtitle"/>
<TextBlock Text="Diese Phase gehört zu einem alternativen Unterrichtsverlauf, z.B. einer Kurzversion bei Zeitmangel."
FontSize="12" Opacity="0.6" TextWrapping="Wrap"/>
<StackPanel Spacing="4">
<TextBlock Text="Vorhandene Abläufe" FontSize="12" Opacity="0.7"/>
<ComboBox ItemsSource="{Binding Available}" SelectedItem="{Binding SelectedPath}"
HorizontalAlignment="Stretch" PlaceholderText="Ablauf auswählen">
<ComboBox.ItemTemplate>
<DataTemplate x:DataType="models:AlternativeLessonPath">
<StackPanel Margin="0,2">
<TextBlock Text="{Binding Name}"/>
<TextBlock Text="{Binding Description}" FontSize="11" Opacity="0.6" TextWrapping="Wrap"
IsVisible="{Binding Description, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
</StackPanel>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
<TextBlock Text="{Binding SelectionError}" Foreground="Red" FontSize="11"
IsVisible="{Binding SelectionError, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
</StackPanel>
<Separator Margin="0,4"/>
<StackPanel Spacing="10">
<TextBlock Text="Neuen Ablauf anlegen" FontSize="12" FontWeight="SemiBold" Opacity="0.7"/>
<StackPanel Spacing="4">
<TextBox Text="{Binding NewName}" PlaceholderText="Name, z.B. Kurzversion"/>
<TextBlock Text="{Binding NewNameError}" Foreground="Red" FontSize="11"
IsVisible="{Binding NewNameError, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
</StackPanel>
<TextBox Text="{Binding NewDescription}" AcceptsReturn="True" Height="64" TextWrapping="Wrap"
PlaceholderText="Wann nimmt man diesen Weg? z.B. 'Bei Zeitmangel, wenn Aufgabe 3 nicht mehr passt.'"/>
<Button Content="Anlegen und auswählen" Command="{Binding CreateNewCommand}" HorizontalAlignment="Left"/>
</StackPanel>
</StackPanel>
</ScrollViewer>
<Grid Grid.Row="1" ColumnDefinitions="*,8,*" Margin="0,20,0,0">
<Button Grid.Column="0" Content="Abbrechen" HorizontalAlignment="Stretch" Click="OnCancel"/>
<Button Grid.Column="2" Content="Übernehmen" HorizontalAlignment="Stretch" Click="OnConfirm"/>
</Grid>
</Grid>
</Window>
@@ -0,0 +1,21 @@
using Avalonia.Controls;
using Avalonia.Interactivity;
using LehrerApp.Desktop.ViewModels.Groups;
namespace LehrerApp.Desktop.Views.Groups;
public partial class AlternativePathDialog : Window
{
public AlternativePathDialog() => InitializeComponent();
private void OnConfirm(object? s, RoutedEventArgs e)
{
if (DataContext is AlternativePathDialogViewModel vm && vm.ConfirmCommand.CanExecute(null))
{
vm.ConfirmCommand.Execute(null);
if (vm.Result is not null) Close(true);
}
}
private void OnCancel(object? s, RoutedEventArgs e) => Close(false);
}
@@ -4,7 +4,7 @@
x:Class="LehrerApp.Desktop.Views.Groups.LessonDialog"
x:DataType="vm:LessonDialogViewModel"
Title="{Binding DialogTitle}"
Width="960" Height="760" MinWidth="720" MinHeight="480"
Width="1000" Height="760" MinWidth="780" MinHeight="480"
CanResize="True" WindowStartupLocation="CenterOwner">
<Grid RowDefinitions="*,Auto" Margin="24">
@@ -56,12 +56,14 @@
</Grid>
<!-- Tabellenkopf -->
<Grid ColumnDefinitions="140,130,*,110,100,26,26,26" Margin="4,0,0,0">
<Grid ColumnDefinitions="130,95,120,*,100,95,26,26,26" Margin="4,0,0,0">
<TextBlock Grid.Column="0" Text="Phase" FontSize="11" FontWeight="SemiBold" Opacity="0.6"/>
<TextBlock Grid.Column="1" Text="Dauer / Zeit" FontSize="11" FontWeight="SemiBold" Opacity="0.6"/>
<TextBlock Grid.Column="2" Text="Tätigkeit" FontSize="11" FontWeight="SemiBold" Opacity="0.6"/>
<TextBlock Grid.Column="3" Text="Material" FontSize="11" FontWeight="SemiBold" Opacity="0.6"/>
<TextBlock Grid.Column="4" Text="Kurzsymbol" FontSize="11" FontWeight="SemiBold" Opacity="0.6"/>
<TextBlock Grid.Column="1" Text="Pfad" FontSize="11" FontWeight="SemiBold" Opacity="0.6"
ToolTip.Tip="Ankreuzen, wenn diese Phase zu einem alternativen Ablauf gehört (z.B. Kurzversion bei Zeitnot) — öffnet die Zuweisung. Phasen mit demselben Ablauf werden im Verlaufsplan-Viewer gruppiert."/>
<TextBlock Grid.Column="2" Text="Dauer / Zeit" FontSize="11" FontWeight="SemiBold" Opacity="0.6"/>
<TextBlock Grid.Column="3" Text="Tätigkeit" FontSize="11" FontWeight="SemiBold" Opacity="0.6"/>
<TextBlock Grid.Column="4" Text="Material" FontSize="11" FontWeight="SemiBold" Opacity="0.6"/>
<TextBlock Grid.Column="5" Text="Kurzsymbol" FontSize="11" FontWeight="SemiBold" Opacity="0.6"/>
</Grid>
<Separator/>
@@ -71,34 +73,38 @@
<DataTemplate x:DataType="vm:PhaseStepEditItem">
<Border BorderBrush="{DynamicResource SystemControlForegroundBaseLowBrush}"
BorderThickness="0,0,0,1" Padding="0,8">
<Grid ColumnDefinitions="140,130,*,110,100,26,26,26">
<Grid ColumnDefinitions="130,95,120,*,100,95,26,26,26">
<TextBox Grid.Column="0" Text="{Binding Name}" PlaceholderText="z.B. Erarbeitung"
VerticalAlignment="Top" Margin="0,0,6,0"/>
<StackPanel Grid.Column="1" Orientation="Horizontal" Spacing="6"
<CheckBox Grid.Column="1" IsChecked="{Binding HasAlternativePath}"
Content="{Binding AlternativePathName}" Foreground="{Binding AlternativePathColorHex}"
FontSize="12" VerticalAlignment="Top" Margin="0,4,6,0"
ToolTip.Tip="Teil eines alternativen Ablaufs (z.B. Kurzversion bei Zeitnot). Ankreuzen zum Zuweisen/Anlegen, abwählen zum Entfernen."/>
<StackPanel Grid.Column="2" Orientation="Horizontal" Spacing="6"
VerticalAlignment="Top" Margin="0,0,6,0">
<NumericUpDown Value="{Binding DurationMinutes}" Minimum="0" Maximum="180" Width="62"
FormatString="0" ShowButtonSpinner="False" ToolTip.Tip="Dauer in Minuten"/>
<TextBlock Text="{Binding ComputedTimeDisplay}" FontSize="12" Opacity="0.6"
VerticalAlignment="Center"/>
</StackPanel>
<TextBox Grid.Column="2" Text="{Binding Activity}" AcceptsReturn="True" TextWrapping="Wrap"
<TextBox Grid.Column="3" Text="{Binding Activity}" AcceptsReturn="True" TextWrapping="Wrap"
Height="56" VerticalAlignment="Top" Margin="0,0,6,0"
PlaceholderText="Lehrer-/Schüler-Tätigkeit"
ToolTip.Tip="Bei mehr Text scrollt das Feld intern."/>
<AutoCompleteBox Grid.Column="3" Text="{Binding Material}"
<AutoCompleteBox Grid.Column="4" Text="{Binding Material}"
ItemsSource="{Binding $parent[ItemsControl].((vm:LessonDialogViewModel)DataContext).MaterialSuggestions}"
FilterMode="Contains" MinimumPrefixLength="0" VerticalAlignment="Top"
Margin="0,0,6,0" PlaceholderText="z.B. AB01"/>
<AutoCompleteBox Grid.Column="4" Text="{Binding Shorthand}"
<AutoCompleteBox Grid.Column="5" Text="{Binding Shorthand}"
ItemsSource="{Binding $parent[ItemsControl].((vm:LessonDialogViewModel)DataContext).ShorthandSuggestions}"
FilterMode="Contains" MinimumPrefixLength="0" VerticalAlignment="Top"
Margin="0,0,6,0" PlaceholderText="z.B. AB001-&gt;S, Plenum, LDE"
ToolTip.Tip="Freitext für den schnellen Überblick — mal ein Materialfluss-Pfeil (AB001-&gt;S), mal nur eine Sozialform (Plenum, LDE)."/>
<Button Grid.Column="5" Content="↑" Command="{Binding MoveUpCommand}" Padding="4,2"
<Button Grid.Column="6" Content="↑" Command="{Binding MoveUpCommand}" Padding="4,2"
VerticalAlignment="Top" ToolTip.Tip="Nach oben"/>
<Button Grid.Column="6" Content="↓" Command="{Binding MoveDownCommand}" Padding="4,2"
<Button Grid.Column="7" Content="↓" Command="{Binding MoveDownCommand}" Padding="4,2"
VerticalAlignment="Top" Margin="2,0,0,0" ToolTip.Tip="Nach unten"/>
<Button Grid.Column="7" Content="✕" Command="{Binding RemoveCommand}" Padding="4,2"
<Button Grid.Column="8" Content="✕" Command="{Binding RemoveCommand}" Padding="4,2"
VerticalAlignment="Top" Margin="2,0,0,0" ToolTip.Tip="Entfernen"/>
</Grid>
</Border>
@@ -1,6 +1,9 @@
using Avalonia.Controls;
using Avalonia.Interactivity;
using LehrerApp.Core.Interfaces;
using LehrerApp.Core.Models;
using LehrerApp.Desktop.ViewModels.Groups;
using Microsoft.Extensions.DependencyInjection;
namespace LehrerApp.Desktop.Views.Groups;
@@ -8,6 +11,23 @@ public partial class LessonDialog : Window
{
public LessonDialog() => InitializeComponent();
protected override void OnDataContextChanged(EventArgs e)
{
base.OnDataContextChanged(e);
if (DataContext is LessonDialogViewModel vm)
vm.OnPickAlternativePath = ShowAlternativePathDialog;
}
private async Task<AlternativeLessonPath?> ShowAlternativePathDialog(Guid? currentId)
{
var dialogVm = new AlternativePathDialogViewModel(
App.Services.GetRequiredService<IAlternativeLessonPathRepository>(), currentId);
var dialog = new AlternativePathDialog { DataContext = dialogVm };
var ok = await dialog.ShowDialog<bool>(this);
return ok ? dialogVm.Result : null;
}
private void OnSave(object? s, RoutedEventArgs e)
{
if (DataContext is LessonDialogViewModel vm && vm.SaveCommand.CanExecute(null))
@@ -0,0 +1,90 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="clr-namespace:LehrerApp.Desktop.ViewModels.Groups"
x:Class="LehrerApp.Desktop.Views.Groups.LessonViewerDialog"
x:DataType="vm:LessonViewerViewModel"
Title="{Binding Topic}"
Width="900" Height="700" MinWidth="600" MinHeight="420"
CanResize="True" WindowStartupLocation="CenterOwner">
<Grid RowDefinitions="Auto,*,Auto" Margin="24">
<StackPanel Grid.Row="0" Spacing="4" Margin="0,0,0,16">
<TextBlock Text="{Binding Topic}" FontSize="20" FontWeight="SemiBold"/>
<StackPanel Orientation="Horizontal" Spacing="16">
<TextBlock FontSize="13" Opacity="0.7">
<Run Text="Datum: "/><Run Text="{Binding DateDisplay}"/>
</TextBlock>
<TextBlock FontSize="13" Opacity="0.7">
<Run Text="Beginn: "/><Run Text="{Binding StartTimeDisplay}"/>
</TextBlock>
<TextBlock FontSize="13" Opacity="0.7">
<Run Text="Status: "/><Run Text="{Binding StatusLabel}"/>
</TextBlock>
</StackPanel>
</StackPanel>
<ScrollViewer Grid.Row="1">
<StackPanel Spacing="0">
<Grid ColumnDefinitions="140,80,*,140,130" Margin="4,0,0,6">
<TextBlock Grid.Column="0" Text="Phase" FontSize="12" FontWeight="SemiBold" Opacity="0.6"/>
<TextBlock Grid.Column="1" Text="Dauer/Zeit" FontSize="12" FontWeight="SemiBold" Opacity="0.6"/>
<TextBlock Grid.Column="2" Text="Tätigkeit" FontSize="12" FontWeight="SemiBold" Opacity="0.6"/>
<TextBlock Grid.Column="3" Text="Material" FontSize="12" FontWeight="SemiBold" Opacity="0.6"/>
<TextBlock Grid.Column="4" Text="Kurzsymbol" FontSize="12" FontWeight="SemiBold" Opacity="0.6"/>
</Grid>
<Separator/>
<ItemsControl ItemsSource="{Binding PhaseGroups}">
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="vm:PhaseGroupViewItem">
<StackPanel Margin="0,0,0,14">
<StackPanel IsVisible="{Binding $parent[ItemsControl].((vm:LessonViewerViewModel)DataContext).HasAlternatives}">
<TextBlock Text="{Binding Label}" FontSize="13" FontWeight="SemiBold" Opacity="0.75"
Margin="4,10,0,2"/>
<TextBlock Text="{Binding Description}" FontSize="12" Opacity="0.55" TextWrapping="Wrap"
Margin="4,0,0,6"
IsVisible="{Binding Description, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
</StackPanel>
<ItemsControl ItemsSource="{Binding Phases}">
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="vm:PhaseViewItem">
<Border BorderBrush="{DynamicResource SystemControlForegroundBaseLowBrush}"
BorderThickness="0,0,0,1" Padding="4,10">
<Grid ColumnDefinitions="140,80,*,140,130">
<TextBlock Grid.Column="0" Text="{Binding Name}" FontSize="15" FontWeight="SemiBold"
TextWrapping="Wrap" VerticalAlignment="Top" Margin="0,0,8,0"/>
<StackPanel Grid.Column="1" VerticalAlignment="Top">
<TextBlock Text="{Binding DurationMinutes, StringFormat='{}{0} Min.'}" FontSize="14"/>
<TextBlock Text="{Binding TimeDisplay}" FontSize="12" Opacity="0.6"/>
</StackPanel>
<TextBlock Grid.Column="2" Text="{Binding Activity}" FontSize="14"
TextWrapping="Wrap" VerticalAlignment="Top" Margin="0,0,8,0"/>
<TextBlock Grid.Column="3" Text="{Binding Material}" FontSize="14"
TextWrapping="Wrap" VerticalAlignment="Top" Margin="0,0,8,0"/>
<TextBlock Grid.Column="4" Text="{Binding Shorthand}" FontSize="14"
TextWrapping="Wrap" VerticalAlignment="Top"/>
</Grid>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<StackPanel Spacing="4" Margin="4,16,0,0" IsVisible="{Binding Homework, Converter={x:Static StringConverters.IsNotNullOrEmpty}}">
<TextBlock Text="Hausaufgabe" FontSize="12" FontWeight="SemiBold" Opacity="0.6"/>
<TextBlock Text="{Binding Homework}" FontSize="14" TextWrapping="Wrap"/>
</StackPanel>
<StackPanel Spacing="4" Margin="4,16,0,0" IsVisible="{Binding Reflection, Converter={x:Static StringConverters.IsNotNullOrEmpty}}">
<TextBlock Text="Reflexion" FontSize="12" FontWeight="SemiBold" Opacity="0.6"/>
<TextBlock Text="{Binding Reflection}" FontSize="14" TextWrapping="Wrap"/>
</StackPanel>
</StackPanel>
</ScrollViewer>
<Button Grid.Row="2" Content="Schließen" HorizontalAlignment="Right" Margin="0,16,0,0" Click="OnClose"/>
</Grid>
</Window>
@@ -0,0 +1,11 @@
using Avalonia.Controls;
using Avalonia.Interactivity;
namespace LehrerApp.Desktop.Views.Groups;
public partial class LessonViewerDialog : Window
{
public LessonViewerDialog() => InitializeComponent();
private void OnClose(object? s, RoutedEventArgs e) => Close();
}
@@ -63,6 +63,8 @@
</StackPanel>
<StackPanel Grid.Column="1" Orientation="Horizontal" Spacing="8">
<Button Content=" Stunde" Command="{Binding AddLessonCommand}"/>
<Button Content="Anzeigen" Command="{Binding ShowLessonCommand}"
ToolTip.Tip="Verlaufsplan schreibgeschützt und größer anzeigen — zum Mitnehmen in den Unterricht."/>
<Button Content="Bearbeiten" Command="{Binding EditLessonCommand}"/>
<Button Content="Verschieben" Command="{Binding MoveLessonCommand}"/>
<Button Content="Status → Durchgeführt" Command="{Binding AdvanceLessonStatusCommand}"/>
@@ -26,6 +26,7 @@ public partial class PlanningTabView : UserControl
ShowLessonDialog(lesson.UnitId, lesson.GroupId, materials, shorthands, editingLesson: lesson);
vm.OnConfirmDeleteLesson = ShowDeleteLessonDialog;
vm.OnPickMoveTarget = ShowMoveLessonDialog;
vm.OnShowLesson = ShowLessonViewerDialog;
}
}
@@ -78,6 +79,7 @@ public partial class PlanningTabView : UserControl
var dialogVm = new LessonDialogViewModel(
App.Services.GetRequiredService<ILessonRepository>(),
App.Services.GetRequiredService<IShorthandCodeRepository>(),
App.Services.GetRequiredService<IAlternativeLessonPathRepository>(),
unitId, groupId, materialSuggestions, shorthandHistorySuggestions, editingLesson);
var dialog = new LessonDialog { DataContext = dialogVm };
@@ -111,4 +113,13 @@ public partial class PlanningTabView : UserControl
var ok = await dialog.ShowDialog<bool>(owner);
return ok ? dialogVm.Result : null;
}
private async Task ShowLessonViewerDialog(Lesson lesson)
{
var viewerVm = new LessonViewerViewModel(lesson,
App.Services.GetRequiredService<IAlternativeLessonPathRepository>());
var dialog = new LessonViewerDialog { DataContext = viewerVm };
var owner = TopLevel.GetTopLevel(this) as Window;
if (owner is not null) await dialog.ShowDialog(owner);
}
}