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:
2026-08-14 01:53:12 +02:00
co-authored by Claude Sonnet 5
parent de6ea001e7
commit ee1a47641e
22 changed files with 904 additions and 33 deletions
+48 -1
View File
@@ -11,7 +11,7 @@ public class LiteDbContext : IDisposable
/// Aktuelle Schema-Version. Migrationsschritte werden versioniert unter
/// <see cref="RunVersionedMigrations"/> ergänzt, statt bei jedem Start erneut
/// (idempotent, aber unnötig) über alle Daten zu laufen.
private const int CurrentSchemaVersion = 3;
private const int CurrentSchemaVersion = 4;
private readonly LiteDatabase _db;
@@ -56,6 +56,7 @@ public class LiteDbContext : IDisposable
public ILiteCollection<Subject> Subjects => _db.GetCollection<Subject>("subjects");
public ILiteCollection<CompetencyDomain> CompetencyDomains => _db.GetCollection<CompetencyDomain>("competency_domains");
public ILiteCollection<ShorthandCode> ShorthandCodes => _db.GetCollection<ShorthandCode>("shorthand_codes");
public ILiteCollection<AlternativeLessonPath> AlternativeLessonPaths => _db.GetCollection<AlternativeLessonPath>("alternative_lesson_paths");
public void Checkpoint() => _db.Checkpoint();
@@ -94,6 +95,11 @@ public class LiteDbContext : IDisposable
MigrateLessonShorthand();
version = 3;
}
if (version < 4)
{
MigrateLessonAlternativePaths();
version = 4;
}
WriteSchemaVersion(version);
}
@@ -258,6 +264,46 @@ public class LiteDbContext : IDisposable
}
}
/// Führt das anfangs freie Textfeld für den alternativen Ablauf einer Phase
/// (<c>AlternativePath</c>, String) in einen Verweis auf einen Katalogeintrag
/// (<see cref="LessonPhaseStep.AlternativePathId"/>) über — pro bisher verwendetem, distinktem
/// Namen wird ein <see cref="AlternativeLessonPath"/> angelegt (bzw. ein gleichnamiger
/// wiederverwendet) und referenziert. Die neue Collection ist von dieser Migration selbst nicht
/// betroffen (keine Altstruktur), daher direkter Zugriff über die typisierte Collection.
private void MigrateLessonAlternativePaths()
{
var lessons = _db.GetCollection<BsonDocument>("lessons");
var nameToId = new Dictionary<string, Guid>(StringComparer.OrdinalIgnoreCase);
foreach (var existing in AlternativeLessonPaths.FindAll()) nameToId[existing.Name] = existing.Id;
foreach (var lesson in lessons.FindAll().ToList())
{
if (!lesson.TryGetValue(nameof(Lesson.Phases), out var phasesValue) || !phasesValue.IsArray) continue;
var changed = false;
foreach (var phaseValue in phasesValue.AsArray)
{
if (phaseValue is not BsonDocument phase) continue;
if (!phase.TryGetValue("AlternativePath", out var oldVal) || !oldVal.IsString) continue;
var name = oldVal.AsString.Trim();
phase.Remove("AlternativePath");
changed = true;
if (name.Length == 0) continue;
if (!nameToId.TryGetValue(name, out var id))
{
var entry = new AlternativeLessonPath { Name = name };
AlternativeLessonPaths.Insert(entry);
id = entry.Id;
nameToId[name] = id;
}
phase[nameof(LessonPhaseStep.AlternativePathId)] = id;
}
if (changed) lessons.Update(lesson);
}
}
private void RemoveRedundantLegacyFields()
{
RemoveFields("exams", "Subject");
@@ -322,6 +368,7 @@ public class LiteDbContext : IDisposable
CompetencyDomains.EnsureIndex(x => x.SubjectId);
CompetencyDomains.EnsureIndex(x => x.GradeLevel);
ShorthandCodes.EnsureIndex("ux_shorthand_code", BsonExpression.Create("LOWER(TRIM($.Code))"), unique: true);
AlternativeLessonPaths.EnsureIndex("ux_alt_lesson_path_name", BsonExpression.Create("LOWER(TRIM($.Name))"), unique: true);
}
public void Dispose() => _db.Dispose();
@@ -367,6 +367,25 @@ public class ShorthandCodeRepository(LiteDbContext db) : IShorthandCodeRepositor
public void Delete(Guid id) => db.ShorthandCodes.Delete(id);
}
public class AlternativeLessonPathRepository(LiteDbContext db) : IAlternativeLessonPathRepository
{
public List<AlternativeLessonPath> GetAll() => db.AlternativeLessonPaths.FindAll().OrderBy(p => p.Name).ToList();
public AlternativeLessonPath? GetById(Guid id) => db.AlternativeLessonPaths.FindById(id);
public void Save(AlternativeLessonPath p)
{
p.Name = p.Name.Trim();
p.Description = string.IsNullOrWhiteSpace(p.Description) ? null : p.Description.Trim();
if (p.Name.Length == 0) throw new ArgumentException("Der Name darf nicht leer sein.");
var duplicate = db.AlternativeLessonPaths.FindAll()
.FirstOrDefault(x => string.Equals(x.Name, p.Name, StringComparison.OrdinalIgnoreCase));
if (duplicate is not null && duplicate.Id != p.Id)
throw new InvalidOperationException("Ein alternativer Ablauf mit diesem Namen existiert bereits.");
p.UpdatedAt = DateTime.UtcNow;
db.AlternativeLessonPaths.Upsert(p);
}
public void Delete(Guid id) => db.AlternativeLessonPaths.Delete(id);
}
public class CompetencyDomainRepository(LiteDbContext db) : ICompetencyDomainRepository
{
public List<CompetencyDomain> GetBySubjectAndGrade(Guid subjectId, int gradeLevel) =>