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:
@@ -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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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 <see cref="RecomputeTimeBudget"/> 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 <see cref="RecomputeTimeBudget"/> 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.
|
||||
/// </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);
|
||||
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<PhaseViewItem> 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<PhaseGroupViewItem> 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<int>? 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);
|
||||
|
||||
@@ -90,6 +90,7 @@ public partial class TimetableViewModel : ObservableObject
|
||||
public Action<Guid>? OnNavigateToGroup { get; set; }
|
||||
public Func<Task>? OnAddSubstitution { get; set; }
|
||||
public Action<SettingsTab>? OnNavigateToSettings { get; set; }
|
||||
public Func<Lesson, Task>? 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(
|
||||
|
||||
@@ -85,6 +85,13 @@
|
||||
</StackPanel>
|
||||
</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>
|
||||
</Window>
|
||||
|
||||
@@ -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<MainWindowViewModel>().NavigateToGroupDetail(vm.GroupId, tabIndex);
|
||||
};
|
||||
}
|
||||
|
||||
private void OnClose(object? s, RoutedEventArgs e) => Close();
|
||||
}
|
||||
|
||||
@@ -100,10 +100,10 @@
|
||||
Foreground="#8E6C00" FontWeight="SemiBold"
|
||||
IsVisible="{Binding HasUnhandledHomework}"/>
|
||||
</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}"
|
||||
Command="{Binding $parent[ItemsControl].((vm:TimetableViewModel)DataContext).OpenGroupCommand}"
|
||||
CommandParameter="{Binding GroupId}"/>
|
||||
Command="{Binding $parent[ItemsControl].((vm:TimetableViewModel)DataContext).OpenTodayLessonCommand}"
|
||||
CommandParameter="{Binding}"/>
|
||||
</Grid>
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
@@ -177,8 +177,8 @@
|
||||
<Button Background="{Binding ColorHex}" IsVisible="{Binding IsAssigned}"
|
||||
HorizontalAlignment="Stretch" VerticalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Stretch" CornerRadius="6" Padding="6"
|
||||
Command="{Binding $parent[ItemsControl;1].((vm:TimetableViewModel)DataContext).OpenGroupCommand}"
|
||||
CommandParameter="{Binding GroupId}">
|
||||
Command="{Binding $parent[ItemsControl;1].((vm:TimetableViewModel)DataContext).OpenWeekCellCommand}"
|
||||
CommandParameter="{Binding}">
|
||||
<StackPanel Spacing="1">
|
||||
<TextBlock FontSize="11" FontWeight="Bold" Foreground="White" TextWrapping="Wrap">
|
||||
<Run Text="{Binding SubjectLabel}"/><Run Text=" · "/><Run Text="{Binding GroupName}"/>
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
using Avalonia.Controls;
|
||||
using LehrerApp.Core.Interfaces;
|
||||
using LehrerApp.Core.Models;
|
||||
using LehrerApp.Core.Services;
|
||||
using LehrerApp.Desktop.ViewModels.Planning;
|
||||
using LehrerApp.Desktop.ViewModels.Groups;
|
||||
using LehrerApp.Desktop.Views.Groups;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace LehrerApp.Desktop.Views.Planning;
|
||||
@@ -17,9 +20,21 @@ public partial class TimetableView : UserControl
|
||||
{
|
||||
vm.OnEditSlot = ShowSlotDialog;
|
||||
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)
|
||||
{
|
||||
var owner = TopLevel.GetTopLevel(this) as Window;
|
||||
|
||||
Reference in New Issue
Block a user