Klausurauswertung (1.5) — Kapitel 1 damit vollständig

Neuer Dialog ExamEvaluationDialog, erreichbar über "Auswertung" im
Klausuren-Tab (Button + Kontextmenü bei ausgewählter Klausur):

- Notenspiegel als Balkendiagramm, Durchschnitt (via bestehendes
  GradingService.WeightedAverage), Median, sowie ein Schwellenwert-
  Anteil ("< 5/4 Punkte" bei Punktesystem, "nicht ausreichend 5/6"
  bei Notensystem 1–6).
- Aufgabenanalyse: durchschnittlicher Erfüllungsgrad je Aufgabe,
  auffällig schwache (Ø < 50 %) rot markiert.
- Notenschlüssel verschieben: gleicher Editor wie im Klausur-Dialog,
  Änderungen wirken sich sofort im Notenspiegel aus; erst
  "Übernehmen" schreibt sie in die Klausur zurück.
- CSV-Export direkt im Dialog, analog zum bestehenden JSON-Export der
  Kompetenzkataloge — ohne die noch fehlende Export-Infrastruktur aus
  Kapitel 11 vorwegzunehmen.
- Abwesende werden konsequent aus allen Statistiken ausgeschlossen
  (löst den in 1.4.3 offen gelassenen Punkt ein).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-12 01:01:30 +02:00
co-authored by Claude Sonnet 5
parent 725de3813f
commit a22674dc19
7 changed files with 478 additions and 6 deletions
@@ -0,0 +1,149 @@
<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.ExamEvaluationDialog"
x:DataType="vm:ExamEvaluationDialogViewModel"
Title="{Binding ExamTitle}"
Width="760" Height="820" MinWidth="600" MinHeight="480"
CanResize="True" WindowStartupLocation="CenterOwner">
<Grid RowDefinitions="*,Auto" Margin="24">
<ScrollViewer Grid.Row="0">
<StackPanel Spacing="16" Margin="0,0,12,0">
<StackPanel Spacing="2">
<TextBlock Text="{Binding ExamTitle}" FontSize="18" FontWeight="SemiBold"/>
<TextBlock Text="{Binding ExamDateLabel}" FontSize="12" Opacity="0.6"/>
</StackPanel>
<!-- Kennzahlen -->
<UniformGrid Columns="3" Rows="2">
<StackPanel Margin="0,0,12,10">
<TextBlock Text="DURCHSCHNITT" FontSize="10" FontWeight="Bold" Opacity="0.5"/>
<TextBlock Text="{Binding AverageDisplay}" FontSize="20" FontWeight="SemiBold"/>
</StackPanel>
<StackPanel Margin="0,0,12,10">
<TextBlock Text="MEDIAN" FontSize="10" FontWeight="Bold" Opacity="0.5"/>
<TextBlock Text="{Binding MedianDisplay}" FontSize="20" FontWeight="SemiBold"/>
</StackPanel>
<StackPanel Margin="0,0,12,10">
<TextBlock Text="BEWERTET / ABWESEND" FontSize="10" FontWeight="Bold" Opacity="0.5"/>
<TextBlock FontSize="20" FontWeight="SemiBold">
<Run Text="{Binding GradedCount}"/>
<Run Text=" / "/>
<Run Text="{Binding AbsentCount}"/>
</TextBlock>
</StackPanel>
<StackPanel Margin="0,0,12,0" IsVisible="{Binding BelowThresholdLabel1, Converter={x:Static StringConverters.IsNotNullOrEmpty}}">
<TextBlock Text="{Binding BelowThresholdLabel1}" FontSize="10" FontWeight="Bold" Opacity="0.5"
TextWrapping="Wrap"/>
<TextBlock Text="{Binding BelowThresholdDisplay1}" FontSize="20" FontWeight="SemiBold"/>
</StackPanel>
<StackPanel Margin="0,0,12,0" IsVisible="{Binding BelowThresholdLabel2, Converter={x:Static StringConverters.IsNotNullOrEmpty}}">
<TextBlock Text="{Binding BelowThresholdLabel2}" FontSize="10" FontWeight="Bold" Opacity="0.5"
TextWrapping="Wrap"/>
<TextBlock Text="{Binding BelowThresholdDisplay2}" FontSize="20" FontWeight="SemiBold"/>
</StackPanel>
</UniformGrid>
<Separator/>
<!-- Notenspiegel -->
<TextBlock Text="Notenspiegel" FontSize="14" FontWeight="SemiBold"/>
<ItemsControl ItemsSource="{Binding GradeDistribution}">
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="vm:GradeBarItem">
<Grid ColumnDefinitions="40,210,*" Margin="0,3">
<TextBlock Grid.Column="0" Text="{Binding Grade}" VerticalAlignment="Center" FontWeight="SemiBold"/>
<Border Grid.Column="1" HorizontalAlignment="Left" Height="16" CornerRadius="3"
Width="{Binding BarWidth}"
Background="{DynamicResource SystemControlBackgroundAccentBrush}"/>
<TextBlock Grid.Column="2" Text="{Binding CountDisplay}" FontSize="12" Opacity="0.7"
VerticalAlignment="Center" Margin="8,0,0,0"/>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<TextBlock Text="Noch keine bewerteten Ergebnisse." Opacity="0.4" FontSize="13"
IsVisible="{Binding !GradedCount}"/>
<Separator/>
<!-- Aufgabenanalyse -->
<TextBlock Text="Aufgabenanalyse" FontSize="14" FontWeight="SemiBold"/>
<ItemsControl ItemsSource="{Binding TaskAnalysis}">
<ItemsControl.Styles>
<Style Selector="Border.taskbar">
<Setter Property="Background" Value="{DynamicResource SystemControlBackgroundAccentBrush}"/>
</Style>
<Style Selector="Border.taskbar.weak">
<Setter Property="Background" Value="#E53935"/>
</Style>
<Style Selector="TextBlock.tasklabel">
<Setter Property="Foreground" Value="{DynamicResource SystemControlForegroundBaseHighBrush}"/>
</Style>
<Style Selector="TextBlock.tasklabel.weak">
<Setter Property="Foreground" Value="#E53935"/>
<Setter Property="FontWeight" Value="SemiBold"/>
</Style>
</ItemsControl.Styles>
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="vm:TaskAnalysisItem">
<Grid ColumnDefinitions="180,210,*" Margin="0,3">
<TextBlock Grid.Column="0" Classes="tasklabel" Classes.weak="{Binding IsWeak}"
Text="{Binding Label}" VerticalAlignment="Center" TextTrimming="CharacterEllipsis"/>
<Border Grid.Column="1" Classes="taskbar" Classes.weak="{Binding IsWeak}"
HorizontalAlignment="Left" Height="16" CornerRadius="3" Width="{Binding BarWidth}"/>
<TextBlock Grid.Column="2" Text="{Binding AvgPercentDisplay}" FontSize="12" Opacity="0.7"
VerticalAlignment="Center" Margin="8,0,0,0"/>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<TextBlock Text="Auffällig schwache Aufgaben (Ø unter 50 %) sind rot markiert." Opacity="0.4" FontSize="11"
IsVisible="{Binding TaskAnalysis.Count}"/>
<Separator/>
<!-- Notenschlüssel verschieben -->
<Grid ColumnDefinitions="*,Auto">
<TextBlock Grid.Column="0" Text="Notenschlüssel verschieben" FontSize="14" FontWeight="SemiBold" VerticalAlignment="Center"/>
<Button Grid.Column="1" Content=" Stufe" Command="{Binding AddGradingKeyRowCommand}"/>
</Grid>
<TextBlock Text="Änderungen wirken sich sofort auf den Notenspiegel oben aus. Erst mit &quot;Übernehmen&quot; werden sie an der Klausur gespeichert."
FontSize="11" Opacity="0.5" TextWrapping="Wrap"/>
<ItemsControl ItemsSource="{Binding GradingKeyEntries}">
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="vm:GradingKeyEntryEditItem">
<Grid ColumnDefinitions="90,90,*,Auto" Margin="0,0,0,4">
<TextBox Grid.Column="0" Text="{Binding Grade}" PlaceholderText="Note" Margin="0,0,6,0"/>
<NumericUpDown Grid.Column="1" Value="{Binding MinPercent}" Minimum="0" Maximum="100"
FormatString="0.##" Width="90" ShowButtonSpinner="False" Margin="0,0,6,0"/>
<TextBlock Grid.Column="2" Text="{Binding AbsolutePointsDisplay}" FontSize="12" Opacity="0.65"
VerticalAlignment="Center"/>
<Button Grid.Column="3" Content="✕" Command="{Binding RemoveCommand}" Padding="6,2"/>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<TextBlock Text="{Binding GradingKeyValidation}" Foreground="Red" FontSize="12"
IsVisible="{Binding GradingKeyValidation, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
<Grid ColumnDefinitions="*,Auto">
<TextBlock Grid.Column="0" Text="{Binding GradingKeyStatus}" Foreground="Green" FontSize="12"
VerticalAlignment="Center"
IsVisible="{Binding GradingKeyStatus, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
<Button Grid.Column="1" Content="Übernehmen" Command="{Binding ApplyGradingKeyCommand}"/>
</Grid>
</StackPanel>
</ScrollViewer>
<Grid Grid.Row="1" ColumnDefinitions="Auto,*,Auto" Margin="0,16,0,0">
<Button Grid.Column="0" Content="Als CSV exportieren" Click="OnExportClick"/>
<Button Grid.Column="2" Content="Fertig" Click="OnClose"/>
</Grid>
</Grid>
</Window>
@@ -0,0 +1,30 @@
using Avalonia.Controls;
using Avalonia.Interactivity;
using Avalonia.Platform.Storage;
using LehrerApp.Desktop.ViewModels.Groups;
namespace LehrerApp.Desktop.Views.Groups;
public partial class ExamEvaluationDialog : Window
{
public ExamEvaluationDialog() => InitializeComponent();
private async void OnExportClick(object? sender, RoutedEventArgs e)
{
if (DataContext is not ExamEvaluationDialogViewModel vm) return;
var topLevel = TopLevel.GetTopLevel(this);
if (topLevel is null) return;
var file = await topLevel.StorageProvider.SaveFilePickerAsync(new FilePickerSaveOptions
{
Title = "Klausurauswertung exportieren",
SuggestedFileName = $"Auswertung_{vm.ExamTitle}.csv",
FileTypeChoices = [new FilePickerFileType("CSV-Dateien") { Patterns = ["*.csv"] }],
});
if (file is null) return;
await File.WriteAllTextAsync(file.Path.LocalPath, vm.ExportCsv());
}
private void OnClose(object? sender, RoutedEventArgs e) => Close();
}
@@ -71,6 +71,7 @@
<StackPanel Grid.Row="0" Orientation="Horizontal" Spacing="8" Margin="0,0,0,8"
IsVisible="{Binding SelectedExam, Converter={x:Static ObjectConverters.IsNotNull}}">
<Button Content="Punkte eingeben" Command="{Binding GradeExamCommand}"/>
<Button Content="Auswertung" Command="{Binding EvaluateExamCommand}"/>
<Button Content="Bearbeiten" Command="{Binding EditExamCommand}"/>
<Button Content="Duplizieren" Command="{Binding DuplicateExamCommand}"/>
<SplitButton Content="Status ▸" Command="{Binding AdvanceExamStatusCommand}">
@@ -113,6 +114,7 @@
<DataGrid.ContextMenu>
<ContextMenu>
<MenuItem Header="Punkte eingeben" Command="{Binding GradeExamCommand}"/>
<MenuItem Header="Auswertung" Command="{Binding EvaluateExamCommand}"/>
<MenuItem Header="Bearbeiten" Command="{Binding EditExamCommand}"/>
<MenuItem Header="Duplizieren" Command="{Binding DuplicateExamCommand}"/>
<MenuItem Header="Status">
@@ -22,6 +22,7 @@ public partial class GroupDetailView : UserControl
vm.OnDuplicateExam = ShowDuplicateExamDialog;
vm.OnConfirmDeleteExam = ShowDeleteExamDialog;
vm.OnGradeExam = ShowGradeExamDialog;
vm.OnEvaluateExam = ShowEvaluateExamDialog;
}
}
@@ -92,4 +93,19 @@ public partial class GroupDetailView : UserControl
var owner = TopLevel.GetTopLevel(this) as Window;
if (owner is not null) await dialog.ShowDialog(owner);
}
private async Task ShowEvaluateExamDialog(Exam exam)
{
if (DataContext is not GroupDetailViewModel vm || vm.Group is null) return;
var dialogVm = new ExamEvaluationDialogViewModel(
App.Services.GetRequiredService<IExamRepository>(),
App.Services.GetRequiredService<IExamResultRepository>(),
App.Services.GetRequiredService<GradingService>(),
exam, vm.Group.GradingSystem);
var dialog = new ExamEvaluationDialog { DataContext = dialogVm };
var owner = TopLevel.GetTopLevel(this) as Window;
if (owner is not null) await dialog.ShowDialog(owner);
}
}