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
+1
View File
@@ -12,6 +12,7 @@ public class Exam
public List<GradingKeyEntry> GradingKey { get; set; } = []; public List<GradingKeyEntry> GradingKey { get; set; } = [];
public ExamStatus Status { get; set; } = ExamStatus.Planned; public ExamStatus Status { get; set; } = ExamStatus.Planned;
public DateOnly? ReturnedAt { get; set; } public DateOnly? ReturnedAt { get; set; }
public Niveau? Niveau { get; set; }
public string? Notes { get; set; } public string? Notes { get; set; }
public DateTime CreatedAt { get; set; } = DateTime.UtcNow; public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow; public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
+3
View File
@@ -13,6 +13,7 @@ public class LearningGroup
public int? HoursPerWeek { get; set; } public int? HoursPerWeek { get; set; }
public bool IsActive { get; set; } = true; public bool IsActive { get; set; } = true;
public bool IsOwnClass { get; set; } public bool IsOwnClass { get; set; }
public bool IsDifferentiated { get; set; }
public DateTime CreatedAt { get; set; } = DateTime.UtcNow; public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public DateTime UpdatedAt { 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 EnrollmentPeriod Period { get; set; } = EnrollmentPeriod.FullYear;
public DateOnly? JoinedAt { get; set; } public DateOnly? JoinedAt { get; set; }
public DateOnly? LeftAt { get; set; } public DateOnly? LeftAt { get; set; }
public Niveau? Niveau { get; set; }
} }
public enum EnrollmentPeriod { FullYear, H1Only, H2Only, Custom } public enum EnrollmentPeriod { FullYear, H1Only, H2Only, Custom }
public enum GroupType { Class, Course } public enum GroupType { Class, Course }
public enum GradingSystem { Grades1To6, Points0To15 } public enum GradingSystem { Grades1To6, Points0To15 }
public enum Niveau { E, G, Foerder }
@@ -23,6 +23,7 @@ public partial class ExamEvaluationDialogViewModel : ObservableObject
public string ExamTitle => _exam.Title; public string ExamTitle => _exam.Title;
public string ExamDateLabel => _exam.Date.ToString("dd.MM.yyyy"); 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 _gradedCount;
[ObservableProperty] private int _absentCount; [ObservableProperty] private int _absentCount;
@@ -18,6 +18,7 @@ public partial class ExamGradingDialogViewModel : ObservableObject
public string ExamTitle => _exam.Title; public string ExamTitle => _exam.Title;
public string ExamDateLabel => _exam.Date.ToString("dd.MM.yyyy"); public string ExamDateLabel => _exam.Date.ToString("dd.MM.yyyy");
public string NiveauLabel => _exam.Niveau.HasValue ? NiveauDisplay.ToName(_exam.Niveau) : "";
public List<ExamTask> Tasks { get; } public List<ExamTask> Tasks { get; }
public ObservableCollection<ExamResultRow> Rows { get; } = []; public ObservableCollection<ExamResultRow> Rows { get; } = [];
@@ -38,6 +39,10 @@ public partial class ExamGradingDialogViewModel : ObservableObject
var enrollment = enrollmentList.FirstOrDefault(e => e.StudentId == s.Id); var enrollment = enrollmentList.FirstOrDefault(e => e.StudentId == s.Id);
if (enrollment is not null && !IsEnrolledAtDate(enrollment, exam.Date)) continue; 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); existing.TryGetValue(s.Id, out var result);
var row = new ExamResultRow(s.Id, s.FullName, Tasks, result, _exam.GradingKey, _examMaxPoints, _grading); var row = new ExamResultRow(s.Id, s.FullName, Tasks, result, _exam.GradingKey, _examMaxPoints, _grading);
row.OnChanged = SaveRow; row.OnChanged = SaveRow;
@@ -35,6 +35,10 @@ public partial class ExamDialogViewModel : ObservableObject
[ObservableProperty] private GradingKeyTemplate? _selectedTemplate; [ObservableProperty] private GradingKeyTemplate? _selectedTemplate;
[ObservableProperty] private string _newTemplateName = ""; [ObservableProperty] private string _newTemplateName = "";
[ObservableProperty] private string _gradingKeyValidation = ""; [ObservableProperty] private string _gradingKeyValidation = "";
[ObservableProperty] private string _selectedNiveauName = "";
public bool IsDifferentiated { get; }
public string[] NiveauOptions => NiveauDisplay.Options;
public bool TotalPointsWarning => TotalPoints <= 0; public bool TotalPointsWarning => TotalPoints <= 0;
public string TotalPointsDisplay => public string TotalPointsDisplay =>
@@ -55,7 +59,7 @@ public partial class ExamDialogViewModel : ObservableObject
public ExamDialogViewModel(IExamRepository exams, ICompetencyDomainRepository competencyDomains, public ExamDialogViewModel(IExamRepository exams, ICompetencyDomainRepository competencyDomains,
IGradingKeyTemplateRepository gradingKeyTemplates, GradingService grading, IGradingKeyTemplateRepository gradingKeyTemplates, GradingService grading,
Guid groupId, Guid? subjectId, int gradeLevel, GradingSystem gradingSystem, 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; _exams = exams; _competencyDomains = competencyDomains;
_gradingKeyTemplates = gradingKeyTemplates; _grading = grading; _gradingKeyTemplates = gradingKeyTemplates; _grading = grading;
@@ -63,6 +67,7 @@ public partial class ExamDialogViewModel : ObservableObject
_gradingSystem = gradingSystem; _gradingSystem = gradingSystem;
_editingExam = editingExam; _editingExam = editingExam;
_subjectName = defaultSubjectName; _subjectName = defaultSubjectName;
IsDifferentiated = isDifferentiated;
HasCompetencyCatalog = subjectId.HasValue HasCompetencyCatalog = subjectId.HasValue
&& _competencyDomains.GetBySubjectAndGrade(subjectId.Value, gradeLevel).Count > 0; && _competencyDomains.GetBySubjectAndGrade(subjectId.Value, gradeLevel).Count > 0;
@@ -81,6 +86,9 @@ public partial class ExamDialogViewModel : ObservableObject
ExamNumber = source.ExamNumber; ExamNumber = source.ExamNumber;
Notes = source.Notes ?? ""; Notes = source.Notes ?? "";
ReturnedAtText = isDuplicate ? "" : source.ReturnedAt?.ToString("dd.MM.yyyy") ?? ""; 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)) foreach (var t in source.Tasks.OrderBy(t => t.Nr))
AddTaskInternal(t.Title, t.MaxPoints, t.Weight, [.. t.CompetencyCodes]); AddTaskInternal(t.Title, t.MaxPoints, t.Weight, [.. t.CompetencyCodes]);
UseWeighting = Tasks.Any(t => Math.Abs(t.Weight - 1.0) > 0.0001); 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.ExamNumber = ExamNumber;
Result.Notes = string.IsNullOrWhiteSpace(Notes) ? null : Notes.Trim(); Result.Notes = string.IsNullOrWhiteSpace(Notes) ? null : Notes.Trim();
Result.ReturnedAt = returnedAt; Result.ReturnedAt = returnedAt;
Result.Niveau = NiveauDisplay.FromName(SelectedNiveauName);
Result.Tasks = Tasks.Select(t => t.ToModel()).ToList(); Result.Tasks = Tasks.Select(t => t.ToModel()).ToList();
Result.GradingKey = gradingKey; Result.GradingKey = gradingKey;
_exams.Save(Result); _exams.Save(Result);
@@ -221,10 +221,21 @@ public partial class GroupDetailViewModel : ObservableObject
foreach (var s in enrolled) foreach (var s in enrolled)
{ {
enrollments.TryGetValue(s.Id, out var enr); 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] [RelayCommand]
private async Task AddStudent() 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); } [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 Guid Id { get; }
public string FullName { get; } public string FullName { get; }
public string PeriodLabel { 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) public StudentSummary(Core.Models.Student s, Enrollment? e)
{ {
Id = s.Id; Id = s.Id;
@@ -380,6 +426,13 @@ public class StudentSummary
EnrollmentPeriod.Custom => BuildCustomLabel(e), EnrollmentPeriod.Custom => BuildCustomLabel(e),
_ => "", _ => "",
}; };
_niveau = e?.Niveau;
}
partial void OnNiveauChanged(Niveau? value)
{
OnPropertyChanged(nameof(NiveauName));
OnChanged?.Invoke(this);
} }
private static string BuildCustomLabel(Enrollment e) private static string BuildCustomLabel(Enrollment e)
@@ -400,11 +453,13 @@ public class ExamSummary
public string StatusLabel { get; } public string StatusLabel { get; }
public string StatusColorHex { get; } public string StatusColorHex { get; }
public string ReturnedAtDisplay { get; } public string ReturnedAtDisplay { get; }
public string NiveauLabel { get; }
public ExamSummary(Core.Models.Exam e) public ExamSummary(Core.Models.Exam e)
{ {
Id = e.Id; Title = e.Title; Date = e.Date.ToString("dd.MM.yyyy"); Status = e.Status; Id = e.Id; Title = e.Title; Date = e.Date.ToString("dd.MM.yyyy"); Status = e.Status;
ReturnedAtDisplay = e.ReturnedAt?.ToString("dd.MM.yyyy") ?? ""; ReturnedAtDisplay = e.ReturnedAt?.ToString("dd.MM.yyyy") ?? "";
NiveauLabel = e.Niveau.HasValue ? NiveauDisplay.ToName(e.Niveau) : "alle";
StatusLabel = e.Status switch StatusLabel = e.Status switch
{ {
ExamStatus.Planned => "Geplant", ExamStatus.Planned => "Geplant",
@@ -552,6 +607,7 @@ public partial class AddGroupDialogViewModel : ObservableObject
[ObservableProperty] private string _selectedSchoolYear = ""; [ObservableProperty] private string _selectedSchoolYear = "";
[ObservableProperty] private int? _hoursPerWeek; [ObservableProperty] private int? _hoursPerWeek;
[ObservableProperty] private bool _isOwnClass; [ObservableProperty] private bool _isOwnClass;
[ObservableProperty] private bool _isDifferentiated;
[ObservableProperty] private string _validationMessage = ""; [ObservableProperty] private string _validationMessage = "";
public List<string> SchoolYears { get; } public List<string> SchoolYears { get; }
@@ -589,6 +645,7 @@ public partial class AddGroupDialogViewModel : ObservableObject
SelectedSchoolYear = group.SchoolYear; SelectedSchoolYear = group.SchoolYear;
HoursPerWeek = group.HoursPerWeek; HoursPerWeek = group.HoursPerWeek;
IsOwnClass = group.IsOwnClass; IsOwnClass = group.IsOwnClass;
IsDifferentiated = group.IsDifferentiated;
OnPropertyChanged(nameof(DialogTitle)); OnPropertyChanged(nameof(DialogTitle));
OnPropertyChanged(nameof(SaveButtonText)); OnPropertyChanged(nameof(SaveButtonText));
} }
@@ -629,6 +686,7 @@ public partial class AddGroupDialogViewModel : ObservableObject
Result.SchoolYear = SelectedSchoolYear; Result.SchoolYear = SelectedSchoolYear;
Result.HoursPerWeek = HoursPerWeek; Result.HoursPerWeek = HoursPerWeek;
Result.IsOwnClass = IsOwnClass; Result.IsOwnClass = IsOwnClass;
Result.IsDifferentiated = IsDifferentiated;
_groups.Save(Result); _groups.Save(Result);
if (_editingGroup is not null && _originalSchoolYear != SelectedSchoolYear) if (_editingGroup is not null && _originalSchoolYear != SelectedSchoolYear)
@@ -67,6 +67,9 @@
<CheckBox Content="Gehört zu meiner Klasse" IsChecked="{Binding IsOwnClass}" <CheckBox Content="Gehört zu meiner Klasse" IsChecked="{Binding IsOwnClass}"
ToolTip.Tip="Für die farbliche Hervorhebung im Dashboard-Kalender. Mehrere Gruppen (Klasse + zugehörige Kurse) können markiert werden."/> ToolTip.Tip="Für die farbliche Hervorhebung im Dashboard-Kalender. Mehrere Gruppen (Klasse + zugehörige Kurse) können markiert werden."/>
<CheckBox Content="Niveaudifferenzierung (E/G/Förder)" IsChecked="{Binding IsDifferentiated}"
ToolTip.Tip="Blendet im Schüler-Tab eine Niveau-Zuordnung je Schüler ein und erlaubt in Klausuren die Zuordnung zu einem Niveau."/>
<TextBlock Text="{Binding ValidationMessage}" Foreground="Red" FontSize="12" <TextBlock Text="{Binding ValidationMessage}" Foreground="Red" FontSize="12"
IsVisible="{Binding ValidationMessage, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/> IsVisible="{Binding ValidationMessage, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
</StackPanel> </StackPanel>
@@ -21,6 +21,13 @@
<TextBox Text="{Binding Title}" PlaceholderText="z.B. 1. Klausur Kinetik"/> <TextBox Text="{Binding Title}" PlaceholderText="z.B. 1. Klausur Kinetik"/>
</StackPanel> </StackPanel>
<StackPanel Spacing="4" IsVisible="{Binding IsDifferentiated}">
<TextBlock Text="Niveau" FontSize="12" Opacity="0.7"/>
<ComboBox ItemsSource="{Binding NiveauOptions}" SelectedItem="{Binding SelectedNiveauName}"
HorizontalAlignment="Stretch"
ToolTip.Tip="Strich = für alle Schüler der Gruppe. Bei E/G/Förder zeigt die Punkteeingabe nur die Schüler mit passendem Niveau."/>
</StackPanel>
<Grid ColumnDefinitions="*,12,*,12,*"> <Grid ColumnDefinitions="*,12,*,12,*">
<StackPanel Grid.Column="0" Spacing="4"> <StackPanel Grid.Column="0" Spacing="4">
<TextBlock Text="Datum *" FontSize="12" Opacity="0.7"/> <TextBlock Text="Datum *" FontSize="12" Opacity="0.7"/>
@@ -12,7 +12,13 @@
<StackPanel Spacing="16" Margin="0,0,12,0"> <StackPanel Spacing="16" Margin="0,0,12,0">
<StackPanel Spacing="2"> <StackPanel Spacing="2">
<TextBlock Text="{Binding ExamTitle}" FontSize="18" FontWeight="SemiBold"/> <StackPanel Orientation="Horizontal" Spacing="8">
<TextBlock Text="{Binding ExamTitle}" FontSize="18" FontWeight="SemiBold" VerticalAlignment="Center"/>
<Border Background="{DynamicResource SystemControlBackgroundAccentBrush}" CornerRadius="4" Padding="8,2"
IsVisible="{Binding NiveauLabel, Converter={x:Static StringConverters.IsNotNullOrEmpty}}">
<TextBlock Text="{Binding NiveauLabel}" FontSize="12" Foreground="White"/>
</Border>
</StackPanel>
<TextBlock Text="{Binding ExamDateLabel}" FontSize="12" Opacity="0.6"/> <TextBlock Text="{Binding ExamDateLabel}" FontSize="12" Opacity="0.6"/>
</StackPanel> </StackPanel>
@@ -11,6 +11,10 @@
<StackPanel Grid.Row="0" Orientation="Horizontal" Spacing="10" Margin="0,0,0,10"> <StackPanel Grid.Row="0" Orientation="Horizontal" Spacing="10" Margin="0,0,0,10">
<TextBlock Text="{Binding ExamTitle}" FontSize="16" FontWeight="SemiBold" VerticalAlignment="Center"/> <TextBlock Text="{Binding ExamTitle}" FontSize="16" FontWeight="SemiBold" VerticalAlignment="Center"/>
<TextBlock Text="{Binding ExamDateLabel}" FontSize="13" Opacity="0.6" VerticalAlignment="Center"/> <TextBlock Text="{Binding ExamDateLabel}" FontSize="13" Opacity="0.6" VerticalAlignment="Center"/>
<Border Background="{DynamicResource SystemControlBackgroundAccentBrush}" CornerRadius="4" Padding="8,2"
IsVisible="{Binding NiveauLabel, Converter={x:Static StringConverters.IsNotNullOrEmpty}}">
<TextBlock Text="{Binding NiveauLabel}" FontSize="12" Foreground="White"/>
</Border>
</StackPanel> </StackPanel>
<DataGrid Grid.Row="1" Name="ResultGrid" <DataGrid Grid.Row="1" Name="ResultGrid"
@@ -56,6 +56,16 @@
<DataGrid.Columns> <DataGrid.Columns>
<DataGridTextColumn Header="Name" Binding="{Binding FullName}" Width="*"/> <DataGridTextColumn Header="Name" Binding="{Binding FullName}" Width="*"/>
<DataGridTextColumn Header="Zeitraum" Binding="{Binding PeriodLabel}" Width="90"/> <DataGridTextColumn Header="Zeitraum" Binding="{Binding PeriodLabel}" Width="90"/>
<DataGridTemplateColumn Header="Niveau" Width="130"
IsVisible="{Binding Group.IsDifferentiated}">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate x:DataType="vm:StudentSummary">
<ComboBox ItemsSource="{x:Static vm:StudentSummary.NiveauOptions}"
SelectedItem="{Binding NiveauName}"
HorizontalAlignment="Stretch" Margin="2"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns> </DataGrid.Columns>
</DataGrid> </DataGrid>
</ContentPage> </ContentPage>
@@ -99,6 +109,8 @@
<DataGrid.Columns> <DataGrid.Columns>
<DataGridTextColumn Header="Datum" Binding="{Binding Date}" Width="110"/> <DataGridTextColumn Header="Datum" Binding="{Binding Date}" Width="110"/>
<DataGridTextColumn Header="Titel" Binding="{Binding Title}" Width="*"/> <DataGridTextColumn Header="Titel" Binding="{Binding Title}" Width="*"/>
<DataGridTextColumn Header="Niveau" Binding="{Binding NiveauLabel}" Width="80"
IsVisible="{Binding Group.IsDifferentiated}"/>
<DataGridTemplateColumn Header="Status" Width="150"> <DataGridTemplateColumn Header="Status" Width="150">
<DataGridTemplateColumn.CellTemplate> <DataGridTemplateColumn.CellTemplate>
<DataTemplate x:DataType="vm:ExamSummary"> <DataTemplate x:DataType="vm:ExamSummary">
@@ -62,7 +62,7 @@ public partial class GroupDetailView : UserControl
App.Services.GetRequiredService<IGradingKeyTemplateRepository>(), App.Services.GetRequiredService<IGradingKeyTemplateRepository>(),
App.Services.GetRequiredService<GradingService>(), App.Services.GetRequiredService<GradingService>(),
groupId, vm.Group.SubjectId, vm.Group.GradeLevel, vm.Group.GradingSystem, 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 dialog = new ExamDialog { DataContext = dialogVm };
var owner = TopLevel.GetTopLevel(this) as Window; var owner = TopLevel.GetTopLevel(this) as Window;
+16 -3
View File
@@ -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 Modelle `Exam`, `ExamTask`, `GradingKeyEntry`, `ExamResult` existieren bereits in
[Exam.cs](LehrerApp.Core/Models/Exam.cs), Repositories ebenfalls. Die komplette UI fehlt. [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 ### 1.1 Klausur anlegen und verwalten
- [x] **1.1.1** Dialog `AddExamDialog` — Titel, Datum, Fach, Klausurnummer, Notizen. - [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). 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 Pfeil-Runter springen zur gleichen Spalte in der Nachbarzeile, direkte Zifferneingabe
und halbe Punkte (Komma oder Punkt) erlaubt. und halbe Punkte (Komma oder Punkt) erlaubt.
- [x] **1.4.3** Kennzeichnung "Abwesend" (`ExamResult.Absent`) per Checkbox — Note zeigt "abwesend" - [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.4** Kommentarfeld pro Schüler (`ExamResult.Comment`).
- [x] **1.4.5** Validierung: Punkte < 0 oder > Maximalpunkte der Aufgabe wird rot markiert und nicht - [x] **1.4.5** Validierung: Punkte < 0 oder > Maximalpunkte der Aufgabe wird rot markiert und nicht
gespeichert (`PointsCell.TrySetValue`). gespeichert (`PointsCell.TrySetValue`).
@@ -258,6 +267,8 @@ Navigationspunkt "Arbeitszeit" ist ein `PlaceholderViewModel`.
Grundfunktionen sind vorhanden ([StudentViewModels.cs](LehrerApp.Desktop/ViewModels/Students/StudentViewModels.cs), Grundfunktionen sind vorhanden ([StudentViewModels.cs](LehrerApp.Desktop/ViewModels/Students/StudentViewModels.cs),
[GroupViewModels.cs](LehrerApp.Desktop/ViewModels/Groups/GroupViewModels.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 Schüler
- [ ] **7.1.1** Schüler löschen / auf inaktiv setzen inkl. Hinweis auf verknüpfte Daten. - [ ] **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.1.5** Sitzplan je Gruppe (Raster mit Drag & Drop), Sprung von Sitzplatz zur Bewertung.
### 7.2 Gruppen ### 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, - [ ] **7.2.2** Gruppe ins neue Schuljahr übernehmen: Kopie mit gleicher Schülerschaft,
neues `SchoolYear`, neue `Enrollment`-Einträge. neues `SchoolYear`, neue `Enrollment`-Einträge.
- [ ] **7.2.3** Schüler aus einer Gruppe entfernen (`Enrollment.LeftAt` setzen statt löschen). - [ ] **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 Verwendung im Unterricht
- [ ] **8.2.1** Kompetenzen einer Unterrichtseinheit zuordnen (siehe 4.1.3). - [ ] **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.2.3** Abdeckungsübersicht: welche Kompetenzen wurden im Schuljahr behandelt/geprüft?
### 8.3 Kompetenzorientierte Auswertung ### 8.3 Kompetenzorientierte Auswertung