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}"/>
-
-
-
-
-
-
-
-
+
+
+
+
@@ -233,36 +241,65 @@
-
+
+
+
+
+
+
+
+
+
+
+
+
+
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