feat: Komfort-Features für Unterrichtsplanung, Zeiterfassung und Dashboard

- Neue-Stunde-Dialog: Datum wird beim Anlegen anhand des Stundenplans
  und der letzten Stunde der Einheit vorbelegt statt auf "heute"
  (behebt eine falsch erkannte Doppelstunde, wenn "heute" nicht auf
  den passenden Wochentag fiel).
- Zeiterfassung: Button "Unterrichtszeit heute übernehmen" schlägt
  Start/Ende aus dem heutigen Stundenplan inkl. Puffer davor/danach vor.
- Dashboard: neue Kachel "Ungeplante Stunden" erinnert an Stunden ohne
  Thema für heute/morgen, mit Opt-out je Gruppe ("Benötigt
  Unterrichtsplanung"), Doppelstunden-Erkennung (keine doppelte Meldung
  für die zweite Periode) und Berücksichtigung von Stundenausfall.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-18 11:30:50 +02:00
co-authored by Claude Sonnet 5
parent a36b1d1d40
commit fd509fc921
17 changed files with 585 additions and 23 deletions
@@ -388,4 +388,53 @@ public sealed class LessonDialogViewModelTests
Assert.False(vm.HasTimeBudgetInfo);
}
// ── Datumsvorschlag für eine neue Stunde (Nutzer-Feedback: Feld stand sonst immer auf
// "heute", was die Doppelstunden-Erkennung stillschweigend auf eine Einzelperiode
// zurückfallen ließ, wenn heute zufällig nicht der Unterrichtstag ist) ────────────────────
[Fact]
public void NeueStunde_OhneStundenplanEintraege_BleibtBeimHeutigenDatum()
{
var vm = BuildVm(Guid.NewGuid(), Guid.NewGuid()); // FakeTimetableSlots leer
Assert.Equal(DateOnly.FromDateTime(DateTime.Today).ToString("dd.MM.yyyy"), vm.DateText);
}
[Fact]
public void NeueStunde_LetzteStundeInDerZukunft_SchlaegtNaechstenPassendenWochentagDanachVor()
{
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 = 3 });
var vm = new LessonDialogViewModel(lessons, new FakeShorthandCodes([]), new FakeAlternativeLessonPaths([]),
slots, NewPeriodSchedule(), unitId, groupId, "10c", "Chemie", [], [], null);
// Nächster Dienstag nach dem 06.01.2099 (einem Dienstag) ist der 13.01.2099.
Assert.Equal("13.01.2099", vm.DateText);
}
[Fact]
public void NeueStunde_LetzteStundeInDerVergangenheit_SchlaegtNaechstenPassendenWochentagAbHeuteVor()
{
var unitId = Guid.NewGuid();
var groupId = Guid.NewGuid();
var lessons = new FakeLessons();
lessons.Add(new Lesson { UnitId = unitId, GroupId = groupId, Date = new DateOnly(2000, 1, 1), LessonNumber = 3 });
var weekday = DateTime.Today.DayOfWeek; // garantiert am selben oder folgenden Tag erreichbar
var slots = new FakeTimetableSlots();
slots.Add(new TimetableSlot { GroupId = groupId, Weekday = weekday, PeriodNumber = 3 });
var vm = new LessonDialogViewModel(lessons, new FakeShorthandCodes([]), new FakeAlternativeLessonPaths([]),
slots, NewPeriodSchedule(), unitId, groupId, "10c", "Chemie", [], [], null);
var suggested = DateOnly.ParseExact(vm.DateText, "dd.MM.yyyy");
Assert.True(suggested >= DateOnly.FromDateTime(DateTime.Today));
Assert.Equal(weekday, suggested.DayOfWeek);
}
}