Stundenplan: Wochenraster, Wochennavigation, Ferien/Feiertage (Kapitel 4.3)

Neuer Stundenplan mit "Heute"-Standardansicht (Tagesliste unten angedockt,
gruppenübergreifendes Wochenraster mit Fach/Klasse/Raum/Thema, Vor-/Zurück-
Navigation zwischen Kalenderwochen) und separatem Bearbeiten-Raster für die
wöchentliche Zuordnung. Badges für Ferien-/Klausur-Nähe und ausgegraute
Ferientage direkt im Plan statt einer separaten Liste. Ferien-/Feiertage-
Pflege (Bundesland, Schulferien) sitzt jetzt in den Einstellungen statt im
Stundenplan selbst.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-15 02:29:18 +02:00
co-authored by Claude Sonnet 5
parent ee1a47641e
commit 99b2de6519
26 changed files with 2154 additions and 18 deletions
+37
View File
@@ -98,6 +98,17 @@ public class FakeResults : IExamResultRepository
public void SaveMany(List<ExamResult> results) { }
}
public class FakeGradingKeyTemplates : IGradingKeyTemplateRepository
{
private readonly List<GradingKeyTemplate> _all = [];
public List<GradingKeyTemplate> GetAll() => _all;
public List<GradingKeyTemplate> GetByGradingSystem(GradingSystem system) =>
_all.Where(t => t.GradingSystem == system).ToList();
public GradingKeyTemplate? GetById(Guid id) => _all.FirstOrDefault(t => t.Id == id);
public void Save(GradingKeyTemplate template) { _all.RemoveAll(t => t.Id == template.Id); _all.Add(template); }
public void Delete(Guid id) => _all.RemoveAll(t => t.Id == id);
}
public class FakeSchemes : IGradingSchemeRepository
{
private readonly Dictionary<Guid, GradingScheme> _byGroup = [];
@@ -206,6 +217,32 @@ public class FakeAlternativeLessonPaths(List<AlternativeLessonPath> all) : IAlte
public void Delete(Guid id) => all.RemoveAll(p => p.Id == id);
}
public class FakeTimetableSlots : ITimetableSlotRepository
{
private readonly List<TimetableSlot> _all = [];
public void Add(TimetableSlot s) => _all.Add(s);
public List<TimetableSlot> GetAll() => _all.ToList();
public List<TimetableSlot> GetByGroup(Guid groupId) => _all.Where(s => s.GroupId == groupId).ToList();
public void Save(TimetableSlot slot)
{
var occupied = _all.FirstOrDefault(s => s.Weekday == slot.Weekday && s.PeriodNumber == slot.PeriodNumber);
if (occupied is not null && occupied.Id != slot.Id)
throw new InvalidOperationException("Diese Stunde ist bereits belegt.");
_all.RemoveAll(s => s.Id == slot.Id);
_all.Add(slot);
}
public void Delete(Guid id) => _all.RemoveAll(s => s.Id == id);
}
public class FakeSchoolHolidays : ISchoolHolidayRepository
{
private readonly List<SchoolHoliday> _all = [];
public void Add(SchoolHoliday h) => _all.Add(h);
public List<SchoolHoliday> GetAll() => _all.ToList();
public void Save(SchoolHoliday holiday) { _all.RemoveAll(h => h.Id == holiday.Id); _all.Add(holiday); }
public void Delete(Guid id) => _all.RemoveAll(h => h.Id == id);
}
public class FakeReportGrades : IReportGradeRepository
{
private readonly List<ReportGrade> _all = [];
@@ -0,0 +1,106 @@
using LehrerApp.Core.Models;
using LehrerApp.Core.Services;
using LehrerApp.Data;
using LehrerApp.Desktop.ViewModels.Settings;
using Xunit;
namespace LehrerApp.Desktop.Tests;
public sealed class SettingsViewModelTests
{
private static SettingsViewModel BuildViewModel(FakeSchoolHolidays? holidays = null)
{
// Bewusst kein "using": SchoolCalendarSettingsService liest den Pfad erst bei SetState,
// das Verzeichnis muss über die Lebensdauer des ViewModels bestehen bleiben.
var tempPath = System.IO.Path.Combine(
System.IO.Path.GetTempPath(), $"lehrerapp-settingsvm-tests-{Guid.NewGuid():N}");
Directory.CreateDirectory(tempPath);
return new SettingsViewModel(
new FakeSubjects([]), new FakeCompetencyDomains(), new FakeGradingKeyTemplates(),
new FakeSchemes(), new GradingService(), new BackupService(tempPath),
new DatabaseEncryptionService(), new AppLockService(tempPath),
new LiteDbContext(new MemoryStream()), new PrivacySettingsService(tempPath),
new FakeDocumentation(), new FakeStudents([]), new FakeShorthandCodes([]),
holidays ?? new FakeSchoolHolidays(), new SchoolCalendarSettingsService(tempPath));
}
[Fact]
public void AddSchoolHoliday_GueltigeEingabe_WirdGespeichertUndInListeAngezeigt()
{
var holidays = new FakeSchoolHolidays();
var vm = BuildViewModel(holidays);
vm.NewHolidayName = "Sommerferien";
vm.NewHolidayStartText = "01.07.2026";
vm.NewHolidayEndText = "10.08.2026";
vm.AddSchoolHolidayCommand.Execute(null);
Assert.Single(holidays.GetAll());
Assert.Single(vm.SchoolHolidayEntries);
}
[Fact]
public void AddSchoolHoliday_EndeVorBeginn_SetztFehlerUndSpeichertNicht()
{
var holidays = new FakeSchoolHolidays();
var vm = BuildViewModel(holidays);
vm.NewHolidayName = "Ungültig";
vm.NewHolidayStartText = "10.08.2026";
vm.NewHolidayEndText = "01.07.2026";
vm.AddSchoolHolidayCommand.Execute(null);
Assert.Empty(holidays.GetAll());
Assert.NotEqual("", vm.HolidayDateError);
}
[Fact]
public void AddSchoolHoliday_FehlenderName_SetztFehlerUndSpeichertNicht()
{
var holidays = new FakeSchoolHolidays();
var vm = BuildViewModel(holidays);
vm.NewHolidayStartText = "01.07.2026";
vm.NewHolidayEndText = "10.08.2026";
vm.AddSchoolHolidayCommand.Execute(null);
Assert.Empty(holidays.GetAll());
Assert.NotEqual("", vm.HolidayNameError);
}
[Fact]
public void RemoveSchoolHoliday_EntferntEintragAusRepositoryUndListe()
{
var holidays = new FakeSchoolHolidays();
var holiday = new SchoolHoliday { Name = "Herbstferien", StartDate = new DateOnly(2026, 10, 12), EndDate = new DateOnly(2026, 10, 24) };
holidays.Add(holiday);
var vm = BuildViewModel(holidays);
vm.RemoveSchoolHolidayCommand.Execute(vm.SchoolHolidayEntries[0]);
Assert.Empty(holidays.GetAll());
Assert.Empty(vm.SchoolHolidayEntries);
}
[Fact]
public void SelectedStateName_Aendern_PersistiertUeberSchoolCalendarSettings()
{
var tempPath = System.IO.Path.Combine(
System.IO.Path.GetTempPath(), $"lehrerapp-settingsvm-state-tests-{Guid.NewGuid():N}");
Directory.CreateDirectory(tempPath);
var calendarSettings = new SchoolCalendarSettingsService(tempPath);
var vm = new SettingsViewModel(
new FakeSubjects([]), new FakeCompetencyDomains(), new FakeGradingKeyTemplates(),
new FakeSchemes(), new GradingService(), new BackupService(tempPath),
new DatabaseEncryptionService(), new AppLockService(tempPath),
new LiteDbContext(new MemoryStream()), new PrivacySettingsService(tempPath),
new FakeDocumentation(), new FakeStudents([]), new FakeShorthandCodes([]),
new FakeSchoolHolidays(), calendarSettings);
vm.SelectedStateName = "Bayern";
Assert.Equal(GermanState.BY, calendarSettings.State);
}
}
@@ -0,0 +1,78 @@
using LehrerApp.Core.Models;
using LehrerApp.Core.Services;
using LehrerApp.Desktop.ViewModels.Planning;
using Xunit;
namespace LehrerApp.Desktop.Tests;
public sealed class TimetableSlotDialogViewModelTests
{
private static SchoolYearService SchoolYear() => new();
[Fact]
public void Save_OhneAusgewaehlteGruppe_SetztFehlerUndSpeichertNicht()
{
var slots = new FakeTimetableSlots();
var groups = new FakeGroups([]);
var vm = new TimetableSlotDialogViewModel(slots, groups, SchoolYear(), DayOfWeek.Monday, 1, null);
vm.SaveCommand.Execute(null);
Assert.Null(vm.Result);
Assert.NotEqual("", vm.GroupError);
}
[Fact]
public void Save_MitAusgewaehlterGruppe_LegtNeuenSlotAn()
{
var group = new LearningGroup { Name = "Q1 Chemie" };
var slots = new FakeTimetableSlots();
var groups = new FakeGroups([group]);
var vm = new TimetableSlotDialogViewModel(slots, groups, SchoolYear(), DayOfWeek.Monday, 1, null)
{
SelectedGroupName = "Q1 Chemie", Room = "R204",
};
vm.SaveCommand.Execute(null);
Assert.NotNull(vm.Result);
Assert.Equal(group.Id, vm.Result!.GroupId);
Assert.Equal("R204", vm.Result.Room);
Assert.Single(slots.GetAll());
}
[Fact]
public void Save_BelegteStunde_ZeigtFreundlicheFehlermeldungStattAbsturz()
{
var groupA = new LearningGroup { Name = "Q1 Chemie" };
var groupB = new LearningGroup { Name = "Q1 Physik" };
var slots = new FakeTimetableSlots();
slots.Add(new TimetableSlot { GroupId = groupA.Id, Weekday = DayOfWeek.Monday, PeriodNumber = 1 });
var groups = new FakeGroups([groupA, groupB]);
var vm = new TimetableSlotDialogViewModel(slots, groups, SchoolYear(), DayOfWeek.Monday, 1, null)
{
SelectedGroupName = "Q1 Physik",
};
vm.SaveCommand.Execute(null);
Assert.Null(vm.Result);
Assert.Equal("Diese Stunde ist bereits belegt.", vm.GroupError);
}
[Fact]
public void Delete_BeimBearbeitenEinesVorhandenenSlots_EntferntIhn()
{
var group = new LearningGroup { Name = "Q1 Chemie" };
var slot = new TimetableSlot { GroupId = group.Id, Weekday = DayOfWeek.Monday, PeriodNumber = 1 };
var slots = new FakeTimetableSlots();
slots.Add(slot);
var groups = new FakeGroups([group]);
var vm = new TimetableSlotDialogViewModel(slots, groups, SchoolYear(), DayOfWeek.Monday, 1, slot);
vm.DeleteCommand.Execute(null);
Assert.True(vm.Deleted);
Assert.Empty(slots.GetAll());
}
}
@@ -0,0 +1,413 @@
using LehrerApp.Core.Models;
using LehrerApp.Core.Services;
using LehrerApp.Desktop.ViewModels.Planning;
using Xunit;
namespace LehrerApp.Desktop.Tests;
public sealed class TimetableViewModelTests
{
private static TimetableViewModel BuildViewModel(
FakeTimetableSlots slots, FakeGroups groups, FakeSchoolHolidays? holidays = null,
FakeSubjects? subjects = null, FakeLessons? lessons = null, FakeExams? exams = null,
SchoolCalendarSettingsService? calendarSettings = null)
{
// Bewusst kein "using": SchoolCalendarSettingsService liest den Pfad nur bei Bedarf
// (SetState), das Verzeichnis muss über die Lebensdauer des ViewModels bestehen bleiben.
var tempPath = System.IO.Path.Combine(
System.IO.Path.GetTempPath(), $"lehrerapp-timetablevm-tests-{Guid.NewGuid():N}");
Directory.CreateDirectory(tempPath);
return new TimetableViewModel(
slots, groups, subjects ?? new FakeSubjects([]),
lessons ?? new FakeLessons(), exams ?? new FakeExams([]),
holidays ?? new FakeSchoolHolidays(),
calendarSettings ?? new SchoolCalendarSettingsService(tempPath),
new PublicHolidayService(), new SchoolYearService());
}
/// Nächstes Datum ab (inkl.) <paramref name="from"/>, das auf einen Wochentag Mo-Fr fällt —
/// nur diese Tage haben eine Spalte im Raster. Für Badge-Tests reicht ein beliebiger
/// Mo-Fr-Wochentag: die Badges hängen nur am Wochentag (nicht am konkreten Kalenderdatum),
/// da `TimetableSlot` ein wiederkehrendes Muster ohne Datum ist.
private static DateOnly NextGridWeekday(DateOnly from)
{
var d = from;
while (d.DayOfWeek is DayOfWeek.Saturday or DayOfWeek.Sunday) d = d.AddDays(1);
return d;
}
/// Datum des angegebenen Wochentags in der laufenden Kalenderwoche — exakt wie
/// `TimetableViewModel.BuildWeekOverview` es berechnet, für Tests der Wochenkachel-Inhalte
/// (Thema, Klausur am Tag), die anders als die Badges an ein konkretes Datum gebunden sind.
private static DateOnly DateInCurrentWeek(DayOfWeek weekday)
{
var today = DateOnly.FromDateTime(DateTime.Today);
var monday = today.AddDays(-((int)today.DayOfWeek + 6) % 7);
return monday.AddDays((int)weekday - (int)DayOfWeek.Monday);
}
[Fact]
public void Load_BautEineZelleProWochentagUndStundePlusKopfzeilen()
{
var vm = BuildViewModel(new FakeTimetableSlots(), new FakeGroups([]));
// 1 Ecke + 5 Wochentage + 10 Stunden × (1 Label + 5 Zellen)
Assert.Equal(6 + 10 * 6, vm.Cells.Count);
Assert.Equal(6 + 10 * 6, vm.WeekItems.Count);
}
[Fact]
public void Load_ZeigtZugewieseneGruppeInDerPassendenZelle()
{
var weekday = NextGridWeekday(DateOnly.FromDateTime(DateTime.Today));
var group = new LearningGroup { Name = "Q1 Chemie" };
var slots = new FakeTimetableSlots();
slots.Add(new TimetableSlot { GroupId = group.Id, Weekday = weekday.DayOfWeek, PeriodNumber = 1, Room = "R204" });
var vm = BuildViewModel(slots, new FakeGroups([group]));
var cell = vm.Cells.Single(c => c.IsSlotCell && c.Weekday == weekday.DayOfWeek && c.PeriodNumber == 1);
Assert.True(cell.IsAssigned);
Assert.Equal("Q1 Chemie · R204", cell.Text);
}
[Fact]
public void HoursWarnings_WeichtDieZugewieseneStundenzahlAb_ZeigtWarnung()
{
var group = new LearningGroup { Name = "Q1 Chemie", SchoolYear = new SchoolYearService().CurrentSchoolYear(), HoursPerWeek = 3 };
var slots = new FakeTimetableSlots();
slots.Add(new TimetableSlot { GroupId = group.Id, Weekday = DayOfWeek.Monday, PeriodNumber = 1 });
var vm = BuildViewModel(slots, new FakeGroups([group]));
Assert.Single(vm.HoursWarnings);
Assert.Contains("1 von 3", vm.HoursWarnings[0].Text);
}
[Fact]
public void HoursWarnings_PassendeStundenzahl_KeineWarnung()
{
var group = new LearningGroup { Name = "Q1 Chemie", SchoolYear = new SchoolYearService().CurrentSchoolYear(), HoursPerWeek = 1 };
var slots = new FakeTimetableSlots();
slots.Add(new TimetableSlot { GroupId = group.Id, Weekday = DayOfWeek.Monday, PeriodNumber = 1 });
var vm = BuildViewModel(slots, new FakeGroups([group]));
Assert.Empty(vm.HoursWarnings);
}
// ── "Heute"-Ansicht: Tagesliste (Nutzer-Feedback: nicht-editierende Standardansicht) ────
[Fact]
public void Load_ZeigtHeutigeStundeMitGruppeUndRaum()
{
var today = DateOnly.FromDateTime(DateTime.Today);
var group = new LearningGroup { Name = "Q1 Chemie" };
var slots = new FakeTimetableSlots();
slots.Add(new TimetableSlot { GroupId = group.Id, Weekday = today.DayOfWeek, PeriodNumber = 3, Room = "R204" });
var vm = BuildViewModel(slots, new FakeGroups([group]));
var item = Assert.Single(vm.TodayItems);
Assert.Equal("Q1 Chemie", item.GroupName);
Assert.Equal("R204", item.Room);
Assert.Equal(3, item.PeriodNumber);
}
[Fact]
public void Load_HeutigeStundeMitLektionUndKlausur_ZeigtBeides()
{
var today = DateOnly.FromDateTime(DateTime.Today);
var group = new LearningGroup { Name = "Q1 Chemie" };
var slots = new FakeTimetableSlots();
slots.Add(new TimetableSlot { GroupId = group.Id, Weekday = today.DayOfWeek, PeriodNumber = 1 });
var lessons = new FakeLessons();
lessons.Add(new Lesson { GroupId = group.Id, Date = today, Topic = "Redoxreaktionen" });
var exams = new FakeExams([new Exam { GroupId = group.Id, Date = today, Title = "Klausur Nr. 2" }]);
var vm = BuildViewModel(slots, new FakeGroups([group]), lessons: lessons, exams: exams);
var item = Assert.Single(vm.TodayItems);
Assert.Equal("Redoxreaktionen", item.LessonTopic);
Assert.Equal("Klausur Nr. 2", item.ExamTitle);
Assert.True(item.HasExam);
}
[Fact]
public void OpenGroup_RuftOnNavigateToGroupMitDerGroupIdAuf()
{
var today = DateOnly.FromDateTime(DateTime.Today);
var group = new LearningGroup { Name = "Q1 Chemie" };
var slots = new FakeTimetableSlots();
slots.Add(new TimetableSlot { GroupId = group.Id, Weekday = today.DayOfWeek, PeriodNumber = 1 });
var vm = BuildViewModel(slots, new FakeGroups([group]));
Guid? navigatedTo = null;
vm.OnNavigateToGroup = id => navigatedTo = id;
vm.OpenGroupCommand.Execute(vm.TodayItems[0].GroupId);
Assert.Equal(group.Id, navigatedTo);
}
[Fact]
public void ShowEditor_SchaltetAufBearbeiten_Tab()
{
var vm = BuildViewModel(new FakeTimetableSlots(), new FakeGroups([]));
vm.ShowEditorCommand.Execute(null);
Assert.Equal(1, vm.ActiveTabIndex);
}
// ── "Heute"-Ansicht: Wochenraster (Nutzer-Feedback, zweite Iteration) ────────────────────
[Fact]
public void Load_Wochenkachel_ZeigtFachKlasseRaumUndThema()
{
var subject = new Subject { Name = "Chemie", ShortName = "Ch" };
var group = new LearningGroup { Name = "Q1 Chemie", SubjectId = subject.Id };
var date = DateInCurrentWeek(DayOfWeek.Monday);
var slots = new FakeTimetableSlots();
slots.Add(new TimetableSlot { GroupId = group.Id, Weekday = DayOfWeek.Monday, PeriodNumber = 1, Room = "R204" });
var lessons = new FakeLessons();
lessons.Add(new Lesson { GroupId = group.Id, Date = date, Topic = "Redoxreaktionen" });
var vm = BuildViewModel(slots, new FakeGroups([group]), subjects: new FakeSubjects([subject]), lessons: lessons);
var cell = vm.WeekItems.Single(c => c.IsSlotCell && c.Weekday == DayOfWeek.Monday && c.PeriodNumber == 1);
Assert.Equal("Ch", cell.SubjectLabel);
Assert.Equal("Q1 Chemie", cell.GroupName);
Assert.Equal("R204", cell.Room);
Assert.Equal("Redoxreaktionen", cell.Topic);
}
[Fact]
public void Load_Wochenkachel_UnbelegteZelle_IstNichtZugewiesen()
{
var vm = BuildViewModel(new FakeTimetableSlots(), new FakeGroups([]));
var cell = vm.WeekItems.Single(c => c.IsSlotCell && c.Weekday == DayOfWeek.Monday && c.PeriodNumber == 1);
Assert.False(cell.IsAssigned);
}
[Fact]
public void Load_Wochenkachel_KlausurAmTag_ZeigtKlausurIcon()
{
var group = new LearningGroup { Name = "Q1 Chemie" };
var date = DateInCurrentWeek(DayOfWeek.Monday);
var slots = new FakeTimetableSlots();
slots.Add(new TimetableSlot { GroupId = group.Id, Weekday = DayOfWeek.Monday, PeriodNumber = 1 });
var exams = new FakeExams([new Exam { GroupId = group.Id, Date = date, Title = "Klausur" }]);
var vm = BuildViewModel(slots, new FakeGroups([group]), exams: exams);
var cell = vm.WeekItems.Single(c => c.IsSlotCell && c.Weekday == DayOfWeek.Monday && c.PeriodNumber == 1);
Assert.True(cell.HasExam);
}
[Fact]
public void Load_Wochenkachel_TagInSchulferien_WirdAusgegraut()
{
var group = new LearningGroup { Name = "Q1 Chemie" };
var date = DateInCurrentWeek(DayOfWeek.Monday);
var slots = new FakeTimetableSlots();
slots.Add(new TimetableSlot { GroupId = group.Id, Weekday = DayOfWeek.Monday, PeriodNumber = 1 });
var holidays = new FakeSchoolHolidays();
holidays.Add(new SchoolHoliday { Name = "Ferien", StartDate = date, EndDate = date.AddDays(5) });
var vm = BuildViewModel(slots, new FakeGroups([group]), holidays);
var cell = vm.WeekItems.Single(c => c.IsSlotCell && c.Weekday == DayOfWeek.Monday && c.PeriodNumber == 1);
Assert.True(cell.IsHoliday);
Assert.Equal("#BDBDBD", cell.ColorHex);
}
[Fact]
public void Load_Wochenkachel_TagAusserhalbVonFerien_WirdNichtAusgegraut()
{
var group = new LearningGroup { Name = "Q1 Chemie" };
var slots = new FakeTimetableSlots();
slots.Add(new TimetableSlot { GroupId = group.Id, Weekday = DayOfWeek.Monday, PeriodNumber = 1 });
var vm = BuildViewModel(slots, new FakeGroups([group]));
var cell = vm.WeekItems.Single(c => c.IsSlotCell && c.Weekday == DayOfWeek.Monday && c.PeriodNumber == 1);
Assert.False(cell.IsHoliday);
}
[Fact]
public void Load_Wochenkachel_ExperimentInPhaseGeplant_ZeigtExperimentIcon()
{
var group = new LearningGroup { Name = "Q1 Chemie" };
var date = DateInCurrentWeek(DayOfWeek.Monday);
var slots = new FakeTimetableSlots();
slots.Add(new TimetableSlot { GroupId = group.Id, Weekday = DayOfWeek.Monday, PeriodNumber = 1 });
var lessons = new FakeLessons();
lessons.Add(new Lesson { GroupId = group.Id, Date = date, Phases = [new LessonPhaseStep { Activity = "Experiment: Redoxreihe" }] });
var vm = BuildViewModel(slots, new FakeGroups([group]), lessons: lessons);
var cell = vm.WeekItems.Single(c => c.IsSlotCell && c.Weekday == DayOfWeek.Monday && c.PeriodNumber == 1);
Assert.True(cell.HasExperiment);
}
[Fact]
public void Load_Wochenkachel_OhneExperimentErwaehnung_KeinExperimentIcon()
{
var group = new LearningGroup { Name = "Q1 Chemie" };
var date = DateInCurrentWeek(DayOfWeek.Monday);
var slots = new FakeTimetableSlots();
slots.Add(new TimetableSlot { GroupId = group.Id, Weekday = DayOfWeek.Monday, PeriodNumber = 1 });
var lessons = new FakeLessons();
lessons.Add(new Lesson { GroupId = group.Id, Date = date, Phases = [new LessonPhaseStep { Activity = "Stillarbeit" }] });
var vm = BuildViewModel(slots, new FakeGroups([group]), lessons: lessons);
var cell = vm.WeekItems.Single(c => c.IsSlotCell && c.Weekday == DayOfWeek.Monday && c.PeriodNumber == 1);
Assert.False(cell.HasExperiment);
}
[Fact]
public void Load_Wochenkachel_LetzteStundeVorKlausur_ZeigtIcon()
{
var date = DateInCurrentWeek(DayOfWeek.Monday);
var group = new LearningGroup { Name = "Q1 Chemie" };
var slots = new FakeTimetableSlots();
slots.Add(new TimetableSlot { GroupId = group.Id, Weekday = DayOfWeek.Monday, PeriodNumber = 1 });
var exams = new FakeExams([new Exam { GroupId = group.Id, Date = date.AddDays(1), Title = "Klausur" }]);
var vm = BuildViewModel(slots, new FakeGroups([group]), exams: exams);
var cell = vm.WeekItems.Single(c => c.IsSlotCell && c.Weekday == DayOfWeek.Monday && c.PeriodNumber == 1);
Assert.True(cell.IsLastBeforeExam);
}
[Fact]
public void Load_Wochenkachel_KeineAnstehendeKlausur_KeinIcon()
{
var group = new LearningGroup { Name = "Q1 Chemie" };
var slots = new FakeTimetableSlots();
slots.Add(new TimetableSlot { GroupId = group.Id, Weekday = DayOfWeek.Monday, PeriodNumber = 1 });
var vm = BuildViewModel(slots, new FakeGroups([group]));
var cell = vm.WeekItems.Single(c => c.IsSlotCell && c.Weekday == DayOfWeek.Monday && c.PeriodNumber == 1);
Assert.False(cell.IsLastBeforeExam);
}
[Fact]
public void PreviousWeek_ZeigtVorherigeKalenderwoche()
{
var vm = BuildViewModel(new FakeTimetableSlots(), new FakeGroups([]));
var initialLabel = vm.WeekRangeLabel;
vm.PreviousWeekCommand.Execute(null);
Assert.NotEqual(initialLabel, vm.WeekRangeLabel);
Assert.False(vm.IsCurrentWeek);
}
[Fact]
public void NextWeek_GefolgtVonCurrentWeek_KehrtZurLaufendenWocheZurueck()
{
var vm = BuildViewModel(new FakeTimetableSlots(), new FakeGroups([]));
var initialLabel = vm.WeekRangeLabel;
vm.NextWeekCommand.Execute(null);
Assert.NotEqual(initialLabel, vm.WeekRangeLabel);
vm.CurrentWeekCommand.Execute(null);
Assert.Equal(initialLabel, vm.WeekRangeLabel);
Assert.True(vm.IsCurrentWeek);
}
[Fact]
public void NextWeek_BadgeGehoertZurAngezeigtenWoche_NichtZuHeute()
{
// Klausur liegt in der übernächsten Woche relativ zu "heute" — im Wochenraster einer
// Woche dahinter (also der übernächsten Woche selbst) muss "letzte Stunde vor Klausur"
// erscheinen, nicht bereits in der aktuellen Woche.
var group = new LearningGroup { Name = "Q1 Chemie" };
var currentWeekMonday = DateInCurrentWeek(DayOfWeek.Monday);
var examWeekMonday = currentWeekMonday.AddDays(14);
var slots = new FakeTimetableSlots();
slots.Add(new TimetableSlot { GroupId = group.Id, Weekday = DayOfWeek.Monday, PeriodNumber = 1 });
var exams = new FakeExams([new Exam { GroupId = group.Id, Date = examWeekMonday.AddDays(1), Title = "Klausur" }]);
var vm = BuildViewModel(slots, new FakeGroups([group]), exams: exams);
var currentWeekCell = vm.WeekItems.Single(c => c.IsSlotCell && c.Weekday == DayOfWeek.Monday && c.PeriodNumber == 1);
Assert.False(currentWeekCell.IsLastBeforeExam);
vm.NextWeekCommand.Execute(null);
vm.NextWeekCommand.Execute(null);
var examWeekCell = vm.WeekItems.Single(c => c.IsSlotCell && c.Weekday == DayOfWeek.Monday && c.PeriodNumber == 1);
Assert.True(examWeekCell.IsLastBeforeExam);
}
// ── Badge: letzte/vorletzte Stunde vor Ferien (Bearbeiten-Raster) ────────────────────────
[Fact]
public void Load_LetzteStundeVorFerien_ZeigtBadge1()
{
var weekday = NextGridWeekday(DateOnly.FromDateTime(DateTime.Today));
var group = new LearningGroup { Name = "Q1 Chemie" };
var slots = new FakeTimetableSlots();
slots.Add(new TimetableSlot { GroupId = group.Id, Weekday = weekday.DayOfWeek, PeriodNumber = 1 });
var holidays = new FakeSchoolHolidays();
holidays.Add(new SchoolHoliday { Name = "Ferien", StartDate = weekday.AddDays(1), EndDate = weekday.AddDays(10) });
var vm = BuildViewModel(slots, new FakeGroups([group]), holidays);
var cell = vm.Cells.Single(c => c.IsSlotCell && c.Weekday == weekday.DayOfWeek && c.PeriodNumber == 1);
Assert.Equal("1", cell.BadgeText);
}
[Fact]
public void Load_Wochenkachel_LetzteStundeVorFerien_ZeigtBadge1()
{
var date = DateInCurrentWeek(DayOfWeek.Monday);
var group = new LearningGroup { Name = "Q1 Chemie" };
var slots = new FakeTimetableSlots();
slots.Add(new TimetableSlot { GroupId = group.Id, Weekday = DayOfWeek.Monday, PeriodNumber = 1 });
var holidays = new FakeSchoolHolidays();
holidays.Add(new SchoolHoliday { Name = "Ferien", StartDate = date.AddDays(1), EndDate = date.AddDays(10) });
var vm = BuildViewModel(slots, new FakeGroups([group]), holidays);
var weekCell = vm.WeekItems.Single(c => c.IsSlotCell && c.Weekday == DayOfWeek.Monday && c.PeriodNumber == 1);
Assert.Equal("1", weekCell.HolidayBadge);
}
[Fact]
public void Load_VorletzteStundeVorFerien_ZeigtBadge2()
{
var firstOccurrence = NextGridWeekday(DateOnly.FromDateTime(DateTime.Today));
var secondOccurrence = firstOccurrence.AddDays(7);
var group = new LearningGroup { Name = "Q1 Chemie" };
var slots = new FakeTimetableSlots();
slots.Add(new TimetableSlot { GroupId = group.Id, Weekday = firstOccurrence.DayOfWeek, PeriodNumber = 1 });
var holidays = new FakeSchoolHolidays();
holidays.Add(new SchoolHoliday { Name = "Ferien", StartDate = secondOccurrence.AddDays(1), EndDate = secondOccurrence.AddDays(10) });
var vm = BuildViewModel(slots, new FakeGroups([group]), holidays);
var cell = vm.Cells.Single(c => c.IsSlotCell && c.Weekday == firstOccurrence.DayOfWeek && c.PeriodNumber == 1);
Assert.Equal("2", cell.BadgeText);
}
[Fact]
public void Load_Doppelstunde_BeideStundenZeigenDasselbeBadge()
{
var weekday = NextGridWeekday(DateOnly.FromDateTime(DateTime.Today));
var group = new LearningGroup { Name = "Q1 Chemie" };
var slots = new FakeTimetableSlots();
slots.Add(new TimetableSlot { GroupId = group.Id, Weekday = weekday.DayOfWeek, PeriodNumber = 1 });
slots.Add(new TimetableSlot { GroupId = group.Id, Weekday = weekday.DayOfWeek, PeriodNumber = 2 });
var holidays = new FakeSchoolHolidays();
holidays.Add(new SchoolHoliday { Name = "Ferien", StartDate = weekday.AddDays(1), EndDate = weekday.AddDays(10) });
var vm = BuildViewModel(slots, new FakeGroups([group]), holidays);
var period1 = vm.Cells.Single(c => c.IsSlotCell && c.Weekday == weekday.DayOfWeek && c.PeriodNumber == 1);
var period2 = vm.Cells.Single(c => c.IsSlotCell && c.Weekday == weekday.DayOfWeek && c.PeriodNumber == 2);
Assert.Equal("1", period1.BadgeText);
Assert.Equal("1", period2.BadgeText);
}
[Fact]
public void Load_KeineAnstehendenFerien_KeinBadge()
{
var weekday = NextGridWeekday(DateOnly.FromDateTime(DateTime.Today));
var group = new LearningGroup { Name = "Q1 Chemie" };
var slots = new FakeTimetableSlots();
slots.Add(new TimetableSlot { GroupId = group.Id, Weekday = weekday.DayOfWeek, PeriodNumber = 1 });
var vm = BuildViewModel(slots, new FakeGroups([group]));
var cell = vm.Cells.Single(c => c.IsSlotCell && c.Weekday == weekday.DayOfWeek && c.PeriodNumber == 1);
Assert.False(cell.HasBadge);
}
}