Neuer Tab "Planung" in GroupDetailView ersetzt den Platzhalter: Einheiten anlegen/bearbeiten/ als Vorlage in andere Gruppe kopieren, Stunden je Einheit mit Verschieben (inkl. Nachrücken der Folgestunden). Stundeneditor als tabellarischer Verlaufsplan (Phase/Dauer/Tätigkeit/ Material/Kurzsymbol je Zeile, Uhrzeit aus optionalem Stundenbeginn abgeleitet) statt eines einzelnen Phase-Felds mit Methoden-/Materialien-Chips — Kurzsymbol als Freitext mit Vorschlägen aus neuem Kürzel-Katalog (Einstellungen) plus bisher verwendeten Werten. Schema-Migrationen v1-v3 überführen bestehende Daten verlustfrei. 4.2.5 bewusst offen gelassen (hängt an Stundenplan, Kapitel 4.3). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
204 lines
9.6 KiB
C#
204 lines
9.6 KiB
C#
using LehrerApp.Core.Models;
|
|
using LehrerApp.Desktop.ViewModels.Groups;
|
|
using Xunit;
|
|
|
|
namespace LehrerApp.Desktop.Tests;
|
|
|
|
/// Tests für die Unterrichtsplanung (4.1 Einheiten / 4.2 Einzelstunden): Fortschrittsberechnung,
|
|
/// Verschieben mit/ohne Nachrücken der Folgestunden und die Vorlage-Kopie in eine andere Gruppe.
|
|
public class PlanningTabViewModelTests
|
|
{
|
|
private static (PlanningTabViewModel Vm, FakeUnits Units, FakeLessons Lessons, Guid GroupId) BuildScenario()
|
|
{
|
|
var groupId = Guid.NewGuid();
|
|
var group = new LearningGroup { Id = groupId, Name = "Testgruppe" };
|
|
var groups = new FakeGroups([group]);
|
|
var subjects = new FakeSubjects([]);
|
|
var competencyDomains = new FakeCompetencyDomains();
|
|
var units = new FakeUnits();
|
|
var lessons = new FakeLessons();
|
|
|
|
var vm = new PlanningTabViewModel(units, lessons, groups, subjects, competencyDomains);
|
|
vm.Initialize(groupId);
|
|
|
|
return (vm, units, lessons, groupId);
|
|
}
|
|
|
|
[Fact]
|
|
public void LoadUnits_BerechnetFortschrittAusDurchgefuehrtenStunden()
|
|
{
|
|
var (vm, units, lessons, groupId) = BuildScenario();
|
|
var unit = new Unit { GroupId = groupId, Title = "Optik" };
|
|
units.Add(unit);
|
|
lessons.Add(new Lesson { UnitId = unit.Id, GroupId = groupId, Date = new DateOnly(2025, 9, 1), Status = LessonStatus.Conducted, Topic = "A" });
|
|
lessons.Add(new Lesson { UnitId = unit.Id, GroupId = groupId, Date = new DateOnly(2025, 9, 8), Status = LessonStatus.Conducted, Topic = "B" });
|
|
lessons.Add(new Lesson { UnitId = unit.Id, GroupId = groupId, Date = new DateOnly(2025, 9, 15), Status = LessonStatus.Planned, Topic = "C" });
|
|
|
|
vm.Initialize(groupId);
|
|
|
|
var summary = vm.Units.Single(u => u.Id == unit.Id);
|
|
Assert.Equal(2, summary.ConductedCount);
|
|
Assert.Equal(3, summary.TotalCount);
|
|
Assert.Equal(2.0 / 3, summary.ProgressFraction, 3);
|
|
Assert.Contains("2 / 3", summary.ProgressText);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task MoveLesson_MitNachrueckenVerschiebtNurGeplanteFolgestundenNachDemStichtag()
|
|
{
|
|
var (vm, units, lessons, groupId) = BuildScenario();
|
|
var unit = new Unit { GroupId = groupId, Title = "Mechanik" };
|
|
units.Add(unit);
|
|
var l1 = new Lesson { UnitId = unit.Id, GroupId = groupId, Date = new DateOnly(2025, 9, 1), Status = LessonStatus.Planned, Topic = "1" };
|
|
var l2 = new Lesson { UnitId = unit.Id, GroupId = groupId, Date = new DateOnly(2025, 9, 8), Status = LessonStatus.Planned, Topic = "2" };
|
|
var l3 = new Lesson { UnitId = unit.Id, GroupId = groupId, Date = new DateOnly(2025, 9, 15), Status = LessonStatus.Planned, Topic = "3" };
|
|
var l4 = new Lesson { UnitId = unit.Id, GroupId = groupId, Date = new DateOnly(2025, 9, 22), Status = LessonStatus.Conducted, Topic = "4" };
|
|
lessons.Add(l1); lessons.Add(l2); lessons.Add(l3); lessons.Add(l4);
|
|
|
|
vm.Initialize(groupId);
|
|
vm.SelectedUnit = vm.Units.Single(u => u.Id == unit.Id);
|
|
vm.SelectedLesson = vm.Lessons.Single(l => l.Id == l1.Id);
|
|
vm.OnPickMoveTarget = _ => Task.FromResult<MoveLessonTarget?>(new MoveLessonTarget(new DateOnly(2025, 9, 3), true));
|
|
|
|
await vm.MoveLessonCommand.ExecuteAsync(null);
|
|
|
|
var byUnit = lessons.GetByUnit(unit.Id).ToDictionary(l => l.Id);
|
|
Assert.Equal(new DateOnly(2025, 9, 3), byUnit[l1.Id].Date);
|
|
Assert.Equal(new DateOnly(2025, 9, 10), byUnit[l2.Id].Date); // +2 Tage nachgerückt
|
|
Assert.Equal(new DateOnly(2025, 9, 17), byUnit[l3.Id].Date); // +2 Tage nachgerückt
|
|
Assert.Equal(new DateOnly(2025, 9, 22), byUnit[l4.Id].Date); // Conducted: unverändert
|
|
}
|
|
|
|
[Fact]
|
|
public async Task MoveLesson_OhneNachrueckenVerschiebtNurDieAusgewaehlteStunde()
|
|
{
|
|
var (vm, units, lessons, groupId) = BuildScenario();
|
|
var unit = new Unit { GroupId = groupId, Title = "Mechanik" };
|
|
units.Add(unit);
|
|
var l1 = new Lesson { UnitId = unit.Id, GroupId = groupId, Date = new DateOnly(2025, 9, 1), Status = LessonStatus.Planned, Topic = "1" };
|
|
var l2 = new Lesson { UnitId = unit.Id, GroupId = groupId, Date = new DateOnly(2025, 9, 8), Status = LessonStatus.Planned, Topic = "2" };
|
|
lessons.Add(l1); lessons.Add(l2);
|
|
|
|
vm.Initialize(groupId);
|
|
vm.SelectedUnit = vm.Units.Single(u => u.Id == unit.Id);
|
|
vm.SelectedLesson = vm.Lessons.Single(l => l.Id == l1.Id);
|
|
vm.OnPickMoveTarget = _ => Task.FromResult<MoveLessonTarget?>(new MoveLessonTarget(new DateOnly(2025, 9, 5), false));
|
|
|
|
await vm.MoveLessonCommand.ExecuteAsync(null);
|
|
|
|
var byUnit = lessons.GetByUnit(unit.Id).ToDictionary(l => l.Id);
|
|
Assert.Equal(new DateOnly(2025, 9, 5), byUnit[l1.Id].Date);
|
|
Assert.Equal(new DateOnly(2025, 9, 8), byUnit[l2.Id].Date); // unverändert
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CopyUnit_ErhaeltAbstaendeUndSetztGroupIdAufZielgruppe()
|
|
{
|
|
var (vm, units, lessons, sourceGroupId) = BuildScenario();
|
|
var targetGroupId = Guid.NewGuid();
|
|
|
|
var unit = new Unit
|
|
{
|
|
GroupId = sourceGroupId,
|
|
Title = "Kinetik",
|
|
StartDate = new DateOnly(2025, 9, 1),
|
|
EndDate = new DateOnly(2025, 10, 1),
|
|
Notes = "Original-Notiz",
|
|
};
|
|
units.Add(unit);
|
|
var l1 = new Lesson
|
|
{
|
|
UnitId = unit.Id, GroupId = sourceGroupId, Date = new DateOnly(2025, 9, 1),
|
|
Topic = "Einführung", Status = LessonStatus.Conducted, Reflection = "lief gut",
|
|
StartTime = new TimeOnly(11, 45),
|
|
Phases = [new LessonPhaseStep { Name = "Einstieg", DurationMinutes = 10, Material = "AB01", Shorthand = "L->AB01" }],
|
|
};
|
|
var l2 = new Lesson
|
|
{
|
|
UnitId = unit.Id, GroupId = sourceGroupId, Date = new DateOnly(2025, 9, 8),
|
|
Topic = "Vertiefung", Status = LessonStatus.Planned,
|
|
};
|
|
lessons.Add(l1); lessons.Add(l2);
|
|
|
|
vm.Initialize(sourceGroupId);
|
|
vm.SelectedUnit = vm.Units.Single(u => u.Id == unit.Id);
|
|
var anchor = new DateOnly(2025, 10, 1);
|
|
vm.OnPickCopyTarget = _ => Task.FromResult<CopyUnitTarget?>(new CopyUnitTarget(targetGroupId, anchor));
|
|
|
|
await vm.CopyUnitCommand.ExecuteAsync(null);
|
|
|
|
var newUnit = units.GetByGroup(targetGroupId).Single();
|
|
Assert.Equal("Kinetik", newUnit.Title);
|
|
Assert.Equal(UnitStatus.Planned, newUnit.Status);
|
|
Assert.Equal(anchor, newUnit.StartDate);
|
|
Assert.Equal(anchor.AddDays(30), newUnit.EndDate); // ursprünglich 30 Tage nach StartDate
|
|
|
|
var copiedLessons = lessons.GetByUnit(newUnit.Id).OrderBy(l => l.Date).ToList();
|
|
Assert.Equal(2, copiedLessons.Count);
|
|
Assert.All(copiedLessons, l => Assert.Equal(targetGroupId, l.GroupId)); // nicht sourceGroupId
|
|
Assert.Equal(anchor, copiedLessons[0].Date);
|
|
Assert.Equal(anchor.AddDays(7), copiedLessons[1].Date); // 7-Tage-Abstand erhalten
|
|
Assert.All(copiedLessons, l => Assert.Equal(LessonStatus.Planned, l.Status));
|
|
Assert.Null(copiedLessons[0].Reflection); // Reflexion wird bei der Kopie geleert
|
|
Assert.Equal(new TimeOnly(11, 45), copiedLessons[0].StartTime); // Beginn wird übernommen
|
|
Assert.Single(copiedLessons[0].Phases);
|
|
Assert.Equal("Einstieg", copiedLessons[0].Phases[0].Name);
|
|
Assert.Equal("L->AB01", copiedLessons[0].Phases[0].Shorthand);
|
|
Assert.NotEqual(l1.Phases[0].Id, copiedLessons[0].Phases[0].Id); // frische Id, keine geteilte Referenz
|
|
|
|
// Ursprüngliche Einheit/Stunden bleiben unverändert in der Quellgruppe.
|
|
Assert.Single(units.GetByGroup(sourceGroupId));
|
|
Assert.Equal(LessonStatus.Conducted, lessons.GetByUnit(unit.Id).Single(l => l.Id == l1.Id).Status);
|
|
}
|
|
|
|
[Fact]
|
|
public void LoadUnits_SammeltMaterialienAusAllenStundenphasenDerGruppe()
|
|
{
|
|
var (vm, units, lessons, groupId) = BuildScenario();
|
|
var unit1 = new Unit { GroupId = groupId, Title = "Einheit 1" };
|
|
var unit2 = new Unit { GroupId = groupId, Title = "Einheit 2" };
|
|
units.Add(unit1); units.Add(unit2);
|
|
lessons.Add(new Lesson
|
|
{
|
|
UnitId = unit1.Id, GroupId = groupId, Date = new DateOnly(2025, 9, 1), Topic = "A",
|
|
Phases = [new LessonPhaseStep { Material = "Arbeitsblatt" }],
|
|
});
|
|
lessons.Add(new Lesson
|
|
{
|
|
UnitId = unit2.Id, GroupId = groupId, Date = new DateOnly(2025, 9, 2), Topic = "B",
|
|
Phases =
|
|
[
|
|
new LessonPhaseStep { Material = "Modell" },
|
|
new LessonPhaseStep { Material = "Arbeitsblatt" }, // Duplikat, soll nur einmal erscheinen
|
|
new LessonPhaseStep { Material = "" }, // leer, soll ignoriert werden
|
|
],
|
|
});
|
|
|
|
vm.Initialize(groupId);
|
|
|
|
Assert.Equal(["Arbeitsblatt", "Modell"], vm.KnownMaterials);
|
|
}
|
|
|
|
[Fact]
|
|
public void LoadUnits_SammeltKurzsymboleAusAllenStundenphasenDerGruppe()
|
|
{
|
|
var (vm, units, lessons, groupId) = BuildScenario();
|
|
var unit = new Unit { GroupId = groupId, Title = "Einheit 1" };
|
|
units.Add(unit);
|
|
lessons.Add(new Lesson
|
|
{
|
|
UnitId = unit.Id, GroupId = groupId, Date = new DateOnly(2025, 9, 1), Topic = "A",
|
|
Phases =
|
|
[
|
|
new LessonPhaseStep { Shorthand = "Plenum" },
|
|
new LessonPhaseStep { Shorthand = "L->AB01" },
|
|
new LessonPhaseStep { Shorthand = "" }, // leer, soll ignoriert werden
|
|
],
|
|
});
|
|
|
|
vm.Initialize(groupId);
|
|
|
|
Assert.Equal(["L->AB01", "Plenum"], vm.KnownShorthands);
|
|
}
|
|
}
|