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