Unterrichtsplanung: Serienerzeugung, Stundenraster, Aufsichten & Vertretung (Kapitel 4.2/4.3 Nachtrag)

Stunden serienweise aus dem Stundenplan erzeugen (4.2.5); Stundenraster
(Uhrzeiten je Stunde) in den Einstellungen mit Zeitbedarf-Rückmeldung im
Verlaufsplan-Editor; wiederkehrende Pausenaufsicht; neuer "Vertretung
eintragen"-Dialog für einmalige Vertretungsaufsicht, Vertretungsstunde,
Sondereinsätze (Ausflüge, Berufsmessen) und schlichten Stundenausfall.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-15 21:59:07 +02:00
co-authored by Claude Sonnet 5
parent 99b2de6519
commit d6dee72acc
30 changed files with 2743 additions and 48 deletions
+26
View File
@@ -243,6 +243,32 @@ public class FakeSchoolHolidays : ISchoolHolidayRepository
public void Delete(Guid id) => _all.RemoveAll(h => h.Id == id);
}
public class FakeSupervisionDuties : ISupervisionDutyRepository
{
private readonly List<SupervisionDuty> _all = [];
public void Add(SupervisionDuty d) => _all.Add(d);
public List<SupervisionDuty> GetAll() => _all.ToList();
public void Save(SupervisionDuty duty)
{
var occupied = _all.FirstOrDefault(d => d.Weekday == duty.Weekday && d.AfterPeriod == duty.AfterPeriod);
if (occupied is not null && occupied.Id != duty.Id)
throw new InvalidOperationException("Für diese Pause ist bereits eine Aufsicht eingetragen.");
_all.RemoveAll(d => d.Id == duty.Id);
_all.Add(duty);
}
public void Delete(Guid id) => _all.RemoveAll(d => d.Id == id);
}
public class FakeSubstitutionEntries : ISubstitutionEntryRepository
{
private readonly List<SubstitutionEntry> _all = [];
public void Add(SubstitutionEntry e) => _all.Add(e);
public List<SubstitutionEntry> GetAll() => _all.ToList();
public List<SubstitutionEntry> GetByDate(DateOnly date) => _all.Where(e => e.Date == date).ToList();
public void Save(SubstitutionEntry entry) { _all.RemoveAll(e => e.Id == entry.Id); _all.Add(entry); }
public void Delete(Guid id) => _all.RemoveAll(e => e.Id == id);
}
public class FakeReportGrades : IReportGradeRepository
{
private readonly List<ReportGrade> _all = [];
@@ -0,0 +1,142 @@
using LehrerApp.Core.Models;
using LehrerApp.Core.Services;
using LehrerApp.Desktop.ViewModels.Groups;
using Xunit;
namespace LehrerApp.Desktop.Tests;
/// Tests für die Serienerzeugung von Stunden aus dem Stundenplan (4.2.5).
public sealed class GenerateLessonSeriesDialogViewModelTests
{
private static GenerateLessonSeriesDialogViewModel BuildVm(FakeTimetableSlots slots, FakeLessons lessons,
FakeSchoolHolidays holidays, Guid unitId, Guid groupId, DateOnly? from = null, DateOnly? to = null)
{
// Bewusst kein "using": SchoolCalendarSettingsService liest den Pfad nur bei Bedarf.
var tempPath = System.IO.Path.Combine(
System.IO.Path.GetTempPath(), $"lehrerapp-lessonseries-tests-{Guid.NewGuid():N}");
Directory.CreateDirectory(tempPath);
var vm = new GenerateLessonSeriesDialogViewModel(slots, lessons, holidays,
new PublicHolidayService(), new SchoolCalendarSettingsService(tempPath), unitId, groupId, null, null);
if (from is not null) vm.FromDateText = from.Value.ToString("dd.MM.yyyy");
if (to is not null) vm.ToDateText = to.Value.ToString("dd.MM.yyyy");
return vm;
}
[Fact]
public void Save_ErzeugtStundeProVorkommenDesWochentagsImZeitraum()
{
var groupId = Guid.NewGuid();
var unitId = Guid.NewGuid();
var from = new DateOnly(2025, 9, 1);
var to = from.AddDays(13); // deckt zwei Vorkommen des Wochentags von "from" ab (Tag 0 und 7)
var slots = new FakeTimetableSlots();
slots.Add(new TimetableSlot { GroupId = groupId, Weekday = from.DayOfWeek, PeriodNumber = 1 });
var lessons = new FakeLessons();
var vm = BuildVm(slots, lessons, new FakeSchoolHolidays(), unitId, groupId, from, to);
vm.SaveCommand.Execute(null);
Assert.NotNull(vm.Result);
Assert.Equal(2, vm.Result!.Created);
Assert.Equal(0, vm.Result.SkippedHoliday);
Assert.Equal(0, vm.Result.SkippedExisting);
Assert.All(lessons.GetByGroupAndRange(groupId, from, to), l => Assert.Equal(unitId, l.UnitId));
}
[Fact]
public void Save_UeberspringtTerminInSchulferien()
{
var groupId = Guid.NewGuid();
var unitId = Guid.NewGuid();
var from = new DateOnly(2025, 9, 1);
var to = from.AddDays(13);
var secondOccurrence = from.AddDays(7);
var slots = new FakeTimetableSlots();
slots.Add(new TimetableSlot { GroupId = groupId, Weekday = from.DayOfWeek, PeriodNumber = 1 });
var holidays = new FakeSchoolHolidays();
holidays.Add(new SchoolHoliday { Name = "Ferien", StartDate = secondOccurrence, EndDate = secondOccurrence.AddDays(3) });
var vm = BuildVm(slots, new FakeLessons(), holidays, unitId, groupId, from, to);
vm.SaveCommand.Execute(null);
Assert.Equal(1, vm.Result!.Created);
Assert.Equal(1, vm.Result.SkippedHoliday);
}
[Fact]
public void Save_UeberspringtBereitsVorhandeneStundeAmSelbenDatumUndPeriode()
{
var groupId = Guid.NewGuid();
var unitId = Guid.NewGuid();
var from = new DateOnly(2025, 9, 1);
var to = from.AddDays(13);
var slots = new FakeTimetableSlots();
slots.Add(new TimetableSlot { GroupId = groupId, Weekday = from.DayOfWeek, PeriodNumber = 1 });
var lessons = new FakeLessons();
lessons.Add(new Lesson { UnitId = Guid.NewGuid(), GroupId = groupId, Date = from, LessonNumber = 1, Topic = "Bestehend" });
var vm = BuildVm(slots, lessons, new FakeSchoolHolidays(), unitId, groupId, from, to);
vm.SaveCommand.Execute(null);
Assert.Equal(1, vm.Result!.Created);
Assert.Equal(1, vm.Result.SkippedExisting);
Assert.Equal(2, lessons.GetByGroupAndRange(groupId, from, to).Count); // 1 alt + 1 neu
}
[Fact]
public void Save_Doppelstunde_ErzeugtZweiStundenAmSelbenTagMitUnterschiedlicherStundennummer()
{
var groupId = Guid.NewGuid();
var unitId = Guid.NewGuid();
var from = new DateOnly(2025, 9, 1);
var slots = new FakeTimetableSlots();
slots.Add(new TimetableSlot { GroupId = groupId, Weekday = from.DayOfWeek, PeriodNumber = 3 });
slots.Add(new TimetableSlot { GroupId = groupId, Weekday = from.DayOfWeek, PeriodNumber = 4 });
var lessons = new FakeLessons();
var vm = BuildVm(slots, lessons, new FakeSchoolHolidays(), unitId, groupId, from, from);
vm.SaveCommand.Execute(null);
Assert.Equal(2, vm.Result!.Created);
var created = lessons.GetByGroupAndRange(groupId, from, from).Select(l => l.LessonNumber).OrderBy(n => n).ToList();
Assert.Equal([3, 4], created);
}
[Fact]
public void Save_OhneStundenplanEintrag_SetztFehlerUndErzeugtNichts()
{
var groupId = Guid.NewGuid();
var vm = BuildVm(new FakeTimetableSlots(), new FakeLessons(), new FakeSchoolHolidays(), Guid.NewGuid(), groupId,
new DateOnly(2025, 9, 1), new DateOnly(2025, 9, 14));
vm.SaveCommand.Execute(null);
Assert.Null(vm.Result);
Assert.NotEqual("", vm.DateError);
}
[Fact]
public void Save_UngueltigesDatumsformat_SetztFehler()
{
var vm = BuildVm(new FakeTimetableSlots(), new FakeLessons(), new FakeSchoolHolidays(), Guid.NewGuid(), Guid.NewGuid());
vm.FromDateText = "keinDatum";
vm.SaveCommand.Execute(null);
Assert.Null(vm.Result);
Assert.NotEqual("", vm.DateError);
}
[Fact]
public void Save_EndeVorBeginn_SetztFehler()
{
var vm = BuildVm(new FakeTimetableSlots(), new FakeLessons(), new FakeSchoolHolidays(), Guid.NewGuid(), Guid.NewGuid(),
new DateOnly(2025, 9, 10), new DateOnly(2025, 9, 1));
vm.SaveCommand.Execute(null);
Assert.Null(vm.Result);
Assert.NotEqual("", vm.DateError);
}
}
@@ -1,4 +1,5 @@
using LehrerApp.Core.Models;
using LehrerApp.Core.Services;
using LehrerApp.Desktop.ViewModels.Groups;
using Xunit;
@@ -6,8 +7,18 @@ namespace LehrerApp.Desktop.Tests;
public sealed class LessonDialogViewModelTests
{
private static LessonDialogViewModel BuildVm(Guid unitId, Guid groupId, Lesson? editing = null) =>
private static PeriodScheduleService NewPeriodSchedule()
{
var tempPath = System.IO.Path.Combine(
System.IO.Path.GetTempPath(), $"lehrerapp-lessondialogvm-tests-{Guid.NewGuid():N}");
Directory.CreateDirectory(tempPath);
return new PeriodScheduleService(tempPath);
}
private static LessonDialogViewModel BuildVm(Guid unitId, Guid groupId, Lesson? editing = null,
FakeTimetableSlots? slots = null, PeriodScheduleService? periodSchedule = null) =>
new(new FakeLessons(), new FakeShorthandCodes([]), new FakeAlternativeLessonPaths([]),
slots ?? new FakeTimetableSlots(), periodSchedule ?? NewPeriodSchedule(),
unitId, groupId, [], [], editing);
[Fact]
@@ -71,6 +82,7 @@ public sealed class LessonDialogViewModelTests
{
var codes = new FakeShorthandCodes([new ShorthandCode { Code = "Tb" }, new ShorthandCode { Code = "SH" }]);
var vm = new LessonDialogViewModel(new FakeLessons(), codes, new FakeAlternativeLessonPaths([]),
new FakeTimetableSlots(), NewPeriodSchedule(),
Guid.NewGuid(), Guid.NewGuid(), [], ["Plenum", "LDE", "Tb"], null); // "Tb" doppelt (Katalog + Historie), soll nur einmal erscheinen
Assert.Equal(["LDE", "Plenum", "SH", "Tb"], vm.ShorthandSuggestions);
@@ -133,7 +145,7 @@ public sealed class LessonDialogViewModelTests
var groupId = Guid.NewGuid();
var lessons = new FakeLessons();
var vm = new LessonDialogViewModel(lessons, new FakeShorthandCodes([]), new FakeAlternativeLessonPaths([]),
unitId, groupId, [], [], null)
new FakeTimetableSlots(), NewPeriodSchedule(), unitId, groupId, [], [], null)
{
Topic = "Brechung", DateText = "01.09.2025", StartTimeText = "11:45",
};
@@ -230,9 +242,140 @@ public sealed class LessonDialogViewModelTests
};
var vm = new LessonDialogViewModel(new FakeLessons(), new FakeShorthandCodes([]), alternativePaths,
Guid.NewGuid(), Guid.NewGuid(), [], [], editing);
new FakeTimetableSlots(), NewPeriodSchedule(), Guid.NewGuid(), Guid.NewGuid(), [], [], editing);
Assert.True(vm.Phases[0].HasAlternativePath);
Assert.Equal("Kurzversion", vm.Phases[0].AlternativePathName);
}
// ── Zeitbedarf-Rückmeldung (4.2.2 Nachtrag: Stundenraster) ───────────────────
[Fact]
public void RecomputeTimeBudget_EinzelneStunde_ZeigtVerfuegbareZeitAusStundenraster()
{
var groupId = Guid.NewGuid();
var date = new DateOnly(2025, 9, 1);
var periodSchedule = NewPeriodSchedule();
periodSchedule.SetPeriods([new PeriodTimeEntry { PeriodNumber = 3, Start = new TimeOnly(9, 45), End = new TimeOnly(10, 30) }]);
var vm = BuildVm(Guid.NewGuid(), groupId, periodSchedule: periodSchedule);
vm.DateText = date.ToString("dd.MM.yyyy");
vm.LessonNumber = 3;
vm.AddPhaseCommand.Execute(null);
vm.Phases[0].DurationMinutes = 40;
Assert.True(vm.HasTimeBudgetInfo);
Assert.Contains("40 von 45 Minuten geplant", vm.TimeBudgetLabel);
}
[Fact]
public void RecomputeTimeBudget_Doppelstunde_AddiertBeidePerioden()
{
var groupId = Guid.NewGuid();
var date = new DateOnly(2025, 9, 1);
var slots = new FakeTimetableSlots();
slots.Add(new TimetableSlot { GroupId = groupId, Weekday = date.DayOfWeek, PeriodNumber = 3 });
slots.Add(new TimetableSlot { GroupId = groupId, Weekday = date.DayOfWeek, PeriodNumber = 4 });
var periodSchedule = NewPeriodSchedule();
periodSchedule.SetPeriods([
new PeriodTimeEntry { PeriodNumber = 3, Start = new TimeOnly(9, 45), End = new TimeOnly(10, 30) },
new PeriodTimeEntry { PeriodNumber = 4, Start = new TimeOnly(10, 30), End = new TimeOnly(11, 15) },
]);
var vm = BuildVm(Guid.NewGuid(), groupId, slots: slots, periodSchedule: periodSchedule);
vm.DateText = date.ToString("dd.MM.yyyy");
vm.LessonNumber = 3;
vm.AddPhaseCommand.Execute(null);
vm.Phases[0].DurationMinutes = 84;
Assert.True(vm.HasTimeBudgetInfo);
Assert.Contains("84 von 90 Minuten geplant", vm.TimeBudgetLabel);
}
[Fact]
public void RecomputeTimeBudget_FolgeperiodeGehoertAndererGruppe_WirdNichtMitgezaehlt()
{
var groupId = Guid.NewGuid();
var otherGroupId = Guid.NewGuid();
var date = new DateOnly(2025, 9, 1);
var slots = new FakeTimetableSlots();
slots.Add(new TimetableSlot { GroupId = groupId, Weekday = date.DayOfWeek, PeriodNumber = 3 });
slots.Add(new TimetableSlot { GroupId = otherGroupId, Weekday = date.DayOfWeek, PeriodNumber = 4 });
var periodSchedule = NewPeriodSchedule();
periodSchedule.SetPeriods([
new PeriodTimeEntry { PeriodNumber = 3, Start = new TimeOnly(9, 45), End = new TimeOnly(10, 30) },
new PeriodTimeEntry { PeriodNumber = 4, Start = new TimeOnly(10, 30), End = new TimeOnly(11, 15) },
]);
var vm = BuildVm(Guid.NewGuid(), groupId, slots: slots, periodSchedule: periodSchedule);
vm.DateText = date.ToString("dd.MM.yyyy");
vm.LessonNumber = 3;
vm.AddPhaseCommand.Execute(null);
vm.Phases[0].DurationMinutes = 40;
Assert.Contains("40 von 45 Minuten geplant", vm.TimeBudgetLabel);
}
[Fact]
public void RecomputeTimeBudget_FarbeGruenImZielbereich()
{
var date = new DateOnly(2025, 9, 1);
var periodSchedule = NewPeriodSchedule();
periodSchedule.SetPeriods([new PeriodTimeEntry { PeriodNumber = 1, Start = new TimeOnly(8, 0), End = new TimeOnly(8, 45) }]);
var vm = BuildVm(Guid.NewGuid(), Guid.NewGuid(), periodSchedule: periodSchedule);
vm.DateText = date.ToString("dd.MM.yyyy");
vm.LessonNumber = 1;
vm.AddPhaseCommand.Execute(null);
vm.Phases[0].DurationMinutes = 42; // 42/45 ≈ 93 % — Zielbereich
Assert.Equal("#43A047", vm.TimeBudgetColorHex);
}
[Fact]
public void RecomputeTimeBudget_FarbeDunkelrotBeiDeutlicherUeberplanung()
{
var date = new DateOnly(2025, 9, 1);
var periodSchedule = NewPeriodSchedule();
periodSchedule.SetPeriods([new PeriodTimeEntry { PeriodNumber = 1, Start = new TimeOnly(8, 0), End = new TimeOnly(8, 45) }]);
var vm = BuildVm(Guid.NewGuid(), Guid.NewGuid(), periodSchedule: periodSchedule);
vm.DateText = date.ToString("dd.MM.yyyy");
vm.LessonNumber = 1;
vm.AddPhaseCommand.Execute(null);
vm.Phases[0].DurationMinutes = 60; // 60/45 ≈ 133 % — deutlich überplant
Assert.Equal("#B71C1C", vm.TimeBudgetColorHex);
}
[Fact]
public void LessonNumberGeaendert_UebernimmtBeginnAusStundenraster_WennNochKeinerEingetragen()
{
var periodSchedule = NewPeriodSchedule();
periodSchedule.SetPeriods([new PeriodTimeEntry { PeriodNumber = 5, Start = new TimeOnly(11, 45), End = new TimeOnly(12, 30) }]);
var vm = BuildVm(Guid.NewGuid(), Guid.NewGuid(), periodSchedule: periodSchedule);
vm.LessonNumber = 5;
Assert.Equal("11:45", vm.StartTimeText);
}
[Fact]
public void LessonNumberGeaendert_UeberschreibtBereitsEingetragenenBeginnNicht()
{
var periodSchedule = NewPeriodSchedule();
periodSchedule.SetPeriods([new PeriodTimeEntry { PeriodNumber = 5, Start = new TimeOnly(11, 45), End = new TimeOnly(12, 30) }]);
var vm = BuildVm(Guid.NewGuid(), Guid.NewGuid(), periodSchedule: periodSchedule);
vm.StartTimeText = "09:00";
vm.LessonNumber = 5;
Assert.Equal("09:00", vm.StartTimeText);
}
[Fact]
public void RecomputeTimeBudget_OhneStundenrasterEintrag_KeineRueckmeldung()
{
var vm = BuildVm(Guid.NewGuid(), Guid.NewGuid()); // leeres PeriodScheduleService
vm.DateText = "01.09.2025";
vm.LessonNumber = 1;
vm.AddPhaseCommand.Execute(null);
Assert.False(vm.HasTimeBudgetInfo);
}
}
@@ -181,6 +181,29 @@ public class PlanningTabViewModelTests
Assert.Equal(["Arbeitsblatt", "Modell"], vm.KnownMaterials);
}
[Fact]
public async Task GenerateLessonSeries_RuftDelegateMitAusgewaehlterEinheitAufUndLaedtBeiErfolgNeu()
{
var (vm, units, lessons, groupId) = BuildScenario();
var unit = new Unit { GroupId = groupId, Title = "Optik" };
units.Add(unit);
vm.Initialize(groupId);
vm.SelectedUnit = vm.Units.Single(u => u.Id == unit.Id);
Unit? passedUnit = null;
vm.OnGenerateLessonSeries = u =>
{
passedUnit = u;
lessons.Add(new Lesson { UnitId = unit.Id, GroupId = groupId, Date = new DateOnly(2025, 9, 1), Topic = "" });
return Task.FromResult<LessonSeriesResult?>(new LessonSeriesResult(1, 0, 0));
};
await vm.GenerateLessonSeriesCommand.ExecuteAsync(null);
Assert.Equal(unit.Id, passedUnit!.Id);
Assert.Single(vm.Lessons);
}
[Fact]
public void LoadUnits_SammeltKurzsymboleAusAllenStundenphasenDerGruppe()
{
@@ -8,7 +8,8 @@ namespace LehrerApp.Desktop.Tests;
public sealed class SettingsViewModelTests
{
private static SettingsViewModel BuildViewModel(FakeSchoolHolidays? holidays = null)
private static SettingsViewModel BuildViewModel(FakeSchoolHolidays? holidays = null,
FakeSupervisionDuties? supervisionDuties = null)
{
// Bewusst kein "using": SchoolCalendarSettingsService liest den Pfad erst bei SetState,
// das Verzeichnis muss über die Lebensdauer des ViewModels bestehen bleiben.
@@ -22,7 +23,8 @@ public sealed class SettingsViewModelTests
new DatabaseEncryptionService(), new AppLockService(tempPath),
new LiteDbContext(new MemoryStream()), new PrivacySettingsService(tempPath),
new FakeDocumentation(), new FakeStudents([]), new FakeShorthandCodes([]),
holidays ?? new FakeSchoolHolidays(), new SchoolCalendarSettingsService(tempPath));
holidays ?? new FakeSchoolHolidays(), new SchoolCalendarSettingsService(tempPath),
new PeriodScheduleService(tempPath), supervisionDuties ?? new FakeSupervisionDuties());
}
[Fact]
@@ -97,10 +99,123 @@ public sealed class SettingsViewModelTests
new DatabaseEncryptionService(), new AppLockService(tempPath),
new LiteDbContext(new MemoryStream()), new PrivacySettingsService(tempPath),
new FakeDocumentation(), new FakeStudents([]), new FakeShorthandCodes([]),
new FakeSchoolHolidays(), calendarSettings);
new FakeSchoolHolidays(), calendarSettings, new PeriodScheduleService(tempPath),
new FakeSupervisionDuties());
vm.SelectedStateName = "Bayern";
Assert.Equal(GermanState.BY, calendarSettings.State);
}
[Fact]
public void SavePeriodTimes_GueltigeEingabe_WirdPersistiert()
{
var tempPath = System.IO.Path.Combine(
System.IO.Path.GetTempPath(), $"lehrerapp-settingsvm-periods-tests-{Guid.NewGuid():N}");
Directory.CreateDirectory(tempPath);
var periodSchedule = new PeriodScheduleService(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(), new SchoolCalendarSettingsService(tempPath), periodSchedule,
new FakeSupervisionDuties());
vm.PeriodTimes[0].StartText = "08:00";
vm.PeriodTimes[0].EndText = "08:45";
vm.SavePeriodTimesCommand.Execute(null);
Assert.Equal("", vm.PeriodTimesError);
Assert.Equal(45, periodSchedule.GetDurationMinutes(1));
}
[Fact]
public void SavePeriodTimes_EndeVorBeginn_SetztFehlerUndSpeichertNicht()
{
var tempPath = System.IO.Path.Combine(
System.IO.Path.GetTempPath(), $"lehrerapp-settingsvm-periods-invalid-tests-{Guid.NewGuid():N}");
Directory.CreateDirectory(tempPath);
var periodSchedule = new PeriodScheduleService(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(), new SchoolCalendarSettingsService(tempPath), periodSchedule,
new FakeSupervisionDuties());
vm.PeriodTimes[0].StartText = "08:45";
vm.PeriodTimes[0].EndText = "08:00";
vm.SavePeriodTimesCommand.Execute(null);
Assert.NotEqual("", vm.PeriodTimesError);
Assert.Equal(0, periodSchedule.GetDurationMinutes(1));
}
[Fact]
public void AddSupervisionDuty_GueltigeEingabe_WirdGespeichertUndInListeAngezeigt()
{
var duties = new FakeSupervisionDuties();
var vm = BuildViewModel(supervisionDuties: duties);
vm.NewDutyWeekdayName = "Montag";
vm.NewDutyAfterPeriod = 2;
vm.NewDutyLocation = "Pausenhof";
vm.AddSupervisionDutyCommand.Execute(null);
Assert.Single(duties.GetAll());
Assert.Single(vm.SupervisionDuties);
Assert.Equal(DayOfWeek.Monday, duties.GetAll()[0].Weekday);
Assert.Equal(2, duties.GetAll()[0].AfterPeriod);
}
[Fact]
public void AddSupervisionDuty_FehlenderOrt_SetztFehlerUndSpeichertNicht()
{
var duties = new FakeSupervisionDuties();
var vm = BuildViewModel(supervisionDuties: duties);
vm.NewDutyWeekdayName = "Montag";
vm.NewDutyAfterPeriod = 2;
vm.AddSupervisionDutyCommand.Execute(null);
Assert.Empty(duties.GetAll());
Assert.NotEqual("", vm.NewDutyError);
}
[Fact]
public void AddSupervisionDuty_PauseBereitsBelegt_ZeigtFreundlicheFehlermeldung()
{
var duties = new FakeSupervisionDuties();
duties.Add(new SupervisionDuty { Weekday = DayOfWeek.Monday, AfterPeriod = 2, Location = "Pausenhof" });
var vm = BuildViewModel(supervisionDuties: duties);
vm.NewDutyWeekdayName = "Montag";
vm.NewDutyAfterPeriod = 2;
vm.NewDutyLocation = "Bibliothek";
vm.AddSupervisionDutyCommand.Execute(null);
Assert.Single(duties.GetAll());
Assert.Equal("Für diese Pause ist bereits eine Aufsicht eingetragen.", vm.NewDutyError);
}
[Fact]
public void RemoveSupervisionDuty_EntferntEintragAusRepositoryUndListe()
{
var duties = new FakeSupervisionDuties();
duties.Add(new SupervisionDuty { Weekday = DayOfWeek.Monday, AfterPeriod = 2, Location = "Pausenhof" });
var vm = BuildViewModel(supervisionDuties: duties);
vm.RemoveSupervisionDutyCommand.Execute(vm.SupervisionDuties[0]);
Assert.Empty(duties.GetAll());
Assert.Empty(vm.SupervisionDuties);
}
}
@@ -0,0 +1,323 @@
using LehrerApp.Core.Models;
using LehrerApp.Desktop.ViewModels.Planning;
using Xunit;
namespace LehrerApp.Desktop.Tests;
public sealed class SubstitutionEntryDialogViewModelTests
{
private static SubstitutionEntryDialogViewModel BuildVm(
FakeSubstitutionEntries? substitutions = null, FakeGroups? groups = null,
FakeUnits? units = null, FakeLessons? lessons = null) =>
new(substitutions ?? new FakeSubstitutionEntries(), groups ?? new FakeGroups([]),
units ?? new FakeUnits(), lessons ?? new FakeLessons());
[Fact]
public void Save_Aufsicht_GueltigeEingabe_ErzeugtSupervisionEntry()
{
var substitutions = new FakeSubstitutionEntries();
var vm = BuildVm(substitutions);
vm.DateText = "12.03.2026";
vm.KindName = "Aufsicht";
vm.AfterPeriod = 2;
vm.Description = "Vertretung für Hr. Müller";
vm.SaveCommand.Execute(null);
Assert.NotNull(vm.Result);
var saved = Assert.Single(substitutions.GetAll());
Assert.Equal(SubstitutionKind.Supervision, saved.Kind);
Assert.Equal(2, saved.AfterPeriod);
Assert.Equal("Vertretung für Hr. Müller", saved.Description);
}
[Fact]
public void Save_Aufsicht_FehlenderGrund_SetztFehlerUndSpeichertNicht()
{
var substitutions = new FakeSubstitutionEntries();
var vm = BuildVm(substitutions);
vm.DateText = "12.03.2026";
vm.KindName = "Aufsicht";
vm.AfterPeriod = 2;
vm.SaveCommand.Execute(null);
Assert.Null(vm.Result);
Assert.Empty(substitutions.GetAll());
Assert.NotEqual("", vm.ValidationError);
}
[Fact]
public void Save_Stunde_FremdeGruppe_ErzeugtEintragOhneGroupId()
{
var substitutions = new FakeSubstitutionEntries();
var vm = BuildVm(substitutions);
vm.DateText = "12.03.2026";
vm.KindName = "Stunde";
vm.PeriodNumber = 3;
vm.GroupLabel = "8a";
vm.Description = "Vertretung Erdkunde";
vm.SaveCommand.Execute(null);
var saved = Assert.Single(substitutions.GetAll());
Assert.Null(saved.GroupId);
Assert.Equal("8a", saved.GroupLabel);
Assert.Equal(SubstitutionKind.Lesson, saved.Kind);
}
[Fact]
public void Save_Stunde_EigeneGruppeOhnePromote_ErzeugtKeineLesson()
{
var group = new LearningGroup { Name = "Q1 Chemie" };
var unit = new Unit { GroupId = group.Id, Title = "Optik" };
var units = new FakeUnits(); units.Add(unit);
var lessons = new FakeLessons();
var vm = BuildVm(groups: new FakeGroups([group]), units: units, lessons: lessons);
vm.DateText = "12.03.2026";
vm.KindName = "Stunde";
vm.PeriodNumber = 3;
vm.SelectedOwnGroup = group;
vm.Description = "Stillarbeit";
vm.SaveCommand.Execute(null);
Assert.NotNull(vm.Result);
Assert.Empty(lessons.GetByUnit(unit.Id));
}
[Fact]
public void Save_Stunde_EigeneGruppeMitPromote_ErzeugtZusaetzlichLesson()
{
var group = new LearningGroup { Name = "Q1 Chemie" };
var unit = new Unit { GroupId = group.Id, Title = "Optik" };
var units = new FakeUnits(); units.Add(unit);
var lessons = new FakeLessons();
var vm = BuildVm(groups: new FakeGroups([group]), units: units, lessons: lessons);
vm.DateText = "12.03.2026";
vm.KindName = "Stunde";
vm.PeriodNumber = 3;
vm.SelectedOwnGroup = group; // füllt UnitsOfSelectedGroup + SelectedUnit
vm.Description = "Brechung (vorgezogen)";
vm.PromoteToLesson = true;
vm.SaveCommand.Execute(null);
var lesson = Assert.Single(lessons.GetByUnit(unit.Id));
Assert.Equal(group.Id, lesson.GroupId);
Assert.Equal(3, lesson.LessonNumber);
Assert.Equal("Brechung (vorgezogen)", lesson.Topic);
Assert.Equal(new DateOnly(2026, 3, 12), lesson.Date);
}
[Fact]
public void SelectedOwnGroupChanged_FuelltGroupLabelUndEinheitenListe()
{
var group = new LearningGroup { Name = "Q1 Chemie" };
var unit = new Unit { GroupId = group.Id, Title = "Optik" };
var units = new FakeUnits(); units.Add(unit);
var vm = BuildVm(groups: new FakeGroups([group]), units: units);
vm.SelectedOwnGroup = group;
Assert.Equal("Q1 Chemie", vm.GroupLabel);
Assert.Single(vm.UnitsOfSelectedGroup);
Assert.Equal(unit.Id, vm.SelectedUnit!.Id);
}
[Fact]
public void CanPromoteToLesson_FalseWennGruppeKeineEinheitenHat()
{
var group = new LearningGroup { Name = "Q1 Chemie" };
var vm = BuildVm(groups: new FakeGroups([group]));
vm.KindName = "Stunde";
vm.SelectedOwnGroup = group;
Assert.False(vm.CanPromoteToLesson);
}
[Fact]
public void Save_UngueltigesDatum_SetztFehler()
{
var vm = BuildVm();
vm.DateText = "keinDatum";
vm.KindName = "Aufsicht";
vm.Description = "x";
vm.SaveCommand.Execute(null);
Assert.Null(vm.Result);
Assert.NotEqual("", vm.DateError);
}
// ── Sondereinsatz ─────────────────────────────────────────────────────────
[Fact]
public void Save_Sondereinsatz_Ganztaegig_ErzeugtEintragOhneStundenbereich()
{
var substitutions = new FakeSubstitutionEntries();
var vm = BuildVm(substitutions);
vm.DateText = "12.03.2026";
vm.KindName = "Sondereinsatz";
vm.IsAllDay = true;
vm.Description = "Ausflug ins Museum";
vm.SaveCommand.Execute(null);
var saved = Assert.Single(substitutions.GetAll());
Assert.Equal(SubstitutionKind.SpecialAssignment, saved.Kind);
Assert.True(saved.IsAllDay);
Assert.Null(saved.FromPeriod);
Assert.Null(saved.ToPeriod);
}
[Fact]
public void Save_Sondereinsatz_MitStundenbereich_SpeichertVonBis()
{
var substitutions = new FakeSubstitutionEntries();
var vm = BuildVm(substitutions);
vm.DateText = "12.03.2026";
vm.KindName = "Sondereinsatz";
vm.IsAllDay = false;
vm.FromPeriod = 3;
vm.ToPeriod = 4;
vm.Description = "Berufsmesse";
vm.SaveCommand.Execute(null);
var saved = Assert.Single(substitutions.GetAll());
Assert.False(saved.IsAllDay);
Assert.Equal(3, saved.FromPeriod);
Assert.Equal(4, saved.ToPeriod);
}
[Fact]
public void Save_Sondereinsatz_BisVorVon_SetztFehler()
{
var substitutions = new FakeSubstitutionEntries();
var vm = BuildVm(substitutions);
vm.DateText = "12.03.2026";
vm.KindName = "Sondereinsatz";
vm.IsAllDay = false;
vm.FromPeriod = 5;
vm.ToPeriod = 2;
vm.Description = "Berufsmesse";
vm.SaveCommand.Execute(null);
Assert.Null(vm.Result);
Assert.Empty(substitutions.GetAll());
Assert.NotEqual("", vm.ValidationError);
}
[Fact]
public void Save_Sondereinsatz_OhneBeschreibung_SetztFehler()
{
var substitutions = new FakeSubstitutionEntries();
var vm = BuildVm(substitutions);
vm.DateText = "12.03.2026";
vm.KindName = "Sondereinsatz";
vm.SaveCommand.Execute(null);
Assert.Null(vm.Result);
Assert.Empty(substitutions.GetAll());
Assert.NotEqual("", vm.ValidationError);
}
[Fact]
public void Save_Sondereinsatz_OhneGruppe_SpeichertLeeresGroupLabel()
{
var substitutions = new FakeSubstitutionEntries();
var vm = BuildVm(substitutions);
vm.DateText = "12.03.2026";
vm.KindName = "Sondereinsatz";
vm.Description = "Berufsmesse";
vm.SaveCommand.Execute(null);
var saved = Assert.Single(substitutions.GetAll());
Assert.Null(saved.GroupId);
Assert.Equal("", saved.GroupLabel);
}
[Fact]
public void CanPromoteToLesson_FalseFuerSondereinsatz()
{
var group = new LearningGroup { Name = "Q1 Chemie" };
var unit = new Unit { GroupId = group.Id, Title = "Optik" };
var units = new FakeUnits(); units.Add(unit);
var vm = BuildVm(groups: new FakeGroups([group]), units: units);
vm.KindName = "Sondereinsatz";
vm.SelectedOwnGroup = group;
Assert.False(vm.CanPromoteToLesson);
}
// ── Ausfall ───────────────────────────────────────────────────────────────
[Fact]
public void Save_Ausfall_GueltigeEingabe_ErzeugtEintragMitPeriodeUndGrund()
{
var substitutions = new FakeSubstitutionEntries();
var vm = BuildVm(substitutions);
vm.DateText = "12.03.2026";
vm.KindName = "Ausfall";
vm.PeriodNumber = 2;
vm.Description = "6a auf Klassenfahrt";
vm.SaveCommand.Execute(null);
var saved = Assert.Single(substitutions.GetAll());
Assert.Equal(SubstitutionKind.Cancelled, saved.Kind);
Assert.Equal(2, saved.PeriodNumber);
Assert.Equal("6a auf Klassenfahrt", saved.Description);
}
[Fact]
public void Save_Ausfall_OhneGrund_IstErlaubt()
{
var substitutions = new FakeSubstitutionEntries();
var vm = BuildVm(substitutions);
vm.DateText = "12.03.2026";
vm.KindName = "Ausfall";
vm.PeriodNumber = 2;
vm.SaveCommand.Execute(null);
Assert.NotNull(vm.Result);
Assert.Single(substitutions.GetAll());
}
[Fact]
public void Save_Ausfall_UngueltigePeriode_SetztFehler()
{
var substitutions = new FakeSubstitutionEntries();
var vm = BuildVm(substitutions);
vm.DateText = "12.03.2026";
vm.KindName = "Ausfall";
vm.PeriodNumber = 11;
vm.SaveCommand.Execute(null);
Assert.Null(vm.Result);
Assert.Empty(substitutions.GetAll());
Assert.NotEqual("", vm.ValidationError);
}
[Fact]
public void CanPromoteToLesson_FalseFuerAusfall()
{
var group = new LearningGroup { Name = "Q1 Chemie" };
var unit = new Unit { GroupId = group.Id, Title = "Optik" };
var units = new FakeUnits(); units.Add(unit);
var vm = BuildVm(groups: new FakeGroups([group]), units: units);
vm.KindName = "Ausfall";
vm.SelectedOwnGroup = group;
Assert.False(vm.CanPromoteToLesson);
}
}
@@ -10,7 +10,8 @@ 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)
SchoolCalendarSettingsService? calendarSettings = null,
FakeSupervisionDuties? supervisionDuties = null, FakeSubstitutionEntries? substitutions = null)
{
// Bewusst kein "using": SchoolCalendarSettingsService liest den Pfad nur bei Bedarf
// (SetState), das Verzeichnis muss über die Lebensdauer des ViewModels bestehen bleiben.
@@ -22,7 +23,8 @@ public sealed class TimetableViewModelTests
lessons ?? new FakeLessons(), exams ?? new FakeExams([]),
holidays ?? new FakeSchoolHolidays(),
calendarSettings ?? new SchoolCalendarSettingsService(tempPath),
new PublicHolidayService(), new SchoolYearService());
new PublicHolidayService(), new SchoolYearService(),
supervisionDuties ?? new FakeSupervisionDuties(), substitutions ?? new FakeSubstitutionEntries());
}
/// Nächstes Datum ab (inkl.) <paramref name="from"/>, das auf einen Wochentag Mo-Fr fällt —
@@ -410,4 +412,334 @@ public sealed class TimetableViewModelTests
var cell = vm.Cells.Single(c => c.IsSlotCell && c.Weekday == weekday.DayOfWeek && c.PeriodNumber == 1);
Assert.False(cell.HasBadge);
}
// ── Aufsicht: wiederkehrend (Bearbeiten-Raster) ──────────────────────────
[Fact]
public void Load_AufsichtEingetragen_ZeigtSupervisionRowMitOrt()
{
var duties = new FakeSupervisionDuties();
duties.Add(new SupervisionDuty { Weekday = DayOfWeek.Monday, AfterPeriod = 2, Location = "Pausenhof" });
var vm = BuildViewModel(new FakeTimetableSlots(), new FakeGroups([]), supervisionDuties: duties);
var label = vm.Cells.Single(c => c.IsSupervisionRow && c.Text.Contains("n. 2."));
Assert.NotNull(label);
var mondayCell = vm.Cells
.SkipWhile(c => c != label).Skip(1) // erste Zelle nach dem Label = Montag
.First();
Assert.True(mondayCell.IsSupervisionCell);
Assert.Equal("Pausenhof", mondayCell.SupervisionLocation);
}
[Fact]
public void Load_KeineAufsichtEingetragen_KeineSupervisionRow()
{
var vm = BuildViewModel(new FakeTimetableSlots(), new FakeGroups([]));
Assert.DoesNotContain(vm.Cells, c => c.IsSupervisionRow);
}
// ── Aufsicht + Vertretung im Wochenraster ────────────────────────────────
[Fact]
public void Load_Wochenraster_ZeigtWiederkehrendeAufsicht()
{
var duties = new FakeSupervisionDuties();
duties.Add(new SupervisionDuty { Weekday = DayOfWeek.Monday, AfterPeriod = 2, Location = "Pausenhof" });
var vm = BuildViewModel(new FakeTimetableSlots(), new FakeGroups([]), supervisionDuties: duties);
var label = vm.WeekItems.Single(c => c.IsSupervisionRow && c.Text.Contains("n. 2."));
var mondayCell = vm.WeekItems.SkipWhile(c => c != label).Skip(1).First();
Assert.Equal("Pausenhof", mondayCell.SupervisionLocation);
Assert.False(mondayCell.IsSubstitutionSupervision);
}
[Fact]
public void Load_Wochenraster_VertretungsaufsichtUeberschreibtRegulaereAnzeige()
{
var date = DateInCurrentWeek(DayOfWeek.Monday);
var duties = new FakeSupervisionDuties();
duties.Add(new SupervisionDuty { Weekday = DayOfWeek.Monday, AfterPeriod = 2, Location = "Pausenhof" });
var substitutions = new FakeSubstitutionEntries();
substitutions.Add(new SubstitutionEntry
{
Date = date, Kind = SubstitutionKind.Supervision, AfterPeriod = 2,
Description = "Vertretung für Hr. Müller",
});
var vm = BuildViewModel(new FakeTimetableSlots(), new FakeGroups([]),
supervisionDuties: duties, substitutions: substitutions);
var label = vm.WeekItems.Single(c => c.IsSupervisionRow && c.Text.Contains("n. 2."));
var mondayCell = vm.WeekItems.SkipWhile(c => c != label).Skip(1).First();
Assert.Equal("Vertretung für Hr. Müller", mondayCell.SupervisionLocation);
Assert.True(mondayCell.IsSubstitutionSupervision);
}
[Fact]
public void Load_Wochenraster_VertretungsaufsichtOhneRegulaereAufsicht_ZeigtEigeneRow()
{
var date = DateInCurrentWeek(DayOfWeek.Monday);
var substitutions = new FakeSubstitutionEntries();
substitutions.Add(new SubstitutionEntry
{
Date = date, Kind = SubstitutionKind.Supervision, AfterPeriod = 3,
Description = "Vertretung für Fr. Schmidt",
});
var vm = BuildViewModel(new FakeTimetableSlots(), new FakeGroups([]), substitutions: substitutions);
var label = vm.WeekItems.Single(c => c.IsSupervisionRow && c.Text.Contains("n. 3."));
var mondayCell = vm.WeekItems.SkipWhile(c => c != label).Skip(1).First();
Assert.Equal("Vertretung für Fr. Schmidt", mondayCell.SupervisionLocation);
}
[Fact]
public void Load_Wochenraster_VertretungsstundeUeberschreibtNormaleAnzeige()
{
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 substitutions = new FakeSubstitutionEntries();
substitutions.Add(new SubstitutionEntry
{
Date = date, Kind = SubstitutionKind.Lesson, PeriodNumber = 1,
GroupLabel = "8a", Description = "Vertretung Erdkunde",
});
var vm = BuildViewModel(slots, new FakeGroups([group]), substitutions: substitutions);
var cell = vm.WeekItems.Single(c => c.IsSlotCell && c.Weekday == DayOfWeek.Monday && c.PeriodNumber == 1);
Assert.True(cell.IsSubstitutionLesson);
Assert.Equal("8a", cell.GroupName);
Assert.Equal("Vertretung Erdkunde", cell.Topic);
}
[Fact]
public void Load_Wochenraster_VertretungsstundeOhnePassendenSlot_ErscheintTrotzdem()
{
var date = DateInCurrentWeek(DayOfWeek.Monday);
var substitutions = new FakeSubstitutionEntries();
substitutions.Add(new SubstitutionEntry
{
Date = date, Kind = SubstitutionKind.Lesson, PeriodNumber = 5,
GroupLabel = "8a", Description = "Vertretung Erdkunde",
});
var vm = BuildViewModel(new FakeTimetableSlots(), new FakeGroups([]), substitutions: substitutions);
var cell = vm.WeekItems.Single(c => c.IsSlotCell && c.Weekday == DayOfWeek.Monday && c.PeriodNumber == 5);
Assert.True(cell.IsSubstitutionLesson);
Assert.True(cell.IsAssigned);
}
// ── Aufsicht + Vertretung in der Tagesliste ("Heute") ────────────────────
[Fact]
public void Load_Heute_ZeigtHeutigeAufsicht()
{
var today = DateOnly.FromDateTime(DateTime.Today);
var duties = new FakeSupervisionDuties();
duties.Add(new SupervisionDuty { Weekday = today.DayOfWeek, AfterPeriod = 2, Location = "Pausenhof" });
var vm = BuildViewModel(new FakeTimetableSlots(), new FakeGroups([]), supervisionDuties: duties);
var item = Assert.Single(vm.TodaySupervisions);
Assert.Equal("Pausenhof", item.Description);
Assert.False(item.IsSubstitution);
}
[Fact]
public void Load_Heute_VertretungsstundeErsetztNormaleStundenanzeige()
{
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 substitutions = new FakeSubstitutionEntries();
substitutions.Add(new SubstitutionEntry
{
Date = today, Kind = SubstitutionKind.Lesson, PeriodNumber = 1,
GroupLabel = "8a", Description = "Vertretung Erdkunde",
});
var vm = BuildViewModel(slots, new FakeGroups([group]), substitutions: substitutions);
var item = Assert.Single(vm.TodayItems);
Assert.True(item.IsSubstitution);
Assert.Equal("8a", item.GroupName);
Assert.Equal("Vertretung Erdkunde", item.LessonTopic);
}
[Fact]
public void OpenGroup_MitLeererGroupId_RuftDelegateNichtAuf()
{
var vm = BuildViewModel(new FakeTimetableSlots(), new FakeGroups([]));
var called = false;
vm.OnNavigateToGroup = _ => called = true;
vm.OpenGroupCommand.Execute(Guid.Empty);
Assert.False(called);
}
// ── Sondereinsätze (Ausflüge, Berufsmessen, ...) ─────────────────────────
[Fact]
public void Load_Wochenraster_GanztaegigerSondereinsatz_UeberdecktAllePeriodenDesTages()
{
var date = DateInCurrentWeek(DayOfWeek.Monday);
var substitutions = new FakeSubstitutionEntries();
substitutions.Add(new SubstitutionEntry
{
Date = date, Kind = SubstitutionKind.SpecialAssignment, IsAllDay = true,
GroupLabel = "8a", Description = "Ausflug ins Museum",
});
var vm = BuildViewModel(new FakeTimetableSlots(), new FakeGroups([]), substitutions: substitutions);
var period1 = vm.WeekItems.Single(c => c.IsSlotCell && c.Weekday == DayOfWeek.Monday && c.PeriodNumber == 1);
var period7 = vm.WeekItems.Single(c => c.IsSlotCell && c.Weekday == DayOfWeek.Monday && c.PeriodNumber == 7);
Assert.True(period1.IsSpecialAssignment);
Assert.True(period7.IsSpecialAssignment);
Assert.Equal("Ausflug ins Museum", period1.Topic);
Assert.Equal("8a", period1.GroupName);
}
[Fact]
public void Load_Wochenraster_SondereinsatzMitStundenbereich_NurDortSichtbar()
{
var date = DateInCurrentWeek(DayOfWeek.Monday);
var substitutions = new FakeSubstitutionEntries();
substitutions.Add(new SubstitutionEntry
{
Date = date, Kind = SubstitutionKind.SpecialAssignment, IsAllDay = false,
FromPeriod = 3, ToPeriod = 4, Description = "Berufsmesse",
});
var vm = BuildViewModel(new FakeTimetableSlots(), new FakeGroups([]), substitutions: substitutions);
var period2 = vm.WeekItems.Single(c => c.IsSlotCell && c.Weekday == DayOfWeek.Monday && c.PeriodNumber == 2);
var period3 = vm.WeekItems.Single(c => c.IsSlotCell && c.Weekday == DayOfWeek.Monday && c.PeriodNumber == 3);
var period4 = vm.WeekItems.Single(c => c.IsSlotCell && c.Weekday == DayOfWeek.Monday && c.PeriodNumber == 4);
var period5 = vm.WeekItems.Single(c => c.IsSlotCell && c.Weekday == DayOfWeek.Monday && c.PeriodNumber == 5);
Assert.False(period2.IsSpecialAssignment);
Assert.True(period3.IsSpecialAssignment);
Assert.True(period4.IsSpecialAssignment);
Assert.False(period5.IsSpecialAssignment);
}
[Fact]
public void Load_Wochenraster_SondereinsatzOhneGruppe_ZeigtLeeresGroupName()
{
var date = DateInCurrentWeek(DayOfWeek.Monday);
var substitutions = new FakeSubstitutionEntries();
substitutions.Add(new SubstitutionEntry
{
Date = date, Kind = SubstitutionKind.SpecialAssignment, IsAllDay = true, Description = "Berufsmesse",
});
var vm = BuildViewModel(new FakeTimetableSlots(), new FakeGroups([]), substitutions: substitutions);
var cell = vm.WeekItems.Single(c => c.IsSlotCell && c.Weekday == DayOfWeek.Monday && c.PeriodNumber == 1);
Assert.Equal("", cell.GroupName);
}
[Fact]
public void Load_Heute_ZeigtGanztaegigenSondereinsatz()
{
var today = DateOnly.FromDateTime(DateTime.Today);
var substitutions = new FakeSubstitutionEntries();
substitutions.Add(new SubstitutionEntry
{
Date = today, Kind = SubstitutionKind.SpecialAssignment, IsAllDay = true,
GroupLabel = "8a", Description = "Ausflug ins Museum",
});
var vm = BuildViewModel(new FakeTimetableSlots(), new FakeGroups([]), substitutions: substitutions);
var item = Assert.Single(vm.TodaySpecialAssignments);
Assert.Equal("Ganztägig", item.PeriodLabel);
Assert.Equal("Ausflug ins Museum", item.Description);
Assert.Equal("8a", item.GroupLabel);
}
[Fact]
public void Load_Heute_ZeigtSondereinsatzMitStundenbereich()
{
var today = DateOnly.FromDateTime(DateTime.Today);
var substitutions = new FakeSubstitutionEntries();
substitutions.Add(new SubstitutionEntry
{
Date = today, Kind = SubstitutionKind.SpecialAssignment, IsAllDay = false,
FromPeriod = 3, ToPeriod = 4, Description = "Berufsmesse",
});
var vm = BuildViewModel(new FakeTimetableSlots(), new FakeGroups([]), substitutions: substitutions);
var item = Assert.Single(vm.TodaySpecialAssignments);
Assert.Equal("3.4. Stunde", item.PeriodLabel);
}
// ── Stundenausfall ────────────────────────────────────────────────────────
[Fact]
public void Load_Wochenraster_Ausfall_ZeigtGruppeUndFachAusDemStundenplan()
{
var subject = new Subject { Name = "Naturwissenschaften", ShortName = "NAT" };
var group = new LearningGroup { Name = "6a", SubjectId = subject.Id };
var date = DateInCurrentWeek(DayOfWeek.Monday);
var slots = new FakeTimetableSlots();
slots.Add(new TimetableSlot { GroupId = group.Id, Weekday = DayOfWeek.Monday, PeriodNumber = 2 });
var substitutions = new FakeSubstitutionEntries();
substitutions.Add(new SubstitutionEntry
{
Date = date, Kind = SubstitutionKind.Cancelled, PeriodNumber = 2, Description = "6a auf Klassenfahrt",
});
var vm = BuildViewModel(slots, new FakeGroups([group]), subjects: new FakeSubjects([subject]), substitutions: substitutions);
var cell = vm.WeekItems.Single(c => c.IsSlotCell && c.Weekday == DayOfWeek.Monday && c.PeriodNumber == 2);
Assert.True(cell.IsCancelled);
Assert.Equal("NAT", cell.SubjectLabel);
Assert.Equal("6a", cell.GroupName);
Assert.Equal("6a auf Klassenfahrt", cell.Topic);
}
[Fact]
public void Load_Wochenraster_KeinAusfall_ZeigtNormaleStunde()
{
var group = new LearningGroup { Name = "6a" };
var slots = new FakeTimetableSlots();
slots.Add(new TimetableSlot { GroupId = group.Id, Weekday = DayOfWeek.Monday, PeriodNumber = 2 });
var vm = BuildViewModel(slots, new FakeGroups([group]));
var cell = vm.WeekItems.Single(c => c.IsSlotCell && c.Weekday == DayOfWeek.Monday && c.PeriodNumber == 2);
Assert.False(cell.IsCancelled);
}
[Fact]
public void Load_Heute_Ausfall_ErsetztNormaleStundenanzeige()
{
var today = DateOnly.FromDateTime(DateTime.Today);
var group = new LearningGroup { Name = "6a" };
var slots = new FakeTimetableSlots();
slots.Add(new TimetableSlot { GroupId = group.Id, Weekday = today.DayOfWeek, PeriodNumber = 2 });
var substitutions = new FakeSubstitutionEntries();
substitutions.Add(new SubstitutionEntry
{
Date = today, Kind = SubstitutionKind.Cancelled, PeriodNumber = 2, Description = "6a auf Klassenfahrt",
});
var vm = BuildViewModel(slots, new FakeGroups([group]), substitutions: substitutions);
var item = Assert.Single(vm.TodayItems);
Assert.True(item.IsCancelled);
Assert.Equal("6a", item.GroupName);
Assert.Equal("6a auf Klassenfahrt", item.LessonTopic);
Assert.Equal(group.Id, item.GroupId);
}
[Fact]
public void Load_Heute_AusfallOhneGrund_LessonTopicBleibtLeer()
{
var today = DateOnly.FromDateTime(DateTime.Today);
var group = new LearningGroup { Name = "6a" };
var slots = new FakeTimetableSlots();
slots.Add(new TimetableSlot { GroupId = group.Id, Weekday = today.DayOfWeek, PeriodNumber = 2 });
var substitutions = new FakeSubstitutionEntries();
substitutions.Add(new SubstitutionEntry { Date = today, Kind = SubstitutionKind.Cancelled, PeriodNumber = 2 });
var vm = BuildViewModel(slots, new FakeGroups([group]), substitutions: substitutions);
var item = Assert.Single(vm.TodayItems);
Assert.False(item.HasLessonTopic);
}
}