Files
LehrerApp/LehrerApp.Desktop/ViewModels/Students/CreateLetterDialogViewModel.cs
T
admin 2f2761caf8
CI / build-and-test (push) Canceled after 0s
QuestPDF-Vorlagensystem und Designer hinzufügen
2026-08-30 15:37:22 +02:00

122 lines
8.1 KiB
C#

using CommunityToolkit.Mvvm.ComponentModel;
using LehrerApp.Core.Interfaces;
using LehrerApp.Core.Models;
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.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()
{
var contact = SelectedContact?.Model; var group = SelectedGroup?.Model;
var cityLine = string.Join(" ", new[] { contact?.PostalCode, contact?.City }.Where(x => !string.IsNullOrWhiteSpace(x)));
var address = string.Join(Environment.NewLine, new[] { contact?.Name, contact?.Street, cityLine }.Where(x => !string.IsNullOrWhiteSpace(x)));
var date = DateOnly.FromDateTime((LetterDate ?? DateTimeOffset.Now).LocalDateTime);
return new Dictionary<string, PlaceholderValue>(StringComparer.Ordinal)
{
["Datum"] = new DateValue(date), ["CurrentDate"] = new DateValue(date),
["Empfaenger"] = new TextValue(contact?.Name ?? ""), ["Anrede"] = new TextValue(contact?.LetterSalutation ?? ""),
["Brieftext"] = new MultilineValue(LetterText), ["LehrerName"] = new TextValue(TeacherName),
["Student.FirstName"] = new TextValue(_student.FirstName), ["Student.LastName"] = new TextValue(_student.LastName),
["Contact.Name"] = new TextValue(contact?.Name ?? ""), ["Contact.Address"] = new MultilineValue(address),
["Contact.Street"] = new TextValue(contact?.Street ?? ""), ["Contact.PostalCode"] = new TextValue(contact?.PostalCode ?? ""),
["Contact.City"] = new TextValue(contact?.City ?? ""), ["Letter.Salutation"] = new TextValue(contact?.LetterSalutation ?? ""),
["Group.Name"] = new TextValue(group?.Name ?? ""), ["SchoolYear"] = new TextValue(group?.SchoolYear ?? ""),
};
}
private static bool IsEmpty(PlaceholderValue value) => value switch
{ TextValue x => string.IsNullOrWhiteSpace(x.Value), MultilineValue x => string.IsNullOrWhiteSpace(x.Value), _ => false };
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"; }