diff --git a/LehrerApp.Desktop.Tests/LessonDialogViewModelTests.cs b/LehrerApp.Desktop.Tests/LessonDialogViewModelTests.cs
index aacd082..ac4591a 100644
--- a/LehrerApp.Desktop.Tests/LessonDialogViewModelTests.cs
+++ b/LehrerApp.Desktop.Tests/LessonDialogViewModelTests.cs
@@ -470,6 +470,7 @@ public sealed class LessonDialogViewModelTests
// Nächster Dienstag nach dem 06.01.2099 (einem Dienstag) ist der 13.01.2099.
Assert.Equal("13.01.2099", vm.DateText);
+ Assert.Equal(3, vm.LessonNumber);
}
[Fact]
@@ -489,5 +490,42 @@ public sealed class LessonDialogViewModelTests
var suggested = DateOnly.ParseExact(vm.DateText, "dd.MM.yyyy");
Assert.True(suggested >= DateOnly.FromDateTime(DateTime.Today));
Assert.Equal(weekday, suggested.DayOfWeek);
+ Assert.Equal(3, vm.LessonNumber);
+ }
+
+ [Fact]
+ public void NeueStunde_Doppelstunde_SchlaegtDieErstePeriodeAlsStundennummerVor()
+ {
+ var unitId = Guid.NewGuid();
+ var groupId = Guid.NewGuid();
+ var futureLessonDate = new DateOnly(2099, 1, 6); // Dienstag
+ var lessons = new FakeLessons();
+ lessons.Add(new Lesson { UnitId = unitId, GroupId = groupId, Date = futureLessonDate, LessonNumber = 3 });
+ var slots = new FakeTimetableSlots();
+ slots.Add(new TimetableSlot { GroupId = groupId, Weekday = DayOfWeek.Tuesday, PeriodNumber = 4 });
+ slots.Add(new TimetableSlot { GroupId = groupId, Weekday = DayOfWeek.Tuesday, PeriodNumber = 3 });
+
+ var vm = new LessonDialogViewModel(lessons, new FakeShorthandCodes([]), new FakeAlternativeLessonPaths([]),
+ slots, NewPeriodSchedule(), unitId, groupId, "10c", "Chemie", [], [], null);
+
+ Assert.Equal(3, vm.LessonNumber); // frühere Periode, nicht 4
+ }
+
+ [Fact]
+ public void NeueStunde_MitStundenvorschlag_UebernimmtBeginnAusStundenraster()
+ {
+ var unitId = Guid.NewGuid();
+ var groupId = Guid.NewGuid();
+ var weekday = DateTime.Today.DayOfWeek;
+ var slots = new FakeTimetableSlots();
+ slots.Add(new TimetableSlot { GroupId = groupId, Weekday = weekday, PeriodNumber = 2 });
+ var periodSchedule = NewPeriodSchedule();
+ periodSchedule.SetPeriods([new PeriodTimeEntry { PeriodNumber = 2, Start = new TimeOnly(8, 50), End = new TimeOnly(9, 35) }]);
+
+ var vm = new LessonDialogViewModel(new FakeLessons(), new FakeShorthandCodes([]), new FakeAlternativeLessonPaths([]),
+ slots, periodSchedule, unitId, groupId, "10c", "Chemie", [], [], null);
+
+ Assert.Equal(2, vm.LessonNumber);
+ Assert.Equal("08:50", vm.StartTimeText);
}
}
diff --git a/LehrerApp.Desktop.Tests/LessonViewerViewModelTests.cs b/LehrerApp.Desktop.Tests/LessonViewerViewModelTests.cs
index 598b842..89dddeb 100644
--- a/LehrerApp.Desktop.Tests/LessonViewerViewModelTests.cs
+++ b/LehrerApp.Desktop.Tests/LessonViewerViewModelTests.cs
@@ -97,4 +97,34 @@ public sealed class LessonViewerViewModelTests
Assert.Equal("ab 12:00", vm.PhaseGroups[0].Phases[0].TimeDisplay);
Assert.Equal("ab 12:00", vm.PhaseGroups[1].Phases[0].TimeDisplay);
}
+
+ // ── Verzweigung aus dem Viewer (4.5.3) ────────────────────────────────────────
+
+ [Fact]
+ public void NavigateToParticipation_RuftDelegatMitTabIndexDreiAuf()
+ {
+ var groupId = Guid.NewGuid();
+ var lesson = new Lesson { GroupId = groupId, Topic = "Brechung", Date = new DateOnly(2025, 9, 1) };
+ var vm = new LessonViewerViewModel(lesson, new FakeAlternativeLessonPaths([]));
+ int? navigatedTab = null;
+ vm.OnNavigateToTab = idx => navigatedTab = idx;
+
+ vm.NavigateToParticipationCommand.Execute(null);
+
+ Assert.Equal(groupId, vm.GroupId);
+ Assert.Equal(3, navigatedTab);
+ }
+
+ [Fact]
+ public void NavigateToGrades_RuftDelegatMitTabIndexFuenfAuf()
+ {
+ var lesson = new Lesson { GroupId = Guid.NewGuid(), Topic = "Brechung", Date = new DateOnly(2025, 9, 1) };
+ var vm = new LessonViewerViewModel(lesson, new FakeAlternativeLessonPaths([]));
+ int? navigatedTab = null;
+ vm.OnNavigateToTab = idx => navigatedTab = idx;
+
+ vm.NavigateToGradesCommand.Execute(null);
+
+ Assert.Equal(5, navigatedTab);
+ }
}
diff --git a/LehrerApp.Desktop.Tests/TimetableViewModelTests.cs b/LehrerApp.Desktop.Tests/TimetableViewModelTests.cs
index ebe60a2..697355b 100644
--- a/LehrerApp.Desktop.Tests/TimetableViewModelTests.cs
+++ b/LehrerApp.Desktop.Tests/TimetableViewModelTests.cs
@@ -230,6 +230,71 @@ public sealed class TimetableViewModelTests
Assert.Equal(group.Id, navigatedTo);
}
+ // ── Direktsprung in den Verlaufsplan-Viewer (4.5.2) ──────────────────────────
+
+ [Fact]
+ public async Task OpenTodayLesson_MitVorhandenerLesson_OeffnetDenViewerStattZurGruppeZuNavigieren()
+ {
+ var today = DateOnly.FromDateTime(DateTime.Today);
+ var group = new LearningGroup { Name = "Q1 Chemie" };
+ 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 vm = BuildViewModel(slots, new FakeGroups([group]), lessons: lessons);
+ Lesson? openedLesson = null;
+ Guid? navigatedTo = null;
+ vm.OnOpenLessonViewer = l => { openedLesson = l; return Task.CompletedTask; };
+ vm.OnNavigateToGroup = id => navigatedTo = id;
+
+ await vm.OpenTodayLessonCommand.ExecuteAsync(vm.TodayItems[0]);
+
+ Assert.Equal(lesson.Id, openedLesson?.Id);
+ Assert.Null(navigatedTo);
+ Assert.Equal("Verlaufsplan ansehen", vm.TodayItems[0].OpenButtonLabel);
+ }
+
+ [Fact]
+ public async Task OpenTodayLesson_OhneVorhandeneLesson_NavigiertWieBisherZurGruppe()
+ {
+ var today = DateOnly.FromDateTime(DateTime.Today);
+ var group = new LearningGroup { Name = "Q1 Chemie" };
+ var slots = new FakeTimetableSlots();
+ slots.Add(new TimetableSlot { GroupId = group.Id, Weekday = today.DayOfWeek, PeriodNumber = 1 });
+ var vm = BuildViewModel(slots, new FakeGroups([group]));
+ Guid? navigatedTo = null;
+ var viewerOpened = false;
+ vm.OnOpenLessonViewer = _ => { viewerOpened = true; return Task.CompletedTask; };
+ vm.OnNavigateToGroup = id => navigatedTo = id;
+
+ await vm.OpenTodayLessonCommand.ExecuteAsync(vm.TodayItems[0]);
+
+ Assert.Equal(group.Id, navigatedTo);
+ Assert.False(viewerOpened);
+ Assert.Equal("Zur Lerngruppe", vm.TodayItems[0].OpenButtonLabel);
+ }
+
+ [Fact]
+ public async Task OpenWeekCell_MitVorhandenerLesson_OeffnetDenViewer()
+ {
+ var group = new LearningGroup { Name = "Q1 Chemie" };
+ var date = DateInCurrentWeek(DayOfWeek.Monday);
+ var slots = new FakeTimetableSlots();
+ slots.Add(new TimetableSlot { GroupId = group.Id, Weekday = DayOfWeek.Monday, PeriodNumber = 1 });
+ var lesson = new Lesson { GroupId = group.Id, Date = date, LessonNumber = 1, Topic = "Redox" };
+ var lessons = new FakeLessons();
+ lessons.Add(lesson);
+ var vm = BuildViewModel(slots, new FakeGroups([group]), lessons: lessons);
+ Lesson? openedLesson = null;
+ vm.OnOpenLessonViewer = l => { openedLesson = l; return Task.CompletedTask; };
+
+ var cell = vm.WeekItems.Single(c => c.IsSlotCell && c.Weekday == DayOfWeek.Monday && c.PeriodNumber == 1);
+ await vm.OpenWeekCellCommand.ExecuteAsync(cell);
+
+ Assert.Equal(lesson.Id, openedLesson?.Id);
+ }
+
[Fact]
public void OpenSettings_RuftOnNavigateToSettingsAuf()
{
diff --git a/LehrerApp.Desktop/ViewModels/Groups/PlanningViewModels.cs b/LehrerApp.Desktop/ViewModels/Groups/PlanningViewModels.cs
index ebd9652..04e0030 100644
--- a/LehrerApp.Desktop/ViewModels/Groups/PlanningViewModels.cs
+++ b/LehrerApp.Desktop/ViewModels/Groups/PlanningViewModels.cs
@@ -749,31 +749,39 @@ public partial class LessonDialogViewModel : ObservableObject
}
else
{
- DateText = SuggestNextLessonDate().ToString("dd.MM.yyyy");
+ var (date, lessonNumber) = SuggestNextLesson();
+ DateText = date.ToString("dd.MM.yyyy");
+ LessonNumber = lessonNumber;
}
RecomputeTimes();
}
///
- /// Vorbelegung für eine neue Stunde: statt des sonst über den Feld-Default eingesetzten
- /// heutigen Datums (das an einem beliebigen Wochentag steht und die Doppelstunden-Erkennung
- /// in stillschweigend auf eine Einzelperiode zurückfallen
- /// lässt, wenn der Wochentag nicht zufällig passt) der nächste laut Stundenplan (4.3) für
- /// diese Gruppe passende Wochentag — ab der letzten Stunde dieser Einheit, oder ab heute, wenn
- /// noch keine Stunde in dieser Einheit existiert oder die letzte in der Vergangenheit liegt.
- /// Ohne Stundenplan-Einträge für die Gruppe (z.B. Klassenrat) bleibt es beim heutigen Datum.
+ /// Terminvorschlag für eine neue Stunde (4.5.1): statt des sonst über den Feld-Default
+ /// eingesetzten heutigen Datums (das an einem beliebigen Wochentag steht und die
+ /// Doppelstunden-Erkennung in stillschweigend auf eine
+ /// Einzelperiode zurückfallen lässt, wenn der Wochentag nicht zufällig passt) der nächste laut
+ /// Stundenplan (4.3) für diese Gruppe passende Wochentag — ab der letzten Stunde dieser
+ /// Einheit, oder ab heute, wenn noch keine Stunde in dieser Einheit existiert oder die letzte
+ /// in der Vergangenheit liegt. Die Stundennummer wird passend dazu aus dem frühesten
+ /// `TimetableSlot` dieses Wochentags vorbelegt (bei einer Doppelstunde also die erste Periode,
+ /// dieselbe Ankerkonvention wie bei der Serienerzeugung, 4.2.5). Ohne Stundenplan-Einträge für
+ /// die Gruppe (z.B. Klassenrat) bleibt es beim heutigen Datum ohne Stundenvorschlag.
///
- private DateOnly SuggestNextLessonDate()
+ private (DateOnly Date, int? LessonNumber) SuggestNextLesson()
{
- var weekdays = _timetableSlots.GetByGroup(_groupId).Select(s => s.Weekday).ToHashSet();
+ var slots = _timetableSlots.GetByGroup(_groupId);
var today = DateOnly.FromDateTime(DateTime.Today);
- if (weekdays.Count == 0) return today;
+ if (slots.Count == 0) return (today, null);
+ var weekdays = slots.Select(s => s.Weekday).ToHashSet();
var lastLessonDate = _lessons.GetByUnit(_unitId).Select(l => l.Date)
.DefaultIfEmpty(today.AddDays(-1)).Max();
var candidate = lastLessonDate >= today ? lastLessonDate.AddDays(1) : today;
while (!weekdays.Contains(candidate.DayOfWeek)) candidate = candidate.AddDays(1);
- return candidate;
+
+ var lessonNumber = slots.Where(s => s.Weekday == candidate.DayOfWeek).Min(s => (int?)s.PeriodNumber);
+ return (candidate, lessonNumber);
}
[RelayCommand]
@@ -1432,10 +1440,11 @@ public record PhaseViewItem(string Name, int DurationMinutes, string TimeDisplay
/// von Stundenbeginn an nimmt", nicht ab einer gemeinsamen Verzweigungsstelle im Hauptweg.
public record PhaseGroupViewItem(string Label, bool IsMainPath, string? Description, List Phases);
-public class LessonViewerViewModel
+public partial class LessonViewerViewModel : ObservableObject
{
private const string MainPathLabel = "Hauptweg";
+ public Guid GroupId { get; }
public string DateDisplay { get; }
public string Topic { get; }
public string StatusLabel { get; }
@@ -1445,8 +1454,16 @@ public class LessonViewerViewModel
public List PhaseGroups { get; }
public bool HasAlternatives { get; }
+ /// Verzweigung aus dem Viewer heraus (4.5.3): Ziel-Tab-Index von GroupDetailView.axaml
+ /// (3 Mitarbeit, 5 Noten) — das Code-Behind schließt den Viewer und navigiert dorthin.
+ public Action? OnNavigateToTab { get; set; }
+
+ [RelayCommand] private void NavigateToParticipation() => OnNavigateToTab?.Invoke(3);
+ [RelayCommand] private void NavigateToGrades() => OnNavigateToTab?.Invoke(5);
+
public LessonViewerViewModel(Lesson lesson, IAlternativeLessonPathRepository alternativePaths)
{
+ GroupId = lesson.GroupId;
DateDisplay = lesson.Date.ToString("dd.MM.yyyy");
Topic = lesson.Topic;
StatusLabel = LessonStatusDisplay.ToName(lesson.Status);
diff --git a/LehrerApp.Desktop/ViewModels/Planning/TimetableViewModel.cs b/LehrerApp.Desktop/ViewModels/Planning/TimetableViewModel.cs
index 065463c..e5ab89f 100644
--- a/LehrerApp.Desktop/ViewModels/Planning/TimetableViewModel.cs
+++ b/LehrerApp.Desktop/ViewModels/Planning/TimetableViewModel.cs
@@ -90,6 +90,7 @@ public partial class TimetableViewModel : ObservableObject
public Action? OnNavigateToGroup { get; set; }
public Func? OnAddSubstitution { get; set; }
public Action? OnNavigateToSettings { get; set; }
+ public Func? OnOpenLessonViewer { get; set; }
public TimetableViewModel(ITimetableSlotRepository slots, IGroupRepository groups,
ISubjectRepository subjects, ILessonRepository lessons, IExamRepository exams,
@@ -198,7 +199,7 @@ public partial class TimetableViewModel : ObservableObject
var exam = _exams.GetByGroup(slot.GroupId).FirstOrDefault(e => e.Date == today);
items.Add(new TodayLessonItem(slot.GroupId, slot.PeriodNumber, group.Name,
slot.Room ?? "", ColorFor(group.Name), lesson?.Topic, exam?.Title,
- HasUnhandledHomework(slot.GroupId, today)));
+ HasUnhandledHomework(slot.GroupId, today), lesson));
}
foreach (var sub in substitutionsToday.Where(s => s.Kind == SubstitutionKind.Lesson &&
@@ -245,6 +246,27 @@ public partial class TimetableViewModel : ObservableObject
OnNavigateToGroup?.Invoke(groupId);
}
+ /// Springt aus dem Stundenplan direkt in den Verlaufsplan-Viewer der zugehörigen Lesson (4.5.2)
+ /// — sofern für den Slot schon eine Lesson existiert. Ohne Lesson (Slot laut Stundenplan belegt,
+ /// aber noch keine konkrete Stunde geplant) bleibt es bei der bisherigen, gröberen Navigation
+ /// zum Planung-Tab der Gruppe: eine neue Lesson direkt von hier aus anzulegen bräuchte eine
+ /// Antwort auf "welcher Unit wird sie zugeordnet", die bewusst noch offen ist (siehe TODO 4.5.2).
+ [RelayCommand]
+ private async Task OpenTodayLesson(TodayLessonItem? item)
+ {
+ if (item is null || item.GroupId == Guid.Empty) return;
+ if (item.Lesson is { } lesson && OnOpenLessonViewer is not null) await OnOpenLessonViewer(lesson);
+ else OnNavigateToGroup?.Invoke(item.GroupId);
+ }
+
+ [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);
+ else OnNavigateToGroup?.Invoke(item.GroupId);
+ }
+
[RelayCommand]
private void OpenSettings() => OnNavigateToSettings?.Invoke(SettingsTab.Holidays);
@@ -333,7 +355,7 @@ public partial class TimetableViewModel : ObservableObject
group?.Name ?? "?", slot.Room ?? "", lesson?.Topic ?? "",
colorHex, holidayBadge, hasExam, isLastBeforeExam,
MentionsExperiment(lesson), slot.GroupId, isHoliday,
- HasUnhandledHomework(slot.GroupId, date)));
+ HasUnhandledHomework(slot.GroupId, date), lesson));
}
AddWeekSupervisionRowIfAny(period, dutiesByPeriod, substitutionsThisWeek, dateByWeekday);
}
@@ -652,6 +674,10 @@ public class WeekCellItem
public bool HasSupervision => SupervisionLocation.Length > 0;
public bool HasRoom => !string.IsNullOrWhiteSpace(Room);
public bool HasTopic => !string.IsNullOrWhiteSpace(Topic);
+ /// 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 string OpenButtonLabel => Lesson is not null ? "Verlaufsplan ansehen" : "Zur Lerngruppe";
public static WeekCellItem Corner() => new() { IsHeader = true };
@@ -686,13 +712,13 @@ public class WeekCellItem
public static WeekCellItem ForSlot(DayOfWeek day, int period, bool isToday, string subjectLabel,
string groupName, string room, string topic, string colorHex, string holidayBadge,
bool hasExam, bool isLastBeforeExam, bool hasExperiment, Guid groupId, bool isHoliday,
- bool hasUnhandledHomework = false) => new()
+ bool hasUnhandledHomework = false, Lesson? lesson = null) => new()
{
Weekday = day, PeriodNumber = period, IsAssigned = true, IsToday = isToday,
SubjectLabel = subjectLabel, GroupName = groupName, Room = room, Topic = topic,
ColorHex = colorHex, HolidayBadge = holidayBadge, HasExam = hasExam,
IsLastBeforeExam = isLastBeforeExam, HasExperiment = hasExperiment, GroupId = groupId,
- IsHoliday = isHoliday, HasUnhandledHomework = hasUnhandledHomework,
+ IsHoliday = isHoliday, HasUnhandledHomework = hasUnhandledHomework, Lesson = lesson,
};
public static WeekCellItem ForSubstitutionLesson(DayOfWeek day, int period, bool isToday, SubstitutionEntry entry) => new()
@@ -757,13 +783,18 @@ public class TodayLessonItem
public bool HasExam => ExamTitle is not null;
public bool HasGroupId => GroupId != Guid.Empty;
public bool HasUnhandledHomework { get; private init; }
+ /// Nur gesetzt, wenn für diesen Slot/Tag bereits eine Lesson existiert — Grundlage für den
+ /// Direktsprung in den Verlaufsplan-Viewer (4.5.2).
+ public Lesson? Lesson { get; private init; }
+ public string OpenButtonLabel => Lesson is not null ? "Verlaufsplan ansehen" : "Zur Lerngruppe";
public TodayLessonItem(Guid groupId, int periodNumber, string groupName, string room,
- string colorHex, string? lessonTopic, string? examTitle, bool hasUnhandledHomework = false)
+ string colorHex, string? lessonTopic, string? examTitle, bool hasUnhandledHomework = false,
+ Lesson? lesson = null)
{
GroupId = groupId; PeriodNumber = periodNumber; GroupName = groupName; Room = room;
ColorHex = colorHex; LessonTopic = lessonTopic; ExamTitle = examTitle;
- HasUnhandledHomework = hasUnhandledHomework;
+ HasUnhandledHomework = hasUnhandledHomework; Lesson = lesson;
}
public static TodayLessonItem ForSubstitution(int periodNumber, SubstitutionEntry entry) => new(
diff --git a/LehrerApp.Desktop/Views/Groups/LessonViewerDialog.axaml b/LehrerApp.Desktop/Views/Groups/LessonViewerDialog.axaml
index 9d03996..8eb777f 100644
--- a/LehrerApp.Desktop/Views/Groups/LessonViewerDialog.axaml
+++ b/LehrerApp.Desktop/Views/Groups/LessonViewerDialog.axaml
@@ -85,6 +85,13 @@
-
+
+
+
+
+
+
+
diff --git a/LehrerApp.Desktop/Views/Groups/LessonViewerDialog.axaml.cs b/LehrerApp.Desktop/Views/Groups/LessonViewerDialog.axaml.cs
index 8fa09fd..0faff79 100644
--- a/LehrerApp.Desktop/Views/Groups/LessonViewerDialog.axaml.cs
+++ b/LehrerApp.Desktop/Views/Groups/LessonViewerDialog.axaml.cs
@@ -1,5 +1,8 @@
using Avalonia.Controls;
using Avalonia.Interactivity;
+using LehrerApp.Desktop.ViewModels;
+using LehrerApp.Desktop.ViewModels.Groups;
+using Microsoft.Extensions.DependencyInjection;
namespace LehrerApp.Desktop.Views.Groups;
@@ -7,5 +10,16 @@ public partial class LessonViewerDialog : Window
{
public LessonViewerDialog() => InitializeComponent();
+ protected override void OnDataContextChanged(EventArgs e)
+ {
+ base.OnDataContextChanged(e);
+ if (DataContext is LessonViewerViewModel vm)
+ vm.OnNavigateToTab = tabIndex =>
+ {
+ Close();
+ App.Services.GetRequiredService().NavigateToGroupDetail(vm.GroupId, tabIndex);
+ };
+ }
+
private void OnClose(object? s, RoutedEventArgs e) => Close();
}
diff --git a/LehrerApp.Desktop/Views/Planning/TimetableView.axaml b/LehrerApp.Desktop/Views/Planning/TimetableView.axaml
index 1f49228..ff650b0 100644
--- a/LehrerApp.Desktop/Views/Planning/TimetableView.axaml
+++ b/LehrerApp.Desktop/Views/Planning/TimetableView.axaml
@@ -100,10 +100,10 @@
Foreground="#8E6C00" FontWeight="SemiBold"
IsVisible="{Binding HasUnhandledHomework}"/>
-
+ Command="{Binding $parent[ItemsControl].((vm:TimetableViewModel)DataContext).OpenTodayLessonCommand}"
+ CommandParameter="{Binding}"/>
@@ -177,8 +177,8 @@