Stundenplan: Wochenraster, Wochennavigation, Ferien/Feiertage (Kapitel 4.3)

Neuer Stundenplan mit "Heute"-Standardansicht (Tagesliste unten angedockt,
gruppenübergreifendes Wochenraster mit Fach/Klasse/Raum/Thema, Vor-/Zurück-
Navigation zwischen Kalenderwochen) und separatem Bearbeiten-Raster für die
wöchentliche Zuordnung. Badges für Ferien-/Klausur-Nähe und ausgegraute
Ferientage direkt im Plan statt einer separaten Liste. Ferien-/Feiertage-
Pflege (Bundesland, Schulferien) sitzt jetzt in den Einstellungen statt im
Stundenplan selbst.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-15 02:29:18 +02:00
co-authored by Claude Sonnet 5
parent ee1a47641e
commit 99b2de6519
26 changed files with 2154 additions and 18 deletions
+5
View File
@@ -9,6 +9,8 @@
xmlns:vs="clr-namespace:LehrerApp.Desktop.Views.Students"
xmlns:vset="clr-namespace:LehrerApp.Desktop.Views.Settings"
xmlns:vmset="clr-namespace:LehrerApp.Desktop.ViewModels.Settings"
xmlns:vp="clr-namespace:LehrerApp.Desktop.Views.Planning"
xmlns:vmp="clr-namespace:LehrerApp.Desktop.ViewModels.Planning"
xmlns:svc="clr-namespace:LehrerApp.Desktop.Services"
x:Class="LehrerApp.Desktop.Views.MainWindow"
x:DataType="vm:MainWindowViewModel"
@@ -48,6 +50,9 @@
<DataTemplate DataType="vmset:SettingsViewModel">
<vset:SettingsView/>
</DataTemplate>
<DataTemplate DataType="vmp:TimetableViewModel">
<vp:TimetableView/>
</DataTemplate>
<DataTemplate DataType="vm:PlaceholderViewModel">
<views:PlaceholderView/>
</DataTemplate>
@@ -0,0 +1,34 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="clr-namespace:LehrerApp.Desktop.ViewModels.Planning"
x:Class="LehrerApp.Desktop.Views.Planning.TimetableSlotDialog"
x:DataType="vm:TimetableSlotDialogViewModel"
Title="{Binding DialogTitle}"
Width="360" SizeToContent="Height"
CanResize="False" WindowStartupLocation="CenterOwner">
<StackPanel Margin="24" Spacing="12">
<TextBlock Text="{Binding DialogTitle}" Classes="dialogtitle"/>
<StackPanel Spacing="4">
<TextBlock Text="Lerngruppe *" FontSize="12" Opacity="0.7"/>
<ComboBox ItemsSource="{Binding GroupOptions}" SelectedItem="{Binding SelectedGroupName}"
HorizontalAlignment="Stretch"/>
<TextBlock Text="{Binding GroupError}" Foreground="Red" FontSize="11"
IsVisible="{Binding GroupError, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
</StackPanel>
<StackPanel Spacing="4">
<TextBlock Text="Raum (optional)" FontSize="12" Opacity="0.7"/>
<TextBox Text="{Binding Room}"/>
</StackPanel>
<Button Content="Eintrag entfernen" HorizontalAlignment="Stretch" Margin="0,10,0,0"
IsVisible="{Binding IsEditing}" Click="OnDelete"/>
<Grid ColumnDefinitions="*,8,*" Margin="0,4,0,0">
<Button Grid.Column="0" Content="Abbrechen" HorizontalAlignment="Stretch" Click="OnCancel"/>
<Button Grid.Column="2" Content="Speichern" HorizontalAlignment="Stretch" Click="OnSave"/>
</Grid>
</StackPanel>
</Window>
@@ -0,0 +1,30 @@
using Avalonia.Controls;
using Avalonia.Interactivity;
using LehrerApp.Desktop.ViewModels.Planning;
namespace LehrerApp.Desktop.Views.Planning;
public partial class TimetableSlotDialog : Window
{
public TimetableSlotDialog() => InitializeComponent();
private void OnSave(object? sender, RoutedEventArgs e)
{
if (DataContext is TimetableSlotDialogViewModel vm)
{
vm.SaveCommand.Execute(null);
if (vm.Result is not null) Close();
}
}
private void OnDelete(object? sender, RoutedEventArgs e)
{
if (DataContext is TimetableSlotDialogViewModel vm)
{
vm.DeleteCommand.Execute(null);
Close();
}
}
private void OnCancel(object? sender, RoutedEventArgs e) => Close();
}
@@ -0,0 +1,210 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="clr-namespace:LehrerApp.Desktop.ViewModels.Planning"
xmlns:shared="clr-namespace:LehrerApp.Desktop.Views.Shared"
x:Class="LehrerApp.Desktop.Views.Planning.TimetableView"
x:DataType="vm:TimetableViewModel">
<UserControl.Styles>
<Style Selector="Border.timetablecell">
<Setter Property="Background" Value="{DynamicResource SystemControlBackgroundBaseLowBrush}"/>
</Style>
<Style Selector="Border.timetablecell.assigned">
<Setter Property="Background" Value="Transparent"/>
</Style>
<Style Selector="Border.weekheader.today">
<Setter Property="Background" Value="{DynamicResource SystemAccentColor}"/>
</Style>
</UserControl.Styles>
<Grid RowDefinitions="Auto,*">
<shared:PageHeader Grid.Row="0" Margin="32,28,32,0" Title="Stundenplan"
Subtitle="Wiederkehrendes wöchentliches Muster, keine konkreten Termine"/>
<TabbedPage Grid.Row="1" TabPlacement="Top" SelectedIndex="{Binding ActiveTabIndex}">
<!-- Tab: Heute (Standardansicht) -->
<ContentPage Header="Heute">
<DockPanel Margin="32,20,32,20">
<!-- Unten angedockt: heutige Stunden im Detail -->
<Border DockPanel.Dock="Bottom" Margin="0,16,0,0"
Background="{DynamicResource SystemControlBackgroundAltHighBrush}"
CornerRadius="8" Padding="16" MaxHeight="240">
<DockPanel>
<TextBlock DockPanel.Dock="Top" Text="{Binding TodayLabel}" FontSize="15" FontWeight="SemiBold" Margin="0,0,0,8"/>
<ScrollViewer>
<ItemsControl ItemsSource="{Binding TodayItems}">
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="vm:TodayLessonItem">
<Border BorderBrush="{DynamicResource SystemControlForegroundBaseLowBrush}"
BorderThickness="0,0,0,1" Padding="0,8">
<Grid ColumnDefinitions="6,Auto,*,Auto">
<Rectangle Grid.Column="0" Fill="{Binding ColorHex}" Width="5" HorizontalAlignment="Left"/>
<TextBlock Grid.Column="1" Text="{Binding PeriodNumber}" FontSize="17" FontWeight="Bold"
Width="30" HorizontalAlignment="Center" VerticalAlignment="Center" Margin="8,0"/>
<StackPanel Grid.Column="2" Spacing="2">
<TextBlock Text="{Binding GroupName}" FontSize="14" FontWeight="SemiBold"/>
<TextBlock FontSize="11" Opacity="0.6" IsVisible="{Binding HasRoom}">
<Run Text="Raum "/><Run Text="{Binding Room}"/>
</TextBlock>
<TextBlock Text="{Binding LessonTopic}" FontSize="11" Opacity="0.75"
TextWrapping="Wrap" IsVisible="{Binding HasLessonTopic}"/>
<TextBlock Text="{Binding ExamTitle}" FontSize="11" Foreground="#D85A30" FontWeight="SemiBold"
IsVisible="{Binding HasExam}"/>
</StackPanel>
<Button Grid.Column="3" Content="Zur Lerngruppe" FontSize="11" Padding="9,4"
VerticalAlignment="Center"
Command="{Binding $parent[ItemsControl].((vm:TimetableViewModel)DataContext).OpenGroupCommand}"
CommandParameter="{Binding GroupId}"/>
</Grid>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
</DockPanel>
</Border>
<TextBlock DockPanel.Dock="Bottom" Text="Heute sind keine Stunden im Stundenplan eingetragen." Classes="emptyhint"
IsVisible="{Binding !TodayItems.Count}" Margin="0,8,0,0"/>
<!-- Rest: Wochenüberblick (nicht editierbar) -->
<ScrollViewer>
<StackPanel Spacing="14">
<Grid ColumnDefinitions="Auto,Auto,*,Auto,Auto">
<Button Grid.Column="0" Content="" FontWeight="Bold" Padding="10,4"
Command="{Binding PreviousWeekCommand}" ToolTip.Tip="Vorherige Woche"/>
<Button Grid.Column="1" Content="" FontWeight="Bold" Padding="10,4" Margin="4,0,0,0"
Command="{Binding NextWeekCommand}" ToolTip.Tip="Nächste Woche"/>
<TextBlock Grid.Column="2" FontSize="16" FontWeight="SemiBold"
VerticalAlignment="Center" Margin="12,0,0,0">
<Run Text="Woche "/><Run Text="{Binding WeekRangeLabel}"/>
</TextBlock>
<Button Grid.Column="3" Content="Diese Woche" Margin="0,0,8,0"
Command="{Binding CurrentWeekCommand}" IsVisible="{Binding !IsCurrentWeek}"/>
<Button Grid.Column="4" Content="Stundenplan bearbeiten" Command="{Binding ShowEditorCommand}"/>
</Grid>
<ItemsControl ItemsSource="{Binding WeekItems}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate><UniformGrid Columns="6"/></ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="vm:WeekCellItem">
<Grid Margin="2" MinHeight="76">
<Border Classes="weekheader" Classes.today="{Binding IsToday}" CornerRadius="4"
IsVisible="{Binding IsHeader}">
<TextBlock Text="{Binding Text}" FontWeight="SemiBold" FontSize="12" Opacity="0.75"
HorizontalAlignment="Center" VerticalAlignment="Center" Margin="4"/>
</Border>
<TextBlock Text="{Binding Text}" IsVisible="{Binding IsPeriodLabel}"
FontWeight="SemiBold" FontSize="13"
HorizontalAlignment="Center" VerticalAlignment="Center"/>
<Border Classes="timetablecell" Classes.assigned="{Binding IsAssigned}"
CornerRadius="6" IsVisible="{Binding IsSlotCell}">
<Button Background="{Binding ColorHex}" IsVisible="{Binding IsAssigned}"
HorizontalAlignment="Stretch" VerticalAlignment="Stretch"
HorizontalContentAlignment="Stretch" CornerRadius="6" Padding="6"
Command="{Binding $parent[ItemsControl].((vm:TimetableViewModel)DataContext).OpenGroupCommand}"
CommandParameter="{Binding GroupId}">
<StackPanel Spacing="1">
<TextBlock FontSize="11" FontWeight="Bold" Foreground="White" TextWrapping="Wrap">
<Run Text="{Binding SubjectLabel}"/><Run Text=" · "/><Run Text="{Binding GroupName}"/>
</TextBlock>
<TextBlock Text="{Binding Room}" FontSize="10" Foreground="White" Opacity="0.9"
IsVisible="{Binding HasRoom}"/>
<TextBlock Text="{Binding Topic}" FontSize="10" Foreground="White" Opacity="0.8"
TextWrapping="Wrap" IsVisible="{Binding HasTopic}"/>
<TextBlock Text="Ferien" FontSize="10" Foreground="White" FontWeight="SemiBold"
Opacity="0.9" IsVisible="{Binding IsHoliday}" Margin="0,2,0,0"/>
<StackPanel Orientation="Horizontal" Spacing="3" Margin="0,2,0,0"
IsVisible="{Binding !IsHoliday}">
<Border Background="#B71C1C" CornerRadius="7" Padding="4,0" IsVisible="{Binding HasHolidayBadge}">
<TextBlock Text="{Binding HolidayBadge}" FontSize="9" FontWeight="Bold" Foreground="White"/>
</Border>
<TextBlock Text="📝" FontSize="11" IsVisible="{Binding HasExam}" ToolTip.Tip="Klausur"/>
<TextBlock Text="⏰" FontSize="11" IsVisible="{Binding IsLastBeforeExam}" ToolTip.Tip="Letzte Stunde vor der Klausur"/>
<TextBlock Text="🧪" FontSize="11" IsVisible="{Binding HasExperiment}" ToolTip.Tip="Experiment geplant"/>
</StackPanel>
</StackPanel>
</Button>
</Border>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<Border Background="{DynamicResource SystemControlBackgroundAltHighBrush}"
CornerRadius="8" Padding="16" IsVisible="{Binding HoursWarnings.Count}">
<StackPanel Spacing="8">
<TextBlock Text="ABWEICHENDE WOCHENSTUNDEN" FontSize="11" FontWeight="Bold" Opacity="0.5"/>
<ItemsControl ItemsSource="{Binding HoursWarnings}">
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="vm:HoursWarningItem">
<TextBlock Text="{Binding Text}" FontSize="12" Foreground="#FB8C00"
TextWrapping="Wrap" Margin="0,3"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</Border>
</StackPanel>
</ScrollViewer>
</DockPanel>
</ContentPage>
<!-- Tab: Bearbeiten (Raster) -->
<ContentPage Header="Bearbeiten">
<ScrollViewer>
<StackPanel Margin="32,20,32,28" Spacing="10">
<ItemsControl ItemsSource="{Binding Cells}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate><UniformGrid Columns="6"/></ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="vm:TimetableCellItem">
<Grid Margin="2" MinHeight="46">
<TextBlock Text="{Binding Text}" IsVisible="{Binding IsHeader}"
FontWeight="SemiBold" FontSize="12" Opacity="0.6"
HorizontalAlignment="Center" VerticalAlignment="Center"/>
<TextBlock Text="{Binding Text}" IsVisible="{Binding IsPeriodLabel}"
FontWeight="SemiBold" FontSize="13"
HorizontalAlignment="Center" VerticalAlignment="Center"/>
<Border Classes="timetablecell" Classes.assigned="{Binding IsAssigned}"
CornerRadius="6" IsVisible="{Binding IsSlotCell}">
<Panel>
<Button Background="{Binding ColorHex}"
IsVisible="{Binding IsAssigned}"
HorizontalAlignment="Stretch" VerticalAlignment="Stretch"
HorizontalContentAlignment="Center" CornerRadius="6"
Command="{Binding $parent[ItemsControl].((vm:TimetableViewModel)DataContext).EditCellCommand}"
CommandParameter="{Binding}">
<TextBlock Text="{Binding Text}" FontSize="11" Foreground="White"
TextWrapping="Wrap" TextAlignment="Center"/>
</Button>
<Button Content="+" Opacity="0.35"
IsVisible="{Binding !IsAssigned}"
HorizontalAlignment="Stretch" VerticalAlignment="Stretch"
HorizontalContentAlignment="Center" Background="Transparent"
Command="{Binding $parent[ItemsControl].((vm:TimetableViewModel)DataContext).EditCellCommand}"
CommandParameter="{Binding}"/>
<Border Background="#D85A30" CornerRadius="8" Padding="5,1"
IsVisible="{Binding HasBadge}"
HorizontalAlignment="Right" VerticalAlignment="Top" Margin="2">
<TextBlock Text="{Binding BadgeText}" FontSize="10" FontWeight="Bold" Foreground="White"/>
</Border>
</Panel>
</Border>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</ScrollViewer>
</ContentPage>
</TabbedPage>
</Grid>
</UserControl>
@@ -0,0 +1,33 @@
using Avalonia.Controls;
using LehrerApp.Core.Interfaces;
using LehrerApp.Core.Services;
using LehrerApp.Desktop.ViewModels.Planning;
using Microsoft.Extensions.DependencyInjection;
namespace LehrerApp.Desktop.Views.Planning;
public partial class TimetableView : UserControl
{
public TimetableView() => InitializeComponent();
protected override void OnDataContextChanged(EventArgs e)
{
base.OnDataContextChanged(e);
if (DataContext is TimetableViewModel vm) vm.OnEditSlot = ShowSlotDialog;
}
private async Task ShowSlotDialog(TimetableCellItem cell)
{
var owner = TopLevel.GetTopLevel(this) as Window;
if (owner is null || cell.Weekday is null) return;
var vm = new TimetableSlotDialogViewModel(
App.Services.GetRequiredService<ITimetableSlotRepository>(),
App.Services.GetRequiredService<IGroupRepository>(),
App.Services.GetRequiredService<SchoolYearService>(),
cell.Weekday.Value, cell.PeriodNumber, cell.Slot);
var dialog = new TimetableSlotDialog { DataContext = vm };
await dialog.ShowDialog(owner);
}
}
@@ -478,6 +478,64 @@
</ScrollViewer>
</ContentPage>
<!-- Tab: Ferien & Feiertage (4.3.5) -->
<ContentPage Header="Ferien &amp; Feiertage">
<ScrollViewer>
<StackPanel Margin="32,20,32,28" Spacing="14" MaxWidth="480">
<StackPanel Spacing="4">
<TextBlock Text="Bundesland (für Feiertage)" FontSize="13" FontWeight="SemiBold"/>
<ComboBox ItemsSource="{Binding StateOptions}" SelectedItem="{Binding SelectedStateName}"
HorizontalAlignment="Stretch"/>
</StackPanel>
<Separator Margin="0,4"/>
<TextBlock Text="Schulferien" FontSize="16" FontWeight="SemiBold"/>
<TextBlock Text="Werden im Stundenplan als unterrichtsfreie Tage angezeigt."
FontSize="12" Opacity="0.6" TextWrapping="Wrap"/>
<ItemsControl ItemsSource="{Binding SchoolHolidayEntries}">
<ItemsControl.ItemTemplate>
<DataTemplate DataType="vm:SchoolHolidayItem">
<Border BorderBrush="{DynamicResource SystemControlForegroundBaseLowBrush}"
BorderThickness="0,0,0,1" Padding="0,7">
<Grid ColumnDefinitions="*,Auto">
<StackPanel Grid.Column="0">
<TextBlock Text="{Binding Name}" FontSize="13" FontWeight="SemiBold"/>
<TextBlock Text="{Binding RangeDisplay}" FontSize="12" Opacity="0.6"/>
</StackPanel>
<Button Grid.Column="1" Content="×" FontSize="14" Padding="9,3"
Command="{Binding $parent[ItemsControl].((vm:SettingsViewModel)DataContext).RemoveSchoolHolidayCommand}"
CommandParameter="{Binding}"/>
</Grid>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<TextBlock Text="Noch keine Schulferien hinterlegt." Classes="emptyhint"
IsVisible="{Binding !SchoolHolidayEntries.Count}"/>
<Separator Margin="0,4"/>
<StackPanel Spacing="4">
<TextBox Text="{Binding NewHolidayName}" PlaceholderText="Name (z.B. Sommerferien)"/>
<TextBlock Text="{Binding HolidayNameError}" Foreground="Red" FontSize="11"
IsVisible="{Binding HolidayNameError, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
<Grid ColumnDefinitions="*,8,*">
<TextBox Grid.Column="0" Text="{Binding NewHolidayStartText}" PlaceholderText="Beginn TT.MM.JJJJ"/>
<TextBox Grid.Column="2" Text="{Binding NewHolidayEndText}" PlaceholderText="Ende TT.MM.JJJJ"/>
</Grid>
<TextBlock Text="{Binding HolidayDateError}" Foreground="Red" FontSize="11"
IsVisible="{Binding HolidayDateError, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
<Button Content=" Schulferien hinzufügen" Command="{Binding AddSchoolHolidayCommand}"
HorizontalAlignment="Left"/>
</StackPanel>
</StackPanel>
</ScrollViewer>
</ContentPage>
</TabbedPage>
</Grid>
</UserControl>