From f8dd567ef8be4507f39611014e562aa04f2ec4f1 Mon Sep 17 00:00:00 2001 From: Sebastian Hedtrich Date: Sun, 16 Aug 2026 23:54:52 +0200 Subject: [PATCH] Dashboard 9.1/9.2: Uhrzeit/Raum bei heutigen Stunden + Absprung zur Mitarbeit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit TodaysLessons löst je Stunde den Raum über den passenden TimetableSlot auf und die Uhrzeit aus Lesson.StartTime bzw. dem Stundenraster - gleiche Quellen wie im Verlaufsplan-Editor und Stundenplan. Bewusst nicht dupliziert: die Vertretung/Ausfall-Logik der Stundenplan-eigenen "Heute"-Ansicht bleibt dort, das Dashboard zeigt nur die einfache geplante Stunde. Klick auf eine Stunde springt in die Mitarbeitserfassung der Gruppe - bewusst anderes Ziel als der bestehende Stundenplan-Sprung (dort "Planung"), da vom Dashboard aus morgens eher die Mitarbeitserfassung naheliegt. Co-Authored-By: Claude Sonnet 5 --- .../DashboardViewModelTests.cs | 111 ++++++++++++++++++ LehrerApp.Desktop/App.axaml.cs | 1 + .../ViewModels/DashboardViewModel.cs | 44 ++++++- .../Views/Dashboard/DashboardView.axaml | 32 +++-- TODO.md | 17 ++- 5 files changed, 188 insertions(+), 17 deletions(-) create mode 100644 LehrerApp.Desktop.Tests/DashboardViewModelTests.cs diff --git a/LehrerApp.Desktop.Tests/DashboardViewModelTests.cs b/LehrerApp.Desktop.Tests/DashboardViewModelTests.cs new file mode 100644 index 0000000..5205dca --- /dev/null +++ b/LehrerApp.Desktop.Tests/DashboardViewModelTests.cs @@ -0,0 +1,111 @@ +using LehrerApp.Core.Models; +using LehrerApp.Core.Services; +using LehrerApp.Desktop.ViewModels; +using Xunit; + +namespace LehrerApp.Desktop.Tests; + +/// Tests für 9.1 (heutige Stunden mit Uhrzeit und Raum im Dashboard) und 9.2 (Absprung von einer +/// Stunde). Deckt nur die neue Zeit-/Raum-Auflösung ab, nicht die übrigen, unveränderten +/// Dashboard-Bausteine (Kalender, offene Aufgaben, Fehlzeiten-Warnungen, ...). +public sealed class DashboardViewModelTests +{ + private static PeriodScheduleService NewPeriodSchedule() + { + var tempPath = System.IO.Path.Combine( + System.IO.Path.GetTempPath(), $"lehrerapp-dashboardvm-tests-{Guid.NewGuid():N}"); + Directory.CreateDirectory(tempPath); + return new PeriodScheduleService(tempPath); + } + + private static DashboardViewModel BuildVm(LearningGroup group, Lesson lesson, + FakeTimetableSlots? slots = null, PeriodScheduleService? periodSchedule = null) + { + var lessons = new FakeLessons(); + lessons.Add(lesson); + return new DashboardViewModel( + new FakeGroups([group]), new FakeSubjects([]), lessons, + new FakeExams([]), new FakeWorkTasks(), new FakeSessions([]), new FakeEntries(), + new FakeStudents([]), new FakeDocumentation(), + slots ?? new FakeTimetableSlots(), periodSchedule ?? NewPeriodSchedule(), + new AttendanceBalanceService(), new SchoolYearService()); + } + + [Fact] + public void TodaysLessons_LoestRaumUeberPassendenStundenplanSlotAuf() + { + var group = new LearningGroup { Name = "9c" }; + var today = DateOnly.FromDateTime(DateTime.Today); + var lesson = new Lesson { GroupId = group.Id, Date = today, LessonNumber = 3, Topic = "Redox" }; + var slots = new FakeTimetableSlots(); + slots.Add(new TimetableSlot { GroupId = group.Id, Weekday = today.DayOfWeek, PeriodNumber = 3, Room = "R204" }); + + var vm = BuildVm(group, lesson, slots: slots); + + var item = Assert.Single(vm.TodaysLessons); + Assert.Equal("R204", item.Room); + Assert.True(item.HasRoom); + } + + [Fact] + public void TodaysLessons_OhnePassendenSlot_HatKeinenRaum() + { + var group = new LearningGroup { Name = "9c" }; + var today = DateOnly.FromDateTime(DateTime.Today); + var lesson = new Lesson { GroupId = group.Id, Date = today, LessonNumber = 3, Topic = "Redox" }; + + var vm = BuildVm(group, lesson); + + var item = Assert.Single(vm.TodaysLessons); + Assert.False(item.HasRoom); + Assert.Equal("", item.Room); + } + + [Fact] + public void TodaysLessons_ZeitKommtAusLessonStartTimeWennGesetzt() + { + var group = new LearningGroup { Name = "9c" }; + var today = DateOnly.FromDateTime(DateTime.Today); + var lesson = new Lesson + { + GroupId = group.Id, Date = today, LessonNumber = 3, Topic = "Redox", + StartTime = new TimeOnly(9, 15), + }; + var periodSchedule = NewPeriodSchedule(); + periodSchedule.SetPeriods([new PeriodTimeEntry { PeriodNumber = 3, Start = new TimeOnly(10, 0), End = new TimeOnly(10, 45) }]); + + var vm = BuildVm(group, lesson, periodSchedule: periodSchedule); + + Assert.Equal("09:15", Assert.Single(vm.TodaysLessons).TimeDisplay); + } + + [Fact] + public void TodaysLessons_OhneStartTime_FaelltAufStundenrasterZurueck() + { + var group = new LearningGroup { Name = "9c" }; + var today = DateOnly.FromDateTime(DateTime.Today); + var lesson = new Lesson { GroupId = group.Id, Date = today, LessonNumber = 3, Topic = "Redox" }; + var periodSchedule = NewPeriodSchedule(); + periodSchedule.SetPeriods([new PeriodTimeEntry { PeriodNumber = 3, Start = new TimeOnly(10, 0), End = new TimeOnly(10, 45) }]); + + var vm = BuildVm(group, lesson, periodSchedule: periodSchedule); + + Assert.Equal("10:00", Assert.Single(vm.TodaysLessons).TimeDisplay); + } + + [Fact] + public void OpenLesson_RuftNavigationMitGruppenIdAuf() + { + var group = new LearningGroup { Name = "9c" }; + var today = DateOnly.FromDateTime(DateTime.Today); + var lesson = new Lesson { GroupId = group.Id, Date = today, LessonNumber = 3, Topic = "Redox" }; + var vm = BuildVm(group, lesson); + + Guid? navigatedTo = null; + vm.OnNavigateToLesson = id => navigatedTo = id; + + vm.OpenLessonCommand.Execute(vm.TodaysLessons[0]); + + Assert.Equal(group.Id, navigatedTo); + } +} diff --git a/LehrerApp.Desktop/App.axaml.cs b/LehrerApp.Desktop/App.axaml.cs index 9cdafac..ae2149f 100644 --- a/LehrerApp.Desktop/App.axaml.cs +++ b/LehrerApp.Desktop/App.axaml.cs @@ -99,6 +99,7 @@ public class App : Application var dash = Services.GetRequiredService(); dash.OnNavigateToGroup = id => main.NavigateToGroupDetail(id); dash.OnNavigateToStudent = id => main.NavigateToStudent(id); + dash.OnNavigateToLesson = id => main.NavigateToGroupDetail(id, 2); // Tab "Mitarbeit" // StudentList → StudentDetail + Anlegen var sl = Services.GetRequiredService(); diff --git a/LehrerApp.Desktop/ViewModels/DashboardViewModel.cs b/LehrerApp.Desktop/ViewModels/DashboardViewModel.cs index 20a5d47..f41d980 100644 --- a/LehrerApp.Desktop/ViewModels/DashboardViewModel.cs +++ b/LehrerApp.Desktop/ViewModels/DashboardViewModel.cs @@ -21,6 +21,8 @@ public partial class DashboardViewModel : ObservableObject private readonly IParticipationRepository _participationEntries; private readonly IStudentRepository _students; private readonly IDocumentationRepository _documentation; + private readonly ITimetableSlotRepository _timetableSlots; + private readonly PeriodScheduleService _periodSchedule; private readonly AttendanceBalanceService _attendanceBalance; private readonly SchoolYearService _sy; @@ -46,15 +48,22 @@ public partial class DashboardViewModel : ObservableObject // Navigation-Callback – wird von App.axaml.cs verdrahtet public Action? OnNavigateToGroup { get; set; } public Action? OnNavigateToStudent { get; set; } + // Sprungziel eigens für eine Stunde in TodaysLessons (9.2) — bewusst getrennt von + // OnNavigateToGroup (Lerngruppen-Kacheln, Tab "Übersicht"), da der Sprung von einer konkreten + // Stunde aus sinnvollerweise direkt in die Mitarbeitserfassung führt. + public Action? OnNavigateToLesson { get; set; } public DashboardViewModel(IGroupRepository groups, ISubjectRepository subjects, ILessonRepository lessons, IExamRepository exams, IWorkTaskRepository tasks, IParticipationSessionRepository participationSessions, IParticipationRepository participationEntries, IStudentRepository students, - IDocumentationRepository documentation, AttendanceBalanceService attendanceBalance, SchoolYearService sy) + IDocumentationRepository documentation, ITimetableSlotRepository timetableSlots, + PeriodScheduleService periodSchedule, AttendanceBalanceService attendanceBalance, SchoolYearService sy) { _groups = groups; _subjects = subjects; _lessons = lessons; _exams = exams; _tasks = tasks; _participationSessions = participationSessions; _participationEntries = participationEntries; - _students = students; _documentation = documentation; _attendanceBalance = attendanceBalance; _sy = sy; + _students = students; _documentation = documentation; + _timetableSlots = timetableSlots; _periodSchedule = periodSchedule; + _attendanceBalance = attendanceBalance; _sy = sy; Load(); } @@ -73,8 +82,23 @@ public partial class DashboardViewModel : ObservableObject foreach (var l in groups.Keys.SelectMany(gid => _lessons.GetByGroupAndDate(gid, today)) .OrderBy(l => l.LessonNumber)) { - if (groups.TryGetValue(l.GroupId, out var g)) - TodaysLessons.Add(new() { GroupName = g.Name, Topic = l.Topic }); + if (!groups.TryGetValue(l.GroupId, out var g)) continue; + + // Raum kommt aus dem Stundenplan-Slot (4.3), sofern die Stunde eine Stundennummer hat + // und dafür ein Slot am heutigen Wochentag existiert — Vertretungen/Ausfälle werden + // hier bewusst NICHT berücksichtigt (das leistet bereits die "Heute"-Ansicht im + // Stundenplan selbst, eine Dopplung dieser Logik wäre hier nicht sinnvoll). + var slot = l.LessonNumber is int period + ? _timetableSlots.GetByGroup(l.GroupId).FirstOrDefault(s => s.Weekday == today.DayOfWeek && s.PeriodNumber == period) + : null; + var timeDisplay = l.StartTime is { } st ? st.ToString("HH:mm") + : l.LessonNumber is int p2 && _periodSchedule.GetTimes(p2) is { } t ? t.Start.ToString("HH:mm") : ""; + + TodaysLessons.Add(new() + { + LessonId = l.Id, GroupId = l.GroupId, GroupName = g.Name, Topic = l.Topic, + TimeDisplay = timeDisplay, Room = slot?.Room ?? "", + }); } OpenTasks.Clear(); @@ -264,6 +288,7 @@ public partial class DashboardViewModel : ObservableObject } [RelayCommand] private void OpenGroup(GroupChip? c) { if (c is not null) OnNavigateToGroup?.Invoke(c.GroupId); } + [RelayCommand] private void OpenLesson(LessonItem? l) { if (l is not null) OnNavigateToLesson?.Invoke(l.GroupId); } [RelayCommand] private void Refresh() => Load(); private class DayAgg @@ -275,7 +300,16 @@ public partial class DashboardViewModel : ObservableObject } } -public class LessonItem { public string GroupName { get; set; } = ""; public string Topic { get; set; } = ""; } +public class LessonItem +{ + public Guid LessonId { get; set; } + public Guid GroupId { get; set; } + public string GroupName { get; set; } = ""; + public string Topic { get; set; } = ""; + public string TimeDisplay { get; set; } = ""; + public string Room { get; set; } = ""; + public bool HasRoom => !string.IsNullOrWhiteSpace(Room); +} public class TaskItem { public string Title { get; set; } = ""; public string DueDate { get; set; } = ""; public bool IsOverdue { get; set; } } public class GroupChip { public Guid GroupId { get; set; } public string Name { get; set; } = ""; public string Subject { get; set; } = ""; } diff --git a/LehrerApp.Desktop/Views/Dashboard/DashboardView.axaml b/LehrerApp.Desktop/Views/Dashboard/DashboardView.axaml index 5693422..affe294 100644 --- a/LehrerApp.Desktop/Views/Dashboard/DashboardView.axaml +++ b/LehrerApp.Desktop/Views/Dashboard/DashboardView.axaml @@ -32,16 +32,28 @@ - - - - - - - + diff --git a/TODO.md b/TODO.md index 2a32fe9..4f9b4c3 100644 --- a/TODO.md +++ b/TODO.md @@ -1204,8 +1204,21 @@ Basis vorhanden in [DashboardViewModel.cs](LehrerApp.Desktop/ViewModels/Dashboar Kleiner Monatskalender ist bereits umgesetzt (Farbcodierung Unterricht/Klausur, Hervorhebung "eigene Klasse" über `LearningGroup.IsOwnClass`, feste Kartenbreite/-position). -- [ ] **9.1** Heutige Stunden mit Uhrzeit und Raum anzeigen (Abhängigkeit zu 4.3). -- [ ] **9.2** Direkter Absprung von einer Stunde in Mitarbeitserfassung bzw. Stundenplanung. +- [x] **9.1** Heutige Stunden mit Uhrzeit und Raum anzeigen (Abhängigkeit zu 4.3, jetzt erfüllt). + **Umsetzung:** `DashboardViewModel.TodaysLessons` löst je Stunde den Raum über den zur + Stundennummer und dem heutigen Wochentag passenden `TimetableSlot` auf, die Uhrzeit aus + `Lesson.StartTime` (falls manuell gesetzt) oder sonst aus dem Stundenraster + (`PeriodScheduleService.GetTimes`) — gleiche Quellen, wie sie auch im Verlaufsplan-Editor + (4.2.2) und im Stundenplan verwendet werden. **Bewusst nicht dupliziert:** die reichhaltigere + Logik der "Heute"-Ansicht im Stundenplan selbst (Vertretung/Ausfall/Sondereinsatz-bewusst, + siehe 4.3 Nachträge) — das Dashboard zeigt hier nur die einfache, tatsächlich geplante + Stunde, für den vollständigen Tagesüberblick bleibt der Stundenplan zuständig. +- [x] **9.2** Direkter Absprung von einer Stunde in Mitarbeitserfassung bzw. Stundenplanung. + **Umsetzung:** Klick auf eine Stunde in "Heute" springt in die Lerngruppe, Tab "Mitarbeit" + (`DashboardViewModel.OnNavigateToLesson`, `NavigateToGroupDetail(id, 2)`) — bewusst anderes + Sprungziel als der bereits bestehende Klick in der Stundenplan-eigenen "Heute"-Ansicht + (springt dort auf Tab "Planung", siehe 4.4.2): vom Dashboard aus ist der naheliegende nächste + Schritt morgens eher die Mitarbeitserfassung als die Planung. - [ ] **9.3** Kachel "Anstehende Termine": Klausuren, Förderplan-Überprüfungen, Abgabefristen. - [ ] **9.4** Kachel "Offene Korrekturen" mit Fortschritt (x von y Klausuren bewertet). - [ ] **9.5** Kachel "Auffälligkeiten": Fehlzeitenüberschreitungen, Notenabfall, Versetzungsgefährdung.