using CommunityToolkit.Mvvm.ComponentModel; using LehrerApp.Core.Interfaces; using LehrerApp.Core.Models; using LehrerApp.Core.Services; using System.Collections.ObjectModel; using System.Globalization; namespace LehrerApp.Desktop.ViewModels.Groups; // ── Punkteeingabe & Korrektur (1.4) ────────────────────────────────────────── public partial class ExamGradingDialogViewModel : ObservableObject { private readonly IExamResultRepository _results; private readonly GradingService _grading; private readonly Exam _exam; private readonly double _examMaxPoints; public string ExamTitle => _exam.Title; public string ExamDateLabel => _exam.Date.ToString("dd.MM.yyyy"); public string NiveauLabel => _exam.Niveau.HasValue ? NiveauDisplay.ToName(_exam.Niveau) : ""; public List Tasks { get; } public ObservableCollection Rows { get; } = []; public ExamGradingDialogViewModel(IExamResultRepository results, IStudentRepository students, IGroupMembershipRepository memberships, GradingService grading, Exam exam, Guid groupId) { _results = results; _grading = grading; _exam = exam; Tasks = exam.Tasks.OrderBy(t => t.Nr).ToList(); _examMaxPoints = Tasks.Sum(t => t.MaxPoints); var enrolled = students.GetByGroup(groupId); var membershipList = memberships.GetByGroup(groupId); var existing = results.GetByExam(exam.Id).ToDictionary(r => r.StudentId); foreach (var s in enrolled.OrderBy(s => s.LastName).ThenBy(s => s.FirstName)) { var membership = membershipList.FirstOrDefault(e => e.StudentId == s.Id); if (membership is not null && !GroupMembershipService.IsActiveOn(membership, exam.Date)) continue; // Niveau-Klausur: nur Schüler mit passendem Niveau zeigen. Klausuren ohne // Niveau-Zuordnung gelten weiterhin für die ganze Gruppe. if (exam.Niveau.HasValue && membership?.Niveau != exam.Niveau) continue; existing.TryGetValue(s.Id, out var result); var row = new ExamResultRow(s.Id, s.FullName, Tasks, result, _exam.GradingKey, _examMaxPoints, _grading); row.OnChanged = SaveRow; Rows.Add(row); } } private void SaveRow(ExamResultRow row) => _results.Save(row.ToModel(_exam.Id)); } // ── Zeile im Punkteraster ────────────────────────────────────────────────── public partial class ExamResultRow : ObservableObject { private readonly Guid _resultId; private readonly List _gradingKey; private readonly double _examMaxPoints; private readonly GradingService _grading; public Guid StudentId { get; } public string Name { get; } public ObservableCollection Cells { get; } = []; [ObservableProperty] private bool _absent; [ObservableProperty] private string _comment = ""; [ObservableProperty] private double _totalPoints; [ObservableProperty] private string _gradeDisplay = ""; public string TotalPointsDisplay => TotalPoints.ToString("0.##", CultureInfo.InvariantCulture); public Action? OnChanged { get; set; } public ExamResultRow(Guid studentId, string name, List tasks, ExamResult? existing, List gradingKey, double examMaxPoints, GradingService grading) { StudentId = studentId; Name = name; _gradingKey = gradingKey; _examMaxPoints = examMaxPoints; _grading = grading; _resultId = existing?.Id ?? Guid.NewGuid(); _absent = existing?.Absent ?? false; _comment = existing?.Comment ?? ""; for (var i = 0; i < tasks.Count; i++) { double? pts = existing is not null && i < existing.Points.Count ? existing.Points[i] : null; var cell = new PointsCell(tasks[i].MaxPoints, pts); cell.OnChanged = () => { RecomputeTotals(); OnChanged?.Invoke(this); }; Cells.Add(cell); } RecomputeTotals(); } partial void OnAbsentChanged(bool value) { RecomputeTotals(); OnChanged?.Invoke(this); } partial void OnCommentChanged(string value) => OnChanged?.Invoke(this); private void RecomputeTotals() { TotalPoints = Cells.Sum(c => c.IsInvalid ? 0 : (c.Value ?? 0)); OnPropertyChanged(nameof(TotalPointsDisplay)); GradeDisplay = Absent ? "abwesend" : _grading.CalculateGrade(TotalPoints, _examMaxPoints, _gradingKey); } public ExamResult ToModel(Guid examId) => new() { Id = _resultId, ExamId = examId, StudentId = StudentId, Points = Cells.Select(c => c.Value ?? 0).ToList(), TotalPoints = TotalPoints, Grade = Absent ? null : GradeDisplay, Absent = Absent, Comment = string.IsNullOrWhiteSpace(Comment) ? null : Comment.Trim(), }; } // ── Eine Punktezelle ───────────────────────────────────────────────────────── public partial class PointsCell : ObservableObject { public double MaxPoints { get; } [ObservableProperty] private double? _value; [ObservableProperty] private bool _isInvalid; public Action? OnChanged { get; set; } public PointsCell(double maxPoints, double? value) { MaxPoints = maxPoints; _value = value; _isInvalid = IsOutOfRange(value); } /// Setzt den Wert; bei Punkten außerhalb 0..Maximalpunkte wird nur rot markiert, /// aber nicht gespeichert (1.4.5) — OnChanged (und damit Autosave) wird dann nicht ausgelöst. public void TrySetValue(double? value) { Value = value; IsInvalid = IsOutOfRange(value); if (!IsInvalid) OnChanged?.Invoke(); } private bool IsOutOfRange(double? value) => value.HasValue && (value.Value < 0 || value.Value > MaxPoints + 0.0001); }