LearningGroup.PreviousGroupId wird von GroupRolloverService jetzt automatisch gesetzt; neue Karte "Vorjahresvergleich" im Kurs-Tab Übersicht zeigt den gepoolten Klausurdurchschnitt neben dem der Vorgängergruppe, sobald dort Ergebnisse vorliegen. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -26,6 +26,13 @@ public class LearningGroup
|
||||
/// deshalb händisch je Lerngruppe/Schuljahr gepflegt werden - keine JSON-RPC-Methode liefert sie.
|
||||
/// </summary>
|
||||
public int? WebUntisLessonId { get; set; }
|
||||
/// <summary>
|
||||
/// Vorgängergruppe aus dem Hochstufen (<see cref="Services.GroupRolloverService"/>) —
|
||||
/// ermöglicht einen Schuljahresvergleich (z.B. Klausurschnitt) über die Kette hinweg.
|
||||
/// Wird ausschließlich beim Hochstufen automatisch gesetzt; vor Einführung dieses Felds
|
||||
/// hochgestufte Gruppen bleiben unverknüpft (kein rückwirkendes Verknüpfen vorgesehen).
|
||||
/// </summary>
|
||||
public Guid? PreviousGroupId { get; set; }
|
||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
@@ -60,6 +60,7 @@ public sealed class GroupRolloverService(
|
||||
IsActive = true,
|
||||
IsOwnClass = source.IsOwnClass,
|
||||
IsDifferentiated = source.IsDifferentiated,
|
||||
PreviousGroupId = source.Id,
|
||||
// WebUntisLessonId bewusst nicht übernommen: WebUntis vergibt sie pro Schuljahr neu,
|
||||
// eine übernommene alte lsid würde in der Folgegruppe stumm falsche Fehlzeiten liefern.
|
||||
};
|
||||
|
||||
@@ -18,8 +18,8 @@ public sealed class GroupDetailViewModelTests
|
||||
var grades = new FakeGrades();
|
||||
|
||||
var vm = new GroupDetailViewModel(groups, students, memberships, subjects, exams, grades, tasks,
|
||||
new GroupOverviewViewModel(new FakeLessons(), exams, new FakeSessions([]), new FakeEntries(), students,
|
||||
new FakeDocumentation(), tasks, new AttendanceBalanceService(), new SchoolYearService()),
|
||||
new GroupOverviewViewModel(new FakeLessons(), exams, new FakeResults(), new FakeSessions([]), new FakeEntries(), students,
|
||||
new FakeDocumentation(), tasks, groups, new AttendanceBalanceService(), new GradingService(), new SchoolYearService()),
|
||||
new ParticipationTabViewModel(new FakeSessions([]), new FakeEntries(), new FakeAspects(),
|
||||
students, memberships, groups, new FakeCompetencyDomains()),
|
||||
new GradeOverviewTabViewModel(grades, exams, new FakeResults(), students, memberships, new GradingService()),
|
||||
|
||||
@@ -12,10 +12,13 @@ public sealed class GroupOverviewViewModelTests
|
||||
{
|
||||
private static GroupOverviewViewModel NewVm(FakeLessons? lessons = null, FakeExams? exams = null,
|
||||
FakeSessions? sessions = null, FakeEntries? entries = null, FakeStudents? students = null,
|
||||
FakeDocumentation? documentation = null, FakeWorkTasks? tasks = null) =>
|
||||
new(lessons ?? new FakeLessons(), exams ?? new FakeExams([]), sessions ?? new FakeSessions([]),
|
||||
FakeDocumentation? documentation = null, FakeWorkTasks? tasks = null,
|
||||
FakeResults? results = null, FakeGroups? groups = null) =>
|
||||
new(lessons ?? new FakeLessons(), exams ?? new FakeExams([]), results ?? new FakeResults(),
|
||||
sessions ?? new FakeSessions([]),
|
||||
entries ?? new FakeEntries(), students ?? new FakeStudents([]), documentation ?? new FakeDocumentation(),
|
||||
tasks ?? new FakeWorkTasks(), new AttendanceBalanceService(), new SchoolYearService());
|
||||
tasks ?? new FakeWorkTasks(), groups ?? new FakeGroups([]), new AttendanceBalanceService(),
|
||||
new GradingService(), new SchoolYearService());
|
||||
|
||||
private static (GroupOverviewViewModel Vm, FakeLessons Lessons, FakeExams Exams,
|
||||
FakeSessions Sessions, FakeEntries Entries, FakeStudents Students, Guid GroupId) BuildScenario(
|
||||
@@ -76,6 +79,86 @@ public sealed class GroupOverviewViewModelTests
|
||||
Assert.Contains("Klassenarbeit 2", vm.NextExamLabel);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Vorjahresvergleich_OhneVerknuepfteVorgaengergruppe_BleibtVerborgen()
|
||||
{
|
||||
var groupId = Guid.NewGuid();
|
||||
var groups = new FakeGroups([new LearningGroup { Id = groupId, GradingSystem = GradingSystem.Grades1To6 }]);
|
||||
var vm = NewVm(groups: groups);
|
||||
|
||||
vm.Initialize(groupId, "Testkurs");
|
||||
|
||||
Assert.False(vm.HasYearComparison);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Vorjahresvergleich_VorgaengerOhneKlausurergebnisse_BleibtVerborgen()
|
||||
{
|
||||
var previousId = Guid.NewGuid();
|
||||
var groupId = Guid.NewGuid();
|
||||
var groups = new FakeGroups([
|
||||
new LearningGroup { Id = groupId, GradingSystem = GradingSystem.Grades1To6, PreviousGroupId = previousId },
|
||||
new LearningGroup { Id = previousId, GradingSystem = GradingSystem.Grades1To6 },
|
||||
]);
|
||||
var vm = NewVm(groups: groups);
|
||||
|
||||
vm.Initialize(groupId, "Testkurs");
|
||||
|
||||
Assert.False(vm.HasYearComparison);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Vorjahresvergleich_ZeigtDurchschnitteBeiderJahreAn()
|
||||
{
|
||||
var previousId = Guid.NewGuid();
|
||||
var groupId = Guid.NewGuid();
|
||||
var groups = new FakeGroups([
|
||||
new LearningGroup { Id = groupId, GradingSystem = GradingSystem.Grades1To6, PreviousGroupId = previousId },
|
||||
new LearningGroup { Id = previousId, GradingSystem = GradingSystem.Grades1To6 },
|
||||
]);
|
||||
|
||||
var previousExam = new Exam { Id = Guid.NewGuid(), GroupId = previousId };
|
||||
var currentExam = new Exam { Id = Guid.NewGuid(), GroupId = groupId };
|
||||
var exams = new FakeExams([previousExam, currentExam]);
|
||||
|
||||
var results = new FakeResults();
|
||||
results.Add(new ExamResult { ExamId = previousExam.Id, StudentId = Guid.NewGuid(), Grade = "2" });
|
||||
results.Add(new ExamResult { ExamId = previousExam.Id, StudentId = Guid.NewGuid(), Grade = "4" });
|
||||
results.Add(new ExamResult { ExamId = currentExam.Id, StudentId = Guid.NewGuid(), Grade = "1" });
|
||||
// Abwesende und leere Ergebnisse fließen bewusst nicht in den Schnitt ein.
|
||||
results.Add(new ExamResult { ExamId = currentExam.Id, StudentId = Guid.NewGuid(), Absent = true, Grade = "6" });
|
||||
|
||||
var vm = NewVm(exams: exams, results: results, groups: groups);
|
||||
|
||||
vm.Initialize(groupId, "Testkurs");
|
||||
|
||||
Assert.True(vm.HasYearComparison);
|
||||
Assert.Contains("Dieses Jahr: Ø 1,0 (1 Klausuren)", vm.YearComparisonLabel);
|
||||
Assert.Contains("Vorjahr: Ø 3,0 (2 Klausuren)", vm.YearComparisonLabel);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Vorjahresvergleich_OhneEigeneKlausurenZeigtNurVorjahr()
|
||||
{
|
||||
var previousId = Guid.NewGuid();
|
||||
var groupId = Guid.NewGuid();
|
||||
var groups = new FakeGroups([
|
||||
new LearningGroup { Id = groupId, GradingSystem = GradingSystem.Grades1To6, PreviousGroupId = previousId },
|
||||
new LearningGroup { Id = previousId, GradingSystem = GradingSystem.Grades1To6 },
|
||||
]);
|
||||
var previousExam = new Exam { Id = Guid.NewGuid(), GroupId = previousId };
|
||||
var exams = new FakeExams([previousExam]);
|
||||
var results = new FakeResults();
|
||||
results.Add(new ExamResult { ExamId = previousExam.Id, StudentId = Guid.NewGuid(), Grade = "3" });
|
||||
|
||||
var vm = NewVm(exams: exams, results: results, groups: groups);
|
||||
|
||||
vm.Initialize(groupId, "Testkurs");
|
||||
|
||||
Assert.True(vm.HasYearComparison);
|
||||
Assert.Contains("noch keine Klausur", vm.YearComparisonLabel);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Mitarbeit_OhneSitzungenZeigtHinweisOhneWarnung()
|
||||
{
|
||||
|
||||
@@ -33,12 +33,15 @@ public partial class GroupOverviewViewModel : ObservableObject
|
||||
|
||||
private readonly ILessonRepository _lessons;
|
||||
private readonly IExamRepository _exams;
|
||||
private readonly IExamResultRepository _examResults;
|
||||
private readonly IParticipationSessionRepository _sessions;
|
||||
private readonly IParticipationRepository _entries;
|
||||
private readonly IStudentRepository _students;
|
||||
private readonly IDocumentationRepository _documentation;
|
||||
private readonly IWorkTaskRepository _tasks;
|
||||
private readonly IGroupRepository _groups;
|
||||
private readonly AttendanceBalanceService _attendanceBalance;
|
||||
private readonly GradingService _grading;
|
||||
private readonly SchoolYearService _schoolYear;
|
||||
|
||||
private Guid _groupId;
|
||||
@@ -48,6 +51,8 @@ public partial class GroupOverviewViewModel : ObservableObject
|
||||
[ObservableProperty] private string _nextLessonLabel = "";
|
||||
[ObservableProperty] private bool _hasNextExam;
|
||||
[ObservableProperty] private string _nextExamLabel = "";
|
||||
[ObservableProperty] private bool _hasYearComparison;
|
||||
[ObservableProperty] private string _yearComparisonLabel = "";
|
||||
[ObservableProperty] private string _participationHintLabel = "";
|
||||
[ObservableProperty] private bool _participationHintIsStale;
|
||||
[ObservableProperty] private bool _hasOpenHomeworkCheck;
|
||||
@@ -105,13 +110,14 @@ public partial class GroupOverviewViewModel : ObservableObject
|
||||
}
|
||||
|
||||
public GroupOverviewViewModel(ILessonRepository lessons, IExamRepository exams,
|
||||
IParticipationSessionRepository sessions, IParticipationRepository entries,
|
||||
IStudentRepository students, IDocumentationRepository documentation, IWorkTaskRepository tasks,
|
||||
AttendanceBalanceService attendanceBalance, SchoolYearService schoolYear)
|
||||
IExamResultRepository examResults, IParticipationSessionRepository sessions,
|
||||
IParticipationRepository entries, IStudentRepository students,
|
||||
IDocumentationRepository documentation, IWorkTaskRepository tasks, IGroupRepository groups,
|
||||
AttendanceBalanceService attendanceBalance, GradingService grading, SchoolYearService schoolYear)
|
||||
{
|
||||
_lessons = lessons; _exams = exams; _sessions = sessions;
|
||||
_lessons = lessons; _exams = exams; _examResults = examResults; _sessions = sessions;
|
||||
_entries = entries; _students = students; _documentation = documentation; _tasks = tasks;
|
||||
_attendanceBalance = attendanceBalance; _schoolYear = schoolYear;
|
||||
_groups = groups; _attendanceBalance = attendanceBalance; _grading = grading; _schoolYear = schoolYear;
|
||||
}
|
||||
|
||||
public void Initialize(Guid groupId, string groupName)
|
||||
@@ -126,6 +132,7 @@ public partial class GroupOverviewViewModel : ObservableObject
|
||||
var today = DateOnly.FromDateTime(DateTime.Today);
|
||||
LoadNextLesson(today);
|
||||
LoadNextExam(today);
|
||||
LoadYearComparison();
|
||||
LoadParticipationHint(today);
|
||||
LoadOpenHomeworkCheck(today);
|
||||
LoadOpenExcuses(today);
|
||||
@@ -156,6 +163,38 @@ public partial class GroupOverviewViewModel : ObservableObject
|
||||
NextExamLabel = next is null ? "" : $"{next.Date:dd.MM.yyyy} — {next.Title}";
|
||||
}
|
||||
|
||||
// ── Klausurschnitt-Vorjahresvergleich (Nutzer-Feedback) ──────────────────
|
||||
//
|
||||
// Setzt voraus, dass diese Gruppe über GroupRolloverService aus einer Vorgängergruppe
|
||||
// hochgestuft wurde (LearningGroup.PreviousGroupId) — vor Einführung dieses Felds
|
||||
// hochgestufte Gruppen bleiben unverknüpft und zeigen keinen Vergleich. Nur sichtbar, wenn
|
||||
// im Vorjahr tatsächlich Klausurergebnisse vorliegen; ohne die ist kein Vergleich möglich,
|
||||
// auch wenn eine Verknüpfung besteht.
|
||||
|
||||
private void LoadYearComparison()
|
||||
{
|
||||
var group = _groups.GetById(_groupId);
|
||||
var previous = group?.PreviousGroupId is Guid previousId ? ComputeExamAverage(previousId) : null;
|
||||
if (previous is null) { HasYearComparison = false; return; }
|
||||
|
||||
var current = ComputeExamAverage(_groupId);
|
||||
YearComparisonLabel = current is null
|
||||
? $"Vorjahr: Ø {previous.Value.Average:0.0} ({previous.Value.Count} Klausuren) — dieses Jahr noch keine Klausur."
|
||||
: $"Dieses Jahr: Ø {current.Value.Average:0.0} ({current.Value.Count} Klausuren) · " +
|
||||
$"Vorjahr: Ø {previous.Value.Average:0.0} ({previous.Value.Count} Klausuren)";
|
||||
HasYearComparison = true;
|
||||
}
|
||||
|
||||
private (double Average, int Count)? ComputeExamAverage(Guid groupId)
|
||||
{
|
||||
var grades = _exams.GetByGroup(groupId)
|
||||
.SelectMany(e => _examResults.GetByExam(e.Id))
|
||||
.Where(r => !r.Absent && !string.IsNullOrWhiteSpace(r.Grade))
|
||||
.Select(r => (Grade: r.Grade!, Weight: 1.0))
|
||||
.ToList();
|
||||
return grades.Count == 0 ? null : (_grading.WeightedAverage(grades), grades.Count);
|
||||
}
|
||||
|
||||
/// Erinnert nicht an eine Note, sondern schlicht daran, überhaupt wieder eine Sitzung
|
||||
/// anzulegen — genau das vergisst man in Kursen, die man seltener unterrichtet, zuerst.
|
||||
private void LoadParticipationHint(DateOnly today)
|
||||
|
||||
@@ -66,6 +66,14 @@
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- Klausurschnitt-Vorjahresvergleich (Nutzer-Feedback) -->
|
||||
<Border Classes="card" IsVisible="{Binding HasYearComparison}">
|
||||
<StackPanel>
|
||||
<TextBlock Text="VORJAHRESVERGLEICH" Classes="cardTitle"/>
|
||||
<TextBlock Text="{Binding YearComparisonLabel}" TextWrapping="Wrap" Classes="cardBody"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- Mitarbeit -->
|
||||
<Border Classes="card">
|
||||
<StackPanel>
|
||||
|
||||
@@ -40,6 +40,7 @@ public sealed class GroupRolloverServiceTests
|
||||
Assert.True(target.IsDifferentiated);
|
||||
Assert.Null(target.WebUntisLessonId);
|
||||
Assert.False(source.IsActive);
|
||||
Assert.Equal(source.Id, target.PreviousGroupId);
|
||||
|
||||
var copied = Assert.Single(memberships.GetByGroup(target.Id));
|
||||
Assert.NotEqual(oldMembership.Id, copied.Id);
|
||||
|
||||
@@ -2530,6 +2530,19 @@ Hinweis in Kapitel 1 — betrifft auch Kurse, nicht nur Klassen.
|
||||
die einzige Bereichsnavigation. Das Verwaltungsmenü sitzt direkt an jeder Karte und ist per
|
||||
Automation-Namen zugänglich. `GroupListViewModelTests` prüfen Direktnavigation, kontextuelle
|
||||
Bearbeitung, Suche und Archivierung.
|
||||
- [x] **7.2.7** Schuljahresübergreifende Auswertung, erster Baustein: Klausurschnitt-Vergleich
|
||||
mit der Vorgängergruppe (Nutzer-Feedback). Neues Feld `LearningGroup.PreviousGroupId`, von
|
||||
`GroupRolloverService.RollOver` (7.2.2) automatisch auf die Ausgangsgruppe gesetzt — vor
|
||||
dieser Ergänzung hochgestufte Gruppen bleiben unverknüpft, ein rückwirkendes manuelles
|
||||
Verknüpfen ist bewusst nicht Teil dieses Durchgangs. Neue Karte "Vorjahresvergleich" im
|
||||
Kurs-Tab "Übersicht" (`GroupOverviewViewModel`) zeigt den gepoolten Klausurdurchschnitt
|
||||
(alle nicht-abwesenden `ExamResult`-Einträge über alle Klausuren der Gruppe, via
|
||||
`GradingService.WeightedAverage`) dieses Jahres neben dem der verknüpften Vorgängergruppe;
|
||||
nur sichtbar, wenn im Vorjahr tatsächlich Klausurergebnisse vorliegen. **Bewusst
|
||||
zurückgestellt:** ein Vergleich, welcher Notenschlüssel bei welcher Aufgabenstruktur wie
|
||||
gewirkt hat — `Exam.GradingKey` ist eine reine Momentaufnahme ohne Verweis auf die
|
||||
verwendete `GradingKeyTemplate`, eine saubere Gruppierung dafür bräuchte zunächst diesen
|
||||
Verweis.
|
||||
|
||||
### 7.3 Import
|
||||
|
||||
|
||||
Reference in New Issue
Block a user