using CommunityToolkit.Mvvm.ComponentModel; using LehrerApp.Core.Interfaces; using LehrerApp.Core.Models; using LehrerApp.Core.Services; using System.Collections.ObjectModel; using System.Globalization; namespace LehrerApp.Desktop.ViewModels.Students; public partial class CreateLetterDialogViewModel : ObservableObject { private readonly Student _student; private readonly LetterTemplateService _templates; [ObservableProperty] private LetterTemplateChoice? _selectedTemplate; [ObservableProperty] private LetterContactChoice? _selectedContact; [ObservableProperty] private LetterGroupChoice? _selectedGroup; [ObservableProperty] private DateTimeOffset? _letterDate = DateTimeOffset.Now; [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}.docx"); public CreateLetterDialogViewModel(Student student, LetterTemplateService templates, IGroupMembershipRepository memberships, IGroupRepository groups) { _student = student; _templates = templates; foreach (var template in templates.GetTemplates()) Templates.Add(new LetterTemplateChoice(template)); foreach (var contact in student.Contacts.Where(c => !c.InvalidSince.HasValue).OrderBy(c => c.Name)) Contacts.Add(new LetterContactChoice(contact)); foreach (var membership in memberships.GetByStudent(student.Id)) { var group = groups.GetById(membership.GroupId); if (group is not null) Groups.Add(new LetterGroupChoice(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(); public bool Generate(string outputPath) { RefreshValidation(); if (!CanGenerate || SelectedTemplate is null) return false; GenerationError = ""; try { _templates.Generate(_templates.GetTemplatePath(SelectedTemplate.Model), outputPath, BuildValues()); return true; } catch (Exception ex) when (ex is IOException or InvalidDataException or UnauthorizedAccessException) { 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 eine DOCX-Briefvorlage 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) { var validation = _templates.Validate(SelectedTemplate.Model); foreach (var issue in validation.Issues) Issues.Add(new(issue.Message, issue.Severity is TemplateIssueSeverity.StrongWarning or TemplateIssueSeverity.Error)); var tags = validation.Tags.ToHashSet(StringComparer.Ordinal); var values = BuildValues(); foreach (var tag in tags.Where(t => values.TryGetValue(t, out var value) && string.IsNullOrWhiteSpace(value))) { var message = tag switch { "Letter.Salutation" => "Beim ausgewählten Kontakt fehlt die Briefanrede.", "Contact.Address" or "Contact.Street" or "Contact.PostalCode" or "Contact.City" => $"Beim ausgewählten Kontakt fehlt der Wert für „{tag}“.", "Group.Name" or "SchoolYear" => "Die Vorlage verwendet Gruppendaten; bitte eine Lerngruppe auswählen.", _ => $"Für das Vorlagenfeld „{tag}“ ist kein Wert vorhanden.", }; Issues.Add(new(message, true)); } } CanGenerate = SelectedTemplate is not null && SelectedContact is not null && LetterDate is not null && Issues.Count == 0; OnPropertyChanged(nameof(HasIssues)); } private IReadOnlyDictionary BuildValues() { var contact = SelectedContact?.Model; var group = SelectedGroup?.Model; var cityLine = string.Join(" ", new[] { contact?.PostalCode, contact?.City } .Where(v => !string.IsNullOrWhiteSpace(v))); var address = string.Join(Environment.NewLine, new[] { contact?.Street, cityLine } .Where(v => !string.IsNullOrWhiteSpace(v))); var date = LetterDate is null ? null : DateOnly.FromDateTime(LetterDate.Value.LocalDateTime) .ToString("dd.MM.yyyy", CultureInfo.GetCultureInfo("de-DE")); return new Dictionary { ["Student.FirstName"] = _student.FirstName, ["Student.LastName"] = _student.LastName, ["Contact.Name"] = contact?.Name, ["Contact.Address"] = address, ["Contact.Street"] = contact?.Street, ["Contact.PostalCode"] = contact?.PostalCode, ["Contact.City"] = contact?.City, ["Letter.Salutation"] = contact?.LetterSalutation, ["Group.Name"] = group?.Name, ["SchoolYear"] = group?.SchoolYear, ["CurrentDate"] = date, }; } private static string SanitizeFileName(string value) { foreach (var character in Path.GetInvalidFileNameChars()) value = value.Replace(character, '_'); return value; } } public sealed class LetterTemplateChoice { public LetterTemplateInfo Model { get; } public string Name => Model.Name; public LetterTemplateChoice(LetterTemplateInfo model) => Model = model; } public sealed class LetterContactChoice { public Contact Model { get; } public string Display => string.IsNullOrWhiteSpace(Model.Relation) ? Model.Name : $"{Model.Name} · {Model.Relation}"; public LetterContactChoice(Contact model) => Model = model; } public sealed class LetterGroupChoice { public LearningGroup Model { get; } public string Display => $"{Model.Name} · {Model.SchoolYear}"; public LetterGroupChoice(LearningGroup model) => Model = model; } 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"; }