Kurs-Übersicht: fehlende Hausaufgaben; Erinnerungen als eigene Aufgabenart; Kalender-/Sitzplan-Bugfixes
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>
This commit is contained in:
@@ -6,8 +6,9 @@ using Xunit;
|
||||
namespace LehrerApp.Desktop.Tests;
|
||||
|
||||
/// Tests für 9.1 (heutige Stunden mit Uhrzeit und Raum im Dashboard) und 9.2 (Absprung von einer
|
||||
/// Stunde). Deckt nur die neue Zeit-/Raum-Auflösung ab, nicht die übrigen, unveränderten
|
||||
/// Dashboard-Bausteine (Kalender, offene Aufgaben, Fehlzeiten-Warnungen, ...).
|
||||
/// Stunde), außerdem die Kalender-Sitzungsanzeige (3.3.4) und die Mindeststichprobe der
|
||||
/// Fehlzeiten-Warnung (5.2.3). Deckt nicht die übrigen, unveränderten Dashboard-Bausteine
|
||||
/// (offene Aufgaben, ...).
|
||||
public sealed class DashboardViewModelTests
|
||||
{
|
||||
private static PeriodScheduleService NewPeriodSchedule()
|
||||
@@ -41,7 +42,7 @@ public sealed class DashboardViewModelTests
|
||||
FakeWorkTasks? tasks = null, FakeStudents? students = null, FakeDocumentation? documentation = null,
|
||||
DashboardSettingsService? dashboardSettings = null, FakeSchoolHolidays? schoolHolidays = null,
|
||||
FakeLessons? lessons = null, FakeSubstitutionEntries? substitutions = null,
|
||||
FakeSessions? sessions = null)
|
||||
FakeSessions? sessions = null, FakeEntries? entries = null)
|
||||
{
|
||||
lessons ??= new FakeLessons();
|
||||
lessons.Add(lesson);
|
||||
@@ -49,7 +50,7 @@ public sealed class DashboardViewModelTests
|
||||
new FakeGroups([group]), new FakeSubjects([]), lessons,
|
||||
exams ?? new FakeExams([]), results ?? new FakeResults(), grades ?? new FakeGrades(),
|
||||
reportGrades ?? new FakeReportGrades(), memberships ?? new FakeMemberships([]),
|
||||
tasks ?? new FakeWorkTasks(), sessions ?? new FakeSessions([]), new FakeEntries(),
|
||||
tasks ?? new FakeWorkTasks(), sessions ?? new FakeSessions([]), entries ?? new FakeEntries(),
|
||||
students ?? new FakeStudents([]), documentation ?? new FakeDocumentation(),
|
||||
slots ?? new FakeTimetableSlots(), periodSchedule ?? NewPeriodSchedule(),
|
||||
new AttendanceBalanceService(), new SchoolYearService(), dashboardSettings ?? NewDashboardSettings(),
|
||||
@@ -243,6 +244,45 @@ public sealed class DashboardViewModelTests
|
||||
e => e.Kind == CalendarEventKind.ParticipationSession && e.Subtitle == "Aufsatz");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Kalender_AusStundeErzeugteSitzungErzeugtKeinenDoppelteintrag()
|
||||
{
|
||||
// Regressionstest (Nutzer-Feedback): "Sitzung erzeugen" (3.3.1) verknüpft die Sitzung über
|
||||
// LessonId mit der Stunde — dieselbe Unterrichtsstunde darf im Kalender nicht zusätzlich
|
||||
// als eigener Sitzungstermin auftauchen.
|
||||
var today = DateOnly.FromDateTime(DateTime.Today);
|
||||
var group = new LearningGroup { Name = "9c" };
|
||||
var lesson = new Lesson { GroupId = group.Id, Date = today, Topic = "Brechung" };
|
||||
var session = new ParticipationSession { GroupId = group.Id, Date = today, LessonId = lesson.Id, Comment = "Brechung" };
|
||||
var vm = BuildVm(group, lesson, sessions: new FakeSessions([session]));
|
||||
|
||||
var day = vm.CalendarDays.Single(d => d.Date == today);
|
||||
Assert.True(day.HasLesson);
|
||||
Assert.False(day.HasSession);
|
||||
|
||||
vm.SelectCalendarDayCommand.Execute(day);
|
||||
|
||||
Assert.Single(vm.SelectedDayEvents);
|
||||
Assert.DoesNotContain(vm.SelectedDayEvents, e => e.Kind == CalendarEventKind.ParticipationSession);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Kalender_EigeneKlasseAmStundenplanTagOhneLessonZeigtDenRingTrotzdem()
|
||||
{
|
||||
// Regressionstest (Nutzer-Feedback): der "Meine Klasse"-Ring soll schon laut Stundenplan
|
||||
// gelten, nicht erst, sobald für den Tag eine Lesson angelegt wurde.
|
||||
var today = DateOnly.FromDateTime(DateTime.Today);
|
||||
var tomorrow = today.AddDays(1);
|
||||
var group = new LearningGroup { Name = "9c", IsOwnClass = true };
|
||||
var slots = new FakeTimetableSlots();
|
||||
slots.Add(new TimetableSlot { GroupId = group.Id, Weekday = tomorrow.DayOfWeek, PeriodNumber = 3 });
|
||||
|
||||
var vm = BuildVm(group, new Lesson { GroupId = group.Id, Date = today }, slots: slots);
|
||||
|
||||
var day = vm.CalendarDays.Single(d => d.Date == tomorrow);
|
||||
Assert.True(day.IsOwnClassDay);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DashboardKacheln_SichtbarkeitUndReihenfolgeWerdenGespeichert()
|
||||
{
|
||||
@@ -373,4 +413,53 @@ public sealed class DashboardViewModelTests
|
||||
|
||||
Assert.Empty(vm.UnplannedLessons);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AttendanceWarnings_KleineStichprobeMitHoherQuoteWirdNichtGemeldet()
|
||||
{
|
||||
var today = DateOnly.FromDateTime(DateTime.Today);
|
||||
var group = new LearningGroup { Name = "9c" };
|
||||
var student = new Student { FirstName = "Anna", LastName = "Beispiel" };
|
||||
var sessions = new FakeSessions([]);
|
||||
var entries = new FakeEntries();
|
||||
|
||||
// Nur 3 erfasste Termine, alle unentschuldigt (100 %) — Stichprobe zu klein für eine Meldung.
|
||||
for (var i = 0; i < 3; i++)
|
||||
{
|
||||
var session = new ParticipationSession { GroupId = group.Id, Date = today.AddDays(-i - 1) };
|
||||
sessions.Save(session);
|
||||
entries.Add(new ParticipationEntry { SessionId = session.Id, StudentId = student.Id, Attendance = AttendanceStatus.Unexcused });
|
||||
}
|
||||
|
||||
var vm = BuildVm(group, new Lesson { GroupId = group.Id, Date = today },
|
||||
students: new FakeStudents([student]), sessions: sessions, entries: entries);
|
||||
|
||||
Assert.Empty(vm.AttendanceWarnings);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AttendanceWarnings_MeldetHoheQuoteAbMindeststichprobe()
|
||||
{
|
||||
var today = DateOnly.FromDateTime(DateTime.Today);
|
||||
var group = new LearningGroup { Name = "9c" };
|
||||
var student = new Student { FirstName = "Anna", LastName = "Beispiel" };
|
||||
var sessions = new FakeSessions([]);
|
||||
var entries = new FakeEntries();
|
||||
|
||||
// 10 erfasste Termine, 3 davon unentschuldigt (30 % > 20 % Schwelle) — genug Stichprobe.
|
||||
for (var i = 0; i < 10; i++)
|
||||
{
|
||||
var session = new ParticipationSession { GroupId = group.Id, Date = today.AddDays(-i - 1) };
|
||||
sessions.Save(session);
|
||||
var status = i < 3 ? AttendanceStatus.Unexcused : AttendanceStatus.Present;
|
||||
entries.Add(new ParticipationEntry { SessionId = session.Id, StudentId = student.Id, Attendance = status });
|
||||
}
|
||||
|
||||
var vm = BuildVm(group, new Lesson { GroupId = group.Id, Date = today },
|
||||
students: new FakeStudents([student]), sessions: sessions, entries: entries);
|
||||
|
||||
var item = Assert.Single(vm.AttendanceWarnings);
|
||||
Assert.Equal(student.FullName, item.StudentName);
|
||||
Assert.Equal(30.0, item.AbsenceRatePercent);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -254,4 +254,51 @@ public sealed class GroupOverviewViewModelTests
|
||||
|
||||
Assert.Equal([6, 4, 3, 8], indices);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FehlendeHausaufgaben_ListetNurSchuelerAusDerLetztenSitzung()
|
||||
{
|
||||
var today = DateOnly.FromDateTime(DateTime.Today);
|
||||
var groupId = Guid.NewGuid();
|
||||
var studentMissing = new Student { FirstName = "Anna", LastName = "Beispiel" };
|
||||
var studentDone = new Student { FirstName = "Ben", LastName = "Muster" };
|
||||
var students = new FakeStudents([studentMissing, studentDone]);
|
||||
var sessions = new FakeSessions([]);
|
||||
var entries = new FakeEntries();
|
||||
|
||||
var olderSession = new ParticipationSession { GroupId = groupId, Date = today.AddDays(-10) };
|
||||
sessions.Save(olderSession);
|
||||
entries.Add(new ParticipationEntry
|
||||
{
|
||||
SessionId = olderSession.Id, StudentId = studentDone.Id,
|
||||
Homework = HomeworkStatus.MissingOpen, HomeworkMissing = true,
|
||||
});
|
||||
|
||||
var lastSession = new ParticipationSession { GroupId = groupId, Date = today.AddDays(-1) };
|
||||
sessions.Save(lastSession);
|
||||
entries.Add(new ParticipationEntry
|
||||
{
|
||||
SessionId = lastSession.Id, StudentId = studentMissing.Id,
|
||||
Homework = HomeworkStatus.MissingOverdue, HomeworkMissing = true,
|
||||
});
|
||||
entries.Add(new ParticipationEntry
|
||||
{
|
||||
SessionId = lastSession.Id, StudentId = studentDone.Id,
|
||||
Homework = HomeworkStatus.Completed, HomeworkMissing = false,
|
||||
});
|
||||
|
||||
var vm = NewVm(sessions: sessions, entries: entries, students: students);
|
||||
vm.Initialize(groupId, "Testkurs");
|
||||
|
||||
var item = Assert.Single(vm.MissingHomeworkStudents);
|
||||
Assert.Equal(studentMissing.FullName, item.StudentName);
|
||||
Assert.Contains(lastSession.Date.ToString("dd.MM.yyyy"), vm.MissingHomeworkSessionLabel);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FehlendeHausaufgaben_OhneOffenePostenIstLeer()
|
||||
{
|
||||
var (vm, _, _, _, _, _, _) = BuildScenario();
|
||||
Assert.False(vm.HasMissingHomework);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -284,4 +284,50 @@ public sealed class SeatingPlanViewModelTests
|
||||
Assert.False(vm.CanEdit);
|
||||
Assert.Empty(sessions.GetByGroup(groupId));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Initialize_LegtOhneVorhandeneSitzungKeineGeistersitzungAn()
|
||||
{
|
||||
// Regressionstest: Initialize() läuft schon beim bloßen Navigieren zur Kursübersicht
|
||||
// (GroupDetailViewModel.LoadGroup ruft SeatingPlanTab.Initialize für jede Gruppe auf),
|
||||
// unabhängig davon, ob der Sitzplan-Tab überhaupt angesehen wird. Vorher legte
|
||||
// LoadTodaySessions() dabei sofort eine "Sitzplan"-Sitzung für heute an.
|
||||
var groupId = Guid.NewGuid();
|
||||
var plan = new SeatingPlan { GroupId = groupId, Name = "Standard", Rows = 1, Columns = 1 };
|
||||
var sessions = new FakeSessions([]);
|
||||
var vm = new SeatingPlanTabViewModel(
|
||||
new FakeSeatingPlans([plan]), new FakeStudents([]), new FakeMemberships([]),
|
||||
sessions, new FakeEntries(), new FakeAspects());
|
||||
|
||||
vm.Initialize(groupId, isReadOnly: false);
|
||||
|
||||
Assert.Empty(sessions.GetByGroup(groupId));
|
||||
Assert.Null(vm.SelectedSession);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AssessStudent_LegtErstBeiTatsaechlicherBewertungEineSitzungAn()
|
||||
{
|
||||
var groupId = Guid.NewGuid();
|
||||
var student = new Student { FirstName = "Anna", LastName = "A" };
|
||||
var plan = new SeatingPlan
|
||||
{
|
||||
GroupId = groupId, Name = "Standard", Rows = 1, Columns = 1,
|
||||
Assignments = [new SeatAssignment { StudentId = student.Id }],
|
||||
};
|
||||
var sessions = new FakeSessions([]);
|
||||
var vm = new SeatingPlanTabViewModel(new FakeSeatingPlans([plan]), new FakeStudents([student]),
|
||||
new FakeMemberships([new GroupMembership { GroupId = groupId, StudentId = student.Id }]),
|
||||
sessions, new FakeEntries(), new FakeAspects());
|
||||
vm.Initialize(groupId, isReadOnly: false);
|
||||
vm.OnAssessStudent = _ => Task.CompletedTask;
|
||||
Assert.Empty(sessions.GetByGroup(groupId));
|
||||
|
||||
await vm.AssessStudent(vm.Seats.Single());
|
||||
|
||||
var session = Assert.Single(sessions.GetByGroup(groupId));
|
||||
Assert.Equal(DateOnly.FromDateTime(DateTime.Today), session.Date);
|
||||
Assert.Equal("Sitzplan", session.Comment);
|
||||
Assert.Equal(session.Id, vm.SelectedSession?.Id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -171,7 +171,7 @@ public sealed class WorkTaskListViewModelTests
|
||||
{
|
||||
var tasks = new FakeWorkTasks();
|
||||
var vm = new WorkTaskListViewModel(tasks, new FakeGroups([]), new FakeTimeEntries());
|
||||
vm.OnEditTask = _ => Task.FromResult<WorkTask?>(new WorkTask { Title = "Neu" });
|
||||
vm.OnEditTask = (_, _) => Task.FromResult<WorkTask?>(new WorkTask { Title = "Neu" });
|
||||
|
||||
await vm.AddTaskCommand.ExecuteAsync(null);
|
||||
|
||||
@@ -179,6 +179,26 @@ public sealed class WorkTaskListViewModelTests
|
||||
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()
|
||||
{
|
||||
@@ -285,6 +305,38 @@ public sealed class AddEditWorkTaskDialogViewModelTests
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user