Dashboard 9.1/9.2: Uhrzeit/Raum bei heutigen Stunden + Absprung zur Mitarbeit

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 <noreply@anthropic.com>
This commit is contained in:
2026-08-16 23:54:52 +02:00
co-authored by Claude Sonnet 5
parent e40585801c
commit f8dd567ef8
5 changed files with 188 additions and 17 deletions
@@ -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);
}
}
+1
View File
@@ -99,6 +99,7 @@ public class App : Application
var dash = Services.GetRequiredService<DashboardViewModel>();
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<StudentListViewModel>();
@@ -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<Guid>? OnNavigateToGroup { get; set; }
public Action<Guid>? 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<Guid>? 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; } = ""; }
@@ -32,16 +32,28 @@
<ItemsControl ItemsSource="{Binding TodaysLessons}">
<ItemsControl.ItemTemplate>
<DataTemplate DataType="vm:LessonItem">
<Grid ColumnDefinitions="4,*" Margin="0,4">
<Border Grid.Column="0" Width="4" CornerRadius="2"
Background="{DynamicResource SystemAccentColor}"
Margin="0,0,10,0"/>
<StackPanel Grid.Column="1">
<TextBlock Text="{Binding GroupName}" FontWeight="SemiBold" FontSize="13"/>
<TextBlock Text="{Binding Topic}" FontSize="12" Opacity="0.7"
TextTrimming="CharacterEllipsis"/>
</StackPanel>
</Grid>
<Button Command="{Binding $parent[ItemsControl].((vm:DashboardViewModel)DataContext).OpenLessonCommand}"
CommandParameter="{Binding}"
Background="Transparent" BorderThickness="0" Padding="0" Margin="0,4"
HorizontalAlignment="Stretch" HorizontalContentAlignment="Stretch">
<Grid ColumnDefinitions="4,*,Auto">
<Border Grid.Column="0" Width="4" CornerRadius="2"
Background="{DynamicResource SystemAccentColor}"
Margin="0,0,10,0"/>
<StackPanel Grid.Column="1">
<TextBlock Text="{Binding GroupName}" FontWeight="SemiBold" FontSize="13"/>
<TextBlock Text="{Binding Topic}" FontSize="12" Opacity="0.7"
TextTrimming="CharacterEllipsis"/>
</StackPanel>
<StackPanel Grid.Column="2" HorizontalAlignment="Right" Margin="8,0,0,0">
<TextBlock Text="{Binding TimeDisplay}" FontSize="12" Opacity="0.7"
HorizontalAlignment="Right"
IsVisible="{Binding TimeDisplay, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
<TextBlock Text="{Binding Room}" FontSize="11" Opacity="0.6"
HorizontalAlignment="Right" IsVisible="{Binding HasRoom}"/>
</StackPanel>
</Grid>
</Button>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
+15 -2
View File
@@ -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.