Schließt die MCP-Server-Spec ab. "Worksheets" aus der Spec entsprechen im tatsächlichen Datenmodell den .lavorlage-Elternbrief-Vorlagen (LehrerApp.Templating) - es gibt kein separates Arbeitsblatt-Konzept mit Fach/Klassenstufe-Metadaten. Neue Tools list_letter_templates (Read) und render_letter (Read, liefert Base64-PDF, kein DB-Schreibzugriff). upload_worksheet/update_worksheet bewusst nicht umgesetzt: das Seitenlayout ist eine eigene positionsbasierte DSL mit eigenem visuellen Editor (LehrerApp.TemplateDesigner) - ein LLM müsste sie blind erzeugen, mit hohem Risiko für kaputte Layouts. Platzhalter-Logik aus CreateLetterDialogViewModel nach LetterPlaceholderBuilder extrahiert, damit Dialog und MCP-Tool nicht auseinanderdriften. Neuer McpClientRegistrationService trägt den Bridge-Pfad in Claude Desktops claude_desktop_config.json ein (Button in den Einstellungen, nie automatisch), ohne bestehende Fremdeinträge zu verlieren und ohne eine nicht lesbare Konfigurationsdatei zu überschreiben. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
108 lines
6.8 KiB
C#
108 lines
6.8 KiB
C#
using CommunityToolkit.Mvvm.ComponentModel;
|
|
using LehrerApp.Core.Interfaces;
|
|
using LehrerApp.Core.Models;
|
|
using LehrerApp.Desktop.Services;
|
|
using LehrerApp.Templating;
|
|
using System.Collections.ObjectModel;
|
|
|
|
namespace LehrerApp.Desktop.ViewModels.Students;
|
|
|
|
public partial class CreateLetterDialogViewModel : ObservableObject
|
|
{
|
|
private readonly Student _student;
|
|
private readonly TemplateStore _templates;
|
|
private readonly ITemplateRenderer _renderer;
|
|
|
|
[ObservableProperty] private LetterTemplateChoice? _selectedTemplate;
|
|
[ObservableProperty] private LetterContactChoice? _selectedContact;
|
|
[ObservableProperty] private LetterGroupChoice? _selectedGroup;
|
|
[ObservableProperty] private DateTimeOffset? _letterDate = DateTimeOffset.Now;
|
|
[ObservableProperty] private string _letterText = "";
|
|
[ObservableProperty] private string _teacherName = "";
|
|
[ObservableProperty] private string _generationError = "";
|
|
[ObservableProperty] private bool _canGenerate;
|
|
|
|
public string StudentName => _student.FullName;
|
|
public ObservableCollection<LetterTemplateChoice> Templates { get; } = [];
|
|
public ObservableCollection<LetterContactChoice> Contacts { get; } = [];
|
|
public ObservableCollection<LetterGroupChoice> Groups { get; } = [];
|
|
public ObservableCollection<LetterGenerationIssue> Issues { get; } = [];
|
|
public bool HasIssues => Issues.Count > 0;
|
|
public bool HasNoTemplates => Templates.Count == 0;
|
|
public bool HasNoContacts => Contacts.Count == 0;
|
|
public string SuggestedFileName => SanitizeFileName(
|
|
$"{SelectedTemplate?.Name ?? "Elternbrief"}_{_student.LastName}_{_student.FirstName}.pdf");
|
|
|
|
public CreateLetterDialogViewModel(Student student, TemplateStore templates, ITemplateRenderer renderer,
|
|
IGroupMembershipRepository memberships, IGroupRepository groups)
|
|
{
|
|
_student = student; _templates = templates; _renderer = renderer;
|
|
foreach (var template in templates.GetTemplates()) Templates.Add(new(template));
|
|
foreach (var contact in student.Contacts.Where(c => !c.InvalidSince.HasValue).OrderBy(c => c.Name)) Contacts.Add(new(contact));
|
|
foreach (var membership in memberships.GetByStudent(student.Id))
|
|
if (groups.GetById(membership.GroupId) is { } group) Groups.Add(new(group));
|
|
SelectedTemplate = Templates.FirstOrDefault(); SelectedContact = Contacts.FirstOrDefault(); SelectedGroup = Groups.FirstOrDefault();
|
|
RefreshValidation();
|
|
}
|
|
|
|
partial void OnSelectedTemplateChanged(LetterTemplateChoice? value) { OnPropertyChanged(nameof(SuggestedFileName)); RefreshValidation(); }
|
|
partial void OnSelectedContactChanged(LetterContactChoice? value) => RefreshValidation();
|
|
partial void OnSelectedGroupChanged(LetterGroupChoice? value) => RefreshValidation();
|
|
partial void OnLetterDateChanged(DateTimeOffset? value) => RefreshValidation();
|
|
partial void OnLetterTextChanged(string value) => RefreshValidation();
|
|
partial void OnTeacherNameChanged(string value) => RefreshValidation();
|
|
|
|
public bool Generate(string outputPath)
|
|
{
|
|
RefreshValidation(); if (!CanGenerate || SelectedTemplate is null) return false;
|
|
try
|
|
{
|
|
var pdf = _renderer.RenderToPdf(_templates.Load(SelectedTemplate.Model), new LetterDataProvider(BuildValues()));
|
|
var directory = Path.GetDirectoryName(outputPath); if (!string.IsNullOrEmpty(directory)) Directory.CreateDirectory(directory);
|
|
File.WriteAllBytes(outputPath, pdf); GenerationError = ""; return true;
|
|
}
|
|
catch (Exception ex) when (ex is IOException or InvalidDataException or UnauthorizedAccessException or TemplateValidationException)
|
|
{ GenerationError = $"Der Brief konnte nicht erzeugt werden: {ex.Message}"; return false; }
|
|
}
|
|
|
|
private void RefreshValidation()
|
|
{
|
|
Issues.Clear(); GenerationError = "";
|
|
if (SelectedTemplate is null) Issues.Add(new("Bitte zuerst in den Einstellungen ein .lavorlage-Paket importieren.", true));
|
|
if (SelectedContact is null) Issues.Add(new("Für den Schüler ist kein aktueller Kontakt ausgewählt.", true));
|
|
if (LetterDate is null) Issues.Add(new("Bitte ein Briefdatum auswählen.", true));
|
|
if (SelectedTemplate is not null)
|
|
{
|
|
try
|
|
{
|
|
var loaded = _templates.Load(SelectedTemplate.Model); var values = BuildValues();
|
|
var validation = new TemplateLoader().Validate(loaded, values.ToDictionary(x => x.Key, x => x.Value.Type));
|
|
foreach (var issue in validation.Issues) Issues.Add(new(issue.Message, issue.Severity == ValidationSeverity.Error));
|
|
foreach (var required in loaded.Manifest.Placeholders.Where(x => !x.IsConstant && x.Required
|
|
&& values.TryGetValue(x.Name, out var value) && IsEmpty(value)))
|
|
Issues.Add(new($"Für das Pflichtfeld „{required.Name}“ ist kein Wert vorhanden.", true));
|
|
}
|
|
catch (Exception ex) when (ex is IOException or InvalidDataException or TemplateValidationException)
|
|
{ Issues.Add(new($"Vorlage ist ungültig: {ex.Message}", true)); }
|
|
}
|
|
CanGenerate = SelectedTemplate is not null && SelectedContact is not null && LetterDate is not null && Issues.Count == 0;
|
|
OnPropertyChanged(nameof(HasIssues));
|
|
}
|
|
|
|
private IReadOnlyDictionary<string, PlaceholderValue> BuildValues() => LetterPlaceholderBuilder.BuildStandardValues(
|
|
_student, SelectedContact?.Model, SelectedGroup?.Model,
|
|
DateOnly.FromDateTime((LetterDate ?? DateTimeOffset.Now).LocalDateTime), LetterText, TeacherName);
|
|
|
|
private static bool IsEmpty(PlaceholderValue value) => LetterPlaceholderBuilder.IsEmpty(value);
|
|
private static string SanitizeFileName(string value)
|
|
{ foreach (var character in Path.GetInvalidFileNameChars()) value = value.Replace(character, '_'); return value; }
|
|
}
|
|
|
|
internal sealed class LetterDataProvider(IReadOnlyDictionary<string, PlaceholderValue> values) : ITemplateDataProvider
|
|
{ public IReadOnlyDictionary<string, PlaceholderValue> GetValues() => values; }
|
|
public sealed class LetterTemplateChoice(InstalledTemplate model) { public InstalledTemplate Model { get; } = model; public string Name => Model.Name; }
|
|
public sealed class LetterContactChoice(Contact model) { public Contact Model { get; } = model; public string Display => string.IsNullOrWhiteSpace(Model.Relation) ? Model.Name : $"{Model.Name} · {Model.Relation}"; }
|
|
public sealed class LetterGroupChoice(LearningGroup model) { public LearningGroup Model { get; } = model; public string Display => $"{Model.Name} · {Model.SchoolYear}"; }
|
|
public sealed class LetterGenerationIssue(string message, bool isStrong)
|
|
{ public string Icon { get; } = isStrong ? "⚠" : "ⓘ"; public string Message { get; } = message; public string Color { get; } = isStrong ? "#D97706" : "#6B7280"; }
|