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>
This commit is contained in:
2026-08-17 00:12:16 +02:00
co-authored by Claude Sonnet 5
parent f8dd567ef8
commit 16ff4875d8
12 changed files with 502 additions and 97 deletions
@@ -2,6 +2,7 @@ using Avalonia.Controls;
using Avalonia.Controls.Templates;
using Avalonia.Data;
using LehrerApp.Core.Interfaces;
using LehrerApp.Core.Models;
using LehrerApp.Core.Services;
using LehrerApp.Desktop.ViewModels.Groups;
using Microsoft.Extensions.DependencyInjection;
@@ -103,39 +104,76 @@ public partial class ParticipationTabView : UserControl
: row.Cells.ElementAtOrDefault(cellIndex);
if (cell is null) return new TextBlock();
var panel = new StackPanel
{
Orientation = Avalonia.Layout.Orientation.Horizontal,
HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Center,
VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center,
Spacing = 2,
Margin = new Avalonia.Thickness(0, 2),
};
foreach (var (label, val) in new[] { ("−−", -2), ("", -1), ("", 0), ("+", 1), ("++", 2) })
{
var btn = new Button
{
Content = label,
Padding = new Avalonia.Thickness(5, 1),
FontSize = 11,
Opacity = cell.Value == val ? 1.0 : 0.3,
IsEnabled = !isReadOnly,
};
var capturedVal = val;
btn.Click += (_, _) => cell.SetValue(capturedVal);
cell.PropertyChanged += (_, pe) =>
{
if (pe.PropertyName == nameof(RatingCell.Value))
btn.Opacity = cell.Value == capturedVal ? 1.0 : 0.3;
};
panel.Children.Add(btn);
}
return panel;
// Punkte-Aspekte (3.1.3) sind eine freie Zahl 0..MaxPoints statt einer festen
// Stufenauswahl — dafür ein NumericUpDown statt der Stufen-Buttons unten.
return cell.Type == AspectValueType.Points
? BuildPointsCell(cell, isReadOnly)
: BuildStepButtonsCell(cell, isReadOnly);
});
}
private static Control BuildStepButtonsCell(RatingCell cell, bool isReadOnly)
{
var panel = new StackPanel
{
Orientation = Avalonia.Layout.Orientation.Horizontal,
HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Center,
VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center,
Spacing = 2,
Margin = new Avalonia.Thickness(0, 2),
};
foreach (var (val, label) in ParticipationRatingScale.Steps(cell.Type))
{
var btn = new Button
{
Content = label,
Padding = new Avalonia.Thickness(5, 1),
FontSize = 11,
Opacity = cell.Value == val ? 1.0 : 0.3,
IsEnabled = !isReadOnly,
};
var capturedVal = val;
btn.Click += (_, _) => cell.SetValue(capturedVal);
cell.PropertyChanged += (_, pe) =>
{
if (pe.PropertyName == nameof(RatingCell.Value))
btn.Opacity = cell.Value == capturedVal ? 1.0 : 0.3;
};
panel.Children.Add(btn);
}
return panel;
}
private static Control BuildPointsCell(RatingCell cell, bool isReadOnly)
{
var updown = new NumericUpDown
{
Minimum = 0,
Maximum = cell.MaxPoints,
Increment = 1,
FormatString = "0",
Value = cell.Value,
IsEnabled = !isReadOnly,
Width = 72,
HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Center,
VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center,
};
updown.ValueChanged += (_, e) =>
{
var newVal = e.NewValue.HasValue ? (int?)e.NewValue.Value : null;
if (newVal != cell.Value) cell.SetValue(newVal);
};
cell.PropertyChanged += (_, pe) =>
{
if (pe.PropertyName != nameof(RatingCell.Value)) return;
var current = cell.Value.HasValue ? (decimal?)cell.Value.Value : null;
if (updown.Value != current) updown.Value = current;
};
return updown;
}
private static IDataTemplate BuildHomeworkCellTemplate(bool isReadOnly)
{
return new FuncDataTemplate<ParticipationStudentRow>((row, _) =>