244 lines
9.0 KiB
C#
244 lines
9.0 KiB
C#
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 || GroupMembershipService.IsActiveOn(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,
|
||
};
|
||
|
||
}
|
||
|
||
// ── 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 "→";
|
||
}
|
||
}
|