diff --git a/LehrerApp.Core/Interfaces/IRepositories.cs b/LehrerApp.Core/Interfaces/IRepositories.cs index ea953f7..27a0f20 100644 --- a/LehrerApp.Core/Interfaces/IRepositories.cs +++ b/LehrerApp.Core/Interfaces/IRepositories.cs @@ -284,16 +284,31 @@ public interface ISubjectRepository } public interface ICompetencyDomainRepository { - List GetBySubjectAndGrade(Guid subjectId, int gradeLevel); - // Über alle Klassenstufen eines Fachs hinweg - für einen Überblick über den gesamten Katalog - // eines Fachs (z.B. MCP-Tool get_competency_catalog ohne gradeLevel-Filter), ohne dass der - // Aufrufer erst jede vorkommende Klassenstufe einzeln erraten/abfragen müsste. - List GetBySubject(Guid subjectId); + List GetBySubjectAndGrade(Guid subjectId, int gradeLevel, Guid versionId); + // Über alle Klassenstufen einer Version eines Fachs hinweg - für einen Überblick über den + // gesamten Katalog einer Version (z.B. MCP-Tool get_competency_catalog ohne gradeLevel-Filter), + // ohne dass der Aufrufer erst jede vorkommende Klassenstufe einzeln erraten/abfragen müsste. + List GetBySubjectAndVersion(Guid subjectId, Guid versionId); CompetencyDomain? GetById(Guid id); void Save(CompetencyDomain domain); void Delete(Guid id); - void DeleteBySubjectAndGrade(Guid subjectId, int gradeLevel); - void ReplaceForSubjectAndGrade(Guid subjectId, int gradeLevel, List domains); + void DeleteBySubjectGradeAndVersion(Guid subjectId, int gradeLevel, Guid versionId); + void ReplaceForSubjectGradeAndVersion(Guid subjectId, int gradeLevel, Guid versionId, List domains); +} + +public interface ICompetencyCatalogVersionRepository +{ + List GetBySubject(Guid subjectId); + CompetencyCatalogVersion? GetById(Guid id); + /// Liefert die Standardversion des Fachs und legt sie an ("Bestehender Kompetenzkatalog"), + /// falls noch keine existiert - so brauchen Aufrufer, die nur einen Guid für die Domain-Suche + /// benötigen, keinen eigenen Anlegen-oder-Lesen-Ablauf. + CompetencyCatalogVersion GetOrCreateDefault(Guid subjectId); + void Save(CompetencyCatalogVersion version); + /// Blockiert (InvalidOperationException), wenn noch ein CompetencyDomain oder eine + /// LearningGroup diese Version referenziert, oder wenn sie die Standardversion ihres Fachs ist + /// (erst eine andere Version des Fachs als Standard setzen). + void Delete(Guid id); } public interface IShorthandCodeRepository { diff --git a/LehrerApp.Core/Models/Competency.cs b/LehrerApp.Core/Models/Competency.cs index c46e033..7d46e03 100644 --- a/LehrerApp.Core/Models/Competency.cs +++ b/LehrerApp.Core/Models/Competency.cs @@ -5,6 +5,11 @@ public class CompetencyDomain public Guid Id { get; set; } = Guid.NewGuid(); public Guid SubjectId { get; set; } public int GradeLevel { get; set; } + /// Bildungsplan-Version dieses Bereichs () - ein + /// Fach kann mehrere parallel gültige Versionen haben (altes/neues Bildungsplan, während dieser + /// klassenstufenweise hochwächst). Wird von der Migration für bestehende Datensätze auf die + /// jeweilige Standardversion des Fachs gesetzt. + public Guid VersionId { get; set; } public string Name { get; set; } = ""; public string Code { get; set; } = ""; public int SortOrder { get; set; } @@ -12,6 +17,22 @@ public class CompetencyDomain public DateTime UpdatedAt { get; set; } = DateTime.UtcNow; } +/// Eine Bildungsplan-Version eines Fachs (z.B. "Bildungsplan 2016" vs. "Bildungsplan 2026"). +/// Ermöglicht es, während ein neuer Bildungsplan klassenstufenweise hochwächst, alten und neuen +/// Kompetenzkatalog parallel zu pflegen - +/// wählt je Kurs/Klasse, welche Version gilt. Je Fach ist genau eine Version +/// (Kurse ohne explizite Wahl nutzen sie, siehe Doc-Kommentar auf ). +public class CompetencyCatalogVersion +{ + public Guid Id { get; set; } = Guid.NewGuid(); + public Guid SubjectId { get; set; } + public string Name { get; set; } = ""; + public bool IsDefault { get; set; } + public int SortOrder { get; set; } + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; + public DateTime UpdatedAt { get; set; } = DateTime.UtcNow; +} + public class CompetencyItem { public Guid Id { get; set; } = Guid.NewGuid(); diff --git a/LehrerApp.Core/Models/LearningGroup.cs b/LehrerApp.Core/Models/LearningGroup.cs index bf6c2a9..7cc20db 100644 --- a/LehrerApp.Core/Models/LearningGroup.cs +++ b/LehrerApp.Core/Models/LearningGroup.cs @@ -8,6 +8,14 @@ public class LearningGroup public Guid? SubjectId { get; set; } public string SchoolYear { get; set; } = ""; public int GradeLevel { get; set; } + /// + /// Gewählte Bildungsplan-Version () für den Kompetenzkatalog + /// dieses Kurses/dieser Klasse. Null bedeutet bewusst nicht "keine Version", sondern "Standardversion + /// des Fachs" () - analog dazu, dass das Schuljahr + /// nicht auf der Mitgliedschaft dupliziert wird, sondern von der Gruppe geerbt wird. So brauchen + /// bestehende Kurse ohne neuen Bildungsplan keine Migration. + /// + public Guid? CompetencyCatalogVersionId { get; set; } public GradingSystem GradingSystem { get; set; } public int? HoursPerWeek { get; set; } public bool IsActive { get; set; } = true; diff --git a/LehrerApp.Core/Services/CompetencyCatalogImportService.cs b/LehrerApp.Core/Services/CompetencyCatalogImportService.cs index ef55087..6c6dd2e 100644 --- a/LehrerApp.Core/Services/CompetencyCatalogImportService.cs +++ b/LehrerApp.Core/Services/CompetencyCatalogImportService.cs @@ -15,6 +15,7 @@ public sealed class CompetencyCatalogImportPreview public required Guid SubjectId { get; init; } public required string SubjectName { get; init; } public required int GradeLevel { get; init; } + public required Guid VersionId { get; init; } public required List ImportedDomains { get; init; } public required string ExistingFingerprint { get; init; } public List Warnings { get; init; } = []; @@ -28,7 +29,7 @@ public sealed class CompetencyCatalogImportPreview public sealed class CompetencyCatalogImportService(ICompetencyDomainRepository repository) { public CompetencyCatalogImportPreview Analyze( - string json, Guid subjectId, string subjectName, int gradeLevel) + string json, Guid subjectId, string subjectName, int gradeLevel, Guid versionId) { CatalogDto dto; try @@ -100,7 +101,7 @@ public sealed class CompetencyCatalogImportService(ICompetencyDomainRepository r if (dto.GradeLevel is > 0 and <= 13 && dto.GradeLevel != gradeLevel) warnings.Add($"Die Datei nennt Klassenstufe {dto.GradeLevel}, ausgewählt ist Klassenstufe {gradeLevel}."); - var existing = repository.GetBySubjectAndGrade(subjectId, gradeLevel); + var existing = repository.GetBySubjectAndGrade(subjectId, gradeLevel, versionId); var conflicts = new List(); var newDomains = 0; var newItems = 0; @@ -147,6 +148,7 @@ public sealed class CompetencyCatalogImportService(ICompetencyDomainRepository r SubjectId = subjectId, SubjectName = subjectName, GradeLevel = gradeLevel, + VersionId = versionId, ImportedDomains = imported, ExistingFingerprint = Fingerprint(existing), Warnings = warnings, @@ -160,7 +162,7 @@ public sealed class CompetencyCatalogImportService(ICompetencyDomainRepository r public void Apply(CompetencyCatalogImportPreview preview, CompetencyCatalogImportMode mode, IReadOnlySet? useImportedConflicts = null) { - var current = repository.GetBySubjectAndGrade(preview.SubjectId, preview.GradeLevel); + var current = repository.GetBySubjectAndGrade(preview.SubjectId, preview.GradeLevel, preview.VersionId); if (Fingerprint(current) != preview.ExistingFingerprint) throw new InvalidOperationException( "Der Kompetenzkatalog wurde seit der Vorschau verändert. Bitte öffne den Import erneut."); @@ -169,7 +171,7 @@ public sealed class CompetencyCatalogImportService(ICompetencyDomainRepository r ? preview.ImportedDomains.Select((d, index) => CloneAsNew(d, preview.SubjectId, preview.GradeLevel, index)).ToList() : Merge(current, preview, useImportedConflicts ?? new HashSet()); - repository.ReplaceForSubjectAndGrade(preview.SubjectId, preview.GradeLevel, result); + repository.ReplaceForSubjectGradeAndVersion(preview.SubjectId, preview.GradeLevel, preview.VersionId, result); } private static List Merge(List existing, diff --git a/LehrerApp.Core/Services/CompetencyCatalogVersionResolver.cs b/LehrerApp.Core/Services/CompetencyCatalogVersionResolver.cs new file mode 100644 index 0000000..4adaee7 --- /dev/null +++ b/LehrerApp.Core/Services/CompetencyCatalogVersionResolver.cs @@ -0,0 +1,17 @@ +using LehrerApp.Core.Interfaces; + +namespace LehrerApp.Core.Services; + +/// Löst die für einen Kurs/eine Klasse geltende Bildungsplan-Version auf: die explizit am +/// Kurs gewählte Version (), falls sie +/// noch existiert, sonst die Standardversion des Fachs (siehe Doc-Kommentar auf dem Feld). Ohne Fach +/// gibt es keine Version (). +public static class CompetencyCatalogVersionResolver +{ + public static Guid Resolve(ICompetencyCatalogVersionRepository versions, Guid? subjectId, Guid? groupVersionId) + { + if (subjectId is not Guid sid) return Guid.Empty; + if (groupVersionId is Guid vid && versions.GetById(vid) is { } explicitVersion) return explicitVersion.Id; + return versions.GetOrCreateDefault(sid).Id; + } +} diff --git a/LehrerApp.Data.Tests/ChangeHookCascadeTests.cs b/LehrerApp.Data.Tests/ChangeHookCascadeTests.cs index 5968e88..a58bf94 100644 --- a/LehrerApp.Data.Tests/ChangeHookCascadeTests.cs +++ b/LehrerApp.Data.Tests/ChangeHookCascadeTests.cs @@ -123,7 +123,7 @@ public sealed class ChangeHookCascadeTests } [Fact] - public void CompetencyDomainRepository_ReplaceForSubjectAndGrade_LoestDeleteFuerAlteUndSaveFuerNeueAus() + public void CompetencyDomainRepository_ReplaceForSubjectGradeAndVersion_LoestDeleteFuerAlteUndSaveFuerNeueAus() { using var db = NewInMemoryContext(); var repo = new CompetencyDomainRepository(db); @@ -132,7 +132,7 @@ public sealed class ChangeHookCascadeTests var calls = new List<(string EntityType, string Operation)>(); db.OnChange = (type, id, op, payload) => calls.Add((type, op)); - repo.ReplaceForSubjectAndGrade(subjectId, 8, + repo.ReplaceForSubjectGradeAndVersion(subjectId, 8, Guid.Empty, [ new CompetencyDomain { Name = "Neu 1" }, new CompetencyDomain { Name = "Neu 2" }, @@ -145,7 +145,7 @@ public sealed class ChangeHookCascadeTests } [Fact] - public void CompetencyDomainRepository_DeleteBySubjectAndGrade_LoestEinEreignisProDomaenAus() + public void CompetencyDomainRepository_DeleteBySubjectGradeAndVersion_LoestEinEreignisProDomaenAus() { using var db = NewInMemoryContext(); var repo = new CompetencyDomainRepository(db); @@ -155,7 +155,7 @@ public sealed class ChangeHookCascadeTests var calls = new List<(string EntityType, string Operation)>(); db.OnChange = (type, id, op, payload) => calls.Add((type, op)); - repo.DeleteBySubjectAndGrade(subjectId, 8); + repo.DeleteBySubjectGradeAndVersion(subjectId, 8, Guid.Empty); Assert.Equal(2, calls.Count); Assert.All(calls, c => Assert.Equal((nameof(CompetencyDomain), "Delete"), c)); diff --git a/LehrerApp.Data.Tests/ChangeHookMatrixTests.cs b/LehrerApp.Data.Tests/ChangeHookMatrixTests.cs index 47ed3a4..bebf8ad 100644 --- a/LehrerApp.Data.Tests/ChangeHookMatrixTests.cs +++ b/LehrerApp.Data.Tests/ChangeHookMatrixTests.cs @@ -121,6 +121,14 @@ public sealed class ChangeHookMatrixTests return (subject.Id, () => repo.Delete(subject.Id)); }); + yield return Case("CompetencyCatalogVersion", nameof(CompetencyCatalogVersion), db => + { + var repo = new CompetencyCatalogVersionRepository(db); + var version = new CompetencyCatalogVersion { SubjectId = Guid.NewGuid(), Name = "Bildungsplan 2026" }; + repo.Save(version); + return (version.Id, () => repo.Delete(version.Id)); + }); + yield return Case("ShorthandCode", nameof(ShorthandCode), db => { var repo = new ShorthandCodeRepository(db); diff --git a/LehrerApp.Data.Tests/LiteDbContextTests.cs b/LehrerApp.Data.Tests/LiteDbContextTests.cs index 3e26242..c37d2b3 100644 --- a/LehrerApp.Data.Tests/LiteDbContextTests.cs +++ b/LehrerApp.Data.Tests/LiteDbContextTests.cs @@ -121,7 +121,7 @@ public sealed class LiteDbContextTests using var temp = new TempDatabase(); using var context = new LiteDbContext(temp.Path); - Assert.Equal(4, context.SchemaVersion); + Assert.Equal(5, context.SchemaVersion); } [Fact] @@ -132,7 +132,52 @@ public sealed class LiteDbContextTests using var second = new LiteDbContext(temp.Path); - Assert.Equal(4, second.SchemaVersion); + Assert.Equal(5, second.SchemaVersion); + } + + [Fact] + public void MigrateCompetencyCatalogVersions_ErzeugtStandardversionJeFachUndVerknuepftBestehendeBereiche() + { + using var temp = new TempDatabase(); + var subjectAId = Guid.NewGuid(); + var subjectBId = Guid.NewGuid(); + var domainA1Id = Guid.NewGuid(); + var domainA2Id = Guid.NewGuid(); + var domainBId = Guid.NewGuid(); + + using (var legacy = new LiteDatabase(temp.Path)) + { + var domains = legacy.GetCollection("competency_domains"); + domains.Insert(new BsonDocument + { + ["_id"] = domainA1Id, [nameof(CompetencyDomain.SubjectId)] = subjectAId, + [nameof(CompetencyDomain.GradeLevel)] = 8, [nameof(CompetencyDomain.Name)] = "Bereich A1", + }); + domains.Insert(new BsonDocument + { + ["_id"] = domainA2Id, [nameof(CompetencyDomain.SubjectId)] = subjectAId, + [nameof(CompetencyDomain.GradeLevel)] = 9, [nameof(CompetencyDomain.Name)] = "Bereich A2", + }); + domains.Insert(new BsonDocument + { + ["_id"] = domainBId, [nameof(CompetencyDomain.SubjectId)] = subjectBId, + [nameof(CompetencyDomain.GradeLevel)] = 8, [nameof(CompetencyDomain.Name)] = "Bereich B", + }); + } + + using var context = new LiteDbContext(temp.Path); + + var versions = context.CompetencyCatalogVersions.FindAll().ToList(); + var versionA = Assert.Single(versions, v => v.SubjectId == subjectAId); + var versionB = Assert.Single(versions, v => v.SubjectId == subjectBId); + Assert.True(versionA.IsDefault); + Assert.True(versionB.IsDefault); + Assert.Equal("Bestehender Kompetenzkatalog", versionA.Name); + + var domainsAfter = context.CompetencyDomains.FindAll().ToDictionary(d => d.Id); + Assert.Equal(versionA.Id, domainsAfter[domainA1Id].VersionId); + Assert.Equal(versionA.Id, domainsAfter[domainA2Id].VersionId); + Assert.Equal(versionB.Id, domainsAfter[domainBId].VersionId); } [Fact] diff --git a/LehrerApp.Data.Tests/RepositoryTests.cs b/LehrerApp.Data.Tests/RepositoryTests.cs index 8aa38ae..c6cc065 100644 --- a/LehrerApp.Data.Tests/RepositoryTests.cs +++ b/LehrerApp.Data.Tests/RepositoryTests.cs @@ -1249,4 +1249,93 @@ public sealed class RepositoryTests Assert.Equal(studentId, item.StudentId); Assert.True(item.IsDone); } + + // ── CompetencyCatalogVersionRepository ────────────────────────────────────────────────── + + [Fact] + public void CompetencyCatalogVersionRepository_GetOrCreateDefault_LegtGenauEinmalAn() + { + using var db = NewInMemoryContext(); + var repo = new CompetencyCatalogVersionRepository(db); + var subjectId = Guid.NewGuid(); + + var first = repo.GetOrCreateDefault(subjectId); + var second = repo.GetOrCreateDefault(subjectId); + + Assert.Equal(first.Id, second.Id); + Assert.True(first.IsDefault); + Assert.Single(repo.GetBySubject(subjectId)); + } + + [Fact] + public void CompetencyCatalogVersionRepository_Save_NimmtVorherigeStandardversionZurueck() + { + using var db = NewInMemoryContext(); + var repo = new CompetencyCatalogVersionRepository(db); + var subjectId = Guid.NewGuid(); + var alt = repo.GetOrCreateDefault(subjectId); + var neu = new CompetencyCatalogVersion { SubjectId = subjectId, Name = "Bildungsplan 2026", IsDefault = true }; + + repo.Save(neu); + + Assert.False(repo.GetById(alt.Id)!.IsDefault); + Assert.True(repo.GetById(neu.Id)!.IsDefault); + } + + [Fact] + public void CompetencyCatalogVersionRepository_Delete_BlockiertStandardversion() + { + using var db = NewInMemoryContext(); + var repo = new CompetencyCatalogVersionRepository(db); + var version = repo.GetOrCreateDefault(Guid.NewGuid()); + + Assert.Throws(() => repo.Delete(version.Id)); + } + + [Fact] + public void CompetencyCatalogVersionRepository_Delete_BlockiertBeiReferenzierendemKompetenzbereich() + { + using var db = NewInMemoryContext(); + var versionRepo = new CompetencyCatalogVersionRepository(db); + var domainRepo = new CompetencyDomainRepository(db); + var subjectId = Guid.NewGuid(); + var version = new CompetencyCatalogVersion { SubjectId = subjectId, Name = "Bildungsplan 2026" }; + versionRepo.Save(version); + domainRepo.Save(new CompetencyDomain { SubjectId = subjectId, GradeLevel = 5, VersionId = version.Id, Name = "Bereich" }); + + Assert.Throws(() => versionRepo.Delete(version.Id)); + } + + [Fact] + public void CompetencyCatalogVersionRepository_Delete_BlockiertBeiReferenzierenderGruppe() + { + using var db = NewInMemoryContext(); + var versionRepo = new CompetencyCatalogVersionRepository(db); + var groupRepo = new GroupRepository(db); + var subject = new Subject { Name = "Chemie" }; + new SubjectRepository(db).Save(subject); + var version = new CompetencyCatalogVersion { SubjectId = subject.Id, Name = "Bildungsplan 2026" }; + versionRepo.Save(version); + groupRepo.Save(new LearningGroup + { + Name = "9a", SchoolYear = "2025/26", SubjectId = subject.Id, CompetencyCatalogVersionId = version.Id, + }); + + Assert.Throws(() => versionRepo.Delete(version.Id)); + } + + [Fact] + public void CompetencyCatalogVersionRepository_Delete_ErlaubtUnbenutzteNichtStandardversion() + { + using var db = NewInMemoryContext(); + var repo = new CompetencyCatalogVersionRepository(db); + var subjectId = Guid.NewGuid(); + repo.GetOrCreateDefault(subjectId); + var extra = new CompetencyCatalogVersion { SubjectId = subjectId, Name = "Entwurf" }; + repo.Save(extra); + + repo.Delete(extra.Id); + + Assert.Null(repo.GetById(extra.Id)); + } } diff --git a/LehrerApp.Data/LiteDbContext.cs b/LehrerApp.Data/LiteDbContext.cs index e00646f..34699c1 100644 --- a/LehrerApp.Data/LiteDbContext.cs +++ b/LehrerApp.Data/LiteDbContext.cs @@ -15,7 +15,7 @@ public class LiteDbContext : IDisposable /// Aktuelle Schema-Version. Migrationsschritte werden versioniert unter /// ergänzt, statt bei jedem Start erneut /// (idempotent, aber unnötig) über alle Daten zu laufen. - private const int CurrentSchemaVersion = 4; + private const int CurrentSchemaVersion = 5; private readonly LiteDatabase _db; @@ -61,6 +61,7 @@ public class LiteDbContext : IDisposable public ILiteCollection ParticipationSections => _db.GetCollection("participation_sections"); public ILiteCollection Subjects => _db.GetCollection("subjects"); public ILiteCollection CompetencyDomains => _db.GetCollection("competency_domains"); + public ILiteCollection CompetencyCatalogVersions => _db.GetCollection("competency_catalog_versions"); public ILiteCollection ShorthandCodes => _db.GetCollection("shorthand_codes"); public ILiteCollection AlternativeLessonPaths => _db.GetCollection("alternative_lesson_paths"); public ILiteCollection TimetableSlots => _db.GetCollection("timetable_slots"); @@ -262,6 +263,11 @@ public class LiteDbContext : IDisposable MigrateLessonAlternativePaths(); version = 4; } + if (version < 5) + { + MigrateCompetencyCatalogVersions(); + version = 5; + } WriteSchemaVersion(version); } @@ -485,6 +491,31 @@ public class LiteDbContext : IDisposable } } + /// Backfill für : bestehende Bereiche (vor + /// Einführung der Bildungsplan-Versionierung, also ohne VersionId) bekommen je Fach genau eine + /// neu angelegte Standardversion ("Bestehender Kompetenzkatalog"). LearningGroup.CompetencyCatalogVersionId + /// bleibt bewusst null (= Standardversion) - siehe Doc-Kommentar auf dem Feld. + private void MigrateCompetencyCatalogVersions() + { + var defaultVersionBySubject = new Dictionary(); + foreach (var domain in CompetencyDomains.FindAll().ToList()) + { + if (domain.VersionId != Guid.Empty) continue; + if (!defaultVersionBySubject.TryGetValue(domain.SubjectId, out var versionId)) + { + var version = new CompetencyCatalogVersion + { + SubjectId = domain.SubjectId, Name = "Bestehender Kompetenzkatalog", IsDefault = true, + }; + CompetencyCatalogVersions.Upsert(version); + versionId = version.Id; + defaultVersionBySubject[domain.SubjectId] = versionId; + } + domain.VersionId = versionId; + CompetencyDomains.Update(domain); + } + } + private void EnsureIndexes() { Students.EnsureIndex(x => x.LastName); @@ -530,6 +561,8 @@ public class LiteDbContext : IDisposable Subjects.EnsureIndex("ux_subject_name", BsonExpression.Create("LOWER(TRIM($.Name))"), unique: true); CompetencyDomains.EnsureIndex(x => x.SubjectId); CompetencyDomains.EnsureIndex(x => x.GradeLevel); + CompetencyDomains.EnsureIndex(x => x.VersionId); + CompetencyCatalogVersions.EnsureIndex(x => x.SubjectId); 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); TimetableSlots.EnsureIndex(x => x.GroupId); diff --git a/LehrerApp.Data/Repositories/AllRepositories.cs b/LehrerApp.Data/Repositories/AllRepositories.cs index 304a6c2..04bf926 100644 --- a/LehrerApp.Data/Repositories/AllRepositories.cs +++ b/LehrerApp.Data/Repositories/AllRepositories.cs @@ -670,8 +670,9 @@ public class SubjectRepository(LiteDbContext db) : ISubjectRepository } public void Delete(Guid id) { - if (db.Groups.Exists(g => g.SubjectId == id) || db.CompetencyDomains.Exists(d => d.SubjectId == id)) - throw new InvalidOperationException("Das Fach wird noch von einer Lerngruppe oder einem Kompetenzkatalog verwendet."); + if (db.Groups.Exists(g => g.SubjectId == id) || db.CompetencyDomains.Exists(d => d.SubjectId == id) + || db.CompetencyCatalogVersions.Exists(v => v.SubjectId == id)) + throw new InvalidOperationException("Das Fach wird noch von einer Lerngruppe, einem Kompetenzkatalog oder einer Bildungsplan-Version verwendet."); db.Subjects.Delete(id); db.OnChange?.Invoke(nameof(Subject), id.ToString(), "Delete", null); } @@ -916,14 +917,14 @@ public class UntisHubJobStateRepository(LiteDbContext db) : IUntisHubJobStateRep public class CompetencyDomainRepository(LiteDbContext db) : ICompetencyDomainRepository { - public List GetBySubjectAndGrade(Guid subjectId, int gradeLevel) => + public List GetBySubjectAndGrade(Guid subjectId, int gradeLevel, Guid versionId) => db.CompetencyDomains - .Find(d => d.SubjectId == subjectId && d.GradeLevel == gradeLevel) + .Find(d => d.SubjectId == subjectId && d.GradeLevel == gradeLevel && d.VersionId == versionId) .OrderBy(d => d.SortOrder) .ToList(); - public List GetBySubject(Guid subjectId) => + public List GetBySubjectAndVersion(Guid subjectId, Guid versionId) => db.CompetencyDomains - .Find(d => d.SubjectId == subjectId) + .Find(d => d.SubjectId == subjectId && d.VersionId == versionId) .OrderBy(d => d.GradeLevel).ThenBy(d => d.SortOrder) .ToList(); public CompetencyDomain? GetById(Guid id) => db.CompetencyDomains.FindById(id); @@ -938,22 +939,22 @@ public class CompetencyDomainRepository(LiteDbContext db) : ICompetencyDomainRep db.CompetencyDomains.Delete(id); db.OnChange?.Invoke(nameof(CompetencyDomain), id.ToString(), "Delete", null); } - public void DeleteBySubjectAndGrade(Guid subjectId, int gradeLevel) + public void DeleteBySubjectGradeAndVersion(Guid subjectId, int gradeLevel, Guid versionId) { foreach (var d in db.CompetencyDomains - .Find(d => d.SubjectId == subjectId && d.GradeLevel == gradeLevel) + .Find(d => d.SubjectId == subjectId && d.GradeLevel == gradeLevel && d.VersionId == versionId) .ToList()) { db.CompetencyDomains.Delete(d.Id); db.OnChange?.Invoke(nameof(CompetencyDomain), d.Id.ToString(), "Delete", null); } } - public void ReplaceForSubjectAndGrade(Guid subjectId, int gradeLevel, List domains) + public void ReplaceForSubjectGradeAndVersion(Guid subjectId, int gradeLevel, Guid versionId, List domains) { db.ExecuteInTransaction(() => { foreach (var domain in db.CompetencyDomains - .Find(d => d.SubjectId == subjectId && d.GradeLevel == gradeLevel).ToList()) + .Find(d => d.SubjectId == subjectId && d.GradeLevel == gradeLevel && d.VersionId == versionId).ToList()) { db.CompetencyDomains.Delete(domain.Id); db.OnChange?.Invoke(nameof(CompetencyDomain), domain.Id.ToString(), "Delete", null); @@ -963,6 +964,7 @@ public class CompetencyDomainRepository(LiteDbContext db) : ICompetencyDomainRep { domain.SubjectId = subjectId; domain.GradeLevel = gradeLevel; + domain.VersionId = versionId; domain.UpdatedAt = now; db.CompetencyDomains.Upsert(domain); db.OnChange?.Invoke(nameof(CompetencyDomain), domain.Id.ToString(), "Save", domain); @@ -971,6 +973,57 @@ public class CompetencyDomainRepository(LiteDbContext db) : ICompetencyDomainRep } } +public class CompetencyCatalogVersionRepository(LiteDbContext db) : ICompetencyCatalogVersionRepository +{ + public List GetBySubject(Guid subjectId) => + db.CompetencyCatalogVersions + .Find(v => v.SubjectId == subjectId) + .OrderBy(v => v.SortOrder) + .ToList(); + public CompetencyCatalogVersion? GetById(Guid id) => db.CompetencyCatalogVersions.FindById(id); + public CompetencyCatalogVersion GetOrCreateDefault(Guid subjectId) + { + var existing = GetBySubject(subjectId).FirstOrDefault(v => v.IsDefault); + if (existing is not null) return existing; + var version = new CompetencyCatalogVersion + { + SubjectId = subjectId, Name = "Bestehender Kompetenzkatalog", IsDefault = true, + }; + Save(version); + return version; + } + public void Save(CompetencyCatalogVersion v) + { + v.Name = v.Name.Trim(); + if (v.Name.Length == 0) throw new ArgumentException("Der Name der Version darf nicht leer sein."); + if (v.IsDefault) + { + // Höchstens eine Standardversion je Fach - beim Setzen alle anderen des Fachs zurücknehmen. + foreach (var other in db.CompetencyCatalogVersions.Find(o => o.SubjectId == v.SubjectId && o.Id != v.Id && o.IsDefault).ToList()) + { + other.IsDefault = false; + other.UpdatedAt = DateTime.UtcNow; + db.CompetencyCatalogVersions.Upsert(other); + db.OnChange?.Invoke(nameof(CompetencyCatalogVersion), other.Id.ToString(), "Save", other); + } + } + v.UpdatedAt = DateTime.UtcNow; + db.CompetencyCatalogVersions.Upsert(v); + db.OnChange?.Invoke(nameof(CompetencyCatalogVersion), v.Id.ToString(), "Save", v); + } + public void Delete(Guid id) + { + var version = db.CompetencyCatalogVersions.FindById(id); + if (version is null) return; + if (version.IsDefault) + throw new InvalidOperationException("Die Standardversion eines Fachs kann nicht gelöscht werden. Erst eine andere Version als Standard setzen."); + if (db.CompetencyDomains.Exists(d => d.VersionId == id) || db.Groups.Exists(g => g.CompetencyCatalogVersionId == id)) + throw new InvalidOperationException("Die Version wird noch von einem Kompetenzkatalog oder einem Kurs verwendet."); + db.CompetencyCatalogVersions.Delete(id); + db.OnChange?.Invoke(nameof(CompetencyCatalogVersion), id.ToString(), "Delete", null); + } +} + /// Papierkorb (14.3) — bewusst nicht sync-fähig: TrashedItem wird nie über /// db.OnChange gemeldet, bleibt also rein lokal auf dem Gerät, auf dem gelöscht wurde. Ein /// "Fehlklick sofort rückgängig machen"-Werkzeug, kein geräteübergreifendes Archiv. diff --git a/LehrerApp.Desktop.Tests/AiPlanningServiceTests.cs b/LehrerApp.Desktop.Tests/AiPlanningServiceTests.cs index 404d7ba..9ad7acc 100644 --- a/LehrerApp.Desktop.Tests/AiPlanningServiceTests.cs +++ b/LehrerApp.Desktop.Tests/AiPlanningServiceTests.cs @@ -15,8 +15,10 @@ namespace LehrerApp.Desktop.Tests; public sealed class AiPlanningServiceTests { private static AiPlanningService Build(FakeLessons lessons, FakeGroups groups, FakeSubjects subjects, - FakeCompetencyDomains competencyDomains, FakeAlternativeLessonPaths altPaths) => - new(new HttpClient(), lessons, groups, subjects, competencyDomains, altPaths); + FakeCompetencyDomains competencyDomains, FakeAlternativeLessonPaths altPaths, + FakeCompetencyCatalogVersions? competencyVersions = null) => + new(new HttpClient(), lessons, groups, subjects, competencyDomains, + competencyVersions ?? new FakeCompetencyCatalogVersions(), altPaths); [Fact] public void ParsePlanningLessons_AkzeptiertNormaleAntwortUndDeutschesDatum() @@ -90,7 +92,7 @@ public sealed class AiPlanningServiceTests BaseAddress = new Uri("https://example.invalid/"), }; var service = new AiPlanningService(http, new FakeLessons(), new FakeGroups([]), new FakeSubjects([]), - new FakeCompetencyDomains(), new FakeAlternativeLessonPaths([])); + new FakeCompetencyDomains(), new FakeCompetencyCatalogVersions(), new FakeAlternativeLessonPaths([])); var error = await Assert.ThrowsAsync(() => service.RequestPlanAsync(new Unit(), "", "token")); @@ -189,7 +191,7 @@ public sealed class AiPlanningServiceTests BaseAddress = new Uri("https://example.invalid/"), }; return new AiPlanningService(http, new FakeLessons(), new FakeGroups([]), new FakeSubjects([]), - new FakeCompetencyDomains(), new FakeAlternativeLessonPaths([])); + new FakeCompetencyDomains(), new FakeCompetencyCatalogVersions(), new FakeAlternativeLessonPaths([])); } private sealed class StaticResponseHandler(HttpStatusCode status, string body) : HttpMessageHandler @@ -235,14 +237,16 @@ public sealed class AiPlanningServiceTests var unit = new Unit { GroupId = group.Id, Title = "T" }; var domains = new FakeCompetencyDomains(); + var versions = new FakeCompetencyCatalogVersions(); + var version = versions.GetOrCreateDefault(subject.Id); domains.Add(new CompetencyDomain { - SubjectId = subject.Id, GradeLevel = 9, Name = "Chemische Reaktionen", + SubjectId = subject.Id, GradeLevel = 9, VersionId = version.Id, Name = "Chemische Reaktionen", Items = [new CompetencyItem { Code = "C1", Description = "Redoxreaktionen erklären" }], }); var service = Build(new FakeLessons(), new FakeGroups([group]), new FakeSubjects([subject]), - domains, new FakeAlternativeLessonPaths([])); + domains, new FakeAlternativeLessonPaths([]), versions); var context = service.BuildContext(unit, ""); diff --git a/LehrerApp.Desktop.Tests/CompetencyOverviewViewModelTests.cs b/LehrerApp.Desktop.Tests/CompetencyOverviewViewModelTests.cs index 4978bc9..23bed65 100644 --- a/LehrerApp.Desktop.Tests/CompetencyOverviewViewModelTests.cs +++ b/LehrerApp.Desktop.Tests/CompetencyOverviewViewModelTests.cs @@ -26,16 +26,19 @@ public sealed class CompetencyOverviewViewModelTests }; var results = new FakeResults(); results.Add(new ExamResult { ExamId = exam.Id, StudentId = student.Id, Points = [5] }); + var versions = new FakeCompetencyCatalogVersions(); + var version = versions.GetOrCreateDefault(subjectId); var catalog = new FakeCompetencyDomains(); catalog.Add(new CompetencyDomain { SubjectId = subjectId, GradeLevel = 8, + VersionId = version.Id, Name = "Bereich", Items = [new CompetencyItem { Code = "K1", Description = "Kompetenz" }], }); var vm = new CompetencyOverviewTabViewModel(units, new FakeExams([exam]), results, - catalog, new FakeStudents([student]), new CompetencyAnalysisService()); + catalog, versions, new FakeStudents([student]), new CompetencyAnalysisService()); vm.Initialize(group); diff --git a/LehrerApp.Desktop.Tests/Fakes.cs b/LehrerApp.Desktop.Tests/Fakes.cs index 21a8acb..7b39501 100644 --- a/LehrerApp.Desktop.Tests/Fakes.cs +++ b/LehrerApp.Desktop.Tests/Fakes.cs @@ -30,7 +30,7 @@ public static class TestSupport /// solange AiSettingsService.IsLoggedIn false ist. public static AiPlanningService BuildAiPlanningService() => new( new HttpClient(), new FakeLessons(), new FakeGroups([]), new FakeSubjects([]), - new FakeCompetencyDomains(), new FakeAlternativeLessonPaths([])); + new FakeCompetencyDomains(), new FakeCompetencyCatalogVersions(), new FakeAlternativeLessonPaths([])); /// Analog zu , eigenes Temp-Verzeichnis je Aufruf. public static McpSettingsService BuildMcpSettingsService() @@ -391,24 +391,57 @@ public class FakeCompetencyDomains : ICompetencyDomainRepository { private readonly List _all = []; public void Add(CompetencyDomain d) => _all.Add(d); - public List GetBySubjectAndGrade(Guid subjectId, int gradeLevel) => - _all.Where(d => d.SubjectId == subjectId && d.GradeLevel == gradeLevel) + public List GetBySubjectAndGrade(Guid subjectId, int gradeLevel, Guid versionId) => + _all.Where(d => d.SubjectId == subjectId && d.GradeLevel == gradeLevel && d.VersionId == versionId) .OrderBy(d => d.SortOrder).ToList(); - public List GetBySubject(Guid subjectId) => - _all.Where(d => d.SubjectId == subjectId) + public List GetBySubjectAndVersion(Guid subjectId, Guid versionId) => + _all.Where(d => d.SubjectId == subjectId && d.VersionId == versionId) .OrderBy(d => d.GradeLevel).ThenBy(d => d.SortOrder).ToList(); public CompetencyDomain? GetById(Guid id) => _all.FirstOrDefault(d => d.Id == id); public void Save(CompetencyDomain domain) { _all.RemoveAll(d => d.Id == domain.Id); _all.Add(domain); } public void Delete(Guid id) => _all.RemoveAll(d => d.Id == id); - public void DeleteBySubjectAndGrade(Guid subjectId, int gradeLevel) => - _all.RemoveAll(d => d.SubjectId == subjectId && d.GradeLevel == gradeLevel); - public void ReplaceForSubjectAndGrade(Guid subjectId, int gradeLevel, List domains) + public void DeleteBySubjectGradeAndVersion(Guid subjectId, int gradeLevel, Guid versionId) => + _all.RemoveAll(d => d.SubjectId == subjectId && d.GradeLevel == gradeLevel && d.VersionId == versionId); + public void ReplaceForSubjectGradeAndVersion(Guid subjectId, int gradeLevel, Guid versionId, List domains) { - DeleteBySubjectAndGrade(subjectId, gradeLevel); + DeleteBySubjectGradeAndVersion(subjectId, gradeLevel, versionId); + foreach (var d in domains) { d.SubjectId = subjectId; d.GradeLevel = gradeLevel; d.VersionId = versionId; } _all.AddRange(domains); } } +public class FakeCompetencyCatalogVersions : ICompetencyCatalogVersionRepository +{ + private readonly List _all = []; + public void Add(CompetencyCatalogVersion v) => _all.Add(v); + public List GetBySubject(Guid subjectId) => + _all.Where(v => v.SubjectId == subjectId).OrderBy(v => v.SortOrder).ToList(); + public CompetencyCatalogVersion? GetById(Guid id) => _all.FirstOrDefault(v => v.Id == id); + public CompetencyCatalogVersion GetOrCreateDefault(Guid subjectId) + { + var existing = GetBySubject(subjectId).FirstOrDefault(v => v.IsDefault); + if (existing is not null) return existing; + var version = new CompetencyCatalogVersion { SubjectId = subjectId, Name = "Bestehender Kompetenzkatalog", IsDefault = true }; + Save(version); + return version; + } + public void Save(CompetencyCatalogVersion version) + { + if (version.IsDefault) + foreach (var other in _all.Where(v => v.SubjectId == version.SubjectId && v.Id != version.Id)) other.IsDefault = false; + _all.RemoveAll(v => v.Id == version.Id); + _all.Add(version); + } + public void Delete(Guid id) + { + var version = _all.FirstOrDefault(v => v.Id == id); + if (version is null) return; + if (version.IsDefault) + throw new InvalidOperationException("Die Standardversion eines Fachs kann nicht gelöscht werden. Erst eine andere Version als Standard setzen."); + _all.RemoveAll(v => v.Id == id); + } +} + public class FakeShorthandCodes(List all) : IShorthandCodeRepository { public List GetAll() => all; diff --git a/LehrerApp.Desktop.Tests/GroupDetailViewModelTests.cs b/LehrerApp.Desktop.Tests/GroupDetailViewModelTests.cs index eb84f63..8b747ec 100644 --- a/LehrerApp.Desktop.Tests/GroupDetailViewModelTests.cs +++ b/LehrerApp.Desktop.Tests/GroupDetailViewModelTests.cs @@ -21,12 +21,12 @@ public sealed class GroupDetailViewModelTests new GroupOverviewViewModel(new FakeLessons(), exams, new FakeResults(), new FakeSessions([]), new FakeEntries(), students, new FakeDocumentation(), tasks, groups, new AttendanceBalanceService(), new GradingService(), new SchoolYearService()), new ParticipationTabViewModel(new FakeSessions([]), new FakeEntries(), new FakeAspects(), - students, memberships, groups, new FakeCompetencyDomains()), + students, memberships, groups, new FakeCompetencyDomains(), new FakeCompetencyCatalogVersions()), new GradeOverviewTabViewModel(grades, exams, new FakeResults(), students, memberships, new GradingService()), new PlanningTabViewModel(new FakeUnits(), new FakeLessons(), groups, subjects, - new FakeCompetencyDomains(), TestSupport.BuildAiSettingsService(), new FakeSessions([])), + new FakeCompetencyDomains(), new FakeCompetencyCatalogVersions(), TestSupport.BuildAiSettingsService(), new FakeSessions([])), new CompetencyOverviewTabViewModel(new FakeUnits(), exams, new FakeResults(), - new FakeCompetencyDomains(), students, new CompetencyAnalysisService()), + new FakeCompetencyDomains(), new FakeCompetencyCatalogVersions(), students, new CompetencyAnalysisService()), new SeatingPlanTabViewModel(new FakeSeatingPlans(), students, memberships, new FakeSessions([]), new FakeEntries(), new FakeAspects()), new GroupDocumentationTabViewModel(new FakeDocumentation(), students, groups)); diff --git a/LehrerApp.Desktop.Tests/HazardAssessmentWizardViewModelTests.cs b/LehrerApp.Desktop.Tests/HazardAssessmentWizardViewModelTests.cs index 72b7ae6..af08d66 100644 --- a/LehrerApp.Desktop.Tests/HazardAssessmentWizardViewModelTests.cs +++ b/LehrerApp.Desktop.Tests/HazardAssessmentWizardViewModelTests.cs @@ -254,7 +254,7 @@ public sealed class HazardAssessmentWizardViewModelTests aiSettings.SetEnabled(true); aiSettings.SetCredentialsAndToken("sebastian", "token-abc"); var ai = new AiPlanningService(new HttpClient(), new FakeLessons(), new FakeGroups([]), - new FakeSubjects([]), new FakeCompetencyDomains(), new FakeAlternativeLessonPaths([])); + new FakeSubjects([]), new FakeCompetencyDomains(), new FakeCompetencyCatalogVersions(), new FakeAlternativeLessonPaths([])); var unit = new Unit { Title = "Redoxreaktionen" }; var lesson = new Lesson { Topic = "Natrium in Wasser" }; diff --git a/LehrerApp.Desktop.Tests/McpToolsTests.cs b/LehrerApp.Desktop.Tests/McpToolsTests.cs index e578491..8936af0 100644 --- a/LehrerApp.Desktop.Tests/McpToolsTests.cs +++ b/LehrerApp.Desktop.Tests/McpToolsTests.cs @@ -15,7 +15,8 @@ public sealed class McpToolsTests Assert.Equal( new[] { - "download_lesson_attachment", "get_competency_catalog", "get_exams", "get_grades", + "download_lesson_attachment", "get_competency_catalog", "get_competency_plan_versions", + "get_exams", "get_grades", "get_groups", "get_lesson_plans", "get_named_untis_absence_pattern", "get_schedule", "get_students", "get_subjects", "get_time_entries", "get_untis_absence_rows", "get_untis_hub_status", "list_letter_templates", "render_letter", @@ -30,10 +31,12 @@ public sealed class McpToolsTests new[] { "add_competency_item", "add_lesson_attachment", "add_lesson_competency", "add_lesson_phase", - "apply_untis_absence_status", "create_competency_domain", "create_grade_entry", "create_lesson", + "apply_untis_absence_status", "create_competency_domain", "create_competency_plan_version", + "create_grade_entry", "create_lesson", "create_subject", "create_time_entry", "create_unit", "move_lesson", "remove_competency_item", "remove_lesson_competency", "remove_lesson_phase", "update_competency_domain", - "update_competency_item", "update_lesson", "update_lesson_phase", "update_student_group_assignment", + "update_competency_item", "update_competency_plan_version", "update_group_competency_version", + "update_lesson", "update_lesson_phase", "update_student_group_assignment", "update_subject", "update_unit", }, McpToolScope.AllowedWriteTools.OrderBy(n => n, StringComparer.Ordinal)); @@ -43,7 +46,7 @@ public sealed class McpToolsTests public void AllowedDestructiveWriteTools_EnthaeltDeleteLessonSubjectUndCompetencyDomain() { Assert.Equal( - new[] { "delete_competency_domain", "delete_lesson", "delete_subject" }, + new[] { "delete_competency_domain", "delete_competency_plan_version", "delete_lesson", "delete_subject" }, McpToolScope.AllowedDestructiveWriteTools.OrderBy(n => n, StringComparer.Ordinal)); } @@ -68,7 +71,7 @@ public sealed class McpToolsTests { Name = "9a", Type = GroupType.Class, SchoolYear = "2025/26", GradeLevel = 9, IsActive = true, }; - var tool = new GroupTools(new FakeGroups([group])); + var tool = new GroupTools(new FakeGroups([group]), new FakeCompetencyCatalogVersions(), new FakeMcpConfirmation()); var result = Assert.Single(tool.GetGroups()); @@ -735,7 +738,7 @@ public sealed class McpToolsTests public void GetSubjects_LiefertAlleFaecher() { var subject = new Subject { Name = "Mathematik", ShortName = "Ma" }; - var tool = new CompetencyTools(new FakeSubjects([subject]), new FakeCompetencyDomains(), new FakeMcpConfirmation()); + var tool = new CompetencyTools(new FakeSubjects([subject]), new FakeCompetencyDomains(), new FakeCompetencyCatalogVersions(), new FakeMcpConfirmation()); var dto = Assert.Single(tool.GetSubjects()); @@ -746,7 +749,7 @@ public sealed class McpToolsTests public async Task CreateSubject_NutzerBestaetigt_SpeichertFach() { var subjects = new FakeSubjects([]); - var tool = new CompetencyTools(subjects, new FakeCompetencyDomains(), new FakeMcpConfirmation()); + var tool = new CompetencyTools(subjects, new FakeCompetencyDomains(), new FakeCompetencyCatalogVersions(), new FakeMcpConfirmation()); var result = await tool.CreateSubject("Mathematik", "Ma"); @@ -759,7 +762,7 @@ public sealed class McpToolsTests public async Task CreateSubject_NameBereitsVergeben_LiefertFehler() { var subjects = new FakeSubjects([new Subject { Name = "Mathematik" }]); - var tool = new CompetencyTools(subjects, new FakeCompetencyDomains(), new FakeMcpConfirmation()); + var tool = new CompetencyTools(subjects, new FakeCompetencyDomains(), new FakeCompetencyCatalogVersions(), new FakeMcpConfirmation()); var result = await tool.CreateSubject("Mathematik"); @@ -772,7 +775,7 @@ public sealed class McpToolsTests { var subject = new Subject { Name = "Mathematik", ShortName = "Ma" }; var subjects = new FakeSubjects([subject]); - var tool = new CompetencyTools(subjects, new FakeCompetencyDomains(), new FakeMcpConfirmation()); + var tool = new CompetencyTools(subjects, new FakeCompetencyDomains(), new FakeCompetencyCatalogVersions(), new FakeMcpConfirmation()); var result = await tool.UpdateSubject(subject.Id, shortName: "M"); @@ -787,7 +790,7 @@ public sealed class McpToolsTests { var subject = new Subject { Name = "Mathematik" }; var subjects = new FakeSubjects([subject]); - var tool = new CompetencyTools(subjects, new FakeCompetencyDomains(), new FakeMcpConfirmation()); + var tool = new CompetencyTools(subjects, new FakeCompetencyDomains(), new FakeCompetencyCatalogVersions(), new FakeMcpConfirmation()); var result = await tool.DeleteSubject(subject.Id); @@ -799,7 +802,7 @@ public sealed class McpToolsTests public async Task DeleteSubject_UnbekannteId_LiefertFehlerOhneNachfrage() { var confirmation = new FakeMcpConfirmation(); - var tool = new CompetencyTools(new FakeSubjects([]), new FakeCompetencyDomains(), confirmation); + var tool = new CompetencyTools(new FakeSubjects([]), new FakeCompetencyDomains(), new FakeCompetencyCatalogVersions(), confirmation); var result = await tool.DeleteSubject(Guid.NewGuid()); @@ -812,9 +815,11 @@ public sealed class McpToolsTests { var subjectId = Guid.NewGuid(); var domains = new FakeCompetencyDomains(); - domains.Add(new CompetencyDomain { SubjectId = subjectId, GradeLevel = 5, Name = "Zahlen" }); - domains.Add(new CompetencyDomain { SubjectId = subjectId, GradeLevel = 9, Name = "Funktionen" }); - var tool = new CompetencyTools(new FakeSubjects([]), domains, new FakeMcpConfirmation()); + var versions = new FakeCompetencyCatalogVersions(); + var version = versions.GetOrCreateDefault(subjectId); + domains.Add(new CompetencyDomain { SubjectId = subjectId, GradeLevel = 5, VersionId = version.Id, Name = "Zahlen" }); + domains.Add(new CompetencyDomain { SubjectId = subjectId, GradeLevel = 9, VersionId = version.Id, Name = "Funktionen" }); + var tool = new CompetencyTools(new FakeSubjects([]), domains, versions, new FakeMcpConfirmation()); var result = tool.GetCompetencyCatalog(subjectId); @@ -826,12 +831,14 @@ public sealed class McpToolsTests { var subject = new Subject { Name = "Mathematik" }; var domains = new FakeCompetencyDomains(); - var tool = new CompetencyTools(new FakeSubjects([subject]), domains, new FakeMcpConfirmation()); + var versions = new FakeCompetencyCatalogVersions(); + var tool = new CompetencyTools(new FakeSubjects([subject]), domains, versions, new FakeMcpConfirmation()); var result = await tool.CreateCompetencyDomain(subject.Id, 9, "Funktionen"); Assert.True(result.Applied); - var domain = Assert.Single(domains.GetBySubjectAndGrade(subject.Id, 9)); + var defaultVersionId = versions.GetOrCreateDefault(subject.Id).Id; + var domain = Assert.Single(domains.GetBySubjectAndGrade(subject.Id, 9, defaultVersionId)); Assert.Equal("Funktionen", domain.Name); } @@ -842,7 +849,7 @@ public sealed class McpToolsTests domain.Items.Add(new CompetencyItem { Code = "M.9.1", Description = "..." }); var domains = new FakeCompetencyDomains(); domains.Add(domain); - var tool = new CompetencyTools(new FakeSubjects([]), domains, new FakeMcpConfirmation()); + var tool = new CompetencyTools(new FakeSubjects([]), domains, new FakeCompetencyCatalogVersions(), new FakeMcpConfirmation()); var result = await tool.DeleteCompetencyDomain(domain.Id); @@ -856,7 +863,7 @@ public sealed class McpToolsTests var domain = new CompetencyDomain { Name = "Funktionen" }; var domains = new FakeCompetencyDomains(); domains.Add(domain); - var tool = new CompetencyTools(new FakeSubjects([]), domains, new FakeMcpConfirmation()); + var tool = new CompetencyTools(new FakeSubjects([]), domains, new FakeCompetencyCatalogVersions(), new FakeMcpConfirmation()); var result = await tool.AddCompetencyItem(domain.Id, "M.9.1", "lineare Funktionen erkennen"); @@ -873,7 +880,7 @@ public sealed class McpToolsTests domain.Items.Add(item); var domains = new FakeCompetencyDomains(); domains.Add(domain); - var tool = new CompetencyTools(new FakeSubjects([]), domains, new FakeMcpConfirmation()); + var tool = new CompetencyTools(new FakeSubjects([]), domains, new FakeCompetencyCatalogVersions(), new FakeMcpConfirmation()); var result = await tool.UpdateCompetencyItem(domain.Id, item.Id, description: "neu"); @@ -891,11 +898,102 @@ public sealed class McpToolsTests domain.Items.Add(item); var domains = new FakeCompetencyDomains(); domains.Add(domain); - var tool = new CompetencyTools(new FakeSubjects([]), domains, new FakeMcpConfirmation()); + var tool = new CompetencyTools(new FakeSubjects([]), domains, new FakeCompetencyCatalogVersions(), new FakeMcpConfirmation()); var result = await tool.RemoveCompetencyItem(domain.Id, item.Id); Assert.True(result.Applied); Assert.Empty(domains.GetById(domain.Id)!.Items); } + + // ── CompetencyTools: Bildungsplan-Versionen ───────────────────────────────────────────── + + [Fact] + public void GetCompetencyPlanVersions_ListetVersionenDesFachs() + { + var subjectId = Guid.NewGuid(); + var versions = new FakeCompetencyCatalogVersions(); + versions.GetOrCreateDefault(subjectId); + var tool = new CompetencyTools(new FakeSubjects([]), new FakeCompetencyDomains(), versions, new FakeMcpConfirmation()); + + var result = Assert.Single(tool.GetCompetencyPlanVersions(subjectId)); + + Assert.True(result.IsDefault); + Assert.Equal("Bestehender Kompetenzkatalog", result.Name); + } + + [Fact] + public async Task CreateCompetencyPlanVersion_NutzerBestaetigt_SpeichertVersion() + { + var subject = new Subject { Name = "Chemie" }; + var versions = new FakeCompetencyCatalogVersions(); + var tool = new CompetencyTools(new FakeSubjects([subject]), new FakeCompetencyDomains(), versions, new FakeMcpConfirmation()); + + var result = await tool.CreateCompetencyPlanVersion(subject.Id, "Bildungsplan 2026"); + + Assert.True(result.Applied); + Assert.Single(versions.GetBySubject(subject.Id), v => v.Name == "Bildungsplan 2026"); + } + + [Fact] + public async Task UpdateCompetencyPlanVersion_SetAsDefault_NimmtBisherigeStandardversionZurueck() + { + var subjectId = Guid.NewGuid(); + var versions = new FakeCompetencyCatalogVersions(); + var alt = versions.GetOrCreateDefault(subjectId); + var neu = new CompetencyCatalogVersion { SubjectId = subjectId, Name = "Bildungsplan 2026" }; + versions.Add(neu); + var tool = new CompetencyTools(new FakeSubjects([]), new FakeCompetencyDomains(), versions, new FakeMcpConfirmation()); + + var result = await tool.UpdateCompetencyPlanVersion(neu.Id, setAsDefault: true); + + Assert.True(result.Applied); + Assert.False(versions.GetById(alt.Id)!.IsDefault); + Assert.True(versions.GetById(neu.Id)!.IsDefault); + } + + [Fact] + public async Task DeleteCompetencyPlanVersion_Standardversion_LiefertFehler() + { + var versions = new FakeCompetencyCatalogVersions(); + var version = versions.GetOrCreateDefault(Guid.NewGuid()); + var tool = new CompetencyTools(new FakeSubjects([]), new FakeCompetencyDomains(), versions, new FakeMcpConfirmation()); + + var result = await tool.DeleteCompetencyPlanVersion(version.Id); + + Assert.False(result.Applied); + } + + // ── GroupTools: Bildungsplan-Version eines Kurses ─────────────────────────────────────── + + [Fact] + public async Task UpdateGroupCompetencyVersion_NutzerBestaetigt_SpeichertVersion() + { + var subjectId = Guid.NewGuid(); + var group = new LearningGroup { Name = "9a", SchoolYear = "2025/26", SubjectId = subjectId }; + var groups = new FakeGroups([group]); + var versions = new FakeCompetencyCatalogVersions(); + var version = new CompetencyCatalogVersion { SubjectId = subjectId, Name = "Bildungsplan 2026" }; + versions.Add(version); + var tool = new GroupTools(groups, versions, new FakeMcpConfirmation()); + + var result = await tool.UpdateGroupCompetencyVersion(group.Id, version.Id); + + Assert.True(result.Applied); + Assert.Equal(version.Id, groups.GetById(group.Id)!.CompetencyCatalogVersionId); + } + + [Fact] + public async Task UpdateGroupCompetencyVersion_UnbekannteVersion_LiefertFehlerOhneNachfrage() + { + var group = new LearningGroup { Name = "9a", SchoolYear = "2025/26" }; + var groups = new FakeGroups([group]); + var confirmation = new FakeMcpConfirmation(); + var tool = new GroupTools(groups, new FakeCompetencyCatalogVersions(), confirmation); + + var result = await tool.UpdateGroupCompetencyVersion(group.Id, Guid.NewGuid()); + + Assert.False(result.Applied); + Assert.Equal(0, confirmation.CallCount); + } } diff --git a/LehrerApp.Desktop.Tests/ParticipationSessionEditingTests.cs b/LehrerApp.Desktop.Tests/ParticipationSessionEditingTests.cs index 6cd5dbe..ea3f6a7 100644 --- a/LehrerApp.Desktop.Tests/ParticipationSessionEditingTests.cs +++ b/LehrerApp.Desktop.Tests/ParticipationSessionEditingTests.cs @@ -45,7 +45,7 @@ public sealed class ParticipationSessionEditingTests var sessions = new FakeSessions([session]); var vm = new ParticipationTabViewModel( sessions, new FakeEntries(), new FakeAspects(), new FakeStudents([]), - new FakeMemberships([]), new FakeGroups([group]), new FakeCompetencyDomains()); + new FakeMemberships([]), new FakeGroups([group]), new FakeCompetencyDomains(), new FakeCompetencyCatalogVersions()); vm.OnEditSession = existing => { existing.Comment = "Nachbesprechung Elektrizität"; @@ -70,7 +70,7 @@ public sealed class ParticipationSessionEditingTests entries.Add(new ParticipationEntry { SessionId = session.Id, StudentId = Guid.NewGuid() }); var vm = new ParticipationTabViewModel( sessions, entries, new FakeAspects(), new FakeStudents([]), - new FakeMemberships([]), new FakeGroups([group]), new FakeCompetencyDomains()); + new FakeMemberships([]), new FakeGroups([group]), new FakeCompetencyDomains(), new FakeCompetencyCatalogVersions()); vm.Initialize(group.Id, group.SchoolYear); vm.SelectedSession = vm.Sessions.Single(s => s.Id == session.Id); @@ -95,7 +95,7 @@ public sealed class ParticipationSessionEditingTests var sessions = new FakeSessions([session]); var vm = new ParticipationTabViewModel( sessions, new FakeEntries(), new FakeAspects(), new FakeStudents([]), - new FakeMemberships([]), new FakeGroups([group]), new FakeCompetencyDomains()); + new FakeMemberships([]), new FakeGroups([group]), new FakeCompetencyDomains(), new FakeCompetencyCatalogVersions()); vm.Initialize(group.Id, group.SchoolYear); vm.SelectedSession = vm.Sessions.Single(s => s.Id == session.Id); vm.OnConfirmDeleteSession = (_, _) => Task.FromResult(false); diff --git a/LehrerApp.Desktop.Tests/PlanningTabViewModelTests.cs b/LehrerApp.Desktop.Tests/PlanningTabViewModelTests.cs index 8a8e53c..8bcabc2 100644 --- a/LehrerApp.Desktop.Tests/PlanningTabViewModelTests.cs +++ b/LehrerApp.Desktop.Tests/PlanningTabViewModelTests.cs @@ -20,7 +20,7 @@ public class PlanningTabViewModelTests var lessons = new FakeLessons(); var vm = new PlanningTabViewModel(units, lessons, groups, subjects, competencyDomains, - TestSupport.BuildAiSettingsService(), new FakeSessions([])); + new FakeCompetencyCatalogVersions(), TestSupport.BuildAiSettingsService(), new FakeSessions([])); vm.Initialize(groupId); return (vm, units, lessons, groupId); @@ -277,7 +277,7 @@ public class PlanningTabViewModelTests var lessons = new FakeLessons(); var sessions = new FakeSessions([]); var vm = new PlanningTabViewModel(units, lessons, groups, new FakeSubjects([]), - new FakeCompetencyDomains(), TestSupport.BuildAiSettingsService(), sessions); + new FakeCompetencyDomains(), new FakeCompetencyCatalogVersions(), TestSupport.BuildAiSettingsService(), sessions); vm.Initialize(groupId); var unit = new Unit { GroupId = groupId, Title = "Optik" }; @@ -312,7 +312,7 @@ public class PlanningTabViewModelTests var lessons = new FakeLessons(); var sessions = new FakeSessions([]); var vm = new PlanningTabViewModel(units, lessons, groups, new FakeSubjects([]), - new FakeCompetencyDomains(), TestSupport.BuildAiSettingsService(), sessions); + new FakeCompetencyDomains(), new FakeCompetencyCatalogVersions(), TestSupport.BuildAiSettingsService(), sessions); vm.Initialize(groupId); var unit = new Unit { GroupId = groupId, Title = "Optik" }; @@ -346,7 +346,7 @@ public class PlanningTabViewModelTests var lessons = new FakeLessons(); var sessions = new FakeSessions([]); var vm = new PlanningTabViewModel(units, lessons, groups, new FakeSubjects([]), - new FakeCompetencyDomains(), TestSupport.BuildAiSettingsService(), sessions); + new FakeCompetencyDomains(), new FakeCompetencyCatalogVersions(), TestSupport.BuildAiSettingsService(), sessions); vm.Initialize(groupId); var unit = new Unit { GroupId = groupId, Title = "Optik" }; diff --git a/LehrerApp.Desktop.Tests/SettingsViewModelTests.cs b/LehrerApp.Desktop.Tests/SettingsViewModelTests.cs index ebf77a5..5347cf5 100644 --- a/LehrerApp.Desktop.Tests/SettingsViewModelTests.cs +++ b/LehrerApp.Desktop.Tests/SettingsViewModelTests.cs @@ -16,6 +16,7 @@ public sealed class SettingsViewModelTests private static SettingsViewModel BuildViewModel(FakeSchoolHolidays? holidays = null, FakeSupervisionDuties? supervisionDuties = null, FakeSubjects? subjects = null, FakeCompetencyDomains? competencyDomains = null, + FakeCompetencyCatalogVersions? competencyVersions = null, EventQueue? eventQueue = null, SyncKeyStatus? syncKeyStatus = null, SyncKeyRecoveryService? syncKeyRecovery = null, AppearanceSettingsService? appearance = null, TrashViewModel? trashTab = null) @@ -27,7 +28,8 @@ public sealed class SettingsViewModelTests Directory.CreateDirectory(tempPath); return new SettingsViewModel( - subjects ?? new FakeSubjects([]), competencyDomains ?? new FakeCompetencyDomains(), new FakeGradingKeyTemplates(), + subjects ?? new FakeSubjects([]), competencyDomains ?? new FakeCompetencyDomains(), + competencyVersions ?? new FakeCompetencyCatalogVersions(), new FakeGradingKeyTemplates(), new FakeSchemes(), new GradingService(), new BackupService(tempPath), new BackupSettingsService(tempPath), new DatabaseEncryptionService(), new AppLockService(tempPath), @@ -335,7 +337,7 @@ public sealed class SettingsViewModelTests var calendarSettings = new SchoolCalendarSettingsService(tempPath); var vm = new SettingsViewModel( - new FakeSubjects([]), new FakeCompetencyDomains(), new FakeGradingKeyTemplates(), + new FakeSubjects([]), new FakeCompetencyDomains(), new FakeCompetencyCatalogVersions(), new FakeGradingKeyTemplates(), new FakeSchemes(), new GradingService(), new BackupService(tempPath), new BackupSettingsService(tempPath), new DatabaseEncryptionService(), new AppLockService(tempPath), @@ -365,7 +367,7 @@ public sealed class SettingsViewModelTests var periodSchedule = new PeriodScheduleService(tempPath); var vm = new SettingsViewModel( - new FakeSubjects([]), new FakeCompetencyDomains(), new FakeGradingKeyTemplates(), + new FakeSubjects([]), new FakeCompetencyDomains(), new FakeCompetencyCatalogVersions(), new FakeGradingKeyTemplates(), new FakeSchemes(), new GradingService(), new BackupService(tempPath), new BackupSettingsService(tempPath), new DatabaseEncryptionService(), new AppLockService(tempPath), @@ -399,7 +401,7 @@ public sealed class SettingsViewModelTests var periodSchedule = new PeriodScheduleService(tempPath); var vm = new SettingsViewModel( - new FakeSubjects([]), new FakeCompetencyDomains(), new FakeGradingKeyTemplates(), + new FakeSubjects([]), new FakeCompetencyDomains(), new FakeCompetencyCatalogVersions(), new FakeGradingKeyTemplates(), new FakeSchemes(), new GradingService(), new BackupService(tempPath), new BackupSettingsService(tempPath), new DatabaseEncryptionService(), new AppLockService(tempPath), @@ -489,21 +491,24 @@ public sealed class SettingsViewModelTests { var subject = new Subject { Name = "Biologie", ShortName = "Bio" }; var repository = new FakeCompetencyDomains(); + var versions = new FakeCompetencyCatalogVersions(); + var version = versions.GetOrCreateDefault(subject.Id); repository.Add(new CompetencyDomain { - SubjectId = subject.Id, GradeLevel = 10, Name = "Erster Bereich", SortOrder = 0, + SubjectId = subject.Id, GradeLevel = 10, VersionId = version.Id, Name = "Erster Bereich", SortOrder = 0, }); repository.Add(new CompetencyDomain { - SubjectId = subject.Id, GradeLevel = 10, Name = "Zweiter Bereich", SortOrder = 1, + SubjectId = subject.Id, GradeLevel = 10, VersionId = version.Id, Name = "Zweiter Bereich", SortOrder = 1, }); - var vm = BuildViewModel(subjects: new FakeSubjects([subject]), competencyDomains: repository); + var vm = BuildViewModel(subjects: new FakeSubjects([subject]), competencyDomains: repository, + competencyVersions: versions); vm.CatalogSubject = vm.Subjects.Single(); vm.Domains[1].MoveUpCommand.Execute(null); Assert.Equal(["Zweiter Bereich", "Erster Bereich"], vm.Domains.Select(x => x.Name)); - var stored = repository.GetBySubjectAndGrade(subject.Id, 10); + var stored = repository.GetBySubjectAndGrade(subject.Id, 10, version.Id); Assert.Equal(["Zweiter Bereich", "Erster Bereich"], stored.Select(x => x.Name)); Assert.Equal([0, 1], stored.Select(x => x.SortOrder)); } @@ -537,15 +542,19 @@ public sealed class SettingsViewModelTests { var subject = new Subject { Name = "Biologie", ShortName = "Bio" }; var repository = new FakeCompetencyDomains(); + var versions = new FakeCompetencyCatalogVersions(); + var version = versions.GetOrCreateDefault(subject.Id); repository.Add(new CompetencyDomain { SubjectId = subject.Id, GradeLevel = 10, + VersionId = version.Id, Name = "Erkenntnisgewinnung", Code = "EG", Items = [new CompetencyItem { Code = "EG1", Description = "Modelle verwenden" }], }); - var vm = BuildViewModel(subjects: new FakeSubjects([subject]), competencyDomains: repository); + var vm = BuildViewModel(subjects: new FakeSubjects([subject]), competencyDomains: repository, + competencyVersions: versions); vm.CatalogSubject = vm.Subjects.Single(); var preview = vm.PrepareCatalogCopy(11); @@ -556,9 +565,9 @@ public sealed class SettingsViewModelTests Assert.Equal(1, preview.NewCompetencies); vm.ApplyCatalogImport(preview, CompetencyCatalogImportMode.Merge, new HashSet()); - var copied = repository.GetBySubjectAndGrade(subject.Id, 11); + var copied = repository.GetBySubjectAndGrade(subject.Id, 11, version.Id); Assert.Single(copied); - Assert.NotEqual(repository.GetBySubjectAndGrade(subject.Id, 10)[0].Id, copied[0].Id); + Assert.NotEqual(repository.GetBySubjectAndGrade(subject.Id, 10, version.Id)[0].Id, copied[0].Id); Assert.Equal("EG1", copied[0].Items.Single().Code); } } diff --git a/LehrerApp.Desktop.Tests/TeachingModeViewModelTests.cs b/LehrerApp.Desktop.Tests/TeachingModeViewModelTests.cs index 9873012..e9d55c5 100644 --- a/LehrerApp.Desktop.Tests/TeachingModeViewModelTests.cs +++ b/LehrerApp.Desktop.Tests/TeachingModeViewModelTests.cs @@ -22,7 +22,7 @@ public sealed class TeachingModeViewModelTests FakeSessions? sessions = null) => new(sessions ?? new FakeSessions([]), new FakeEntries(), new FakeAspects(), new FakeStudents([]), new FakeMemberships([]), new FakeGroups([group]), - new FakeCompetencyDomains()); + new FakeCompetencyDomains(), new FakeCompetencyCatalogVersions()); [Fact] public void Konstruktor_LaedtVerlaufsplanUndInitialisiertSitzplanFuerDieGruppe() diff --git a/LehrerApp.Desktop/AppBootstrapper.cs b/LehrerApp.Desktop/AppBootstrapper.cs index 04c57cf..fd209f3 100644 --- a/LehrerApp.Desktop/AppBootstrapper.cs +++ b/LehrerApp.Desktop/AppBootstrapper.cs @@ -176,6 +176,7 @@ public static class AppBootstrapper services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); diff --git a/LehrerApp.Desktop/Services/AiPlanningService.cs b/LehrerApp.Desktop/Services/AiPlanningService.cs index 48cc0ac..0d56b93 100644 --- a/LehrerApp.Desktop/Services/AiPlanningService.cs +++ b/LehrerApp.Desktop/Services/AiPlanningService.cs @@ -1,6 +1,7 @@ using LehrerApp.Core.AiPlanning; using LehrerApp.Core.Interfaces; using LehrerApp.Core.Models; +using LehrerApp.Core.Services; using System.Globalization; using System.Net; using System.Net.Http.Json; @@ -72,7 +73,8 @@ public class GermanTimeOnlyJsonConverter : JsonConverter /// public class AiPlanningService(HttpClient http, ILessonRepository lessons, IGroupRepository groups, ISubjectRepository subjects, - ICompetencyDomainRepository competencyDomains, IAlternativeLessonPathRepository altPaths) + ICompetencyDomainRepository competencyDomains, ICompetencyCatalogVersionRepository competencyVersions, + IAlternativeLessonPathRepository altPaths) { // Wire-Format zum PHP-Backend ist camelCase (siehe ai-backend/) — beide Seiten sind hier // im eigenen Zugriff, daher bewusst konsistent camelCase statt der C#-üblichen PascalCase-Defaults. @@ -165,7 +167,8 @@ public class AiPlanningService(HttpClient http, ILessonRepository lessons, var competencyCatalog = subject is null ? [] - : competencyDomains.GetBySubjectAndGrade(subject.Id, gradeLevel) + : competencyDomains.GetBySubjectAndGrade(subject.Id, gradeLevel, + CompetencyCatalogVersionResolver.Resolve(competencyVersions, subject.Id, group?.CompetencyCatalogVersionId)) .Select(d => new AiCompetencyDomain { Name = d.Name, diff --git a/LehrerApp.Desktop/Services/Mcp/McpServerHostedService.cs b/LehrerApp.Desktop/Services/Mcp/McpServerHostedService.cs index f813271..07b1724 100644 --- a/LehrerApp.Desktop/Services/Mcp/McpServerHostedService.cs +++ b/LehrerApp.Desktop/Services/Mcp/McpServerHostedService.cs @@ -177,8 +177,10 @@ public sealed class McpServerHostedService : IAsyncDisposable "Erzeugt einen Elternbrief aus einer Vorlage für einen Schüler als Base64-PDF."); AddReadTool(competencyTools.GetSubjects, "get_subjects", "Listet alle Fächer."); + AddReadTool(competencyTools.GetCompetencyPlanVersions, "get_competency_plan_versions", + "Listet die Bildungsplan-Versionen eines Fachs."); AddReadTool(competencyTools.GetCompetencyCatalog, "get_competency_catalog", - "Listet den Kompetenzkatalog eines Fachs, optional gefiltert auf eine Klassenstufe."); + "Listet den Kompetenzkatalog eines Fachs für eine Bildungsplan-Version, optional gefiltert auf eine Klassenstufe."); AddReadTool(untisComparisonTools.GetUntisHubStatus, "get_untis_hub_status", "Listet die Fälligkeit der Untis-Hub-Abgleiche, ohne selbst WebUntis anzufragen."); AddReadTool(untisComparisonTools.GetUntisAbsenceRows, "get_untis_absence_rows", @@ -228,6 +230,12 @@ public sealed class McpServerHostedService : IAsyncDisposable "Ändert Code/Beschreibung einer Einzelkompetenz (Bestätigung durch den Nutzer nötig)."); AddWriteTool(competencyTools.RemoveCompetencyItem, "remove_competency_item", "Entfernt eine Einzelkompetenz aus einem Kompetenzbereich (Bestätigung durch den Nutzer nötig)."); + AddWriteTool(competencyTools.CreateCompetencyPlanVersion, "create_competency_plan_version", + "Legt eine neue Bildungsplan-Version für ein Fach an (Bestätigung durch den Nutzer nötig)."); + AddWriteTool(competencyTools.UpdateCompetencyPlanVersion, "update_competency_plan_version", + "Ändert Name/Standardstatus einer Bildungsplan-Version (Bestätigung durch den Nutzer nötig)."); + AddWriteTool(groupTools.UpdateGroupCompetencyVersion, "update_group_competency_version", + "Setzt oder löscht die Bildungsplan-Version eines Kurses/einer Klasse (Bestätigung durch den Nutzer nötig)."); AddWriteTool(untisComparisonTools.ApplyUntisAbsenceStatus, "apply_untis_absence_status", "Übernimmt einen Statusvorschlag für eine über get_untis_absence_rows gelieferte row-id (Bestätigung durch den Nutzer nötig, nennt keinen Schülernamen)."); @@ -237,6 +245,8 @@ public sealed class McpServerHostedService : IAsyncDisposable "Löscht ein Fach endgültig, ohne Papierkorb (Bestätigung durch den Nutzer nötig)."); AddDestructiveWriteTool(competencyTools.DeleteCompetencyDomain, "delete_competency_domain", "Löscht einen Kompetenzbereich endgültig samt Einzelkompetenzen, ohne Papierkorb (Bestätigung durch den Nutzer nötig)."); + AddDestructiveWriteTool(competencyTools.DeleteCompetencyPlanVersion, "delete_competency_plan_version", + "Löscht eine Bildungsplan-Version endgültig, ohne Papierkorb (Bestätigung durch den Nutzer nötig)."); System.Diagnostics.Debug.Assert( toolCollection.Select(t => t.ProtocolTool.Name).OrderBy(n => n) diff --git a/LehrerApp.Desktop/Services/Mcp/McpToolScope.cs b/LehrerApp.Desktop/Services/Mcp/McpToolScope.cs index 020e2c1..8b8964a 100644 --- a/LehrerApp.Desktop/Services/Mcp/McpToolScope.cs +++ b/LehrerApp.Desktop/Services/Mcp/McpToolScope.cs @@ -29,6 +29,7 @@ public static class McpToolScope "list_letter_templates", "render_letter", "get_subjects", + "get_competency_plan_versions", "get_competency_catalog", "get_untis_hub_status", "get_untis_absence_rows", @@ -63,6 +64,9 @@ public static class McpToolScope "add_competency_item", "update_competency_item", "remove_competency_item", + "create_competency_plan_version", + "update_competency_plan_version", + "update_group_competency_version", "apply_untis_absence_status", ]; @@ -77,5 +81,6 @@ public static class McpToolScope "delete_lesson", "delete_subject", "delete_competency_domain", + "delete_competency_plan_version", ]; } diff --git a/LehrerApp.Desktop/Services/Mcp/Tools/CompetencyTools.cs b/LehrerApp.Desktop/Services/Mcp/Tools/CompetencyTools.cs index 3f9f878..b61738c 100644 --- a/LehrerApp.Desktop/Services/Mcp/Tools/CompetencyTools.cs +++ b/LehrerApp.Desktop/Services/Mcp/Tools/CompetencyTools.cs @@ -5,18 +5,21 @@ using LehrerApp.Core.Models; namespace LehrerApp.Desktop.Services.Mcp.Tools; -/// MCP-Tools für die Fach- und Kompetenzkatalog-Verwaltung (Nutzer-Nachtrag zu 4.5.28ff.): -/// und / vollständig -/// anlegen/ändern/löschen. Die Zuordnung von Kompetenzcodes zu einzelnen Stunden lebt bewusst in -/// (AddLessonCompetency/RemoveLessonCompetency), nicht hier - sie -/// gehört fachlich zur Stundenverwaltung, nicht zum Katalog selbst. +/// MCP-Tools für die Fach- und Kompetenzkatalog-Verwaltung (Nutzer-Nachtrag zu 4.5.28ff., +/// später um Bildungsplan-Versionierung erweitert): , +/// und / +/// vollständig anlegen/ändern/löschen. Die Zuordnung von Kompetenzcodes zu einzelnen Stunden lebt +/// bewusst in (AddLessonCompetency/RemoveLessonCompetency), nicht hier - +/// sie gehört fachlich zur Stundenverwaltung, nicht zum Katalog selbst. /// -/// "delete_subject" und "delete_competency_domain" sind wie "delete_lesson" (LessonPlanTools) -/// endgültige Löschungen ohne Papierkorb (die Repository-Implementierungen löschen direkt in der -/// LiteDB-Collection statt über MoveToTrash) - deshalb als Destructive registriert (siehe -/// McpServerHostedService) und mit entsprechend deutlicher Bestätigungsnachricht. +/// "delete_subject", "delete_competency_domain" und "delete_competency_plan_version" sind wie +/// "delete_lesson" (LessonPlanTools) endgültige Löschungen ohne Papierkorb (die +/// Repository-Implementierungen löschen direkt in der LiteDB-Collection statt über MoveToTrash) - +/// deshalb als Destructive registriert (siehe McpServerHostedService) und mit entsprechend +/// deutlicher Bestätigungsnachricht. public class CompetencyTools( - ISubjectRepository subjects, ICompetencyDomainRepository domains, IMcpConfirmationService confirmation) + ISubjectRepository subjects, ICompetencyDomainRepository domains, + ICompetencyCatalogVersionRepository versions, IMcpConfirmationService confirmation) { // ── Lesen ──────────────────────────────────────────────────────────────────────────────── @@ -24,17 +27,93 @@ public class CompetencyTools( public List GetSubjects() => subjects.GetAll().Select(s => new SubjectDto(s.Id, s.Name, s.ShortName)).ToList(); - [Description("Listet den Kompetenzkatalog (Bereiche mit Einzelkompetenzen) eines Fachs, optional gefiltert auf eine Klassenstufe.")] + [Description("Listet die Bildungsplan-Versionen eines Fachs (z.B. \"Bildungsplan 2016\" vs. \"Bildungsplan 2026\", während ein neuer Plan klassenstufenweise hochwächst). Genau eine Version je Fach ist die Standardversion.")] + public List GetCompetencyPlanVersions( + [Description("Fach-ID, aus get_subjects.")] Guid subjectId) => + versions.GetBySubject(subjectId) + .Select(v => new CompetencyPlanVersionDto(v.Id, v.SubjectId, v.Name, v.IsDefault, v.SortOrder)) + .ToList(); + + [Description("Listet den Kompetenzkatalog (Bereiche mit Einzelkompetenzen) eines Fachs für eine Bildungsplan-Version, optional gefiltert auf eine Klassenstufe.")] public List GetCompetencyCatalog( [Description("Fach-ID, aus get_subjects.")] Guid subjectId, - [Description("Optionale Klassenstufe. Weglassen: alle Klassenstufen dieses Fachs.")] int? gradeLevel = null) + [Description("Optionale Klassenstufe. Weglassen: alle Klassenstufen dieser Version.")] int? gradeLevel = null, + [Description("Bildungsplan-Version, aus get_competency_plan_versions. Weglassen: Standardversion des Fachs.")] Guid? versionId = null) { + var effectiveVersionId = versionId ?? versions.GetOrCreateDefault(subjectId).Id; var result = gradeLevel is not null - ? domains.GetBySubjectAndGrade(subjectId, gradeLevel.Value) - : domains.GetBySubject(subjectId); + ? domains.GetBySubjectAndGrade(subjectId, gradeLevel.Value, effectiveVersionId) + : domains.GetBySubjectAndVersion(subjectId, effectiveVersionId); return result.Select(ToDto).ToList(); } + // ── Bildungsplan-Versionen (CompetencyCatalogVersion) ──────────────────────────────────── + + [Description("Legt eine neue Bildungsplan-Version für ein Fach an (z.B. \"Bildungsplan 2026\"), um einen neuen, parallel gültigen Kompetenzkatalog zu pflegen. Muss der Nutzer erst bestätigen.")] + public async Task CreateCompetencyPlanVersion( + [Description("Fach-ID, aus get_subjects.")] Guid subjectId, + [Description("Name der Version, z.B. \"Bildungsplan 2026\".")] string name, + CancellationToken ct = default) + { + var subject = subjects.GetById(subjectId); + if (subject is null) return new WriteResultDto(false, null, "Unbekannte Fach-ID."); + if (string.IsNullOrWhiteSpace(name)) return new WriteResultDto(false, null, "Name der Version darf nicht leer sein."); + + var message = $"Neue Bildungsplan-Version „{name.Trim()}“ für {subject.Name} anlegen?"; + if (!await confirmation.ConfirmAsync("Version anlegen?", message, ct)) + return new WriteResultDto(false, null, "Vom Nutzer abgelehnt oder nicht bestätigt."); + + var version = new CompetencyCatalogVersion + { + SubjectId = subjectId, Name = name, SortOrder = versions.GetBySubject(subjectId).Count, + }; + try { versions.Save(version); } + catch (ArgumentException ex) { return new WriteResultDto(false, null, ex.Message); } + return new WriteResultDto(true, version.Id, "Version gespeichert."); + } + + [Description("Ändert Name/Standardstatus einer bestehenden Bildungsplan-Version. Nur angegebene Felder werden geändert.")] + public async Task UpdateCompetencyPlanVersion( + [Description("ID der Version, aus get_competency_plan_versions.")] Guid versionId, + [Description("Neuer Name. Unverändert lassen: weglassen.")] string? name = null, + [Description("Auf true setzen, um diese Version zur Standardversion ihres Fachs zu machen (bisherige Standardversion wird automatisch zurückgenommen).")] bool? setAsDefault = null, + CancellationToken ct = default) + { + var version = versions.GetById(versionId); + if (version is null) return new WriteResultDto(false, null, "Unbekannte Versions-ID."); + + var changes = new StringBuilder(); + if (name is not null && name != version.Name) { changes.AppendLine($"Name: „{version.Name}“ → „{name}“"); version.Name = name; } + if (setAsDefault is true && !version.IsDefault) { changes.AppendLine("wird Standardversion des Fachs"); version.IsDefault = true; } + + if (changes.Length == 0) + return new WriteResultDto(true, version.Id, "Keine Änderung nötig."); + + if (!await confirmation.ConfirmAsync("Version ändern?", $"„{version.Name}“\n{changes}".TrimEnd(), ct)) + return new WriteResultDto(false, null, "Vom Nutzer abgelehnt oder nicht bestätigt."); + + try { versions.Save(version); } + catch (ArgumentException ex) { return new WriteResultDto(false, null, ex.Message); } + return new WriteResultDto(true, version.Id, "Version gespeichert."); + } + + [Description("Löscht eine Bildungsplan-Version endgültig. Schlägt fehl, wenn sie noch Kompetenzbereiche oder Kurse referenziert, oder wenn sie die Standardversion ihres Fachs ist. Kein Papierkorb - nicht rückgängig zu machen. Muss der Nutzer erst bestätigen.")] + public async Task DeleteCompetencyPlanVersion( + [Description("ID der Version.")] Guid versionId, + CancellationToken ct = default) + { + var version = versions.GetById(versionId); + if (version is null) return new WriteResultDto(false, null, "Unbekannte Versions-ID."); + + var message = $"Die Bildungsplan-Version „{version.Name}“ wird endgültig gelöscht. Das kann NICHT rückgängig gemacht werden (kein Papierkorb für Versionen). Schlägt fehl, falls noch Kompetenzbereiche oder Kurse diese Version nutzen."; + if (!await confirmation.ConfirmAsync("Version endgültig löschen?", message, ct)) + return new WriteResultDto(false, null, "Vom Nutzer abgelehnt oder nicht bestätigt."); + + try { versions.Delete(versionId); } + catch (InvalidOperationException ex) { return new WriteResultDto(false, null, ex.Message); } + return new WriteResultDto(true, versionId, "Version gelöscht."); + } + // ── Fächer (Subject) ───────────────────────────────────────────────────────────────────── [Description("Legt ein neues Fach an. Muss der Nutzer erst bestätigen.")] @@ -109,26 +188,30 @@ public class CompetencyTools( // ── Kompetenzbereiche (CompetencyDomain) ──────────────────────────────────────────────── - [Description("Legt einen neuen Kompetenzbereich (z.B. \"Zahlen und Operationen\") für ein Fach und eine Klassenstufe an, zunächst ohne Einzelkompetenzen. Muss der Nutzer erst bestätigen.")] + [Description("Legt einen neuen Kompetenzbereich (z.B. \"Zahlen und Operationen\") für ein Fach, eine Klassenstufe und eine Bildungsplan-Version an, zunächst ohne Einzelkompetenzen. Muss der Nutzer erst bestätigen.")] public async Task CreateCompetencyDomain( [Description("Fach-ID, aus get_subjects.")] Guid subjectId, [Description("Klassenstufe.")] int gradeLevel, [Description("Name des Bereichs.")] string name, [Description("Optionales Kürzel, z.B. \"ZO\".")] string code = "", + [Description("Bildungsplan-Version, aus get_competency_plan_versions. Weglassen: Standardversion des Fachs.")] Guid? versionId = null, CancellationToken ct = default) { var subject = subjects.GetById(subjectId); if (subject is null) return new WriteResultDto(false, null, "Unbekannte Fach-ID."); if (string.IsNullOrWhiteSpace(name)) return new WriteResultDto(false, null, "Bereichsname darf nicht leer sein."); - var sortOrder = domains.GetBySubjectAndGrade(subjectId, gradeLevel).Count; - var message = $"Neuen Kompetenzbereich „{name.Trim()}“ für {subject.Name}, Klasse {gradeLevel}, anlegen?"; + var effectiveVersionId = versionId ?? versions.GetOrCreateDefault(subjectId).Id; + var versionName = versions.GetById(effectiveVersionId)?.Name ?? ""; + var sortOrder = domains.GetBySubjectAndGrade(subjectId, gradeLevel, effectiveVersionId).Count; + var message = $"Neuen Kompetenzbereich „{name.Trim()}“ für {subject.Name}, Klasse {gradeLevel} ({versionName}), anlegen?"; if (!await confirmation.ConfirmAsync("Kompetenzbereich anlegen?", message, ct)) return new WriteResultDto(false, null, "Vom Nutzer abgelehnt oder nicht bestätigt."); var domain = new CompetencyDomain { - SubjectId = subjectId, GradeLevel = gradeLevel, Name = name, Code = code, SortOrder = sortOrder, + SubjectId = subjectId, GradeLevel = gradeLevel, VersionId = effectiveVersionId, + Name = name, Code = code, SortOrder = sortOrder, }; domains.Save(domain); return new WriteResultDto(true, domain.Id, "Kompetenzbereich gespeichert."); @@ -246,7 +329,7 @@ public class CompetencyTools( } private static CompetencyDomainDto ToDto(CompetencyDomain d) => new( - d.Id, d.SubjectId, d.GradeLevel, d.Name, d.Code, d.SortOrder, + d.Id, d.SubjectId, d.GradeLevel, d.VersionId, d.Name, d.Code, d.SortOrder, d.Items.OrderBy(i => i.SortOrder) .Select(i => new CompetencyItemDto(i.Id, i.Code, i.Description, i.SortOrder)).ToList()); } diff --git a/LehrerApp.Desktop/Services/Mcp/Tools/Dto.cs b/LehrerApp.Desktop/Services/Mcp/Tools/Dto.cs index 4d0a796..23eb61b 100644 --- a/LehrerApp.Desktop/Services/Mcp/Tools/Dto.cs +++ b/LehrerApp.Desktop/Services/Mcp/Tools/Dto.cs @@ -56,9 +56,12 @@ public record SubjectDto(Guid Id, string Name, string ShortName); public record CompetencyItemDto(Guid Id, string Code, string Description, int SortOrder); public record CompetencyDomainDto( - Guid Id, Guid SubjectId, int GradeLevel, string Name, string Code, int SortOrder, + Guid Id, Guid SubjectId, int GradeLevel, Guid VersionId, string Name, string Code, int SortOrder, List Items); +/// Eine Bildungsplan-Version eines Fachs, siehe CompetencyTools.GetCompetencyPlanVersions. +public record CompetencyPlanVersionDto(Guid Id, Guid SubjectId, string Name, bool IsDefault, int SortOrder); + public record GroupMembershipDto( Guid Id, Guid StudentId, Guid GroupId, MembershipPeriod Period, DateOnly? JoinedAt, DateOnly? LeftAt, Niveau? Niveau); @@ -91,7 +94,7 @@ public record UntisHubStatusRowDto( /// Fach-Datensatz mitzuschleppen. public record GroupDto( Guid Id, string Name, GroupType Type, string SchoolYear, int GradeLevel, - Guid? SubjectId, bool IsActive); + Guid? SubjectId, bool IsActive, Guid? CompetencyPlanVersionId); /// Anonymisierte Fehlzeiten-Diskrepanz (siehe UntisComparisonTools.GetUntisAbsenceRows): /// bewusst KEIN Schülername/keine Klasse - ist die einzige Kennung, über die diff --git a/LehrerApp.Desktop/Services/Mcp/Tools/GroupTools.cs b/LehrerApp.Desktop/Services/Mcp/Tools/GroupTools.cs index 8914487..b53c41e 100644 --- a/LehrerApp.Desktop/Services/Mcp/Tools/GroupTools.cs +++ b/LehrerApp.Desktop/Services/Mcp/Tools/GroupTools.cs @@ -1,14 +1,18 @@ using System.ComponentModel; +using System.Text; using LehrerApp.Core.Interfaces; namespace LehrerApp.Desktop.Services.Mcp.Tools; -/// MCP-Read-Tool "get_groups" (Nutzer-Nachtrag: die GroupId, die praktisch jedes andere -/// Tool als Pflichtparameter verlangt - get_grades, get_schedule, get_lesson_plans, -/// get_untis_absence_rows usw. - war nirgends über MCP auflösbar; ein KI-Client kannte bestenfalls -/// den Klarnamen einer Lerngruppe aus dem Gespräch, nie ihre Id). Reiner Lesezugriff auf das -/// bestehende Repository, keine eigene Datenzugriffslogik. -public class GroupTools(IGroupRepository groups) +/// MCP-Tools für Lerngruppen (Klassen/Kurse). "get_groups" (Nutzer-Nachtrag: die GroupId, +/// die praktisch jedes andere Tool als Pflichtparameter verlangt - get_grades, get_schedule, +/// get_lesson_plans, get_untis_absence_rows usw. - war nirgends über MCP auflösbar; ein KI-Client +/// kannte bestenfalls den Klarnamen einer Lerngruppe aus dem Gespräch, nie ihre Id) ist reiner +/// Lesezugriff. "update_group_competency_version" ist das erste Schreib-Tool auf einem +/// LearningGroup-Feld selbst (Nutzer-Nachtrag zur Bildungsplan-Versionierung) - deshalb bewusst eng +/// auf genau dieses eine Feld geschnitten statt eines allgemeinen "update_group". +public class GroupTools(IGroupRepository groups, ICompetencyCatalogVersionRepository versions, + IMcpConfirmationService confirmation) { [Description("Listet Lerngruppen (Klassen/Kurse) mit ihrer Id - Voraussetzung, um andere Tools (z.B. get_grades, get_schedule, get_untis_absence_rows) für eine bestimmte Gruppe aufzurufen, wenn nur ihr Name bekannt ist. Ohne schoolYear werden alle Schuljahre zurückgegeben.")] public List GetGroups( @@ -18,6 +22,32 @@ public class GroupTools(IGroupRepository groups) var list = schoolYear is null ? groups.GetAll(includeInactive) : groups.GetBySchoolYear(schoolYear, includeInactive); - return list.Select(g => new GroupDto(g.Id, g.Name, g.Type, g.SchoolYear, g.GradeLevel, g.SubjectId, g.IsActive)).ToList(); + return list.Select(g => new GroupDto(g.Id, g.Name, g.Type, g.SchoolYear, g.GradeLevel, + g.SubjectId, g.IsActive, g.CompetencyCatalogVersionId)).ToList(); + } + + [Description("Setzt oder löscht die Bildungsplan-Version eines Kurses/einer Klasse (welcher Kompetenzkatalog für diese Gruppe gilt). Muss der Nutzer erst bestätigen.")] + public async Task UpdateGroupCompetencyVersion( + [Description("Gruppen-ID, aus get_groups.")] Guid groupId, + [Description("Bildungsplan-Version, aus get_competency_plan_versions. Weglassen: zurück auf die Standardversion des Fachs.")] Guid? versionId = null, + CancellationToken ct = default) + { + var group = groups.GetById(groupId); + if (group is null) return new WriteResultDto(false, null, "Unbekannte Gruppen-ID."); + if (versionId is Guid vid && versions.GetById(vid) is null) + return new WriteResultDto(false, null, "Unbekannte Versions-ID."); + + var oldName = group.CompetencyCatalogVersionId is Guid oldId ? versions.GetById(oldId)?.Name ?? "unbekannt" : "Standardversion"; + var newName = versionId is Guid newId ? versions.GetById(newId)?.Name ?? "unbekannt" : "Standardversion"; + if (oldName == newName && group.CompetencyCatalogVersionId == versionId) + return new WriteResultDto(true, group.Id, "Keine Änderung nötig."); + + var message = new StringBuilder($"Bildungsplan-Version von „{group.Name}“ ändern:\n{oldName} → {newName}"); + if (!await confirmation.ConfirmAsync("Bildungsplan-Version ändern?", message.ToString(), ct)) + return new WriteResultDto(false, null, "Vom Nutzer abgelehnt oder nicht bestätigt."); + + group.CompetencyCatalogVersionId = versionId; + groups.Save(group); + return new WriteResultDto(true, group.Id, "Bildungsplan-Version gespeichert."); } } diff --git a/LehrerApp.Desktop/ViewModels/Groups/CompetencyOverviewViewModels.cs b/LehrerApp.Desktop/ViewModels/Groups/CompetencyOverviewViewModels.cs index 89612e3..61e8e84 100644 --- a/LehrerApp.Desktop/ViewModels/Groups/CompetencyOverviewViewModels.cs +++ b/LehrerApp.Desktop/ViewModels/Groups/CompetencyOverviewViewModels.cs @@ -13,6 +13,7 @@ public partial class CompetencyOverviewTabViewModel : ObservableObject private readonly IExamRepository _exams; private readonly IExamResultRepository _results; private readonly ICompetencyDomainRepository _catalog; + private readonly ICompetencyCatalogVersionRepository _versions; private readonly IStudentRepository _students; private readonly CompetencyAnalysisService _analysis; private LearningGroup? _group; @@ -23,6 +24,7 @@ public partial class CompetencyOverviewTabViewModel : ObservableObject [ObservableProperty] private string _emptyMessage = ""; public string GroupLabel => _group?.Name ?? ""; + public string VersionLabel { get; private set; } = ""; public ObservableCollection StudentOptions { get; } = []; public ObservableCollection Rows { get; } = []; @@ -32,12 +34,14 @@ public partial class CompetencyOverviewTabViewModel : ObservableObject public CompetencyOverviewTabViewModel(IUnitRepository units, IExamRepository exams, IExamResultRepository results, ICompetencyDomainRepository catalog, + ICompetencyCatalogVersionRepository versions, IStudentRepository students, CompetencyAnalysisService analysis) { _units = units; _exams = exams; _results = results; _catalog = catalog; + _versions = versions; _students = students; _analysis = analysis; } @@ -53,9 +57,18 @@ public partial class CompetencyOverviewTabViewModel : ObservableObject { if (_group is null) return; var selectedStudentId = SelectedStudent?.Id; - var domains = _group.SubjectId is Guid subjectId - ? _catalog.GetBySubjectAndGrade(subjectId, _group.GradeLevel) - : []; + List domains; + if (_group.SubjectId is Guid subjectId) + { + var versionId = CompetencyCatalogVersionResolver.Resolve(_versions, subjectId, _group.CompetencyCatalogVersionId); + VersionLabel = _versions.GetById(versionId)?.Name ?? ""; + domains = _catalog.GetBySubjectAndGrade(subjectId, _group.GradeLevel, versionId); + } + else + { + VersionLabel = ""; + domains = []; + } var units = _units.GetByGroup(_group.Id); var exams = _exams.GetByGroup(_group.Id); var results = exams.SelectMany(x => _results.GetByExam(x.Id)).ToList(); @@ -81,6 +94,7 @@ public partial class CompetencyOverviewTabViewModel : ObservableObject : ""; OnPropertyChanged(nameof(HasRows)); OnPropertyChanged(nameof(HasStudents)); + OnPropertyChanged(nameof(VersionLabel)); } partial void OnSelectedStudentChanged(CompetencyStudentOption? value) diff --git a/LehrerApp.Desktop/ViewModels/Groups/ExamViewModels.cs b/LehrerApp.Desktop/ViewModels/Groups/ExamViewModels.cs index c39716c..444c68c 100644 --- a/LehrerApp.Desktop/ViewModels/Groups/ExamViewModels.cs +++ b/LehrerApp.Desktop/ViewModels/Groups/ExamViewModels.cs @@ -19,6 +19,7 @@ public partial class ExamDialogViewModel : ObservableObject private readonly Guid _groupId; private readonly Guid? _subjectId; private readonly int _gradeLevel; + private readonly Guid _competencyCatalogVersionId; private readonly GradingSystem _gradingSystem; private readonly Exam? _editingExam; private readonly Exam? _duplicateSource; @@ -63,11 +64,13 @@ public partial class ExamDialogViewModel : ObservableObject public ExamDialogViewModel(IExamRepository exams, ICompetencyDomainRepository competencyDomains, IGradingKeyTemplateRepository gradingKeyTemplates, GradingService grading, Guid groupId, Guid? subjectId, int gradeLevel, GradingSystem gradingSystem, - string defaultSubjectName, bool isDifferentiated, Exam? editingExam, Exam? duplicateSource) + string defaultSubjectName, bool isDifferentiated, Exam? editingExam, Exam? duplicateSource, + Guid competencyCatalogVersionId = default) { _exams = exams; _competencyDomains = competencyDomains; _gradingKeyTemplates = gradingKeyTemplates; _grading = grading; _groupId = groupId; _subjectId = subjectId; _gradeLevel = gradeLevel; + _competencyCatalogVersionId = competencyCatalogVersionId; _gradingSystem = gradingSystem; _editingExam = editingExam; _duplicateSource = duplicateSource; @@ -75,7 +78,7 @@ public partial class ExamDialogViewModel : ObservableObject IsDifferentiated = isDifferentiated; HasCompetencyCatalog = subjectId.HasValue - && _competencyDomains.GetBySubjectAndGrade(subjectId.Value, gradeLevel).Count > 0; + && _competencyDomains.GetBySubjectAndGrade(subjectId.Value, gradeLevel, competencyCatalogVersionId).Count > 0; foreach (var t in _gradingKeyTemplates.GetByGradingSystem(gradingSystem)) AvailableTemplates.Add(t); @@ -164,7 +167,7 @@ public partial class ExamDialogViewModel : ObservableObject var groups = new List(); if (!_subjectId.HasValue) return groups; var selected = selectedCodes.ToHashSet(); - foreach (var domain in _competencyDomains.GetBySubjectAndGrade(_subjectId.Value, _gradeLevel)) + foreach (var domain in _competencyDomains.GetBySubjectAndGrade(_subjectId.Value, _gradeLevel, _competencyCatalogVersionId)) { var group = new CompetencyTagGroup(domain.Name, domain.Code); foreach (var item in domain.Items.OrderBy(i => i.SortOrder)) diff --git a/LehrerApp.Desktop/ViewModels/Groups/GroupViewModels.cs b/LehrerApp.Desktop/ViewModels/Groups/GroupViewModels.cs index 175c256..4542332 100644 --- a/LehrerApp.Desktop/ViewModels/Groups/GroupViewModels.cs +++ b/LehrerApp.Desktop/ViewModels/Groups/GroupViewModels.cs @@ -799,8 +799,10 @@ public partial class AddGroupDialogViewModel : ObservableObject { private readonly IGroupRepository _groups; private readonly ISubjectRepository _subjects; + private readonly ICompetencyCatalogVersionRepository _competencyVersions; private List _allSubjects = []; private LearningGroup? _editingGroup; + private Guid? _pendingCompetencyVersionId; public List TypeOptions { get; } = ["Klasse", "Kurs"]; [ObservableProperty] private string _selectedTypeName = "Kurs"; @@ -829,18 +831,21 @@ public partial class AddGroupDialogViewModel : ObservableObject [ObservableProperty] private bool _excludedFromUntisHub; [ObservableProperty] private string _nameError = ""; [ObservableProperty] private string _gradeLevelError = ""; + [ObservableProperty] private CompetencyVersionListItem? _selectedCompetencyVersion; public List SchoolYears { get; } public List GradingOptions { get; } = ["Noten 1–6", "Punkte 0–15"]; public List KnownSubjectNames { get; private set; } = []; + public ObservableCollection AvailableCompetencyVersions { get; } = []; + public bool HasCompetencyVersionChoice => AvailableCompetencyVersions.Count > 0; public LearningGroup? Result { get; private set; } public string DialogTitle => _editingGroup is null ? "Neue Lerngruppe anlegen" : "Lerngruppe bearbeiten"; public string SaveButtonText => _editingGroup is null ? "Anlegen" : "Speichern"; public AddGroupDialogViewModel(IGroupRepository groups, ISubjectRepository subjects, - SchoolYearService sy) + ICompetencyCatalogVersionRepository competencyVersions, SchoolYearService sy) { - _groups = groups; _subjects = subjects; + _groups = groups; _subjects = subjects; _competencyVersions = competencyVersions; _allSubjects = subjects.GetAll(); KnownSubjectNames = _allSubjects.Select(s => s.Name).ToList(); SchoolYears = sy.RecentSchoolYears(3); @@ -852,11 +857,40 @@ public partial class AddGroupDialogViewModel : ObservableObject }; } + partial void OnSubjectChanged(string value) => LoadCompetencyVersions(); + + /// Baut die Versionsliste für das aktuell im Freitextfeld erkannte + /// Fach neu auf. Ein neues, noch nicht angelegtes Fach hat noch keine Versionen - die Auswahl + /// bleibt dann leer ( blendet den Picker aus), der Kurs + /// bekommt beim Speichern automatisch die Standardversion, sobald das Fach existiert. + private void LoadCompetencyVersions() + { + var targetId = _pendingCompetencyVersionId ?? SelectedCompetencyVersion?.Id; + _pendingCompetencyVersionId = null; + AvailableCompetencyVersions.Clear(); + + var subjectName = Subject.Trim(); + var subject = subjectName.Length == 0 ? null : _allSubjects.FirstOrDefault( + s => s.Name.Equals(subjectName, StringComparison.OrdinalIgnoreCase)); + if (subject is not null) + { + var versions = _competencyVersions.GetBySubject(subject.Id); + if (versions.Count == 0) versions = [_competencyVersions.GetOrCreateDefault(subject.Id)]; + foreach (var v in versions) AvailableCompetencyVersions.Add(new CompetencyVersionListItem(v)); + } + + SelectedCompetencyVersion = AvailableCompetencyVersions.FirstOrDefault(v => v.Id == targetId) + ?? AvailableCompetencyVersions.FirstOrDefault(v => v.IsDefault) + ?? AvailableCompetencyVersions.FirstOrDefault(); + OnPropertyChanged(nameof(HasCompetencyVersionChoice)); + } + public void LoadForEdit(LearningGroup group) { _editingGroup = group; SelectedTypeName = group.Type == GroupType.Class ? "Klasse" : "Kurs"; Name = group.Name; + _pendingCompetencyVersionId = group.CompetencyCatalogVersionId; Subject = group.SubjectId is Guid subjectId ? _subjects.GetById(subjectId)?.Name ?? "" : ""; GradeLevel = group.GradeLevel; SelectedGradingName = group.GradingSystem == GradingSystem.Grades1To6 @@ -905,6 +939,7 @@ public partial class AddGroupDialogViewModel : ObservableObject Result = _editingGroup ?? new LearningGroup(); Result.Name = Name.Trim(); Result.SubjectId = subjectId; + Result.CompetencyCatalogVersionId = subjectId is null ? null : SelectedCompetencyVersion?.Id; Result.Type = IsKurs ? GroupType.Course : GroupType.Class; Result.GradeLevel = GradeLevel; Result.GradingSystem = SelectedGradingName == "Punkte 0–15" @@ -919,3 +954,13 @@ public partial class AddGroupDialogViewModel : ObservableObject _groups.Save(Result); } } + +/// Anzeige-Wrapper für den Bildungsplan-Versions-Picker im Kursdialog, analog zu +/// . +public sealed class CompetencyVersionListItem(CompetencyCatalogVersion v) +{ + public Guid Id { get; } = v.Id; + public string Name { get; } = v.Name; + public bool IsDefault { get; } = v.IsDefault; + public string DisplayName => IsDefault ? $"{Name} (Standard)" : Name; +} diff --git a/LehrerApp.Desktop/ViewModels/Groups/ParticipationViewModels.cs b/LehrerApp.Desktop/ViewModels/Groups/ParticipationViewModels.cs index c3eacf0..b7e04e0 100644 --- a/LehrerApp.Desktop/ViewModels/Groups/ParticipationViewModels.cs +++ b/LehrerApp.Desktop/ViewModels/Groups/ParticipationViewModels.cs @@ -19,11 +19,13 @@ public partial class ParticipationTabViewModel : ObservableObject private readonly IGroupMembershipRepository _memberships; private readonly IGroupRepository _groups; private readonly ICompetencyDomainRepository _competencyDomains; + private readonly ICompetencyCatalogVersionRepository _competencyVersions; private Guid _groupId; private string _schoolYear = ""; private Guid? _subjectId; private int _gradeLevel; + private Guid _competencyCatalogVersionId; private GradingSystem _gradingSystem; public Guid GroupId => _groupId; @@ -63,12 +65,14 @@ public partial class ParticipationTabViewModel : ObservableObject IStudentRepository students, IGroupMembershipRepository memberships, IGroupRepository groups, - ICompetencyDomainRepository competencyDomains) + ICompetencyDomainRepository competencyDomains, + ICompetencyCatalogVersionRepository competencyVersions) { _sessions = sessions; _entries = entries; _aspects = aspects; _students = students; _memberships = memberships; _groups = groups; _competencyDomains = competencyDomains; + _competencyVersions = competencyVersions; } public void Initialize(Guid groupId, string schoolYear, bool isReadOnly = false) @@ -80,11 +84,13 @@ public partial class ParticipationTabViewModel : ObservableObject var group = _groups.GetById(groupId); _subjectId = group?.SubjectId; _gradeLevel = group?.GradeLevel ?? 0; + _competencyCatalogVersionId = CompetencyCatalogVersionResolver.Resolve( + _competencyVersions, _subjectId, group?.CompetencyCatalogVersionId); _gradingSystem = group?.GradingSystem ?? GradingSystem.Grades1To6; GroupLabel = group?.Name ?? ""; HasCompetencyCatalog = _subjectId.HasValue - && _competencyDomains.GetBySubjectAndGrade(_subjectId.Value, _gradeLevel).Count > 0; + && _competencyDomains.GetBySubjectAndGrade(_subjectId.Value, _gradeLevel, _competencyCatalogVersionId).Count > 0; LoadAspects(); LoadSessions(); @@ -236,7 +242,7 @@ public partial class ParticipationTabViewModel : ObservableObject if (!_subjectId.HasValue) return; var active = ActiveCompetencyCodes.ToHashSet(); - foreach (var domain in _competencyDomains.GetBySubjectAndGrade(_subjectId.Value, _gradeLevel)) + foreach (var domain in _competencyDomains.GetBySubjectAndGrade(_subjectId.Value, _gradeLevel, _competencyCatalogVersionId)) { var group = new CompetencyTagGroup(domain.Name, domain.Code); foreach (var item in domain.Items.OrderBy(i => i.SortOrder)) diff --git a/LehrerApp.Desktop/ViewModels/Groups/PlanningViewModels.cs b/LehrerApp.Desktop/ViewModels/Groups/PlanningViewModels.cs index b4a95fd..113be43 100644 --- a/LehrerApp.Desktop/ViewModels/Groups/PlanningViewModels.cs +++ b/LehrerApp.Desktop/ViewModels/Groups/PlanningViewModels.cs @@ -34,6 +34,7 @@ public partial class PlanningTabViewModel : ObservableObject private readonly IGroupRepository _groups; private readonly ISubjectRepository _subjects; private readonly ICompetencyDomainRepository _competencyDomains; + private readonly ICompetencyCatalogVersionRepository _competencyVersions; private readonly AiSettingsService _aiSettings; private readonly IParticipationSessionRepository _participationSessions; @@ -44,6 +45,9 @@ public partial class PlanningTabViewModel : ObservableObject public int GradeLevel { get; private set; } public string SubjectName { get; private set; } = ""; public string GroupLabel { get; private set; } = ""; + /// Aufgelöste Bildungsplan-Version dieser Gruppe (Gruppenwahl, sonst Standardversion des + /// Fachs) - ohne Fach. + public Guid CompetencyCatalogVersionId { get; private set; } [ObservableProperty] private UnitSummary? _selectedUnit; [ObservableProperty] private LessonSummary? _selectedLesson; @@ -85,11 +89,12 @@ public partial class PlanningTabViewModel : ObservableObject public PlanningTabViewModel(IUnitRepository units, ILessonRepository lessons, IGroupRepository groups, ISubjectRepository subjects, - ICompetencyDomainRepository competencyDomains, AiSettingsService aiSettings, - IParticipationSessionRepository participationSessions) + ICompetencyDomainRepository competencyDomains, ICompetencyCatalogVersionRepository competencyVersions, + AiSettingsService aiSettings, IParticipationSessionRepository participationSessions) { _units = units; _lessons = lessons; _groups = groups; _subjects = subjects; _competencyDomains = competencyDomains; + _competencyVersions = competencyVersions; _aiSettings = aiSettings; _participationSessions = participationSessions; } @@ -102,6 +107,8 @@ public partial class PlanningTabViewModel : ObservableObject GradeLevel = group?.GradeLevel ?? 0; SubjectName = SubjectId is Guid sid ? _subjects.GetById(sid)?.Name ?? "" : ""; GroupLabel = group?.Name ?? ""; + CompetencyCatalogVersionId = CompetencyCatalogVersionResolver.Resolve( + _competencyVersions, SubjectId, group?.CompetencyCatalogVersionId); LoadUnits(preferActive: true); } @@ -577,6 +584,7 @@ public partial class UnitDialogViewModel : ObservableObject private readonly Guid _groupId; private readonly Guid? _subjectId; private readonly int _gradeLevel; + private readonly Guid _competencyCatalogVersionId; private readonly Unit? _editingUnit; private readonly List _competencyCodes; @@ -620,10 +628,12 @@ public partial class UnitDialogViewModel : ObservableObject } public UnitDialogViewModel(IUnitRepository units, ICompetencyDomainRepository competencyDomains, - Guid groupId, Guid? subjectId, int gradeLevel, string groupName, string subjectName, Unit? editingUnit) + Guid groupId, Guid? subjectId, int gradeLevel, string groupName, string subjectName, Unit? editingUnit, + Guid competencyCatalogVersionId = default) { _units = units; _competencyDomains = competencyDomains; _groupId = groupId; _subjectId = subjectId; _gradeLevel = gradeLevel; + _competencyCatalogVersionId = competencyCatalogVersionId; _editingUnit = editingUnit; GroupSubjectDisplay = string.IsNullOrWhiteSpace(subjectName) ? $"{groupName} · kein Fach hinterlegt (siehe Lerngruppe)" : $"{groupName} · {subjectName}"; @@ -646,7 +656,7 @@ public partial class UnitDialogViewModel : ObservableObject CompetencyTagGroups.Clear(); if (!_subjectId.HasValue) return; var selected = _competencyCodes.ToHashSet(); - foreach (var domain in _competencyDomains.GetBySubjectAndGrade(_subjectId.Value, _gradeLevel)) + foreach (var domain in _competencyDomains.GetBySubjectAndGrade(_subjectId.Value, _gradeLevel, _competencyCatalogVersionId)) { var group = new CompetencyTagGroup(domain.Name, domain.Code); foreach (var item in domain.Items.OrderBy(i => i.SortOrder)) diff --git a/LehrerApp.Desktop/ViewModels/Settings/SettingsViewModel.Competencies.cs b/LehrerApp.Desktop/ViewModels/Settings/SettingsViewModel.Competencies.cs index a32853c..58a3fee 100644 --- a/LehrerApp.Desktop/ViewModels/Settings/SettingsViewModel.Competencies.cs +++ b/LehrerApp.Desktop/ViewModels/Settings/SettingsViewModel.Competencies.cs @@ -22,39 +22,97 @@ public partial class SettingsViewModel [ObservableProperty] private SubjectListItem? _catalogSubject; [ObservableProperty] private int _catalogGradeLevel = 10; + [ObservableProperty] private CompetencyCatalogVersion? _catalogVersion; [ObservableProperty] private string _newDomainName = ""; [ObservableProperty] private string _newDomainCode = ""; [ObservableProperty] private string _catalogValidation = ""; + [ObservableProperty] private string _newVersionName = ""; + [ObservableProperty] private string _versionValidation = ""; public ObservableCollection Domains { get; } = []; + public ObservableCollection CatalogVersions { get; } = []; // ── Katalog: Laden ──────────────────────────────────────────────────────── - partial void OnCatalogSubjectChanged(SubjectListItem? value) => LoadCatalog(); + partial void OnCatalogSubjectChanged(SubjectListItem? value) => LoadVersions(); partial void OnCatalogGradeLevelChanged(int value) => LoadCatalog(); + partial void OnCatalogVersionChanged(CompetencyCatalogVersion? value) => LoadCatalog(); + + private void LoadVersions() + { + var selectedId = CatalogVersion?.Id; + CatalogVersions.Clear(); + if (CatalogSubject is null) { LoadCatalog(); return; } + foreach (var v in _versionRepo.GetBySubject(CatalogSubject.Id)) + CatalogVersions.Add(v); + if (CatalogVersions.Count == 0) + CatalogVersions.Add(_versionRepo.GetOrCreateDefault(CatalogSubject.Id)); + CatalogVersion = CatalogVersions.FirstOrDefault(v => v.Id == selectedId) + ?? CatalogVersions.FirstOrDefault(v => v.IsDefault) + ?? CatalogVersions.FirstOrDefault(); + } private void LoadCatalog() { Domains.Clear(); CatalogValidation = ""; - if (CatalogSubject is null) return; - foreach (var d in _domainRepo.GetBySubjectAndGrade(CatalogSubject.Id, CatalogGradeLevel)) + if (CatalogSubject is null || CatalogVersion is null) return; + foreach (var d in _domainRepo.GetBySubjectAndGrade(CatalogSubject.Id, CatalogGradeLevel, CatalogVersion.Id)) Domains.Add(CreateDomainEditItem(d)); RefreshDomainMoveState(); } + // ── Versionen (Bildungspläne) verwalten ────────────────────────────────── + + [RelayCommand] + private void AddCatalogVersion() + { + if (CatalogSubject is null) { VersionValidation = "Bitte zuerst ein Fach auswählen."; return; } + if (string.IsNullOrWhiteSpace(NewVersionName)) { VersionValidation = "Name der Version erforderlich."; return; } + + var version = new CompetencyCatalogVersion + { + SubjectId = CatalogSubject.Id, Name = NewVersionName.Trim(), + SortOrder = CatalogVersions.Count, + }; + _versionRepo.Save(version); + CatalogVersions.Add(version); + CatalogVersion = version; + NewVersionName = ""; VersionValidation = ""; + } + + [RelayCommand] + private void SetCatalogVersionAsDefault(CompetencyCatalogVersion? version) + { + if (version is null || CatalogSubject is null) return; + version.IsDefault = true; + _versionRepo.Save(version); + LoadVersions(); + } + + [RelayCommand] + private void DeleteCatalogVersion(CompetencyCatalogVersion? version) + { + if (version is null) return; + try { _versionRepo.Delete(version.Id); } + catch (InvalidOperationException ex) { VersionValidation = ex.Message; return; } + LoadVersions(); + } + // ── Katalog: Bereich hinzufügen / löschen ──────────────────────────────── [RelayCommand] private void AddDomain() { if (CatalogSubject is null) { CatalogValidation = "Bitte zuerst ein Fach auswählen."; return; } + if (CatalogVersion is null) { CatalogValidation = "Bitte zuerst eine Version auswählen."; return; } if (string.IsNullOrWhiteSpace(NewDomainName)) { CatalogValidation = "Bereichsname erforderlich."; return; } var domain = new CompetencyDomain { SubjectId = CatalogSubject.Id, GradeLevel = CatalogGradeLevel, + VersionId = CatalogVersion.Id, Name = NewDomainName.Trim(), Code = NewDomainCode.Trim(), SortOrder = Domains.Count, diff --git a/LehrerApp.Desktop/ViewModels/Settings/SettingsViewModel.JsonImportExport.cs b/LehrerApp.Desktop/ViewModels/Settings/SettingsViewModel.JsonImportExport.cs index 0f2b36f..f4f6e53 100644 --- a/LehrerApp.Desktop/ViewModels/Settings/SettingsViewModel.JsonImportExport.cs +++ b/LehrerApp.Desktop/ViewModels/Settings/SettingsViewModel.JsonImportExport.cs @@ -27,11 +27,16 @@ public partial class SettingsViewModel CatalogValidation = "Bitte zuerst ein Fach auswählen."; return null; } + if (CatalogVersion is null) + { + CatalogValidation = "Bitte zuerst eine Version auswählen."; + return null; + } try { CatalogValidation = ""; return _catalogImport.Analyze( - json, CatalogSubject.Id, CatalogSubject.Name, CatalogGradeLevel); + json, CatalogSubject.Id, CatalogSubject.Name, CatalogGradeLevel, CatalogVersion.Id); } catch (InvalidDataException ex) { @@ -71,9 +76,15 @@ public partial class SettingsViewModel return null; } + if (CatalogVersion is null) + { + CatalogValidation = "Bitte zuerst eine Version auswählen."; + return null; + } + CatalogValidation = ""; return _catalogImport.Analyze(SerializeCatalog(targetGradeLevel), CatalogSubject.Id, - CatalogSubject.Name, targetGradeLevel); + CatalogSubject.Name, targetGradeLevel, CatalogVersion.Id); } public void SetCatalogCopyStatus(int targetGradeLevel) => diff --git a/LehrerApp.Desktop/ViewModels/Settings/SettingsViewModel.cs b/LehrerApp.Desktop/ViewModels/Settings/SettingsViewModel.cs index e0f1d23..ef4857f 100644 --- a/LehrerApp.Desktop/ViewModels/Settings/SettingsViewModel.cs +++ b/LehrerApp.Desktop/ViewModels/Settings/SettingsViewModel.cs @@ -49,6 +49,7 @@ public partial class SettingsViewModel : ObservableObject { private readonly ISubjectRepository _subjects; private readonly ICompetencyDomainRepository _domainRepo; + private readonly ICompetencyCatalogVersionRepository _versionRepo; private readonly IGradingKeyTemplateRepository _gradingKeyTemplates; private readonly IGradingSchemeRepository _gradingSchemes; private readonly GradingService _grading; @@ -95,6 +96,7 @@ public partial class SettingsViewModel : ObservableObject public TrashViewModel TrashTab { get; } public SettingsViewModel(ISubjectRepository subjects, ICompetencyDomainRepository domainRepo, + ICompetencyCatalogVersionRepository versionRepo, IGradingKeyTemplateRepository gradingKeyTemplates, IGradingSchemeRepository gradingSchemes, GradingService grading, BackupService backups, BackupSettingsService backupSettings, DatabaseEncryptionService dbEncryption, @@ -123,6 +125,7 @@ public partial class SettingsViewModel : ObservableObject TrashTab = trashTab; _subjects = subjects; _domainRepo = domainRepo; + _versionRepo = versionRepo; _gradingKeyTemplates = gradingKeyTemplates; _gradingSchemes = gradingSchemes; _grading = grading; diff --git a/LehrerApp.Desktop/Views/Groups/AddGroupDialog.axaml b/LehrerApp.Desktop/Views/Groups/AddGroupDialog.axaml index d98b768..f59bcc1 100644 --- a/LehrerApp.Desktop/Views/Groups/AddGroupDialog.axaml +++ b/LehrerApp.Desktop/Views/Groups/AddGroupDialog.axaml @@ -39,6 +39,21 @@ HorizontalAlignment="Stretch"/> + + + + + + + + + + + + diff --git a/LehrerApp.Desktop/Views/Groups/CompetencyOverviewTabView.axaml b/LehrerApp.Desktop/Views/Groups/CompetencyOverviewTabView.axaml index 53cfcc6..6f85772 100644 --- a/LehrerApp.Desktop/Views/Groups/CompetencyOverviewTabView.axaml +++ b/LehrerApp.Desktop/Views/Groups/CompetencyOverviewTabView.axaml @@ -8,6 +8,8 @@ + diff --git a/LehrerApp.Desktop/Views/Groups/GroupDetailView.axaml.cs b/LehrerApp.Desktop/Views/Groups/GroupDetailView.axaml.cs index 4526e16..47e3473 100644 --- a/LehrerApp.Desktop/Views/Groups/GroupDetailView.axaml.cs +++ b/LehrerApp.Desktop/Views/Groups/GroupDetailView.axaml.cs @@ -245,13 +245,17 @@ public partial class GroupDetailView : UserControl { if (DataContext is not GroupDetailViewModel vm || vm.Group is null) return false; + var competencyCatalogVersionId = CompetencyCatalogVersionResolver.Resolve( + App.Services.GetRequiredService(), + vm.Group.SubjectId, vm.Group.CompetencyCatalogVersionId); var dialogVm = new ExamDialogViewModel( App.Services.GetRequiredService(), App.Services.GetRequiredService(), App.Services.GetRequiredService(), App.Services.GetRequiredService(), groupId, vm.Group.SubjectId, vm.Group.GradeLevel, vm.Group.GradingSystem, - vm.SubjectName, vm.Group.IsDifferentiated, editingExam, duplicateSource); + vm.SubjectName, vm.Group.IsDifferentiated, editingExam, duplicateSource, + competencyCatalogVersionId); var dialog = new ExamDialog { DataContext = dialogVm }; var owner = TopLevel.GetTopLevel(this) as Window; diff --git a/LehrerApp.Desktop/Views/Groups/PlanningTabView.axaml.cs b/LehrerApp.Desktop/Views/Groups/PlanningTabView.axaml.cs index 88b0c49..7713aba 100644 --- a/LehrerApp.Desktop/Views/Groups/PlanningTabView.axaml.cs +++ b/LehrerApp.Desktop/Views/Groups/PlanningTabView.axaml.cs @@ -54,7 +54,8 @@ public partial class PlanningTabView : UserControl var dialogVm = new UnitDialogViewModel( App.Services.GetRequiredService(), App.Services.GetRequiredService(), - groupId, vm.SubjectId, vm.GradeLevel, vm.GroupLabel, vm.SubjectName, editingUnit); + groupId, vm.SubjectId, vm.GradeLevel, vm.GroupLabel, vm.SubjectName, editingUnit, + vm.CompetencyCatalogVersionId); var dialog = new UnitDialog { DataContext = dialogVm }; var owner = TopLevel.GetTopLevel(this) as Window; diff --git a/LehrerApp.Desktop/Views/Settings/SettingsView.axaml b/LehrerApp.Desktop/Views/Settings/SettingsView.axaml index 907c0c8..cefe0fd 100644 --- a/LehrerApp.Desktop/Views/Settings/SettingsView.axaml +++ b/LehrerApp.Desktop/Views/Settings/SettingsView.axaml @@ -2,6 +2,7 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:vm="clr-namespace:LehrerApp.Desktop.ViewModels.Settings" xmlns:services="clr-namespace:LehrerApp.Core.Services;assembly=LehrerApp.Core" + xmlns:models="clr-namespace:LehrerApp.Core.Models;assembly=LehrerApp.Core" xmlns:shared="clr-namespace:LehrerApp.Desktop.Views.Shared" x:Class="LehrerApp.Desktop.Views.Settings.SettingsView" x:DataType="vm:SettingsViewModel"> @@ -112,8 +113,8 @@ - - + + -