diff --git a/LehrerApp.Desktop.Tests/TimetableViewModelTests.cs b/LehrerApp.Desktop.Tests/TimetableViewModelTests.cs index ae641f8..3ff9d7e 100644 --- a/LehrerApp.Desktop.Tests/TimetableViewModelTests.cs +++ b/LehrerApp.Desktop.Tests/TimetableViewModelTests.cs @@ -14,10 +14,11 @@ public sealed class TimetableViewModelTests FakeSubjects? subjects = null, FakeLessons? lessons = null, FakeExams? exams = null, SchoolCalendarSettingsService? calendarSettings = null, FakeSupervisionDuties? supervisionDuties = null, FakeSubstitutionEntries? substitutions = null, - FakeUntisSlotMappings? untisMappings = null, WebUntisSettingsService? untisSettings = null) + FakeUntisSlotMappings? untisMappings = null, WebUntisSettingsService? untisSettings = null, + PeriodScheduleService? periodSchedule = null) { - // Bewusst kein "using": SchoolCalendarSettingsService liest den Pfad nur bei Bedarf - // (SetState), das Verzeichnis muss über die Lebensdauer des ViewModels bestehen bleiben. + // Bewusst kein "using": SchoolCalendarSettingsService/PeriodScheduleService lesen den Pfad + // nur bei Bedarf, das Verzeichnis muss über die Lebensdauer des ViewModels bestehen bleiben. var tempPath = System.IO.Path.Combine( System.IO.Path.GetTempPath(), $"lehrerapp-timetablevm-tests-{Guid.NewGuid():N}"); Directory.CreateDirectory(tempPath); @@ -28,7 +29,8 @@ public sealed class TimetableViewModelTests calendarSettings ?? new SchoolCalendarSettingsService(tempPath), new PublicHolidayService(), new SchoolYearService(), supervisionDuties ?? new FakeSupervisionDuties(), substitutions ?? new FakeSubstitutionEntries(), - untisMappings ?? new FakeUntisSlotMappings(), untisSettings ?? TestSupport.BuildWebUntisSettingsService()); + untisMappings ?? new FakeUntisSlotMappings(), untisSettings ?? TestSupport.BuildWebUntisSettingsService(), + periodSchedule ?? new PeriodScheduleService(tempPath)); } /// Nächstes Datum ab (inkl.) , das auf einen Wochentag Mo-Fr fällt — @@ -336,6 +338,80 @@ public sealed class TimetableViewModelTests Assert.Equal(lesson.Id, openedLesson?.Id); } + /// Nutzer-Feedback (zweite Runde): der Direktklick auf eine Wochenraster-Kachel soll + /// "einheitlicher" springen — bei einer Lesson HEUTE, während gerade Unterrichtszeit ist, + /// direkt in den Unterrichtsmodus statt in den (schreibgeschützten) Planungsviewer. + [Fact] + public async Task OpenWeekCell_LessonHeuteWaehrendUnterrichtszeit_OeffnetUnterrichtsmodus() + { + var group = new LearningGroup { Name = "Q1 Chemie" }; + var today = DateOnly.FromDateTime(DateTime.Today); + var slots = new FakeTimetableSlots(); + slots.Add(new TimetableSlot { GroupId = group.Id, Weekday = today.DayOfWeek, PeriodNumber = 1 }); + var lesson = new Lesson { GroupId = group.Id, Date = today, LessonNumber = 1, Topic = "Redox" }; + var lessons = new FakeLessons(); + lessons.Add(lesson); + var periodSchedule = BuildPeriodSchedule(1); + var vm = BuildViewModel(slots, new FakeGroups([group]), lessons: lessons, periodSchedule: periodSchedule); + Lesson? teachingModeLesson = null; + Lesson? viewerLesson = null; + vm.OnOpenTeachingMode = l => { teachingModeLesson = l; return Task.CompletedTask; }; + vm.OnOpenLessonViewer = l => { viewerLesson = l; return Task.CompletedTask; }; + + var cell = vm.WeekItems.Single(c => c.IsSlotCell && c.Weekday == today.DayOfWeek && c.PeriodNumber == 1); + await vm.OpenWeekCellCommand.ExecuteAsync(cell); + + Assert.Equal(lesson.Id, teachingModeLesson?.Id); + Assert.Null(viewerLesson); + } + + /// Dieselbe Stundenzeit passt, die Lesson liegt aber nicht heute — bleibt beim Viewer. + [Fact] + public async Task OpenWeekCell_LessonNichtHeuteTrotzPassenderUhrzeit_OeffnetViewer() + { + var group = new LearningGroup { Name = "Q1 Chemie" }; + var today = DateOnly.FromDateTime(DateTime.Today); + var otherWeekday = today.DayOfWeek == DayOfWeek.Monday ? DayOfWeek.Tuesday : DayOfWeek.Monday; + var otherDate = DateInCurrentWeek(otherWeekday); + var slots = new FakeTimetableSlots(); + slots.Add(new TimetableSlot { GroupId = group.Id, Weekday = otherWeekday, PeriodNumber = 1 }); + var lesson = new Lesson { GroupId = group.Id, Date = otherDate, LessonNumber = 1, Topic = "Redox" }; + var lessons = new FakeLessons(); + lessons.Add(lesson); + var periodSchedule = BuildPeriodSchedule(1); + var vm = BuildViewModel(slots, new FakeGroups([group]), lessons: lessons, periodSchedule: periodSchedule); + Lesson? teachingModeLesson = null; + Lesson? viewerLesson = null; + vm.OnOpenTeachingMode = l => { teachingModeLesson = l; return Task.CompletedTask; }; + vm.OnOpenLessonViewer = l => { viewerLesson = l; return Task.CompletedTask; }; + + var cell = vm.WeekItems.Single(c => c.IsSlotCell && c.Weekday == otherWeekday && c.PeriodNumber == 1); + await vm.OpenWeekCellCommand.ExecuteAsync(cell); + + Assert.Equal(lesson.Id, viewerLesson?.Id); + Assert.Null(teachingModeLesson); + } + + /// Weites Zeitfenster um herum (±2 Stunden), damit der Test nicht an + /// die exakte Toleranz von TimetableViewModel.TeachingTimeTolerance gebunden ist. + /// Start UND Ende exakt auf "jetzt" statt eines künstlich breiteren Fensters (z.B. ganzer Tag + /// via TimeOnly.MinValue/MaxValue): TimetableViewModel.IsAroundTeachingTime addiert selbst + /// noch ±10 Minuten Toleranz (TeachingTimeTolerance) auf Start/Ende — ein zusätzliches, vom + /// Test vorgegebenes Fenster würde nahe Mitternacht durch die TimeOnly-Arithmetik (wrappt bei + /// 24 Uhr) selbst kippen. "Jetzt" als Start/Ende bleibt nur dann riskant, wenn der Test + /// zufällig innerhalb der letzten/ersten 10 Minuten des Tages läuft — dasselbe inhärente + /// Randproblem hätte dann auch die Produktion, kein Testartefakt. + private static PeriodScheduleService BuildPeriodSchedule(int periodNumber) + { + var tempPath = System.IO.Path.Combine( + System.IO.Path.GetTempPath(), $"lehrerapp-periodschedule-tests-{Guid.NewGuid():N}"); + Directory.CreateDirectory(tempPath); + var service = new PeriodScheduleService(tempPath); + var now = TimeOnly.FromDateTime(DateTime.Now); + service.SetPeriods([new PeriodTimeEntry { PeriodNumber = periodNumber, Start = now, End = now }]); + return service; + } + [Fact] public void OpenSettings_RuftOnNavigateToSettingsAuf() { @@ -1178,44 +1254,42 @@ public sealed class TimetableViewModelTests } } -/// Nutzer-Feedback: unklar, wie man aus der "Heute"-Tagesliste zwischen Unterrichtsansicht, -/// Sitzplan, Planung und Planungsviewer wechselt — TodayLessonItem.DestinationOptions ersetzt die -/// bisherigen zwei Buttons durch ein Dropdown mit ausdrücklich benannten Zielen. -public sealed class TodayLessonItemDestinationOptionsTests +/// Nutzer-Feedback (zweite Runde): das Popup-Menü je Wochenraster-Kachel (TimetableView.axaml, +/// MenuFlyout) blendet "Unterrichtsansicht"/"Planungsviewer" per IsVisible="{Binding HasLesson}" +/// aus, wenn für den Slot noch keine Lesson existiert, und den ganzen Menü-Trigger per +/// IsVisible="{Binding HasGroupId}", wenn die Kachel gar keiner Gruppe zugeordnet ist (z.B. eine +/// GroupId-lose Vertretung). Diese beiden Properties sind die Grundlage dafür. +public sealed class WeekCellItemMenuVisibilityTests { [Fact] - public void MitLesson_BietetAlleVierZiele() + public void ForSlot_MitLesson_HatHasLessonUndHasGroupId() { var lesson = new Lesson { Topic = "Redox" }; - var item = new TodayLessonItem(Guid.NewGuid(), 3, "Q1 Chemie", "R204", - "#4C8DFF", "Redox", null, lesson: lesson); + var cell = WeekCellItem.ForSlot(DayOfWeek.Monday, 3, isToday: true, "Che", "Q1 Chemie", + "R204", "Redox", "#4C8DFF", "", hasExam: false, isLastBeforeExam: false, + hasExperiment: false, Guid.NewGuid(), isHoliday: false, lesson: lesson); - Assert.True(item.HasDestinationOptions); - Assert.Equal( - [ - TimetableLessonDestination.TeachingMode, TimetableLessonDestination.Viewer, - TimetableLessonDestination.SeatingPlan, TimetableLessonDestination.Planning, - ], - item.DestinationOptions.Select(o => o.Kind)); + Assert.True(cell.HasLesson); + Assert.True(cell.HasGroupId); } [Fact] - public void OhneLesson_BietetNurSitzplanUndPlanung() + public void ForSlot_OhneLesson_HatKeinHasLesson() { - var item = new TodayLessonItem(Guid.NewGuid(), 3, "Q1 Chemie", "R204", - "#4C8DFF", null, null); + var cell = WeekCellItem.ForSlot(DayOfWeek.Monday, 3, isToday: true, "Che", "Q1 Chemie", + "R204", "", "#4C8DFF", "", hasExam: false, isLastBeforeExam: false, + hasExperiment: false, Guid.NewGuid(), isHoliday: false); - Assert.Equal( - [TimetableLessonDestination.SeatingPlan, TimetableLessonDestination.Planning], - item.DestinationOptions.Select(o => o.Kind)); + Assert.False(cell.HasLesson); + Assert.True(cell.HasGroupId); } [Fact] - public void OhneGroupId_BietetKeineZiele() + public void ForSubstitutionLesson_OhneGroupId_HatKeinHasGroupId() { - var item = TodayLessonItem.ForSubstitution(3, new SubstitutionEntry { GroupId = null, Description = "Vertretung" }); + var cell = WeekCellItem.ForSubstitutionLesson(DayOfWeek.Monday, 3, isToday: true, + new SubstitutionEntry { GroupId = null, GroupLabel = "Fremde Klasse", Description = "Vertretung" }); - Assert.False(item.HasDestinationOptions); - Assert.Empty(item.DestinationOptions); + Assert.False(cell.HasGroupId); } } diff --git a/LehrerApp.Desktop/ViewModels/Planning/TimetableViewModel.cs b/LehrerApp.Desktop/ViewModels/Planning/TimetableViewModel.cs index 3030e9e..2560bff 100644 --- a/LehrerApp.Desktop/ViewModels/Planning/TimetableViewModel.cs +++ b/LehrerApp.Desktop/ViewModels/Planning/TimetableViewModel.cs @@ -65,6 +65,7 @@ public partial class TimetableViewModel : ObservableObject private readonly ISubstitutionEntryRepository _substitutions; private readonly IUntisSlotMappingRepository _untisMappings; private readonly WebUntisSettingsService _untisSettings; + private readonly PeriodScheduleService _periodSchedule; private readonly SchoolWeatherService? _schoolWeather; private readonly SemaphoreSlim _weatherGate = new(1, 1); private WeatherSnapshot? _weatherSnapshot; @@ -114,13 +115,14 @@ public partial class TimetableViewModel : ObservableObject PublicHolidayService publicHolidays, SchoolYearService schoolYear, ISupervisionDutyRepository supervisionDuties, ISubstitutionEntryRepository substitutions, IUntisSlotMappingRepository untisMappings, WebUntisSettingsService untisSettings, - SchoolWeatherService? schoolWeather = null) + PeriodScheduleService periodSchedule, SchoolWeatherService? schoolWeather = null) { _slots = slots; _groups = groups; _subjects = subjects; _lessons = lessons; _exams = exams; _schoolHolidays = schoolHolidays; _calendarSettings = calendarSettings; _publicHolidays = publicHolidays; _schoolYear = schoolYear; _supervisionDuties = supervisionDuties; _substitutions = substitutions; _untisMappings = untisMappings; _untisSettings = untisSettings; + _periodSchedule = periodSchedule; _schoolWeather = schoolWeather; Load(); } @@ -368,14 +370,41 @@ public partial class TimetableViewModel : ObservableObject await OnOpenTeachingMode(lesson); } + /// Nutzer-Feedback (zweite Runde): der Direktklick auf eine Wochenraster-Kachel soll + /// "einheitlicher" springen. Existiert eine Lesson UND ist gerade (heute, mit Toleranz vor/ + /// nach der Stunde) Unterrichtszeit, geht es direkt in den Unterrichtsmodus — sonst wie + /// bisher in den (schreibgeschützten) Planungsviewer bzw., ohne Lesson, zur Einheitenplanung + /// der Gruppe. Das Popup-Menü (siehe TimetableView.axaml, MenuFlyout je Kachel) bietet + /// daneben immer alle vier Ziele explizit an, unabhängig von dieser Automatik. [RelayCommand] private async Task OpenWeekCell(WeekCellItem? item) { if (item is null || item.GroupId == Guid.Empty) return; - if (item.Lesson is { } lesson && OnOpenLessonViewer is not null) await OnOpenLessonViewer(lesson); + if (item.Lesson is { } lesson) + { + if (IsAroundTeachingTime(lesson.Date, item.PeriodNumber) && OnOpenTeachingMode is not null) + await OnOpenTeachingMode(lesson); + else if (OnOpenLessonViewer is not null) await OnOpenLessonViewer(lesson); + } else OnNavigateToGroup?.Invoke(item.GroupId); } + /// Bewusst mit Toleranz vor/nach der eingetragenen Stundenzeit (siehe + /// , "Stundenraster" in den Einstellungen) statt exakt an + /// Start-/Endzeit gebunden — man klickt auch kurz vor Stundenbeginn oder in einer kurzen + /// Verzögerung danach noch typischerweise in Unterrichtsabsicht. Ohne konfiguriertes + /// Stundenraster oder an einem anderen Tag als heute bleibt es beim Planungsviewer. + /// Nimmt bewusst das Datum der Lesson selbst statt WeekCellItem.Date entgegen — Date ist dort + /// nur bei Kopfzeilen (WeekdayHeader) gesetzt, nicht bei regulären Stunden-Kacheln (ForSlot). + private static readonly TimeSpan TeachingTimeTolerance = TimeSpan.FromMinutes(10); + private bool IsAroundTeachingTime(DateOnly lessonDate, int periodNumber) + { + if (lessonDate != DateOnly.FromDateTime(DateTime.Today)) return false; + if (_periodSchedule.GetTimes(periodNumber) is not { } times) return false; + var now = TimeOnly.FromDateTime(DateTime.Now); + return now >= times.Start.Add(-TeachingTimeTolerance) && now <= times.End.Add(TeachingTimeTolerance); + } + [RelayCommand] private void OpenSettings() => OnNavigateToSettings?.Invoke(SettingsTab.Holidays); @@ -784,9 +813,11 @@ public partial class WeekCellItem : ObservableObject public bool HasSupervision => SupervisionLocation.Length > 0; public bool HasRoom => !string.IsNullOrWhiteSpace(Room); public bool HasTopic => !string.IsNullOrWhiteSpace(Topic); + public bool HasGroupId => GroupId != Guid.Empty; /// Nur bei einer regulären, zugewiesenen Zelle mit bereits existierender Lesson gesetzt — /// Grundlage für den Direktsprung in den Verlaufsplan-Viewer (4.5.2). public Lesson? Lesson { get; private init; } + public bool HasLesson => Lesson is not null; public string OpenButtonLabel => Lesson is not null ? "Verlaufsplan ansehen" : "Zur Lerngruppe"; [ObservableProperty] private string _weatherSymbol = ""; [ObservableProperty] private string _weatherTooltip = ""; @@ -925,33 +956,6 @@ public class TodayLessonItem public bool HasLesson => Lesson is not null; public string OpenButtonLabel => Lesson is not null ? "Verlaufsplan ansehen" : "Zur Lerngruppe"; - /// Nutzer-Feedback: es war unklar, wie man aus der "Heute"-Tagesliste zwischen - /// Unterrichtsansicht, Sitzplan, Planung und Planungsviewer wechselt — bisher zwei Buttons - /// mit unterschiedlicher, teils vom Lesson-Status abhängiger Bedeutung - /// ("Verlaufsplan ansehen"/"Zur Lerngruppe"). Ein Dropdown mit ausdrücklich benannten Zielen - /// statt dessen (Wiring/Routing in TimetableView.axaml.cs). Unterrichtsansicht/Planungsviewer - /// brauchen eine existierende Lesson, Sitzplan/Planung sind auch ohne bereits möglich (Planung - /// ist ohnehin der Ort, an dem man eine Lesson für den Slot erst anlegt). - public IReadOnlyList DestinationOptions - { - get - { - var options = new List(); - if (HasLesson) - { - options.Add(new(TimetableLessonDestination.TeachingMode, "▶ Unterrichtsansicht")); - options.Add(new(TimetableLessonDestination.Viewer, "📋 Planungsviewer")); - } - if (HasGroupId) - { - options.Add(new(TimetableLessonDestination.SeatingPlan, "🪑 Sitzplan")); - options.Add(new(TimetableLessonDestination.Planning, "✏️ Planung")); - } - return options; - } - } - public bool HasDestinationOptions => DestinationOptions.Count > 0; - public TodayLessonItem(Guid groupId, int periodNumber, string groupName, string room, string colorHex, string? lessonTopic, string? examTitle, bool hasUnhandledHomework = false, Lesson? lesson = null) diff --git a/LehrerApp.Desktop/Views/Planning/TimetableView.axaml b/LehrerApp.Desktop/Views/Planning/TimetableView.axaml index 11ed202..f1c35fb 100644 --- a/LehrerApp.Desktop/Views/Planning/TimetableView.axaml +++ b/LehrerApp.Desktop/Views/Planning/TimetableView.axaml @@ -21,6 +21,21 @@ + + @@ -111,24 +126,17 @@ Foreground="#8E6C00" FontWeight="SemiBold" IsVisible="{Binding HasUnhandledHomework}"/> - - - - - - - - + + + + + + diff --git a/LehrerApp.Desktop/Views/Planning/TimetableView.axaml.cs b/LehrerApp.Desktop/Views/Planning/TimetableView.axaml.cs index 4026c26..c094308 100644 --- a/LehrerApp.Desktop/Views/Planning/TimetableView.axaml.cs +++ b/LehrerApp.Desktop/Views/Planning/TimetableView.axaml.cs @@ -1,4 +1,5 @@ using Avalonia.Controls; +using Avalonia.Interactivity; using LehrerApp.Core.Interfaces; using LehrerApp.Core.Models; using LehrerApp.Core.Services; @@ -28,35 +29,42 @@ public partial class TimetableView : UserControl } } - /// Nutzer-Feedback: unklar, wie man aus der "Heute"-Tagesliste zwischen Unterrichtsansicht, - /// Sitzplan, Planung und Planungsviewer wechselt. Statt der bisherigen zwei Buttons mit - /// teils vom Lesson-Status abhängiger Doppelbedeutung ein Dropdown mit ausdrücklich benannten - /// Zielen (TodayLessonItem.DestinationOptions) — Menü-Charakter, die Auswahl wird danach - /// bewusst zurückgesetzt statt dauerhaft angezeigt. Drei der vier Ziele nutzen unverändert - /// die bestehenden TimetableViewModel-Commands, nur "Sitzplan" ist neu (bisher nur über den - /// Unterrichtsmodus erreichbar, siehe TODO.md 4.5.23-Nachtrag). - private void OnLessonDestinationSelected(object? sender, SelectionChangedEventArgs e) + /// Nutzer-Feedback (zweite Runde — die erste Fassung hatte das Problem an der falschen + /// Stelle gelöst, in der "Heute"-Tagesliste statt im Wochenraster): im Wochenraster oben + /// führt ein Klick auf eine Stunden-Kachel bislang entweder in den Planungsviewer oder zur + /// Einheitenplanung — je nachdem, ob schon eine Lesson existiert, ohne dass das von außen + /// erkennbar wäre. Popup-Menü (MenuFlyout, kein ComboBox mehr) mit allen vier Zielen als + /// Alternative zum Direktklick, siehe + /// für den "einheitlicheren" Standard-Klick (Unterrichtsansicht bei laufender Stunde, sonst + /// Planungsviewer, sonst Einheitenplanung). MenuItem.Click statt Command-Binding: ein + /// $parent[ItemsControl]-Vorfahrenpfad (wie beim Zeilen-Button) funktioniert innerhalb eines + /// Flyouts nicht zuverlässig, weil dessen Popup nicht im normalen visuellen Baum hängt (siehe + /// TODO.md-Nachtrag zum Klassenlehrer-Bereich) — DataContext-Vererbung (kein Pfad-Suchen, + /// nur der normale Eltern-Wert) funktioniert dort aber sehr wohl, deshalb liest der Handler + /// die Zelle über ((MenuItem)sender).DataContext statt über eine Binding. + private async void OnWeekCellMenuItemClick(object? sender, RoutedEventArgs e) { - if (sender is not ComboBox comboBox) return; - var item = comboBox.DataContext as TodayLessonItem; - var option = e.AddedItems.Count > 0 ? e.AddedItems[0] as TimetableDestinationOption : null; - comboBox.SelectedItem = null; - if (item is null || option is null || DataContext is not TimetableViewModel vm) return; + if (sender is not MenuItem { Tag: TimetableLessonDestination destination } menuItem) return; + if (menuItem.DataContext is not WeekCellItem cell) return; + if (DataContext is not TimetableViewModel vm) return; - switch (option.Kind) + switch (destination) { case TimetableLessonDestination.TeachingMode: - vm.StartTeachingModeCommand.Execute(item); + if (cell.Lesson is { } teachLesson && vm.OnOpenTeachingMode is not null) + await vm.OnOpenTeachingMode(teachLesson); break; case TimetableLessonDestination.Viewer: - vm.OpenTodayLessonCommand.Execute(item); + if (cell.Lesson is { } viewLesson && vm.OnOpenLessonViewer is not null) + await vm.OnOpenLessonViewer(viewLesson); break; case TimetableLessonDestination.Planning: - vm.OpenGroupCommand.Execute(item.GroupId); + if (cell.GroupId != Guid.Empty) + App.Services.GetRequiredService().NavigateToGroupDetail(cell.GroupId, 6); break; case TimetableLessonDestination.SeatingPlan: - App.Services.GetRequiredService() - .NavigateToGroupDetail(item.GroupId, 2); + if (cell.GroupId != Guid.Empty) + App.Services.GetRequiredService().NavigateToGroupDetail(cell.GroupId, 2); break; } } diff --git a/TODO.md b/TODO.md index 79ac4ea..44fbf08 100644 --- a/TODO.md +++ b/TODO.md @@ -1918,18 +1918,35 @@ folgenden Punkte gehören direkt in `LehrerApp.Desktop`: den vollen Verlaufsplan-Editor öffnen müssen. Neue kleine `TeachingModeHomeworkViewModel : ObservableObject`, da `TeachingModeViewModel` selbst (wie `LessonViewerViewModel`) keine Bindable-Basisklasse hat. -- [x] **4.5.24** Dropdown in der "Heute"-Tagesliste für Unterrichtsansicht/Sitzplan/Planung/ - Planungsviewer (August 2026, Nutzer-Feedback): unklar, wie man aus dem Stundenplan zwischen - diesen vier Ansichten wechselt — bisher zwei Buttons mit teils vom Lesson-Status abhängiger - Doppelbedeutung ("Verlaufsplan ansehen"/"Zur Lerngruppe"). Ersetzt durch eine `ComboBox` pro - Zeile mit vier ausdrücklich benannten Zielen (`TodayLessonItem.DestinationOptions`, - `TimetableLessonDestination`-Enum) — Unterrichtsansicht/Planungsviewer nur wenn für den Slot - schon eine `Lesson` existiert, Sitzplan/Planung immer. Menü-Charakter: `SelectionChanged` in - `TimetableView.axaml.cs` setzt `SelectedItem` nach jeder Auswahl bewusst auf `null` zurück, - statt die Auswahl dauerhaft anzuzeigen. Drei der vier Ziele nutzen unverändert bestehende - `TimetableViewModel`-Commands (`StartTeachingModeCommand`/`OpenTodayLessonCommand`/ - `OpenGroupCommand` — letzterer navigiert bereits zum Planung-Tab, Index 6); "Sitzplan" ist - neu (Sitzpläne-Tab, Index 2 — bisher nur indirekt über den Unterrichtsmodus erreichbar). +- [x] **4.5.24** Popup-Menü im Wochenraster für Unterrichtsansicht/Sitzplan/Planung/Planungsviewer + (August 2026, Nutzer-Feedback, zweite Runde). Die erste Fassung hatte das Problem am + falschen Ort gelöst — ein Dropdown in der "Heute"-**Tagesliste** (unten angedockt), obwohl + die dortigen zwei Buttons laut Nutzer schon in Ordnung waren. Zurückgesetzt auf den + ursprünglichen Stand (zwei Buttons, `TodayLessonItem` ohne `DestinationOptions`). Das + eigentliche Problem lag im **Wochenraster darüber**: ein Klick auf eine Stunden-Kachel führt + dort in den Planungsviewer oder zur Einheitenplanung, je nachdem ob schon eine `Lesson` + existiert — von außen nicht erkennbar, welches Ziel man bekommt. + - **Popup-Menü:** kleiner "⋮"-Button pro Kachel (`Button.weekCellMenuTrigger`, oben rechts in + der Zelle überlagert) öffnet ein `MenuFlyout` mit vier ausdrücklich benannten Zielen. + Unterrichtsansicht/Planungsviewer nur wenn schon eine `Lesson` existiert, Sitzplan/Planung + immer (Planung ist ohnehin der Ort, an dem man eine Lesson für den Slot erst anlegt). + **Wichtiger XAML-Fallstrick, diesmal genauer verstanden:** ein `$parent[ItemsControl]`- + Vorfahrenpfad (wie beim normalen Zeilen-Button) funktioniert in einem `MenuFlyout` nicht, + weil dessen Popup nicht im normalen visuellen Baum hängt — **aber** eine normale + `{Binding}` ohne Vorfahrensuche funktioniert dort sehr wohl, weil Avalonia die + DataContext-**Vererbung** (kein Baum-Durchsuchen, nur der geerbte Wert) auch in Flyouts + korrekt weiterreicht. Trotzdem bewusst `MenuItem.Click` statt `Command`-Binding gewählt + (kein `WeekCellItem`-eigenes Command nötig): Handler in `TimetableView.axaml.cs` liest die + Zelle über `((MenuItem)sender).DataContext`, das Ziel über `Tag="{x:Static + vm:TimetableLessonDestination.…}"` — beides ohne jede Vorfahrensuche. + - **Einheitlicherer Direktklick:** `OpenWeekCellCommand` prüft jetzt zusätzlich, ob gerade + (heute, ±10 Minuten Toleranz) Unterrichtszeit der Stunde ist (neues + `TimetableViewModel.IsAroundTeachingTime`, `PeriodScheduleService`/"Stundenraster" als + Grundlage) — dann direkt in den Unterrichtsmodus statt in den Planungsviewer. Ohne Lesson + bleibt es beim Sprung zur Einheitenplanung. Das Popup-Menü bietet immer alle vier Ziele + explizit an, unabhängig von dieser Automatik. Nutzt bewusst `lesson.Date` statt + `WeekCellItem.Date` — letzteres ist nur bei Kopfzeilen gesetzt, nicht bei + Stunden-Kacheln (führte im ersten Testlauf zu einem Bug: die Automatik griff nie). **Wichtige Abweichung von der ursprünglichen Planung (5.2):** Vor der Umsetzung zeigte sich, dass 5.2 wie ursprünglich beschrieben eine zweite, parallele Fehlzeiten-Erfassung neben dem