From 482942ce09aeb1d5d5a737d17dc25af029c95c7b Mon Sep 17 00:00:00 2001 From: Sebastian Hedtrich Date: Wed, 12 Aug 2026 02:24:26 +0200 Subject: [PATCH] =?UTF-8?q?Niveaudifferenzierung=20(E/G/F=C3=B6rder)=20f?= =?UTF-8?q?=C3=BCr=20Klassen=20und=20Kurse?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- LehrerApp.Core/Models/Exam.cs | 1 + LehrerApp.Core/Models/LearningGroup.cs | 3 + .../Groups/ExamEvaluationViewModels.cs | 1 + .../Groups/ExamGradingViewModels.cs | 5 ++ .../ViewModels/Groups/ExamViewModels.cs | 11 +++- .../ViewModels/Groups/GroupViewModels.cs | 62 ++++++++++++++++++- .../Views/Groups/AddGroupDialog.axaml | 3 + .../Views/Groups/ExamDialog.axaml | 7 +++ .../Views/Groups/ExamEvaluationDialog.axaml | 8 ++- .../Views/Groups/ExamGradingDialog.axaml | 4 ++ .../Views/Groups/GroupDetailView.axaml | 12 ++++ .../Views/Groups/GroupDetailView.axaml.cs | 2 +- TODO.md | 19 +++++- 13 files changed, 130 insertions(+), 8 deletions(-) diff --git a/LehrerApp.Core/Models/Exam.cs b/LehrerApp.Core/Models/Exam.cs index 4b3f688..b9f6f9c 100644 --- a/LehrerApp.Core/Models/Exam.cs +++ b/LehrerApp.Core/Models/Exam.cs @@ -12,6 +12,7 @@ public class Exam public List GradingKey { get; set; } = []; public ExamStatus Status { get; set; } = ExamStatus.Planned; public DateOnly? ReturnedAt { get; set; } + public Niveau? Niveau { get; set; } public string? Notes { get; set; } public DateTime CreatedAt { get; set; } = DateTime.UtcNow; public DateTime UpdatedAt { get; set; } = DateTime.UtcNow; diff --git a/LehrerApp.Core/Models/LearningGroup.cs b/LehrerApp.Core/Models/LearningGroup.cs index 6ca2e01..1a43b10 100644 --- a/LehrerApp.Core/Models/LearningGroup.cs +++ b/LehrerApp.Core/Models/LearningGroup.cs @@ -13,6 +13,7 @@ public class LearningGroup public int? HoursPerWeek { get; set; } public bool IsActive { get; set; } = true; public bool IsOwnClass { get; set; } + public bool IsDifferentiated { get; set; } public DateTime CreatedAt { get; set; } = DateTime.UtcNow; public DateTime UpdatedAt { get; set; } = DateTime.UtcNow; } @@ -26,7 +27,9 @@ public class Enrollment public EnrollmentPeriod Period { get; set; } = EnrollmentPeriod.FullYear; public DateOnly? JoinedAt { get; set; } public DateOnly? LeftAt { get; set; } + public Niveau? Niveau { get; set; } } public enum EnrollmentPeriod { FullYear, H1Only, H2Only, Custom } public enum GroupType { Class, Course } public enum GradingSystem { Grades1To6, Points0To15 } +public enum Niveau { E, G, Foerder } diff --git a/LehrerApp.Desktop/ViewModels/Groups/ExamEvaluationViewModels.cs b/LehrerApp.Desktop/ViewModels/Groups/ExamEvaluationViewModels.cs index 9d3e6a5..d229ed4 100644 --- a/LehrerApp.Desktop/ViewModels/Groups/ExamEvaluationViewModels.cs +++ b/LehrerApp.Desktop/ViewModels/Groups/ExamEvaluationViewModels.cs @@ -23,6 +23,7 @@ public partial class ExamEvaluationDialogViewModel : ObservableObject public string ExamTitle => _exam.Title; public string ExamDateLabel => _exam.Date.ToString("dd.MM.yyyy"); + public string NiveauLabel => _exam.Niveau.HasValue ? NiveauDisplay.ToName(_exam.Niveau) : ""; [ObservableProperty] private int _gradedCount; [ObservableProperty] private int _absentCount; diff --git a/LehrerApp.Desktop/ViewModels/Groups/ExamGradingViewModels.cs b/LehrerApp.Desktop/ViewModels/Groups/ExamGradingViewModels.cs index 2f98766..935bcc0 100644 --- a/LehrerApp.Desktop/ViewModels/Groups/ExamGradingViewModels.cs +++ b/LehrerApp.Desktop/ViewModels/Groups/ExamGradingViewModels.cs @@ -18,6 +18,7 @@ public partial class ExamGradingDialogViewModel : ObservableObject public string ExamTitle => _exam.Title; public string ExamDateLabel => _exam.Date.ToString("dd.MM.yyyy"); + public string NiveauLabel => _exam.Niveau.HasValue ? NiveauDisplay.ToName(_exam.Niveau) : ""; public List Tasks { get; } public ObservableCollection Rows { get; } = []; @@ -38,6 +39,10 @@ public partial class ExamGradingDialogViewModel : ObservableObject var enrollment = enrollmentList.FirstOrDefault(e => e.StudentId == s.Id); if (enrollment is not null && !IsEnrolledAtDate(enrollment, exam.Date)) continue; + // Niveau-Klausur: nur Schüler mit passendem Niveau zeigen. Klausuren ohne + // Niveau-Zuordnung gelten weiterhin für die ganze Gruppe. + if (exam.Niveau.HasValue && enrollment?.Niveau != exam.Niveau) continue; + existing.TryGetValue(s.Id, out var result); var row = new ExamResultRow(s.Id, s.FullName, Tasks, result, _exam.GradingKey, _examMaxPoints, _grading); row.OnChanged = SaveRow; diff --git a/LehrerApp.Desktop/ViewModels/Groups/ExamViewModels.cs b/LehrerApp.Desktop/ViewModels/Groups/ExamViewModels.cs index 3d17d2a..4e23e8c 100644 --- a/LehrerApp.Desktop/ViewModels/Groups/ExamViewModels.cs +++ b/LehrerApp.Desktop/ViewModels/Groups/ExamViewModels.cs @@ -35,6 +35,10 @@ public partial class ExamDialogViewModel : ObservableObject [ObservableProperty] private GradingKeyTemplate? _selectedTemplate; [ObservableProperty] private string _newTemplateName = ""; [ObservableProperty] private string _gradingKeyValidation = ""; + [ObservableProperty] private string _selectedNiveauName = "–"; + + public bool IsDifferentiated { get; } + public string[] NiveauOptions => NiveauDisplay.Options; public bool TotalPointsWarning => TotalPoints <= 0; public string TotalPointsDisplay => @@ -55,7 +59,7 @@ public partial class ExamDialogViewModel : ObservableObject public ExamDialogViewModel(IExamRepository exams, ICompetencyDomainRepository competencyDomains, IGradingKeyTemplateRepository gradingKeyTemplates, GradingService grading, Guid groupId, Guid? subjectId, int gradeLevel, GradingSystem gradingSystem, - string defaultSubjectName, Exam? editingExam, Exam? duplicateSource) + string defaultSubjectName, bool isDifferentiated, Exam? editingExam, Exam? duplicateSource) { _exams = exams; _competencyDomains = competencyDomains; _gradingKeyTemplates = gradingKeyTemplates; _grading = grading; @@ -63,6 +67,7 @@ public partial class ExamDialogViewModel : ObservableObject _gradingSystem = gradingSystem; _editingExam = editingExam; _subjectName = defaultSubjectName; + IsDifferentiated = isDifferentiated; HasCompetencyCatalog = subjectId.HasValue && _competencyDomains.GetBySubjectAndGrade(subjectId.Value, gradeLevel).Count > 0; @@ -81,6 +86,9 @@ public partial class ExamDialogViewModel : ObservableObject ExamNumber = source.ExamNumber; Notes = source.Notes ?? ""; ReturnedAtText = isDuplicate ? "" : source.ReturnedAt?.ToString("dd.MM.yyyy") ?? ""; + // Beim Duplizieren bewusst kein Niveau übernehmen — meist wird dupliziert, um + // gerade eine andere Niveau-Variante (E/G/Förder) derselben Klausur anzulegen. + SelectedNiveauName = isDuplicate ? "–" : NiveauDisplay.ToName(source.Niveau); foreach (var t in source.Tasks.OrderBy(t => t.Nr)) AddTaskInternal(t.Title, t.MaxPoints, t.Weight, [.. t.CompetencyCodes]); UseWeighting = Tasks.Any(t => Math.Abs(t.Weight - 1.0) > 0.0001); @@ -285,6 +293,7 @@ public partial class ExamDialogViewModel : ObservableObject Result.ExamNumber = ExamNumber; Result.Notes = string.IsNullOrWhiteSpace(Notes) ? null : Notes.Trim(); Result.ReturnedAt = returnedAt; + Result.Niveau = NiveauDisplay.FromName(SelectedNiveauName); Result.Tasks = Tasks.Select(t => t.ToModel()).ToList(); Result.GradingKey = gradingKey; _exams.Save(Result); diff --git a/LehrerApp.Desktop/ViewModels/Groups/GroupViewModels.cs b/LehrerApp.Desktop/ViewModels/Groups/GroupViewModels.cs index f9df221..37ea713 100644 --- a/LehrerApp.Desktop/ViewModels/Groups/GroupViewModels.cs +++ b/LehrerApp.Desktop/ViewModels/Groups/GroupViewModels.cs @@ -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? 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 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) diff --git a/LehrerApp.Desktop/Views/Groups/AddGroupDialog.axaml b/LehrerApp.Desktop/Views/Groups/AddGroupDialog.axaml index 294776a..c64dafc 100644 --- a/LehrerApp.Desktop/Views/Groups/AddGroupDialog.axaml +++ b/LehrerApp.Desktop/Views/Groups/AddGroupDialog.axaml @@ -67,6 +67,9 @@ + + diff --git a/LehrerApp.Desktop/Views/Groups/ExamDialog.axaml b/LehrerApp.Desktop/Views/Groups/ExamDialog.axaml index 6ffcf14..3423773 100644 --- a/LehrerApp.Desktop/Views/Groups/ExamDialog.axaml +++ b/LehrerApp.Desktop/Views/Groups/ExamDialog.axaml @@ -21,6 +21,13 @@ + + + + + diff --git a/LehrerApp.Desktop/Views/Groups/ExamEvaluationDialog.axaml b/LehrerApp.Desktop/Views/Groups/ExamEvaluationDialog.axaml index 9770e17..27b2fdd 100644 --- a/LehrerApp.Desktop/Views/Groups/ExamEvaluationDialog.axaml +++ b/LehrerApp.Desktop/Views/Groups/ExamEvaluationDialog.axaml @@ -12,7 +12,13 @@ - + + + + + + diff --git a/LehrerApp.Desktop/Views/Groups/ExamGradingDialog.axaml b/LehrerApp.Desktop/Views/Groups/ExamGradingDialog.axaml index 4d47018..90f3d78 100644 --- a/LehrerApp.Desktop/Views/Groups/ExamGradingDialog.axaml +++ b/LehrerApp.Desktop/Views/Groups/ExamGradingDialog.axaml @@ -11,6 +11,10 @@ + + + + + + + + + + @@ -99,6 +109,8 @@ + diff --git a/LehrerApp.Desktop/Views/Groups/GroupDetailView.axaml.cs b/LehrerApp.Desktop/Views/Groups/GroupDetailView.axaml.cs index 7f04a1f..9fe11bf 100644 --- a/LehrerApp.Desktop/Views/Groups/GroupDetailView.axaml.cs +++ b/LehrerApp.Desktop/Views/Groups/GroupDetailView.axaml.cs @@ -62,7 +62,7 @@ public partial class GroupDetailView : UserControl App.Services.GetRequiredService(), App.Services.GetRequiredService(), groupId, vm.Group.SubjectId, vm.Group.GradeLevel, vm.Group.GradingSystem, - vm.Group.Subject ?? "", editingExam, duplicateSource); + vm.Group.Subject ?? "", vm.Group.IsDifferentiated, editingExam, duplicateSource); var dialog = new ExamDialog { DataContext = dialogVm }; var owner = TopLevel.GetTopLevel(this) as Window; diff --git a/TODO.md b/TODO.md index 6557164..db863a5 100644 --- a/TODO.md +++ b/TODO.md @@ -31,6 +31,15 @@ Diese Regeln gelten für jede Aufgabe unten und müssen nicht wiederholt werden. Modelle `Exam`, `ExamTask`, `GradingKeyEntry`, `ExamResult` existieren bereits in [Exam.cs](LehrerApp.Core/Models/Exam.cs), Repositories ebenfalls. Die komplette UI fehlt. +**Niveaudifferenzierung (E/G/Förder) ist bereits umgesetzt** — nicht aus dieser Liste, sondern +ein separater Bedarf (Nachteilsausgleich/Binnendifferenzierung): `LearningGroup.IsDifferentiated` +(Checkbox in den Stammdaten) blendet eine Niveau-Zuordnung je Schüler ein (`Enrollment.Niveau`, +Schüler-Tab). Klausuren bekommen optional ein `Niveau`; Punkteeingabe und Auswertung filtern dann +automatisch auf die passenden Schüler. Workflow: G-Klausur normal anlegen, per "Duplizieren" +(1.1.4) die E-Variante mit eigenen Aufgaben/Punkten ableiten (Niveau wird beim Duplizieren bewusst +zurückgesetzt). Vollständig individuelle Förderklausuren (nur für einen einzelnen Schüler) laufen +pragmatisch über "Abwesend" bei den übrigen Schülern statt über eine eigene Schüler-Zuordnung. + ### 1.1 Klausur anlegen und verwalten - [x] **1.1.1** Dialog `AddExamDialog` — Titel, Datum, Fach, Klausurnummer, Notizen. Ersetzt den TODO-Stub `AddExam()` in [GroupViewModels.cs](LehrerApp.Desktop/ViewModels/Groups/GroupViewModels.cs). @@ -68,7 +77,7 @@ Modelle `Exam`, `ExamTask`, `GradingKeyEntry`, `ExamResult` existieren bereits i Pfeil-Runter springen zur gleichen Spalte in der Nachbarzeile, direkte Zifferneingabe und halbe Punkte (Komma oder Punkt) erlaubt. - [x] **1.4.3** Kennzeichnung "Abwesend" (`ExamResult.Absent`) per Checkbox — Note zeigt "abwesend" - statt einer berechneten Note. Ausschluss aus Statistiken folgt mit 1.5 (noch nicht umgesetzt). + statt einer berechneten Note. Ausschluss aus Statistiken über 1.5 umgesetzt. - [x] **1.4.4** Kommentarfeld pro Schüler (`ExamResult.Comment`). - [x] **1.4.5** Validierung: Punkte < 0 oder > Maximalpunkte der Aufgabe wird rot markiert und nicht gespeichert (`PointsCell.TrySetValue`). @@ -258,6 +267,8 @@ Navigationspunkt "Arbeitszeit" ist ein `PlaceholderViewModel`. Grundfunktionen sind vorhanden ([StudentViewModels.cs](LehrerApp.Desktop/ViewModels/Students/StudentViewModels.cs), [GroupViewModels.cs](LehrerApp.Desktop/ViewModels/Groups/GroupViewModels.cs)). +Niveau-Zuordnung je Schüler (E/G/Förder, `Enrollment.Niveau`) ist bereits umgesetzt, siehe +Hinweis in Kapitel 1 — betrifft auch Kurse, nicht nur Klassen. ### 7.1 Schüler - [ ] **7.1.1** Schüler löschen / auf inaktiv setzen inkl. Hinweis auf verknüpfte Daten. @@ -267,7 +278,9 @@ Grundfunktionen sind vorhanden ([StudentViewModels.cs](LehrerApp.Desktop/ViewMod - [ ] **7.1.5** Sitzplan je Gruppe (Raster mit Drag & Drop), Sprung von Sitzplatz zur Bewertung. ### 7.2 Gruppen -- [ ] **7.2.1** Gruppe bearbeiten und löschen (aktuell nur Anlegen möglich). +- [x] **7.2.1** Gruppe bearbeiten und löschen — bereits vorhanden (`EditGroupCommand`/`DeleteGroupCommand`/ + `ToggleArchiveCommand` in `GroupListViewModel`), nicht Teil der aktuellen Klausuren-Arbeit, + beim Review aber bestätigt. - [ ] **7.2.2** Gruppe ins neue Schuljahr übernehmen: Kopie mit gleicher Schülerschaft, neues `SchoolYear`, neue `Enrollment`-Einträge. - [ ] **7.2.3** Schüler aus einer Gruppe entfernen (`Enrollment.LeftAt` setzen statt löschen). @@ -296,7 +309,7 @@ Format dokumentiert in [Kompetenzkatalog-KI-Prompt.md](docs/Kompetenzkatalog-KI- ### 8.2 Verwendung im Unterricht - [ ] **8.2.1** Kompetenzen einer Unterrichtseinheit zuordnen (siehe 4.1.3). -- [ ] **8.2.2** Kompetenzen einzelnen Klausuraufgaben zuordnen (siehe 1.2.4). +- [x] **8.2.2** Kompetenzen einzelnen Klausuraufgaben zuordnen — umgesetzt mit 1.2.4. - [ ] **8.2.3** Abdeckungsübersicht: welche Kompetenzen wurden im Schuljahr behandelt/geprüft? ### 8.3 Kompetenzorientierte Auswertung