feat: Abwesenheits-Hinweise, Fehlquote bei Zeugnisnoten, Gruppen-Dokumentation
- Schnellbewerten-Dialog: abwesende Schüler werden gedimmt und mit ihrem Anwesenheitsstatus statt der Aspektbeschriftung angezeigt, damit keine Mitarbeitsnote für nicht anwesende Schüler vergeben wird. - Zeugnisnoten-Dialog: zeigt je Schüler die Fehlquote im gewählten Zeitraum, ab 50 % hervorgehoben (informativ, keine automatische Notenänderung). - Gruppen-Tab "Dokumentation" (bisher Platzhalter) implementiert: listet alle Dokumentationseinträge der Gruppen-Schüler, mit Schüler-Filter und optionalem "Nur dieser Unterricht"-Schalter. Einträge aus anderen Lerngruppen werden standardmäßig mitangezeigt, aber gedimmt. Der Dokumentationsdialog bekommt dafür einen optionalen Schüler-Picker. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -187,6 +187,60 @@ public sealed class DocumentationDialogViewModelTests
|
||||
Assert.NotEqual("", vm.AttachmentError);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OhneStudentOptions_CanPickStudentIstFalse()
|
||||
{
|
||||
var vm = new DocumentationDialogViewModel(Guid.NewGuid(), null, new FakeAttachmentStorage());
|
||||
|
||||
Assert.False(vm.CanPickStudent);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MitStudentOptions_OhneAuswahl_SaveSetztFehlerUndSpeichertNicht()
|
||||
{
|
||||
var anna = new StudentOption(Guid.NewGuid(), "Anna Beispiel");
|
||||
var vm = new DocumentationDialogViewModel(Guid.Empty, null, new FakeAttachmentStorage(), [anna])
|
||||
{
|
||||
Title = "Vorfall", TypeName = "Vorkommnis",
|
||||
};
|
||||
|
||||
vm.SaveCommand.Execute(null);
|
||||
|
||||
Assert.Null(vm.Result);
|
||||
Assert.NotEqual("", vm.StudentError);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MitStudentOptions_AuswahlGetroffen_SaveUebernimmtSchuelerUndGruppe()
|
||||
{
|
||||
var groupId = Guid.NewGuid();
|
||||
var anna = new StudentOption(Guid.NewGuid(), "Anna Beispiel");
|
||||
var ben = new StudentOption(Guid.NewGuid(), "Ben Muster");
|
||||
var vm = new DocumentationDialogViewModel(Guid.Empty, null, new FakeAttachmentStorage(),
|
||||
[anna, ben], groupId)
|
||||
{
|
||||
Title = "Vorfall", TypeName = "Vorkommnis", SelectedStudent = ben,
|
||||
};
|
||||
|
||||
vm.SaveCommand.Execute(null);
|
||||
|
||||
Assert.NotNull(vm.Result);
|
||||
Assert.Equal(ben.Id, vm.Result!.StudentId);
|
||||
Assert.Equal(groupId, vm.Result.GroupId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MitStudentOptions_Bearbeiten_SchuelerIstVorausgewaehlt()
|
||||
{
|
||||
var anna = new StudentOption(Guid.NewGuid(), "Anna Beispiel");
|
||||
var ben = new StudentOption(Guid.NewGuid(), "Ben Muster");
|
||||
var existing = new Documentation { StudentId = ben.Id, Title = "Alt" };
|
||||
|
||||
var vm = new DocumentationDialogViewModel(ben.Id, existing, new FakeAttachmentStorage(), [anna, ben]);
|
||||
|
||||
Assert.Equal(ben, vm.SelectedStudent);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DiscardUnsavedAttachments_EntferntNurNieGespeicherteAnhaenge()
|
||||
{
|
||||
|
||||
@@ -26,7 +26,8 @@ public sealed class GroupDetailViewModelTests
|
||||
new CompetencyOverviewTabViewModel(new FakeUnits(), exams, new FakeResults(),
|
||||
new FakeCompetencyDomains(), students, new CompetencyAnalysisService()),
|
||||
new SeatingPlanTabViewModel(new FakeSeatingPlans(), students, memberships,
|
||||
new FakeSessions([]), new FakeEntries(), new FakeAspects()));
|
||||
new FakeSessions([]), new FakeEntries(), new FakeAspects()),
|
||||
new GroupDocumentationTabViewModel(new FakeDocumentation(), students, groups));
|
||||
|
||||
vm.LoadGroup(group.Id);
|
||||
vm.SelectedExam = vm.Exams.First(e => e.Id == exam.Id);
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
using LehrerApp.Core.Models;
|
||||
using LehrerApp.Desktop.ViewModels.Groups;
|
||||
using LehrerApp.Desktop.ViewModels.Students;
|
||||
using Xunit;
|
||||
|
||||
namespace LehrerApp.Desktop.Tests;
|
||||
|
||||
/// Tests für den bisher als Platzhalter existierenden Gruppen-Tab "Dokumentation" (Nutzer-Feedback):
|
||||
/// listet Dokumentationseinträge aller Gruppen-Schüler, zeigt Einträge aus anderen Lerngruppen
|
||||
/// standardmäßig mit an (optisch abgesetzt statt versteckt), mit optionalem Filter darauf.
|
||||
public sealed class GroupDocumentationTabViewModelTests
|
||||
{
|
||||
private static GroupDocumentationTabViewModel BuildVm(
|
||||
List<Student> students, List<LearningGroup> groups, FakeDocumentation? docs = null)
|
||||
{
|
||||
var vm = new GroupDocumentationTabViewModel(docs ?? new FakeDocumentation(),
|
||||
new FakeStudents(students), new FakeGroups(groups));
|
||||
return vm;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Initialize_ZeigtEintraegeAllerGruppenschuelerSortiertNachDatumAbsteigend()
|
||||
{
|
||||
var group = new LearningGroup { Name = "9c" };
|
||||
var anna = new Student { FirstName = "Anna", LastName = "Beispiel" };
|
||||
var ben = new Student { FirstName = "Ben", LastName = "Muster" };
|
||||
var docs = new FakeDocumentation();
|
||||
docs.Add(new Documentation { StudentId = anna.Id, GroupId = group.Id, Title = "Alt", Date = new DateOnly(2025, 9, 1) });
|
||||
docs.Add(new Documentation { StudentId = ben.Id, GroupId = group.Id, Title = "Neu", Date = new DateOnly(2025, 9, 10) });
|
||||
|
||||
var vm = BuildVm([anna, ben], [group], docs);
|
||||
vm.Initialize(group.Id);
|
||||
|
||||
Assert.Equal(2, vm.Entries.Count);
|
||||
Assert.Equal("Neu", vm.Entries[0].Model.Title);
|
||||
Assert.Equal("Alt", vm.Entries[1].Model.Title);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Initialize_EintragAusAndererGruppe_WirdMitangezeigtAberAlsFremdMarkiert()
|
||||
{
|
||||
var group = new LearningGroup { Name = "9c" };
|
||||
var otherGroup = new LearningGroup { Name = "Mathematik 9b" };
|
||||
var anna = new Student { FirstName = "Anna", LastName = "Beispiel" };
|
||||
var docs = new FakeDocumentation();
|
||||
docs.Add(new Documentation { StudentId = anna.Id, GroupId = otherGroup.Id, Title = "Aus anderem Kurs", Date = new DateOnly(2025, 9, 1) });
|
||||
|
||||
var vm = BuildVm([anna], [group, otherGroup], docs);
|
||||
vm.Initialize(group.Id);
|
||||
|
||||
var entry = Assert.Single(vm.Entries);
|
||||
Assert.False(entry.IsOwnGroup);
|
||||
Assert.Equal("Mathematik 9b", entry.OtherGroupLabel);
|
||||
Assert.True(entry.ContentOpacity < 1.0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Initialize_EintragOhneGruppe_GiltAlsEigen()
|
||||
{
|
||||
var group = new LearningGroup { Name = "9c" };
|
||||
var anna = new Student { FirstName = "Anna", LastName = "Beispiel" };
|
||||
var docs = new FakeDocumentation();
|
||||
docs.Add(new Documentation { StudentId = anna.Id, GroupId = null, Title = "Allgemein", Date = new DateOnly(2025, 9, 1) });
|
||||
|
||||
var vm = BuildVm([anna], [group], docs);
|
||||
vm.Initialize(group.Id);
|
||||
|
||||
var entry = Assert.Single(vm.Entries);
|
||||
Assert.True(entry.IsOwnGroup);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OnlyThisGroup_Aktiviert_BlendetEintraegeAusAnderenGruppenAus()
|
||||
{
|
||||
var group = new LearningGroup { Name = "9c" };
|
||||
var otherGroup = new LearningGroup { Name = "Mathematik 9b" };
|
||||
var anna = new Student { FirstName = "Anna", LastName = "Beispiel" };
|
||||
var docs = new FakeDocumentation();
|
||||
docs.Add(new Documentation { StudentId = anna.Id, GroupId = group.Id, Title = "Eigen", Date = new DateOnly(2025, 9, 1) });
|
||||
docs.Add(new Documentation { StudentId = anna.Id, GroupId = otherGroup.Id, Title = "Fremd", Date = new DateOnly(2025, 9, 2) });
|
||||
|
||||
var vm = BuildVm([anna], [group, otherGroup], docs);
|
||||
vm.Initialize(group.Id);
|
||||
vm.OnlyThisGroup = true;
|
||||
|
||||
var entry = Assert.Single(vm.Entries);
|
||||
Assert.Equal("Eigen", entry.Model.Title);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SelectedStudentFilter_AufEinzelnenSchueler_FiltertListe()
|
||||
{
|
||||
var group = new LearningGroup { Name = "9c" };
|
||||
var anna = new Student { FirstName = "Anna", LastName = "Beispiel" };
|
||||
var ben = new Student { FirstName = "Ben", LastName = "Muster" };
|
||||
var docs = new FakeDocumentation();
|
||||
docs.Add(new Documentation { StudentId = anna.Id, GroupId = group.Id, Title = "Anna-Eintrag", Date = new DateOnly(2025, 9, 1) });
|
||||
docs.Add(new Documentation { StudentId = ben.Id, GroupId = group.Id, Title = "Ben-Eintrag", Date = new DateOnly(2025, 9, 1) });
|
||||
|
||||
var vm = BuildVm([anna, ben], [group], docs);
|
||||
vm.Initialize(group.Id);
|
||||
vm.SelectedStudentFilter = vm.StudentFilterOptions.Single(s => s.Id == anna.Id);
|
||||
|
||||
var entry = Assert.Single(vm.Entries);
|
||||
Assert.Equal("Anna-Eintrag", entry.Model.Title);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AddDocumentation_SpeichertErgebnisUndLaedtNeu()
|
||||
{
|
||||
var group = new LearningGroup { Name = "9c" };
|
||||
var anna = new Student { FirstName = "Anna", LastName = "Beispiel" };
|
||||
var docs = new FakeDocumentation();
|
||||
var vm = BuildVm([anna], [group], docs);
|
||||
vm.Initialize(group.Id);
|
||||
|
||||
vm.OnEditDocumentation = (groupId, options, editing) =>
|
||||
{
|
||||
Assert.Equal(group.Id, groupId);
|
||||
Assert.Contains(options, o => o.Id == anna.Id);
|
||||
Assert.Null(editing);
|
||||
return Task.FromResult<Documentation?>(new Documentation
|
||||
{ StudentId = anna.Id, GroupId = groupId, Title = "Neu", Date = new DateOnly(2025, 9, 1) });
|
||||
};
|
||||
|
||||
await vm.AddDocumentationCommand.ExecuteAsync(null);
|
||||
|
||||
var entry = Assert.Single(vm.Entries);
|
||||
Assert.Equal("Neu", entry.Model.Title);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DeleteDocumentation_NachBestaetigung_EntferntEintragAusListe()
|
||||
{
|
||||
var group = new LearningGroup { Name = "9c" };
|
||||
var anna = new Student { FirstName = "Anna", LastName = "Beispiel" };
|
||||
var docs = new FakeDocumentation();
|
||||
docs.Add(new Documentation { StudentId = anna.Id, GroupId = group.Id, Title = "Weg", Date = new DateOnly(2025, 9, 1) });
|
||||
var vm = BuildVm([anna], [group], docs);
|
||||
vm.Initialize(group.Id);
|
||||
vm.OnConfirmDeleteDocumentation = _ => Task.FromResult(true);
|
||||
|
||||
await vm.DeleteDocumentationCommand.ExecuteAsync(vm.Entries[0]);
|
||||
|
||||
Assert.Empty(vm.Entries);
|
||||
}
|
||||
}
|
||||
@@ -27,6 +27,44 @@ public sealed class QuickInputViewModelTests
|
||||
Assert.Equal(1, vm.AspectRows[0].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AnwesenderSchueler_CurrentStudentIsAbsent_IstFalse()
|
||||
{
|
||||
var aspects = new List<AspectColumnDef>
|
||||
{
|
||||
new(new ParticipationAspect { Key = "a", Label = "A" }),
|
||||
};
|
||||
var rows = new List<ParticipationStudentRow>
|
||||
{
|
||||
new(Guid.NewGuid(), "Anna", new ParticipationEntry { Attendance = AttendanceStatus.Present }, aspects, []),
|
||||
};
|
||||
var vm = new QuickInputViewModel(rows, aspects);
|
||||
|
||||
Assert.False(vm.CurrentStudentIsAbsent);
|
||||
Assert.Equal(1.0, vm.CurrentStudentContentOpacity);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AbwesenderSchueler_CurrentStudentIsAbsent_IstTrueUndGedimmt()
|
||||
{
|
||||
var aspects = new List<AspectColumnDef>
|
||||
{
|
||||
new(new ParticipationAspect { Key = "a", Label = "A" }),
|
||||
};
|
||||
var rows = new List<ParticipationStudentRow>
|
||||
{
|
||||
new(Guid.NewGuid(), "Anna", new ParticipationEntry(), aspects, []),
|
||||
new(Guid.NewGuid(), "Ben", new ParticipationEntry { Attendance = AttendanceStatus.Unexcused }, aspects, []),
|
||||
};
|
||||
var vm = new QuickInputViewModel(rows, aspects);
|
||||
|
||||
vm.NextStudent(); // zu Ben, unentschuldigt abwesend
|
||||
|
||||
Assert.True(vm.CurrentStudentIsAbsent);
|
||||
Assert.Equal("Krank, unentschuldigt", vm.CurrentStudentAttendanceLabel);
|
||||
Assert.Equal(0.4, vm.CurrentStudentContentOpacity);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VorherigerAspekt_SpringtRueckwaertsMitUmlauf()
|
||||
{
|
||||
|
||||
@@ -15,14 +15,15 @@ public class ReportGradeCalculationTests
|
||||
|
||||
private static ReportGradeDialogViewModel BuildViewModel(
|
||||
FakeGrades grades, FakeSchemes schemes, FakeReportGrades reportGrades,
|
||||
FakeExams exams, FakeResults results)
|
||||
FakeExams exams, FakeResults results, FakeSessions? sessions = null, FakeEntries? entries = null)
|
||||
{
|
||||
var students = new FakeStudents([Anna, Ben]);
|
||||
var memberships = new FakeMemberships([]);
|
||||
var grading = new GradingService();
|
||||
|
||||
return new ReportGradeDialogViewModel(grades, exams, results, students, memberships,
|
||||
schemes, reportGrades, grading, GroupId, GroupType.Class, GradingSystem.Grades1To6,
|
||||
schemes, reportGrades, sessions ?? new FakeSessions([]), entries ?? new FakeEntries(),
|
||||
new AttendanceBalanceService(), grading, GroupId, GroupType.Class, GradingSystem.Grades1To6,
|
||||
"Testgruppe", "2025/26");
|
||||
}
|
||||
|
||||
@@ -68,6 +69,28 @@ public class ReportGradeCalculationTests
|
||||
Assert.Equal("–", ben.CalculatedDisplay);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AbsenceRatePercent_BerechnetFehlquoteAusSitzungenDieserGruppeImZeitraum()
|
||||
{
|
||||
var (_, _, exams, results) = BuildExams();
|
||||
var session1 = new ParticipationSession { GroupId = GroupId, Date = new DateOnly(2025, 9, 5) };
|
||||
var session2 = new ParticipationSession { GroupId = GroupId, Date = new DateOnly(2025, 9, 12) };
|
||||
var sessions = new FakeSessions([session1, session2]);
|
||||
var entries = new FakeEntries();
|
||||
entries.Add(new ParticipationEntry { SessionId = session1.Id, StudentId = Anna.Id, Attendance = AttendanceStatus.Present });
|
||||
entries.Add(new ParticipationEntry { SessionId = session2.Id, StudentId = Anna.Id, Attendance = AttendanceStatus.Unexcused });
|
||||
|
||||
var vm = BuildViewModel(new FakeGrades(), new FakeSchemes(), new FakeReportGrades(), exams, results,
|
||||
sessions, entries);
|
||||
|
||||
var anna = vm.Rows.Single(r => r.StudentId == Anna.Id);
|
||||
Assert.Equal(50.0, anna.AbsenceRatePercent);
|
||||
Assert.True(anna.HasHighAbsenceRate);
|
||||
var ben = vm.Rows.Single(r => r.StudentId == Ben.Id);
|
||||
Assert.Equal(0.0, ben.AbsenceRatePercent);
|
||||
Assert.False(ben.HasHighAbsenceRate);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ResolveScheme_BevorzugtGruppenspezifischesSchemaVorVoreinstellung()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user