Förderschüler je Gruppe markieren und Förderarbeit darauf beschränken
GroupMembership.IsSupportStudent markiert Förderschüler unabhängig von der Niveaudifferenzierung (Checkbox-Spalte in der Schülerliste; in differenzierten Gruppen zählt das Förderniveau). Punkteeingabe und Deckblätter einer Förderarbeit zeigen nur Förderschüler und Schüler mit vorhandenem Ergebnis. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
651c9cddf8
commit
41649b0a05
12 files changed
+64
-5
No files matched your search
@@ -68,6 +68,11 @@ public class GroupMembership
|
||||
public DateOnly? JoinedAt { get; set; }
|
||||
public DateOnly? LeftAt { get; set; }
|
||||
public Niveau? Niveau { get; set; }
|
||||
/// <summary>Förderschüler-Markierung, unabhängig von der Niveaudifferenzierung der Gruppe
|
||||
/// (<see cref="LearningGroup.IsDifferentiated"/>): auch in einem nicht differenzierten Kurs
|
||||
/// bekommen einzelne Schüler eigenes Fördermaterial. Auswertung siehe
|
||||
/// <see cref="Services.GroupMembershipService.IsSupportStudent"/>.</summary>
|
||||
public bool IsSupportStudent { get; set; }
|
||||
}
|
||||
public enum MembershipPeriod { FullYear, H1Only, H2Only, Custom }
|
||||
public enum GroupType { Class, Course }
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -10,6 +10,11 @@ public enum SchoolYearPeriodKind { FullYear, H1, H2 }
|
||||
/// </summary>
|
||||
public static class GroupMembershipService
|
||||
{
|
||||
/// <summary>Förderschüler ist, wer markiert ist oder in einer differenzierten Gruppe das
|
||||
/// Förderniveau hat.</summary>
|
||||
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;
|
||||
|
||||
@@ -80,6 +80,7 @@ public sealed class GroupRolloverService(
|
||||
JoinedAt = targetStart,
|
||||
LeftAt = null,
|
||||
Niveau = membership.Niveau,
|
||||
IsSupportStudent = membership.IsSupportStudent,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -78,7 +78,7 @@
|
||||
</Grid>
|
||||
<ComboBox ItemsSource="{Binding FollowUpKindOptions}" SelectedItem="{Binding FollowUpKindName}"
|
||||
IsVisible="{Binding HasSelectedOriginalExam}" HorizontalAlignment="Stretch"
|
||||
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 einzelne Schüler, 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."/>
|
||||
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."/>
|
||||
<ComboBox ItemsSource="{Binding RepeatRuleOptions}" SelectedItem="{Binding RepeatRuleName}"
|
||||
IsVisible="{Binding ShowRepeatRule}"/>
|
||||
<TextBlock Text="{Binding RepeatError}" Foreground="Red" FontSize="11"
|
||||
|
||||
@@ -96,6 +96,16 @@
|
||||
</DataTemplate>
|
||||
</DataGridTemplateColumn.CellTemplate>
|
||||
</DataGridTemplateColumn>
|
||||
<DataGridTemplateColumn Header="Förder" Width="80"
|
||||
IsVisible="{Binding ShowSupportColumn}">
|
||||
<DataGridTemplateColumn.CellTemplate>
|
||||
<DataTemplate x:DataType="vm:StudentSummary">
|
||||
<CheckBox IsChecked="{Binding IsSupportStudent}" IsEnabled="{Binding CanEdit}"
|
||||
HorizontalAlignment="Center" Margin="2"
|
||||
ToolTip.Tip="Förderschüler: bekommt eigenes Fördermaterial. Förderarbeiten zeigen in der Punkteeingabe nur markierte Schüler."/>
|
||||
</DataTemplate>
|
||||
</DataGridTemplateColumn.CellTemplate>
|
||||
</DataGridTemplateColumn>
|
||||
</DataGrid.Columns>
|
||||
</DataGrid>
|
||||
</ContentPage>
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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.
|
||||
|
||||
+6
-1
@@ -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
|
||||
|
||||
Reference in new issue
Block a user