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 Templates { get; } = []; public ObservableCollection Contacts { get; } = []; public ObservableCollection Groups { get; } = []; public ObservableCollection 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 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 values) : ITemplateDataProvider { public IReadOnlyDictionary 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"; }