diff --git a/LehrerApp.Core/Models/Exam.cs b/LehrerApp.Core/Models/Exam.cs
index 69e436b..0a65a14 100644
--- a/LehrerApp.Core/Models/Exam.cs
+++ b/LehrerApp.Core/Models/Exam.cs
@@ -32,6 +32,10 @@ public class Exam
/// erste Arbeit () versäumt haben. Anders als eine Wiederholung
/// wird sie nur von diesen Schülern geschrieben und ersetzt deren fehlende Note.
public bool IsMakeUp { get; set; }
+ /// Förderarbeit: inhaltlich eigene Arbeit, die einzelne Schüler statt der verknüpften
+ /// ersten Arbeit schreiben — typischerweise am selben Tag, daher darf sie am Datum der ersten
+ /// Arbeit liegen. Zählt wie eine Nachschreibarbeit (ersetzt nur eine fehlende Note).
+ public bool IsSupportWork { get; set; }
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
}
@@ -78,6 +82,13 @@ public enum RepeatGradeRule { Better, Repeat, Original }
/// Reihenfolge ist persistiert (LiteDB speichert den Enum-Wert) — nur hinten anhängen.
public enum ExamKind { Klausur, Test, KontrollierteHausaufgabe, Sonstiges }
+public static class ExamFollowUpExtensions
+{
+ /// Nachschreib- und Förderarbeiten sind inhaltlich eigene Arbeiten nur für einzelne
+ /// Schüler: ihre Note ersetzt eine fehlende Note der ersten Arbeit, verdrängt sie aber nie.
+ public static bool ReplacesMissingGrade(this Exam exam) => exam.IsMakeUp || exam.IsSupportWork;
+}
+
public static class ExamKindExtensions
{
public static bool CountsAsWritten(this ExamKind kind) => kind == ExamKind.Klausur;
diff --git a/LehrerApp.Core/Services/ExamCoverSheetService.cs b/LehrerApp.Core/Services/ExamCoverSheetService.cs
index 936d9cf..43a0361 100644
--- a/LehrerApp.Core/Services/ExamCoverSheetService.cs
+++ b/LehrerApp.Core/Services/ExamCoverSheetService.cs
@@ -37,12 +37,12 @@ public static class ExamCoverSheetService
/// Wer eine Arbeit mitschreibt — eine Regel für Punkteeingabe und Deckblätter.
public static class ExamParticipantService
{
- /// Bei einer Nachschreibarbeit: Schüler, die die erste Arbeit bereits mit Note
- /// geschrieben haben. Sonst leer.
+ /// Bei einer Nachschreib- oder Förderarbeit: Schüler, die die erste Arbeit bereits mit
+ /// Note geschrieben haben. Sonst leer.
public static HashSet WroteOriginal(Exam exam, Func> resultsByExam)
{
var wroteOriginal = new HashSet();
- if (exam.IsMakeUp && exam.RepeatOfExamId is { } originalId)
+ if (exam.ReplacesMissingGrade() && exam.RepeatOfExamId is { } originalId)
foreach (var r in resultsByExam(originalId).Where(r => !r.Absent && r.Grade is not null))
wroteOriginal.Add(r.StudentId);
return wroteOriginal;
@@ -54,7 +54,7 @@ public static class ExamParticipantService
// Niveau-Klausur: nur Schüler mit passendem Niveau. Klausuren ohne Niveau-Zuordnung gelten
// weiterhin für die ganze Gruppe.
if (exam.Niveau.HasValue && membership?.Niveau != exam.Niveau) return false;
- // Nachschreibarbeit: wer die erste Arbeit schon mit Note geschrieben hat, taucht nicht auf —
+ // Nachschreib-/Förderarbeit: wer die erste Arbeit schon mit Note geschrieben hat, taucht nicht auf —
// außer es gibt hier bereits ein Ergebnis (nichts verstecken, was schon eingetragen wurde).
return hasResult || !wroteOriginal;
}
diff --git a/LehrerApp.Core/Services/ExamScoringService.cs b/LehrerApp.Core/Services/ExamScoringService.cs
index 9b673d1..6e32721 100644
--- a/LehrerApp.Core/Services/ExamScoringService.cs
+++ b/LehrerApp.Core/Services/ExamScoringService.cs
@@ -45,10 +45,10 @@ public static class ExamScoringService
AppliesTo(task, variant) && index < points.Count ? points[index] : 0).Sum();
///
- /// Wählt pro Arbeit samt allen verknüpften Folgearbeiten (Wiederholung, Nachschreibarbeit)
+ /// Wählt pro Arbeit samt allen verknüpften Folgearbeiten (Wiederholung, Nachschreib-/Förderarbeit)
/// genau eine Note. Andere Arbeiten bleiben erhalten. Fehlt die erste Arbeit (Schüler war
- /// abwesend), zählt die Nachschreibarbeit allein; hat der Schüler beide geschrieben, zählt bei
- /// einer Nachschreibarbeit immer die erste — sie ersetzt nur eine fehlende Note.
+ /// abwesend), zählt die Nachschreib-/Förderarbeit allein; hat der Schüler beide geschrieben,
+ /// zählt immer die erste — sie ersetzen nur eine fehlende Note.
///
public static List EffectiveGrades(IEnumerable<(Exam Exam, string Grade)> entries,
GradingSystem system)
@@ -72,7 +72,7 @@ public static class ExamScoringService
var current = ordered[0].Grade;
foreach (var next in ordered.Skip(1))
current = Choose(current, next.Grade,
- next.Exam.IsMakeUp ? RepeatGradeRule.Original : next.Exam.RepeatGradeRule, system);
+ next.Exam.ReplacesMissingGrade() ? RepeatGradeRule.Original : next.Exam.RepeatGradeRule, system);
result.Add(current);
}
return result;
diff --git a/LehrerApp.Desktop.Tests/ExamDialogViewModelTests.cs b/LehrerApp.Desktop.Tests/ExamDialogViewModelTests.cs
index fc3796b..429c7ab 100644
--- a/LehrerApp.Desktop.Tests/ExamDialogViewModelTests.cs
+++ b/LehrerApp.Desktop.Tests/ExamDialogViewModelTests.cs
@@ -84,4 +84,81 @@ public sealed class ExamDialogViewModelTests
Assert.Equal(10, result.TotalPoints);
Assert.Equal("1", result.Grade);
}
+
+ // ── Förderarbeit ─────────────────────────────────────────────────────────
+
+ private static (ExamDialogViewModel Vm, Exam Original) BuildFollowUpViewModel(DateOnly originalDate)
+ {
+ var original = new Exam { GroupId = Guid.NewGuid(), Title = "Atombau", Date = originalDate };
+ var vm = new ExamDialogViewModel(new FakeExams([original]), new FakeCompetencyDomains(),
+ new FakeGradingKeyTemplates(), new GradingService(), original.GroupId, null, 9,
+ GradingSystem.Grades1To6, "Chemie", false, null, null);
+ vm.Title = "Atombau (Förder)";
+ vm.Tasks.Add(new ExamTaskEditItem("A1", 10, 1, [], []) { OnChanged = () => { } });
+ vm.Tasks[0].MaxPoints = 10;
+ vm.SelectedOriginalExam = original;
+ return (vm, original);
+ }
+
+ [Fact]
+ public void Foerderarbeit_UebernimmtDatumDerErstenArbeitUndDarfAmSelbenTagLiegen()
+ {
+ var (vm, original) = BuildFollowUpViewModel(new DateOnly(2026, 10, 14));
+
+ vm.FollowUpKindName = "Förderarbeit";
+
+ Assert.Equal("14.10.2026", vm.DateText);
+ vm.SaveCommand.Execute(null);
+ Assert.Equal("", vm.RepeatError);
+ Assert.NotNull(vm.Result);
+ Assert.True(vm.Result!.IsSupportWork);
+ Assert.False(vm.Result.IsMakeUp);
+ Assert.Equal(original.Id, vm.Result.RepeatOfExamId);
+ Assert.Equal(original.Date, vm.Result.Date);
+ }
+
+ [Fact]
+ public void Foerderarbeit_VorDerErstenArbeit_WirdAbgelehnt()
+ {
+ var (vm, _) = BuildFollowUpViewModel(new DateOnly(2026, 10, 14));
+ vm.FollowUpKindName = "Förderarbeit";
+
+ vm.DateText = "13.10.2026";
+ vm.SaveCommand.Execute(null);
+
+ Assert.Contains("Förderarbeit", vm.RepeatError);
+ Assert.Null(vm.Result);
+ }
+
+ [Fact]
+ public void Nachschreibarbeit_AmSelbenTag_BleibtAbgelehnt()
+ {
+ var (vm, _) = BuildFollowUpViewModel(new DateOnly(2026, 10, 14));
+ vm.FollowUpKindName = "Nachschreibarbeit";
+
+ vm.DateText = "14.10.2026";
+ vm.SaveCommand.Execute(null);
+
+ Assert.Contains("Nachschreibarbeit", vm.RepeatError);
+ Assert.Null(vm.Result);
+ }
+
+ [Fact]
+ public void Foerderarbeit_WirdBeimBearbeitenWiederhergestelltOhneDatumZuUeberschreiben()
+ {
+ var original = new Exam { GroupId = Guid.NewGuid(), Title = "Atombau", Date = new DateOnly(2026, 10, 14) };
+ var support = new Exam
+ {
+ GroupId = original.GroupId, Title = "Förder", Date = new DateOnly(2026, 10, 16),
+ RepeatOfExamId = original.Id, IsSupportWork = true,
+ };
+
+ var vm = new ExamDialogViewModel(new FakeExams([original, support]), new FakeCompetencyDomains(),
+ new FakeGradingKeyTemplates(), new GradingService(), original.GroupId, null, 9,
+ GradingSystem.Grades1To6, "Chemie", false, support, null);
+
+ Assert.True(vm.IsSupportWork);
+ Assert.Equal("16.10.2026", vm.DateText);
+ Assert.False(vm.ShowRepeatRule);
+ }
}
diff --git a/LehrerApp.Desktop/Services/GradeReportDrawingBuilder.cs b/LehrerApp.Desktop/Services/GradeReportDrawingBuilder.cs
index 61cdbff..59103dc 100644
--- a/LehrerApp.Desktop/Services/GradeReportDrawingBuilder.cs
+++ b/LehrerApp.Desktop/Services/GradeReportDrawingBuilder.cs
@@ -22,7 +22,8 @@ public static class GradeReportDrawingBuilder
if (exam.RepeatOfExamId is null) return new DrawingValue([], 0);
if (original is null || original.Id != exam.RepeatOfExamId || original.GroupId != exam.GroupId)
throw new InvalidDataException("Die verknüpfte erste Arbeit ist nicht verfügbar.");
- var rule = exam.IsMakeUp ? "Die Nachschreibarbeit ersetzt die versäumte erste Arbeit. Liegen beide Noten vor, zählt die erste Arbeit."
+ var rule = exam.IsSupportWork ? "Die Förderarbeit ersetzt die erste Arbeit. Liegen beide Noten vor, zählt die erste Arbeit."
+ : exam.IsMakeUp ? "Die Nachschreibarbeit ersetzt die versäumte erste Arbeit. Liegen beide Noten vor, zählt die erste Arbeit."
: exam.RepeatGradeRule switch
{
RepeatGradeRule.Repeat => "Für die Bewertung zählt die Note der Wiederholungsarbeit.",
@@ -35,7 +36,7 @@ public static class GradeReportDrawingBuilder
ExamScoringService.Maximum(original.Tasks, originalResult.Variant), original.GradingKey);
var firstGrade = originalResult is { Absent: true } ? "abwesend" : Value(grade);
return Text(template, ReportTextDrawing.RepeatInfo,
- exam.IsMakeUp ? "Nachschreibarbeit" : "Wiederholungsarbeit",
+ exam.IsSupportWork ? "Förderarbeit" : exam.IsMakeUp ? "Nachschreibarbeit" : "Wiederholungsarbeit",
[$"Erste Arbeit: {original.Title} vom {original.Date:dd.MM.yyyy}",
$"Note der ersten Arbeit: {firstGrade}", rule]);
}
diff --git a/LehrerApp.Desktop/ViewModels/Groups/ExamEvaluationViewModels.cs b/LehrerApp.Desktop/ViewModels/Groups/ExamEvaluationViewModels.cs
index a850c0e..bd44e3a 100644
--- a/LehrerApp.Desktop/ViewModels/Groups/ExamEvaluationViewModels.cs
+++ b/LehrerApp.Desktop/ViewModels/Groups/ExamEvaluationViewModels.cs
@@ -42,7 +42,7 @@ public partial class ExamEvaluationDialogViewModel : ObservableObject
public ObservableCollection RepeatComparison { get; } = [];
public bool HasRepeatComparison => RepeatComparison.Count > 0;
public string RepeatComparisonTitle { get; private set; } = "";
- /// "Wiederholung" bzw. "Nachschreibarbeit" — Beschriftung der Folgearbeit im Vergleich.
+ /// "Wiederholung", "Nachschreibarbeit" bzw. "Förderarbeit" — Beschriftung der Folgearbeit im Vergleich.
public string FollowUpLabel { get; private set; } = "Wiederholung";
public string FollowUpComparisonHeading => $"Vergleich mit der {FollowUpLabel}";
public ObservableCollection GradingKeyEntries { get; } = [];
@@ -72,8 +72,8 @@ public partial class ExamEvaluationDialogViewModel : ObservableObject
: _exams.GetByGroup(_exam.GroupId).Where(e => e.RepeatOfExamId == _exam.Id)
.OrderBy(e => e.Date).FirstOrDefault();
if (repeat is null) return;
- FollowUpLabel = repeat.IsMakeUp ? "Nachschreibarbeit" : "Wiederholung";
- var rule = repeat.IsMakeUp ? "ersetzt fehlende Note" : repeat.RepeatGradeRule switch
+ FollowUpLabel = repeat.IsSupportWork ? "Förderarbeit" : repeat.IsMakeUp ? "Nachschreibarbeit" : "Wiederholung";
+ var rule = repeat.ReplacesMissingGrade() ? "ersetzt fehlende Note" : repeat.RepeatGradeRule switch
{
RepeatGradeRule.Repeat => "Wiederholung zählt",
RepeatGradeRule.Original => "Erste Arbeit zählt",
diff --git a/LehrerApp.Desktop/ViewModels/Groups/ExamViewModels.cs b/LehrerApp.Desktop/ViewModels/Groups/ExamViewModels.cs
index e18694b..8ee5101 100644
--- a/LehrerApp.Desktop/ViewModels/Groups/ExamViewModels.cs
+++ b/LehrerApp.Desktop/ViewModels/Groups/ExamViewModels.cs
@@ -47,19 +47,33 @@ public partial class ExamDialogViewModel : ObservableObject
[ObservableProperty] private string _repeatError = "";
[ObservableProperty] private string _taskPointsError = "";
[ObservableProperty] private string _kindName = ExamKind.Klausur.Label();
- [ObservableProperty] private bool _isMakeUp;
+ [ObservableProperty] private string _followUpKindName = WiederholungName;
+
+ private const string WiederholungName = "Wiederholung";
+ private const string NachschreibarbeitName = "Nachschreibarbeit";
+ private const string FoerderarbeitName = "Förderarbeit";
+ // Beim Befüllen aus einer vorhandenen Arbeit darf die Datums-Übernahme das gespeicherte Datum
+ // nicht überschreiben.
+ private bool _initializing = true;
public bool IsDifferentiated { get; }
public string[] NiveauOptions => NiveauDisplay.Options;
public string[] KindOptions { get; } = Enum.GetValues().Select(k => k.Label()).ToArray();
public string[] RepeatRuleOptions => ["Bessere zählt", "Wiederholung zählt", "Erste Arbeit zählt"];
+ public string[] FollowUpKindOptions { get; } = [WiederholungName, NachschreibarbeitName, FoerderarbeitName];
+ public bool IsMakeUp => FollowUpKindName == NachschreibarbeitName;
+ public bool IsSupportWork => FollowUpKindName == FoerderarbeitName;
public bool HasSelectedOriginalExam => SelectedOriginalExam is not null;
/// Art und Zählweise sind bei einer verknüpften Folgearbeit festgelegt: sie erbt die Art der
- /// ersten Arbeit, eine Nachschreibarbeit ersetzt nur fehlende Noten (keine Regelwahl nötig).
+ /// ersten Arbeit, Nachschreib- und Förderarbeiten ersetzen nur fehlende Noten (keine Regelwahl nötig).
public bool CanChooseKind => !HasSelectedOriginalExam;
- public bool ShowRepeatRule => HasSelectedOriginalExam && !IsMakeUp;
- public string LinkedExamLabel => IsMakeUp
- ? "Nachschreibarbeit zu (optional)" : "Wiederholung oder Nachschreibarbeit zu (optional)";
+ public bool ShowRepeatRule => HasSelectedOriginalExam && FollowUpKindName == WiederholungName;
+ public string LinkedExamLabel => FollowUpKindName switch
+ {
+ NachschreibarbeitName => "Nachschreibarbeit zu (optional)",
+ FoerderarbeitName => "Förderarbeit zu (optional)",
+ _ => "Wiederholung, Nachschreib- oder Förderarbeit zu (optional)",
+ };
private ExamKind Kind => Enum.GetValues().FirstOrDefault(k => k.Label() == KindName);
public ObservableCollection AvailableOriginalExams { get; } = [];
@@ -128,7 +142,9 @@ public partial class ExamDialogViewModel : ObservableObject
if (!isDuplicate)
{
SelectedOriginalExam = AvailableOriginalExams.FirstOrDefault(e => e.Id == source.RepeatOfExamId);
- IsMakeUp = source.IsMakeUp && SelectedOriginalExam is not null;
+ if (SelectedOriginalExam is not null)
+ FollowUpKindName = source.IsSupportWork ? FoerderarbeitName
+ : source.IsMakeUp ? NachschreibarbeitName : WiederholungName;
RepeatRuleName = source.RepeatGradeRule switch
{
RepeatGradeRule.Repeat => "Wiederholung zählt",
@@ -152,6 +168,7 @@ public partial class ExamDialogViewModel : ObservableObject
foreach (var t in Tasks) t.ShowWeight = UseWeighting;
RecomputeTotals();
UpdateWeekLoadHint();
+ _initializing = false;
}
partial void OnUseWeightingChanged(bool value)
@@ -161,17 +178,29 @@ public partial class ExamDialogViewModel : ObservableObject
partial void OnSelectedOriginalExamChanged(Exam? value)
{
if (value is not null) KindName = value.Kind.Label();
- else IsMakeUp = false;
+ else FollowUpKindName = WiederholungName;
OnPropertyChanged(nameof(HasSelectedOriginalExam));
OnPropertyChanged(nameof(CanChooseKind));
OnPropertyChanged(nameof(ShowRepeatRule));
+ TakeOverOriginalDateForSupportWork();
UpdateWeekLoadHint();
}
- partial void OnIsMakeUpChanged(bool value)
+ partial void OnFollowUpKindNameChanged(string value)
{
+ OnPropertyChanged(nameof(IsMakeUp));
+ OnPropertyChanged(nameof(IsSupportWork));
OnPropertyChanged(nameof(ShowRepeatRule));
OnPropertyChanged(nameof(LinkedExamLabel));
+ TakeOverOriginalDateForSupportWork();
+ }
+
+ /// Eine Förderarbeit wird meist parallel zur ersten Arbeit geschrieben: das Datum der
+ /// ersten Arbeit ist der Vorschlag, bleibt aber änderbar.
+ private void TakeOverOriginalDateForSupportWork()
+ {
+ if (_initializing || !IsSupportWork || SelectedOriginalExam is null) return;
+ DateText = SelectedOriginalExam.Date.ToString("dd.MM.yyyy");
}
partial void OnKindNameChanged(string value) => UpdateWeekLoadHint();
@@ -382,12 +411,25 @@ public partial class ExamDialogViewModel : ObservableObject
var gradingKey = BuildGradingKeyEntries();
var gradingKeyError = _grading.ValidateGradingKey(gradingKey);
if (gradingKeyError is not null) { GradingKeyValidation = gradingKeyError; valid = false; }
- if (SelectedOriginalExam is not null && SelectedOriginalExam.Date >= date)
+ // Datumsprüfung nur bei gültigem Datum — sonst meldet das Datumsfeld den Fehler bereits.
+ // Eine Förderarbeit entsteht parallel zur ersten Arbeit und darf am selben Tag liegen.
+ if (SelectedOriginalExam is not null && DateTextError == "")
{
- RepeatError = IsMakeUp
- ? "Die erste Arbeit muss vor der Nachschreibarbeit liegen."
- : "Die erste Arbeit muss vor der Wiederholung liegen.";
- valid = false;
+ if (IsSupportWork)
+ {
+ if (SelectedOriginalExam.Date > date)
+ {
+ RepeatError = "Die Förderarbeit darf nicht vor der ersten Arbeit liegen.";
+ valid = false;
+ }
+ }
+ else if (SelectedOriginalExam.Date >= date)
+ {
+ RepeatError = IsMakeUp
+ ? "Die erste Arbeit muss vor der Nachschreibarbeit liegen."
+ : "Die erste Arbeit muss vor der Wiederholung liegen.";
+ valid = false;
+ }
}
if (Tasks.Any(t => t.VariantName != "Beide") &&
(TotalPoints <= 0 || Tasks.Where(t => !t.IsBonus && t.VariantName != "A").Sum(t => t.MaxPoints) <= 0))
@@ -410,6 +452,7 @@ public partial class ExamDialogViewModel : ObservableObject
Result.RepeatOfExamId = SelectedOriginalExam?.Id;
Result.Kind = SelectedOriginalExam?.Kind ?? Kind;
Result.IsMakeUp = IsMakeUp && SelectedOriginalExam is not null;
+ Result.IsSupportWork = IsSupportWork && SelectedOriginalExam is not null;
Result.RepeatGradeRule = RepeatRuleName switch
{
"Wiederholung zählt" => RepeatGradeRule.Repeat,
diff --git a/LehrerApp.Desktop/ViewModels/Groups/GroupViewModels.cs b/LehrerApp.Desktop/ViewModels/Groups/GroupViewModels.cs
index c3498b8..a8ccc99 100644
--- a/LehrerApp.Desktop/ViewModels/Groups/GroupViewModels.cs
+++ b/LehrerApp.Desktop/ViewModels/Groups/GroupViewModels.cs
@@ -656,6 +656,7 @@ public class ExamSummary
NiveauLabel = e.Niveau.HasValue ? NiveauDisplay.ToName(e.Niveau) : "alle";
var hasVariants = e.Tasks.Any(t => t.Variant is not null);
var followUp = e.RepeatOfExamId is null ? null
+ : e.IsSupportWork ? (hasVariants ? "Förder · A/B" : "Förderarbeit")
: e.IsMakeUp ? (hasVariants ? "Nachschr. · A/B" : "Nachschreiben")
: hasVariants ? "Wdh. · A/B" : "Wiederholung";
// Folgearbeiten behalten ihre Art im Hintergrund (sie erben sie), die Spalte nennt nur
diff --git a/LehrerApp.Desktop/ViewModels/Groups/ReportGradeAssistantViewModels.cs b/LehrerApp.Desktop/ViewModels/Groups/ReportGradeAssistantViewModels.cs
index 4063417..ef3b276 100644
--- a/LehrerApp.Desktop/ViewModels/Groups/ReportGradeAssistantViewModels.cs
+++ b/LehrerApp.Desktop/ViewModels/Groups/ReportGradeAssistantViewModels.cs
@@ -373,7 +373,7 @@ public partial class ReportGradeAssistantViewModel : ObservableObject
if (!ReportGradeCalculator.ExamApplies(exam, membership)) continue;
var results = _calculator.ResultsByExam[exam.Id];
results.TryGetValue(studentId, out var own);
- // Nachschreib- und Wiederholungsarbeiten betreffen nur einzelne Schüler — ohne eigenes
+ // Nachschreib-, Förder- und Wiederholungsarbeiten betreffen nur einzelne Schüler — ohne eigenes
// Ergebnis gehören sie nicht in diese Übersicht.
if (own is null && exam.RepeatOfExamId.HasValue) continue;
@@ -396,7 +396,8 @@ public partial class ReportGradeAssistantViewModel : ObservableObject
var classAverage = classGrades.Count > 0 ? $"Ø Kurs {Format(_grading.WeightedAverage(classGrades))}" : "";
var kind = exam.Kind.Label();
- if (exam.IsMakeUp) kind += " · Nachschreibarbeit";
+ if (exam.IsSupportWork) kind += " · Förderarbeit";
+ else if (exam.IsMakeUp) kind += " · Nachschreibarbeit";
else if (exam.RepeatOfExamId.HasValue) kind += " · Wiederholung";
Works.Add(new AssistantWorkCard(exam.Title, exam.Date, kind, gradeDisplay, percentDisplay,
diff --git a/LehrerApp.Desktop/Views/Groups/ExamDialog.axaml b/LehrerApp.Desktop/Views/Groups/ExamDialog.axaml
index 01ceb31..0b8cb32 100644
--- a/LehrerApp.Desktop/Views/Groups/ExamDialog.axaml
+++ b/LehrerApp.Desktop/Views/Groups/ExamDialog.axaml
@@ -76,9 +76,9 @@
-
+ originalResults =
+ [
+ new() { ExamId = original.Id, StudentId = wrote, Grade = "3" },
+ new() { ExamId = original.Id, StudentId = supported, Absent = true },
+ ];
+
+ var wroteOriginal = ExamParticipantService.WroteOriginal(support, _ => originalResults);
+
+ Assert.Equal([wrote], wroteOriginal);
+ Assert.False(ExamParticipantService.Includes(support, null, false, wroteOriginal.Contains(wrote)));
+ Assert.True(ExamParticipantService.Includes(support, null, false, wroteOriginal.Contains(supported)));
+ }
+
[Fact]
public void Includes_Nachschreibarbeit_OhneErgebnisNurWerDieErsteArbeitVersaeumtHat()
{
diff --git a/LehrerApp.Tests/ExamScoringServiceTests.cs b/LehrerApp.Tests/ExamScoringServiceTests.cs
index d1564f8..564848c 100644
--- a/LehrerApp.Tests/ExamScoringServiceTests.cs
+++ b/LehrerApp.Tests/ExamScoringServiceTests.cs
@@ -97,6 +97,19 @@ public class ExamScoringServiceTests
[(original, "4"), (makeUp, "1")], GradingSystem.Grades1To6));
}
+ [Fact]
+ public void Foerderarbeit_ErsetztNurFehlendeNote()
+ {
+ var original = new Exam();
+ var support = new Exam { RepeatOfExamId = original.Id, IsSupportWork = true };
+
+ // Förderschüler hat die erste Arbeit nicht geschrieben: die Förderarbeit zählt allein.
+ Assert.Equal(["3"], ExamScoringService.EffectiveGrades([(support, "3")], GradingSystem.Grades1To6));
+ // Liegen beide Noten vor, zählt die erste — auch wenn die Förderarbeit besser ist.
+ Assert.Equal(["4"], ExamScoringService.EffectiveGrades(
+ [(original, "4"), (support, "1")], GradingSystem.Grades1To6));
+ }
+
[Fact]
public void MehrereFolgearbeiten_ZaehlenGenauEinmal()
{
diff --git a/TODO.md b/TODO.md
index 598fc5b..63e4768 100644
--- a/TODO.md
+++ b/TODO.md
@@ -59,6 +59,18 @@ Diese Regeln gelten für jede Aufgabe unten und müssen nicht wiederholt werden.
nicht in Klausurwochen-Belastung und Vorjahresvergleich. Folgearbeiten erben die Art.
Bestehende Klausuren bleiben unverändert (Default `Klausur`).
+- [x] **Nachtrag 2026-10: Förderarbeit als dritte Folgearbeits-Art.** Eine Förderarbeit
+ (`Exam.IsSupportWork`) ist eine inhaltlich eigene Arbeit, die einzelne Schüler statt der
+ parallel geschriebenen ersten Arbeit schreiben (Workflow: erste Arbeit per „Duplizieren“
+ kopieren, Aufgaben/Schlüssel anpassen, im Arbeitsdialog mit der ersten Arbeit verknüpfen).
+ Der Arbeitsdialog ersetzt die Nachschreib-Checkbox durch eine Auswahl
+ Wiederholung/Nachschreibarbeit/Förderarbeit; die Förderarbeit darf am selben Tag wie die erste
+ Arbeit liegen (nur *vor* der ersten Arbeit ist gesperrt) und übernimmt beim Auswählen dessen
+ Datum als änderbaren Vorschlag. Sie zählt wie eine Nachschreibarbeit
+ (`ExamFollowUpExtensions.ReplacesMissingGrade`): ihre Note ersetzt nur eine fehlende Note, die
+ Punkteeingabe blendet Schüler mit Note der ersten Arbeit aus — Schüler ohne Eintrag in der
+ ersten Arbeit (z. B. Kranke) lassen sich mit „Rest als abwesend markieren“ ausblenden.
+
Modelle `Exam`, `ExamTask`, `GradingKeyEntry`, `ExamResult` existieren bereits in
[Exam.cs](LehrerApp.Core/Models/Exam.cs), Repositories ebenfalls. Die komplette UI fehlt.
diff --git a/docs/Datenmodell.md b/docs/Datenmodell.md
index 567ce5f..97293e2 100644
--- a/docs/Datenmodell.md
+++ b/docs/Datenmodell.md
@@ -142,7 +142,10 @@ Einige berechnete Werte bleiben absichtlich gespeichert:
- `Exam.RepeatOfExamId` verweist auf die ursprüngliche Arbeit derselben Gruppe.
`RepeatGradeRule` legt fest, ob die bessere, die wiederholte oder die erste Note
zählt. Beide Ergebnisse bleiben sichtbar, gehen aber als ein Wert in die
- berechneten Durchschnitts- und Zeugnisnoten ein.
+ berechneten Durchschnitts- und Zeugnisnoten ein. `Exam.IsMakeUp` (Nachschreibarbeit) und
+ `Exam.IsSupportWork` (Förderarbeit) markieren Folgearbeiten nur für einzelne Schüler; ihre Note
+ ersetzt ausschließlich eine fehlende Note der ersten Arbeit. Anders als Nachschreibarbeiten darf
+ eine Förderarbeit am selben Tag wie die erste Arbeit liegen (sie wird parallel geschrieben).
- `CompetencyReference` (`Unit.Competencies`, `Lesson.Competencies`, `ExamTask.Competencies`,
`ParticipationSession.Competencies`) verweist über `ItemId` auf ein `CompetencyItem`, hält aber
zusätzlich `Code`/`Description` zum Zuordnungszeitpunkt fest (TODO 16.5, abgelöst von einer reinen