Kurs-Übersicht: neue Karte "Fehlende Hausaufgaben" listet Schüler mit nicht gemachter Hausaufgabe aus der letzten Mitarbeitssitzung. Aufgabenverwaltung: WorkTask.Kind unterscheidet jetzt Arbeitsaufträge von reinen Erinnerungen ohne Zeitbezug (z.B. "morgen Ansage an die Klasse machen") - eigener Button "🔔 Erinnerung", eigenes Icon in Liste/Dashboard, läuft nicht in die Zeitauswertung ein. Drei Bugfixes aus Nutzer-Feedback: - Dashboard-Kalender zeigte eine Stunde doppelt, wenn sie über "Sitzung erzeugen" mit einer Mitarbeitssitzung verknüpft war. - Der Sitzplan-Tab legte beim bloßen Öffnen einer Gruppe (nicht erst bei echter Nutzung) eine leere "Sitzplan"-Sitzung für heute an. - Der "Meine Klasse"-Ring im Kalender erschien erst, sobald für den Tag eine Lesson existierte, statt schon laut Stundenplan. Außerdem: Drawer-Icons pinnen jetzt explizit auf die farbige Emoji-Schrift (Windows kann sonst je nach Font-Fallback auf eine einfarbige Variante ausweichen). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
702 lines
25 KiB
C#
702 lines
25 KiB
C#
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<WorkTask?>(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<WorkTask?>(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);
|
|
}
|
|
}
|
|
|
|
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<TimeEntry?>(
|
|
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<TimeEntry?>(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<TimeEntry?>(null); };
|
|
|
|
await vm.SuggestTeachingTimeCommand.ExecuteAsync(null);
|
|
|
|
Assert.False(called);
|
|
}
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|