Niveaudifferenzierung (E/G/Förder) für Klassen und Kurse

Für Fächer mit Binnendifferenzierung (z.B. Mathematik E/G-Niveau,
Förderniveau) innerhalb derselben Lerngruppe:

- LearningGroup.IsDifferentiated: neue Checkbox in den Stammdaten,
  aktiviert die Niveau-Funktionen optional für Klassen und Kurse.
- Enrollment.Niveau: Niveau-Zuordnung je Schüler innerhalb der Gruppe,
  editierbar über eine neue Spalte im Schüler-Tab (nur sichtbar bei
  differenzierter Gruppe).
- Exam.Niveau: Klausuren können optional einem Niveau zugeordnet
  werden. Workflow nutzt die bestehende Duplizieren-Funktion (1.1.4):
  G-Klausur anlegen, für die E-Variante duplizieren und eigene
  Aufgaben/Punkte vergeben — Niveau wird beim Duplizieren bewusst
  zurückgesetzt, damit nicht versehentlich zwei gleiche entstehen.
- Punkteeingabe und Auswertung filtern automatisch auf die Schüler
  mit passendem Niveau; Klausuren ohne Niveau-Zuordnung verhalten
  sich unverändert (gesamte Gruppe). Beide Dialoge zeigen das Niveau
  als Badge im Titel, die Klausurenliste als eigene Spalte.
- Individuelle Förderklausuren (nur für einen Schüler) laufen
  pragmatisch über die bestehende Abwesend-Markierung der übrigen
  Schüler statt über eine eigene Pro-Schüler-Zuordnung.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-12 02:24:26 +02:00
co-authored by Claude Sonnet 5
parent a22674dc19
commit 482942ce09
13 changed files with 130 additions and 8 deletions
@@ -221,10 +221,21 @@ public partial class GroupDetailViewModel : ObservableObject
foreach (var s in enrolled)
{
enrollments.TryGetValue(s.Id, out var enr);
Students.Add(new StudentSummary(s, enr));
var summary = new StudentSummary(s, enr) { OnChanged = SaveStudentNiveau };
Students.Add(summary);
}
}
private void SaveStudentNiveau(StudentSummary summary)
{
if (Group is null) return;
var enrollment = _enrollments.GetByGroupAndYear(Group.Id, Group.SchoolYear)
.FirstOrDefault(e => e.StudentId == summary.Id);
if (enrollment is null) return;
enrollment.Niveau = summary.Niveau;
_enrollments.Save(enrollment);
}
[RelayCommand]
private async Task AddStudent()
{
@@ -363,12 +374,47 @@ public partial class GroupDetailViewModel : ObservableObject
[RelayCommand] private void Refresh() { if (Group is not null) LoadGroup(Group.Id); }
}
public class StudentSummary
// ── Niveau-Anzeige (Niveaudifferenzierung) ────────────────────────────────────
public static class NiveauDisplay
{
public static string[] Options { get; } = ["", "E-Niveau", "G-Niveau", "Förderniveau"];
public static string ToName(Niveau? n) => n switch
{
Core.Models.Niveau.E => "E-Niveau",
Core.Models.Niveau.G => "G-Niveau",
Core.Models.Niveau.Foerder => "Förderniveau",
_ => "",
};
public static Niveau? FromName(string? name) => name switch
{
"E-Niveau" => Core.Models.Niveau.E,
"G-Niveau" => Core.Models.Niveau.G,
"Förderniveau" => Core.Models.Niveau.Foerder,
_ => null,
};
}
public partial class StudentSummary : ObservableObject
{
public static string[] NiveauOptions => NiveauDisplay.Options;
public Guid Id { get; }
public string FullName { get; }
public string PeriodLabel { get; }
[ObservableProperty] private Niveau? _niveau;
public string NiveauName
{
get => NiveauDisplay.ToName(Niveau);
set => Niveau = NiveauDisplay.FromName(value);
}
public Action<StudentSummary>? OnChanged { get; set; }
public StudentSummary(Core.Models.Student s, Enrollment? e)
{
Id = s.Id;
@@ -380,6 +426,13 @@ public class StudentSummary
EnrollmentPeriod.Custom => BuildCustomLabel(e),
_ => "",
};
_niveau = e?.Niveau;
}
partial void OnNiveauChanged(Niveau? value)
{
OnPropertyChanged(nameof(NiveauName));
OnChanged?.Invoke(this);
}
private static string BuildCustomLabel(Enrollment e)
@@ -400,11 +453,13 @@ public class ExamSummary
public string StatusLabel { get; }
public string StatusColorHex { get; }
public string ReturnedAtDisplay { get; }
public string NiveauLabel { get; }
public ExamSummary(Core.Models.Exam e)
{
Id = e.Id; Title = e.Title; Date = e.Date.ToString("dd.MM.yyyy"); Status = e.Status;
ReturnedAtDisplay = e.ReturnedAt?.ToString("dd.MM.yyyy") ?? "";
NiveauLabel = e.Niveau.HasValue ? NiveauDisplay.ToName(e.Niveau) : "alle";
StatusLabel = e.Status switch
{
ExamStatus.Planned => "Geplant",
@@ -552,6 +607,7 @@ public partial class AddGroupDialogViewModel : ObservableObject
[ObservableProperty] private string _selectedSchoolYear = "";
[ObservableProperty] private int? _hoursPerWeek;
[ObservableProperty] private bool _isOwnClass;
[ObservableProperty] private bool _isDifferentiated;
[ObservableProperty] private string _validationMessage = "";
public List<string> SchoolYears { get; }
@@ -589,6 +645,7 @@ public partial class AddGroupDialogViewModel : ObservableObject
SelectedSchoolYear = group.SchoolYear;
HoursPerWeek = group.HoursPerWeek;
IsOwnClass = group.IsOwnClass;
IsDifferentiated = group.IsDifferentiated;
OnPropertyChanged(nameof(DialogTitle));
OnPropertyChanged(nameof(SaveButtonText));
}
@@ -629,6 +686,7 @@ public partial class AddGroupDialogViewModel : ObservableObject
Result.SchoolYear = SelectedSchoolYear;
Result.HoursPerWeek = HoursPerWeek;
Result.IsOwnClass = IsOwnClass;
Result.IsDifferentiated = IsDifferentiated;
_groups.Save(Result);
if (_editingGroup is not null && _originalSchoolYear != SelectedSchoolYear)