diff --git a/LehrerApp.Core/Models/LearningGroup.cs b/LehrerApp.Core/Models/LearningGroup.cs
index 3dc490b..98dff4d 100644
--- a/LehrerApp.Core/Models/LearningGroup.cs
+++ b/LehrerApp.Core/Models/LearningGroup.cs
@@ -68,6 +68,11 @@ public class GroupMembership
public DateOnly? JoinedAt { get; set; }
public DateOnly? LeftAt { get; set; }
public Niveau? Niveau { get; set; }
+ /// Förderschüler-Markierung, unabhängig von der Niveaudifferenzierung der Gruppe
+ /// (): auch in einem nicht differenzierten Kurs
+ /// bekommen einzelne Schüler eigenes Fördermaterial. Auswertung siehe
+ /// .
+ public bool IsSupportStudent { get; set; }
}
public enum MembershipPeriod { FullYear, H1Only, H2Only, Custom }
public enum GroupType { Class, Course }
diff --git a/LehrerApp.Core/Services/ExamCoverSheetService.cs b/LehrerApp.Core/Services/ExamCoverSheetService.cs
index 43a0361..3eb20cb 100644
--- a/LehrerApp.Core/Services/ExamCoverSheetService.cs
+++ b/LehrerApp.Core/Services/ExamCoverSheetService.cs
@@ -54,6 +54,8 @@ 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;
+ // Förderarbeit: nur Förderschüler (außer es gibt hier bereits ein Ergebnis).
+ if (exam.IsSupportWork && !hasResult && !GroupMembershipService.IsSupportStudent(membership)) return false;
// 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/GroupMembershipService.cs b/LehrerApp.Core/Services/GroupMembershipService.cs
index f3c47c1..9028069 100644
--- a/LehrerApp.Core/Services/GroupMembershipService.cs
+++ b/LehrerApp.Core/Services/GroupMembershipService.cs
@@ -10,6 +10,11 @@ public enum SchoolYearPeriodKind { FullYear, H1, H2 }
///
public static class GroupMembershipService
{
+ /// Förderschüler ist, wer markiert ist oder in einer differenzierten Gruppe das
+ /// Förderniveau hat.
+ public static bool IsSupportStudent(GroupMembership? membership) =>
+ membership is { } m && (m.IsSupportStudent || m.Niveau == Niveau.Foerder);
+
public static bool IsActiveOn(GroupMembership membership, DateOnly date)
{
if (membership.JoinedAt is { } joinedAt && date < joinedAt) return false;
diff --git a/LehrerApp.Core/Services/GroupRolloverService.cs b/LehrerApp.Core/Services/GroupRolloverService.cs
index 80e0c62..1280d72 100644
--- a/LehrerApp.Core/Services/GroupRolloverService.cs
+++ b/LehrerApp.Core/Services/GroupRolloverService.cs
@@ -80,6 +80,7 @@ public sealed class GroupRolloverService(
JoinedAt = targetStart,
LeftAt = null,
Niveau = membership.Niveau,
+ IsSupportStudent = membership.IsSupportStudent,
});
}
diff --git a/LehrerApp.Desktop/Services/Mcp/Tools/GroupMembershipTools.cs b/LehrerApp.Desktop/Services/Mcp/Tools/GroupMembershipTools.cs
index 87b4031..c259939 100644
--- a/LehrerApp.Desktop/Services/Mcp/Tools/GroupMembershipTools.cs
+++ b/LehrerApp.Desktop/Services/Mcp/Tools/GroupMembershipTools.cs
@@ -17,6 +17,7 @@ public class GroupMembershipTools(
[Description("Schüler-ID.")] Guid studentId,
[Description("Lerngruppen-ID.")] Guid groupId,
[Description("Niveau: E, G oder Foerder. Unverändert lassen: weglassen.")] Niveau? niveau = null,
+ [Description("Förderschüler-Markierung (auch ohne Niveaudifferenzierung der Gruppe). Unverändert lassen: weglassen.")] bool? isSupportStudent = null,
[Description("Zeitraum: FullYear, H1Only, H2Only oder Custom. Unverändert lassen: weglassen.")] MembershipPeriod? period = null,
[Description("Beitrittsdatum bei Custom-Zeitraum, Format YYYY-MM-DD.")] DateOnly? joinedAt = null,
[Description("Austrittsdatum bei Custom-Zeitraum, Format YYYY-MM-DD.")] DateOnly? leftAt = null,
@@ -34,6 +35,7 @@ public class GroupMembershipTools(
var changes = new StringBuilder();
if (niveau is not null && niveau != target.Niveau) { changes.AppendLine($"Niveau: {NiveauName(target.Niveau)} → {NiveauName(niveau)}"); target.Niveau = niveau; }
+ if (isSupportStudent is not null && isSupportStudent != target.IsSupportStudent) { changes.AppendLine($"Förderschüler: {(target.IsSupportStudent ? "ja" : "nein")} → {(isSupportStudent.Value ? "ja" : "nein")}"); target.IsSupportStudent = isSupportStudent.Value; }
if (period is not null && period != target.Period) { changes.AppendLine($"Zeitraum: {target.Period} → {period}"); target.Period = period.Value; }
if (joinedAt is not null && joinedAt != target.JoinedAt) { changes.AppendLine($"Beitritt: {target.JoinedAt:dd.MM.yyyy} → {joinedAt:dd.MM.yyyy}"); target.JoinedAt = joinedAt; }
if (leftAt is not null && leftAt != target.LeftAt) { changes.AppendLine($"Austritt: {target.LeftAt:dd.MM.yyyy} → {leftAt:dd.MM.yyyy}"); target.LeftAt = leftAt; }
@@ -57,6 +59,7 @@ public class GroupMembershipTools(
{
Id = m.Id, StudentId = m.StudentId, GroupId = m.GroupId, AddedOn = m.AddedOn,
Period = m.Period, JoinedAt = m.JoinedAt, LeftAt = m.LeftAt, Niveau = m.Niveau,
+ IsSupportStudent = m.IsSupportStudent,
};
private static string NiveauName(Niveau? n) => n switch
diff --git a/LehrerApp.Desktop/ViewModels/Groups/GroupViewModels.cs b/LehrerApp.Desktop/ViewModels/Groups/GroupViewModels.cs
index a8ccc99..8ee3158 100644
--- a/LehrerApp.Desktop/ViewModels/Groups/GroupViewModels.cs
+++ b/LehrerApp.Desktop/ViewModels/Groups/GroupViewModels.cs
@@ -198,6 +198,8 @@ public partial class GroupDetailViewModel : ObservableObject
// bevor LoadGroup() läuft (siehe MainWindowViewModel.NavigateToGroupDetail), Group ist dann
// kurzzeitig null — ein verschachtelter Pfad würde dafür jedes Mal einen Binding-Fehler loggen.
public bool IsDifferentiated => Group?.IsDifferentiated ?? false;
+ /// In differenzierten Gruppen übernimmt das Förderniveau die Rolle der Markierung.
+ public bool ShowSupportColumn => !IsDifferentiated;
public bool IsReadOnly => Group is { IsActive: false };
public bool IsEditable => Group is { IsActive: true };
public string SubjectName { get; private set; } = "";
@@ -205,6 +207,7 @@ public partial class GroupDetailViewModel : ObservableObject
partial void OnGroupChanged(LearningGroup? value)
{
OnPropertyChanged(nameof(IsDifferentiated));
+ OnPropertyChanged(nameof(ShowSupportColumn));
OnPropertyChanged(nameof(IsReadOnly));
OnPropertyChanged(nameof(IsEditable));
NotifyWriteCommands();
@@ -324,6 +327,7 @@ public partial class GroupDetailViewModel : ObservableObject
var membership = _memberships.GetByStudentAndGroup(summary.Id, Group.Id);
if (membership is null) return;
membership.Niveau = summary.Niveau;
+ membership.IsSupportStudent = summary.IsSupportStudent;
_memberships.Save(membership);
}
@@ -587,6 +591,7 @@ public partial class StudentSummary : ObservableObject
public string AvatarTooltip { get; }
[ObservableProperty] private Niveau? _niveau;
+ [ObservableProperty] private bool _isSupportStudent;
public string NiveauName
{
@@ -612,8 +617,11 @@ public partial class StudentSummary : ObservableObject
? IsFormer ? $"Ausgetreten am {exitDate:dd.MM.yyyy}" : $"Austritt am {exitDate:dd.MM.yyyy}"
: "Aktiv";
_niveau = membership?.Niveau;
+ _isSupportStudent = membership?.IsSupportStudent ?? false;
}
+ partial void OnIsSupportStudentChanged(bool value) => OnChanged?.Invoke(this);
+
partial void OnNiveauChanged(Niveau? value)
{
OnPropertyChanged(nameof(NiveauName));
diff --git a/LehrerApp.Desktop/Views/Groups/ExamDialog.axaml b/LehrerApp.Desktop/Views/Groups/ExamDialog.axaml
index 0b8cb32..bac7b76 100644
--- a/LehrerApp.Desktop/Views/Groups/ExamDialog.axaml
+++ b/LehrerApp.Desktop/Views/Groups/ExamDialog.axaml
@@ -78,7 +78,7 @@
+ ToolTip.Tip="Wiederholung: Arbeit für alle, es zählt die gewählte Regel. Nachschreibarbeit: inhaltlich eigene Arbeit nur für Schüler, die die erste Arbeit versäumt haben. Förderarbeit: inhaltlich eigene Arbeit für die als Förderschüler markierten Schüler der Gruppe, die sie statt der ersten Arbeit schreiben (darf am selben Tag liegen; das Datum der ersten Arbeit wird vorgeschlagen). Bei Nachschreib- und Förderarbeit ersetzt die Note eine fehlende Note der ersten Arbeit, und die Punkteeingabe blendet Schüler mit vorhandener Note der ersten Arbeit aus."/>
+
+
+
+
+
+
+
diff --git a/LehrerApp.Tests/ExamCoverSheetServiceTests.cs b/LehrerApp.Tests/ExamCoverSheetServiceTests.cs
index beb4ff8..eaf2a12 100644
--- a/LehrerApp.Tests/ExamCoverSheetServiceTests.cs
+++ b/LehrerApp.Tests/ExamCoverSheetServiceTests.cs
@@ -76,8 +76,23 @@ public class ExamCoverSheetServiceTests
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)));
+ var flagged = new GroupMembership { IsSupportStudent = true };
+ Assert.False(ExamParticipantService.Includes(support, flagged, false, wroteOriginal.Contains(wrote)));
+ Assert.True(ExamParticipantService.Includes(support, flagged, false, wroteOriginal.Contains(supported)));
+ }
+
+ [Fact]
+ public void Includes_Foerderarbeit_NurMarkierteFoerderschueler()
+ {
+ var support = new Exam { IsSupportWork = true, RepeatOfExamId = Guid.NewGuid() };
+
+ Assert.True(ExamParticipantService.Includes(support, new GroupMembership { IsSupportStudent = true }, false, false));
+ // In differenzierten Gruppen gilt das Förderniveau als Markierung.
+ Assert.True(ExamParticipantService.Includes(support, new GroupMembership { Niveau = Niveau.Foerder }, false, false));
+ Assert.False(ExamParticipantService.Includes(support, new GroupMembership { Niveau = Niveau.G }, false, false));
+ Assert.False(ExamParticipantService.Includes(support, new GroupMembership(), false, false));
+ // Ein bereits vorhandenes Ergebnis bleibt sichtbar, auch ohne Markierung.
+ Assert.True(ExamParticipantService.Includes(support, new GroupMembership(), true, false));
}
[Fact]
diff --git a/LehrerApp.Tests/GroupRolloverServiceTests.cs b/LehrerApp.Tests/GroupRolloverServiceTests.cs
index c7c38e2..0373f49 100644
--- a/LehrerApp.Tests/GroupRolloverServiceTests.cs
+++ b/LehrerApp.Tests/GroupRolloverServiceTests.cs
@@ -19,7 +19,7 @@ public sealed class GroupRolloverServiceTests
var oldMembership = new GroupMembership
{
StudentId = Guid.NewGuid(), GroupId = source.Id, Period = MembershipPeriod.H2Only,
- JoinedAt = new DateOnly(2026, 2, 1), LeftAt = new DateOnly(2026, 7, 31), Niveau = Niveau.E,
+ JoinedAt = new DateOnly(2026, 2, 1), LeftAt = new DateOnly(2026, 7, 31), Niveau = Niveau.E, IsSupportStudent = true,
};
var groups = new MemoryGroups([source]);
var memberships = new MemoryMemberships([oldMembership]);
@@ -48,6 +48,7 @@ public sealed class GroupRolloverServiceTests
Assert.Equal(new DateOnly(2026, 8, 1), copied.JoinedAt);
Assert.Null(copied.LeftAt);
Assert.Equal(Niveau.E, copied.Niveau);
+ Assert.True(copied.IsSupportStudent);
var scheme = schemes.GetByGroup(target.Id);
Assert.NotNull(scheme);
diff --git a/TODO.md b/TODO.md
index 63e4768..945def2 100644
--- a/TODO.md
+++ b/TODO.md
@@ -70,6 +70,10 @@ Diese Regeln gelten für jede Aufgabe unten und müssen nicht wiederholt werden.
(`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.
+ Nachtrag: `GroupMembership.IsSupportStudent` markiert Förderschüler je Gruppe, auch ohne
+ Niveaudifferenzierung (Checkbox-Spalte „Förder“ in der Schülerliste; in differenzierten
+ Gruppen zählt das Förderniveau). Die Punkteeingabe/Deckblätter einer Förderarbeit zeigen nur
+ Förderschüler (plus Schüler mit vorhandenem Ergebnis), das Kranke-Ausblenden entfällt damit.
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 97293e2..c674876 100644
--- a/docs/Datenmodell.md
+++ b/docs/Datenmodell.md
@@ -29,6 +29,9 @@ keine Aufnahme oder Einschreibung an der Schule.
- `Period`: ganzes Schuljahr, erstes Halbjahr, zweites Halbjahr oder eigener Zeitraum
- `JoinedAt` / `LeftAt`: Grenzen eines eigenen Teilnahmezeitraums
- `Niveau`: optionale Niveaudifferenzierung
+- `IsSupportStudent`: Förderschüler-Markierung je Gruppe, unabhängig von der Niveaudifferenzierung
+ (auch ein nicht differenzierter Kurs kann Förderschüler mit eigenem Material haben); als
+ Förderschüler gilt außerdem, wer das Niveau `Foerder` hat (`GroupMembershipService.IsSupportStudent`)
Das Schuljahr wird über die Lerngruppe ermittelt und deshalb nicht zusätzlich
in der Gruppenzuordnung gespeichert. Für eine spätere echte Schulaufnahme wäre
@@ -145,7 +148,9 @@ Einige berechnete Werte bleiben absichtlich gespeichert:
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).
+ eine Förderarbeit am selben Tag wie die erste Arbeit liegen (sie wird parallel geschrieben). Die
+ Punkteeingabe einer Förderarbeit zeigt nur Förderschüler der Gruppe (`GroupMembership.IsSupportStudent`
+ oder Niveau `Foerder`) sowie Schüler mit bereits vorhandenem Ergebnis.
- `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