Files
adminandClaude Sonnet 5 16ff4875d8 3.1.3: Aspekt-Typen Scale3/Binary/Points in Raster und Dialog unterstützt
Neue Klasse ParticipationRatingScale (Core) ist die einzige Quelle für
Rohwerte je Aspekt-Typ. Scale3/Binary liegen bewusst direkt auf derselben
-2..+2-Achse wie Scale5 (nur mit weniger Zwischenschritten), damit die
bestehende Gewichtung/Mittelwertbildung zur Mitarbeitsnote unverändert
kompatibel bleibt. Points ist grundverschieden (echter Zählwert 0..MaxPoints,
neues Feld auf ParticipationAspect) und wird nur zur Aggregation linear auf
dieselbe Achse normiert.

Ohne diese Normierung hätte ein Punkte-Aspekt die Mitarbeitsnote verfälscht:
drei Stellen summierten bisher den Rohwert direkt (ParticipationGradeDialog-
ViewModel.Recompute, ParticipationWizardViewModels.WeightedRating und
.ComputeSuggestion) - alle drei sind jetzt auf die Normierung umgestellt.

Raster: Punkte-Aspekte zeigen ein NumericUpDown statt fester Stufen-Buttons.
Schnelleingabe-Dialog: Zifferntasten/+/- sind jetzt typabhängig, Legende
zeigt live die für den aktuellen Aspekt gültigen Tasten. Aspekt-Verwaltung
um "Max. Punkte"-Feld ergänzt (nur bei Typ "Punkte" sichtbar).

Bewusst nicht angefasst: die Trendlinien-Visualisierung im
Mitarbeits-Assistenten bleibt fest auf die drei Standardaspekte
zugeschnitten - eigener, größerer Umbau.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-17 00:12:16 +02:00

251 lines
9.5 KiB
C#
Raw Permalink 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; } = [];
// Für die Normierung von Punkte-Aspekten (3.1.3) auf die gemeinsame -2..+2-Achse vor der
// Gewichtung — AspectWeightItem kennt selbst nur Key/Label/Weight, nicht ValueType/MaxPoints.
private readonly Dictionary<string, ParticipationAspect> _aspectsByKey = [];
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)
{
_aspectsByKey[a.Key] = a;
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;
var normalized = _aspectsByKey.TryGetValue(r.Key, out var aspect)
? ParticipationRatingScale.Normalize(aspect.ValueType, r.Value, aspect.MaxPoints)
: r.Value;
valueSum += normalized * 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 "→";
}
}