Sitzungen (3.3): aus Stunde erzeugen, löschen, Kalenderüberblick; Doppelstunden-Bugfix Serienerzeugung

Kapitel 3.3: neuer Button erzeugt eine Mitarbeitssitzung aus einer geplanten
Stunde (Datum/Thema, dupl.-sicher über Lesson.LessonId), Sitzungen lassen
sich jetzt mit Rückfrage bei vorhandenen Bewertungen löschen, und der
Dashboard-Kalender zeigt Sitzungen als dritten Termintyp neben Stunden/
Klausuren. Das Bearbeiten von Sitzungen war bereits vorhanden (undokumentiert
aus früherer Ad-hoc-Arbeit) und wird hier nur nachgezogen.

Bugfix: "Serie erzeugen" legte für Doppelstunden zwei Lessons mit gleichem
Datum an statt einer, da jede Stundenplan-Periode einzeln behandelt wurde.
Folgeperioden werden jetzt der Lesson der ersten Periode zugerechnet.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-19 22:17:46 +02:00
co-authored by Claude Sonnet 5
parent 0faa3f54de
commit f73bd6c9ec
14 changed files with 330 additions and 24 deletions
@@ -40,7 +40,8 @@ public sealed class DashboardViewModelTests
FakeReportGrades? reportGrades = null, FakeMemberships? memberships = null,
FakeWorkTasks? tasks = null, FakeStudents? students = null, FakeDocumentation? documentation = null,
DashboardSettingsService? dashboardSettings = null, FakeSchoolHolidays? schoolHolidays = null,
FakeLessons? lessons = null, FakeSubstitutionEntries? substitutions = null)
FakeLessons? lessons = null, FakeSubstitutionEntries? substitutions = null,
FakeSessions? sessions = null)
{
lessons ??= new FakeLessons();
lessons.Add(lesson);
@@ -48,7 +49,7 @@ public sealed class DashboardViewModelTests
new FakeGroups([group]), new FakeSubjects([]), lessons,
exams ?? new FakeExams([]), results ?? new FakeResults(), grades ?? new FakeGrades(),
reportGrades ?? new FakeReportGrades(), memberships ?? new FakeMemberships([]),
tasks ?? new FakeWorkTasks(), new FakeSessions([]), new FakeEntries(),
tasks ?? new FakeWorkTasks(), sessions ?? new FakeSessions([]), new FakeEntries(),
students ?? new FakeStudents([]), documentation ?? new FakeDocumentation(),
slots ?? new FakeTimetableSlots(), periodSchedule ?? NewPeriodSchedule(),
new AttendanceBalanceService(), new SchoolYearService(), dashboardSettings ?? NewDashboardSettings(),
@@ -223,6 +224,25 @@ public sealed class DashboardViewModelTests
Assert.Contains(vm.SelectedDayEvents, e => e.Kind == CalendarEventKind.Exam && e.Title == "Test");
}
[Fact]
public void Kalender_ZeigtMitarbeitssitzungenAlsEigenenTermintyp()
{
var today = DateOnly.FromDateTime(DateTime.Today);
var group = new LearningGroup { Name = "9c" };
var sessionDate = today.AddDays(3);
var session = new ParticipationSession { GroupId = group.Id, Date = sessionDate, Comment = "Aufsatz" };
var vm = BuildVm(group, new Lesson { GroupId = group.Id, Date = today },
sessions: new FakeSessions([session]));
var day = vm.CalendarDays.Single(d => d.Date == sessionDate);
Assert.True(day.HasSession);
vm.SelectCalendarDayCommand.Execute(day);
Assert.Contains(vm.SelectedDayEvents,
e => e.Kind == CalendarEventKind.ParticipationSession && e.Subtitle == "Aufsatz");
}
[Fact]
public void DashboardKacheln_SichtbarkeitUndReihenfolgeWerdenGespeichert()
{
@@ -85,7 +85,7 @@ public sealed class GenerateLessonSeriesDialogViewModelTests
}
[Fact]
public void Save_Doppelstunde_ErzeugtZweiStundenAmSelbenTagMitUnterschiedlicherStundennummer()
public void Save_Doppelstunde_ErzeugtNurEineStundeVerankertAnDerErstenPeriode()
{
var groupId = Guid.NewGuid();
var unitId = Guid.NewGuid();
@@ -98,9 +98,50 @@ public sealed class GenerateLessonSeriesDialogViewModelTests
vm.SaveCommand.Execute(null);
Assert.Equal(1, vm.Result!.Created);
var created = lessons.GetByGroupAndRange(groupId, from, from).Select(l => l.LessonNumber).ToList();
Assert.Equal([3], created);
}
[Fact]
public void Save_Dreifachstunde_ErzeugtNurEineStundeVerankertAnDerErstenPeriode()
{
var groupId = Guid.NewGuid();
var unitId = Guid.NewGuid();
var from = new DateOnly(2025, 9, 1);
var slots = new FakeTimetableSlots();
slots.Add(new TimetableSlot { GroupId = groupId, Weekday = from.DayOfWeek, PeriodNumber = 3 });
slots.Add(new TimetableSlot { GroupId = groupId, Weekday = from.DayOfWeek, PeriodNumber = 4 });
slots.Add(new TimetableSlot { GroupId = groupId, Weekday = from.DayOfWeek, PeriodNumber = 5 });
var lessons = new FakeLessons();
var vm = BuildVm(slots, lessons, new FakeSchoolHolidays(), unitId, groupId, from, from);
vm.SaveCommand.Execute(null);
Assert.Equal(1, vm.Result!.Created);
var created = lessons.GetByGroupAndRange(groupId, from, from).Select(l => l.LessonNumber).ToList();
Assert.Equal([3], created);
}
[Fact]
public void Save_ZweiGetrennteEinzelstundenAmSelbenTagBleibenGetrennt()
{
// Perioden 1 und 3 sind NICHT aufeinanderfolgend (Periode 2 fehlt) — keine Doppelstunde,
// beide bekommen eine eigene Lesson.
var groupId = Guid.NewGuid();
var unitId = Guid.NewGuid();
var from = new DateOnly(2025, 9, 1);
var slots = new FakeTimetableSlots();
slots.Add(new TimetableSlot { GroupId = groupId, Weekday = from.DayOfWeek, PeriodNumber = 1 });
slots.Add(new TimetableSlot { GroupId = groupId, Weekday = from.DayOfWeek, PeriodNumber = 3 });
var lessons = new FakeLessons();
var vm = BuildVm(slots, lessons, new FakeSchoolHolidays(), unitId, groupId, from, from);
vm.SaveCommand.Execute(null);
Assert.Equal(2, vm.Result!.Created);
var created = lessons.GetByGroupAndRange(groupId, from, from).Select(l => l.LessonNumber).OrderBy(n => n).ToList();
Assert.Equal([3, 4], created);
Assert.Equal([1, 3], created);
}
[Fact]
@@ -22,7 +22,7 @@ public sealed class GroupDetailViewModelTests
students, memberships, groups, new FakeCompetencyDomains()),
new GradeOverviewTabViewModel(grades, exams, new FakeResults(), students, memberships, new GradingService()),
new PlanningTabViewModel(new FakeUnits(), new FakeLessons(), groups, subjects,
new FakeCompetencyDomains(), TestSupport.BuildAiSettingsService()),
new FakeCompetencyDomains(), TestSupport.BuildAiSettingsService(), new FakeSessions([])),
new CompetencyOverviewTabViewModel(new FakeUnits(), exams, new FakeResults(),
new FakeCompetencyDomains(), students, new CompetencyAnalysisService()),
new SeatingPlanTabViewModel(new FakeSeatingPlans(), students, memberships,
@@ -59,4 +59,49 @@ public sealed class ParticipationSessionEditingTests
Assert.Equal("Nachbesprechung Elektrizität", sessions.GetById(session.Id)?.Comment);
Assert.Contains("Nachbesprechung Elektrizität", vm.SelectedSessionDisplay);
}
[Fact]
public async Task LoeschenBefehl_FragtBestaetigungAbUndEntferntSessionMitsamtBewertungen()
{
var group = new LearningGroup { Name = "8a", SchoolYear = "2026/27" };
var session = new ParticipationSession { GroupId = group.Id, Date = new DateOnly(2026, 8, 19) };
var sessions = new FakeSessions([session]);
var entries = new FakeEntries();
entries.Add(new ParticipationEntry { SessionId = session.Id, StudentId = Guid.NewGuid() });
var vm = new ParticipationTabViewModel(
sessions, entries, new FakeAspects(), new FakeStudents([]),
new FakeMemberships([]), new FakeGroups([group]), new FakeCompetencyDomains());
vm.Initialize(group.Id, group.SchoolYear);
vm.SelectedSession = vm.Sessions.Single(s => s.Id == session.Id);
int? passedEntryCount = null;
vm.OnConfirmDeleteSession = (_, count) =>
{
passedEntryCount = count;
return Task.FromResult(true);
};
await vm.DeleteSessionCommand.ExecuteAsync(null);
Assert.Equal(1, passedEntryCount);
Assert.Null(sessions.GetById(session.Id));
}
[Fact]
public async Task LoeschenBefehl_BrichtBeiAbgelehnterBestaetigungAb()
{
var group = new LearningGroup { Name = "8a", SchoolYear = "2026/27" };
var session = new ParticipationSession { GroupId = group.Id, Date = new DateOnly(2026, 8, 19) };
var sessions = new FakeSessions([session]);
var vm = new ParticipationTabViewModel(
sessions, new FakeEntries(), new FakeAspects(), new FakeStudents([]),
new FakeMemberships([]), new FakeGroups([group]), new FakeCompetencyDomains());
vm.Initialize(group.Id, group.SchoolYear);
vm.SelectedSession = vm.Sessions.Single(s => s.Id == session.Id);
vm.OnConfirmDeleteSession = (_, _) => Task.FromResult(false);
await vm.DeleteSessionCommand.ExecuteAsync(null);
Assert.NotNull(sessions.GetById(session.Id));
}
}
@@ -20,7 +20,7 @@ public class PlanningTabViewModelTests
var lessons = new FakeLessons();
var vm = new PlanningTabViewModel(units, lessons, groups, subjects, competencyDomains,
TestSupport.BuildAiSettingsService());
TestSupport.BuildAiSettingsService(), new FakeSessions([]));
vm.Initialize(groupId);
return (vm, units, lessons, groupId);
@@ -227,4 +227,71 @@ public class PlanningTabViewModelTests
Assert.Equal(["L->AB01", "Plenum"], vm.KnownShorthands);
}
[Fact]
public void CreateParticipationSession_LegtSitzungMitDatumUndThemaDerStundeAn()
{
var groupId = Guid.NewGuid();
var group = new LearningGroup { Id = groupId, Name = "Testgruppe" };
var groups = new FakeGroups([group]);
var units = new FakeUnits();
var lessons = new FakeLessons();
var sessions = new FakeSessions([]);
var vm = new PlanningTabViewModel(units, lessons, groups, new FakeSubjects([]),
new FakeCompetencyDomains(), TestSupport.BuildAiSettingsService(), sessions);
vm.Initialize(groupId);
var unit = new Unit { GroupId = groupId, Title = "Optik" };
units.Add(unit);
var lesson = new Lesson
{
UnitId = unit.Id, GroupId = groupId, Date = new DateOnly(2025, 9, 1), Topic = "Brechung",
};
lessons.Add(lesson);
vm.RefreshPlanning(unit.Id, lesson.Id);
vm.SelectedLesson = vm.Lessons.Single(l => l.Id == lesson.Id);
string? notified = null;
vm.OnNotify = m => notified = m;
vm.CreateParticipationSessionCommand.Execute(null);
var created = Assert.Single(sessions.GetByGroup(groupId));
Assert.Equal(lesson.Id, created.LessonId);
Assert.Equal(lesson.Date, created.Date);
Assert.Equal("Brechung", created.Comment);
Assert.Equal("Sitzung aus der Stunde erstellt.", notified);
}
[Fact]
public void CreateParticipationSession_ZweiterAufrufLegtKeineDoppelteSitzungAn()
{
var groupId = Guid.NewGuid();
var group = new LearningGroup { Id = groupId, Name = "Testgruppe" };
var groups = new FakeGroups([group]);
var units = new FakeUnits();
var lessons = new FakeLessons();
var sessions = new FakeSessions([]);
var vm = new PlanningTabViewModel(units, lessons, groups, new FakeSubjects([]),
new FakeCompetencyDomains(), TestSupport.BuildAiSettingsService(), sessions);
vm.Initialize(groupId);
var unit = new Unit { GroupId = groupId, Title = "Optik" };
units.Add(unit);
var lesson = new Lesson
{
UnitId = unit.Id, GroupId = groupId, Date = new DateOnly(2025, 9, 1), Topic = "Brechung",
};
lessons.Add(lesson);
vm.RefreshPlanning(unit.Id, lesson.Id);
vm.SelectedLesson = vm.Lessons.Single(l => l.Id == lesson.Id);
vm.CreateParticipationSessionCommand.Execute(null);
string? notified = null;
vm.OnNotify = m => notified = m;
vm.CreateParticipationSessionCommand.Execute(null);
Assert.Single(sessions.GetByGroup(groupId));
Assert.Equal("Für diese Stunde existiert bereits eine Sitzung.", notified);
}
}