Files
LehrerApp/LehrerApp.Desktop/ViewModels/Groups/ExamEvaluationViewModels.cs
T

260 lines
11 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using LehrerApp.Core.Interfaces;
using LehrerApp.Core.Models;
using LehrerApp.Core.Services;
using LehrerApp.Desktop.Services;
using System.Collections.ObjectModel;
using System.Globalization;
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");
public string NiveauLabel => _exam.Niveau.HasValue ? NiveauDisplay.ToName(_exam.Niveau) : "";
[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 csv = new CsvBuilder()
.AddRow("Klausur", _exam.Title)
.AddRow("Datum", _exam.Date.ToString("dd.MM.yyyy", CultureInfo.InvariantCulture))
.AddBlankRow()
.AddRow("Notenspiegel")
.AddRow("Note", "Anzahl", "Anteil");
foreach (var g in GradeDistribution)
csv.AddRow(g.Grade, g.Count, Percent(g.Count, GradedCount));
csv.AddBlankRow()
.AddRow("Durchschnitt", AverageDisplay)
.AddRow("Median", MedianDisplay);
if (!string.IsNullOrEmpty(BelowThresholdLabel1)) csv.AddRow(BelowThresholdLabel1, BelowThresholdDisplay1);
if (!string.IsNullOrEmpty(BelowThresholdLabel2)) csv.AddRow(BelowThresholdLabel2, BelowThresholdDisplay2);
csv.AddRow("Bewertet", GradedCount)
.AddRow("Abwesend", AbsentCount)
.AddBlankRow()
.AddRow("Aufgabenanalyse")
.AddRow("Aufgabe", "Ø Erfüllungsgrad", "Auffällig schwach");
foreach (var t in TaskAnalysis)
csv.AddRow(t.Label, t.AvgPercentDisplay, t.IsWeak ? "ja" : "");
return csv.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;
}
}