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:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user