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; // ── Zeugnisnote (2.4) ───────────────────────────────────────────────────────── public partial class ReportGradeDialogViewModel : ObservableObject { private readonly IGradeRepository _grades; private readonly IExamRepository _exams; private readonly IExamResultRepository _results; private readonly IStudentRepository _students; private readonly IGroupMembershipRepository _memberships; private readonly IGradingSchemeRepository _schemes; private readonly IReportGradeRepository _reportGrades; private readonly GradingService _grading; private readonly Guid _groupId; private readonly GroupType _groupType; private readonly GradingSystem _gradingSystem; private readonly string _groupLabel; private readonly string _schoolYear; [ObservableProperty] private ParticipationPeriodOption _selectedPeriod; [ObservableProperty] private RoundingRule _roundingRule = RoundingRule.Commercial; [ObservableProperty] private string _schemeSummary = ""; public string GroupLabel => _groupLabel; public List PeriodOptions { get; } = [ new(ParticipationPeriod.FullYear, "Gesamtes Schuljahr"), new(ParticipationPeriod.H1, "1. Halbjahr"), new(ParticipationPeriod.H2, "2. Halbjahr"), ]; public string[] RoundingOptions { get; } = RoundingRuleDisplay.Options; // Für die ComboBox: deutsche Anzeige statt des rohen Enum-Namens. public string RoundingRuleName { get => RoundingRuleDisplay.Label(RoundingRule); set => RoundingRule = RoundingRuleDisplay.FromLabel(value); } public ObservableCollection Rows { get; } = []; public ReportGradeDialogViewModel(IGradeRepository grades, IExamRepository exams, IExamResultRepository results, IStudentRepository students, IGroupMembershipRepository memberships, IGradingSchemeRepository schemes, IReportGradeRepository reportGrades, GradingService grading, Guid groupId, GroupType groupType, GradingSystem gradingSystem, string groupLabel, string schoolYear) { _grades = grades; _exams = exams; _results = results; _students = students; _memberships = memberships; _schemes = schemes; _reportGrades = reportGrades; _grading = grading; _groupId = groupId; _groupType = groupType; _gradingSystem = gradingSystem; _groupLabel = groupLabel; _schoolYear = schoolYear; _selectedPeriod = PeriodOptions[0]; Recompute(); } partial void OnSelectedPeriodChanged(ParticipationPeriodOption value) => Recompute(); partial void OnRoundingRuleChanged(RoundingRule value) => Recompute(); private GradingScheme ResolveScheme() => _schemes.GetByGroup(_groupId) ?? _schemes.GetDefaultForType(_groupType) ?? new GradingScheme { ExamsPercent = 50, ParticipationPercent = 40, OtherPercent = 10 }; private void Recompute() { var scheme = ResolveScheme(); SchemeSummary = $"Klausuren {scheme.ExamsPercent:0.#} % · Mitarbeit {scheme.ParticipationPercent:0.#} % · " + $"Sonstige {scheme.OtherPercent:0.#} %"; var period = SelectedPeriod.Period; var periodTag = SelectedPeriod.Label; var (periodFrom, periodTo) = GroupMembershipService.SchoolYearPeriod(_schoolYear, period switch { ParticipationPeriod.H1 => SchoolYearPeriodKind.H1, ParticipationPeriod.H2 => SchoolYearPeriodKind.H2, _ => SchoolYearPeriodKind.FullYear, }); var students = _students.GetByGroup(_groupId); var membershipsByStudent = _memberships.GetByGroup(_groupId).ToDictionary(m => m.StudentId); var exams = _exams.GetByGroup(_groupId).Where(e => e.Date >= periodFrom && e.Date <= periodTo).ToList(); var resultsByExam = exams.ToDictionary(e => e.Id, e => _results.GetByExam(e.Id).ToDictionary(r => r.StudentId)); var allGrades = _grades.GetByGroup(_groupId).Where(g => g.Date >= periodFrom && g.Date <= periodTo).ToList(); Rows.Clear(); foreach (var student in students.OrderBy(s => s.LastName).ThenBy(s => s.FirstName)) { membershipsByStudent.TryGetValue(student.Id, out var membership); if (membership is not null && !GroupMembershipService.Overlaps(membership, periodFrom, periodTo)) continue; var existing = _reportGrades.GetByStudentGroupPeriod(student.Id, _groupId, periodTag); if (existing is { IsLocked: true }) { Rows.Add(ReportGradeRow.FromLocked(student.Id, student.FullName, existing, Save, ToggleLock)); continue; } var examGrades = new List<(string Grade, double Weight)>(); foreach (var exam in exams) { if (membership is not null && !GroupMembershipService.IsActiveOn(membership, exam.Date)) continue; if (exam.Niveau.HasValue && membership?.Niveau != exam.Niveau) continue; if (!resultsByExam[exam.Id].TryGetValue(student.Id, out var result)) continue; if (result.Absent || result.Grade is null) continue; examGrades.Add((result.Grade, 1.0)); } var participationGrades = allGrades .Where(g => g.StudentId == student.Id && g.Category == GradeCategory.Participation && (membership is null || GroupMembershipService.IsActiveOn(membership, g.Date))) .Select(g => (g.Value, g.Weight)).ToList(); var otherGrades = allGrades .Where(g => g.StudentId == student.Id && g.Category != GradeCategory.Participation && (membership is null || GroupMembershipService.IsActiveOn(membership, g.Date))) .Select(g => (g.Value, g.Weight)).ToList(); var calculated = _grading.CalculateReportGrade(examGrades, participationGrades, otherGrades, scheme, _gradingSystem, RoundingRule); Rows.Add(ReportGradeRow.FromCalculated(student.Id, student.FullName, calculated, existing, Save, ToggleLock)); } } private void Save(ReportGradeRow row) { if (!string.IsNullOrWhiteSpace(row.OverrideValue) && string.IsNullOrWhiteSpace(row.OverrideReason)) { row.ValidationMessage = "Für ein manuelles Übersteuern ist eine Begründung Pflicht."; return; } row.ValidationMessage = ""; var record = _reportGrades.GetByStudentGroupPeriod(row.StudentId, _groupId, SelectedPeriod.Label) ?? new ReportGrade { StudentId = row.StudentId, GroupId = _groupId, Period = SelectedPeriod.Label }; record.CalculatedValue = row.CalculatedValue ?? ""; record.OverrideValue = string.IsNullOrWhiteSpace(row.OverrideValue) ? null : row.OverrideValue.Trim(); record.OverrideReason = string.IsNullOrWhiteSpace(row.OverrideReason) ? null : row.OverrideReason.Trim(); _reportGrades.Save(record); row.MarkSaved(); } private void ToggleLock(ReportGradeRow row) { var record = _reportGrades.GetByStudentGroupPeriod(row.StudentId, _groupId, SelectedPeriod.Label); if (record is null) { if (!row.IsLocked) { // Festschreiben ohne vorherigen Save: aktuellen Stand zuerst sichern. Save(row); record = _reportGrades.GetByStudentGroupPeriod(row.StudentId, _groupId, SelectedPeriod.Label); if (record is null) return; } else return; } record.IsLocked = !record.IsLocked; _reportGrades.Save(record); Recompute(); } public string ExportCsv() { var sb = new StringBuilder(); sb.AppendLine($"Zeugnisnoten;{_groupLabel};{SelectedPeriod.Label}"); sb.AppendLine("Schüler;Berechnet;Übersteuert;Begründung;Endnote;Gesperrt"); foreach (var r in Rows) sb.AppendLine($"{r.Name};{r.CalculatedValue};{r.OverrideValue};{r.OverrideReason};{r.FinalDisplay};{(r.IsLocked ? "ja" : "")}"); return sb.ToString(); } } // ── Rundungsregel-Anzeige ───────────────────────────────────────────────────── public static class RoundingRuleDisplay { public static string Label(RoundingRule r) => r switch { RoundingRule.Commercial => "Kaufmännisch", RoundingRule.Pedagogical => "Pädagogisch", _ => r.ToString(), }; public static string[] Options { get; } = [Label(RoundingRule.Commercial), Label(RoundingRule.Pedagogical)]; public static RoundingRule FromLabel(string? label) => label == Label(RoundingRule.Pedagogical) ? RoundingRule.Pedagogical : RoundingRule.Commercial; } // ── Zeile: Zeugnisnote eines Schülers ──────────────────────────────────────── public partial class ReportGradeRow : ObservableObject { public Guid StudentId { get; } public string Name { get; } public string? CalculatedValue { get; private set; } public bool IsLocked { get; private set; } [ObservableProperty] private string? _overrideValue; [ObservableProperty] private string? _overrideReason; [ObservableProperty] private string _validationMessage = ""; public string CalculatedDisplay => CalculatedValue ?? "–"; public string FinalDisplay => !string.IsNullOrWhiteSpace(OverrideValue) ? OverrideValue! : CalculatedDisplay; public string LockLabel => IsLocked ? "Entsperren" : "Festschreiben"; public IRelayCommand SaveCommand { get; } public IRelayCommand ToggleLockCommand { get; } private ReportGradeRow(Guid studentId, string name, string? calculated, ReportGrade? existing, bool locked, Action onSave, Action onToggleLock) { StudentId = studentId; Name = name; CalculatedValue = calculated; IsLocked = locked; _overrideValue = existing?.OverrideValue; _overrideReason = existing?.OverrideReason; SaveCommand = new RelayCommand(() => onSave(this)); ToggleLockCommand = new RelayCommand(() => onToggleLock(this)); } public static ReportGradeRow FromCalculated(Guid studentId, string name, string? calculated, ReportGrade? existing, Action onSave, Action onToggleLock) => new(studentId, name, calculated, existing, existing?.IsLocked ?? false, onSave, onToggleLock); public static ReportGradeRow FromLocked(Guid studentId, string name, ReportGrade locked, Action onSave, Action onToggleLock) => new(studentId, name, locked.CalculatedValue, locked, true, onSave, onToggleLock); public void MarkSaved() { OnPropertyChanged(nameof(FinalDisplay)); } partial void OnOverrideValueChanged(string? value) => OnPropertyChanged(nameof(FinalDisplay)); }