Stundenverlaufsplan: Viewer und Alternativpfad-Katalog (Kapitel 4.2 Nachtrag)
Schreibgeschützter LessonViewerDialog (größere Schrift, ohne Bearbeitungs-/Verlängern-/ Verschieben-Funktion) für den Einsatz während des Unterrichtens, erreichbar über "Anzeigen" im Stunden-Toolbar. Alternative Unterrichtsabläufe (z.B. Kurzversion bei Zeitmangel) laufen jetzt über einen echten Katalog (neues Modell AlternativeLessonPath: Name + Beschreibung) statt Freitext direkt an der Phase: im Verlaufsplan-Editor eine kompakte, farbig unterstützte Checkbox statt einer durchgehend sichtbaren Eingabespalte, Zuordnung/Neuanlage über einen eigenen Dialog. Der Viewer gruppiert Phasen entsprechend und zeigt die hinterlegte Beschreibung. Schema-Migration v3→v4 führt bestehende Freitextwerte verlustfrei in Katalogeinträge über. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -198,6 +198,14 @@ public class FakeShorthandCodes(List<ShorthandCode> all) : IShorthandCodeReposit
|
||||
public void Delete(Guid id) { }
|
||||
}
|
||||
|
||||
public class FakeAlternativeLessonPaths(List<AlternativeLessonPath> all) : IAlternativeLessonPathRepository
|
||||
{
|
||||
public List<AlternativeLessonPath> GetAll() => all;
|
||||
public AlternativeLessonPath? GetById(Guid id) => all.FirstOrDefault(p => p.Id == id);
|
||||
public void Save(AlternativeLessonPath path) { all.RemoveAll(p => p.Id == path.Id); all.Add(path); }
|
||||
public void Delete(Guid id) => all.RemoveAll(p => p.Id == id);
|
||||
}
|
||||
|
||||
public class FakeReportGrades : IReportGradeRepository
|
||||
{
|
||||
private readonly List<ReportGrade> _all = [];
|
||||
|
||||
@@ -7,7 +7,8 @@ namespace LehrerApp.Desktop.Tests;
|
||||
public sealed class LessonDialogViewModelTests
|
||||
{
|
||||
private static LessonDialogViewModel BuildVm(Guid unitId, Guid groupId, Lesson? editing = null) =>
|
||||
new(new FakeLessons(), new FakeShorthandCodes([]), unitId, groupId, [], [], editing);
|
||||
new(new FakeLessons(), new FakeShorthandCodes([]), new FakeAlternativeLessonPaths([]),
|
||||
unitId, groupId, [], [], editing);
|
||||
|
||||
[Fact]
|
||||
public void AddPhase_FuegtZeileMitStandardwertenHinzu()
|
||||
@@ -18,6 +19,7 @@ public sealed class LessonDialogViewModelTests
|
||||
|
||||
Assert.Single(vm.Phases);
|
||||
Assert.Equal(5, vm.Phases[0].DurationMinutes);
|
||||
Assert.False(vm.Phases[0].HasAlternativePath);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -68,8 +70,8 @@ public sealed class LessonDialogViewModelTests
|
||||
public void ShorthandSuggestions_KombiniertKatalogUndBisherigeStundenwerte()
|
||||
{
|
||||
var codes = new FakeShorthandCodes([new ShorthandCode { Code = "Tb" }, new ShorthandCode { Code = "SH" }]);
|
||||
var vm = new LessonDialogViewModel(new FakeLessons(), codes, Guid.NewGuid(), Guid.NewGuid(),
|
||||
[], ["Plenum", "LDE", "Tb"], null); // "Tb" doppelt (Katalog + Historie), soll nur einmal erscheinen
|
||||
var vm = new LessonDialogViewModel(new FakeLessons(), codes, new FakeAlternativeLessonPaths([]),
|
||||
Guid.NewGuid(), Guid.NewGuid(), [], ["Plenum", "LDE", "Tb"], null); // "Tb" doppelt (Katalog + Historie), soll nur einmal erscheinen
|
||||
|
||||
Assert.Equal(["LDE", "Plenum", "SH", "Tb"], vm.ShorthandSuggestions);
|
||||
}
|
||||
@@ -130,7 +132,8 @@ public sealed class LessonDialogViewModelTests
|
||||
var unitId = Guid.NewGuid();
|
||||
var groupId = Guid.NewGuid();
|
||||
var lessons = new FakeLessons();
|
||||
var vm = new LessonDialogViewModel(lessons, new FakeShorthandCodes([]), unitId, groupId, [], [], null)
|
||||
var vm = new LessonDialogViewModel(lessons, new FakeShorthandCodes([]), new FakeAlternativeLessonPaths([]),
|
||||
unitId, groupId, [], [], null)
|
||||
{
|
||||
Topic = "Brechung", DateText = "01.09.2025", StartTimeText = "11:45",
|
||||
};
|
||||
@@ -153,4 +156,83 @@ public sealed class LessonDialogViewModelTests
|
||||
Assert.Equal("L->AB01", vm.Result.Phases[0].Shorthand);
|
||||
Assert.Single(lessons.GetByUnit(unitId));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HasAlternativePath_AngehaktOeffnetZuweisungUndUebernimmtErgebnis()
|
||||
{
|
||||
var vm = BuildVm(Guid.NewGuid(), Guid.NewGuid());
|
||||
var path = new AlternativeLessonPath { Name = "Kurzversion" };
|
||||
vm.OnPickAlternativePath = _ => Task.FromResult<AlternativeLessonPath?>(path);
|
||||
vm.AddPhaseCommand.Execute(null);
|
||||
|
||||
vm.Phases[0].HasAlternativePath = true;
|
||||
|
||||
Assert.True(vm.Phases[0].HasAlternativePath);
|
||||
Assert.Equal("Kurzversion", vm.Phases[0].AlternativePathName);
|
||||
Assert.Equal(path.Id, vm.Phases[0].AlternativePathId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HasAlternativePath_AbbruchDerZuweisungLaesstEsUnangehakt()
|
||||
{
|
||||
var vm = BuildVm(Guid.NewGuid(), Guid.NewGuid());
|
||||
vm.OnPickAlternativePath = _ => Task.FromResult<AlternativeLessonPath?>(null); // abgebrochen
|
||||
vm.AddPhaseCommand.Execute(null);
|
||||
|
||||
vm.Phases[0].HasAlternativePath = true;
|
||||
|
||||
Assert.False(vm.Phases[0].HasAlternativePath);
|
||||
Assert.Null(vm.Phases[0].AlternativePathId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HasAlternativePath_AbwaehlenEntferntZuordnungOhneDialogZuOeffnen()
|
||||
{
|
||||
var vm = BuildVm(Guid.NewGuid(), Guid.NewGuid());
|
||||
var path = new AlternativeLessonPath { Name = "Kurzversion" };
|
||||
var dialogCalls = 0;
|
||||
vm.OnPickAlternativePath = _ => { dialogCalls++; return Task.FromResult<AlternativeLessonPath?>(path); };
|
||||
vm.AddPhaseCommand.Execute(null);
|
||||
vm.Phases[0].HasAlternativePath = true; // zuweisen -> 1 Dialogaufruf
|
||||
|
||||
vm.Phases[0].HasAlternativePath = false; // abwählen -> kein weiterer Dialogaufruf
|
||||
|
||||
Assert.Equal(1, dialogCalls);
|
||||
Assert.False(vm.Phases[0].HasAlternativePath);
|
||||
Assert.Equal("", vm.Phases[0].AlternativePathName);
|
||||
Assert.Null(vm.Phases[0].AlternativePathId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Save_UebernimmtZugewiesenenAlternativePfad()
|
||||
{
|
||||
var vm = BuildVm(Guid.NewGuid(), Guid.NewGuid());
|
||||
var path = new AlternativeLessonPath { Name = "Kurzversion" };
|
||||
vm.OnPickAlternativePath = _ => Task.FromResult<AlternativeLessonPath?>(path);
|
||||
vm.Topic = "Thema"; vm.DateText = "01.09.2025";
|
||||
vm.AddPhaseCommand.Execute(null);
|
||||
vm.Phases[0].HasAlternativePath = true;
|
||||
|
||||
vm.SaveCommand.Execute(null);
|
||||
|
||||
Assert.Equal(path.Id, vm.Result!.Phases[0].AlternativePathId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AddPhaseInternal_LoestBestehendeZuordnungBeimBearbeitenAuf()
|
||||
{
|
||||
var path = new AlternativeLessonPath { Name = "Kurzversion" };
|
||||
var alternativePaths = new FakeAlternativeLessonPaths([path]);
|
||||
var editing = new Lesson
|
||||
{
|
||||
Topic = "Thema", Date = new DateOnly(2025, 9, 1),
|
||||
Phases = [new LessonPhaseStep { Name = "Einstieg", AlternativePathId = path.Id }],
|
||||
};
|
||||
|
||||
var vm = new LessonDialogViewModel(new FakeLessons(), new FakeShorthandCodes([]), alternativePaths,
|
||||
Guid.NewGuid(), Guid.NewGuid(), [], [], editing);
|
||||
|
||||
Assert.True(vm.Phases[0].HasAlternativePath);
|
||||
Assert.Equal("Kurzversion", vm.Phases[0].AlternativePathName);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
using LehrerApp.Core.Models;
|
||||
using LehrerApp.Desktop.ViewModels.Groups;
|
||||
using Xunit;
|
||||
|
||||
namespace LehrerApp.Desktop.Tests;
|
||||
|
||||
public sealed class LessonViewerViewModelTests
|
||||
{
|
||||
[Fact]
|
||||
public void OhneAlternativenGibtEsGenauEineHauptwegGruppe()
|
||||
{
|
||||
var lesson = new Lesson
|
||||
{
|
||||
Topic = "Brechung", Date = new DateOnly(2025, 9, 1), StartTime = new TimeOnly(11, 45),
|
||||
Phases =
|
||||
[
|
||||
new LessonPhaseStep { Name = "Einstieg", DurationMinutes = 5, Activity = "Begrüßung" },
|
||||
new LessonPhaseStep { Name = "Erarbeitung", DurationMinutes = 15, Activity = "AB01 bearbeiten" },
|
||||
],
|
||||
};
|
||||
|
||||
var vm = new LessonViewerViewModel(lesson, new FakeAlternativeLessonPaths([]));
|
||||
|
||||
Assert.False(vm.HasAlternatives);
|
||||
var group = Assert.Single(vm.PhaseGroups);
|
||||
Assert.Equal("Hauptweg", group.Label);
|
||||
Assert.True(group.IsMainPath);
|
||||
Assert.Equal(2, group.Phases.Count);
|
||||
Assert.Equal("ab 11:45", group.Phases[0].TimeDisplay);
|
||||
Assert.Equal("ab 11:50", group.Phases[1].TimeDisplay);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OhneBeginnBleibtDieUhrzeitanzeigeLeer()
|
||||
{
|
||||
var lesson = new Lesson
|
||||
{
|
||||
Topic = "Brechung", Date = new DateOnly(2025, 9, 1),
|
||||
Phases = [new LessonPhaseStep { Name = "Einstieg", DurationMinutes = 5 }],
|
||||
};
|
||||
|
||||
var vm = new LessonViewerViewModel(lesson, new FakeAlternativeLessonPaths([]));
|
||||
|
||||
Assert.Equal("", vm.PhaseGroups[0].Phases[0].TimeDisplay);
|
||||
Assert.Equal("–", vm.StartTimeDisplay);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GruppiertPhasenNachAlternativePathUndHauptwegStehtImmerZuerst()
|
||||
{
|
||||
var kurzversion = new AlternativeLessonPath { Name = "Kurzversion", Description = "Bei Zeitmangel." };
|
||||
var alternativePaths = new FakeAlternativeLessonPaths([kurzversion]);
|
||||
var lesson = new Lesson
|
||||
{
|
||||
Topic = "Brechung", Date = new DateOnly(2025, 9, 1), StartTime = new TimeOnly(12, 0),
|
||||
Phases =
|
||||
[
|
||||
new LessonPhaseStep { Name = "Einstieg", DurationMinutes = 5, AlternativePathId = kurzversion.Id },
|
||||
new LessonPhaseStep { Name = "Einstieg", DurationMinutes = 5 }, // Hauptweg, aber nach der Alternative angelegt
|
||||
new LessonPhaseStep { Name = "Sicherung (kurz)", DurationMinutes = 5, AlternativePathId = kurzversion.Id },
|
||||
new LessonPhaseStep { Name = "Erarbeitung", DurationMinutes = 20 },
|
||||
],
|
||||
};
|
||||
|
||||
var vm = new LessonViewerViewModel(lesson, alternativePaths);
|
||||
|
||||
Assert.True(vm.HasAlternatives);
|
||||
Assert.Equal(2, vm.PhaseGroups.Count);
|
||||
Assert.Equal("Hauptweg", vm.PhaseGroups[0].Label); // steht zuerst trotz Reihenfolge im Modell
|
||||
Assert.True(vm.PhaseGroups[0].IsMainPath);
|
||||
Assert.Null(vm.PhaseGroups[0].Description);
|
||||
Assert.Equal(2, vm.PhaseGroups[0].Phases.Count);
|
||||
Assert.Equal("Kurzversion", vm.PhaseGroups[1].Label);
|
||||
Assert.False(vm.PhaseGroups[1].IsMainPath);
|
||||
Assert.Equal("Bei Zeitmangel.", vm.PhaseGroups[1].Description);
|
||||
Assert.Equal(2, vm.PhaseGroups[1].Phases.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void JedeGruppeBerechnetIhreEigeneUhrzeitAbStundenbeginn()
|
||||
{
|
||||
var planB = new AlternativeLessonPath { Name = "Plan B" };
|
||||
var alternativePaths = new FakeAlternativeLessonPaths([planB]);
|
||||
var lesson = new Lesson
|
||||
{
|
||||
Topic = "Brechung", Date = new DateOnly(2025, 9, 1), StartTime = new TimeOnly(12, 0),
|
||||
Phases =
|
||||
[
|
||||
new LessonPhaseStep { Name = "Einstieg", DurationMinutes = 5 },
|
||||
new LessonPhaseStep { Name = "Vertiefung", DurationMinutes = 30, AlternativePathId = planB.Id },
|
||||
],
|
||||
};
|
||||
|
||||
var vm = new LessonViewerViewModel(lesson, alternativePaths);
|
||||
|
||||
// Beide Gruppen starten unabhängig voneinander bei Stundenbeginn, nicht hintereinander.
|
||||
Assert.Equal("ab 12:00", vm.PhaseGroups[0].Phases[0].TimeDisplay);
|
||||
Assert.Equal("ab 12:00", vm.PhaseGroups[1].Phases[0].TimeDisplay);
|
||||
}
|
||||
}
|
||||
@@ -96,6 +96,7 @@ public class PlanningTabViewModelTests
|
||||
{
|
||||
var (vm, units, lessons, sourceGroupId) = BuildScenario();
|
||||
var targetGroupId = Guid.NewGuid();
|
||||
var kurzversionId = Guid.NewGuid(); // Katalog-Referenz, bleibt beim Kopieren unverändert
|
||||
|
||||
var unit = new Unit
|
||||
{
|
||||
@@ -111,7 +112,7 @@ public class PlanningTabViewModelTests
|
||||
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" }],
|
||||
Phases = [new LessonPhaseStep { Name = "Einstieg", DurationMinutes = 10, Material = "AB01", Shorthand = "L->AB01", AlternativePathId = kurzversionId }],
|
||||
};
|
||||
var l2 = new Lesson
|
||||
{
|
||||
@@ -144,6 +145,7 @@ public class PlanningTabViewModelTests
|
||||
Assert.Single(copiedLessons[0].Phases);
|
||||
Assert.Equal("Einstieg", copiedLessons[0].Phases[0].Name);
|
||||
Assert.Equal("L->AB01", copiedLessons[0].Phases[0].Shorthand);
|
||||
Assert.Equal(kurzversionId, copiedLessons[0].Phases[0].AlternativePathId);
|
||||
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.
|
||||
|
||||
Reference in New Issue
Block a user