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; namespace LehrerApp.Desktop.ViewModels.Groups; // ── Aggregation zur Mitarbeitsnote (3.2) ───────────────────────────────────── public partial class ParticipationGradeDialogViewModel : ObservableObject { private readonly IParticipationSessionRepository _sessions; private readonly IParticipationRepository _entries; private readonly IParticipationAspectRepository _aspects; private readonly IStudentRepository _students; private readonly IEnrollmentRepository _enrollments; private readonly IGradeRepository _grades; private readonly GradingService _grading; private readonly Guid _groupId; private readonly string _schoolYear; private readonly GradingSystem _gradingSystem; [ObservableProperty] private ParticipationPeriodOption _selectedPeriod; [ObservableProperty] private string _statusMessage = ""; public List PeriodOptions { get; } = [ new(ParticipationPeriod.FullYear, "Gesamtes Schuljahr"), new(ParticipationPeriod.H1, "1. Halbjahr"), new(ParticipationPeriod.H2, "2. Halbjahr"), ]; public ObservableCollection Rows { get; } = []; public ObservableCollection AspectWeights { get; } = []; public ParticipationGradeDialogViewModel( IParticipationSessionRepository sessions, IParticipationRepository entries, IParticipationAspectRepository aspects, IStudentRepository students, IEnrollmentRepository enrollments, IGradeRepository grades, GradingService grading, Guid groupId, string schoolYear, GradingSystem gradingSystem) { _sessions = sessions; _entries = entries; _aspects = aspects; _students = students; _enrollments = enrollments; _grades = grades; _grading = grading; _groupId = groupId; _schoolYear = schoolYear; _gradingSystem = gradingSystem; _selectedPeriod = PeriodOptions[0]; LoadAspectWeights(); Recompute(); } // 3.2.1: Gewichtung je Aspekt konfigurierbar — direkt hier, da es noch keine eigene // Aspekt-Verwaltung (3.1) gibt. Änderungen fließen sofort in die Vorschau ein. private void LoadAspectWeights() { var defaults = _aspects.GetDefaults(); var specific = _aspects.GetByGroup(_groupId); var all = defaults.Concat(specific).ToList(); if (all.Count == 0) all = DefaultParticipationAspects.All.ToList(); foreach (var a in all) { var item = new AspectWeightItem(a, _aspects); item.OnChanged = Recompute; AspectWeights.Add(item); } } partial void OnSelectedPeriodChanged(ParticipationPeriodOption value) => Recompute(); private void Recompute() { Rows.Clear(); StatusMessage = ""; var aspectWeights = AspectWeights.ToDictionary(a => a.Key, a => a.Weight); var sessions = _sessions.GetByGroup(_groupId) .Where(s => InPeriod(s.Date, SelectedPeriod.Period)) .OrderBy(s => s.Date) .ToList(); var studentList = _students.GetByGroup(_groupId, _schoolYear); var enrollmentList = _enrollments.GetByGroupAndYear(_groupId, _schoolYear); foreach (var student in studentList.OrderBy(s => s.LastName).ThenBy(s => s.FirstName)) { var enrollment = enrollmentList.FirstOrDefault(e => e.StudentId == student.Id); var relevantSessions = sessions .Where(s => enrollment is null || IsEnrolledAtDate(enrollment, s.Date)) .ToList(); var points = new List<(DateOnly Date, double Rating)>(); foreach (var session in relevantSessions) { var entry = _entries.GetBySessionAndStudent(session.Id, student.Id); if (entry is null || entry.Ratings.Count == 0) continue; var weightSum = 0.0; var valueSum = 0.0; foreach (var r in entry.Ratings) { var w = aspectWeights.TryGetValue(r.Key, out var aw) ? aw : 1.0; if (w <= 0) continue; valueSum += r.Value * w; weightSum += w; } if (weightSum > 0) points.Add((session.Date, valueSum / weightSum)); } Rows.Add(new ParticipationGradeRow(student.Id, student.FullName, points, _grading, _gradingSystem)); } } // 3.2.4: Übernahme als Grade (Category = Participation). Eine bereits übernommene // Note für denselben Zeitraum wird aktualisiert statt dupliziert (erkannt am Note-Tag). [RelayCommand] private void Apply() { var noteTag = $"Mitarbeit {SelectedPeriod.Label} {_schoolYear}"; var applied = 0; foreach (var row in Rows) { if (row.Grade is null) continue; var grade = _grades.GetByStudentAndGroup(row.StudentId, _groupId) .FirstOrDefault(g => g.Category == GradeCategory.Participation && g.Note == noteTag) ?? new Grade { StudentId = row.StudentId, GroupId = _groupId, SchoolYear = _schoolYear, Category = GradeCategory.Participation, Note = noteTag, }; grade.Value = row.Grade; grade.Date = DateOnly.FromDateTime(DateTime.Today); _grades.Save(grade); applied++; } StatusMessage = applied == 0 ? "Keine Schüler mit Bewertungen im gewählten Zeitraum." : $"{applied} Mitarbeitsnote(n) übernommen."; } private static bool InPeriod(DateOnly date, ParticipationPeriod period) => period switch { ParticipationPeriod.H1 => date.Month >= 8 || date.Month <= 1, ParticipationPeriod.H2 => date.Month >= 2 && date.Month <= 7, _ => true, }; private static bool IsEnrolledAtDate(Enrollment e, DateOnly date) => e.Period switch { EnrollmentPeriod.H1Only => date.Month >= 8 || date.Month <= 1, EnrollmentPeriod.H2Only => date.Month >= 2 && date.Month <= 7, EnrollmentPeriod.Custom => (e.JoinedAt is null || date >= e.JoinedAt.Value) && (e.LeftAt is null || date <= e.LeftAt.Value), _ => true, }; } // ── Gewichtung eines Aspekts (3.2.1) ───────────────────────────────────────── public partial class AspectWeightItem : ObservableObject { private readonly ParticipationAspect _aspect; private readonly IParticipationAspectRepository _repo; public string Key => _aspect.Key; public string Label => _aspect.Label; [ObservableProperty] private double _weight; public Action? OnChanged { get; set; } public AspectWeightItem(ParticipationAspect aspect, IParticipationAspectRepository repo) { _aspect = aspect; _repo = repo; _weight = aspect.Weight; } partial void OnWeightChanged(double value) { _aspect.Weight = value; _repo.Save(_aspect); OnChanged?.Invoke(); } } public enum ParticipationPeriod { FullYear, H1, H2 } public class ParticipationPeriodOption(ParticipationPeriod period, string label) { public ParticipationPeriod Period { get; } = period; public string Label { get; } = label; public override string ToString() => Label; } // ── Zeile: berechnete Mitarbeitsnote pro Schüler ───────────────────────────── public class ParticipationGradeRow { public Guid StudentId { get; } public string Name { get; } public string AverageDisplay { get; } public string? Grade { get; } public string GradeDisplay { get; } public string TrendSymbol { get; } public int SessionCount { get; } public ParticipationGradeRow(Guid studentId, string name, List<(DateOnly Date, double Rating)> points, GradingService grading, GradingSystem system) { StudentId = studentId; Name = name; SessionCount = points.Count; if (points.Count == 0) { // 3.2.3: nicht bewertet ≠ schlecht bewertet — kein Grade-Wert, klar erkennbar. AverageDisplay = "–"; Grade = null; GradeDisplay = "nicht bewertet"; TrendSymbol = ""; return; } var average = points.Average(p => p.Rating); AverageDisplay = average.ToString("0.00", CultureInfo.InvariantCulture); Grade = grading.ParticipationGrade(average, system); GradeDisplay = Grade; TrendSymbol = ComputeTrend(points); } // 3.2.5: einfache Trendanzeige — Vergleich erste vs. zweite Hälfte der Sitzungen. private static string ComputeTrend(List<(DateOnly Date, double Rating)> points) { if (points.Count < 2) return ""; var mid = points.Count / 2; var first = points.Take(mid).Average(p => p.Rating); var second = points.Skip(mid).Average(p => p.Rating); var diff = second - first; if (diff >= 0.4) return "↑"; if (diff <= -0.4) return "↓"; return "→"; } }