Klausurtermine im Kalender (4.4.3) + Stundenplan: Ausnahme eintragen, Zahnrad-Settings-Link

- 4.4.3: Box "ANSTEHENDE KLAUSUREN" im Heute-Tab, gruppenübergreifend, nächste 21 Tage.
  IExamRepository um GetAll() erweitert.
- Nutzer-Feedback: "Vertretung eintragen" deckt auch Ausfall/Sondereinsatz ab, daher
  umbenannt in "Ausnahme eintragen". "Stundenplan bearbeiten" (redundant zum Bearbeiten-Tab)
  ersetzt durch Zahnrad-Button, der direkt in die Einstellungen springt.
This commit is contained in:
2026-08-15 22:24:04 +02:00
parent d6dee72acc
commit 0800832ae5
13 changed files with 189 additions and 21 deletions
@@ -29,6 +29,7 @@ public interface IGroupMembershipRepository
public interface IExamRepository
{
Exam? GetById(Guid id);
List<Exam> GetAll();
List<Exam> GetByGroup(Guid groupId);
void Save(Exam exam);
void Delete(Guid id);
+14
View File
@@ -224,6 +224,20 @@ public sealed class RepositoryTests
Assert.Equal("Früh", result[1].Title);
}
[Fact]
public void ExamRepository_GetAll_UeberAlleGruppenHinwegSortiertAufsteigendNachDatum()
{
using var db = NewInMemoryContext();
var repo = new ExamRepository(db);
repo.Save(new Exam { GroupId = Guid.NewGuid(), Title = "Spät", Date = new DateOnly(2025, 11, 1) });
repo.Save(new Exam { GroupId = Guid.NewGuid(), Title = "Früh", Date = new DateOnly(2025, 9, 1) });
var result = repo.GetAll();
Assert.Equal("Früh", result[0].Title);
Assert.Equal("Spät", result[1].Title);
}
// ── SubjectRepository ─────────────────────────────────────────────────────
[Fact]
@@ -132,6 +132,7 @@ public class GroupMembershipRepository(LiteDbContext db) : IGroupMembershipRepos
public class ExamRepository(LiteDbContext db) : IExamRepository
{
public Exam? GetById(Guid id) => db.Exams.FindById(id);
public List<Exam> GetAll() => db.Exams.FindAll().OrderBy(e => e.Date).ToList();
public List<Exam> GetByGroup(Guid groupId) =>
db.Exams.Find(e => e.GroupId == groupId).OrderByDescending(e => e.Date).ToList();
public void Save(Exam e) { e.UpdatedAt = DateTime.UtcNow; db.Exams.Upsert(e); }
+1
View File
@@ -76,6 +76,7 @@ public class FakeGrades : IGradeRepository
public class FakeExams(List<Exam> all) : IExamRepository
{
public Exam? GetById(Guid id) => all.FirstOrDefault(e => e.Id == id);
public List<Exam> GetAll() => all;
public List<Exam> GetByGroup(Guid groupId) => all.Where(e => e.GroupId == groupId).ToList();
public void Save(Exam exam) { }
public void Delete(Guid id) { }
@@ -148,13 +148,15 @@ public sealed class TimetableViewModelTests
}
[Fact]
public void ShowEditor_SchaltetAufBearbeiten_Tab()
public void OpenSettings_RuftOnNavigateToSettingsAuf()
{
var vm = BuildViewModel(new FakeTimetableSlots(), new FakeGroups([]));
var called = false;
vm.OnNavigateToSettings = () => called = true;
vm.ShowEditorCommand.Execute(null);
vm.OpenSettingsCommand.Execute(null);
Assert.Equal(1, vm.ActiveTabIndex);
Assert.True(called);
}
// ── "Heute"-Ansicht: Wochenraster (Nutzer-Feedback, zweite Iteration) ────────────────────
@@ -742,4 +744,68 @@ public sealed class TimetableViewModelTests
var item = Assert.Single(vm.TodayItems);
Assert.False(item.HasLessonTopic);
}
// ── Anstehende Klausurtermine (4.4.3) ────────────────────────────────────
[Fact]
public void Load_ZeigtKlausurInnerhalbDesVorausblicks()
{
var today = DateOnly.FromDateTime(DateTime.Today);
var group = new LearningGroup { Name = "Q1 Chemie" };
var exams = new FakeExams([new Exam { GroupId = group.Id, Date = today.AddDays(10), Title = "Klausur Nr. 2" }]);
var vm = BuildViewModel(new FakeTimetableSlots(), new FakeGroups([group]), exams: exams);
var item = Assert.Single(vm.UpcomingExams);
Assert.Equal("Q1 Chemie", item.GroupName);
Assert.Equal("Klausur Nr. 2", item.Title);
Assert.Equal(today.AddDays(10), item.Date);
}
[Fact]
public void Load_KlausurAusserhalbDesVorausblicks_WirdNichtGezeigt()
{
var today = DateOnly.FromDateTime(DateTime.Today);
var group = new LearningGroup { Name = "Q1 Chemie" };
var exams = new FakeExams([new Exam { GroupId = group.Id, Date = today.AddDays(30), Title = "Klausur Nr. 3" }]);
var vm = BuildViewModel(new FakeTimetableSlots(), new FakeGroups([group]), exams: exams);
Assert.Empty(vm.UpcomingExams);
}
[Fact]
public void Load_VergangeneKlausur_WirdNichtGezeigt()
{
var today = DateOnly.FromDateTime(DateTime.Today);
var group = new LearningGroup { Name = "Q1 Chemie" };
var exams = new FakeExams([new Exam { GroupId = group.Id, Date = today.AddDays(-1), Title = "Alte Klausur" }]);
var vm = BuildViewModel(new FakeTimetableSlots(), new FakeGroups([group]), exams: exams);
Assert.Empty(vm.UpcomingExams);
}
[Fact]
public void Load_MehrereKlausuren_SindNachDatumSortiert()
{
var today = DateOnly.FromDateTime(DateTime.Today);
var group = new LearningGroup { Name = "Q1 Chemie" };
var exams = new FakeExams(
[
new Exam { GroupId = group.Id, Date = today.AddDays(15), Title = "Später" },
new Exam { GroupId = group.Id, Date = today.AddDays(3), Title = "Früher" },
]);
var vm = BuildViewModel(new FakeTimetableSlots(), new FakeGroups([group]), exams: exams);
Assert.Equal(["Früher", "Später"], vm.UpcomingExams.Select(e => e.Title));
}
[Fact]
public void Load_HeutigeKlausur_IstImVorausblickEnthalten()
{
var today = DateOnly.FromDateTime(DateTime.Today);
var group = new LearningGroup { Name = "Q1 Chemie" };
var exams = new FakeExams([new Exam { GroupId = group.Id, Date = today, Title = "Heute" }]);
var vm = BuildViewModel(new FakeTimetableSlots(), new FakeGroups([group]), exams: exams);
Assert.Single(vm.UpcomingExams);
}
}
+2 -1
View File
@@ -105,8 +105,9 @@ public class App : Application
sl.OnNavigateToDetail = id => main.NavigateToStudent(id);
sl.OnAddStudent = () => ShowAddStudentDialog();
// Stundenplan "Heute" → GroupDetail (Tab "Planung")
// Stundenplan "Heute" → GroupDetail (Tab "Planung") / Einstellungen (Zahnrad, Tab "Ferien & Feiertage")
var timetable = Services.GetRequiredService<TimetableViewModel>();
timetable.OnNavigateToSettings = () => main.NavigateToSettings(7);
timetable.OnNavigateToGroup = id => main.NavigateToGroupDetail(id, 5);
}
@@ -97,6 +97,14 @@ public partial class MainWindowViewModel : ObservableObject
vm.LoadGroup(groupId); // dann Daten laden
}
public void NavigateToSettings(int initialTab = 0)
{
ActiveNavItem = NavItem.Settings;
var vm = _services.GetRequiredService<SettingsViewModel>();
vm.ActiveTabIndex = initialTab;
CurrentPage = vm;
}
public void NavigateToStudent(Guid studentId)
{
ActiveNavItem = NavItem.Students;
@@ -67,6 +67,7 @@ public partial class TimetableViewModel : ObservableObject
public ObservableCollection<TodayLessonItem> TodayItems { get; } = [];
public ObservableCollection<TodaySupervisionItem> TodaySupervisions { get; } = [];
public ObservableCollection<TodaySpecialAssignmentItem> TodaySpecialAssignments { get; } = [];
public ObservableCollection<UpcomingExamItem> UpcomingExams { get; } = [];
[ObservableProperty] private int _activeTabIndex;
[ObservableProperty] private string _todayLabel = "";
@@ -77,6 +78,7 @@ public partial class TimetableViewModel : ObservableObject
public Func<TimetableCellItem, Task>? OnEditSlot { get; set; }
public Action<Guid>? OnNavigateToGroup { get; set; }
public Func<Task>? OnAddSubstitution { get; set; }
public Action? OnNavigateToSettings { get; set; }
public TimetableViewModel(ITimetableSlotRepository slots, IGroupRepository groups,
ISubjectRepository subjects, ILessonRepository lessons, IExamRepository exams,
@@ -127,6 +129,31 @@ public partial class TimetableViewModel : ObservableObject
BuildWeekOverview(today, publicHolidayDates, duties);
BuildToday(today);
BuildHoursWarnings();
BuildUpcomingExams(today);
}
// ── Anstehende Klausurtermine (4.4.3) ────────────────────────────────────
/// <summary>
/// Mehrtägiger Vorausblick über alle Gruppen hinweg (nicht nur "heute", wie die Tagesliste,
/// und unabhängig von <see cref="WeekOffset"/>, anders als das Wochenraster) — Abgabefristen
/// sind bewusst nicht enthalten, dafür gibt es noch kein eigenes Datenmodell (siehe TODO.md).
/// </summary>
private const int UpcomingExamsHorizonDays = 21;
private void BuildUpcomingExams(DateOnly today)
{
UpcomingExams.Clear();
var groupNames = _groups.GetAll(includeInactive: true).ToDictionary(g => g.Id, g => g.Name);
var horizon = today.AddDays(UpcomingExamsHorizonDays);
var upcoming = _exams.GetAll()
.Where(e => e.Date >= today && e.Date <= horizon)
.OrderBy(e => e.Date)
.Take(10);
foreach (var exam in upcoming)
UpcomingExams.Add(new UpcomingExamItem(exam.Date, groupNames.GetValueOrDefault(exam.GroupId, "?"), exam.Title));
}
// ── "Heute": Tagesliste ──────────────────────────────────────────────────
@@ -207,7 +234,7 @@ public partial class TimetableViewModel : ObservableObject
}
[RelayCommand]
private void ShowEditor() => ActiveTabIndex = 1;
private void OpenSettings() => OnNavigateToSettings?.Invoke();
// ── "Heute": Wochenraster (Nutzer-Feedback) — wie das Bearbeiten-Raster, aber nur Anzeige ──
@@ -649,6 +676,14 @@ public class HoursWarningItem(string groupName, int assigned, int expected)
public string Text { get; } = $"{groupName}: {assigned} von {expected} Wochenstunden eingetragen";
}
public class UpcomingExamItem(DateOnly date, string groupName, string title)
{
public DateOnly Date { get; } = date;
public string DateDisplay { get; } = date.ToString("dd.MM.yyyy");
public string GroupName { get; } = groupName;
public string Title { get; } = title;
}
public class TodayLessonItem
{
public Guid GroupId { get; private init; }
@@ -30,6 +30,8 @@ public partial class SettingsViewModel : ObservableObject
private readonly IStudentRepository _students;
private readonly IShorthandCodeRepository _shorthandCodes;
[ObservableProperty] private int _activeTabIndex;
// ── Fächer ────────────────────────────────────────────────────────────────
[ObservableProperty] private string _newName = "";
@@ -4,14 +4,14 @@
xmlns:models="clr-namespace:LehrerApp.Core.Models;assembly=LehrerApp.Core"
x:Class="LehrerApp.Desktop.Views.Planning.SubstitutionEntryDialog"
x:DataType="vm:SubstitutionEntryDialogViewModel"
Title="Vertretung eintragen"
Title="Ausnahme eintragen"
Width="440" Height="560" MinWidth="400" MinHeight="480"
CanResize="True" WindowStartupLocation="CenterOwner">
<Grid RowDefinitions="*,Auto" Margin="24">
<ScrollViewer Grid.Row="0">
<StackPanel Spacing="14" Margin="0,0,12,0">
<TextBlock Text="Vertretung eintragen" Classes="dialogtitle"/>
<TextBlock Text="Ausnahme eintragen" Classes="dialogtitle"/>
<TextBlock FontSize="12" Opacity="0.6" TextWrapping="Wrap"
Text="Für einmalige Ausnahmen — Vertretungsaufsicht, Vertretungsstunde, Sondereinsatz (Ausflug, Berufsmesse, ...) oder schlichter Ausfall an einem konkreten Tag. Wiederkehrende Aufsicht wird in den Einstellungen gepflegt."/>
@@ -127,9 +127,10 @@
</TextBlock>
<Button Grid.Column="3" Content="Diese Woche" Margin="0,0,8,0"
Command="{Binding CurrentWeekCommand}" IsVisible="{Binding !IsCurrentWeek}"/>
<Button Grid.Column="4" Content="Vertretung eintragen" Margin="0,0,8,0"
<Button Grid.Column="4" Content="Ausnahme eintragen" Margin="0,0,8,0"
Command="{Binding AddSubstitutionCommand}"/>
<Button Grid.Column="5" Content="Stundenplan bearbeiten" Command="{Binding ShowEditorCommand}"/>
<Button Grid.Column="5" Content="⚙️" Command="{Binding OpenSettingsCommand}"
ToolTip.Tip="Einstellungen (Ferien, Aufsichten, Stundenraster)"/>
</Grid>
<ItemsControl ItemsSource="{Binding WeekItems}">
@@ -208,6 +209,25 @@
</StackPanel>
</Border>
<Border Background="{DynamicResource SystemControlBackgroundAltHighBrush}"
CornerRadius="8" Padding="16" IsVisible="{Binding UpcomingExams.Count}">
<StackPanel Spacing="8">
<TextBlock Text="ANSTEHENDE KLAUSUREN" FontSize="11" FontWeight="Bold" Opacity="0.5"/>
<ItemsControl ItemsSource="{Binding UpcomingExams}">
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="vm:UpcomingExamItem">
<Grid ColumnDefinitions="90,*" Margin="0,3">
<TextBlock Grid.Column="0" Text="{Binding DateDisplay}" FontSize="12" Opacity="0.6"/>
<TextBlock Grid.Column="1" FontSize="12">
<Run Text="{Binding GroupName}"/><Run Text=" — "/><Run Text="{Binding Title}"/>
</TextBlock>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</Border>
</StackPanel>
</ScrollViewer>
</DockPanel>
@@ -9,7 +9,7 @@
<shared:PageHeader Grid.Row="0" Title="Einstellungen" Margin="32,28,32,0"/>
<TabbedPage Grid.Row="1" TabPlacement="Top">
<TabbedPage Grid.Row="1" TabPlacement="Top" SelectedIndex="{Binding ActiveTabIndex}">
<!-- Tab: Fächer -->
<ContentPage Header="Fächer">
+30 -11
View File
@@ -572,6 +572,20 @@ NAT-Unterricht der 6a aus, weil die 6a selbst auf Klassenfahrt ist. Kein Vertret
die betroffene Stunde (Grau `#757575`, Aufschrift "Ausfall") — dieselbe Überschreiben-Logik wie
bei Vertretungsstunden und Sondereinsätzen, nur dass hier nichts an die Stelle tritt.
**Nachtrag zu 4.3, achte Iteration (Button-Umbenennung + Zahnrad statt "bearbeiten"):**
Nutzer-Feedback: Der Button "Vertretung eintragen" deckt nach der fünften bis siebten Iteration
auch Sondereinsatz und Ausfall ab, der Name passt nicht mehr. Der Button "Stundenplan bearbeiten"
war zudem redundant (das Bearbeiten-Tab ist direkt anklickbar) und fühlte sich unzuverlässig an.
- "Vertretung eintragen" → "Ausnahme eintragen" (Button und Dialogtitel in
`SubstitutionEntryDialog.axaml`) als neutraler Oberbegriff für alle vier `SubstitutionKind`-Fälle.
- "Stundenplan bearbeiten" ersetzt durch einen kleinen Zahnrad-Button ("⚙️"), der in die
Einstellungen springt, direkt auf den Tab "Ferien & Feiertage" (Nachbar-Tabs "Stundenraster" und
"Aufsichten" sind von dort ein Klick entfernt) — dieselbe Navigate-mit-Zieltab-Konvention wie
`NavigateToGroupDetail`. Dafür `SettingsViewModel.ActiveTabIndex` (an `TabbedPage.SelectedIndex`
gebunden) sowie `MainWindowViewModel.NavigateToSettings(int initialTab = 0)` neu eingeführt.
- `TimetableViewModel.ShowEditorCommand` entfernt (kein Aufrufer mehr) statt umbenannt — es tat
ohnehin nur `ActiveTabIndex = 1`, was jetzt nirgends mehr gebraucht wird.
### 4.4 Wochen-/Tagesansicht
- [x] **4.4.1** Kalenderansicht über alle Gruppen: Woche und Tag — siehe Nachtrag zu 4.3
("Heute"-Tab: Tagesliste unten angedockt, gruppenübergreifendes Wochenraster darüber, inkl.
@@ -581,8 +595,13 @@ NAT-Unterricht der 6a aus, weil die 6a selbst auf Klassenfahrt ist. Kein Vertret
- [x] **4.4.2** Sprung von einer Stunde direkt in die Lerngruppe — siehe Nachtrag zu 4.3
("Heute"-Tab, Klick navigiert zur Planung der Gruppe statt direkt in die
Mitarbeitserfassung, da das für den Vorbereitungs-Kontext des Tages passender ist).
- [ ] **4.4.3** Anzeige anstehender Klausurtermine und Abgabefristen im Kalender. *(Heutige
Klausuren bereits im "Heute"-Tab sichtbar — mehrtägiger Vorausblick fehlt noch.)*
- [x] **4.4.3** Anzeige anstehender Klausurtermine im Kalender — neue Box "ANSTEHENDE KLAUSUREN"
im "Heute"-Tab, gruppenübergreifend, nächste 21 Tage (max. 10 Einträge), unabhängig vom
`WeekOffset` des Wochenrasters. `IExamRepository` brauchte dafür ein `GetAll()` (bisher nur
`GetByGroup`). **Abgabefristen bewusst nicht enthalten:** es gibt im Datenmodell noch kein
Frist-/Deadline-Konzept (nur `Exam.Date`) — das bräuchte ein neues Modell, siehe auch 9.8
("ggf. einem neuen Termine-Modell"). Kein Aufschub aus Unklarheit, sondern weil das über den
angefragten Umfang hinausginge.
### 4.5 Vernetzung Stundenplan ↔ Unterrichtsplanung (zurückgestellt)
@@ -1156,13 +1175,13 @@ Die Abschnitte sind thematisch, nicht chronologisch nummeriert. Sinnvolle Bearbe
erledigt (13.113.4 vollständig; 13.4.2 bewusst zurückgestellt, siehe dort).
5. ~~**Kapitel 5** (Schülerdokumentation)~~ — erledigt (5.2 als Auswertung des bestehenden
Anwesenheits-Trackings statt zweiter Erfassung, siehe dort).
6. ~~**Kapitel 4.1 + 4.2 + 4.3** (Unterrichtsplanung: Einheiten, Einzelstunden, Stundenplan)~~
erledigt, inkl. mehrerer Nachtrag-Iterationen aus Nutzer-Feedback (Ferien-Pflege in den
Einstellungen, Wochenraster mit Wochennavigation, Ferientage ausgegraut). ~~**Kapitel 4.4.1/
4.4.2**~~ — erledigt (im Zuge der 4.3-Nachträge miterledigt). ~~**4.2.5**~~ (Serienerzeugung
von Stunden aus dem Stundenplan) — erledigt. Offen bleiben **4.4.3** (Abgabefristen im
Kalender) und **4.5** (engere Vernetzung Stundenplan ↔ Lesson-Planung — ausdrücklich vom
Nutzer zurückgestellt, nicht aus Unklarheit; Desktop bekommt dort perspektivisch Graph/
Kompetenz-Verknüpfung/KI-Planung, ein möglicher Companion-Client bleibt bewusst minimal).
**→ nächster sinnvoller Schritt: 4.4.3, sonst weiter mit Kapitel 6/10/11.**
6. ~~**Kapitel 4.1 + 4.2 + 4.3 + 4.4** (Unterrichtsplanung: Einheiten, Einzelstunden, Stundenplan,
Wochen-/Tagesansicht)~~ — vollständig erledigt, inkl. mehrerer Nachtrag-Iterationen aus
Nutzer-Feedback (Ferien-Pflege in den Einstellungen, Wochenraster mit Wochennavigation,
Ferientage ausgegraut, Serienerzeugung, Stundenraster mit Zeitbedarf-Rückmeldung, Aufsichten,
Vertretung/Sondereinsatz/Ausfall, anstehende Klausuren). Offen bleibt nur **4.5** (engere
Vernetzung Stundenplan ↔ Lesson-Planung — ausdrücklich vom Nutzer zurückgestellt, nicht aus
Unklarheit; Desktop bekommt dort perspektivisch Graph/Kompetenz-Verknüpfung/KI-Planung, ein
möglicher Companion-Client bleibt bewusst minimal).
**→ nächster sinnvoller Schritt: Kapitel 6, 10 oder 11.**
7. **Kapitel 6** (Arbeitszeit), **11** (Export), **10** (Sync) — danach.