diff --git a/LehrerApp.Desktop.Tests/TimetableViewModelTests.cs b/LehrerApp.Desktop.Tests/TimetableViewModelTests.cs index f1d3b69..399c869 100644 --- a/LehrerApp.Desktop.Tests/TimetableViewModelTests.cs +++ b/LehrerApp.Desktop.Tests/TimetableViewModelTests.cs @@ -712,6 +712,31 @@ public sealed class TimetableViewModelTests Assert.Equal("1", period2.BadgeText); } + [Fact] + public void Load_Doppelstunde_ZeigtLessonThemaInBeidenKacheln_AuchOhnePhasenUndStundenraster() + { + var date = DateInCurrentWeek(DayOfWeek.Monday); + var group = new LearningGroup { Name = "10c" }; + var slots = new FakeTimetableSlots(); + slots.Add(new TimetableSlot { GroupId = group.Id, Weekday = date.DayOfWeek, PeriodNumber = 3 }); + slots.Add(new TimetableSlot { GroupId = group.Id, Weekday = date.DayOfWeek, PeriodNumber = 4 }); + var lesson = new Lesson + { + GroupId = group.Id, Date = date, LessonNumber = 3, Topic = "Werkstoffprüfung", + Phases = [], + }; + var lessons = new FakeLessons(); + lessons.Add(lesson); + + var vm = BuildViewModel(slots, new FakeGroups([group]), lessons: lessons); + + var first = vm.WeekItems.Single(c => c.IsSlotCell && c.Weekday == date.DayOfWeek && c.PeriodNumber == 3); + var second = vm.WeekItems.Single(c => c.IsSlotCell && c.Weekday == date.DayOfWeek && c.PeriodNumber == 4); + Assert.Equal("Werkstoffprüfung", first.Topic); + Assert.Equal("Werkstoffprüfung", second.Topic); + Assert.Equal(lesson.Id, second.Lesson?.Id); + } + [Fact] public void Load_KeineAnstehendenFerien_KeinBadge() { diff --git a/LehrerApp.Desktop/ViewModels/Planning/TimetableViewModel.cs b/LehrerApp.Desktop/ViewModels/Planning/TimetableViewModel.cs index baab819..1fa95aa 100644 --- a/LehrerApp.Desktop/ViewModels/Planning/TimetableViewModel.cs +++ b/LehrerApp.Desktop/ViewModels/Planning/TimetableViewModel.cs @@ -570,9 +570,10 @@ public partial class TimetableViewModel : ObservableObject p.Material?.Contains("Experiment", StringComparison.OrdinalIgnoreCase) == true)) == true; /// Eine Doppelstunde wird als eine Lesson an der ersten Periode gespeichert. Für die zweite - /// Rasterzelle liefern wir dieselbe Lesson nur dann, wenn der Stundenplan dort unmittelbar - /// fortgesetzt wird und der geplante Verlauf länger als die erste Periode ist. Eine exakt an - /// der Zielperiode verankerte Lesson hat immer Vorrang. + /// Rasterzelle liefern wir dieselbe Lesson, wenn der Stundenplan dort unmittelbar mit derselben + /// Lerngruppe fortgesetzt wird. Das darf nicht von bereits eingetragenen Phasendauern abhängen: + /// Gerade ein früher Entwurf hat oft erst Datum und Thema, gehört aber trotzdem zum kompletten + /// Doppelstundenblock. Eine exakt an der Zielperiode verankerte Lesson hat immer Vorrang. private Lesson? FindLessonForSlot(Guid groupId, DateOnly date, int period) { var lessons = _lessons.GetByGroupAndDate(groupId, date); @@ -584,14 +585,22 @@ public partial class TimetableViewModel : ObservableObject var withoutPeriod = lessons.Where(l => l.LessonNumber is null).ToList(); if (withoutPeriod.Count == 1) return withoutPeriod[0]; - var previous = lessons.Where(l => l.LessonNumber is int p && p == period - 1) - .OrderByDescending(l => l.UpdatedAt).FirstOrDefault(); - if (previous?.LessonNumber is not int anchor) return null; - var isConsecutiveSlot = _slots.GetByGroup(groupId) - .Any(s => s.Weekday == date.DayOfWeek && s.PeriodNumber == period); - var firstPeriodMinutes = _periodSchedule.GetDurationMinutes(anchor); - return isConsecutiveSlot && firstPeriodMinutes > 0 && - previous.Phases.Sum(p => p.DurationMinutes) > firstPeriodMinutes ? previous : null; + var periodsInBlock = _slots.GetByGroup(groupId) + .Where(s => s.Weekday == date.DayOfWeek) + .Select(s => s.PeriodNumber) + .ToHashSet(); + + // Vom angefragten Slot innerhalb desselben lückenlosen Blocks rückwärts zur verankerten + // Lesson laufen. Damit funktionieren neben Doppelstunden auch seltene Dreifachblöcke. + for (var candidatePeriod = period - 1; + periodsInBlock.Contains(candidatePeriod) && periodsInBlock.Contains(candidatePeriod + 1); + candidatePeriod--) + { + var blockLesson = lessons.Where(l => l.LessonNumber == candidatePeriod) + .OrderByDescending(l => l.UpdatedAt).FirstOrDefault(); + if (blockLesson is not null) return blockLesson; + } + return null; } /// 4.5.4: Hat die letzte vor liegende Lesson dieser Gruppe eine