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:
@@ -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;
|
||||||
|
|||||||
@@ -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.1–4.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.1–4.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.
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user