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 _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 GradeDistribution { get; } = []; public ObservableCollection TaskAnalysis { get; } = []; public ObservableCollection 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 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 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; } }