Unterrichtsplanung: Einheiten, Verlaufsplan-Editor, Kürzel-Katalog (Kapitel 4.1/4.2)

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>
This commit is contained in:
2026-08-14 00:50:57 +02:00
co-authored by Claude Sonnet 5
parent 27359794f7
commit de6ea001e7
28 changed files with 2372 additions and 32 deletions
+91 -2
View File
@@ -121,7 +121,7 @@ public sealed class LiteDbContextTests
using var temp = new TempDatabase();
using var context = new LiteDbContext(temp.Path);
Assert.Equal(1, context.SchemaVersion);
Assert.Equal(3, context.SchemaVersion);
}
[Fact]
@@ -132,7 +132,96 @@ public sealed class LiteDbContextTests
using var second = new LiteDbContext(temp.Path);
Assert.Equal(1, second.SchemaVersion);
Assert.Equal(3, second.SchemaVersion);
}
[Fact]
public void MigrateLessonPhases_FasstAltePhaseMethodenUndMaterialienVerlustfreiZusammen()
{
using var temp = new TempDatabase();
var lessonWithDataId = Guid.NewGuid();
var lessonEmptyId = Guid.NewGuid();
using (var legacy = new LiteDatabase(temp.Path))
{
legacy.GetCollection<BsonDocument>("lessons").Insert(new BsonDocument
{
["_id"] = lessonWithDataId,
[nameof(Lesson.UnitId)] = Guid.NewGuid(),
[nameof(Lesson.GroupId)] = Guid.NewGuid(),
["Phase"] = "Einstieg",
["Methods"] = new BsonArray { "Gespräch", "Tafelbild" },
["Materials"] = new BsonArray { "Arbeitsblatt" },
});
// Alte Stunde ganz ohne Phase/Methoden/Materialien — soll leere Phases-Liste bekommen,
// nicht eine leere synthetisierte Zeile.
legacy.GetCollection<BsonDocument>("lessons").Insert(new BsonDocument
{
["_id"] = lessonEmptyId,
[nameof(Lesson.UnitId)] = Guid.NewGuid(),
[nameof(Lesson.GroupId)] = Guid.NewGuid(),
});
}
using (var context = new LiteDbContext(temp.Path))
{
var withData = context.Lessons.FindById(lessonWithDataId);
Assert.NotNull(withData);
Assert.Single(withData.Phases);
Assert.Equal("Einstieg", withData.Phases[0].Name);
Assert.Equal("Gespräch; Tafelbild", withData.Phases[0].Activity);
Assert.Equal("Arbeitsblatt", withData.Phases[0].Material);
Assert.Equal(0, withData.Phases[0].DurationMinutes);
var empty = context.Lessons.FindById(lessonEmptyId);
Assert.NotNull(empty);
Assert.Empty(empty.Phases);
}
using var migrated = new LiteDatabase(temp.Path);
var raw = migrated.GetCollection<BsonDocument>("lessons").FindById(lessonWithDataId);
Assert.False(raw.ContainsKey("Phase"));
Assert.False(raw.ContainsKey("Methods"));
Assert.False(raw.ContainsKey("Materials"));
}
[Fact]
public void MigrateLessonShorthand_FasstVonUndNachInEinFreitextfeldZusammen()
{
using var temp = new TempDatabase();
var lessonId = Guid.NewGuid();
using (var legacy = new LiteDatabase(temp.Path))
{
legacy.GetCollection<BsonDocument>("lessons").Insert(new BsonDocument
{
["_id"] = lessonId,
[nameof(Lesson.UnitId)] = Guid.NewGuid(),
[nameof(Lesson.GroupId)] = Guid.NewGuid(),
[nameof(Lesson.Phases)] = new BsonArray
{
new BsonDocument { ["ShorthandFrom"] = "Tb", ["ShorthandTo"] = "SH" },
new BsonDocument { ["ShorthandFrom"] = "Plenum" }, // nur "Von" gesetzt, kein Pfeil
new BsonDocument { }, // weder Von noch Nach
},
});
}
using (var context = new LiteDbContext(temp.Path))
{
var lesson = context.Lessons.FindById(lessonId);
Assert.NotNull(lesson);
Assert.Equal(3, lesson.Phases.Count);
Assert.Equal("Tb->SH", lesson.Phases[0].Shorthand);
Assert.Equal("Plenum", lesson.Phases[1].Shorthand);
Assert.Equal("", lesson.Phases[2].Shorthand);
}
using var migratedShorthand = new LiteDatabase(temp.Path);
var rawLesson = migratedShorthand.GetCollection<BsonDocument>("lessons").FindById(lessonId);
var rawPhases = rawLesson["Phases"].AsArray;
Assert.False(rawPhases[0].AsDocument.ContainsKey("ShorthandFrom"));
Assert.False(rawPhases[0].AsDocument.ContainsKey("ShorthandTo"));
}
private sealed class TempDatabase : IDisposable
+64
View File
@@ -424,4 +424,68 @@ public sealed class RepositoryTests
Assert.Null(db.Documentation.FindById(doc.Id));
Assert.Null(storage.OpenRead(attachmentId));
}
// ── LessonRepository ──────────────────────────────────────────────────────
[Fact]
public void LessonRepository_GetByUnit_SortiertNachDatumDannStundennummer()
{
using var db = NewInMemoryContext();
var repo = new LessonRepository(db);
var unitId = Guid.NewGuid();
var groupId = Guid.NewGuid();
// Doppelstunde am selben Tag: Stunde 2 vor Stunde 1 eingefügt, muss trotzdem
// nach LessonNumber sortiert erscheinen.
repo.Save(new Lesson { UnitId = unitId, GroupId = groupId, Date = new DateOnly(2025, 9, 8), LessonNumber = 2 });
repo.Save(new Lesson { UnitId = unitId, GroupId = groupId, Date = new DateOnly(2025, 9, 8), LessonNumber = 1 });
repo.Save(new Lesson { UnitId = unitId, GroupId = groupId, Date = new DateOnly(2025, 9, 1), LessonNumber = 1 });
var result = repo.GetByUnit(unitId);
Assert.Equal(new DateOnly(2025, 9, 1), result[0].Date);
Assert.Equal(new DateOnly(2025, 9, 8), result[1].Date);
Assert.Equal(1, result[1].LessonNumber);
Assert.Equal(new DateOnly(2025, 9, 8), result[2].Date);
Assert.Equal(2, result[2].LessonNumber);
}
// ── ShorthandCodeRepository ───────────────────────────────────────────────
[Fact]
public void ShorthandCodeRepository_Save_LehntDuplikatCodeAb()
{
using var db = NewInMemoryContext();
var repo = new ShorthandCodeRepository(db);
repo.Save(new ShorthandCode { Code = "Tb", Label = "Tafelbild" });
Assert.Throws<InvalidOperationException>(() =>
repo.Save(new ShorthandCode { Code = "tb", Label = "Anderes Tafelbild" }));
}
[Fact]
public void ShorthandCodeRepository_GetAll_SortiertNachCode()
{
using var db = NewInMemoryContext();
var repo = new ShorthandCodeRepository(db);
repo.Save(new ShorthandCode { Code = "SH", Label = "Schülerheft" });
repo.Save(new ShorthandCode { Code = "AB", Label = "Arbeitsblatt" });
var result = repo.GetAll();
Assert.Equal(["AB", "SH"], result.Select(c => c.Code));
}
[Fact]
public void ShorthandCodeRepository_Delete_EntferntEintrag()
{
using var db = NewInMemoryContext();
var repo = new ShorthandCodeRepository(db);
repo.Save(new ShorthandCode { Code = "Tb", Label = "Tafelbild" });
var id = repo.GetAll().Single().Id;
repo.Delete(id);
Assert.Empty(repo.GetAll());
}
}