using System.ComponentModel; using System.Text; using LehrerApp.Core.Interfaces; using LehrerApp.Core.Models; namespace LehrerApp.Desktop.Services.Mcp.Tools; /// MCP-Tools für die Fach- und Kompetenzkatalog-Verwaltung (Nutzer-Nachtrag zu 4.5.28ff.): /// und / vollständig /// anlegen/ändern/löschen. Die Zuordnung von Kompetenzcodes zu einzelnen Stunden lebt bewusst in /// (AddLessonCompetency/RemoveLessonCompetency), nicht hier - sie /// gehört fachlich zur Stundenverwaltung, nicht zum Katalog selbst. /// /// "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. public class CompetencyTools( ISubjectRepository subjects, ICompetencyDomainRepository domains, IMcpConfirmationService confirmation) { // ── Lesen ──────────────────────────────────────────────────────────────────────────────── [Description("Listet alle Fächer.")] public List GetSubjects() => subjects.GetAll().Select(s => new SubjectDto(s.Id, s.Name, s.ShortName)).ToList(); [Description("Listet den Kompetenzkatalog (Bereiche mit Einzelkompetenzen) eines Fachs, optional gefiltert auf eine Klassenstufe.")] public List GetCompetencyCatalog( [Description("Fach-ID, aus get_subjects.")] Guid subjectId, [Description("Optionale Klassenstufe. Weglassen: alle Klassenstufen dieses Fachs.")] int? gradeLevel = null) { var result = gradeLevel is not null ? domains.GetBySubjectAndGrade(subjectId, gradeLevel.Value) : domains.GetBySubject(subjectId); return result.Select(ToDto).ToList(); } // ── Fächer (Subject) ───────────────────────────────────────────────────────────────────── [Description("Legt ein neues Fach an. Muss der Nutzer erst bestätigen.")] public async Task CreateSubject( [Description("Name des Fachs, z.B. \"Mathematik\".")] string name, [Description("Kurzform, z.B. \"Ma\".")] string shortName = "", CancellationToken ct = default) { if (string.IsNullOrWhiteSpace(name)) return new WriteResultDto(false, null, "Fachname darf nicht leer sein."); var message = $"Neues Fach „{name.Trim()}“" + (string.IsNullOrWhiteSpace(shortName) ? "" : $" ({shortName.Trim()})") + " anlegen?"; if (!await confirmation.ConfirmAsync("Fach anlegen?", message, ct)) return new WriteResultDto(false, null, "Vom Nutzer abgelehnt oder nicht bestätigt."); var subject = new Subject { Name = name, ShortName = shortName }; try { subjects.Save(subject); } catch (Exception ex) when (ex is ArgumentException or InvalidOperationException) { return new WriteResultDto(false, null, ex.Message); } return new WriteResultDto(true, subject.Id, "Fach gespeichert."); } [Description("Ändert Name/Kurzform eines bestehenden Fachs. Nur angegebene Felder werden geändert.")] public async Task UpdateSubject( [Description("Fach-ID.")] Guid subjectId, [Description("Neuer Name. Unverändert lassen: weglassen.")] string? name = null, [Description("Neue Kurzform. Unverändert lassen: weglassen.")] string? shortName = null, CancellationToken ct = default) { var subject = subjects.GetById(subjectId); if (subject is null) return new WriteResultDto(false, null, "Unbekannte Fach-ID."); var changes = new StringBuilder(); if (name is not null && name != subject.Name) { changes.AppendLine($"Name: „{subject.Name}“ → „{name}“"); subject.Name = name; } if (shortName is not null && shortName != subject.ShortName) { changes.AppendLine($"Kurzform: „{subject.ShortName}“ → „{shortName}“"); subject.ShortName = shortName; } if (changes.Length == 0) return new WriteResultDto(true, subject.Id, "Keine Änderung nötig."); if (!await confirmation.ConfirmAsync("Fach ändern?", $"„{subject.Name}“\n{changes}".TrimEnd(), ct)) return new WriteResultDto(false, null, "Vom Nutzer abgelehnt oder nicht bestätigt."); try { subjects.Save(subject); } catch (Exception ex) when (ex is ArgumentException or InvalidOperationException) { return new WriteResultDto(false, null, ex.Message); } return new WriteResultDto(true, subject.Id, "Fach gespeichert."); } [Description("Löscht ein Fach endgültig. Schlägt fehl, wenn noch eine Lerngruppe oder ein Kompetenzkatalog dieses Fach referenziert. Kein Papierkorb - nicht rückgängig zu machen. Muss der Nutzer erst bestätigen.")] public async Task DeleteSubject( [Description("Fach-ID.")] Guid subjectId, CancellationToken ct = default) { var subject = subjects.GetById(subjectId); if (subject is null) return new WriteResultDto(false, null, "Unbekannte Fach-ID."); var message = $"Das Fach „{subject.Name}“ wird endgültig gelöscht. Das kann NICHT rückgängig gemacht werden (kein Papierkorb für Fächer)."; if (!await confirmation.ConfirmAsync("Fach endgültig löschen?", message, ct)) return new WriteResultDto(false, null, "Vom Nutzer abgelehnt oder nicht bestätigt."); try { subjects.Delete(subjectId); } catch (InvalidOperationException ex) { return new WriteResultDto(false, null, ex.Message); } return new WriteResultDto(true, subjectId, "Fach gelöscht."); } // ── 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.")] public async Task CreateCompetencyDomain( [Description("Fach-ID, aus get_subjects.")] Guid subjectId, [Description("Klassenstufe.")] int gradeLevel, [Description("Name des Bereichs.")] string name, [Description("Optionales Kürzel, z.B. \"ZO\".")] string code = "", 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?"; 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, }; domains.Save(domain); return new WriteResultDto(true, domain.Id, "Kompetenzbereich gespeichert."); } [Description("Ändert Name/Kürzel eines bestehenden Kompetenzbereichs. Nur angegebene Felder werden geändert.")] public async Task UpdateCompetencyDomain( [Description("ID des Kompetenzbereichs, aus get_competency_catalog.")] Guid domainId, [Description("Neuer Name. Unverändert lassen: weglassen.")] string? name = null, [Description("Neues Kürzel. Unverändert lassen: weglassen.")] string? code = null, CancellationToken ct = default) { var domain = domains.GetById(domainId); if (domain is null) return new WriteResultDto(false, null, "Unbekannte Bereichs-ID."); var changes = new StringBuilder(); if (name is not null && name != domain.Name) { changes.AppendLine($"Name: „{domain.Name}“ → „{name}“"); domain.Name = name; } if (code is not null && code != domain.Code) { changes.AppendLine($"Kürzel: „{domain.Code}“ → „{code}“"); domain.Code = code; } if (changes.Length == 0) return new WriteResultDto(true, domain.Id, "Keine Änderung nötig."); if (!await confirmation.ConfirmAsync("Kompetenzbereich ändern?", $"„{domain.Name}“\n{changes}".TrimEnd(), ct)) return new WriteResultDto(false, null, "Vom Nutzer abgelehnt oder nicht bestätigt."); domains.Save(domain); return new WriteResultDto(true, domain.Id, "Kompetenzbereich gespeichert."); } [Description("Löscht einen Kompetenzbereich endgültig, inklusive aller darin enthaltenen Einzelkompetenzen. Bereits vergebene Kompetenzcodes an Einheiten/Stunden/Klausuraufgaben bleiben als Freitext bestehen, gelten aber danach als \"nicht im Katalog\". Kein Papierkorb - nicht rückgängig zu machen. Muss der Nutzer erst bestätigen.")] public async Task DeleteCompetencyDomain( [Description("ID des Kompetenzbereichs.")] Guid domainId, CancellationToken ct = default) { var domain = domains.GetById(domainId); if (domain is null) return new WriteResultDto(false, null, "Unbekannte Bereichs-ID."); var message = $"Der Kompetenzbereich „{domain.Name}“ mit {domain.Items.Count} Einzelkompetenz(en) wird endgültig gelöscht. " + "Das kann NICHT rückgängig gemacht werden (kein Papierkorb für Kompetenzbereiche)."; if (!await confirmation.ConfirmAsync("Kompetenzbereich endgültig löschen?", message, ct)) return new WriteResultDto(false, null, "Vom Nutzer abgelehnt oder nicht bestätigt."); domains.Delete(domainId); return new WriteResultDto(true, domainId, "Kompetenzbereich gelöscht."); } // ── Einzelkompetenzen (CompetencyItem) ────────────────────────────────────────────────── [Description("Fügt einem Kompetenzbereich eine neue Einzelkompetenz hinzu (ans Ende). Muss der Nutzer erst bestätigen.")] public async Task AddCompetencyItem( [Description("ID des Kompetenzbereichs, aus get_competency_catalog.")] Guid domainId, [Description("Kompetenzcode, z.B. \"M.5.1\".")] string code, [Description("Beschreibung der Kompetenz.")] string description, CancellationToken ct = default) { var domain = domains.GetById(domainId); if (domain is null) return new WriteResultDto(false, null, "Unbekannte Bereichs-ID."); if (string.IsNullOrWhiteSpace(description)) return new WriteResultDto(false, null, "Beschreibung darf nicht leer sein."); var message = $"Neue Kompetenz „{code}“ zu Bereich „{domain.Name}“ hinzufügen?\n{description}"; if (!await confirmation.ConfirmAsync("Kompetenz hinzufügen?", message, ct)) return new WriteResultDto(false, null, "Vom Nutzer abgelehnt oder nicht bestätigt."); var item = new CompetencyItem { Code = code.Trim(), Description = description.Trim(), SortOrder = domain.Items.Count }; domain.Items.Add(item); domains.Save(domain); return new WriteResultDto(true, item.Id, "Kompetenz hinzugefügt."); } [Description("Ändert Code/Beschreibung einer bestehenden Einzelkompetenz. Nur angegebene Felder werden geändert.")] public async Task UpdateCompetencyItem( [Description("ID des Kompetenzbereichs.")] Guid domainId, [Description("ID der Einzelkompetenz, aus get_competency_catalog.")] Guid itemId, [Description("Neuer Kompetenzcode. Unverändert lassen: weglassen.")] string? code = null, [Description("Neue Beschreibung. Unverändert lassen: weglassen.")] string? description = null, CancellationToken ct = default) { var domain = domains.GetById(domainId); if (domain is null) return new WriteResultDto(false, null, "Unbekannte Bereichs-ID."); var item = domain.Items.FirstOrDefault(i => i.Id == itemId); if (item is null) return new WriteResultDto(false, null, "Unbekannte Kompetenz-ID in diesem Bereich."); var changes = new StringBuilder(); if (code is not null && code != item.Code) { changes.AppendLine($"Code: „{item.Code}“ → „{code}“"); item.Code = code; } if (description is not null && description != item.Description) { changes.AppendLine($"Beschreibung: „{item.Description}“ → „{description}“"); item.Description = description; } if (changes.Length == 0) return new WriteResultDto(true, item.Id, "Keine Änderung nötig."); if (!await confirmation.ConfirmAsync("Kompetenz ändern?", $"Kompetenz „{item.Code}“ in Bereich „{domain.Name}“\n{changes}".TrimEnd(), ct)) return new WriteResultDto(false, null, "Vom Nutzer abgelehnt oder nicht bestätigt."); domains.Save(domain); return new WriteResultDto(true, item.Id, "Kompetenz gespeichert."); } [Description("Entfernt eine Einzelkompetenz aus einem Kompetenzbereich. Muss der Nutzer erst bestätigen.")] public async Task RemoveCompetencyItem( [Description("ID des Kompetenzbereichs.")] Guid domainId, [Description("ID der Einzelkompetenz, aus get_competency_catalog.")] Guid itemId, CancellationToken ct = default) { var domain = domains.GetById(domainId); if (domain is null) return new WriteResultDto(false, null, "Unbekannte Bereichs-ID."); var item = domain.Items.FirstOrDefault(i => i.Id == itemId); if (item is null) return new WriteResultDto(false, null, "Unbekannte Kompetenz-ID in diesem Bereich."); var message = $"Kompetenz „{item.Code}“ ({item.Description}) aus Bereich „{domain.Name}“ entfernen?"; if (!await confirmation.ConfirmAsync("Kompetenz entfernen?", message, ct)) return new WriteResultDto(false, null, "Vom Nutzer abgelehnt oder nicht bestätigt."); domain.Items.Remove(item); domains.Save(domain); return new WriteResultDto(true, item.Id, "Kompetenz entfernt."); } private static CompetencyDomainDto ToDto(CompetencyDomain d) => new( d.Id, d.SubjectId, d.GradeLevel, 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()); }