Klausurverwaltung: Anlegen, Bearbeiten, Duplizieren, Status, Aufgaben

Implementiert TODO 1.1 (Klausur anlegen/verwalten) und die dazu passenden
Teile von 1.2 (Aufgabenstruktur), da beides im selben Dialog verwaltet wird:

- ExamDialog: Klausur anlegen/bearbeiten/duplizieren mit Aufgaben-Editor
  (Nr./Titel/Maximalpunkte/Gewichtung, sortierbar), Gesamtpunkte-Anzeige
  mit Warnung bei 0 Punkten, und Kompetenz-Zuordnung je Aufgabe.
- Klausuren-Tab: Kontextmenü und Toolbar für Bearbeiten/Duplizieren/
  Löschen (mit Rückfrage), farbcodierte Status-Spalte, SplitButton für
  Statuswechsel (Klick = nächster Status, Dropdown = manuelles Setzen).
- ExamRepository.Delete löscht zugehörige ExamResults kaskadierend.
- Gewichtung ist standardmäßig ausgeblendet (Checkbox zum Einblenden),
  NumericUpDown-Felder ohne Spinner-Buttons wegen eines Layout-Bugs im
  Avalonia-Standardtemplate, der das interne Textfeld auf wenige Pixel
  schrumpfen ließ.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-11 22:00:48 +02:00
co-authored by Claude Sonnet 5
parent fd715dd5a5
commit 21378bbebd
11 changed files with 643 additions and 25 deletions
@@ -0,0 +1,20 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="LehrerApp.Desktop.Views.Groups.DeleteExamDialog"
x:CompileBindings="False"
Title="Klausur löschen"
Width="430" SizeToContent="Height"
CanResize="False" WindowStartupLocation="CenterOwner">
<Grid RowDefinitions="*,Auto" Margin="24">
<StackPanel Grid.Row="0" Spacing="12">
<TextBlock Text="Klausur wirklich löschen?" FontSize="18" FontWeight="SemiBold"/>
<TextBlock Text="{Binding}" FontSize="15" FontWeight="SemiBold" TextWrapping="Wrap"/>
<TextBlock Text="Dabei werden auch alle erfassten Ergebnisse dieser Klausur dauerhaft gelöscht."
TextWrapping="Wrap" Opacity="0.7"/>
</StackPanel>
<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="Endgültig löschen" HorizontalAlignment="Stretch" Click="OnDelete"/>
</Grid>
</Grid>
</Window>
@@ -0,0 +1,12 @@
using Avalonia.Controls;
using Avalonia.Interactivity;
namespace LehrerApp.Desktop.Views.Groups;
public partial class DeleteExamDialog : Window
{
public DeleteExamDialog() => InitializeComponent();
private void OnDelete(object? sender, RoutedEventArgs e) => Close(true);
private void OnCancel(object? sender, RoutedEventArgs e) => Close(false);
}
@@ -0,0 +1,121 @@
<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.ExamDialog"
x:DataType="vm:ExamDialogViewModel"
Title="{Binding DialogTitle}"
Width="640" Height="680" MinWidth="560" MinHeight="420"
CanResize="True" WindowStartupLocation="CenterOwner">
<Grid RowDefinitions="*,Auto" Margin="24">
<ScrollViewer Grid.Row="0">
<StackPanel Spacing="14" Margin="0,0,12,0">
<TextBlock Text="{Binding DialogTitle}" FontSize="18" FontWeight="SemiBold"/>
<StackPanel Spacing="4">
<TextBlock Text="Titel *" FontSize="12" Opacity="0.7"/>
<TextBox Text="{Binding Title}" PlaceholderText="z.B. 1. Klausur Kinetik"/>
</StackPanel>
<Grid ColumnDefinitions="*,12,*,12,*">
<StackPanel Grid.Column="0" Spacing="4">
<TextBlock Text="Datum *" FontSize="12" Opacity="0.7"/>
<TextBox Text="{Binding DateText}" PlaceholderText="TT.MM.JJJJ"/>
</StackPanel>
<StackPanel Grid.Column="2" Spacing="4">
<TextBlock Text="Fach" FontSize="12" Opacity="0.7"/>
<TextBox Text="{Binding Subject}"/>
</StackPanel>
<StackPanel Grid.Column="4" Spacing="4">
<TextBlock Text="Klausurnummer" FontSize="12" Opacity="0.7"/>
<NumericUpDown Value="{Binding ExamNumber}" Minimum="1" Maximum="20" FormatString="0"
ShowButtonSpinner="False"/>
</StackPanel>
</Grid>
<StackPanel Spacing="4">
<TextBlock Text="Notizen" FontSize="12" Opacity="0.7"/>
<TextBox Text="{Binding Notes}" AcceptsReturn="True" Height="56" TextWrapping="Wrap"/>
</StackPanel>
<Separator Margin="0,4"/>
<Grid ColumnDefinitions="*,Auto">
<TextBlock Grid.Column="0" Text="Aufgaben" FontSize="14" FontWeight="SemiBold" VerticalAlignment="Center"/>
<Button Grid.Column="1" Content=" Aufgabe" Command="{Binding AddTaskCommand}"/>
</Grid>
<CheckBox Content="Gewichtung verwenden" IsChecked="{Binding UseWeighting}"/>
<Grid ColumnDefinitions="*,Auto">
<TextBlock Grid.Column="0" Text="{Binding TotalPointsDisplay}" FontSize="12" Opacity="0.7"
VerticalAlignment="Center"/>
<TextBlock Grid.Column="1" Text="Achtung: noch keine Punkte vergeben" Foreground="Red" FontSize="12"
IsVisible="{Binding TotalPointsWarning}" VerticalAlignment="Center"/>
</Grid>
<ItemsControl ItemsSource="{Binding Tasks}">
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="vm:ExamTaskEditItem">
<Border BorderBrush="{DynamicResource SystemControlForegroundBaseLowBrush}"
BorderThickness="1" CornerRadius="4" Padding="8" Margin="0,0,0,6">
<StackPanel Spacing="6">
<Grid ColumnDefinitions="26,*,Auto,Auto,Auto,Auto,Auto">
<TextBlock Grid.Column="0" Text="{Binding Nr}" VerticalAlignment="Center" Opacity="0.6"/>
<TextBox Grid.Column="1" Text="{Binding Title}" PlaceholderText="Titel der Aufgabe" Margin="0,0,6,0"/>
<NumericUpDown Grid.Column="2" Value="{Binding MaxPoints}" Minimum="0" FormatString="0.##"
Width="72" ShowButtonSpinner="False" Margin="0,0,6,0" ToolTip.Tip="Maximalpunkte"/>
<NumericUpDown Grid.Column="3" Value="{Binding Weight}" Minimum="0" FormatString="0.##"
Width="72" ShowButtonSpinner="False" Margin="0,0,6,0" ToolTip.Tip="Gewichtung"
IsVisible="{Binding ShowWeight}"/>
<Button Grid.Column="4" Content="↑" Command="{Binding MoveUpCommand}" Padding="6,2"
ToolTip.Tip="Nach oben"/>
<Button Grid.Column="5" Content="↓" Command="{Binding MoveDownCommand}" Padding="6,2"
ToolTip.Tip="Nach unten" Margin="4,0,0,0"/>
<Button Grid.Column="6" Content="✕" Command="{Binding RemoveCommand}" Padding="6,2"
ToolTip.Tip="Entfernen" Margin="4,0,0,0"/>
</Grid>
<Button Content="{Binding CompetencySummary}" Command="{Binding ToggleCompetencyPanelCommand}"
HorizontalAlignment="Left" FontSize="11" Padding="6,2"/>
<ItemsControl ItemsSource="{Binding CompetencyTagGroups}" IsVisible="{Binding IsCompetencyPanelOpen}">
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="vm:CompetencyTagGroup">
<StackPanel Margin="12,2">
<TextBlock Text="{Binding DisplayName}" FontSize="11" Opacity="0.6"/>
<ItemsControl ItemsSource="{Binding Items}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="vm:CompetencyTag">
<ToggleButton Content="{Binding Display}" IsChecked="{Binding IsSelected}"
Margin="0,2,6,2" FontSize="11" Padding="6,2"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<TextBlock Text="{Binding ValidationMessage}" Foreground="Red" FontSize="12"
IsVisible="{Binding ValidationMessage, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
</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="{Binding SaveButtonText}"
HorizontalAlignment="Stretch" Click="OnSave"/>
</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 ExamDialog : Window
{
public ExamDialog() => InitializeComponent();
private void OnSave(object? s, RoutedEventArgs e)
{
if (DataContext is ExamDialogViewModel vm && vm.SaveCommand.CanExecute(null))
{
vm.SaveCommand.Execute(null);
if (vm.Result is not null) Close(true);
}
}
private void OnCancel(object? s, RoutedEventArgs e) => Close(false);
}
@@ -2,6 +2,7 @@
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"
xmlns:models="clr-namespace:LehrerApp.Core.Models;assembly=LehrerApp.Core"
x:Class="LehrerApp.Desktop.Views.Groups.GroupDetailView"
x:DataType="vm:GroupDetailViewModel">
@@ -66,17 +67,69 @@
<!-- Tab: Klausuren -->
<ContentPage Header="Klausuren">
<DataGrid ItemsSource="{Binding Exams}"
AutoGenerateColumns="False"
IsReadOnly="True"
GridLinesVisibility="Horizontal"
CanUserReorderColumns="False">
<DataGrid.Columns>
<DataGridTextColumn Header="Datum" Binding="{Binding Date}" Width="110"/>
<DataGridTextColumn Header="Titel" Binding="{Binding Title}" Width="*"/>
<DataGridTextColumn Header="Status" Binding="{Binding StatusLabel}" Width="130"/>
</DataGrid.Columns>
</DataGrid>
<Grid RowDefinitions="Auto,*">
<StackPanel Grid.Row="0" Orientation="Horizontal" Spacing="8" Margin="0,0,0,8"
IsVisible="{Binding SelectedExam, Converter={x:Static ObjectConverters.IsNotNull}}">
<Button Content="Bearbeiten" Command="{Binding EditExamCommand}"/>
<Button Content="Duplizieren" Command="{Binding DuplicateExamCommand}"/>
<SplitButton Content="Status ▸" Command="{Binding AdvanceExamStatusCommand}">
<SplitButton.Flyout>
<MenuFlyout Placement="BottomEdgeAlignedLeft">
<MenuItem Header="Geplant" Command="{Binding SetExamStatusCommand}"
CommandParameter="{x:Static models:ExamStatus.Planned}"/>
<MenuItem Header="Durchgeführt" Command="{Binding SetExamStatusCommand}"
CommandParameter="{x:Static models:ExamStatus.Conducted}"/>
<MenuItem Header="Korrigiert" Command="{Binding SetExamStatusCommand}"
CommandParameter="{x:Static models:ExamStatus.Graded}"/>
<MenuItem Header="Zurückgegeben" Command="{Binding SetExamStatusCommand}"
CommandParameter="{x:Static models:ExamStatus.Returned}"/>
</MenuFlyout>
</SplitButton.Flyout>
</SplitButton>
<Button Content="Löschen" Command="{Binding DeleteExamCommand}"/>
</StackPanel>
<DataGrid Grid.Row="1" ItemsSource="{Binding Exams}"
SelectedItem="{Binding SelectedExam}"
AutoGenerateColumns="False"
IsReadOnly="True"
GridLinesVisibility="Horizontal"
CanUserReorderColumns="False">
<DataGrid.Columns>
<DataGridTextColumn Header="Datum" Binding="{Binding Date}" Width="110"/>
<DataGridTextColumn Header="Titel" Binding="{Binding Title}" Width="*"/>
<DataGridTemplateColumn Header="Status" Width="150">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate x:DataType="vm:ExamSummary">
<Border Background="{Binding StatusColorHex}" CornerRadius="4"
Padding="8,2" HorizontalAlignment="Left">
<TextBlock Text="{Binding StatusLabel}" FontSize="12" Foreground="White"/>
</Border>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
<DataGrid.ContextMenu>
<ContextMenu>
<MenuItem Header="Bearbeiten" Command="{Binding EditExamCommand}"/>
<MenuItem Header="Duplizieren" Command="{Binding DuplicateExamCommand}"/>
<MenuItem Header="Status">
<MenuItem Header="Weiter" Command="{Binding AdvanceExamStatusCommand}"/>
<Separator/>
<MenuItem Header="Geplant" Command="{Binding SetExamStatusCommand}"
CommandParameter="{x:Static models:ExamStatus.Planned}"/>
<MenuItem Header="Durchgeführt" Command="{Binding SetExamStatusCommand}"
CommandParameter="{x:Static models:ExamStatus.Conducted}"/>
<MenuItem Header="Korrigiert" Command="{Binding SetExamStatusCommand}"
CommandParameter="{x:Static models:ExamStatus.Graded}"/>
<MenuItem Header="Zurückgegeben" Command="{Binding SetExamStatusCommand}"
CommandParameter="{x:Static models:ExamStatus.Returned}"/>
</MenuItem>
<Separator/>
<MenuItem Header="Löschen" Command="{Binding DeleteExamCommand}"/>
</ContextMenu>
</DataGrid.ContextMenu>
</DataGrid>
</Grid>
</ContentPage>
<!-- Tab: Noten -->
@@ -1,5 +1,6 @@
using Avalonia.Controls;
using LehrerApp.Core.Interfaces;
using LehrerApp.Core.Models;
using LehrerApp.Desktop.ViewModels.Groups;
using Microsoft.Extensions.DependencyInjection;
@@ -13,7 +14,13 @@ public partial class GroupDetailView : UserControl
{
base.OnDataContextChanged(e);
if (DataContext is GroupDetailViewModel vm)
vm.OnAddStudent = ShowAddStudentDialog;
{
vm.OnAddStudent = ShowAddStudentDialog;
vm.OnAddExam = ShowAddExamDialog;
vm.OnEditExam = ShowEditExamDialog;
vm.OnDuplicateExam = ShowDuplicateExamDialog;
vm.OnConfirmDeleteExam = ShowDeleteExamDialog;
}
}
private async Task<bool> ShowAddStudentDialog()
@@ -32,4 +39,37 @@ public partial class GroupDetailView : UserControl
return await dialog.ShowDialog<bool>(owner);
}
private Task<bool> ShowAddExamDialog(Guid groupId) =>
ShowExamDialog(groupId, editingExam: null, duplicateSource: null);
private Task<bool> ShowEditExamDialog(Exam exam) =>
ShowExamDialog(exam.GroupId, editingExam: exam, duplicateSource: null);
private Task<bool> ShowDuplicateExamDialog(Exam exam) =>
ShowExamDialog(exam.GroupId, editingExam: null, duplicateSource: exam);
private async Task<bool> ShowExamDialog(Guid groupId, Exam? editingExam, Exam? duplicateSource)
{
if (DataContext is not GroupDetailViewModel vm || vm.Group is null) return false;
var dialogVm = new ExamDialogViewModel(
App.Services.GetRequiredService<IExamRepository>(),
App.Services.GetRequiredService<ICompetencyDomainRepository>(),
groupId, vm.Group.SubjectId, vm.Group.GradeLevel, vm.Group.GradingSystem,
vm.Group.Subject ?? "", editingExam, duplicateSource);
var dialog = new ExamDialog { DataContext = dialogVm };
var owner = TopLevel.GetTopLevel(this) as Window;
if (owner is null) return false;
return await dialog.ShowDialog<bool>(owner);
}
private async Task<bool> ShowDeleteExamDialog(ExamSummary exam)
{
var dialog = new DeleteExamDialog { DataContext = exam.Title };
var owner = TopLevel.GetTopLevel(this) as Window;
return owner is not null && await dialog.ShowDialog<bool>(owner);
}
}