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:
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -99,6 +99,7 @@ public class App : Application
|
|||||||
var dash = Services.GetRequiredService<DashboardViewModel>();
|
var dash = Services.GetRequiredService<DashboardViewModel>();
|
||||||
dash.OnNavigateToGroup = id => main.NavigateToGroupDetail(id);
|
dash.OnNavigateToGroup = id => main.NavigateToGroupDetail(id);
|
||||||
dash.OnNavigateToStudent = id => main.NavigateToStudent(id);
|
dash.OnNavigateToStudent = id => main.NavigateToStudent(id);
|
||||||
|
dash.OnNavigateToLesson = id => main.NavigateToGroupDetail(id, 2); // Tab "Mitarbeit"
|
||||||
|
|
||||||
// StudentList → StudentDetail + Anlegen
|
// StudentList → StudentDetail + Anlegen
|
||||||
var sl = Services.GetRequiredService<StudentListViewModel>();
|
var sl = Services.GetRequiredService<StudentListViewModel>();
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ public partial class DashboardViewModel : ObservableObject
|
|||||||
private readonly IParticipationRepository _participationEntries;
|
private readonly IParticipationRepository _participationEntries;
|
||||||
private readonly IStudentRepository _students;
|
private readonly IStudentRepository _students;
|
||||||
private readonly IDocumentationRepository _documentation;
|
private readonly IDocumentationRepository _documentation;
|
||||||
|
private readonly ITimetableSlotRepository _timetableSlots;
|
||||||
|
private readonly PeriodScheduleService _periodSchedule;
|
||||||
private readonly AttendanceBalanceService _attendanceBalance;
|
private readonly AttendanceBalanceService _attendanceBalance;
|
||||||
private readonly SchoolYearService _sy;
|
private readonly SchoolYearService _sy;
|
||||||
|
|
||||||
@@ -46,15 +48,22 @@ public partial class DashboardViewModel : ObservableObject
|
|||||||
// Navigation-Callback – wird von App.axaml.cs verdrahtet
|
// Navigation-Callback – wird von App.axaml.cs verdrahtet
|
||||||
public Action<Guid>? OnNavigateToGroup { get; set; }
|
public Action<Guid>? OnNavigateToGroup { get; set; }
|
||||||
public Action<Guid>? OnNavigateToStudent { 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,
|
public DashboardViewModel(IGroupRepository groups, ISubjectRepository subjects, ILessonRepository lessons,
|
||||||
IExamRepository exams, IWorkTaskRepository tasks, IParticipationSessionRepository participationSessions,
|
IExamRepository exams, IWorkTaskRepository tasks, IParticipationSessionRepository participationSessions,
|
||||||
IParticipationRepository participationEntries, IStudentRepository students,
|
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;
|
_groups = groups; _subjects = subjects; _lessons = lessons; _exams = exams; _tasks = tasks;
|
||||||
_participationSessions = participationSessions; _participationEntries = participationEntries;
|
_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();
|
Load();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,8 +82,23 @@ public partial class DashboardViewModel : ObservableObject
|
|||||||
foreach (var l in groups.Keys.SelectMany(gid => _lessons.GetByGroupAndDate(gid, today))
|
foreach (var l in groups.Keys.SelectMany(gid => _lessons.GetByGroupAndDate(gid, today))
|
||||||
.OrderBy(l => l.LessonNumber))
|
.OrderBy(l => l.LessonNumber))
|
||||||
{
|
{
|
||||||
if (groups.TryGetValue(l.GroupId, out var g))
|
if (!groups.TryGetValue(l.GroupId, out var g)) continue;
|
||||||
TodaysLessons.Add(new() { GroupName = g.Name, Topic = l.Topic });
|
|
||||||
|
// 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();
|
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 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();
|
[RelayCommand] private void Refresh() => Load();
|
||||||
|
|
||||||
private class DayAgg
|
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 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; } = ""; }
|
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 ItemsSource="{Binding TodaysLessons}">
|
||||||
<ItemsControl.ItemTemplate>
|
<ItemsControl.ItemTemplate>
|
||||||
<DataTemplate DataType="vm:LessonItem">
|
<DataTemplate DataType="vm:LessonItem">
|
||||||
<Grid ColumnDefinitions="4,*" Margin="0,4">
|
<Button Command="{Binding $parent[ItemsControl].((vm:DashboardViewModel)DataContext).OpenLessonCommand}"
|
||||||
<Border Grid.Column="0" Width="4" CornerRadius="2"
|
CommandParameter="{Binding}"
|
||||||
Background="{DynamicResource SystemAccentColor}"
|
Background="Transparent" BorderThickness="0" Padding="0" Margin="0,4"
|
||||||
Margin="0,0,10,0"/>
|
HorizontalAlignment="Stretch" HorizontalContentAlignment="Stretch">
|
||||||
<StackPanel Grid.Column="1">
|
<Grid ColumnDefinitions="4,*,Auto">
|
||||||
<TextBlock Text="{Binding GroupName}" FontWeight="SemiBold" FontSize="13"/>
|
<Border Grid.Column="0" Width="4" CornerRadius="2"
|
||||||
<TextBlock Text="{Binding Topic}" FontSize="12" Opacity="0.7"
|
Background="{DynamicResource SystemAccentColor}"
|
||||||
TextTrimming="CharacterEllipsis"/>
|
Margin="0,0,10,0"/>
|
||||||
</StackPanel>
|
<StackPanel Grid.Column="1">
|
||||||
</Grid>
|
<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>
|
</DataTemplate>
|
||||||
</ItemsControl.ItemTemplate>
|
</ItemsControl.ItemTemplate>
|
||||||
</ItemsControl>
|
</ItemsControl>
|
||||||
|
|||||||
@@ -1204,8 +1204,21 @@ Basis vorhanden in [DashboardViewModel.cs](LehrerApp.Desktop/ViewModels/Dashboar
|
|||||||
Kleiner Monatskalender ist bereits umgesetzt (Farbcodierung Unterricht/Klausur,
|
Kleiner Monatskalender ist bereits umgesetzt (Farbcodierung Unterricht/Klausur,
|
||||||
Hervorhebung "eigene Klasse" über `LearningGroup.IsOwnClass`, feste Kartenbreite/-position).
|
Hervorhebung "eigene Klasse" über `LearningGroup.IsOwnClass`, feste Kartenbreite/-position).
|
||||||
|
|
||||||
- [ ] **9.1** Heutige Stunden mit Uhrzeit und Raum anzeigen (Abhängigkeit zu 4.3).
|
- [x] **9.1** Heutige Stunden mit Uhrzeit und Raum anzeigen (Abhängigkeit zu 4.3, jetzt erfüllt).
|
||||||
- [ ] **9.2** Direkter Absprung von einer Stunde in Mitarbeitserfassung bzw. Stundenplanung.
|
**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.3** Kachel "Anstehende Termine": Klausuren, Förderplan-Überprüfungen, Abgabefristen.
|
||||||
- [ ] **9.4** Kachel "Offene Korrekturen" mit Fortschritt (x von y Klausuren bewertet).
|
- [ ] **9.4** Kachel "Offene Korrekturen" mit Fortschritt (x von y Klausuren bewertet).
|
||||||
- [ ] **9.5** Kachel "Auffälligkeiten": Fehlzeitenüberschreitungen, Notenabfall, Versetzungsgefährdung.
|
- [ ] **9.5** Kachel "Auffälligkeiten": Fehlzeitenüberschreitungen, Notenabfall, Versetzungsgefährdung.
|
||||||
|
|||||||
Reference in New Issue
Block a user