feat: lokaler MCP-Server, Phase 2 (Write-Tools + Bestätigungsdialog + Lesson-Plans)
Neue Write-Tools create_time_entry, create_grade_entry und update_student_group_assignment schreiben nie direkt: jeder Aufruf zeigt zuerst einen menschenlesbaren Bestätigungsdialog (bestehender ConfirmDialog, über Dispatcher.UIThread aus dem Pipe-Session-Thread angezeigt) und schreibt erst nach Bestätigung, mit 2-Minuten-Timeout gegen eine hängende Session. Zusätzliches Read-Tool get_lesson_plans. create_note bewusst nicht umgesetzt (kollidiert mit dem bestehenden Dokumentations-Ausschluss aus Phase 1), create_lesson_plan/ update_lesson_plan wegen der Modellkomplexität von Lesson zurückgestellt (siehe TODO.md 4.5.26). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,69 @@
|
||||
using System.ComponentModel;
|
||||
using System.Text;
|
||||
using LehrerApp.Core.Interfaces;
|
||||
using LehrerApp.Core.Models;
|
||||
|
||||
namespace LehrerApp.Desktop.Services.Mcp.Tools;
|
||||
|
||||
/// <summary>MCP-Write-Tool "update_student_group_assignment" (Phase 2, siehe Planungsdokument).
|
||||
/// Legt eine <see cref="GroupMembership"/> an, falls noch keine für Schüler+Gruppe existiert, sonst
|
||||
/// werden nur die übergebenen (nicht-null) Felder überschrieben.</summary>
|
||||
public class GroupMembershipTools(
|
||||
IGroupMembershipRepository memberships, IStudentRepository students, IGroupRepository groups,
|
||||
IMcpConfirmationService confirmation)
|
||||
{
|
||||
[Description("Legt eine Gruppenmitgliedschaft eines Schülers an oder ändert Niveau/Zeitraum einer bestehenden. Muss der Nutzer erst in einem Dialog in LehrerApp bestätigen.")]
|
||||
public async Task<WriteResultDto> UpdateStudentGroupAssignment(
|
||||
[Description("Schüler-ID.")] Guid studentId,
|
||||
[Description("Lerngruppen-ID.")] Guid groupId,
|
||||
[Description("Niveau: E, G oder Foerder. Unverändert lassen: weglassen.")] Niveau? niveau = null,
|
||||
[Description("Zeitraum: FullYear, H1Only, H2Only oder Custom. Unverändert lassen: weglassen.")] MembershipPeriod? period = null,
|
||||
[Description("Beitrittsdatum bei Custom-Zeitraum, Format YYYY-MM-DD.")] DateOnly? joinedAt = null,
|
||||
[Description("Austrittsdatum bei Custom-Zeitraum, Format YYYY-MM-DD.")] DateOnly? leftAt = null,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
var student = students.GetById(studentId);
|
||||
var group = groups.GetById(groupId);
|
||||
if (student is null || group is null)
|
||||
return new WriteResultDto(false, null, "Unbekannte Schüler- oder Gruppen-ID.");
|
||||
|
||||
var existing = memberships.GetByStudentAndGroup(studentId, groupId);
|
||||
var target = existing is null
|
||||
? new GroupMembership { StudentId = studentId, GroupId = groupId }
|
||||
: Clone(existing);
|
||||
|
||||
var changes = new StringBuilder();
|
||||
if (niveau is not null && niveau != target.Niveau) { changes.AppendLine($"Niveau: {NiveauName(target.Niveau)} → {NiveauName(niveau)}"); target.Niveau = niveau; }
|
||||
if (period is not null && period != target.Period) { changes.AppendLine($"Zeitraum: {target.Period} → {period}"); target.Period = period.Value; }
|
||||
if (joinedAt is not null && joinedAt != target.JoinedAt) { changes.AppendLine($"Beitritt: {target.JoinedAt:dd.MM.yyyy} → {joinedAt:dd.MM.yyyy}"); target.JoinedAt = joinedAt; }
|
||||
if (leftAt is not null && leftAt != target.LeftAt) { changes.AppendLine($"Austritt: {target.LeftAt:dd.MM.yyyy} → {leftAt:dd.MM.yyyy}"); target.LeftAt = leftAt; }
|
||||
|
||||
if (existing is not null && changes.Length == 0)
|
||||
return new WriteResultDto(true, existing.Id, "Keine Änderung nötig, Mitgliedschaft besteht bereits unverändert.");
|
||||
|
||||
var title = existing is null ? "Gruppenmitgliedschaft anlegen?" : "Gruppenmitgliedschaft ändern?";
|
||||
var message = $"{student.FullName} — {group.Name}" +
|
||||
(existing is null ? "\nNeue Mitgliedschaft anlegen." : "") +
|
||||
(changes.Length > 0 ? "\n" + changes.ToString().TrimEnd() : "");
|
||||
|
||||
if (!await confirmation.ConfirmAsync(title, message, ct))
|
||||
return new WriteResultDto(false, null, "Vom Nutzer abgelehnt oder nicht bestätigt.");
|
||||
|
||||
memberships.Save(target);
|
||||
return new WriteResultDto(true, target.Id, "Gruppenmitgliedschaft gespeichert.");
|
||||
}
|
||||
|
||||
private static GroupMembership Clone(GroupMembership m) => new()
|
||||
{
|
||||
Id = m.Id, StudentId = m.StudentId, GroupId = m.GroupId, AddedOn = m.AddedOn,
|
||||
Period = m.Period, JoinedAt = m.JoinedAt, LeftAt = m.LeftAt, Niveau = m.Niveau,
|
||||
};
|
||||
|
||||
private static string NiveauName(Niveau? n) => n switch
|
||||
{
|
||||
Core.Models.Niveau.E => "E-Niveau",
|
||||
Core.Models.Niveau.G => "G-Niveau",
|
||||
Core.Models.Niveau.Foerder => "Förderniveau",
|
||||
_ => "–",
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user