164 lines
6.5 KiB
C#
164 lines
6.5 KiB
C#
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<ExamTask> Tasks { get; }
|
|
|
|
public ObservableCollection<ExamResultRow> 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 && !IsMemberAtDate(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));
|
|
|
|
private static bool IsMemberAtDate(GroupMembership membership, DateOnly date) => membership.Period switch
|
|
{
|
|
MembershipPeriod.H1Only => date.Month >= 8 || date.Month <= 1,
|
|
MembershipPeriod.H2Only => date.Month >= 2 && date.Month <= 7,
|
|
MembershipPeriod.Custom => (membership.JoinedAt is null || date >= membership.JoinedAt.Value)
|
|
&& (membership.LeftAt is null || date <= membership.LeftAt.Value),
|
|
_ => true,
|
|
};
|
|
}
|
|
|
|
// ── Zeile im Punkteraster ──────────────────────────────────────────────────
|
|
|
|
public partial class ExamResultRow : ObservableObject
|
|
{
|
|
private readonly Guid _resultId;
|
|
private readonly List<GradingKeyEntry> _gradingKey;
|
|
private readonly double _examMaxPoints;
|
|
private readonly GradingService _grading;
|
|
|
|
public Guid StudentId { get; }
|
|
public string Name { get; }
|
|
|
|
public ObservableCollection<PointsCell> 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<ExamResultRow>? OnChanged { get; set; }
|
|
|
|
public ExamResultRow(Guid studentId, string name, List<ExamTask> tasks, ExamResult? existing,
|
|
List<GradingKeyEntry> 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);
|
|
}
|