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:
@@ -163,3 +163,10 @@ public interface IShorthandCodeRepository
|
|||||||
void Save(ShorthandCode code);
|
void Save(ShorthandCode code);
|
||||||
void Delete(Guid id);
|
void Delete(Guid id);
|
||||||
}
|
}
|
||||||
|
public interface IAlternativeLessonPathRepository
|
||||||
|
{
|
||||||
|
List<AlternativeLessonPath> GetAll();
|
||||||
|
AlternativeLessonPath? GetById(Guid id);
|
||||||
|
void Save(AlternativeLessonPath path);
|
||||||
|
void Delete(Guid id);
|
||||||
|
}
|
||||||
|
|||||||
@@ -79,11 +79,37 @@ public class LessonPhaseStep
|
|||||||
public string Activity { get; set; } = "";
|
public string Activity { get; set; } = "";
|
||||||
public string Material { get; set; } = "";
|
public string Material { get; set; } = "";
|
||||||
public string Shorthand { get; set; } = "";
|
public string Shorthand { get; set; } = "";
|
||||||
|
/// <summary>
|
||||||
|
/// null = Hauptweg. Sonst Verweis auf einen benannten <see cref="AlternativeLessonPath"/> aus
|
||||||
|
/// dem Katalog, über den Phasen alternativer Unterrichtsverläufe zusammengehören —
|
||||||
|
/// Verlaufsplan-Editor und -Ansicht gruppieren danach. Bewusst kein Fork-Punkt-Bezug zum
|
||||||
|
/// Hauptweg: eine Alternative beginnt in der Anzeige unabhängig ab dem Stundenbeginn, nicht ab
|
||||||
|
/// einer gemeinsamen Verzweigungsstelle — für den schnellen Überblick ausreichend, siehe TODO.md.
|
||||||
|
/// </summary>
|
||||||
|
public Guid? AlternativePathId { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum UnitStatus { Planned, Active, Completed }
|
public enum UnitStatus { Planned, Active, Completed }
|
||||||
public enum LessonStatus { Planned, Conducted }
|
public enum LessonStatus { Planned, Conducted }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Katalogeintrag für einen wiederverwendbaren "alternativen Ablauf" (z.B. "Kurzversion" bei
|
||||||
|
/// Zeitmangel), dem einzelne <see cref="LessonPhaseStep"/>-Zeilen über
|
||||||
|
/// <see cref="LessonPhaseStep.AlternativePathId"/> zugeordnet werden. Wird direkt beim Zuweisen im
|
||||||
|
/// Verlaufsplan-Editor angelegt (kein separates Verwaltungs-UI in den Einstellungen bisher).
|
||||||
|
/// </summary>
|
||||||
|
public class AlternativeLessonPath
|
||||||
|
{
|
||||||
|
public Guid Id { get; set; } = Guid.NewGuid();
|
||||||
|
public string Name { get; set; } = "";
|
||||||
|
/// Freitext, wann/wofür dieser Ablauf gedacht ist, z.B. "Bei Zeitmangel, wenn Aufgabe 3 nicht
|
||||||
|
/// mehr passt." Absichtlich kein strukturiertes Bedingungsfeld (z.B. "wenn an Stelle X weniger
|
||||||
|
/// als Y Minuten übrig sind") — das setzt eine Live-Zeiterfassung während des Unterrichtens
|
||||||
|
/// voraus, die es noch nicht gibt (siehe Live-Unterrichtsmodus-Ideensammlung in TODO.md).
|
||||||
|
public string? Description { get; set; }
|
||||||
|
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Zeugnisnote eines Schülers in einer Lerngruppe für einen Zeitraum (Halbjahr/Gesamtjahr).
|
/// Zeugnisnote eines Schülers in einer Lerngruppe für einen Zeitraum (Halbjahr/Gesamtjahr).
|
||||||
/// <see cref="CalculatedValue"/> ist das zuletzt berechnete Ergebnis; <see cref="OverrideValue"/>
|
/// <see cref="CalculatedValue"/> ist das zuletzt berechnete Ergebnis; <see cref="OverrideValue"/>
|
||||||
|
|||||||
@@ -121,7 +121,7 @@ public sealed class LiteDbContextTests
|
|||||||
using var temp = new TempDatabase();
|
using var temp = new TempDatabase();
|
||||||
using var context = new LiteDbContext(temp.Path);
|
using var context = new LiteDbContext(temp.Path);
|
||||||
|
|
||||||
Assert.Equal(3, context.SchemaVersion);
|
Assert.Equal(4, context.SchemaVersion);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -132,7 +132,7 @@ public sealed class LiteDbContextTests
|
|||||||
|
|
||||||
using var second = new LiteDbContext(temp.Path);
|
using var second = new LiteDbContext(temp.Path);
|
||||||
|
|
||||||
Assert.Equal(3, second.SchemaVersion);
|
Assert.Equal(4, second.SchemaVersion);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -224,6 +224,60 @@ public sealed class LiteDbContextTests
|
|||||||
Assert.False(rawPhases[0].AsDocument.ContainsKey("ShorthandTo"));
|
Assert.False(rawPhases[0].AsDocument.ContainsKey("ShorthandTo"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void MigrateLessonAlternativePaths_ErzeugtKatalogEintraegeUndVerweistDarauf()
|
||||||
|
{
|
||||||
|
using var temp = new TempDatabase();
|
||||||
|
var lesson1Id = Guid.NewGuid();
|
||||||
|
var lesson2Id = Guid.NewGuid();
|
||||||
|
|
||||||
|
using (var legacy = new LiteDatabase(temp.Path))
|
||||||
|
{
|
||||||
|
legacy.GetCollection<BsonDocument>("lessons").Insert(new BsonDocument
|
||||||
|
{
|
||||||
|
["_id"] = lesson1Id,
|
||||||
|
[nameof(Lesson.UnitId)] = Guid.NewGuid(),
|
||||||
|
[nameof(Lesson.GroupId)] = Guid.NewGuid(),
|
||||||
|
[nameof(Lesson.Phases)] = new BsonArray
|
||||||
|
{
|
||||||
|
new BsonDocument { ["AlternativePath"] = "Kurzversion" },
|
||||||
|
new BsonDocument { ["AlternativePath"] = "" }, // Hauptweg, kein Katalogeintrag
|
||||||
|
new BsonDocument { }, // gar kein Feld, unangetastet
|
||||||
|
},
|
||||||
|
});
|
||||||
|
legacy.GetCollection<BsonDocument>("lessons").Insert(new BsonDocument
|
||||||
|
{
|
||||||
|
["_id"] = lesson2Id,
|
||||||
|
[nameof(Lesson.UnitId)] = Guid.NewGuid(),
|
||||||
|
[nameof(Lesson.GroupId)] = Guid.NewGuid(),
|
||||||
|
[nameof(Lesson.Phases)] = new BsonArray
|
||||||
|
{
|
||||||
|
new BsonDocument { ["AlternativePath"] = "Kurzversion" }, // gleicher Name -> gleicher Katalogeintrag
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
using (var context = new LiteDbContext(temp.Path))
|
||||||
|
{
|
||||||
|
var catalog = context.AlternativeLessonPaths.FindAll().ToList();
|
||||||
|
var entry = Assert.Single(catalog);
|
||||||
|
Assert.Equal("Kurzversion", entry.Name);
|
||||||
|
|
||||||
|
var lesson1 = context.Lessons.FindById(lesson1Id)!;
|
||||||
|
Assert.Equal(entry.Id, lesson1.Phases[0].AlternativePathId);
|
||||||
|
Assert.Null(lesson1.Phases[1].AlternativePathId);
|
||||||
|
Assert.Null(lesson1.Phases[2].AlternativePathId);
|
||||||
|
|
||||||
|
var lesson2 = context.Lessons.FindById(lesson2Id)!;
|
||||||
|
Assert.Equal(entry.Id, lesson2.Phases[0].AlternativePathId); // wiederverwendeter Katalogeintrag
|
||||||
|
}
|
||||||
|
|
||||||
|
using var migrated = new LiteDatabase(temp.Path);
|
||||||
|
var rawLesson1 = migrated.GetCollection<BsonDocument>("lessons").FindById(lesson1Id);
|
||||||
|
var rawPhases = rawLesson1["Phases"].AsArray;
|
||||||
|
Assert.False(rawPhases[0].AsDocument.ContainsKey("AlternativePath"));
|
||||||
|
}
|
||||||
|
|
||||||
private sealed class TempDatabase : IDisposable
|
private sealed class TempDatabase : IDisposable
|
||||||
{
|
{
|
||||||
private readonly string _directory = System.IO.Path.Combine(
|
private readonly string _directory = System.IO.Path.Combine(
|
||||||
|
|||||||
@@ -488,4 +488,45 @@ public sealed class RepositoryTests
|
|||||||
|
|
||||||
Assert.Empty(repo.GetAll());
|
Assert.Empty(repo.GetAll());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── AlternativeLessonPathRepository ───────────────────────────────────────
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AlternativeLessonPathRepository_Save_LehntDuplikatNamenAb()
|
||||||
|
{
|
||||||
|
using var db = NewInMemoryContext();
|
||||||
|
var repo = new AlternativeLessonPathRepository(db);
|
||||||
|
repo.Save(new AlternativeLessonPath { Name = "Kurzversion" });
|
||||||
|
|
||||||
|
Assert.Throws<InvalidOperationException>(() =>
|
||||||
|
repo.Save(new AlternativeLessonPath { Name = "kurzversion" }));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AlternativeLessonPathRepository_GetById_FindetGespeichertenEintragMitBeschreibung()
|
||||||
|
{
|
||||||
|
using var db = NewInMemoryContext();
|
||||||
|
var repo = new AlternativeLessonPathRepository(db);
|
||||||
|
var entry = new AlternativeLessonPath { Name = "Kurzversion", Description = "Bei Zeitmangel." };
|
||||||
|
repo.Save(entry);
|
||||||
|
|
||||||
|
var found = repo.GetById(entry.Id);
|
||||||
|
|
||||||
|
Assert.NotNull(found);
|
||||||
|
Assert.Equal("Kurzversion", found!.Name);
|
||||||
|
Assert.Equal("Bei Zeitmangel.", found.Description);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AlternativeLessonPathRepository_Delete_EntferntEintrag()
|
||||||
|
{
|
||||||
|
using var db = NewInMemoryContext();
|
||||||
|
var repo = new AlternativeLessonPathRepository(db);
|
||||||
|
repo.Save(new AlternativeLessonPath { Name = "Kurzversion" });
|
||||||
|
var id = repo.GetAll().Single().Id;
|
||||||
|
|
||||||
|
repo.Delete(id);
|
||||||
|
|
||||||
|
Assert.Empty(repo.GetAll());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ public class LiteDbContext : IDisposable
|
|||||||
/// Aktuelle Schema-Version. Migrationsschritte werden versioniert unter
|
/// Aktuelle Schema-Version. Migrationsschritte werden versioniert unter
|
||||||
/// <see cref="RunVersionedMigrations"/> ergänzt, statt bei jedem Start erneut
|
/// <see cref="RunVersionedMigrations"/> ergänzt, statt bei jedem Start erneut
|
||||||
/// (idempotent, aber unnötig) über alle Daten zu laufen.
|
/// (idempotent, aber unnötig) über alle Daten zu laufen.
|
||||||
private const int CurrentSchemaVersion = 3;
|
private const int CurrentSchemaVersion = 4;
|
||||||
|
|
||||||
private readonly LiteDatabase _db;
|
private readonly LiteDatabase _db;
|
||||||
|
|
||||||
@@ -56,6 +56,7 @@ public class LiteDbContext : IDisposable
|
|||||||
public ILiteCollection<Subject> Subjects => _db.GetCollection<Subject>("subjects");
|
public ILiteCollection<Subject> Subjects => _db.GetCollection<Subject>("subjects");
|
||||||
public ILiteCollection<CompetencyDomain> CompetencyDomains => _db.GetCollection<CompetencyDomain>("competency_domains");
|
public ILiteCollection<CompetencyDomain> CompetencyDomains => _db.GetCollection<CompetencyDomain>("competency_domains");
|
||||||
public ILiteCollection<ShorthandCode> ShorthandCodes => _db.GetCollection<ShorthandCode>("shorthand_codes");
|
public ILiteCollection<ShorthandCode> ShorthandCodes => _db.GetCollection<ShorthandCode>("shorthand_codes");
|
||||||
|
public ILiteCollection<AlternativeLessonPath> AlternativeLessonPaths => _db.GetCollection<AlternativeLessonPath>("alternative_lesson_paths");
|
||||||
|
|
||||||
public void Checkpoint() => _db.Checkpoint();
|
public void Checkpoint() => _db.Checkpoint();
|
||||||
|
|
||||||
@@ -94,6 +95,11 @@ public class LiteDbContext : IDisposable
|
|||||||
MigrateLessonShorthand();
|
MigrateLessonShorthand();
|
||||||
version = 3;
|
version = 3;
|
||||||
}
|
}
|
||||||
|
if (version < 4)
|
||||||
|
{
|
||||||
|
MigrateLessonAlternativePaths();
|
||||||
|
version = 4;
|
||||||
|
}
|
||||||
WriteSchemaVersion(version);
|
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()
|
private void RemoveRedundantLegacyFields()
|
||||||
{
|
{
|
||||||
RemoveFields("exams", "Subject");
|
RemoveFields("exams", "Subject");
|
||||||
@@ -322,6 +368,7 @@ public class LiteDbContext : IDisposable
|
|||||||
CompetencyDomains.EnsureIndex(x => x.SubjectId);
|
CompetencyDomains.EnsureIndex(x => x.SubjectId);
|
||||||
CompetencyDomains.EnsureIndex(x => x.GradeLevel);
|
CompetencyDomains.EnsureIndex(x => x.GradeLevel);
|
||||||
ShorthandCodes.EnsureIndex("ux_shorthand_code", BsonExpression.Create("LOWER(TRIM($.Code))"), unique: true);
|
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();
|
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 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 class CompetencyDomainRepository(LiteDbContext db) : ICompetencyDomainRepository
|
||||||
{
|
{
|
||||||
public List<CompetencyDomain> GetBySubjectAndGrade(Guid subjectId, int gradeLevel) =>
|
public List<CompetencyDomain> GetBySubjectAndGrade(Guid subjectId, int gradeLevel) =>
|
||||||
|
|||||||
@@ -198,6 +198,14 @@ public class FakeShorthandCodes(List<ShorthandCode> all) : IShorthandCodeReposit
|
|||||||
public void Delete(Guid id) { }
|
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
|
public class FakeReportGrades : IReportGradeRepository
|
||||||
{
|
{
|
||||||
private readonly List<ReportGrade> _all = [];
|
private readonly List<ReportGrade> _all = [];
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ namespace LehrerApp.Desktop.Tests;
|
|||||||
public sealed class LessonDialogViewModelTests
|
public sealed class LessonDialogViewModelTests
|
||||||
{
|
{
|
||||||
private static LessonDialogViewModel BuildVm(Guid unitId, Guid groupId, Lesson? editing = null) =>
|
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]
|
[Fact]
|
||||||
public void AddPhase_FuegtZeileMitStandardwertenHinzu()
|
public void AddPhase_FuegtZeileMitStandardwertenHinzu()
|
||||||
@@ -18,6 +19,7 @@ public sealed class LessonDialogViewModelTests
|
|||||||
|
|
||||||
Assert.Single(vm.Phases);
|
Assert.Single(vm.Phases);
|
||||||
Assert.Equal(5, vm.Phases[0].DurationMinutes);
|
Assert.Equal(5, vm.Phases[0].DurationMinutes);
|
||||||
|
Assert.False(vm.Phases[0].HasAlternativePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -68,8 +70,8 @@ public sealed class LessonDialogViewModelTests
|
|||||||
public void ShorthandSuggestions_KombiniertKatalogUndBisherigeStundenwerte()
|
public void ShorthandSuggestions_KombiniertKatalogUndBisherigeStundenwerte()
|
||||||
{
|
{
|
||||||
var codes = new FakeShorthandCodes([new ShorthandCode { Code = "Tb" }, new ShorthandCode { Code = "SH" }]);
|
var codes = new FakeShorthandCodes([new ShorthandCode { Code = "Tb" }, new ShorthandCode { Code = "SH" }]);
|
||||||
var vm = new LessonDialogViewModel(new FakeLessons(), codes, Guid.NewGuid(), Guid.NewGuid(),
|
var vm = new LessonDialogViewModel(new FakeLessons(), codes, new FakeAlternativeLessonPaths([]),
|
||||||
[], ["Plenum", "LDE", "Tb"], null); // "Tb" doppelt (Katalog + Historie), soll nur einmal erscheinen
|
Guid.NewGuid(), Guid.NewGuid(), [], ["Plenum", "LDE", "Tb"], null); // "Tb" doppelt (Katalog + Historie), soll nur einmal erscheinen
|
||||||
|
|
||||||
Assert.Equal(["LDE", "Plenum", "SH", "Tb"], vm.ShorthandSuggestions);
|
Assert.Equal(["LDE", "Plenum", "SH", "Tb"], vm.ShorthandSuggestions);
|
||||||
}
|
}
|
||||||
@@ -130,7 +132,8 @@ public sealed class LessonDialogViewModelTests
|
|||||||
var unitId = Guid.NewGuid();
|
var unitId = Guid.NewGuid();
|
||||||
var groupId = Guid.NewGuid();
|
var groupId = Guid.NewGuid();
|
||||||
var lessons = new FakeLessons();
|
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",
|
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.Equal("L->AB01", vm.Result.Phases[0].Shorthand);
|
||||||
Assert.Single(lessons.GetByUnit(unitId));
|
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 (vm, units, lessons, sourceGroupId) = BuildScenario();
|
||||||
var targetGroupId = Guid.NewGuid();
|
var targetGroupId = Guid.NewGuid();
|
||||||
|
var kurzversionId = Guid.NewGuid(); // Katalog-Referenz, bleibt beim Kopieren unverändert
|
||||||
|
|
||||||
var unit = new Unit
|
var unit = new Unit
|
||||||
{
|
{
|
||||||
@@ -111,7 +112,7 @@ public class PlanningTabViewModelTests
|
|||||||
UnitId = unit.Id, GroupId = sourceGroupId, Date = new DateOnly(2025, 9, 1),
|
UnitId = unit.Id, GroupId = sourceGroupId, Date = new DateOnly(2025, 9, 1),
|
||||||
Topic = "Einführung", Status = LessonStatus.Conducted, Reflection = "lief gut",
|
Topic = "Einführung", Status = LessonStatus.Conducted, Reflection = "lief gut",
|
||||||
StartTime = new TimeOnly(11, 45),
|
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
|
var l2 = new Lesson
|
||||||
{
|
{
|
||||||
@@ -144,6 +145,7 @@ public class PlanningTabViewModelTests
|
|||||||
Assert.Single(copiedLessons[0].Phases);
|
Assert.Single(copiedLessons[0].Phases);
|
||||||
Assert.Equal("Einstieg", copiedLessons[0].Phases[0].Name);
|
Assert.Equal("Einstieg", copiedLessons[0].Phases[0].Name);
|
||||||
Assert.Equal("L->AB01", copiedLessons[0].Phases[0].Shorthand);
|
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
|
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.
|
// Ursprüngliche Einheit/Stunden bleiben unverändert in der Quellgruppe.
|
||||||
|
|||||||
@@ -128,6 +128,7 @@ public static class AppBootstrapper
|
|||||||
services.AddSingleton<ISubjectRepository, SubjectRepository>();
|
services.AddSingleton<ISubjectRepository, SubjectRepository>();
|
||||||
services.AddSingleton<ICompetencyDomainRepository, CompetencyDomainRepository>();
|
services.AddSingleton<ICompetencyDomainRepository, CompetencyDomainRepository>();
|
||||||
services.AddSingleton<IShorthandCodeRepository, ShorthandCodeRepository>();
|
services.AddSingleton<IShorthandCodeRepository, ShorthandCodeRepository>();
|
||||||
|
services.AddSingleton<IAlternativeLessonPathRepository, AlternativeLessonPathRepository>();
|
||||||
services.AddSingleton<IAttachmentStorage, LiteAttachmentStorage>();
|
services.AddSingleton<IAttachmentStorage, LiteAttachmentStorage>();
|
||||||
|
|
||||||
// ── Services ──────────────────────────────────────────────────────────
|
// ── Services ──────────────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -41,7 +41,9 @@ public partial class PlanningTabViewModel : ObservableObject
|
|||||||
public ObservableCollection<LessonSummary> Lessons { get; } = [];
|
public ObservableCollection<LessonSummary> Lessons { get; } = [];
|
||||||
|
|
||||||
/// Aus den Material-/Kurzsymbol-Werten aller bereits vorhandenen Stunden-Phasen der Gruppe
|
/// Aus den Material-/Kurzsymbol-Werten aller bereits vorhandenen Stunden-Phasen der Gruppe
|
||||||
/// zusammengestellt (4.2.2 Autovervollständigung im Verlaufsplan-Editor).
|
/// zusammengestellt (4.2.2 Autovervollständigung im Verlaufsplan-Editor). Alternative Abläufe
|
||||||
|
/// kommen seit dem Katalog-Redesign nicht mehr aus der Stundenhistorie, sondern direkt aus
|
||||||
|
/// IAlternativeLessonPathRepository (siehe LessonDialogViewModel).
|
||||||
public List<string> KnownMaterials { get; private set; } = [];
|
public List<string> KnownMaterials { get; private set; } = [];
|
||||||
public List<string> KnownShorthands { get; private set; } = [];
|
public List<string> KnownShorthands { get; private set; } = [];
|
||||||
|
|
||||||
@@ -53,6 +55,7 @@ public partial class PlanningTabViewModel : ObservableObject
|
|||||||
public Func<Lesson, List<string>, List<string>, Task<bool>>? OnEditLesson { get; set; }
|
public Func<Lesson, List<string>, List<string>, Task<bool>>? OnEditLesson { get; set; }
|
||||||
public Func<LessonSummary, Task<bool>>? OnConfirmDeleteLesson { get; set; }
|
public Func<LessonSummary, Task<bool>>? OnConfirmDeleteLesson { get; set; }
|
||||||
public Func<Lesson, Task<MoveLessonTarget?>>? OnPickMoveTarget { get; set; }
|
public Func<Lesson, Task<MoveLessonTarget?>>? OnPickMoveTarget { get; set; }
|
||||||
|
public Func<Lesson, Task>? OnShowLesson { get; set; }
|
||||||
|
|
||||||
public PlanningTabViewModel(IUnitRepository units, ILessonRepository lessons,
|
public PlanningTabViewModel(IUnitRepository units, ILessonRepository lessons,
|
||||||
IGroupRepository groups, ISubjectRepository subjects,
|
IGroupRepository groups, ISubjectRepository subjects,
|
||||||
@@ -120,6 +123,7 @@ public partial class PlanningTabViewModel : ObservableObject
|
|||||||
|
|
||||||
partial void OnSelectedLessonChanged(LessonSummary? value)
|
partial void OnSelectedLessonChanged(LessonSummary? value)
|
||||||
{
|
{
|
||||||
|
ShowLessonCommand.NotifyCanExecuteChanged();
|
||||||
EditLessonCommand.NotifyCanExecuteChanged();
|
EditLessonCommand.NotifyCanExecuteChanged();
|
||||||
DeleteLessonCommand.NotifyCanExecuteChanged();
|
DeleteLessonCommand.NotifyCanExecuteChanged();
|
||||||
MoveLessonCommand.NotifyCanExecuteChanged();
|
MoveLessonCommand.NotifyCanExecuteChanged();
|
||||||
@@ -209,6 +213,7 @@ public partial class PlanningTabViewModel : ObservableObject
|
|||||||
Activity = p.Activity,
|
Activity = p.Activity,
|
||||||
Material = p.Material,
|
Material = p.Material,
|
||||||
Shorthand = p.Shorthand,
|
Shorthand = p.Shorthand,
|
||||||
|
AlternativePathId = p.AlternativePathId,
|
||||||
})],
|
})],
|
||||||
Homework = lesson.Homework,
|
Homework = lesson.Homework,
|
||||||
Reflection = null,
|
Reflection = null,
|
||||||
@@ -233,6 +238,16 @@ public partial class PlanningTabViewModel : ObservableObject
|
|||||||
if (await OnEditLesson(SelectedLesson.Model, KnownMaterials, KnownShorthands)) LoadUnits();
|
if (await OnEditLesson(SelectedLesson.Model, KnownMaterials, KnownShorthands)) LoadUnits();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Schreibgeschützte Anzeige des Verlaufsplans für den Einsatz im Unterricht (kein
|
||||||
|
/// Bearbeitungsrisiko). Bewusst ohne Live-Anpassung (Verlängern/Verschieben während des
|
||||||
|
/// Haltens) — das gehört zur zurückgestellten Live-Unterrichtsmodus-Ideensammlung (TODO.md).
|
||||||
|
[RelayCommand(CanExecute = nameof(HasSelectedLesson))]
|
||||||
|
private async Task ShowLesson()
|
||||||
|
{
|
||||||
|
if (OnShowLesson is null || SelectedLesson is null) return;
|
||||||
|
await OnShowLesson(SelectedLesson.Model);
|
||||||
|
}
|
||||||
|
|
||||||
[RelayCommand(CanExecute = nameof(HasSelectedLesson))]
|
[RelayCommand(CanExecute = nameof(HasSelectedLesson))]
|
||||||
private async Task DeleteLesson()
|
private async Task DeleteLesson()
|
||||||
{
|
{
|
||||||
@@ -542,6 +557,7 @@ public partial class UnitDialogViewModel : ObservableObject
|
|||||||
public partial class LessonDialogViewModel : ObservableObject
|
public partial class LessonDialogViewModel : ObservableObject
|
||||||
{
|
{
|
||||||
private readonly ILessonRepository _lessons;
|
private readonly ILessonRepository _lessons;
|
||||||
|
private readonly IAlternativeLessonPathRepository _alternativePaths;
|
||||||
private readonly Guid _unitId;
|
private readonly Guid _unitId;
|
||||||
private readonly Guid _groupId;
|
private readonly Guid _groupId;
|
||||||
private readonly Lesson? _editingLesson;
|
private readonly Lesson? _editingLesson;
|
||||||
@@ -563,15 +579,21 @@ public partial class LessonDialogViewModel : ObservableObject
|
|||||||
public string[] ShorthandSuggestions { get; }
|
public string[] ShorthandSuggestions { get; }
|
||||||
public ObservableCollection<PhaseStepEditItem> Phases { get; } = [];
|
public ObservableCollection<PhaseStepEditItem> Phases { get; } = [];
|
||||||
|
|
||||||
|
/// Vom Code-Behind gesetzt (Fenster als Owner für den Zuweisen-Dialog): fragt nach dem
|
||||||
|
/// alternativen Ablauf, dem eine Phase zugeordnet werden soll (Auswahl oder Neuanlage
|
||||||
|
/// per Combobox-Dialog). null zurückgegeben = abgebrochen.
|
||||||
|
public Func<Guid?, Task<AlternativeLessonPath?>>? OnPickAlternativePath { get; set; }
|
||||||
|
|
||||||
public Lesson? Result { get; private set; }
|
public Lesson? Result { get; private set; }
|
||||||
public string DialogTitle => _editingLesson is null ? "Neue Stunde anlegen" : "Stunde bearbeiten";
|
public string DialogTitle => _editingLesson is null ? "Neue Stunde anlegen" : "Stunde bearbeiten";
|
||||||
public string SaveButtonText => _editingLesson is null ? "Anlegen" : "Speichern";
|
public string SaveButtonText => _editingLesson is null ? "Anlegen" : "Speichern";
|
||||||
|
|
||||||
public LessonDialogViewModel(ILessonRepository lessons, IShorthandCodeRepository shorthandCodes,
|
public LessonDialogViewModel(ILessonRepository lessons, IShorthandCodeRepository shorthandCodes,
|
||||||
Guid unitId, Guid groupId, List<string> materialSuggestions, List<string> shorthandHistorySuggestions,
|
IAlternativeLessonPathRepository alternativePaths, Guid unitId, Guid groupId,
|
||||||
Lesson? editingLesson)
|
List<string> materialSuggestions, List<string> shorthandHistorySuggestions, Lesson? editingLesson)
|
||||||
{
|
{
|
||||||
_lessons = lessons; _unitId = unitId; _groupId = groupId; _editingLesson = editingLesson;
|
_lessons = lessons; _alternativePaths = alternativePaths;
|
||||||
|
_unitId = unitId; _groupId = groupId; _editingLesson = editingLesson;
|
||||||
MaterialSuggestions = [.. materialSuggestions];
|
MaterialSuggestions = [.. materialSuggestions];
|
||||||
|
|
||||||
// Vorschläge kommen sowohl aus dem gepflegten Kürzel-Katalog (Einstellungen) als auch aus
|
// Vorschläge kommen sowohl aus dem gepflegten Kürzel-Katalog (Einstellungen) als auch aus
|
||||||
@@ -620,9 +642,19 @@ public partial class LessonDialogViewModel : ObservableObject
|
|||||||
item.OnRemove = RemovePhase;
|
item.OnRemove = RemovePhase;
|
||||||
item.OnMoveUp = MovePhaseUp;
|
item.OnMoveUp = MovePhaseUp;
|
||||||
item.OnMoveDown = MovePhaseDown;
|
item.OnMoveDown = MovePhaseDown;
|
||||||
|
item.OnAssignAlternativePath = AssignAlternativePath;
|
||||||
|
if (source?.AlternativePathId is Guid pathId)
|
||||||
|
item.SetAlternativePath(_alternativePaths.GetById(pathId));
|
||||||
Phases.Add(item);
|
Phases.Add(item);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task AssignAlternativePath(PhaseStepEditItem item)
|
||||||
|
{
|
||||||
|
if (OnPickAlternativePath is null) { item.SetAlternativePath(null); return; }
|
||||||
|
var picked = await OnPickAlternativePath(item.AlternativePathId);
|
||||||
|
item.SetAlternativePath(picked);
|
||||||
|
}
|
||||||
|
|
||||||
private void RemovePhase(PhaseStepEditItem item) { Phases.Remove(item); RecomputeTimes(); }
|
private void RemovePhase(PhaseStepEditItem item) { Phases.Remove(item); RecomputeTimes(); }
|
||||||
|
|
||||||
private void MovePhaseUp(PhaseStepEditItem item)
|
private void MovePhaseUp(PhaseStepEditItem item)
|
||||||
@@ -701,6 +733,9 @@ public partial class LessonDialogViewModel : ObservableObject
|
|||||||
|
|
||||||
public partial class PhaseStepEditItem : ObservableObject
|
public partial class PhaseStepEditItem : ObservableObject
|
||||||
{
|
{
|
||||||
|
private static readonly string[] AlternativePathPalette =
|
||||||
|
["#7F77DD", "#1D9E75", "#D85A30", "#D4537E", "#378ADD", "#639922", "#EF9F27"];
|
||||||
|
|
||||||
[ObservableProperty] private string _name = "";
|
[ObservableProperty] private string _name = "";
|
||||||
[ObservableProperty] private int _durationMinutes = 5;
|
[ObservableProperty] private int _durationMinutes = 5;
|
||||||
[ObservableProperty] private string _activity = "";
|
[ObservableProperty] private string _activity = "";
|
||||||
@@ -708,13 +743,53 @@ public partial class PhaseStepEditItem : ObservableObject
|
|||||||
[ObservableProperty] private string _shorthand = "";
|
[ObservableProperty] private string _shorthand = "";
|
||||||
[ObservableProperty] private string _computedTimeDisplay = "";
|
[ObservableProperty] private string _computedTimeDisplay = "";
|
||||||
|
|
||||||
|
/// Checkbox-Zustand im Editor: unchecked→checked öffnet den Zuweisen-Dialog
|
||||||
|
/// (<see cref="OnAssignAlternativePath"/>); checked→unchecked entfernt die Zuordnung.
|
||||||
|
/// Änderungen, die von <see cref="SetAlternativePath"/> selbst kommen, lösen das nicht erneut aus.
|
||||||
|
[ObservableProperty] private bool _hasAlternativePath;
|
||||||
|
[ObservableProperty] private string _alternativePathName = "";
|
||||||
|
[ObservableProperty] private string _alternativePathColorHex = "#9E9E9E";
|
||||||
|
|
||||||
|
private bool _suppressAlternativePathToggle;
|
||||||
|
|
||||||
|
public Guid? AlternativePathId { get; private set; }
|
||||||
|
|
||||||
public Action? OnChanged { get; set; }
|
public Action? OnChanged { get; set; }
|
||||||
public Action<PhaseStepEditItem>? OnRemove { get; set; }
|
public Action<PhaseStepEditItem>? OnRemove { get; set; }
|
||||||
public Action<PhaseStepEditItem>? OnMoveUp { get; set; }
|
public Action<PhaseStepEditItem>? OnMoveUp { get; set; }
|
||||||
public Action<PhaseStepEditItem>? OnMoveDown { get; set; }
|
public Action<PhaseStepEditItem>? OnMoveDown { get; set; }
|
||||||
|
public Func<PhaseStepEditItem, Task>? OnAssignAlternativePath { get; set; }
|
||||||
|
|
||||||
partial void OnDurationMinutesChanged(int value) => OnChanged?.Invoke();
|
partial void OnDurationMinutesChanged(int value) => OnChanged?.Invoke();
|
||||||
|
|
||||||
|
partial void OnHasAlternativePathChanged(bool value)
|
||||||
|
{
|
||||||
|
if (_suppressAlternativePathToggle) return;
|
||||||
|
if (value) _ = OnAssignAlternativePath?.Invoke(this);
|
||||||
|
else SetAlternativePath(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Wird sowohl beim Laden einer bestehenden Zuordnung als auch nach dem Zuweisen-Dialog
|
||||||
|
/// aufgerufen (auch mit null bei Abbruch/Entfernen) — setzt Id/Anzeigename/Farbe konsistent
|
||||||
|
/// und unterdrückt dabei das erneute Öffnen des Dialogs über <see cref="OnHasAlternativePathChanged"/>.
|
||||||
|
public void SetAlternativePath(AlternativeLessonPath? path)
|
||||||
|
{
|
||||||
|
_suppressAlternativePathToggle = true;
|
||||||
|
AlternativePathId = path?.Id;
|
||||||
|
AlternativePathName = path?.Name ?? "";
|
||||||
|
AlternativePathColorHex = path is null ? "#9E9E9E" : ColorFor(path.Name);
|
||||||
|
HasAlternativePath = path is not null;
|
||||||
|
_suppressAlternativePathToggle = false;
|
||||||
|
OnChanged?.Invoke();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ColorFor(string name)
|
||||||
|
{
|
||||||
|
var hash = 0;
|
||||||
|
foreach (var c in name) hash = hash * 31 + c;
|
||||||
|
return AlternativePathPalette[Math.Abs(hash) % AlternativePathPalette.Length];
|
||||||
|
}
|
||||||
|
|
||||||
[RelayCommand] private void Remove() => OnRemove?.Invoke(this);
|
[RelayCommand] private void Remove() => OnRemove?.Invoke(this);
|
||||||
[RelayCommand] private void MoveUp() => OnMoveUp?.Invoke(this);
|
[RelayCommand] private void MoveUp() => OnMoveUp?.Invoke(this);
|
||||||
[RelayCommand] private void MoveDown() => OnMoveDown?.Invoke(this);
|
[RelayCommand] private void MoveDown() => OnMoveDown?.Invoke(this);
|
||||||
@@ -726,9 +801,61 @@ public partial class PhaseStepEditItem : ObservableObject
|
|||||||
Activity = Activity.Trim(),
|
Activity = Activity.Trim(),
|
||||||
Material = Material.Trim(),
|
Material = Material.Trim(),
|
||||||
Shorthand = Shorthand.Trim(),
|
Shorthand = Shorthand.Trim(),
|
||||||
|
AlternativePathId = AlternativePathId,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Dialog: Alternativen Ablauf zuweisen/anlegen (4.2.2 Nachtrag) ────────────
|
||||||
|
|
||||||
|
public partial class AlternativePathDialogViewModel : ObservableObject
|
||||||
|
{
|
||||||
|
private readonly IAlternativeLessonPathRepository _repo;
|
||||||
|
|
||||||
|
[ObservableProperty] private AlternativeLessonPath? _selectedPath;
|
||||||
|
[ObservableProperty] private string _newName = "";
|
||||||
|
[ObservableProperty] private string _newDescription = "";
|
||||||
|
[ObservableProperty] private string _newNameError = "";
|
||||||
|
[ObservableProperty] private string _selectionError = "";
|
||||||
|
|
||||||
|
public ObservableCollection<AlternativeLessonPath> Available { get; } = [];
|
||||||
|
public AlternativeLessonPath? Result { get; private set; }
|
||||||
|
|
||||||
|
public AlternativePathDialogViewModel(IAlternativeLessonPathRepository repo, Guid? currentId)
|
||||||
|
{
|
||||||
|
_repo = repo;
|
||||||
|
foreach (var p in repo.GetAll()) Available.Add(p);
|
||||||
|
if (currentId is Guid id) SelectedPath = Available.FirstOrDefault(p => p.Id == id);
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private void CreateNew()
|
||||||
|
{
|
||||||
|
NewNameError = "";
|
||||||
|
if (string.IsNullOrWhiteSpace(NewName)) { NewNameError = "Name erforderlich."; return; }
|
||||||
|
|
||||||
|
var entry = new AlternativeLessonPath
|
||||||
|
{
|
||||||
|
Name = NewName.Trim(),
|
||||||
|
Description = string.IsNullOrWhiteSpace(NewDescription) ? null : NewDescription.Trim(),
|
||||||
|
};
|
||||||
|
try { _repo.Save(entry); }
|
||||||
|
catch (InvalidOperationException ex) { NewNameError = ex.Message; return; }
|
||||||
|
catch (ArgumentException ex) { NewNameError = ex.Message; return; }
|
||||||
|
|
||||||
|
Available.Add(entry);
|
||||||
|
SelectedPath = entry;
|
||||||
|
NewName = ""; NewDescription = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private void Confirm()
|
||||||
|
{
|
||||||
|
SelectionError = "";
|
||||||
|
if (SelectedPath is null) { SelectionError = "Bitte einen Ablauf auswählen oder neu anlegen."; return; }
|
||||||
|
Result = SelectedPath;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ── Dialog: Stunde verschieben (4.2.4) ────────────────────────────────────────
|
// ── Dialog: Stunde verschieben (4.2.4) ────────────────────────────────────────
|
||||||
|
|
||||||
public partial class MoveLessonDialogViewModel : ObservableObject
|
public partial class MoveLessonDialogViewModel : ObservableObject
|
||||||
@@ -794,3 +921,84 @@ public partial class CopyUnitDialogViewModel : ObservableObject
|
|||||||
Result = new CopyUnitTarget(SelectedGroup!.Id, anchor);
|
Result = new CopyUnitTarget(SelectedGroup!.Id, anchor);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Verlaufsplan-Ansicht (schreibgeschützt, für den Unterrichtseinsatz) ──────
|
||||||
|
|
||||||
|
/// Eine Phasen-Zeile in der schreibgeschützten Ansicht — reine Anzeige, keine Bearbeitung.
|
||||||
|
public record PhaseViewItem(string Name, int DurationMinutes, string TimeDisplay,
|
||||||
|
string Activity, string Material, string Shorthand);
|
||||||
|
|
||||||
|
/// Eine Gruppe von Phasen mit demselben <see cref="LessonPhaseStep.AlternativePathId"/>
|
||||||
|
/// ("Hauptweg" bei null). Jede Gruppe bekommt ihre eigene kumulierte Zeitberechnung ab
|
||||||
|
/// Lesson.StartTime — eine Alternative zeigt also "so würde die Uhr laufen, wenn man diesen Weg
|
||||||
|
/// von Stundenbeginn an nimmt", nicht ab einer gemeinsamen Verzweigungsstelle im Hauptweg.
|
||||||
|
public record PhaseGroupViewItem(string Label, bool IsMainPath, string? Description, List<PhaseViewItem> Phases);
|
||||||
|
|
||||||
|
public class LessonViewerViewModel
|
||||||
|
{
|
||||||
|
private const string MainPathLabel = "Hauptweg";
|
||||||
|
|
||||||
|
public string DateDisplay { get; }
|
||||||
|
public string Topic { get; }
|
||||||
|
public string StatusLabel { get; }
|
||||||
|
public string StartTimeDisplay { get; }
|
||||||
|
public string? Homework { get; }
|
||||||
|
public string? Reflection { get; }
|
||||||
|
public List<PhaseGroupViewItem> PhaseGroups { get; }
|
||||||
|
public bool HasAlternatives { get; }
|
||||||
|
|
||||||
|
public LessonViewerViewModel(Lesson lesson, IAlternativeLessonPathRepository alternativePaths)
|
||||||
|
{
|
||||||
|
DateDisplay = lesson.Date.ToString("dd.MM.yyyy");
|
||||||
|
Topic = lesson.Topic;
|
||||||
|
StatusLabel = LessonStatusDisplay.ToName(lesson.Status);
|
||||||
|
StartTimeDisplay = lesson.StartTime?.ToString("HH:mm") ?? "–";
|
||||||
|
Homework = lesson.Homework;
|
||||||
|
Reflection = lesson.Reflection;
|
||||||
|
|
||||||
|
var order = new List<string>();
|
||||||
|
var descriptions = new Dictionary<string, string?>();
|
||||||
|
var byLabel = new Dictionary<string, List<LessonPhaseStep>>();
|
||||||
|
foreach (var p in lesson.Phases)
|
||||||
|
{
|
||||||
|
string label; string? description = null;
|
||||||
|
if (p.AlternativePathId is Guid pathId)
|
||||||
|
{
|
||||||
|
var path = alternativePaths.GetById(pathId);
|
||||||
|
label = path?.Name ?? "Unbekannter Ablauf";
|
||||||
|
description = path?.Description;
|
||||||
|
}
|
||||||
|
else label = MainPathLabel;
|
||||||
|
|
||||||
|
if (!byLabel.TryGetValue(label, out var list))
|
||||||
|
{
|
||||||
|
list = [];
|
||||||
|
byLabel[label] = list;
|
||||||
|
descriptions[label] = description;
|
||||||
|
order.Add(label);
|
||||||
|
}
|
||||||
|
list.Add(p);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hauptweg immer zuerst, unabhängig davon, in welcher Reihenfolge Phasen angelegt wurden.
|
||||||
|
var orderedLabels = order.OrderBy(l => l == MainPathLabel ? 0 : 1).ToList();
|
||||||
|
PhaseGroups = orderedLabels
|
||||||
|
.Select(label => new PhaseGroupViewItem(
|
||||||
|
label, label == MainPathLabel, descriptions[label], BuildPhaseViewItems(byLabel[label], lesson.StartTime)))
|
||||||
|
.ToList();
|
||||||
|
HasAlternatives = PhaseGroups.Count > 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<PhaseViewItem> BuildPhaseViewItems(List<LessonPhaseStep> steps, TimeOnly? startTime)
|
||||||
|
{
|
||||||
|
var cursor = startTime;
|
||||||
|
var items = new List<PhaseViewItem>();
|
||||||
|
foreach (var p in steps)
|
||||||
|
{
|
||||||
|
var timeDisplay = cursor is { } c ? $"ab {c:HH:mm}" : "";
|
||||||
|
items.Add(new PhaseViewItem(p.Name, p.DurationMinutes, timeDisplay, p.Activity, p.Material, p.Shorthand));
|
||||||
|
if (cursor is { } cc) cursor = cc.AddMinutes(p.DurationMinutes);
|
||||||
|
}
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
<Window xmlns="https://github.com/avaloniaui"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:vm="clr-namespace:LehrerApp.Desktop.ViewModels.Groups"
|
||||||
|
xmlns:models="clr-namespace:LehrerApp.Core.Models;assembly=LehrerApp.Core"
|
||||||
|
x:Class="LehrerApp.Desktop.Views.Groups.AlternativePathDialog"
|
||||||
|
x:DataType="vm:AlternativePathDialogViewModel"
|
||||||
|
Title="Alternativer Ablauf"
|
||||||
|
Width="440" Height="500" MinWidth="380" MinHeight="380"
|
||||||
|
CanResize="True" WindowStartupLocation="CenterOwner">
|
||||||
|
|
||||||
|
<Grid RowDefinitions="*,Auto" Margin="24">
|
||||||
|
<ScrollViewer Grid.Row="0">
|
||||||
|
<StackPanel Spacing="14">
|
||||||
|
<TextBlock Text="Alternativer Ablauf" Classes="dialogtitle"/>
|
||||||
|
<TextBlock Text="Diese Phase gehört zu einem alternativen Unterrichtsverlauf, z.B. einer Kurzversion bei Zeitmangel."
|
||||||
|
FontSize="12" Opacity="0.6" TextWrapping="Wrap"/>
|
||||||
|
|
||||||
|
<StackPanel Spacing="4">
|
||||||
|
<TextBlock Text="Vorhandene Abläufe" FontSize="12" Opacity="0.7"/>
|
||||||
|
<ComboBox ItemsSource="{Binding Available}" SelectedItem="{Binding SelectedPath}"
|
||||||
|
HorizontalAlignment="Stretch" PlaceholderText="Ablauf auswählen">
|
||||||
|
<ComboBox.ItemTemplate>
|
||||||
|
<DataTemplate x:DataType="models:AlternativeLessonPath">
|
||||||
|
<StackPanel Margin="0,2">
|
||||||
|
<TextBlock Text="{Binding Name}"/>
|
||||||
|
<TextBlock Text="{Binding Description}" FontSize="11" Opacity="0.6" TextWrapping="Wrap"
|
||||||
|
IsVisible="{Binding Description, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
|
||||||
|
</StackPanel>
|
||||||
|
</DataTemplate>
|
||||||
|
</ComboBox.ItemTemplate>
|
||||||
|
</ComboBox>
|
||||||
|
<TextBlock Text="{Binding SelectionError}" Foreground="Red" FontSize="11"
|
||||||
|
IsVisible="{Binding SelectionError, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<Separator Margin="0,4"/>
|
||||||
|
|
||||||
|
<StackPanel Spacing="10">
|
||||||
|
<TextBlock Text="Neuen Ablauf anlegen" FontSize="12" FontWeight="SemiBold" Opacity="0.7"/>
|
||||||
|
<StackPanel Spacing="4">
|
||||||
|
<TextBox Text="{Binding NewName}" PlaceholderText="Name, z.B. Kurzversion"/>
|
||||||
|
<TextBlock Text="{Binding NewNameError}" Foreground="Red" FontSize="11"
|
||||||
|
IsVisible="{Binding NewNameError, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
|
||||||
|
</StackPanel>
|
||||||
|
<TextBox Text="{Binding NewDescription}" AcceptsReturn="True" Height="64" TextWrapping="Wrap"
|
||||||
|
PlaceholderText="Wann nimmt man diesen Weg? z.B. 'Bei Zeitmangel, wenn Aufgabe 3 nicht mehr passt.'"/>
|
||||||
|
<Button Content="Anlegen und auswählen" Command="{Binding CreateNewCommand}" HorizontalAlignment="Left"/>
|
||||||
|
</StackPanel>
|
||||||
|
</StackPanel>
|
||||||
|
</ScrollViewer>
|
||||||
|
|
||||||
|
<Grid Grid.Row="1" ColumnDefinitions="*,8,*" Margin="0,20,0,0">
|
||||||
|
<Button Grid.Column="0" Content="Abbrechen" HorizontalAlignment="Stretch" Click="OnCancel"/>
|
||||||
|
<Button Grid.Column="2" Content="Übernehmen" HorizontalAlignment="Stretch" Click="OnConfirm"/>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</Window>
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Interactivity;
|
||||||
|
using LehrerApp.Desktop.ViewModels.Groups;
|
||||||
|
|
||||||
|
namespace LehrerApp.Desktop.Views.Groups;
|
||||||
|
|
||||||
|
public partial class AlternativePathDialog : Window
|
||||||
|
{
|
||||||
|
public AlternativePathDialog() => InitializeComponent();
|
||||||
|
|
||||||
|
private void OnConfirm(object? s, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (DataContext is AlternativePathDialogViewModel vm && vm.ConfirmCommand.CanExecute(null))
|
||||||
|
{
|
||||||
|
vm.ConfirmCommand.Execute(null);
|
||||||
|
if (vm.Result is not null) Close(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnCancel(object? s, RoutedEventArgs e) => Close(false);
|
||||||
|
}
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
x:Class="LehrerApp.Desktop.Views.Groups.LessonDialog"
|
x:Class="LehrerApp.Desktop.Views.Groups.LessonDialog"
|
||||||
x:DataType="vm:LessonDialogViewModel"
|
x:DataType="vm:LessonDialogViewModel"
|
||||||
Title="{Binding DialogTitle}"
|
Title="{Binding DialogTitle}"
|
||||||
Width="960" Height="760" MinWidth="720" MinHeight="480"
|
Width="1000" Height="760" MinWidth="780" MinHeight="480"
|
||||||
CanResize="True" WindowStartupLocation="CenterOwner">
|
CanResize="True" WindowStartupLocation="CenterOwner">
|
||||||
|
|
||||||
<Grid RowDefinitions="*,Auto" Margin="24">
|
<Grid RowDefinitions="*,Auto" Margin="24">
|
||||||
@@ -56,12 +56,14 @@
|
|||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<!-- Tabellenkopf -->
|
<!-- Tabellenkopf -->
|
||||||
<Grid ColumnDefinitions="140,130,*,110,100,26,26,26" Margin="4,0,0,0">
|
<Grid ColumnDefinitions="130,95,120,*,100,95,26,26,26" Margin="4,0,0,0">
|
||||||
<TextBlock Grid.Column="0" Text="Phase" FontSize="11" FontWeight="SemiBold" Opacity="0.6"/>
|
<TextBlock Grid.Column="0" Text="Phase" FontSize="11" FontWeight="SemiBold" Opacity="0.6"/>
|
||||||
<TextBlock Grid.Column="1" Text="Dauer / Zeit" FontSize="11" FontWeight="SemiBold" Opacity="0.6"/>
|
<TextBlock Grid.Column="1" Text="Pfad" FontSize="11" FontWeight="SemiBold" Opacity="0.6"
|
||||||
<TextBlock Grid.Column="2" Text="Tätigkeit" FontSize="11" FontWeight="SemiBold" Opacity="0.6"/>
|
ToolTip.Tip="Ankreuzen, wenn diese Phase zu einem alternativen Ablauf gehört (z.B. Kurzversion bei Zeitnot) — öffnet die Zuweisung. Phasen mit demselben Ablauf werden im Verlaufsplan-Viewer gruppiert."/>
|
||||||
<TextBlock Grid.Column="3" Text="Material" FontSize="11" FontWeight="SemiBold" Opacity="0.6"/>
|
<TextBlock Grid.Column="2" Text="Dauer / Zeit" FontSize="11" FontWeight="SemiBold" Opacity="0.6"/>
|
||||||
<TextBlock Grid.Column="4" Text="Kurzsymbol" FontSize="11" FontWeight="SemiBold" Opacity="0.6"/>
|
<TextBlock Grid.Column="3" Text="Tätigkeit" FontSize="11" FontWeight="SemiBold" Opacity="0.6"/>
|
||||||
|
<TextBlock Grid.Column="4" Text="Material" FontSize="11" FontWeight="SemiBold" Opacity="0.6"/>
|
||||||
|
<TextBlock Grid.Column="5" Text="Kurzsymbol" FontSize="11" FontWeight="SemiBold" Opacity="0.6"/>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Separator/>
|
<Separator/>
|
||||||
|
|
||||||
@@ -71,34 +73,38 @@
|
|||||||
<DataTemplate x:DataType="vm:PhaseStepEditItem">
|
<DataTemplate x:DataType="vm:PhaseStepEditItem">
|
||||||
<Border BorderBrush="{DynamicResource SystemControlForegroundBaseLowBrush}"
|
<Border BorderBrush="{DynamicResource SystemControlForegroundBaseLowBrush}"
|
||||||
BorderThickness="0,0,0,1" Padding="0,8">
|
BorderThickness="0,0,0,1" Padding="0,8">
|
||||||
<Grid ColumnDefinitions="140,130,*,110,100,26,26,26">
|
<Grid ColumnDefinitions="130,95,120,*,100,95,26,26,26">
|
||||||
<TextBox Grid.Column="0" Text="{Binding Name}" PlaceholderText="z.B. Erarbeitung"
|
<TextBox Grid.Column="0" Text="{Binding Name}" PlaceholderText="z.B. Erarbeitung"
|
||||||
VerticalAlignment="Top" Margin="0,0,6,0"/>
|
VerticalAlignment="Top" Margin="0,0,6,0"/>
|
||||||
<StackPanel Grid.Column="1" Orientation="Horizontal" Spacing="6"
|
<CheckBox Grid.Column="1" IsChecked="{Binding HasAlternativePath}"
|
||||||
|
Content="{Binding AlternativePathName}" Foreground="{Binding AlternativePathColorHex}"
|
||||||
|
FontSize="12" VerticalAlignment="Top" Margin="0,4,6,0"
|
||||||
|
ToolTip.Tip="Teil eines alternativen Ablaufs (z.B. Kurzversion bei Zeitnot). Ankreuzen zum Zuweisen/Anlegen, abwählen zum Entfernen."/>
|
||||||
|
<StackPanel Grid.Column="2" Orientation="Horizontal" Spacing="6"
|
||||||
VerticalAlignment="Top" Margin="0,0,6,0">
|
VerticalAlignment="Top" Margin="0,0,6,0">
|
||||||
<NumericUpDown Value="{Binding DurationMinutes}" Minimum="0" Maximum="180" Width="62"
|
<NumericUpDown Value="{Binding DurationMinutes}" Minimum="0" Maximum="180" Width="62"
|
||||||
FormatString="0" ShowButtonSpinner="False" ToolTip.Tip="Dauer in Minuten"/>
|
FormatString="0" ShowButtonSpinner="False" ToolTip.Tip="Dauer in Minuten"/>
|
||||||
<TextBlock Text="{Binding ComputedTimeDisplay}" FontSize="12" Opacity="0.6"
|
<TextBlock Text="{Binding ComputedTimeDisplay}" FontSize="12" Opacity="0.6"
|
||||||
VerticalAlignment="Center"/>
|
VerticalAlignment="Center"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
<TextBox Grid.Column="2" Text="{Binding Activity}" AcceptsReturn="True" TextWrapping="Wrap"
|
<TextBox Grid.Column="3" Text="{Binding Activity}" AcceptsReturn="True" TextWrapping="Wrap"
|
||||||
Height="56" VerticalAlignment="Top" Margin="0,0,6,0"
|
Height="56" VerticalAlignment="Top" Margin="0,0,6,0"
|
||||||
PlaceholderText="Lehrer-/Schüler-Tätigkeit"
|
PlaceholderText="Lehrer-/Schüler-Tätigkeit"
|
||||||
ToolTip.Tip="Bei mehr Text scrollt das Feld intern."/>
|
ToolTip.Tip="Bei mehr Text scrollt das Feld intern."/>
|
||||||
<AutoCompleteBox Grid.Column="3" Text="{Binding Material}"
|
<AutoCompleteBox Grid.Column="4" Text="{Binding Material}"
|
||||||
ItemsSource="{Binding $parent[ItemsControl].((vm:LessonDialogViewModel)DataContext).MaterialSuggestions}"
|
ItemsSource="{Binding $parent[ItemsControl].((vm:LessonDialogViewModel)DataContext).MaterialSuggestions}"
|
||||||
FilterMode="Contains" MinimumPrefixLength="0" VerticalAlignment="Top"
|
FilterMode="Contains" MinimumPrefixLength="0" VerticalAlignment="Top"
|
||||||
Margin="0,0,6,0" PlaceholderText="z.B. AB01"/>
|
Margin="0,0,6,0" PlaceholderText="z.B. AB01"/>
|
||||||
<AutoCompleteBox Grid.Column="4" Text="{Binding Shorthand}"
|
<AutoCompleteBox Grid.Column="5" Text="{Binding Shorthand}"
|
||||||
ItemsSource="{Binding $parent[ItemsControl].((vm:LessonDialogViewModel)DataContext).ShorthandSuggestions}"
|
ItemsSource="{Binding $parent[ItemsControl].((vm:LessonDialogViewModel)DataContext).ShorthandSuggestions}"
|
||||||
FilterMode="Contains" MinimumPrefixLength="0" VerticalAlignment="Top"
|
FilterMode="Contains" MinimumPrefixLength="0" VerticalAlignment="Top"
|
||||||
Margin="0,0,6,0" PlaceholderText="z.B. AB001->S, Plenum, LDE"
|
Margin="0,0,6,0" PlaceholderText="z.B. AB001->S, Plenum, LDE"
|
||||||
ToolTip.Tip="Freitext für den schnellen Überblick — mal ein Materialfluss-Pfeil (AB001->S), mal nur eine Sozialform (Plenum, LDE)."/>
|
ToolTip.Tip="Freitext für den schnellen Überblick — mal ein Materialfluss-Pfeil (AB001->S), mal nur eine Sozialform (Plenum, LDE)."/>
|
||||||
<Button Grid.Column="5" Content="↑" Command="{Binding MoveUpCommand}" Padding="4,2"
|
<Button Grid.Column="6" Content="↑" Command="{Binding MoveUpCommand}" Padding="4,2"
|
||||||
VerticalAlignment="Top" ToolTip.Tip="Nach oben"/>
|
VerticalAlignment="Top" ToolTip.Tip="Nach oben"/>
|
||||||
<Button Grid.Column="6" Content="↓" Command="{Binding MoveDownCommand}" Padding="4,2"
|
<Button Grid.Column="7" Content="↓" Command="{Binding MoveDownCommand}" Padding="4,2"
|
||||||
VerticalAlignment="Top" Margin="2,0,0,0" ToolTip.Tip="Nach unten"/>
|
VerticalAlignment="Top" Margin="2,0,0,0" ToolTip.Tip="Nach unten"/>
|
||||||
<Button Grid.Column="7" Content="✕" Command="{Binding RemoveCommand}" Padding="4,2"
|
<Button Grid.Column="8" Content="✕" Command="{Binding RemoveCommand}" Padding="4,2"
|
||||||
VerticalAlignment="Top" Margin="2,0,0,0" ToolTip.Tip="Entfernen"/>
|
VerticalAlignment="Top" Margin="2,0,0,0" ToolTip.Tip="Entfernen"/>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Border>
|
</Border>
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
using Avalonia.Interactivity;
|
using Avalonia.Interactivity;
|
||||||
|
using LehrerApp.Core.Interfaces;
|
||||||
|
using LehrerApp.Core.Models;
|
||||||
using LehrerApp.Desktop.ViewModels.Groups;
|
using LehrerApp.Desktop.ViewModels.Groups;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
namespace LehrerApp.Desktop.Views.Groups;
|
namespace LehrerApp.Desktop.Views.Groups;
|
||||||
|
|
||||||
@@ -8,6 +11,23 @@ public partial class LessonDialog : Window
|
|||||||
{
|
{
|
||||||
public LessonDialog() => InitializeComponent();
|
public LessonDialog() => InitializeComponent();
|
||||||
|
|
||||||
|
protected override void OnDataContextChanged(EventArgs e)
|
||||||
|
{
|
||||||
|
base.OnDataContextChanged(e);
|
||||||
|
if (DataContext is LessonDialogViewModel vm)
|
||||||
|
vm.OnPickAlternativePath = ShowAlternativePathDialog;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<AlternativeLessonPath?> ShowAlternativePathDialog(Guid? currentId)
|
||||||
|
{
|
||||||
|
var dialogVm = new AlternativePathDialogViewModel(
|
||||||
|
App.Services.GetRequiredService<IAlternativeLessonPathRepository>(), currentId);
|
||||||
|
var dialog = new AlternativePathDialog { DataContext = dialogVm };
|
||||||
|
|
||||||
|
var ok = await dialog.ShowDialog<bool>(this);
|
||||||
|
return ok ? dialogVm.Result : null;
|
||||||
|
}
|
||||||
|
|
||||||
private void OnSave(object? s, RoutedEventArgs e)
|
private void OnSave(object? s, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
if (DataContext is LessonDialogViewModel vm && vm.SaveCommand.CanExecute(null))
|
if (DataContext is LessonDialogViewModel vm && vm.SaveCommand.CanExecute(null))
|
||||||
|
|||||||
@@ -0,0 +1,90 @@
|
|||||||
|
<Window xmlns="https://github.com/avaloniaui"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:vm="clr-namespace:LehrerApp.Desktop.ViewModels.Groups"
|
||||||
|
x:Class="LehrerApp.Desktop.Views.Groups.LessonViewerDialog"
|
||||||
|
x:DataType="vm:LessonViewerViewModel"
|
||||||
|
Title="{Binding Topic}"
|
||||||
|
Width="900" Height="700" MinWidth="600" MinHeight="420"
|
||||||
|
CanResize="True" WindowStartupLocation="CenterOwner">
|
||||||
|
|
||||||
|
<Grid RowDefinitions="Auto,*,Auto" Margin="24">
|
||||||
|
<StackPanel Grid.Row="0" Spacing="4" Margin="0,0,0,16">
|
||||||
|
<TextBlock Text="{Binding Topic}" FontSize="20" FontWeight="SemiBold"/>
|
||||||
|
<StackPanel Orientation="Horizontal" Spacing="16">
|
||||||
|
<TextBlock FontSize="13" Opacity="0.7">
|
||||||
|
<Run Text="Datum: "/><Run Text="{Binding DateDisplay}"/>
|
||||||
|
</TextBlock>
|
||||||
|
<TextBlock FontSize="13" Opacity="0.7">
|
||||||
|
<Run Text="Beginn: "/><Run Text="{Binding StartTimeDisplay}"/>
|
||||||
|
</TextBlock>
|
||||||
|
<TextBlock FontSize="13" Opacity="0.7">
|
||||||
|
<Run Text="Status: "/><Run Text="{Binding StatusLabel}"/>
|
||||||
|
</TextBlock>
|
||||||
|
</StackPanel>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<ScrollViewer Grid.Row="1">
|
||||||
|
<StackPanel Spacing="0">
|
||||||
|
<Grid ColumnDefinitions="140,80,*,140,130" Margin="4,0,0,6">
|
||||||
|
<TextBlock Grid.Column="0" Text="Phase" FontSize="12" FontWeight="SemiBold" Opacity="0.6"/>
|
||||||
|
<TextBlock Grid.Column="1" Text="Dauer/Zeit" FontSize="12" FontWeight="SemiBold" Opacity="0.6"/>
|
||||||
|
<TextBlock Grid.Column="2" Text="Tätigkeit" FontSize="12" FontWeight="SemiBold" Opacity="0.6"/>
|
||||||
|
<TextBlock Grid.Column="3" Text="Material" FontSize="12" FontWeight="SemiBold" Opacity="0.6"/>
|
||||||
|
<TextBlock Grid.Column="4" Text="Kurzsymbol" FontSize="12" FontWeight="SemiBold" Opacity="0.6"/>
|
||||||
|
</Grid>
|
||||||
|
<Separator/>
|
||||||
|
|
||||||
|
<ItemsControl ItemsSource="{Binding PhaseGroups}">
|
||||||
|
<ItemsControl.ItemTemplate>
|
||||||
|
<DataTemplate x:DataType="vm:PhaseGroupViewItem">
|
||||||
|
<StackPanel Margin="0,0,0,14">
|
||||||
|
<StackPanel IsVisible="{Binding $parent[ItemsControl].((vm:LessonViewerViewModel)DataContext).HasAlternatives}">
|
||||||
|
<TextBlock Text="{Binding Label}" FontSize="13" FontWeight="SemiBold" Opacity="0.75"
|
||||||
|
Margin="4,10,0,2"/>
|
||||||
|
<TextBlock Text="{Binding Description}" FontSize="12" Opacity="0.55" TextWrapping="Wrap"
|
||||||
|
Margin="4,0,0,6"
|
||||||
|
IsVisible="{Binding Description, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
|
||||||
|
</StackPanel>
|
||||||
|
<ItemsControl ItemsSource="{Binding Phases}">
|
||||||
|
<ItemsControl.ItemTemplate>
|
||||||
|
<DataTemplate x:DataType="vm:PhaseViewItem">
|
||||||
|
<Border BorderBrush="{DynamicResource SystemControlForegroundBaseLowBrush}"
|
||||||
|
BorderThickness="0,0,0,1" Padding="4,10">
|
||||||
|
<Grid ColumnDefinitions="140,80,*,140,130">
|
||||||
|
<TextBlock Grid.Column="0" Text="{Binding Name}" FontSize="15" FontWeight="SemiBold"
|
||||||
|
TextWrapping="Wrap" VerticalAlignment="Top" Margin="0,0,8,0"/>
|
||||||
|
<StackPanel Grid.Column="1" VerticalAlignment="Top">
|
||||||
|
<TextBlock Text="{Binding DurationMinutes, StringFormat='{}{0} Min.'}" FontSize="14"/>
|
||||||
|
<TextBlock Text="{Binding TimeDisplay}" FontSize="12" Opacity="0.6"/>
|
||||||
|
</StackPanel>
|
||||||
|
<TextBlock Grid.Column="2" Text="{Binding Activity}" FontSize="14"
|
||||||
|
TextWrapping="Wrap" VerticalAlignment="Top" Margin="0,0,8,0"/>
|
||||||
|
<TextBlock Grid.Column="3" Text="{Binding Material}" FontSize="14"
|
||||||
|
TextWrapping="Wrap" VerticalAlignment="Top" Margin="0,0,8,0"/>
|
||||||
|
<TextBlock Grid.Column="4" Text="{Binding Shorthand}" FontSize="14"
|
||||||
|
TextWrapping="Wrap" VerticalAlignment="Top"/>
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
</DataTemplate>
|
||||||
|
</ItemsControl.ItemTemplate>
|
||||||
|
</ItemsControl>
|
||||||
|
</StackPanel>
|
||||||
|
</DataTemplate>
|
||||||
|
</ItemsControl.ItemTemplate>
|
||||||
|
</ItemsControl>
|
||||||
|
|
||||||
|
<StackPanel Spacing="4" Margin="4,16,0,0" IsVisible="{Binding Homework, Converter={x:Static StringConverters.IsNotNullOrEmpty}}">
|
||||||
|
<TextBlock Text="Hausaufgabe" FontSize="12" FontWeight="SemiBold" Opacity="0.6"/>
|
||||||
|
<TextBlock Text="{Binding Homework}" FontSize="14" TextWrapping="Wrap"/>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<StackPanel Spacing="4" Margin="4,16,0,0" IsVisible="{Binding Reflection, Converter={x:Static StringConverters.IsNotNullOrEmpty}}">
|
||||||
|
<TextBlock Text="Reflexion" FontSize="12" FontWeight="SemiBold" Opacity="0.6"/>
|
||||||
|
<TextBlock Text="{Binding Reflection}" FontSize="14" TextWrapping="Wrap"/>
|
||||||
|
</StackPanel>
|
||||||
|
</StackPanel>
|
||||||
|
</ScrollViewer>
|
||||||
|
|
||||||
|
<Button Grid.Row="2" Content="Schließen" HorizontalAlignment="Right" Margin="0,16,0,0" Click="OnClose"/>
|
||||||
|
</Grid>
|
||||||
|
</Window>
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Interactivity;
|
||||||
|
|
||||||
|
namespace LehrerApp.Desktop.Views.Groups;
|
||||||
|
|
||||||
|
public partial class LessonViewerDialog : Window
|
||||||
|
{
|
||||||
|
public LessonViewerDialog() => InitializeComponent();
|
||||||
|
|
||||||
|
private void OnClose(object? s, RoutedEventArgs e) => Close();
|
||||||
|
}
|
||||||
@@ -63,6 +63,8 @@
|
|||||||
</StackPanel>
|
</StackPanel>
|
||||||
<StackPanel Grid.Column="1" Orientation="Horizontal" Spacing="8">
|
<StackPanel Grid.Column="1" Orientation="Horizontal" Spacing="8">
|
||||||
<Button Content="+ Stunde" Command="{Binding AddLessonCommand}"/>
|
<Button Content="+ Stunde" Command="{Binding AddLessonCommand}"/>
|
||||||
|
<Button Content="Anzeigen" Command="{Binding ShowLessonCommand}"
|
||||||
|
ToolTip.Tip="Verlaufsplan schreibgeschützt und größer anzeigen — zum Mitnehmen in den Unterricht."/>
|
||||||
<Button Content="Bearbeiten" Command="{Binding EditLessonCommand}"/>
|
<Button Content="Bearbeiten" Command="{Binding EditLessonCommand}"/>
|
||||||
<Button Content="Verschieben" Command="{Binding MoveLessonCommand}"/>
|
<Button Content="Verschieben" Command="{Binding MoveLessonCommand}"/>
|
||||||
<Button Content="Status → Durchgeführt" Command="{Binding AdvanceLessonStatusCommand}"/>
|
<Button Content="Status → Durchgeführt" Command="{Binding AdvanceLessonStatusCommand}"/>
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ public partial class PlanningTabView : UserControl
|
|||||||
ShowLessonDialog(lesson.UnitId, lesson.GroupId, materials, shorthands, editingLesson: lesson);
|
ShowLessonDialog(lesson.UnitId, lesson.GroupId, materials, shorthands, editingLesson: lesson);
|
||||||
vm.OnConfirmDeleteLesson = ShowDeleteLessonDialog;
|
vm.OnConfirmDeleteLesson = ShowDeleteLessonDialog;
|
||||||
vm.OnPickMoveTarget = ShowMoveLessonDialog;
|
vm.OnPickMoveTarget = ShowMoveLessonDialog;
|
||||||
|
vm.OnShowLesson = ShowLessonViewerDialog;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,6 +79,7 @@ public partial class PlanningTabView : UserControl
|
|||||||
var dialogVm = new LessonDialogViewModel(
|
var dialogVm = new LessonDialogViewModel(
|
||||||
App.Services.GetRequiredService<ILessonRepository>(),
|
App.Services.GetRequiredService<ILessonRepository>(),
|
||||||
App.Services.GetRequiredService<IShorthandCodeRepository>(),
|
App.Services.GetRequiredService<IShorthandCodeRepository>(),
|
||||||
|
App.Services.GetRequiredService<IAlternativeLessonPathRepository>(),
|
||||||
unitId, groupId, materialSuggestions, shorthandHistorySuggestions, editingLesson);
|
unitId, groupId, materialSuggestions, shorthandHistorySuggestions, editingLesson);
|
||||||
|
|
||||||
var dialog = new LessonDialog { DataContext = dialogVm };
|
var dialog = new LessonDialog { DataContext = dialogVm };
|
||||||
@@ -111,4 +113,13 @@ public partial class PlanningTabView : UserControl
|
|||||||
var ok = await dialog.ShowDialog<bool>(owner);
|
var ok = await dialog.ShowDialog<bool>(owner);
|
||||||
return ok ? dialogVm.Result : null;
|
return ok ? dialogVm.Result : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task ShowLessonViewerDialog(Lesson lesson)
|
||||||
|
{
|
||||||
|
var viewerVm = new LessonViewerViewModel(lesson,
|
||||||
|
App.Services.GetRequiredService<IAlternativeLessonPathRepository>());
|
||||||
|
var dialog = new LessonViewerDialog { DataContext = viewerVm };
|
||||||
|
var owner = TopLevel.GetTopLevel(this) as Window;
|
||||||
|
if (owner is not null) await dialog.ShowDialog(owner);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -314,13 +314,57 @@ brauchen:
|
|||||||
müsste.
|
müsste.
|
||||||
- "Fertig"/"Überziehen" je Phase anklickbar, nachfolgende Phasenzeiten passen sich automatisch an;
|
- "Fertig"/"Überziehen" je Phase anklickbar, nachfolgende Phasenzeiten passen sich automatisch an;
|
||||||
Visualisierung, wie weit man dem Plan hinterherhängt.
|
Visualisierung, wie weit man dem Plan hinterherhängt.
|
||||||
- Hinterlegte alternative Stundenenden, in die man bei Zeitnot direkt hineinspringen kann.
|
- Live-Sprung zwischen Wegen während des Haltens der Stunde (per Klick direkt in den alternativen
|
||||||
|
Ablauf springen) — die Planungs-/Anzeigeseite (Kennzeichnen, gruppierte Darstellung) ist bereits
|
||||||
|
umgesetzt, siehe Nachtrag unten; nur die Live-Interaktion beim Halten fehlt noch.
|
||||||
- "Phase in nächste Stunde schieben"-Aktion, mit Hinweis-Symbol an der Folgestunde, dass dort noch
|
- "Phase in nächste Stunde schieben"-Aktion, mit Hinweis-Symbol an der Folgestunde, dass dort noch
|
||||||
offene Phasen/Arbeitsaufträge der Vorstunde einzuplanen sind.
|
offene Phasen/Arbeitsaufträge der Vorstunde einzuplanen sind.
|
||||||
- Phasen-"Parkplatz": beim Planen Phasen ablegen können, ohne sie sofort einer Stunde zuzuordnen,
|
- Phasen-"Parkplatz": beim Planen Phasen ablegen können, ohne sie sofort einer Stunde zuzuordnen,
|
||||||
und später flexibel in eine beliebige Stunde einfügen — Werkzeug, um Einheiten während des
|
und später flexibel in eine beliebige Stunde einfügen — Werkzeug, um Einheiten während des
|
||||||
Schuljahres an die Realität anzupassen, wenn der Unterricht nicht wie geplant läuft.
|
Schuljahres an die Realität anzupassen, wenn der Unterricht nicht wie geplant läuft.
|
||||||
|
|
||||||
|
**Nachtrag — schreibgeschützte Verlaufsplan-Ansicht:** Um den Verlaufsplan-Editor überhaupt im
|
||||||
|
Unterricht benutzen zu können, ohne versehentlich etwas zu verändern, gibt es jetzt
|
||||||
|
`LessonViewerDialog` (erreichbar über "Anzeigen" im Stunden-Toolbar, sichtbar bei ausgewählter
|
||||||
|
Stunde) — reine, größer geschriebene Anzeige des Verlaufsplans inkl. abgeleiteter Uhrzeit je
|
||||||
|
Phase, ohne jede Bearbeitungs- oder Verlängern-/Verschieben-Funktion. Das ist bewusst nur ein
|
||||||
|
Ausschnitt aus der "Fortschrittslinie"-Idee oben (rein statische Anzeige, keine Live-Aktualisierung
|
||||||
|
nach Uhrzeit, kein "fertig"/"überziehen") — der volle Live-Modus bleibt Ideensammlung.
|
||||||
|
|
||||||
|
**Nachtrag — Alternative Unterrichtsabläufe, zweite Iteration (echter Katalog statt Freitext):**
|
||||||
|
Die erste Umsetzung (`LessonPhaseStep.AlternativePath: string?`, freier Bezeichner, Zuweisung über
|
||||||
|
eine durchgehend sichtbare `AutoCompleteBox`-Spalte) wirkte im Alltag zu prominent für einen Fall,
|
||||||
|
der selten vorkommt. Nutzer-Feedback: eine unauffällige Checkbox reicht für den Normalfall, die
|
||||||
|
eigentliche Zuordnung gehört in einen separaten Dialog, und ein alternativer Ablauf sollte eine
|
||||||
|
Erklärung tragen können (wann/wofür er gedacht ist, z.B. "bei Zeitmangel").
|
||||||
|
|
||||||
|
Redesign:
|
||||||
|
- Neues Modell [`AlternativeLessonPath`](LehrerApp.Core/Models/Planning.cs) (Name + optionale
|
||||||
|
`Description`) als eigener, wiederverwendbarer Katalog — `LessonPhaseStep.AlternativePathId:
|
||||||
|
Guid?` referenziert einen Eintrag statt Freitext direkt zu speichern (`null` = Hauptweg).
|
||||||
|
`IAlternativeLessonPathRepository`/`AlternativeLessonPathRepository`, eindeutig auf `Name`,
|
||||||
|
kein eigenes Verwaltungs-UI in den Einstellungen — Einträge entstehen inline beim Zuweisen.
|
||||||
|
- **Editor**: Die "Pfad"-Spalte ist jetzt eine einzelne `CheckBox` (`PhaseStepEditItem.
|
||||||
|
HasAlternativePath`), deren `Content` bei Zuordnung den Namen des Ablaufs zeigt, `Foreground`
|
||||||
|
deterministisch aus dem Namen eingefärbt (kleines, farbig unterstütztes Label statt breiter
|
||||||
|
Eingabespalte). Ankreuzen öffnet `AlternativePathDialog` (Combobox über vorhandene Abläufe +
|
||||||
|
Formular zum Anlegen eines neuen inkl. Beschreibung); Abwählen entfernt die Zuordnung direkt
|
||||||
|
ohne Dialog. `LessonDialogViewModel.OnPickAlternativePath` (vom Code-Behind von `LessonDialog`
|
||||||
|
selbst verdrahtet, nicht vom übergeordneten `PlanningTabView` — der Dialog braucht das
|
||||||
|
`LessonDialog`-Fenster als Owner) kapselt diesen Ablauf.
|
||||||
|
- **Viewer**: `LessonViewerViewModel` löst `AlternativePathId` über den Katalog auf und zeigt die
|
||||||
|
hinterlegte Beschreibung unter der Gruppenüberschrift.
|
||||||
|
- **Migration v3→v4** (`MigrateLessonAlternativePaths`): alte `AlternativePath`-Freitextwerte
|
||||||
|
werden pro distinktem Namen zu einem `AlternativeLessonPath`-Katalogeintrag zusammengeführt
|
||||||
|
(gleicher Name über mehrere Stunden hinweg → derselbe Eintrag, wiederverwendet) und referenziert.
|
||||||
|
- Weiterhin bewusst **kein** Bedingungsfeld ("wenn an Stelle X nur noch weniger als Y Minuten") —
|
||||||
|
das setzt Live-Zeiterfassung während des Unterrichtens voraus (siehe Live-Unterrichtsmodus oben)
|
||||||
|
und war ausdrücklich als Idee, nicht als Anforderung formuliert. `AlternativeLessonPath.
|
||||||
|
Description` deckt die Erklärung als Freitext ab; eine echte Bedingung bliebe für später.
|
||||||
|
- Bewusst weiterhin **kein** Fork-Punkt-Modell: jede Gruppe berechnet ihre abgeleitete Uhrzeit im
|
||||||
|
Viewer unabhängig ab `Lesson.StartTime`, nicht ab einer gemeinsamen Verzweigungsstelle im
|
||||||
|
Hauptweg — deutlich einfacher und für den schnellen Überblick ausreichend.
|
||||||
|
|
||||||
### 4.3 Stundenplan
|
### 4.3 Stundenplan
|
||||||
- [ ] **4.3.1** Neues Modell `TimetableSlot` (Gruppe, Wochentag, Stunde, Raum) + Repository.
|
- [ ] **4.3.1** Neues Modell `TimetableSlot` (Gruppe, Wochentag, Stunde, Raum) + Repository.
|
||||||
- [ ] **4.3.2** Wochenstundenplan-Ansicht als Raster mit Farbcodierung je Gruppe.
|
- [ ] **4.3.2** Wochenstundenplan-Ansicht als Raster mit Farbcodierung je Gruppe.
|
||||||
|
|||||||
+20
-6
@@ -100,8 +100,17 @@ der Praxis ist es mal ein Materialfluss-Pfeil ("AB001->S"), mal nur eine Sozialf
|
|||||||
Stunden verwendete Werte dienen nur als Autovervollständigungs-Vorschläge, erzwingen aber keine
|
Stunden verwendete Werte dienen nur als Autovervollständigungs-Vorschläge, erzwingen aber keine
|
||||||
Struktur.
|
Struktur.
|
||||||
|
|
||||||
`LiteDbContext`-Schema-Version 3 führt zwei aufeinanderfolgende, unabhängig versionierte
|
`AlternativePathId` (`Guid?`, `null` = Hauptweg) verweist auf einen Eintrag im eigenständigen
|
||||||
Migrationsschritte für dieses Feld:
|
Katalog `AlternativeLessonPath` (Name + optionale `Description`, verwaltet über
|
||||||
|
`IAlternativeLessonPathRepository`) — anders als `Shorthand` bewusst kein Freitext direkt am
|
||||||
|
Phasen-Datensatz: die Zuordnung muss zuverlässig gruppierbar sein (`LessonViewerViewModel`
|
||||||
|
gruppiert danach für die Anzeige) und soll eine wiederverwendbare Erklärung tragen können ("wann
|
||||||
|
nimmt man diesen Weg"), was ein reines Freitextfeld nicht sauber leisten kann. Kein
|
||||||
|
Fork-Punkt-Bezug zum Hauptweg: jede Gruppe wird beim Anzeigen unabhängig ab `Lesson.StartTime`
|
||||||
|
durchgerechnet, nicht ab einer gemeinsamen Verzweigungsstelle.
|
||||||
|
|
||||||
|
`LiteDbContext`-Schema-Version 4 führt drei aufeinanderfolgende, unabhängig versionierte
|
||||||
|
Migrationsschritte für dieses Feld bzw. seine Vorstufen:
|
||||||
- **v1→v2** (`MigrateLessonPhases()`): führt bereits gespeicherte alte Stunden (einzelnes
|
- **v1→v2** (`MigrateLessonPhases()`): führt bereits gespeicherte alte Stunden (einzelnes
|
||||||
`Phase`-Feld, `Methods`/`Materials`-Listen) verlustfrei in eine einzige synthetisierte
|
`Phase`-Feld, `Methods`/`Materials`-Listen) verlustfrei in eine einzige synthetisierte
|
||||||
`LessonPhaseStep`-Zeile zusammen (`Name` = altes `Phase`, `Activity` = alte `Methods` verbunden,
|
`LessonPhaseStep`-Zeile zusammen (`Name` = altes `Phase`, `Activity` = alte `Methods` verbunden,
|
||||||
@@ -109,11 +118,16 @@ Migrationsschritte für dieses Feld:
|
|||||||
- **v2→v3** (`MigrateLessonShorthand()`): das ursprünglich als Von/Nach-Paar (`ShorthandFrom`/
|
- **v2→v3** (`MigrateLessonShorthand()`): das ursprünglich als Von/Nach-Paar (`ShorthandFrom`/
|
||||||
`ShorthandTo`) modellierte Kurzsymbol wird auf das einzelne Freitextfeld zusammengeführt (beide
|
`ShorthandTo`) modellierte Kurzsymbol wird auf das einzelne Freitextfeld zusammengeführt (beide
|
||||||
gesetzt → `"Von->Nach"`, nur eines gesetzt → dieser Einzelwert).
|
gesetzt → `"Von->Nach"`, nur eines gesetzt → dieser Einzelwert).
|
||||||
|
- **v3→v4** (`MigrateLessonAlternativePaths()`): das ursprünglich als Freitext (`AlternativePath`,
|
||||||
|
string) modellierte Kennzeichen für den alternativen Ablauf wird pro distinktem Namen zu einem
|
||||||
|
`AlternativeLessonPath`-Katalogeintrag zusammengeführt (derselbe Name über mehrere Stunden hinweg
|
||||||
|
referenziert denselben, wiederverwendeten Eintrag) und die Phase auf `AlternativePathId`
|
||||||
|
umgestellt.
|
||||||
|
|
||||||
Beide Migrationen lesen dafür die rohe `BsonDocument`-Repräsentation der `lessons`-Collection statt
|
Alle drei Migrationen lesen dafür die rohe `BsonDocument`-Repräsentation der `lessons`-Collection
|
||||||
der typisierten `Lessons`-Collection — nach jeder Modelländerung kennt die typisierte `Lesson`-
|
statt der typisierten `Lessons`-Collection — nach jeder Modelländerung kennt die typisierte
|
||||||
Klasse die alten Feldnamen nicht mehr, ein Zugriff darüber hätte sie beim Deserialisieren bereits
|
`Lesson`-Klasse die alten Feldnamen nicht mehr, ein Zugriff darüber hätte sie beim Deserialisieren
|
||||||
verworfen, bevor sie gelesen werden können.
|
bereits verworfen, bevor sie gelesen werden können.
|
||||||
|
|
||||||
## Eindeutige Schlüssel
|
## Eindeutige Schlüssel
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user