feat: Bildungsplan-Versionierung für Kompetenzkataloge, vollständig über MCP exponiert
Neue Bildungspläne wachsen je Fach klassenstufenweise hoch, sodass Kurse abhängig vom Einschulungsjahr entweder dem alten oder dem neuen Bildungsplan folgen. CompetencyCatalogVersion (Fach + Name + IsDefault) macht das abbildbar: CompetencyDomain bekommt eine VersionId, Kurse wählen über LearningGroup.CompetencyCatalogVersionId (null = Standardversion des Fachs) welche Version gilt. Migration legt für bestehende Kataloge automatisch eine Standardversion an, damit alte Datenbanken unverändert weiterlaufen. MCP: neue Tools für Versions-CRUD (get/create/update/delete_competency_plan_version, update_group_competency_version), get_competency_catalog/create_competency_domain um optionalen versionId-Parameter erweitert. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
4ffba35eff
commit
f97edff2fe
45 files changed
+1020
-163
No files matched your search
@@ -284,16 +284,31 @@ public interface ISubjectRepository
|
||||
}
|
||||
public interface ICompetencyDomainRepository
|
||||
{
|
||||
List<CompetencyDomain> 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<CompetencyDomain> GetBySubject(Guid subjectId);
|
||||
List<CompetencyDomain> 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<CompetencyDomain> 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<CompetencyDomain> domains);
|
||||
void DeleteBySubjectGradeAndVersion(Guid subjectId, int gradeLevel, Guid versionId);
|
||||
void ReplaceForSubjectGradeAndVersion(Guid subjectId, int gradeLevel, Guid versionId, List<CompetencyDomain> domains);
|
||||
}
|
||||
|
||||
public interface ICompetencyCatalogVersionRepository
|
||||
{
|
||||
List<CompetencyCatalogVersion> GetBySubject(Guid subjectId);
|
||||
CompetencyCatalogVersion? GetById(Guid id);
|
||||
/// <summary>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.</summary>
|
||||
CompetencyCatalogVersion GetOrCreateDefault(Guid subjectId);
|
||||
void Save(CompetencyCatalogVersion version);
|
||||
/// <summary>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).</summary>
|
||||
void Delete(Guid id);
|
||||
}
|
||||
public interface IShorthandCodeRepository
|
||||
{
|
||||
|
||||
@@ -5,6 +5,11 @@ public class CompetencyDomain
|
||||
public Guid Id { get; set; } = Guid.NewGuid();
|
||||
public Guid SubjectId { get; set; }
|
||||
public int GradeLevel { get; set; }
|
||||
/// <summary>Bildungsplan-Version dieses Bereichs (<see cref="CompetencyCatalogVersion"/>) - 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.</summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>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 - <see cref="LearningGroup.CompetencyCatalogVersionId"/>
|
||||
/// wählt je Kurs/Klasse, welche Version gilt. Je Fach ist genau eine Version <see cref="IsDefault"/>
|
||||
/// (Kurse ohne explizite Wahl nutzen sie, siehe Doc-Kommentar auf <see cref="LearningGroup"/>).</summary>
|
||||
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();
|
||||
|
||||
@@ -8,6 +8,14 @@ public class LearningGroup
|
||||
public Guid? SubjectId { get; set; }
|
||||
public string SchoolYear { get; set; } = "";
|
||||
public int GradeLevel { get; set; }
|
||||
/// <summary>
|
||||
/// Gewählte Bildungsplan-Version (<see cref="CompetencyCatalogVersion"/>) für den Kompetenzkatalog
|
||||
/// dieses Kurses/dieser Klasse. Null bedeutet bewusst nicht "keine Version", sondern "Standardversion
|
||||
/// des Fachs" (<see cref="CompetencyCatalogVersion.IsDefault"/>) - 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.
|
||||
/// </summary>
|
||||
public Guid? CompetencyCatalogVersionId { get; set; }
|
||||
public GradingSystem GradingSystem { get; set; }
|
||||
public int? HoursPerWeek { get; set; }
|
||||
public bool IsActive { get; set; } = true;
|
||||
|
||||
@@ -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<CompetencyDomain> ImportedDomains { get; init; }
|
||||
public required string ExistingFingerprint { get; init; }
|
||||
public List<string> 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<CompetencyCatalogImportConflict>();
|
||||
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<string>? 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<string>());
|
||||
|
||||
repository.ReplaceForSubjectAndGrade(preview.SubjectId, preview.GradeLevel, result);
|
||||
repository.ReplaceForSubjectGradeAndVersion(preview.SubjectId, preview.GradeLevel, preview.VersionId, result);
|
||||
}
|
||||
|
||||
private static List<CompetencyDomain> Merge(List<CompetencyDomain> existing,
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
using LehrerApp.Core.Interfaces;
|
||||
|
||||
namespace LehrerApp.Core.Services;
|
||||
|
||||
/// <summary>Löst die für einen Kurs/eine Klasse geltende Bildungsplan-Version auf: die explizit am
|
||||
/// Kurs gewählte Version (<see cref="Models.LearningGroup.CompetencyCatalogVersionId"/>), falls sie
|
||||
/// noch existiert, sonst die Standardversion des Fachs (siehe Doc-Kommentar auf dem Feld). Ohne Fach
|
||||
/// gibt es keine Version (<see cref="Guid.Empty"/>).</summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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<BsonDocument>("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]
|
||||
|
||||
@@ -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<InvalidOperationException>(() => 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<InvalidOperationException>(() => 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<InvalidOperationException>(() => 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));
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,7 @@ public class LiteDbContext : IDisposable
|
||||
/// Aktuelle Schema-Version. Migrationsschritte werden versioniert unter
|
||||
/// <see cref="RunVersionedMigrations"/> ergänzt, statt bei jedem Start erneut
|
||||
/// (idempotent, aber unnötig) über alle Daten zu laufen.
|
||||
private const int CurrentSchemaVersion = 4;
|
||||
private const int CurrentSchemaVersion = 5;
|
||||
|
||||
private readonly LiteDatabase _db;
|
||||
|
||||
@@ -61,6 +61,7 @@ public class LiteDbContext : IDisposable
|
||||
public ILiteCollection<ParticipationSection> ParticipationSections => _db.GetCollection<ParticipationSection>("participation_sections");
|
||||
public ILiteCollection<Subject> Subjects => _db.GetCollection<Subject>("subjects");
|
||||
public ILiteCollection<CompetencyDomain> CompetencyDomains => _db.GetCollection<CompetencyDomain>("competency_domains");
|
||||
public ILiteCollection<CompetencyCatalogVersion> CompetencyCatalogVersions => _db.GetCollection<CompetencyCatalogVersion>("competency_catalog_versions");
|
||||
public ILiteCollection<ShorthandCode> ShorthandCodes => _db.GetCollection<ShorthandCode>("shorthand_codes");
|
||||
public ILiteCollection<AlternativeLessonPath> AlternativeLessonPaths => _db.GetCollection<AlternativeLessonPath>("alternative_lesson_paths");
|
||||
public ILiteCollection<TimetableSlot> TimetableSlots => _db.GetCollection<TimetableSlot>("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
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Backfill für <see cref="CompetencyDomain.VersionId"/>: 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.</summary>
|
||||
private void MigrateCompetencyCatalogVersions()
|
||||
{
|
||||
var defaultVersionBySubject = new Dictionary<Guid, Guid>();
|
||||
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);
|
||||
|
||||
@@ -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<CompetencyDomain> GetBySubjectAndGrade(Guid subjectId, int gradeLevel) =>
|
||||
public List<CompetencyDomain> 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<CompetencyDomain> GetBySubject(Guid subjectId) =>
|
||||
public List<CompetencyDomain> 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<CompetencyDomain> domains)
|
||||
public void ReplaceForSubjectGradeAndVersion(Guid subjectId, int gradeLevel, Guid versionId, List<CompetencyDomain> 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<CompetencyCatalogVersion> 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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>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.</summary>
|
||||
|
||||
@@ -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<AiBackendException>(() =>
|
||||
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, "");
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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 <see cref="BuildAiSettingsService"/>, eigenes Temp-Verzeichnis je Aufruf.
|
||||
public static McpSettingsService BuildMcpSettingsService()
|
||||
@@ -391,24 +391,57 @@ public class FakeCompetencyDomains : ICompetencyDomainRepository
|
||||
{
|
||||
private readonly List<CompetencyDomain> _all = [];
|
||||
public void Add(CompetencyDomain d) => _all.Add(d);
|
||||
public List<CompetencyDomain> GetBySubjectAndGrade(Guid subjectId, int gradeLevel) =>
|
||||
_all.Where(d => d.SubjectId == subjectId && d.GradeLevel == gradeLevel)
|
||||
public List<CompetencyDomain> 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<CompetencyDomain> GetBySubject(Guid subjectId) =>
|
||||
_all.Where(d => d.SubjectId == subjectId)
|
||||
public List<CompetencyDomain> 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<CompetencyDomain> 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<CompetencyDomain> 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<CompetencyCatalogVersion> _all = [];
|
||||
public void Add(CompetencyCatalogVersion v) => _all.Add(v);
|
||||
public List<CompetencyCatalogVersion> 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<ShorthandCode> all) : IShorthandCodeRepository
|
||||
{
|
||||
public List<ShorthandCode> GetAll() => all;
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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" };
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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" };
|
||||
|
||||
@@ -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<string>());
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
@@ -176,6 +176,7 @@ public static class AppBootstrapper
|
||||
services.AddSingleton<IParticipationSectionRepository, ParticipationSectionRepository>();
|
||||
services.AddSingleton<ISubjectRepository, SubjectRepository>();
|
||||
services.AddSingleton<ICompetencyDomainRepository, CompetencyDomainRepository>();
|
||||
services.AddSingleton<ICompetencyCatalogVersionRepository, CompetencyCatalogVersionRepository>();
|
||||
services.AddSingleton<IShorthandCodeRepository, ShorthandCodeRepository>();
|
||||
services.AddSingleton<IAlternativeLessonPathRepository, AlternativeLessonPathRepository>();
|
||||
services.AddSingleton<IAttachmentStorage, LiteAttachmentStorage>();
|
||||
|
||||
@@ -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<TimeOnly>
|
||||
/// </summary>
|
||||
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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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",
|
||||
];
|
||||
}
|
||||
@@ -5,18 +5,21 @@ using LehrerApp.Core.Models;
|
||||
|
||||
namespace LehrerApp.Desktop.Services.Mcp.Tools;
|
||||
|
||||
/// <summary>MCP-Tools für die Fach- und Kompetenzkatalog-Verwaltung (Nutzer-Nachtrag zu 4.5.28ff.):
|
||||
/// <see cref="Subject"/> und <see cref="CompetencyDomain"/>/<see cref="CompetencyItem"/> vollständig
|
||||
/// anlegen/ändern/löschen. Die Zuordnung von Kompetenzcodes zu einzelnen Stunden lebt bewusst in
|
||||
/// <see cref="LessonPlanTools"/> (AddLessonCompetency/RemoveLessonCompetency), nicht hier - sie
|
||||
/// gehört fachlich zur Stundenverwaltung, nicht zum Katalog selbst.
|
||||
/// <summary>MCP-Tools für die Fach- und Kompetenzkatalog-Verwaltung (Nutzer-Nachtrag zu 4.5.28ff.,
|
||||
/// später um Bildungsplan-Versionierung erweitert): <see cref="Subject"/>,
|
||||
/// <see cref="CompetencyCatalogVersion"/> und <see cref="CompetencyDomain"/>/<see cref="CompetencyItem"/>
|
||||
/// vollständig anlegen/ändern/löschen. Die Zuordnung von Kompetenzcodes zu einzelnen Stunden lebt
|
||||
/// bewusst in <see cref="LessonPlanTools"/> (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.</summary>
|
||||
/// "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.</summary>
|
||||
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<SubjectDto> 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<CompetencyPlanVersionDto> 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<CompetencyDomainDto> 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<WriteResultDto> 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<WriteResultDto> 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<WriteResultDto> 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<WriteResultDto> 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());
|
||||
}
|
||||
@@ -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<CompetencyItemDto> Items);
|
||||
|
||||
/// <summary>Eine Bildungsplan-Version eines Fachs, siehe CompetencyTools.GetCompetencyPlanVersions.</summary>
|
||||
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.</summary>
|
||||
public record GroupDto(
|
||||
Guid Id, string Name, GroupType Type, string SchoolYear, int GradeLevel,
|
||||
Guid? SubjectId, bool IsActive);
|
||||
Guid? SubjectId, bool IsActive, Guid? CompetencyPlanVersionId);
|
||||
|
||||
/// <summary>Anonymisierte Fehlzeiten-Diskrepanz (siehe UntisComparisonTools.GetUntisAbsenceRows):
|
||||
/// bewusst KEIN Schülername/keine Klasse - <see cref="RowId"/> ist die einzige Kennung, über die
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
using System.ComponentModel;
|
||||
using System.Text;
|
||||
using LehrerApp.Core.Interfaces;
|
||||
|
||||
namespace LehrerApp.Desktop.Services.Mcp.Tools;
|
||||
|
||||
/// <summary>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.</summary>
|
||||
public class GroupTools(IGroupRepository groups)
|
||||
/// <summary>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".</summary>
|
||||
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<GroupDto> 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<WriteResultDto> 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.");
|
||||
}
|
||||
}
|
||||
@@ -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<CompetencyStudentOption> StudentOptions { get; } = [];
|
||||
public ObservableCollection<CompetencyOverviewRow> 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<CompetencyDomain> 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)
|
||||
|
||||
@@ -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<CompetencyTagGroup>();
|
||||
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))
|
||||
|
||||
@@ -799,8 +799,10 @@ public partial class AddGroupDialogViewModel : ObservableObject
|
||||
{
|
||||
private readonly IGroupRepository _groups;
|
||||
private readonly ISubjectRepository _subjects;
|
||||
private readonly ICompetencyCatalogVersionRepository _competencyVersions;
|
||||
private List<Subject> _allSubjects = [];
|
||||
private LearningGroup? _editingGroup;
|
||||
private Guid? _pendingCompetencyVersionId;
|
||||
|
||||
public List<string> 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<string> SchoolYears { get; }
|
||||
public List<string> GradingOptions { get; } = ["Noten 1–6", "Punkte 0–15"];
|
||||
public List<string> KnownSubjectNames { get; private set; } = [];
|
||||
public ObservableCollection<CompetencyVersionListItem> 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();
|
||||
|
||||
/// <summary>Baut die Versionsliste für das aktuell im Freitextfeld <see cref="Subject"/> erkannte
|
||||
/// Fach neu auf. Ein neues, noch nicht angelegtes Fach hat noch keine Versionen - die Auswahl
|
||||
/// bleibt dann leer (<see cref="HasCompetencyVersionChoice"/> blendet den Picker aus), der Kurs
|
||||
/// bekommt beim Speichern automatisch die Standardversion, sobald das Fach existiert.</summary>
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Anzeige-Wrapper für den Bildungsplan-Versions-Picker im Kursdialog, analog zu
|
||||
/// <see cref="SubjectListItem"/>.</summary>
|
||||
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;
|
||||
}
|
||||
@@ -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))
|
||||
|
||||
@@ -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; } = "";
|
||||
/// <summary>Aufgelöste Bildungsplan-Version dieser Gruppe (Gruppenwahl, sonst Standardversion des
|
||||
/// Fachs) - <see cref="Guid.Empty"/> ohne Fach.</summary>
|
||||
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<string> _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))
|
||||
|
||||
@@ -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<DomainEditItem> Domains { get; } = [];
|
||||
public ObservableCollection<CompetencyCatalogVersion> 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,
|
||||
|
||||
@@ -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) =>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -39,6 +39,21 @@
|
||||
HorizontalAlignment="Stretch"/>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Bildungsplan-Version: nur sichtbar, wenn das Fach bereits existiert und mehr als eine
|
||||
Version hat (neue Bildungspläne wachsen klassenstufenweise hoch, siehe Settings/Kompetenzen) -->
|
||||
<StackPanel Spacing="4" IsVisible="{Binding HasCompetencyVersionChoice}">
|
||||
<TextBlock Text="Bildungsplan-Version" FontSize="12" Opacity="0.7"/>
|
||||
<ComboBox ItemsSource="{Binding AvailableCompetencyVersions}"
|
||||
SelectedItem="{Binding SelectedCompetencyVersion}"
|
||||
HorizontalAlignment="Stretch">
|
||||
<ComboBox.ItemTemplate>
|
||||
<DataTemplate DataType="vm:CompetencyVersionListItem">
|
||||
<TextBlock Text="{Binding DisplayName}"/>
|
||||
</DataTemplate>
|
||||
</ComboBox.ItemTemplate>
|
||||
</ComboBox>
|
||||
</StackPanel>
|
||||
|
||||
<Grid ColumnDefinitions="*,12,*">
|
||||
<StackPanel Grid.Column="0" Spacing="4">
|
||||
<TextBlock Text="Klassenstufe *" FontSize="12" Opacity="0.7"/>
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<StackPanel Grid.Column="0" Spacing="3">
|
||||
<TextBlock Text="Kompetenzübersicht" FontSize="18" FontWeight="SemiBold"/>
|
||||
<TextBlock Text="{Binding VersionLabel, StringFormat='Bildungsplan-Version: {0}'}" FontSize="12" Opacity="0.65"
|
||||
IsVisible="{Binding VersionLabel, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
|
||||
<TextBlock Text="{Binding Summary}" FontSize="12" Opacity="0.65"/>
|
||||
</StackPanel>
|
||||
<StackPanel Grid.Column="1" Orientation="Horizontal" Spacing="8" VerticalAlignment="Top">
|
||||
|
||||
@@ -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<ICompetencyCatalogVersionRepository>(),
|
||||
vm.Group.SubjectId, vm.Group.CompetencyCatalogVersionId);
|
||||
var dialogVm = new ExamDialogViewModel(
|
||||
App.Services.GetRequiredService<IExamRepository>(),
|
||||
App.Services.GetRequiredService<ICompetencyDomainRepository>(),
|
||||
App.Services.GetRequiredService<IGradingKeyTemplateRepository>(),
|
||||
App.Services.GetRequiredService<GradingService>(),
|
||||
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;
|
||||
|
||||
@@ -54,7 +54,8 @@ public partial class PlanningTabView : UserControl
|
||||
var dialogVm = new UnitDialogViewModel(
|
||||
App.Services.GetRequiredService<IUnitRepository>(),
|
||||
App.Services.GetRequiredService<ICompetencyDomainRepository>(),
|
||||
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;
|
||||
|
||||
@@ -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 @@
|
||||
<ScrollViewer>
|
||||
<StackPanel Margin="32,20,32,28" Spacing="14" MaxWidth="640">
|
||||
|
||||
<!-- Fach + Klassenstufe Auswahl + Import/Export -->
|
||||
<Grid ColumnDefinitions="*,12,100,12,Auto,8,Auto,8,Auto">
|
||||
<!-- Fach + Klassenstufe + Bildungsplan-Version Auswahl + Import/Export -->
|
||||
<Grid ColumnDefinitions="*,12,90,12,160,12,Auto,8,Auto,8,Auto">
|
||||
<ComboBox Grid.Column="0"
|
||||
ItemsSource="{Binding Subjects}"
|
||||
SelectedItem="{Binding CatalogSubject}"
|
||||
@@ -128,15 +129,63 @@
|
||||
<NumericUpDown Grid.Column="2"
|
||||
Value="{Binding CatalogGradeLevel}"
|
||||
Minimum="1" Maximum="13" FormatString="0"/>
|
||||
<Button Grid.Column="4" Content="JSON importieren" Click="OnImportClick"
|
||||
<ComboBox Grid.Column="4"
|
||||
ItemsSource="{Binding CatalogVersions}"
|
||||
SelectedItem="{Binding CatalogVersion}"
|
||||
IsEnabled="{Binding CatalogSubject, Converter={x:Static ObjectConverters.IsNotNull}}"
|
||||
HorizontalAlignment="Stretch"
|
||||
ToolTip.Tip="Bildungsplan-Version dieses Katalogs">
|
||||
<ComboBox.ItemTemplate>
|
||||
<DataTemplate DataType="models:CompetencyCatalogVersion">
|
||||
<TextBlock Text="{Binding Name}"/>
|
||||
</DataTemplate>
|
||||
</ComboBox.ItemTemplate>
|
||||
</ComboBox>
|
||||
<Button Grid.Column="6" Content="JSON importieren" Click="OnImportClick"
|
||||
IsEnabled="{Binding CatalogSubject, Converter={x:Static ObjectConverters.IsNotNull}}"/>
|
||||
<Button Grid.Column="6" Content="JSON exportieren" Click="OnExportClick"
|
||||
<Button Grid.Column="8" Content="JSON exportieren" Click="OnExportClick"
|
||||
IsEnabled="{Binding CatalogSubject, Converter={x:Static ObjectConverters.IsNotNull}}"/>
|
||||
<Button Grid.Column="8" Content="Kopieren…" Click="OnCopyCatalogClick"
|
||||
<Button Grid.Column="10" Content="Kopieren…" Click="OnCopyCatalogClick"
|
||||
IsEnabled="{Binding CatalogSubject, Converter={x:Static ObjectConverters.IsNotNull}}"
|
||||
ToolTip.Tip="Katalog in eine andere Klassenstufe kopieren"/>
|
||||
ToolTip.Tip="Katalog in eine andere Klassenstufe (derselben Version) kopieren"/>
|
||||
</Grid>
|
||||
|
||||
<!-- Bildungsplan-Versionen verwalten -->
|
||||
<Border BorderBrush="{DynamicResource SystemControlForegroundBaseLowBrush}"
|
||||
BorderThickness="1" CornerRadius="6" Padding="14,12"
|
||||
IsVisible="{Binding CatalogSubject, Converter={x:Static ObjectConverters.IsNotNull}}">
|
||||
<StackPanel Spacing="10">
|
||||
<TextBlock Text="Bildungsplan-Versionen" FontSize="12" FontWeight="SemiBold" Opacity="0.7"
|
||||
ToolTip.Tip="Für schrittweise hochwachsende neue Bildungspläne: mehrere parallel gültige Kompetenzkataloge je Fach, je Kurs auswählbar."/>
|
||||
<ItemsControl ItemsSource="{Binding CatalogVersions}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate DataType="models:CompetencyCatalogVersion">
|
||||
<Grid ColumnDefinitions="*,Auto,8,Auto,8,Auto" Margin="0,3">
|
||||
<TextBlock Grid.Column="0" Text="{Binding Name}" VerticalAlignment="Center"/>
|
||||
<TextBlock Grid.Column="1" Text="Standard" FontSize="11" Opacity="0.6"
|
||||
VerticalAlignment="Center" IsVisible="{Binding IsDefault}"/>
|
||||
<Button Grid.Column="3" Content="Als Standard" FontSize="12" Padding="8,3"
|
||||
IsVisible="{Binding !IsDefault}"
|
||||
Command="{Binding $parent[ItemsControl].((vm:SettingsViewModel)DataContext).SetCatalogVersionAsDefaultCommand}"
|
||||
CommandParameter="{Binding}"/>
|
||||
<Button Grid.Column="5" Content="Löschen" FontSize="12" Padding="8,3"
|
||||
Command="{Binding $parent[ItemsControl].((vm:SettingsViewModel)DataContext).DeleteCatalogVersionCommand}"
|
||||
CommandParameter="{Binding}"/>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
<Grid ColumnDefinitions="*,12,Auto">
|
||||
<TextBox Grid.Column="0" Text="{Binding NewVersionName}"
|
||||
PlaceholderText="Name der neuen Version (z.B. Bildungsplan 2026)"/>
|
||||
<Button Grid.Column="2" Content="Version anlegen" Command="{Binding AddCatalogVersionCommand}"/>
|
||||
</Grid>
|
||||
<TextBlock Text="{Binding VersionValidation}" Foreground="Red" FontSize="12"
|
||||
TextWrapping="Wrap"
|
||||
IsVisible="{Binding VersionValidation, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- Katalog-Validierungsmeldung -->
|
||||
<TextBlock Text="{Binding CatalogValidation}" Foreground="Red" FontSize="12"
|
||||
IsVisible="{Binding CatalogValidation, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
|
||||
|
||||
@@ -146,6 +146,7 @@ public class EventApplier(LiteDbContext db, byte[] syncKey, HttpClient? http = n
|
||||
Simple<SupervisionDuty>(context => context.SupervisionDuties);
|
||||
Simple<SubstitutionEntry>(context => context.SubstitutionEntries);
|
||||
Simple<CompetencyDomain>(context => context.CompetencyDomains);
|
||||
Simple<CompetencyCatalogVersion>(context => context.CompetencyCatalogVersions);
|
||||
Simple<Vorgang>(context => context.Vorgaenge);
|
||||
Simple<UntisHubJobState>(context => context.UntisHubJobStates);
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ public sealed class CompetencyCatalogImportServiceTests
|
||||
var service = new CompetencyCatalogImportService(repository);
|
||||
|
||||
Assert.Throws<InvalidDataException>(() =>
|
||||
service.Analyze("{ kaputt", SubjectId, "Mathematik", 7));
|
||||
service.Analyze("{ kaputt", SubjectId, "Mathematik", 7, Guid.Empty));
|
||||
|
||||
Assert.False(repository.ReplaceWasCalled);
|
||||
Assert.Equal("Alte Beschreibung", repository.Current.Single().Items.Single().Description);
|
||||
@@ -27,7 +27,7 @@ public sealed class CompetencyCatalogImportServiceTests
|
||||
{
|
||||
var service = new CompetencyCatalogImportService(RepositoryWithExistingCatalog());
|
||||
|
||||
var preview = service.Analyze(JsonWithConflictAndNewItem(), SubjectId, "Mathematik", 7);
|
||||
var preview = service.Analyze(JsonWithConflictAndNewItem(), SubjectId, "Mathematik", 7, Guid.Empty);
|
||||
|
||||
Assert.Equal(0, preview.NewDomains);
|
||||
Assert.Equal(1, preview.NewCompetencies);
|
||||
@@ -40,7 +40,7 @@ public sealed class CompetencyCatalogImportServiceTests
|
||||
{
|
||||
var repository = RepositoryWithExistingCatalog();
|
||||
var service = new CompetencyCatalogImportService(repository);
|
||||
var preview = service.Analyze(JsonWithConflictAndNewItem(), SubjectId, "Mathematik", 7);
|
||||
var preview = service.Analyze(JsonWithConflictAndNewItem(), SubjectId, "Mathematik", 7, Guid.Empty);
|
||||
|
||||
service.Apply(preview, CompetencyCatalogImportMode.Merge);
|
||||
|
||||
@@ -55,7 +55,7 @@ public sealed class CompetencyCatalogImportServiceTests
|
||||
{
|
||||
var repository = RepositoryWithExistingCatalog();
|
||||
var service = new CompetencyCatalogImportService(repository);
|
||||
var preview = service.Analyze(JsonWithConflictAndNewItem(), SubjectId, "Mathematik", 7);
|
||||
var preview = service.Analyze(JsonWithConflictAndNewItem(), SubjectId, "Mathematik", 7, Guid.Empty);
|
||||
|
||||
service.Apply(preview, CompetencyCatalogImportMode.Merge, new HashSet<string> { "item:A1" });
|
||||
|
||||
@@ -75,7 +75,7 @@ public sealed class CompetencyCatalogImportServiceTests
|
||||
] }
|
||||
] }
|
||||
""";
|
||||
var preview = service.Analyze(json, SubjectId, "Mathematik", 7);
|
||||
var preview = service.Analyze(json, SubjectId, "Mathematik", 7, Guid.Empty);
|
||||
|
||||
service.Apply(preview, CompetencyCatalogImportMode.Merge);
|
||||
|
||||
@@ -89,7 +89,7 @@ public sealed class CompetencyCatalogImportServiceTests
|
||||
{
|
||||
var repository = RepositoryWithExistingCatalog();
|
||||
var service = new CompetencyCatalogImportService(repository);
|
||||
var preview = service.Analyze(JsonWithConflictAndNewItem(), SubjectId, "Mathematik", 7);
|
||||
var preview = service.Analyze(JsonWithConflictAndNewItem(), SubjectId, "Mathematik", 7, Guid.Empty);
|
||||
|
||||
service.Apply(preview, CompetencyCatalogImportMode.Replace);
|
||||
|
||||
@@ -103,7 +103,7 @@ public sealed class CompetencyCatalogImportServiceTests
|
||||
{
|
||||
var repository = RepositoryWithExistingCatalog();
|
||||
var service = new CompetencyCatalogImportService(repository);
|
||||
var preview = service.Analyze(JsonWithConflictAndNewItem(), SubjectId, "Mathematik", 7);
|
||||
var preview = service.Analyze(JsonWithConflictAndNewItem(), SubjectId, "Mathematik", 7, Guid.Empty);
|
||||
repository.Current[0].UpdatedAt = repository.Current[0].UpdatedAt.AddSeconds(1);
|
||||
|
||||
var exception = Assert.Throws<InvalidOperationException>(() =>
|
||||
@@ -121,7 +121,7 @@ public sealed class CompetencyCatalogImportServiceTests
|
||||
.Replace("Mathematik", "Deutsch")
|
||||
.Replace("\"gradeLevel\": 7", "\"gradeLevel\": 8");
|
||||
|
||||
var preview = service.Analyze(json, SubjectId, "Mathematik", 7);
|
||||
var preview = service.Analyze(json, SubjectId, "Mathematik", 7, Guid.Empty);
|
||||
|
||||
Assert.Equal(2, preview.Warnings.Count);
|
||||
}
|
||||
@@ -138,7 +138,7 @@ public sealed class CompetencyCatalogImportServiceTests
|
||||
""";
|
||||
|
||||
Assert.Throws<InvalidDataException>(() =>
|
||||
service.Analyze(json, SubjectId, "Mathematik", 7));
|
||||
service.Analyze(json, SubjectId, "Mathematik", 7, Guid.Empty));
|
||||
}
|
||||
|
||||
private static InMemoryCompetencyDomainRepository RepositoryWithExistingCatalog() => new(
|
||||
@@ -179,21 +179,22 @@ public sealed class CompetencyCatalogImportServiceTests
|
||||
public List<CompetencyDomain> Current { get; private set; } = initial;
|
||||
public bool ReplaceWasCalled { get; private set; }
|
||||
|
||||
public List<CompetencyDomain> GetBySubjectAndGrade(Guid subjectId, int gradeLevel) =>
|
||||
Current.Where(x => x.SubjectId == subjectId && x.GradeLevel == gradeLevel).ToList();
|
||||
public List<CompetencyDomain> GetBySubject(Guid subjectId) =>
|
||||
Current.Where(x => x.SubjectId == subjectId).ToList();
|
||||
public List<CompetencyDomain> GetBySubjectAndGrade(Guid subjectId, int gradeLevel, Guid versionId) =>
|
||||
Current.Where(x => x.SubjectId == subjectId && x.GradeLevel == gradeLevel && x.VersionId == versionId).ToList();
|
||||
public List<CompetencyDomain> GetBySubjectAndVersion(Guid subjectId, Guid versionId) =>
|
||||
Current.Where(x => x.SubjectId == subjectId && x.VersionId == versionId).ToList();
|
||||
public CompetencyDomain? GetById(Guid id) => Current.FirstOrDefault(x => x.Id == id);
|
||||
|
||||
public void Save(CompetencyDomain domain) => Current.Add(domain);
|
||||
public void Delete(Guid id) => Current.RemoveAll(x => x.Id == id);
|
||||
public void DeleteBySubjectAndGrade(Guid subjectId, int gradeLevel) =>
|
||||
Current.RemoveAll(x => x.SubjectId == subjectId && x.GradeLevel == gradeLevel);
|
||||
public void DeleteBySubjectGradeAndVersion(Guid subjectId, int gradeLevel, Guid versionId) =>
|
||||
Current.RemoveAll(x => x.SubjectId == subjectId && x.GradeLevel == gradeLevel && x.VersionId == versionId);
|
||||
|
||||
public void ReplaceForSubjectAndGrade(Guid subjectId, int gradeLevel, List<CompetencyDomain> domains)
|
||||
public void ReplaceForSubjectGradeAndVersion(Guid subjectId, int gradeLevel, Guid versionId, List<CompetencyDomain> domains)
|
||||
{
|
||||
ReplaceWasCalled = true;
|
||||
Current.RemoveAll(x => x.SubjectId == subjectId && x.GradeLevel == gradeLevel);
|
||||
Current.RemoveAll(x => x.SubjectId == subjectId && x.GradeLevel == gradeLevel && x.VersionId == versionId);
|
||||
foreach (var d in domains) { d.SubjectId = subjectId; d.GradeLevel = gradeLevel; d.VersionId = versionId; }
|
||||
Current.AddRange(domains);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3601,6 +3601,35 @@ Format dokumentiert in [Kompetenzkatalog-KI-Prompt.md](docs/Kompetenzkatalog-KI-
|
||||
der Kompetenzübersicht (Details siehe 11.4, "Kompetenzbericht"): druckt die Analyse für
|
||||
den aktuell im Tab ausgewählten Schüler.
|
||||
|
||||
### 8.4 Bildungsplan-Versionierung (Nutzer-Nachtrag)
|
||||
|
||||
Neue Bildungspläne wachsen je Fach klassenstufenweise hoch — über mehrere Schuljahre hinweg gelten
|
||||
alter und neuer Plan parallel, je nachdem, wann ein Kurs/eine Klasse eingeschult wurde.
|
||||
|
||||
- [x] **8.4.1** `CompetencyCatalogVersion` (Fach + Name + `IsDefault`) eingeführt;
|
||||
`CompetencyDomain.VersionId` und `LearningGroup.CompetencyCatalogVersionId` (nullable = "nutzt
|
||||
die Standardversion des Fachs") verknüpfen Katalog bzw. Kurs mit einer Version. Migration legt
|
||||
für jedes Fach mit bestehendem Katalog automatisch eine Standardversion an, bestehende Kurse
|
||||
bleiben unverändert nutzbar. Details siehe [Datenmodell.md](docs/Datenmodell.md).
|
||||
- [x] **8.4.2** Versionen verwalten (anlegen, als Standard setzen, löschen — blockiert bei
|
||||
Referenzen oder für die Standardversion) im Settings-Tab „Kompetenzen“, dort auch als dritte
|
||||
Auswahl neben Fach/Klassenstufe zum Filtern des bearbeiteten Katalogs.
|
||||
`CompetencyCatalogImportService` (JSON-Import/Kopieren zwischen Klassenstufen) ist
|
||||
versionsscharf: Import/Kopieren wirkt immer nur innerhalb der gerade gewählten Version.
|
||||
- [x] **8.4.3** Kursanlage/-bearbeitung: Bildungsplan-Version je Kurs auswählbar
|
||||
(`AddGroupDialogViewModel`/`AddGroupDialog.axaml`), sobald das gewählte Fach mehr als eine
|
||||
Version hat. Kompetenzen-Tab, Kompetenzauswahl in Unterrichtseinheiten und in Klausuraufgaben
|
||||
sowie die Kompetenz-Sichtbarkeit im Mitarbeit-Tab zeigen jeweils den Katalog der für den Kurs
|
||||
geltenden Version (`CompetencyCatalogVersionResolver.Resolve`).
|
||||
- [x] **8.4.4** MCP: Bildungsplan-Versionen vollständig exponiert (`get_competency_plan_versions`,
|
||||
`create_competency_plan_version`, `update_competency_plan_version`,
|
||||
`delete_competency_plan_version`), `get_competency_catalog`/`create_competency_domain` um einen
|
||||
optionalen `versionId`-Parameter erweitert (weglassen = Standardversion), sowie
|
||||
`update_group_competency_version` zum Setzen der Version eines Kurses.
|
||||
|
||||
Bewusst nicht Teil dieses Nachtrags: TODO 16.5 (Kompetenz-Referenzen in Unit/Lesson/ExamTask von
|
||||
Freitext-`Code` auf stabile `CompetencyItem.Id` umstellen) — unabhängiges, vorbestehendes Thema.
|
||||
|
||||
---
|
||||
|
||||
## 9. Dashboard
|
||||
|
||||
@@ -37,6 +37,38 @@ ein eigenes Feld wie `Student.SchoolEntryDate` zu verwenden.
|
||||
Pro Kombination aus Schüler und Lerngruppe darf es höchstens eine Zuordnung
|
||||
geben.
|
||||
|
||||
## Bildungsplan-Version (`CompetencyCatalogVersion`)
|
||||
|
||||
Bildet ab, dass für ein Fach mehrere Kompetenzkataloge parallel gültig sein können — typischerweise
|
||||
weil ein neuer Bildungsplan klassenstufenweise hochwächst (z.B. beginnend in Klasse 5, ein Jahrgang
|
||||
pro Schuljahr) und deshalb über mehrere Schuljahre hinweg sowohl der alte als auch der neue Plan
|
||||
gleichzeitig unterrichtet werden.
|
||||
|
||||
- `SubjectId`: Fach, zu dem die Version gehört
|
||||
- `Name`: frei gewählte Bezeichnung, z.B. `Bildungsplan 2016` oder `Bildungsplan 2026`
|
||||
- `IsDefault`: je Fach höchstens eine Version — Kurse ohne explizite Wahl (siehe
|
||||
`LearningGroup.CompetencyCatalogVersionId`) nutzen sie. `CompetencyCatalogVersionRepository.Save`
|
||||
nimmt beim Setzen automatisch die bisherige Standardversion desselben Fachs zurück.
|
||||
|
||||
`CompetencyDomain.VersionId` verweist auf die Version, zu der ein Kompetenzbereich gehört — derselbe
|
||||
Katalog-Slot (Fach + Klassenstufe) kann also mehrfach existieren, einmal je Version.
|
||||
`LearningGroup.CompetencyCatalogVersionId` (`Guid?`) wählt je Kurs/Klasse, welche Version gilt.
|
||||
Analog zum Verhältnis von `GroupMembership` zu `LearningGroup.SchoolYear` (siehe oben) ist dieses
|
||||
Feld bewusst nullable mit einer Sonderbedeutung: `null` heißt nicht "keine Version", sondern "die
|
||||
Standardversion des Fachs" — so brauchen bestehende Kurse und Kompetenzkataloge beim Einführen der
|
||||
Versionierung keine Migration außer dem einmaligen Anlegen je einer Standardversion pro Fach
|
||||
(`LiteDbContext.MigrateCompetencyCatalogVersions`, Schema-Version 5).
|
||||
|
||||
`CompetencyCatalogVersionRepository.Delete` blockiert (statt in den Papierkorb zu verschieben, siehe
|
||||
14.2), wenn die Version noch von einem Kompetenzbereich oder einem Kurs referenziert wird, oder wenn
|
||||
sie die Standardversion ihres Fachs ist — erst eine andere Version des Fachs muss zum Standard
|
||||
werden, damit nie ein Fach ganz ohne Standardversion dasteht.
|
||||
|
||||
Bewusst **nicht** Teil dieser Versionierung: die Verknüpfung von Kompetenzcodes zu Unterrichtseinheiten,
|
||||
Einzelstunden und Klausuraufgaben (`Unit.Competencies`, `Lesson.Competencies`, `ExamTask.CompetencyCodes`)
|
||||
bleibt Freitext-Code-basiert, nicht auf `CompetencyItem.Id` umgestellt — ein vorbestehendes, unabhängiges
|
||||
Thema (siehe TODO 16.5).
|
||||
|
||||
## Gewichtungsschema (`GradingScheme`)
|
||||
|
||||
Legt die prozentuale Gewichtung von Klausuren, Mitarbeit und sonstigen Leistungen für die
|
||||
|
||||
Reference in new issue
Block a user