Files
LehrerApp/LehrerApp.Desktop/ViewModels/Groups/ParticipationGradeViewModels.cs
T
2026-08-12 17:45:02 +02:00

252 lines
9.5 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 IGroupMembershipRepository _memberships;
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<ParticipationPeriodOption> PeriodOptions { get; } =
[
new(ParticipationPeriod.FullYear, "Gesamtes Schuljahr"),
new(ParticipationPeriod.H1, "1. Halbjahr"),
new(ParticipationPeriod.H2, "2. Halbjahr"),
];
public ObservableCollection<ParticipationGradeRow> Rows { get; } = [];
public ObservableCollection<AspectWeightItem> AspectWeights { get; } = [];
public ParticipationGradeDialogViewModel(
IParticipationSessionRepository sessions, IParticipationRepository entries,
IParticipationAspectRepository aspects, IStudentRepository students,
IGroupMembershipRepository memberships, IGradeRepository grades, GradingService grading,
Guid groupId, string schoolYear, GradingSystem gradingSystem)
{
_sessions = sessions; _entries = entries; _aspects = aspects;
_students = students; _memberships = memberships; _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);
var membershipList = _memberships.GetByGroup(_groupId);
foreach (var student in studentList.OrderBy(s => s.LastName).ThenBy(s => s.FirstName))
{
var membership = membershipList.FirstOrDefault(e => e.StudentId == student.Id);
var relevantSessions = sessions
.Where(s => membership is null || IsMemberAtDate(membership, 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,
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 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,
};
}
// ── 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 "→";
}
}