using LehrerApp.Core.Interfaces; using LehrerApp.Core.Models; using LehrerApp.Core.Services; using LehrerApp.Desktop.ViewModels.Workload; using Xunit; namespace LehrerApp.Desktop.Tests; public sealed class WorkTaskListViewModelTests { [Fact] public void Load_BlendetErledigteAufgabenStandardmaessigAus() { var tasks = new FakeWorkTasks(); tasks.Add(new WorkTask { Title = "Offen", Status = WorkTaskStatus.Open }); tasks.Add(new WorkTask { Title = "Erledigt", Status = WorkTaskStatus.Done }); var vm = new WorkTaskListViewModel(tasks, new FakeGroups([]), new FakeTimeEntries()); Assert.Single(vm.Tasks); Assert.Equal("Offen", vm.Tasks[0].Title); } [Fact] public void StatusFilter_Alle_ZeigtAuchErledigteAufgaben() { var tasks = new FakeWorkTasks(); tasks.Add(new WorkTask { Title = "Erledigt", Status = WorkTaskStatus.Done }); var vm = new WorkTaskListViewModel(tasks, new FakeGroups([]), new FakeTimeEntries()) { StatusFilter = WorkTaskListViewModel.AllFilter, }; Assert.Single(vm.Tasks); } [Fact] public void CategoryFilter_FiltertNachKategorie() { var tasks = new FakeWorkTasks(); tasks.Add(new WorkTask { Title = "Korrektur", Category = TaskCategory.Correction }); tasks.Add(new WorkTask { Title = "Vorbereitung", Category = TaskCategory.Preparation }); var vm = new WorkTaskListViewModel(tasks, new FakeGroups([]), new FakeTimeEntries()) { CategoryFilter = TaskCategoryDisplay.Label(TaskCategory.Correction), }; Assert.Single(vm.Tasks); Assert.Equal("Korrektur", vm.Tasks[0].Title); } [Fact] public void GroupFilter_OhneGruppe_ZeigtNurAufgabenOhneGroupId() { var group = new LearningGroup { Name = "8a" }; var tasks = new FakeWorkTasks(); tasks.Add(new WorkTask { Title = "Mit Gruppe", GroupId = group.Id }); tasks.Add(new WorkTask { Title = "Ohne Gruppe", GroupId = null }); var vm = new WorkTaskListViewModel(tasks, new FakeGroups([group]), new FakeTimeEntries()) { GroupFilter = WorkTaskListViewModel.NoGroupFilter, }; Assert.Single(vm.Tasks); Assert.Equal("Ohne Gruppe", vm.Tasks[0].Title); } [Fact] public void CycleStatus_WechseltVonOffenNachInBearbeitung() { var tasks = new FakeWorkTasks(); tasks.Add(new WorkTask { Title = "T", Status = WorkTaskStatus.Open }); var vm = new WorkTaskListViewModel(tasks, new FakeGroups([]), new FakeTimeEntries()) { StatusFilter = WorkTaskListViewModel.AllFilter, }; vm.CycleStatusCommand.Execute(vm.Tasks[0]); Assert.Equal(WorkTaskStatus.InProgress, vm.Tasks[0].Model.Status); } [Fact] public void CycleStatus_VonErledigtSpringtZurueckZuOffen() { var tasks = new FakeWorkTasks(); tasks.Add(new WorkTask { Title = "T", Status = WorkTaskStatus.Done }); var vm = new WorkTaskListViewModel(tasks, new FakeGroups([]), new FakeTimeEntries()) { StatusFilter = WorkTaskListViewModel.AllFilter, }; vm.CycleStatusCommand.Execute(vm.Tasks[0]); Assert.Equal(WorkTaskStatus.Open, vm.Tasks[0].Model.Status); } [Fact] public void CycleStatus_WoechentlicheWiederholungAufErledigt_ErzeugtNaechsteInstanz() { var tasks = new FakeWorkTasks(); tasks.Add(new WorkTask { Title = "Wochenbericht", Status = WorkTaskStatus.InProgress, Recurrence = TaskRecurrence.Weekly, DueDate = new DateOnly(2026, 3, 10), Category = TaskCategory.Admin, }); var vm = new WorkTaskListViewModel(tasks, new FakeGroups([]), new FakeTimeEntries()) { StatusFilter = WorkTaskListViewModel.AllFilter, }; vm.CycleStatusCommand.Execute(vm.Tasks[0]); // -> Done var all = tasks.GetAll(); Assert.Equal(2, all.Count); var next = all.Single(t => t.Status == WorkTaskStatus.Open); Assert.Equal("Wochenbericht", next.Title); Assert.Equal(new DateOnly(2026, 3, 17), next.DueDate); Assert.Equal(TaskRecurrence.Weekly, next.Recurrence); } [Fact] public void CycleStatus_MonatlicheWiederholung_AddiertEinenMonat() { var tasks = new FakeWorkTasks(); tasks.Add(new WorkTask { Title = "Bericht", Status = WorkTaskStatus.InProgress, Recurrence = TaskRecurrence.Monthly, DueDate = new DateOnly(2026, 3, 10), }); var vm = new WorkTaskListViewModel(tasks, new FakeGroups([]), new FakeTimeEntries()) { StatusFilter = WorkTaskListViewModel.AllFilter, }; vm.CycleStatusCommand.Execute(vm.Tasks[0]); // -> Done var next = tasks.GetAll().Single(t => t.Status == WorkTaskStatus.Open); Assert.Equal(new DateOnly(2026, 4, 10), next.DueDate); } [Fact] public void CycleStatus_OhneWiederholung_ErzeugtKeineNeueAufgabe() { var tasks = new FakeWorkTasks(); tasks.Add(new WorkTask { Title = "T", Status = WorkTaskStatus.InProgress }); var vm = new WorkTaskListViewModel(tasks, new FakeGroups([]), new FakeTimeEntries()) { StatusFilter = WorkTaskListViewModel.AllFilter, }; vm.CycleStatusCommand.Execute(vm.Tasks[0]); // -> Done Assert.Single(tasks.GetAll()); } [Fact] public void DeleteTask_EntferntAufgabeAusDerListe() { var tasks = new FakeWorkTasks(); tasks.Add(new WorkTask { Title = "T" }); var vm = new WorkTaskListViewModel(tasks, new FakeGroups([]), new FakeTimeEntries()); vm.DeleteTaskCommand.Execute(vm.Tasks[0]); Assert.Empty(vm.Tasks); } [Fact] public async Task AddTask_RuftOnEditTaskAufUndSpeichertErgebnis() { var tasks = new FakeWorkTasks(); var vm = new WorkTaskListViewModel(tasks, new FakeGroups([]), new FakeTimeEntries()); vm.OnEditTask = (_, _) => Task.FromResult(new WorkTask { Title = "Neu" }); await vm.AddTaskCommand.ExecuteAsync(null); Assert.Single(vm.Tasks); Assert.Equal("Neu", vm.Tasks[0].Title); } [Fact] public async Task AddReminder_OeffnetDialogVorbelegtAlsErinnerungUndSpeichertErgebnis() { var tasks = new FakeWorkTasks(); var vm = new WorkTaskListViewModel(tasks, new FakeGroups([]), new FakeTimeEntries()); bool? passedStartAsReminder = null; vm.OnEditTask = (_, startAsReminder) => { passedStartAsReminder = startAsReminder; return Task.FromResult(new WorkTask { Title = "Ansage machen", Kind = TaskKind.Reminder }); }; await vm.AddReminderCommand.ExecuteAsync(null); Assert.True(passedStartAsReminder); var item = Assert.Single(vm.Tasks); Assert.True(item.IsReminder); Assert.Equal("Erinnerung", item.CategoryLabel); } [Fact] public void Tasks_SindNachFaelligkeitSortiertUnbefristeteAmEnde() { var tasks = new FakeWorkTasks(); tasks.Add(new WorkTask { Title = "Ohne Frist", DueDate = null }); tasks.Add(new WorkTask { Title = "Spät fällig", DueDate = DateOnly.FromDateTime(DateTime.Today.AddDays(10)) }); tasks.Add(new WorkTask { Title = "Bald fällig", DueDate = DateOnly.FromDateTime(DateTime.Today.AddDays(1)) }); var vm = new WorkTaskListViewModel(tasks, new FakeGroups([]), new FakeTimeEntries()); Assert.Equal(["Bald fällig", "Spät fällig", "Ohne Frist"], vm.Tasks.Select(t => t.Title)); } } public sealed class AddEditWorkTaskDialogViewModelTests { [Fact] public void Save_OhneTitel_SetztFehlerUndErzeugtKeinResult() { var vm = new AddEditWorkTaskDialogViewModel(null, []); vm.SaveCommand.Execute(null); Assert.Null(vm.Result); Assert.NotEmpty(vm.TitleError); } [Fact] public void Save_MitGueltigenDaten_ErzeugtWorkTask() { var group = new LearningGroup { Name = "8a" }; var vm = new AddEditWorkTaskDialogViewModel(null, [group]) { Title = "Klausur korrigieren", SelectedCategory = TaskCategoryDisplay.Label(TaskCategory.Correction), SelectedGroup = group, DueDateText = "24.12.2026", EstimatedMinutesText = "90", }; vm.SaveCommand.Execute(null); Assert.NotNull(vm.Result); Assert.Equal("Klausur korrigieren", vm.Result!.Title); Assert.Equal(TaskCategory.Correction, vm.Result.Category); Assert.Equal(group.Id, vm.Result.GroupId); Assert.Equal(new DateOnly(2026, 12, 24), vm.Result.DueDate); Assert.Equal(90, vm.Result.EstimatedMinutes); } [Fact] public void Save_MitUngueltigemDatum_SetztFehler() { var vm = new AddEditWorkTaskDialogViewModel(null, []) { Title = "T", DueDateText = "nicht-ein-datum", }; vm.SaveCommand.Execute(null); Assert.Null(vm.Result); Assert.NotEmpty(vm.DueDateError); } [Fact] public void Save_BestehendeAufgabe_BehaeltIdUndStatus() { var source = new WorkTask { Title = "Alt", Status = WorkTaskStatus.InProgress }; var vm = new AddEditWorkTaskDialogViewModel(source, []) { Title = "Neu" }; vm.SaveCommand.Execute(null); Assert.Equal(source.Id, vm.Result!.Id); Assert.Equal(WorkTaskStatus.InProgress, vm.Result.Status); } [Fact] public void Save_WiederholungOhneFaelligkeitsdatum_SetztFehler() { var vm = new AddEditWorkTaskDialogViewModel(null, []) { Title = "T", SelectedRecurrence = TaskRecurrenceDisplay.Label(TaskRecurrence.Weekly), }; vm.SaveCommand.Execute(null); Assert.Null(vm.Result); Assert.NotEmpty(vm.DueDateError); } [Fact] public void Save_WiederholungMitFaelligkeitsdatum_UebernimmtRecurrence() { var vm = new AddEditWorkTaskDialogViewModel(null, []) { Title = "T", DueDateText = "10.03.2026", SelectedRecurrence = TaskRecurrenceDisplay.Label(TaskRecurrence.Monthly), }; vm.SaveCommand.Execute(null); Assert.NotNull(vm.Result); Assert.Equal(TaskRecurrence.Monthly, vm.Result!.Recurrence); } [Fact] public void Save_AlsErinnerung_SetztKindUndVerwirftGeschaetzteDauer() { var vm = new AddEditWorkTaskDialogViewModel(null, []) { Title = "Ansage an die Klasse machen", IsReminder = true, EstimatedMinutesText = "90", // sollte ignoriert werden }; vm.SaveCommand.Execute(null); Assert.NotNull(vm.Result); Assert.Equal(TaskKind.Reminder, vm.Result!.Kind); Assert.Null(vm.Result.EstimatedMinutes); } [Fact] public void Konstruktor_StartAsReminder_SetztIsReminderVorbelegt() { var vm = new AddEditWorkTaskDialogViewModel(null, [], startAsReminder: true); Assert.True(vm.IsReminder); } [Fact] public void Konstruktor_BestehendeErinnerung_ErkenntKindWieder() { var source = new WorkTask { Title = "Alt", Kind = TaskKind.Reminder }; var vm = new AddEditWorkTaskDialogViewModel(source, []); Assert.True(vm.IsReminder); } [Fact] public void Save_MitPrioritaet_UebernimmtSieInsResult() { var vm = new AddEditWorkTaskDialogViewModel(null, []) { Title = "Wichtig", SelectedPriority = TaskPriorityDisplay.Label(TaskPriority.High), }; vm.SaveCommand.Execute(null); Assert.Equal(TaskPriority.High, vm.Result!.Priority); } [Fact] public void Konstruktor_OhneQuelle_StartetMitNormalerPrioritaet() { var vm = new AddEditWorkTaskDialogViewModel(null, []); Assert.Equal(TaskPriorityDisplay.Label(TaskPriority.Normal), vm.SelectedPriority); } [Fact] public void ChecklistItems_HinzufuegenUndEntfernen_WirdInResultUebernommen() { var vm = new AddEditWorkTaskDialogViewModel(null, []) { Title = "Material einsammeln" }; vm.NewChecklistItemText = "Erste Position"; vm.AddChecklistItemCommand.Execute(null); vm.NewChecklistItemText = "Zweite Position"; vm.AddChecklistItemCommand.Execute(null); Assert.Equal(["Erste Position", "Zweite Position"], vm.ChecklistItems.Select(c => c.Label)); Assert.Equal("", vm.NewChecklistItemText); vm.RemoveChecklistItemCommand.Execute(vm.ChecklistItems[0]); vm.SaveCommand.Execute(null); var item = Assert.Single(vm.Result!.ChecklistItems); Assert.Equal("Zweite Position", item.Label); Assert.Null(item.StudentId); } [Fact] public void Konstruktor_BestehendeAufgabeMitChecklist_UebernimmtEintraege() { var source = new WorkTask { Title = "Alt", ChecklistItems = [new ChecklistItem { Label = "Schon da", IsDone = true }], }; var vm = new AddEditWorkTaskDialogViewModel(source, []); var row = Assert.Single(vm.ChecklistItems); Assert.Equal("Schon da", row.Label); Assert.True(row.IsDone); } [Fact] public void FillFromRoster_OhneGruppeOderLader_MachtNichts() { var vm = new AddEditWorkTaskDialogViewModel(null, []); vm.FillFromRosterCommand.Execute(null); Assert.Empty(vm.ChecklistItems); } [Fact] public void FillFromRoster_BefuelltAusRosterUndUeberspringtDoppelteSchueler() { var group = new LearningGroup { Name = "8a" }; var studentId = Guid.NewGuid(); var otherStudentId = Guid.NewGuid(); var vm = new AddEditWorkTaskDialogViewModel(null, [group], loadRoster: _ => [(studentId, "Anna Beispiel"), (otherStudentId, "Ben Muster")]) { SelectedGroup = group, }; Assert.True(vm.CanFillFromRoster); vm.FillFromRosterCommand.Execute(null); Assert.Equal(2, vm.ChecklistItems.Count); vm.FillFromRosterCommand.Execute(null); Assert.Equal(2, vm.ChecklistItems.Count); // kein Duplikat beim zweiten Aufruf } } public sealed class TimeTrackingViewModelTests { private static TimeTrackingViewModel BuildVm(ITimeEntryRepository entries, IWorkTaskRepository tasks, FakeTimetableSlots? slots = null) => new(entries, tasks, slots ?? new FakeTimetableSlots(), TestSupport.BuildPeriodScheduleService()); [Fact] public void StartStopTimer_SpeichertZeiteintragMitMindestensEinerMinute() { var entries = new FakeTimeEntries(); var vm = BuildVm(entries, new FakeWorkTasks()); vm.SelectedTimerCategory = TaskCategoryDisplay.Label(TaskCategory.Correction); vm.StartTimerCommand.Execute(null); Assert.True(vm.IsTimerRunning); vm.StopTimerCommand.Execute(null); Assert.False(vm.IsTimerRunning); var saved = Assert.Single(entries.GetByDate(DateOnly.FromDateTime(DateTime.Today))); Assert.True(saved.DurationMinutes >= 1); Assert.Equal(TaskCategoryDisplay.Label(TaskCategory.Correction), saved.Category); } [Fact] public void SelectedTimerTask_UebernimmtDessenKategorie() { var task = new WorkTask { Title = "T", Category = TaskCategory.Meeting }; var tasks = new FakeWorkTasks(); tasks.Add(task); var vm = BuildVm(new FakeTimeEntries(), tasks); vm.SelectedTimerTask = task; Assert.Equal(TaskCategoryDisplay.Label(TaskCategory.Meeting), vm.SelectedTimerCategory); } [Fact] public void Load_ZeigtNurEintraegeDerAktuellenWoche() { var entries = new FakeTimeEntries(); entries.Add(new TimeEntry { Category = "X", Date = DateOnly.FromDateTime(DateTime.Today), DurationMinutes = 30 }); entries.Add(new TimeEntry { Category = "X", Date = DateOnly.FromDateTime(DateTime.Today).AddDays(-30), DurationMinutes = 45 }); var vm = BuildVm(entries, new FakeWorkTasks()); Assert.Single(vm.WeekEntries); Assert.Equal(30, vm.WeekEntries[0].Model.DurationMinutes); } [Fact] public void CategorySummaries_GruppiertUndSummiertProKategorie() { var today = DateOnly.FromDateTime(DateTime.Today); var entries = new FakeTimeEntries(); entries.Add(new TimeEntry { Category = "Korrektur", Date = today, DurationMinutes = 30 }); entries.Add(new TimeEntry { Category = "Korrektur", Date = today, DurationMinutes = 20 }); entries.Add(new TimeEntry { Category = "Verwaltung", Date = today, DurationMinutes = 10 }); var vm = BuildVm(entries, new FakeWorkTasks()); var korrektur = vm.CategorySummaries.Single(s => s.Category == "Korrektur"); Assert.Equal("50 min", korrektur.MinutesDisplay); Assert.Equal(1.0, korrektur.BarFraction); Assert.Equal("60 min diese Woche", vm.TotalWeekMinutesDisplay); } [Fact] public void DeleteEntry_EntferntEintragAusDerWoche() { var entries = new FakeTimeEntries(); entries.Add(new TimeEntry { Category = "X", Date = DateOnly.FromDateTime(DateTime.Today), DurationMinutes = 30 }); var vm = BuildVm(entries, new FakeWorkTasks()); vm.DeleteEntryCommand.Execute(vm.WeekEntries[0]); Assert.Empty(vm.WeekEntries); } [Fact] public async Task AddEntry_RuftOnAddEntryAufUndSpeichertErgebnis() { var entries = new FakeTimeEntries(); var vm = BuildVm(entries, new FakeWorkTasks()); vm.OnAddEntry = () => Task.FromResult( new TimeEntry { Category = "X", Date = DateOnly.FromDateTime(DateTime.Today), DurationMinutes = 15 }); await vm.AddEntryCommand.ExecuteAsync(null); Assert.Single(vm.WeekEntries); } // ── Unterrichtszeit-Vorschlag (Nutzer-Feedback: Zeiterfassung soll bei der Unterrichtszeit // nachfragen statt sie komplett automatisch zu erfassen) ──────────────────────────────────── [Fact] public void HasTeachingTimeSuggestionToday_OhneStundenplanHeute_IstFalse() { var vm = BuildVm(new FakeTimeEntries(), new FakeWorkTasks()); // leere FakeTimetableSlots Assert.False(vm.HasTeachingTimeSuggestionToday); } [Fact] public async Task SuggestTeachingTime_SchlaegtBeginnMitPufferVorErsterUndNachLetzterPeriodeVor() { var today = DateTime.Today.DayOfWeek; var slots = new FakeTimetableSlots(); slots.Add(new TimetableSlot { Weekday = today, PeriodNumber = 1 }); slots.Add(new TimetableSlot { Weekday = today, PeriodNumber = 2 }); var periodSchedule = TestSupport.BuildPeriodScheduleService(); periodSchedule.SetPeriods([ new PeriodTimeEntry { PeriodNumber = 1, Start = new TimeOnly(7, 50), End = new TimeOnly(8, 35) }, new PeriodTimeEntry { PeriodNumber = 2, Start = new TimeOnly(8, 35), End = new TimeOnly(9, 20) }, ]); var entries = new FakeTimeEntries(); var vm = new TimeTrackingViewModel(entries, new FakeWorkTasks(), slots, periodSchedule); Assert.True(vm.HasTeachingTimeSuggestionToday); (TimeOnly Start, TimeOnly End)? suggested = null; vm.OnSuggestTeachingTime = (start, end) => { suggested = (start, end); return Task.FromResult(new TimeEntry { Category = "Unterricht", Date = DateOnly.FromDateTime(DateTime.Today), StartTime = start, EndTime = end, DurationMinutes = (int)(end - start).TotalMinutes, }); }; await vm.SuggestTeachingTimeCommand.ExecuteAsync(null); Assert.Equal(new TimeOnly(7, 35), suggested!.Value.Start); // 7:50 - 15 Min Puffer Assert.Equal(new TimeOnly(9, 30), suggested.Value.End); // 9:20 + 10 Min Puffer Assert.Single(entries.GetByDate(DateOnly.FromDateTime(DateTime.Today))); } [Fact] public async Task SuggestTeachingTime_OhneStundenplanHeute_TutNichts() { var vm = BuildVm(new FakeTimeEntries(), new FakeWorkTasks()); var called = false; vm.OnSuggestTeachingTime = (_, _) => { called = true; return Task.FromResult(null); }; await vm.SuggestTeachingTimeCommand.ExecuteAsync(null); Assert.False(called); } [Fact] public void MissingTeachingTime_VergangenerUnterrichtstag_WirdInZeiterfassungAngezeigt() { var pastDay = DateOnly.FromDateTime(DateTime.Today).AddDays(-1); var slots = new FakeTimetableSlots(); slots.Add(new TimetableSlot { Weekday = pastDay.DayOfWeek, PeriodNumber = 1 }); var periodSchedule = TestSupport.BuildPeriodScheduleService(); periodSchedule.SetPeriods([new PeriodTimeEntry { PeriodNumber = 1, Start = new TimeOnly(8, 0), End = new TimeOnly(8, 45) }]); var vm = new TimeTrackingViewModel( new FakeTimeEntries(), new FakeWorkTasks(), slots, periodSchedule); var gap = Assert.Single(vm.MissingTeachingTimeEntries, i => i.Date == pastDay); Assert.Equal(new TimeOnly(7, 45), gap.WindowStart); Assert.Equal(new TimeOnly(8, 55), gap.WindowEnd); } [Fact] public async Task AddMissingTeachingTime_SpeichertVorgefuelltenTagUndEntferntIhnAusOffenerListe() { var pastDay = DateOnly.FromDateTime(DateTime.Today).AddDays(-1); var slots = new FakeTimetableSlots(); slots.Add(new TimetableSlot { Weekday = pastDay.DayOfWeek, PeriodNumber = 1 }); var periodSchedule = TestSupport.BuildPeriodScheduleService(); periodSchedule.SetPeriods([new PeriodTimeEntry { PeriodNumber = 1, Start = new TimeOnly(8, 0), End = new TimeOnly(8, 45) }]); var entries = new FakeTimeEntries(); var vm = new TimeTrackingViewModel(entries, new FakeWorkTasks(), slots, periodSchedule); var gap = Assert.Single(vm.MissingTeachingTimeEntries, i => i.Date == pastDay); vm.OnAddMissingTeachingTime = item => Task.FromResult(new TimeEntry { Date = item.Date, Category = TaskCategoryDisplay.Label(TaskCategory.Teaching), StartTime = item.WindowStart, EndTime = item.WindowEnd, DurationMinutes = (int)(item.WindowEnd - item.WindowStart).TotalMinutes, }); await vm.AddMissingTeachingTimeCommand.ExecuteAsync(gap); Assert.Single(entries.GetByDate(pastDay)); Assert.DoesNotContain(vm.MissingTeachingTimeEntries, i => i.Date == pastDay); } } public sealed class AddTimeEntryDialogViewModelTests { [Fact] public void Save_MitDauer_ErzeugtEintrag() { var vm = new AddTimeEntryDialogViewModel([]) { DateText = "10.01.2026", DurationText = "45", }; vm.SaveCommand.Execute(null); Assert.NotNull(vm.Result); Assert.Equal(45, vm.Result!.DurationMinutes); Assert.Equal(new DateOnly(2026, 1, 10), vm.Result.Date); } [Fact] public void Save_MitVonBis_BerechnetDauer() { var vm = new AddTimeEntryDialogViewModel([]) { DateText = "10.01.2026", StartTimeText = "14:00", EndTimeText = "14:30", }; vm.SaveCommand.Execute(null); Assert.NotNull(vm.Result); Assert.Equal(30, vm.Result!.DurationMinutes); } [Fact] public void Save_OhneDauerUndOhneZeiten_SetztFehler() { var vm = new AddTimeEntryDialogViewModel([]) { DateText = "10.01.2026" }; vm.SaveCommand.Execute(null); Assert.Null(vm.Result); Assert.NotEmpty(vm.TimeError); } [Fact] public void Save_EndeVorBeginn_SetztFehler() { var vm = new AddTimeEntryDialogViewModel([]) { DateText = "10.01.2026", StartTimeText = "14:30", EndTimeText = "14:00", }; vm.SaveCommand.Execute(null); Assert.Null(vm.Result); Assert.NotEmpty(vm.TimeError); } } public sealed class WorkloadEvaluationViewModelTests { private static WorkloadSettingsService BuildSettingsService() { var tempPath = System.IO.Path.Combine( System.IO.Path.GetTempPath(), $"lehrerapp-workloadsettings-tests-{Guid.NewGuid():N}"); Directory.CreateDirectory(tempPath); return new WorkloadSettingsService(tempPath); } [Fact] public void MonatModus_ZeigtNurEintraegeDesGewaehltenMonats() { var entries = new FakeTimeEntries(); entries.Add(new TimeEntry { Category = "Korrektur", Date = new DateOnly(2026, 3, 10), DurationMinutes = 30 }); entries.Add(new TimeEntry { Category = "Korrektur", Date = new DateOnly(2026, 4, 1), DurationMinutes = 45 }); var vm = new WorkloadEvaluationViewModel(entries, new FakeGroups([]), BuildSettingsService(), new SchoolYearService()) { SelectedYear = 2026, }; vm.SelectedMonth = vm.MonthOptions[2]; // März Assert.Equal("30 min (0.5 h)", vm.TotalMinutesDisplay); } [Fact] public void CategorySummaries_GruppiertProKategorie() { var entries = new FakeTimeEntries(); entries.Add(new TimeEntry { Category = "Korrektur", Date = new DateOnly(2026, 3, 5), DurationMinutes = 30 }); entries.Add(new TimeEntry { Category = "Verwaltung", Date = new DateOnly(2026, 3, 6), DurationMinutes = 20 }); var vm = new WorkloadEvaluationViewModel(entries, new FakeGroups([]), BuildSettingsService(), new SchoolYearService()) { SelectedYear = 2026, }; vm.SelectedMonth = vm.MonthOptions[2]; Assert.Equal(2, vm.CategorySummaries.Count); Assert.Contains(vm.CategorySummaries, s => s.Category == "Korrektur" && s.MinutesDisplay == "30 min"); } [Fact] public void GroupSummaries_ZeigtNurEintraegeMitGruppe() { var group = new LearningGroup { Name = "8a" }; var entries = new FakeTimeEntries(); entries.Add(new TimeEntry { Category = "X", GroupId = group.Id, Date = new DateOnly(2026, 3, 5), DurationMinutes = 30 }); entries.Add(new TimeEntry { Category = "X", GroupId = null, Date = new DateOnly(2026, 3, 6), DurationMinutes = 20 }); var vm = new WorkloadEvaluationViewModel(entries, new FakeGroups([group]), BuildSettingsService(), new SchoolYearService()) { SelectedYear = 2026, }; vm.SelectedMonth = vm.MonthOptions[2]; var summary = Assert.Single(vm.GroupSummaries); Assert.Equal("8a", summary.GroupName); Assert.Equal("30 min", summary.MinutesDisplay); } [Fact] public void SchuljahrModus_UmfasstAugustBisJuli() { var entries = new FakeTimeEntries(); entries.Add(new TimeEntry { Category = "X", Date = new DateOnly(2025, 9, 1), DurationMinutes = 30 }); // im Schuljahr 2025/26 entries.Add(new TimeEntry { Category = "X", Date = new DateOnly(2026, 7, 30), DurationMinutes = 15 }); // im Schuljahr 2025/26 entries.Add(new TimeEntry { Category = "X", Date = new DateOnly(2026, 8, 5), DurationMinutes = 99 }); // schon Schuljahr 2026/27 var vm = new WorkloadEvaluationViewModel(entries, new FakeGroups([]), BuildSettingsService(), new SchoolYearService()) { SelectedYear = 2025, PeriodMode = WorkloadEvaluationViewModel.SchoolYearMode, }; Assert.Equal("45 min (0.8 h)", vm.TotalMinutesDisplay); } [Fact] public void SaveRequiredWeeklyHours_AktualisiertSollIstVergleich() { var entries = new FakeTimeEntries(); entries.Add(new TimeEntry { Category = "X", Date = new DateOnly(2026, 3, 2), DurationMinutes = 60 }); var vm = new WorkloadEvaluationViewModel(entries, new FakeGroups([]), BuildSettingsService(), new SchoolYearService()) { SelectedYear = 2026, RequiredWeeklyHoursText = "10", }; vm.SelectedMonth = vm.MonthOptions[2]; vm.SaveRequiredWeeklyHoursCommand.Execute(null); Assert.Contains("Soll:", vm.RequiredVsActualDisplay); Assert.Contains("Ist: 1 h", vm.RequiredVsActualDisplay); } [Fact] public void ExportCsv_EnthaeltZeitraumSummenUndKorrektMaskierteAufschluesselung() { var group = new LearningGroup { Name = "8a \"Chemie\"" }; var entries = new FakeTimeEntries(); entries.Add(new TimeEntry { Category = "Korrektur; schriftlich", GroupId = group.Id, Date = new DateOnly(2026, 3, 5), DurationMinutes = 90, }); var vm = new WorkloadEvaluationViewModel(entries, new FakeGroups([group]), BuildSettingsService(), new SchoolYearService()) { SelectedYear = 2026, }; vm.SelectedMonth = vm.MonthOptions[2]; var csv = vm.ExportCsv(); Assert.Contains("Zeitraum;01.03.2026;31.03.2026", csv); Assert.Contains("Gesamtzeit (Minuten);90", csv); Assert.Contains("\"Korrektur; schriftlich\";90;1,5", csv); Assert.Contains("\"8a \"\"Chemie\"\"\";90;1,5", csv); Assert.Equal("Arbeitszeitauswertung_2026-03.csv", vm.ExportSuggestedFileName); } [Fact] public void ExportCsv_MitPflichtstunden_EnthaeltSollzeitUndAbweichung() { var entries = new FakeTimeEntries(); entries.Add(new TimeEntry { Category = "Unterricht", Date = new DateOnly(2026, 3, 2), DurationMinutes = 60 }); var vm = new WorkloadEvaluationViewModel(entries, new FakeGroups([]), BuildSettingsService(), new SchoolYearService()) { SelectedYear = 2026, RequiredWeeklyHoursText = "10", }; vm.SelectedMonth = vm.MonthOptions[2]; vm.SaveRequiredWeeklyHoursCommand.Execute(null); var csv = vm.ExportCsv(); Assert.Contains("Pflichtstunden pro Woche;10", csv); Assert.Contains("Sollzeit (Stunden);", csv); Assert.Contains("Abweichung (Stunden);", csv); } }