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:
@@ -0,0 +1,258 @@
|
|||||||
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
using CommunityToolkit.Mvvm.Input;
|
||||||
|
using LehrerApp.Core.Interfaces;
|
||||||
|
using LehrerApp.Core.Models;
|
||||||
|
using LehrerApp.Core.Services;
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace LehrerApp.Desktop.ViewModels.Groups;
|
||||||
|
|
||||||
|
// ── Klausurauswertung (1.5) ───────────────────────────────────────────────────
|
||||||
|
|
||||||
|
public partial class ExamEvaluationDialogViewModel : ObservableObject
|
||||||
|
{
|
||||||
|
private readonly IExamRepository _exams;
|
||||||
|
private readonly IExamResultRepository _results;
|
||||||
|
private readonly GradingService _grading;
|
||||||
|
private readonly Exam _exam;
|
||||||
|
private readonly GradingSystem _gradingSystem;
|
||||||
|
private readonly double _examMaxPoints;
|
||||||
|
private List<ExamResult> _gradedResults = [];
|
||||||
|
|
||||||
|
public string ExamTitle => _exam.Title;
|
||||||
|
public string ExamDateLabel => _exam.Date.ToString("dd.MM.yyyy");
|
||||||
|
|
||||||
|
[ObservableProperty] private int _gradedCount;
|
||||||
|
[ObservableProperty] private int _absentCount;
|
||||||
|
[ObservableProperty] private string _averageDisplay = "–";
|
||||||
|
[ObservableProperty] private string _medianDisplay = "–";
|
||||||
|
[ObservableProperty] private string _belowThresholdLabel1 = "";
|
||||||
|
[ObservableProperty] private string _belowThresholdDisplay1 = "";
|
||||||
|
[ObservableProperty] private string _belowThresholdLabel2 = "";
|
||||||
|
[ObservableProperty] private string _belowThresholdDisplay2 = "";
|
||||||
|
[ObservableProperty] private string _gradingKeyValidation = "";
|
||||||
|
[ObservableProperty] private string _gradingKeyStatus = "";
|
||||||
|
|
||||||
|
public ObservableCollection<GradeBarItem> GradeDistribution { get; } = [];
|
||||||
|
public ObservableCollection<TaskAnalysisItem> TaskAnalysis { get; } = [];
|
||||||
|
public ObservableCollection<GradingKeyEntryEditItem> GradingKeyEntries { get; } = [];
|
||||||
|
|
||||||
|
public ExamEvaluationDialogViewModel(IExamRepository exams, IExamResultRepository results,
|
||||||
|
GradingService grading, Exam exam, GradingSystem gradingSystem)
|
||||||
|
{
|
||||||
|
_exams = exams; _results = results; _grading = grading; _exam = exam;
|
||||||
|
_gradingSystem = gradingSystem;
|
||||||
|
_examMaxPoints = exam.Tasks.Sum(t => t.MaxPoints);
|
||||||
|
|
||||||
|
foreach (var e in exam.GradingKey.OrderByDescending(e => e.MinPercent))
|
||||||
|
AddGradingKeyRowInternal(e.Grade, e.MinPercent);
|
||||||
|
|
||||||
|
LoadResults();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void LoadResults()
|
||||||
|
{
|
||||||
|
var all = _results.GetByExam(_exam.Id);
|
||||||
|
AbsentCount = all.Count(r => r.Absent);
|
||||||
|
_gradedResults = all.Where(r => !r.Absent).ToList();
|
||||||
|
GradedCount = _gradedResults.Count;
|
||||||
|
Recompute();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Notenschlüssel-Editor (1.5.3) ────────────────────────────────────────
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private void AddGradingKeyRow() => AddGradingKeyRowInternal("", 0);
|
||||||
|
|
||||||
|
private void AddGradingKeyRowInternal(string grade, double minPercent)
|
||||||
|
{
|
||||||
|
var item = new GradingKeyEntryEditItem
|
||||||
|
{
|
||||||
|
Grade = grade,
|
||||||
|
MinPercent = minPercent,
|
||||||
|
OnChanged = OnGradingKeyRowChanged,
|
||||||
|
OnRemove = RemoveGradingKeyRow,
|
||||||
|
};
|
||||||
|
GradingKeyEntries.Add(item);
|
||||||
|
RecomputeGradingKeyAbsolutes();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnGradingKeyRowChanged()
|
||||||
|
{
|
||||||
|
RecomputeGradingKeyAbsolutes();
|
||||||
|
Recompute();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RemoveGradingKeyRow(GradingKeyEntryEditItem item)
|
||||||
|
{
|
||||||
|
GradingKeyEntries.Remove(item);
|
||||||
|
RecomputeGradingKeyAbsolutes();
|
||||||
|
Recompute();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RecomputeGradingKeyAbsolutes()
|
||||||
|
{
|
||||||
|
foreach (var e in GradingKeyEntries)
|
||||||
|
e.AbsolutePointsDisplay = _examMaxPoints <= 0
|
||||||
|
? "–"
|
||||||
|
: $"ab {(e.MinPercent / 100.0 * _examMaxPoints).ToString("0.##", CultureInfo.InvariantCulture)} " +
|
||||||
|
$"von {_examMaxPoints.ToString("0.##", CultureInfo.InvariantCulture)} Punkten";
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<GradingKeyEntry> BuildCurrentGradingKey() => GradingKeyEntries
|
||||||
|
.Select(e => new GradingKeyEntry { Grade = e.Grade.Trim(), MinPercent = e.MinPercent })
|
||||||
|
.OrderByDescending(e => e.MinPercent)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private void ApplyGradingKey()
|
||||||
|
{
|
||||||
|
var key = BuildCurrentGradingKey();
|
||||||
|
var error = _grading.ValidateGradingKey(key);
|
||||||
|
if (error is not null) { GradingKeyValidation = error; GradingKeyStatus = ""; return; }
|
||||||
|
GradingKeyValidation = "";
|
||||||
|
_exam.GradingKey = key;
|
||||||
|
_exams.Save(_exam);
|
||||||
|
GradingKeyStatus = "Notenschlüssel für diese Klausur übernommen.";
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Auswertung neu berechnen ──────────────────────────────────────────────
|
||||||
|
|
||||||
|
private void Recompute()
|
||||||
|
{
|
||||||
|
var currentKey = BuildCurrentGradingKey();
|
||||||
|
GradingKeyValidation = _grading.ValidateGradingKey(currentKey) ?? "";
|
||||||
|
|
||||||
|
var grades = _gradedResults
|
||||||
|
.Select(r => _grading.CalculateGrade(r.TotalPoints, _examMaxPoints, currentKey))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
GradeDistribution.Clear();
|
||||||
|
var counts = grades.GroupBy(g => g).ToDictionary(g => g.Key, g => g.Count());
|
||||||
|
var maxCount = counts.Count == 0 ? 0 : counts.Values.Max();
|
||||||
|
foreach (var entry in currentKey.OrderByDescending(e => e.MinPercent))
|
||||||
|
{
|
||||||
|
counts.TryGetValue(entry.Grade, out var count);
|
||||||
|
GradeDistribution.Add(new GradeBarItem(entry.Grade, count, grades.Count, maxCount));
|
||||||
|
}
|
||||||
|
|
||||||
|
AverageDisplay = grades.Count == 0
|
||||||
|
? "–"
|
||||||
|
: _grading.WeightedAverage(grades.Select(g => (Grade: g, Weight: 1.0)).ToList())
|
||||||
|
.ToString("0.00", CultureInfo.InvariantCulture);
|
||||||
|
|
||||||
|
var numeric = grades.Select(g => int.TryParse(g, out var n) ? (int?)n : null)
|
||||||
|
.Where(n => n.HasValue).Select(n => n!.Value)
|
||||||
|
.OrderBy(n => n).ToList();
|
||||||
|
MedianDisplay = numeric.Count == 0 ? "–" : ComputeMedian(numeric).ToString("0.##", CultureInfo.InvariantCulture);
|
||||||
|
|
||||||
|
if (_gradingSystem == GradingSystem.Points0To15)
|
||||||
|
{
|
||||||
|
BelowThresholdLabel1 = "Anteil < 5 Punkte";
|
||||||
|
BelowThresholdDisplay1 = Percent(numeric.Count(n => n < 5), numeric.Count);
|
||||||
|
BelowThresholdLabel2 = "Anteil < 4 Punkte";
|
||||||
|
BelowThresholdDisplay2 = Percent(numeric.Count(n => n < 4), numeric.Count);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
BelowThresholdLabel1 = "Anteil nicht ausreichend (Note 5/6)";
|
||||||
|
BelowThresholdDisplay1 = Percent(numeric.Count(n => n >= 5), numeric.Count);
|
||||||
|
BelowThresholdLabel2 = "";
|
||||||
|
BelowThresholdDisplay2 = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
RecomputeTaskAnalysis();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RecomputeTaskAnalysis()
|
||||||
|
{
|
||||||
|
TaskAnalysis.Clear();
|
||||||
|
var tasks = _exam.Tasks.OrderBy(t => t.Nr).ToList();
|
||||||
|
for (var i = 0; i < tasks.Count; i++)
|
||||||
|
{
|
||||||
|
var task = tasks[i];
|
||||||
|
var achieved = _gradedResults.Where(r => i < r.Points.Count).Select(r => r.Points[i]).ToList();
|
||||||
|
var avgPercent = task.MaxPoints <= 0 || achieved.Count == 0
|
||||||
|
? 0
|
||||||
|
: achieved.Average() / task.MaxPoints * 100.0;
|
||||||
|
TaskAnalysis.Add(new TaskAnalysisItem(task.Nr, task.Title, avgPercent));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double ComputeMedian(List<int> sorted)
|
||||||
|
{
|
||||||
|
var n = sorted.Count;
|
||||||
|
return n % 2 == 1 ? sorted[n / 2] : (sorted[n / 2 - 1] + sorted[n / 2]) / 2.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string Percent(int count, int total) =>
|
||||||
|
total == 0 ? "–" : $"{(count * 100.0 / total).ToString("0.#", CultureInfo.InvariantCulture)} %";
|
||||||
|
|
||||||
|
// ── Export (1.5.4) ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
public string ExportCsv()
|
||||||
|
{
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
sb.AppendLine($"Klausur;{_exam.Title}");
|
||||||
|
sb.AppendLine($"Datum;{_exam.Date:dd.MM.yyyy}");
|
||||||
|
sb.AppendLine();
|
||||||
|
sb.AppendLine("Notenspiegel");
|
||||||
|
sb.AppendLine("Note;Anzahl;Anteil");
|
||||||
|
foreach (var g in GradeDistribution)
|
||||||
|
sb.AppendLine($"{g.Grade};{g.Count};{Percent(g.Count, GradedCount)}");
|
||||||
|
sb.AppendLine();
|
||||||
|
sb.AppendLine($"Durchschnitt;{AverageDisplay}");
|
||||||
|
sb.AppendLine($"Median;{MedianDisplay}");
|
||||||
|
if (!string.IsNullOrEmpty(BelowThresholdLabel1)) sb.AppendLine($"{BelowThresholdLabel1};{BelowThresholdDisplay1}");
|
||||||
|
if (!string.IsNullOrEmpty(BelowThresholdLabel2)) sb.AppendLine($"{BelowThresholdLabel2};{BelowThresholdDisplay2}");
|
||||||
|
sb.AppendLine($"Bewertet;{GradedCount}");
|
||||||
|
sb.AppendLine($"Abwesend;{AbsentCount}");
|
||||||
|
sb.AppendLine();
|
||||||
|
sb.AppendLine("Aufgabenanalyse");
|
||||||
|
sb.AppendLine("Aufgabe;Ø Erfüllungsgrad;Auffällig schwach");
|
||||||
|
foreach (var t in TaskAnalysis)
|
||||||
|
sb.AppendLine($"{t.Label};{t.AvgPercentDisplay};{(t.IsWeak ? "ja" : "")}");
|
||||||
|
return sb.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Notenspiegel-Balken ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
public class GradeBarItem
|
||||||
|
{
|
||||||
|
public string Grade { get; }
|
||||||
|
public int Count { get; }
|
||||||
|
public string CountDisplay { get; }
|
||||||
|
public double BarWidth { get; }
|
||||||
|
|
||||||
|
public GradeBarItem(string grade, int count, int totalGraded, int maxCount)
|
||||||
|
{
|
||||||
|
Grade = grade;
|
||||||
|
Count = count;
|
||||||
|
var percent = totalGraded == 0 ? 0 : count * 100.0 / totalGraded;
|
||||||
|
CountDisplay = $"{count} ({percent.ToString("0.#", CultureInfo.InvariantCulture)} %)";
|
||||||
|
BarWidth = maxCount <= 0 ? 0 : count * 200.0 / maxCount;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Aufgabenanalyse-Zeile ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
public class TaskAnalysisItem
|
||||||
|
{
|
||||||
|
public string Label { get; }
|
||||||
|
public double AvgPercent { get; }
|
||||||
|
public string AvgPercentDisplay { get; }
|
||||||
|
public bool IsWeak { get; }
|
||||||
|
public double BarWidth { get; }
|
||||||
|
|
||||||
|
public TaskAnalysisItem(int nr, string? title, double avgPercent)
|
||||||
|
{
|
||||||
|
Label = string.IsNullOrWhiteSpace(title) ? $"Aufgabe {nr}" : $"{nr}. {title}";
|
||||||
|
AvgPercent = avgPercent;
|
||||||
|
AvgPercentDisplay = $"{avgPercent.ToString("0.#", CultureInfo.InvariantCulture)} %";
|
||||||
|
IsWeak = avgPercent < 50.0;
|
||||||
|
BarWidth = Math.Clamp(avgPercent, 0, 100) * 2.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -177,6 +177,7 @@ public partial class GroupDetailViewModel : ObservableObject
|
|||||||
public Func<Exam, Task<bool>>? OnDuplicateExam { get; set; }
|
public Func<Exam, Task<bool>>? OnDuplicateExam { get; set; }
|
||||||
public Func<ExamSummary, Task<bool>>? OnConfirmDeleteExam { get; set; }
|
public Func<ExamSummary, Task<bool>>? OnConfirmDeleteExam { get; set; }
|
||||||
public Func<Exam, Task>? OnGradeExam { get; set; }
|
public Func<Exam, Task>? OnGradeExam { get; set; }
|
||||||
|
public Func<Exam, Task>? OnEvaluateExam { get; set; }
|
||||||
|
|
||||||
public GroupDetailViewModel(IGroupRepository groups, IStudentRepository students,
|
public GroupDetailViewModel(IGroupRepository groups, IStudentRepository students,
|
||||||
IEnrollmentRepository enrollments, IExamRepository exams, IGradeRepository grades,
|
IEnrollmentRepository enrollments, IExamRepository exams, IGradeRepository grades,
|
||||||
@@ -286,6 +287,15 @@ public partial class GroupDetailViewModel : ObservableObject
|
|||||||
await OnGradeExam(exam);
|
await OnGradeExam(exam);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[RelayCommand(CanExecute = nameof(HasSelectedExam))]
|
||||||
|
private async Task EvaluateExam()
|
||||||
|
{
|
||||||
|
if (SelectedExam is null || OnEvaluateExam is null) return;
|
||||||
|
var exam = _exams.GetById(SelectedExam.Id);
|
||||||
|
if (exam is null) return;
|
||||||
|
await OnEvaluateExam(exam);
|
||||||
|
}
|
||||||
|
|
||||||
[RelayCommand(CanExecute = nameof(HasSelectedExam))]
|
[RelayCommand(CanExecute = nameof(HasSelectedExam))]
|
||||||
private async Task DuplicateExam()
|
private async Task DuplicateExam()
|
||||||
{
|
{
|
||||||
@@ -341,6 +351,7 @@ public partial class GroupDetailViewModel : ObservableObject
|
|||||||
{
|
{
|
||||||
EditExamCommand.NotifyCanExecuteChanged();
|
EditExamCommand.NotifyCanExecuteChanged();
|
||||||
GradeExamCommand.NotifyCanExecuteChanged();
|
GradeExamCommand.NotifyCanExecuteChanged();
|
||||||
|
EvaluateExamCommand.NotifyCanExecuteChanged();
|
||||||
DuplicateExamCommand.NotifyCanExecuteChanged();
|
DuplicateExamCommand.NotifyCanExecuteChanged();
|
||||||
DeleteExamCommand.NotifyCanExecuteChanged();
|
DeleteExamCommand.NotifyCanExecuteChanged();
|
||||||
AdvanceExamStatusCommand.NotifyCanExecuteChanged();
|
AdvanceExamStatusCommand.NotifyCanExecuteChanged();
|
||||||
|
|||||||
@@ -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 "Übernehmen" 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"
|
<StackPanel Grid.Row="0" Orientation="Horizontal" Spacing="8" Margin="0,0,0,8"
|
||||||
IsVisible="{Binding SelectedExam, Converter={x:Static ObjectConverters.IsNotNull}}">
|
IsVisible="{Binding SelectedExam, Converter={x:Static ObjectConverters.IsNotNull}}">
|
||||||
<Button Content="Punkte eingeben" Command="{Binding GradeExamCommand}"/>
|
<Button Content="Punkte eingeben" Command="{Binding GradeExamCommand}"/>
|
||||||
|
<Button Content="Auswertung" Command="{Binding EvaluateExamCommand}"/>
|
||||||
<Button Content="Bearbeiten" Command="{Binding EditExamCommand}"/>
|
<Button Content="Bearbeiten" Command="{Binding EditExamCommand}"/>
|
||||||
<Button Content="Duplizieren" Command="{Binding DuplicateExamCommand}"/>
|
<Button Content="Duplizieren" Command="{Binding DuplicateExamCommand}"/>
|
||||||
<SplitButton Content="Status ▸" Command="{Binding AdvanceExamStatusCommand}">
|
<SplitButton Content="Status ▸" Command="{Binding AdvanceExamStatusCommand}">
|
||||||
@@ -113,6 +114,7 @@
|
|||||||
<DataGrid.ContextMenu>
|
<DataGrid.ContextMenu>
|
||||||
<ContextMenu>
|
<ContextMenu>
|
||||||
<MenuItem Header="Punkte eingeben" Command="{Binding GradeExamCommand}"/>
|
<MenuItem Header="Punkte eingeben" Command="{Binding GradeExamCommand}"/>
|
||||||
|
<MenuItem Header="Auswertung" Command="{Binding EvaluateExamCommand}"/>
|
||||||
<MenuItem Header="Bearbeiten" Command="{Binding EditExamCommand}"/>
|
<MenuItem Header="Bearbeiten" Command="{Binding EditExamCommand}"/>
|
||||||
<MenuItem Header="Duplizieren" Command="{Binding DuplicateExamCommand}"/>
|
<MenuItem Header="Duplizieren" Command="{Binding DuplicateExamCommand}"/>
|
||||||
<MenuItem Header="Status">
|
<MenuItem Header="Status">
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ public partial class GroupDetailView : UserControl
|
|||||||
vm.OnDuplicateExam = ShowDuplicateExamDialog;
|
vm.OnDuplicateExam = ShowDuplicateExamDialog;
|
||||||
vm.OnConfirmDeleteExam = ShowDeleteExamDialog;
|
vm.OnConfirmDeleteExam = ShowDeleteExamDialog;
|
||||||
vm.OnGradeExam = ShowGradeExamDialog;
|
vm.OnGradeExam = ShowGradeExamDialog;
|
||||||
|
vm.OnEvaluateExam = ShowEvaluateExamDialog;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -92,4 +93,19 @@ public partial class GroupDetailView : UserControl
|
|||||||
var owner = TopLevel.GetTopLevel(this) as Window;
|
var owner = TopLevel.GetTopLevel(this) as Window;
|
||||||
if (owner is not null) await dialog.ShowDialog(owner);
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -75,12 +75,18 @@ Modelle `Exam`, `ExamTask`, `GradingKeyEntry`, `ExamResult` existieren bereits i
|
|||||||
- [x] **1.4.6** Autosave nach jeder Zelle (kein expliziter Speichern-Button nötig).
|
- [x] **1.4.6** Autosave nach jeder Zelle (kein expliziter Speichern-Button nötig).
|
||||||
|
|
||||||
### 1.5 Klausurauswertung
|
### 1.5 Klausurauswertung
|
||||||
- [ ] **1.5.1** Notenspiegel (Häufigkeitsverteilung als Balken), Durchschnitt, Median,
|
- [x] **1.5.1** Notenspiegel (Häufigkeitsverteilung als Balken), Durchschnitt, Median,
|
||||||
Anteil unter 4 / unter 5 Punkten.
|
Anteil unter 4 / unter 5 Punkten. Umgesetzt als neuer Dialog `ExamEvaluationDialog`,
|
||||||
- [ ] **1.5.2** Aufgabenanalyse: durchschnittlicher Erfüllungsgrad pro Aufgabe in Prozent,
|
erreichbar über "Auswertung" im Klausuren-Tab. Bei Notensystem 1–6 zeigt der Schwellenwert
|
||||||
Kennzeichnung auffällig schwacher Aufgaben.
|
sinngemäß "Anteil nicht ausreichend (Note 5/6)" statt der Punktegrenzen. Abwesende werden
|
||||||
- [ ] **1.5.3** Notenschlüssel nachträglich verschieben und Auswirkung sofort im Notenspiegel sehen.
|
aus allen Statistiken ausgeschlossen (löst damit auch den Hinweis aus 1.4.3 ein).
|
||||||
- [ ] **1.5.4** Export der Auswertung (siehe 11.2).
|
- [x] **1.5.2** Aufgabenanalyse: durchschnittlicher Erfüllungsgrad pro Aufgabe in Prozent,
|
||||||
|
Kennzeichnung auffällig schwacher Aufgaben (Ø < 50 %, rot markiert).
|
||||||
|
- [x] **1.5.3** Notenschlüssel nachträglich verschieben und Auswirkung sofort im Notenspiegel sehen —
|
||||||
|
Änderungen wirken sich live aus, erst "Übernehmen" schreibt sie in die Klausur zurück.
|
||||||
|
- [x] **1.5.4** Export der Auswertung — als eigenständiger CSV-Export direkt im Dialog umgesetzt
|
||||||
|
(analog zum bestehenden JSON-Export der Kompetenzkataloge), nicht über eine gemeinsame
|
||||||
|
Export-Infrastruktur, da Kapitel 11 ("Bisher nicht vorhanden — komplett neu") noch aussteht.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user