Vernetzung Stundenplan ↔ Unterrichtsplanung: Terminvorschlag, Lesson-Viewer-Sprung, Verzweigung (4.5.1–4.5.3)

4.5.1: Terminvorschlag für neue Stunden schlägt jetzt auch die Stundennummer
vor (bei Doppelstunden die erste Periode), nicht nur den Wochentag —
zieht über den bestehenden LessonNumber-Changed-Hook automatisch auch den
Stundenbeginn nach.

4.5.2 (teilweise): Klick auf eine Stunde im Stundenplan ("Heute"-Liste und
read-only Wochenraster) springt bei bereits vorhandener Lesson direkt in
den Verlaufsplan-Viewer statt nur grob zum Planung-Tab. Das Anlegen einer
neuen Lesson direkt aus dem Stundenplan bleibt bewusst offen (ungeklärte
Unit-Zuordnung, siehe TODO.md).

4.5.3: Der Lesson-Viewer hat jetzt Buttons "Zur Mitarbeit"/"Zu den Noten"
zum Weiterverzweigen in die Lerngruppe.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-19 23:39:27 +02:00
co-authored by Claude Sonnet 5
parent 52a1d45539
commit 391628ae36
10 changed files with 280 additions and 31 deletions
@@ -470,6 +470,7 @@ public sealed class LessonDialogViewModelTests
// Nächster Dienstag nach dem 06.01.2099 (einem Dienstag) ist der 13.01.2099. // Nächster Dienstag nach dem 06.01.2099 (einem Dienstag) ist der 13.01.2099.
Assert.Equal("13.01.2099", vm.DateText); Assert.Equal("13.01.2099", vm.DateText);
Assert.Equal(3, vm.LessonNumber);
} }
[Fact] [Fact]
@@ -489,5 +490,42 @@ public sealed class LessonDialogViewModelTests
var suggested = DateOnly.ParseExact(vm.DateText, "dd.MM.yyyy"); var suggested = DateOnly.ParseExact(vm.DateText, "dd.MM.yyyy");
Assert.True(suggested >= DateOnly.FromDateTime(DateTime.Today)); Assert.True(suggested >= DateOnly.FromDateTime(DateTime.Today));
Assert.Equal(weekday, suggested.DayOfWeek); 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);
} }
} }
@@ -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[0].Phases[0].TimeDisplay);
Assert.Equal("ab 12:00", vm.PhaseGroups[1].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);
}
} }
@@ -230,6 +230,71 @@ public sealed class TimetableViewModelTests
Assert.Equal(group.Id, navigatedTo); 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] [Fact]
public void OpenSettings_RuftOnNavigateToSettingsAuf() public void OpenSettings_RuftOnNavigateToSettingsAuf()
{ {
@@ -749,31 +749,39 @@ public partial class LessonDialogViewModel : ObservableObject
} }
else else
{ {
DateText = SuggestNextLessonDate().ToString("dd.MM.yyyy"); var (date, lessonNumber) = SuggestNextLesson();
DateText = date.ToString("dd.MM.yyyy");
LessonNumber = lessonNumber;
} }
RecomputeTimes(); RecomputeTimes();
} }
/// <summary> /// <summary>
/// Vorbelegung für eine neue Stunde: statt des sonst über den Feld-Default eingesetzten /// Terminvorschlag für eine neue Stunde (4.5.1): statt des sonst über den Feld-Default
/// heutigen Datums (das an einem beliebigen Wochentag steht und die Doppelstunden-Erkennung /// eingesetzten heutigen Datums (das an einem beliebigen Wochentag steht und die
/// in <see cref="RecomputeTimeBudget"/> stillschweigend auf eine Einzelperiode zurückfallen /// Doppelstunden-Erkennung in <see cref="RecomputeTimeBudget"/> stillschweigend auf eine
/// lässt, wenn der Wochentag nicht zufällig passt) der nächste laut Stundenplan (4.3) für /// Einzelperiode zurückfallen lässt, wenn der Wochentag nicht zufällig passt) der nächste laut
/// diese Gruppe passende Wochentag — ab der letzten Stunde dieser Einheit, oder ab heute, wenn /// Stundenplan (4.3) für diese Gruppe passende Wochentag — ab der letzten Stunde dieser
/// noch keine Stunde in dieser Einheit existiert oder die letzte in der Vergangenheit liegt. /// Einheit, oder ab heute, wenn noch keine Stunde in dieser Einheit existiert oder die letzte
/// Ohne Stundenplan-Einträge für die Gruppe (z.B. Klassenrat) bleibt es beim heutigen Datum. /// 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.
/// </summary> /// </summary>
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); 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) var lastLessonDate = _lessons.GetByUnit(_unitId).Select(l => l.Date)
.DefaultIfEmpty(today.AddDays(-1)).Max(); .DefaultIfEmpty(today.AddDays(-1)).Max();
var candidate = lastLessonDate >= today ? lastLessonDate.AddDays(1) : today; var candidate = lastLessonDate >= today ? lastLessonDate.AddDays(1) : today;
while (!weekdays.Contains(candidate.DayOfWeek)) candidate = candidate.AddDays(1); 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] [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. /// von Stundenbeginn an nimmt", nicht ab einer gemeinsamen Verzweigungsstelle im Hauptweg.
public record PhaseGroupViewItem(string Label, bool IsMainPath, string? Description, List<PhaseViewItem> Phases); public record PhaseGroupViewItem(string Label, bool IsMainPath, string? Description, List<PhaseViewItem> Phases);
public class LessonViewerViewModel public partial class LessonViewerViewModel : ObservableObject
{ {
private const string MainPathLabel = "Hauptweg"; private const string MainPathLabel = "Hauptweg";
public Guid GroupId { get; }
public string DateDisplay { get; } public string DateDisplay { get; }
public string Topic { get; } public string Topic { get; }
public string StatusLabel { get; } public string StatusLabel { get; }
@@ -1445,8 +1454,16 @@ public class LessonViewerViewModel
public List<PhaseGroupViewItem> PhaseGroups { get; } public List<PhaseGroupViewItem> PhaseGroups { get; }
public bool HasAlternatives { 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<int>? OnNavigateToTab { get; set; }
[RelayCommand] private void NavigateToParticipation() => OnNavigateToTab?.Invoke(3);
[RelayCommand] private void NavigateToGrades() => OnNavigateToTab?.Invoke(5);
public LessonViewerViewModel(Lesson lesson, IAlternativeLessonPathRepository alternativePaths) public LessonViewerViewModel(Lesson lesson, IAlternativeLessonPathRepository alternativePaths)
{ {
GroupId = lesson.GroupId;
DateDisplay = lesson.Date.ToString("dd.MM.yyyy"); DateDisplay = lesson.Date.ToString("dd.MM.yyyy");
Topic = lesson.Topic; Topic = lesson.Topic;
StatusLabel = LessonStatusDisplay.ToName(lesson.Status); StatusLabel = LessonStatusDisplay.ToName(lesson.Status);
@@ -90,6 +90,7 @@ public partial class TimetableViewModel : ObservableObject
public Action<Guid>? OnNavigateToGroup { get; set; } public Action<Guid>? OnNavigateToGroup { get; set; }
public Func<Task>? OnAddSubstitution { get; set; } public Func<Task>? OnAddSubstitution { get; set; }
public Action<SettingsTab>? OnNavigateToSettings { get; set; } public Action<SettingsTab>? OnNavigateToSettings { get; set; }
public Func<Lesson, Task>? OnOpenLessonViewer { get; set; }
public TimetableViewModel(ITimetableSlotRepository slots, IGroupRepository groups, public TimetableViewModel(ITimetableSlotRepository slots, IGroupRepository groups,
ISubjectRepository subjects, ILessonRepository lessons, IExamRepository exams, 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); var exam = _exams.GetByGroup(slot.GroupId).FirstOrDefault(e => e.Date == today);
items.Add(new TodayLessonItem(slot.GroupId, slot.PeriodNumber, group.Name, items.Add(new TodayLessonItem(slot.GroupId, slot.PeriodNumber, group.Name,
slot.Room ?? "", ColorFor(group.Name), lesson?.Topic, exam?.Title, 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 && foreach (var sub in substitutionsToday.Where(s => s.Kind == SubstitutionKind.Lesson &&
@@ -245,6 +246,27 @@ public partial class TimetableViewModel : ObservableObject
OnNavigateToGroup?.Invoke(groupId); 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] [RelayCommand]
private void OpenSettings() => OnNavigateToSettings?.Invoke(SettingsTab.Holidays); private void OpenSettings() => OnNavigateToSettings?.Invoke(SettingsTab.Holidays);
@@ -333,7 +355,7 @@ public partial class TimetableViewModel : ObservableObject
group?.Name ?? "?", slot.Room ?? "", lesson?.Topic ?? "", group?.Name ?? "?", slot.Room ?? "", lesson?.Topic ?? "",
colorHex, holidayBadge, hasExam, isLastBeforeExam, colorHex, holidayBadge, hasExam, isLastBeforeExam,
MentionsExperiment(lesson), slot.GroupId, isHoliday, MentionsExperiment(lesson), slot.GroupId, isHoliday,
HasUnhandledHomework(slot.GroupId, date))); HasUnhandledHomework(slot.GroupId, date), lesson));
} }
AddWeekSupervisionRowIfAny(period, dutiesByPeriod, substitutionsThisWeek, dateByWeekday); AddWeekSupervisionRowIfAny(period, dutiesByPeriod, substitutionsThisWeek, dateByWeekday);
} }
@@ -652,6 +674,10 @@ public class WeekCellItem
public bool HasSupervision => SupervisionLocation.Length > 0; public bool HasSupervision => SupervisionLocation.Length > 0;
public bool HasRoom => !string.IsNullOrWhiteSpace(Room); public bool HasRoom => !string.IsNullOrWhiteSpace(Room);
public bool HasTopic => !string.IsNullOrWhiteSpace(Topic); 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 }; 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, public static WeekCellItem ForSlot(DayOfWeek day, int period, bool isToday, string subjectLabel,
string groupName, string room, string topic, string colorHex, string holidayBadge, string groupName, string room, string topic, string colorHex, string holidayBadge,
bool hasExam, bool isLastBeforeExam, bool hasExperiment, Guid groupId, bool isHoliday, 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, Weekday = day, PeriodNumber = period, IsAssigned = true, IsToday = isToday,
SubjectLabel = subjectLabel, GroupName = groupName, Room = room, Topic = topic, SubjectLabel = subjectLabel, GroupName = groupName, Room = room, Topic = topic,
ColorHex = colorHex, HolidayBadge = holidayBadge, HasExam = hasExam, ColorHex = colorHex, HolidayBadge = holidayBadge, HasExam = hasExam,
IsLastBeforeExam = isLastBeforeExam, HasExperiment = hasExperiment, GroupId = groupId, 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() 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 HasExam => ExamTitle is not null;
public bool HasGroupId => GroupId != Guid.Empty; public bool HasGroupId => GroupId != Guid.Empty;
public bool HasUnhandledHomework { get; private init; } 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, 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; GroupId = groupId; PeriodNumber = periodNumber; GroupName = groupName; Room = room;
ColorHex = colorHex; LessonTopic = lessonTopic; ExamTitle = examTitle; ColorHex = colorHex; LessonTopic = lessonTopic; ExamTitle = examTitle;
HasUnhandledHomework = hasUnhandledHomework; HasUnhandledHomework = hasUnhandledHomework; Lesson = lesson;
} }
public static TodayLessonItem ForSubstitution(int periodNumber, SubstitutionEntry entry) => new( public static TodayLessonItem ForSubstitution(int periodNumber, SubstitutionEntry entry) => new(
@@ -85,6 +85,13 @@
</StackPanel> </StackPanel>
</ScrollViewer> </ScrollViewer>
<Button Grid.Row="2" Content="Schließen" HorizontalAlignment="Right" Margin="0,16,0,0" Click="OnClose"/> <Grid Grid.Row="2" ColumnDefinitions="*,Auto" Margin="0,16,0,0">
<StackPanel Grid.Column="0" Orientation="Horizontal" Spacing="8">
<Button Content="Zur Mitarbeit" Command="{Binding NavigateToParticipationCommand}"
ToolTip.Tip="Springt zum Tab 'Mitarbeit' der Lerngruppe — dort auch Anwesenheit/Hausaufgaben."/>
<Button Content="Zu den Noten" Command="{Binding NavigateToGradesCommand}"/>
</StackPanel>
<Button Grid.Column="1" Content="Schließen" Click="OnClose"/>
</Grid>
</Grid> </Grid>
</Window> </Window>
@@ -1,5 +1,8 @@
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Interactivity; using Avalonia.Interactivity;
using LehrerApp.Desktop.ViewModels;
using LehrerApp.Desktop.ViewModels.Groups;
using Microsoft.Extensions.DependencyInjection;
namespace LehrerApp.Desktop.Views.Groups; namespace LehrerApp.Desktop.Views.Groups;
@@ -7,5 +10,16 @@ public partial class LessonViewerDialog : Window
{ {
public LessonViewerDialog() => InitializeComponent(); public LessonViewerDialog() => InitializeComponent();
protected override void OnDataContextChanged(EventArgs e)
{
base.OnDataContextChanged(e);
if (DataContext is LessonViewerViewModel vm)
vm.OnNavigateToTab = tabIndex =>
{
Close();
App.Services.GetRequiredService<MainWindowViewModel>().NavigateToGroupDetail(vm.GroupId, tabIndex);
};
}
private void OnClose(object? s, RoutedEventArgs e) => Close(); private void OnClose(object? s, RoutedEventArgs e) => Close();
} }
@@ -100,10 +100,10 @@
Foreground="#8E6C00" FontWeight="SemiBold" Foreground="#8E6C00" FontWeight="SemiBold"
IsVisible="{Binding HasUnhandledHomework}"/> IsVisible="{Binding HasUnhandledHomework}"/>
</StackPanel> </StackPanel>
<Button Grid.Column="3" Content="Zur Lerngruppe" FontSize="11" Padding="9,4" <Button Grid.Column="3" Content="{Binding OpenButtonLabel}" FontSize="11" Padding="9,4"
VerticalAlignment="Center" IsVisible="{Binding HasGroupId}" VerticalAlignment="Center" IsVisible="{Binding HasGroupId}"
Command="{Binding $parent[ItemsControl].((vm:TimetableViewModel)DataContext).OpenGroupCommand}" Command="{Binding $parent[ItemsControl].((vm:TimetableViewModel)DataContext).OpenTodayLessonCommand}"
CommandParameter="{Binding GroupId}"/> CommandParameter="{Binding}"/>
</Grid> </Grid>
</Border> </Border>
</DataTemplate> </DataTemplate>
@@ -177,8 +177,8 @@
<Button Background="{Binding ColorHex}" IsVisible="{Binding IsAssigned}" <Button Background="{Binding ColorHex}" IsVisible="{Binding IsAssigned}"
HorizontalAlignment="Stretch" VerticalAlignment="Stretch" HorizontalAlignment="Stretch" VerticalAlignment="Stretch"
HorizontalContentAlignment="Stretch" CornerRadius="6" Padding="6" HorizontalContentAlignment="Stretch" CornerRadius="6" Padding="6"
Command="{Binding $parent[ItemsControl;1].((vm:TimetableViewModel)DataContext).OpenGroupCommand}" Command="{Binding $parent[ItemsControl;1].((vm:TimetableViewModel)DataContext).OpenWeekCellCommand}"
CommandParameter="{Binding GroupId}"> CommandParameter="{Binding}">
<StackPanel Spacing="1"> <StackPanel Spacing="1">
<TextBlock FontSize="11" FontWeight="Bold" Foreground="White" TextWrapping="Wrap"> <TextBlock FontSize="11" FontWeight="Bold" Foreground="White" TextWrapping="Wrap">
<Run Text="{Binding SubjectLabel}"/><Run Text=" · "/><Run Text="{Binding GroupName}"/> <Run Text="{Binding SubjectLabel}"/><Run Text=" · "/><Run Text="{Binding GroupName}"/>
@@ -1,7 +1,10 @@
using Avalonia.Controls; using Avalonia.Controls;
using LehrerApp.Core.Interfaces; using LehrerApp.Core.Interfaces;
using LehrerApp.Core.Models;
using LehrerApp.Core.Services; using LehrerApp.Core.Services;
using LehrerApp.Desktop.ViewModels.Planning; using LehrerApp.Desktop.ViewModels.Planning;
using LehrerApp.Desktop.ViewModels.Groups;
using LehrerApp.Desktop.Views.Groups;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
namespace LehrerApp.Desktop.Views.Planning; namespace LehrerApp.Desktop.Views.Planning;
@@ -17,9 +20,21 @@ public partial class TimetableView : UserControl
{ {
vm.OnEditSlot = ShowSlotDialog; vm.OnEditSlot = ShowSlotDialog;
vm.OnAddSubstitution = ShowSubstitutionDialog; vm.OnAddSubstitution = ShowSubstitutionDialog;
vm.OnOpenLessonViewer = ShowLessonViewerDialog;
} }
} }
private async Task ShowLessonViewerDialog(Lesson lesson)
{
var owner = TopLevel.GetTopLevel(this) as Window;
if (owner is null) return;
var viewerVm = new LessonViewerViewModel(lesson,
App.Services.GetRequiredService<IAlternativeLessonPathRepository>());
var dialog = new LessonViewerDialog { DataContext = viewerVm };
await dialog.ShowDialog(owner);
}
private async Task ShowSlotDialog(TimetableCellItem cell) private async Task ShowSlotDialog(TimetableCellItem cell)
{ {
var owner = TopLevel.GetTopLevel(this) as Window; var owner = TopLevel.GetTopLevel(this) as Window;
+38 -6
View File
@@ -733,23 +733,55 @@ war zudem redundant (das Bearbeiten-Tab ist direkt anklickbar) und fühlte sich
("ggf. einem neuen Termine-Modell"). Kein Aufschub aus Unklarheit, sondern weil das über den ("ggf. einem neuen Termine-Modell"). Kein Aufschub aus Unklarheit, sondern weil das über den
angefragten Umfang hinausginge. angefragten Umfang hinausginge.
### 4.5 Vernetzung Stundenplan ↔ Unterrichtsplanung (zurückgestellt) ### 4.5 Vernetzung Stundenplan ↔ Unterrichtsplanung (zurückgestellt, 4.5.14.5.3 auf Nutzerwunsch vorgezogen)
Nutzer-Feedback nach der 4.3-Iterationen: Stundenplan (4.3, wiederkehrendes Muster) und Nutzer-Feedback nach der 4.3-Iterationen: Stundenplan (4.3, wiederkehrendes Muster) und
Unterrichtsplanung (4.1/4.2, `Unit`/`Lesson` je Gruppe) laufen bisher zu getrennt nebeneinander. Unterrichtsplanung (4.1/4.2, `Unit`/`Lesson` je Gruppe) laufen bisher zu getrennt nebeneinander.
Ausdrücklich als Idee für später festgehalten, **nicht** jetzt umsetzen: Ausdrücklich als Idee für später festgehalten, **nicht** jetzt umsetzen — mit Ausnahme von
4.5.14.5.3, die auf ausdrücklichen Nutzerwunsch vorgezogen wurden (Rest von 4.5 bleibt
zurückgestellt):
- [ ] **4.5.1** Terminvorschlag beim Anlegen einer `Lesson`: Tag/Stunde anhand des Stundenplans - [x] **4.5.1** Terminvorschlag beim Anlegen einer `Lesson`: Tag/Stunde anhand des Stundenplans
vorschlagen, an dem die betroffene Gruppe laut `TimetableSlot` tatsächlich Unterricht hat, vorschlagen, an dem die betroffene Gruppe laut `TimetableSlot` tatsächlich Unterricht hat,
statt Datum/Stunde komplett frei einzutragen. statt Datum/Stunde komplett frei einzutragen. **Vorgefunden (Nebeneffekt der
Doppelstunden-Erkennung, 4.2.2-Nachtrag):** `LessonDialogViewModel` schlug für eine neue
Stunde bereits den nächsten passenden Wochentag vor (`SuggestNextLessonDate`) — nötig, damit
die Zeitbudget-Doppelstunden-Erkennung nicht auf "heute zufällig kein Unterrichtstag" ins
Leere läuft. Die Stundennummer selbst blieb dabei aber leer. **Ergänzt:** `SuggestNextLesson`
schlägt jetzt zusätzlich die früheste `TimetableSlot`-Periode dieses Wochentags vor (bei
einer Doppelstunde also die erste Periode, dieselbe Ankerkonvention wie bei 4.2.5) — über
den bereits bestehenden `OnLessonNumberChanged`-Hook wird dadurch automatisch auch der
Stundenbeginn aus dem Stundenraster übernommen, ganz ohne zusätzlichen Code.
- [ ] **4.5.2** Vom Stundenplan (Wochenraster oder Bearbeiten-Raster) direkt in einen - [ ] **4.5.2** Vom Stundenplan (Wochenraster oder Bearbeiten-Raster) direkt in einen
"Viewer" der zugehörigen `Lesson` springen können — und von dort auch eine neue `Lesson` "Viewer" der zugehörigen `Lesson` springen können — und von dort auch eine neue `Lesson`
anlegen können. **Offene Frage, vor Umsetzung zu klären:** `Lesson` hängt an einer anlegen können. **Offene Frage, vor Umsetzung zu klären:** `Lesson` hängt an einer
übergeordneten `Unit` (4.1) — beim Anlegen direkt aus dem Stundenplan heraus ist unklar, übergeordneten `Unit` (4.1) — beim Anlegen direkt aus dem Stundenplan heraus ist unklar,
welcher `Unit` die neue Lesson zugeordnet werden soll (aktuellste offene Einheit der Gruppe? welcher `Unit` die neue Lesson zugeordnet werden soll (aktuellste offene Einheit der Gruppe?
Rückfrage an die Lehrkraft?). Muss überdacht werden, bevor das gebaut wird. Rückfrage an die Lehrkraft?). Muss überdacht werden, bevor das gebaut wird — deshalb weiterhin
- [ ] **4.5.3** Aus diesem Lesson-Viewer heraus weiter verzweigen können: in die Zeugnisnote/ unimplementiert, auch nach diesem Durchgang.
**Teilweise umgesetzt:** Der erste Teil ("direkt in den Viewer springen") ist jetzt fertig,
sofern für den angeklickten Slot schon eine `Lesson` existiert — genau der unstrittige Teil
ohne die offene Unit-Frage. Sowohl die "Heute"-Tagesliste als auch das schreibgeschützte
Wochenraster im Stundenplan-Tab "Heute" (bewusst NICHT das Bearbeiten-Raster aus 4.3.2/4.3.3
— dessen Klick öffnet weiterhin `TimetableSlotDialog` zur Slot-Zuweisung, das ist ein anderer,
etablierter Zweck) öffnen bei vorhandener Lesson jetzt direkt `LessonViewerDialog` statt nur
grob zum Planung-Tab der Gruppe zu springen (`TimetableViewModel.OnOpenLessonViewer`,
`TodayLessonItem`/`WeekCellItem.Lesson`). Ohne vorhandene Lesson bleibt es unverändert bei der
Navigation zum Planung-Tab — dort lässt sich über " Stunde" weiterhin manuell mit expliziter
Unit-Auswahl eine neue Lesson anlegen.
- [x] **4.5.3** Aus diesem Lesson-Viewer heraus weiter verzweigen können: in die Zeugnisnote/
Bewertung der Gruppe, und in die Schnelldialoge für Mitarbeit sowie Anwesenheit/Hausaufgaben. Bewertung der Gruppe, und in die Schnelldialoge für Mitarbeit sowie Anwesenheit/Hausaufgaben.
**Umsetzung:** Zwei neue Buttons im Footer von `LessonViewerDialog` ("Zur Mitarbeit"/
"Zu den Noten", `LessonViewerViewModel.NavigateToParticipationCommand`/`NavigateToGradesCommand`)
schließen den Viewer und springen in den jeweiligen Tab der Gruppe (3 Mitarbeit, 5 Noten) —
dieselbe `MainWindowViewModel`-Singleton-Navigation wie überall sonst im Code-Behind. Bewusst
**kein** automatisches Öffnen der Schnelldialoge ("Schnell Mitarbeit"/"Anw.-HA") selbst: die
brauchen eine ausgewählte Mitarbeitssitzung, die für das Datum dieser Lesson noch gar nicht
existieren muss — automatisch eine anzulegen hätte dieselbe Geistereintrag-Falle wie der
kürzlich behobene Sitzplan-Bug (14.12/7.1.5-Nachtrag) reproduziert. Im Mitarbeit-Tab
angekommen ist "Sitzung erzeugen" (3.3.1, aus der Planung) bzw. der bestehende
" Sitzung"-Button ohnehin nur einen Klick entfernt.
- [x] **4.5.4** Badges am Stundenplan, die anzeigen, ob eine Hausaufgabe aus der letzten Stunde - [x] **4.5.4** Badges am Stundenplan, die anzeigen, ob eine Hausaufgabe aus der letzten Stunde
noch nicht kontrolliert ist. noch nicht kontrolliert ist.