From 6e57bf407d1635cb9a9181683c20bd0769dfe4d5 Mon Sep 17 00:00:00 2001 From: Baddi86 Date: Tue, 15 Sep 2026 23:30:22 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20fehlende=20Variablen=20erg=C3=A4nzen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../CreateLetterDialogViewModelTests.cs | 23 ++++++ .../Students/CreateLetterDialogViewModel.cs | 81 +++++++++++++++++-- .../Views/Students/CreateLetterDialog.axaml | 20 +++++ .../DesignerViewModel.cs | 3 + LehrerApp.TemplateDesigner/MainWindow.axaml | 4 +- .../PdfImportPipeline.cs | 7 +- 6 files changed, 129 insertions(+), 9 deletions(-) diff --git a/LehrerApp.Desktop.Tests/CreateLetterDialogViewModelTests.cs b/LehrerApp.Desktop.Tests/CreateLetterDialogViewModelTests.cs index 137a937..adc4028 100644 --- a/LehrerApp.Desktop.Tests/CreateLetterDialogViewModelTests.cs +++ b/LehrerApp.Desktop.Tests/CreateLetterDialogViewModelTests.cs @@ -93,6 +93,29 @@ public sealed class CreateLetterDialogViewModelTests : IDisposable Assert.True(vm.CanGenerate); } + [Fact] + public void EigenerPlatzhalter_KannImDialogEingegebenWerden() + { + var store = StoreWithTemplate(new PlaceholderDefinition("Anrede", PlaceholderType.Text, true), + new PlaceholderDefinition("Datum", PlaceholderType.Date, true), + new PlaceholderDefinition("Brieftext", PlaceholderType.Multiline, true), + new PlaceholderDefinition("Betreff", PlaceholderType.Text, true)); + var vm = Build(StudentWithContact("Sehr geehrte Frau Muster,"), store); + vm.LetterText = "Dies ist der Inhalt."; + + var betreff = Assert.Single(vm.CustomPlaceholders); + Assert.Equal("Betreff", betreff.Name); + Assert.False(vm.CanGenerate); + Assert.Contains(vm.Issues, i => i.Message.Contains("Betreff", StringComparison.OrdinalIgnoreCase)); + + betreff.TextValue = "Wichtiger Termin"; + var output = Path.Combine(_directory, "MitBetreff.pdf"); + + Assert.True(vm.CanGenerate); + Assert.True(vm.Generate(output)); + Assert.Equal("%PDF", System.Text.Encoding.ASCII.GetString(File.ReadAllBytes(output), 0, 4)); + } + private CreateLetterDialogViewModel Build(Student student, TemplateStore store) => new(student, store, new QuestTemplateRenderer(), new FakeMemberships([]), new FakeGroups([])); diff --git a/LehrerApp.Desktop/ViewModels/Students/CreateLetterDialogViewModel.cs b/LehrerApp.Desktop/ViewModels/Students/CreateLetterDialogViewModel.cs index 2898c9f..c485bcb 100644 --- a/LehrerApp.Desktop/ViewModels/Students/CreateLetterDialogViewModel.cs +++ b/LehrerApp.Desktop/ViewModels/Students/CreateLetterDialogViewModel.cs @@ -35,7 +35,9 @@ public partial class CreateLetterDialogViewModel : ObservableObject public ObservableCollection Contacts { get; } = []; public ObservableCollection Groups { get; } = []; public ObservableCollection Issues { get; } = []; + public ObservableCollection CustomPlaceholders { get; } = []; public bool HasIssues => Issues.Count > 0; + public bool HasCustomPlaceholders => CustomPlaceholders.Count > 0; public bool HasNoTemplates => Templates.Count == 0; public bool HasNoContacts => Contacts.Count == 0; public bool UsesAttendanceAdvancedContent => UsesAttendanceCalendar || UsesAbsenceDayList; @@ -68,6 +70,7 @@ public partial class CreateLetterDialogViewModel : ObservableObject OnPropertyChanged(nameof(UsesAttendanceAdvancedContent)); AttendanceCalendarConfigured = false; ResetAttendanceCalendarOptions(); + RebuildCustomPlaceholders(value); RefreshValidation(); } partial void OnSelectedContactChanged(LetterContactChoice? value) => RefreshValidation(); @@ -117,11 +120,49 @@ public partial class CreateLetterDialogViewModel : ObservableObject OnPropertyChanged(nameof(HasIssues)); } - private IReadOnlyDictionary BuildValues() => LetterPlaceholderBuilder.BuildStandardValues( - _student, SelectedContact?.Model, SelectedGroup?.Model, - DateOnly.FromDateTime((LetterDate ?? DateTimeOffset.Now).LocalDateTime), LetterText, TeacherName, - UsesAttendanceCalendar ? _attendanceCalendarFactory?.Invoke(_attendanceCalendarOptions) : null, - UsesAbsenceDayList ? _absenceDayListFactory?.Invoke(_attendanceCalendarOptions) : null); + private IReadOnlyDictionary BuildValues() + { + var values = LetterPlaceholderBuilder.BuildStandardValues( + _student, SelectedContact?.Model, SelectedGroup?.Model, + DateOnly.FromDateTime((LetterDate ?? DateTimeOffset.Now).LocalDateTime), LetterText, TeacherName, + UsesAttendanceCalendar ? _attendanceCalendarFactory?.Invoke(_attendanceCalendarOptions) : null, + UsesAbsenceDayList ? _absenceDayListFactory?.Invoke(_attendanceCalendarOptions) : null); + foreach (var custom in CustomPlaceholders) values[custom.Name] = custom.ToPlaceholderValue(); + return values; + } + + private void RebuildCustomPlaceholders(LetterTemplateChoice? choice) + { + foreach (var existing in CustomPlaceholders) existing.PropertyChanged -= OnCustomPlaceholderChanged; + CustomPlaceholders.Clear(); + if (choice is not null) + { + try + { + var loaded = _templates.Load(choice.Model); + foreach (var placeholder in loaded.Manifest.Placeholders.Where(p => !p.IsConstant + && !StandardPlaceholderNames.Contains(p.Name) && p.Type is PlaceholderType.Text + or PlaceholderType.Multiline or PlaceholderType.Date or PlaceholderType.Number)) + { + var input = new LetterPlaceholderInput(placeholder.Name, placeholder.Type); + input.PropertyChanged += OnCustomPlaceholderChanged; + CustomPlaceholders.Add(input); + } + } + catch (Exception ex) when (ex is IOException or InvalidDataException or TemplateValidationException) { } + } + OnPropertyChanged(nameof(HasCustomPlaceholders)); + } + + private void OnCustomPlaceholderChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e) => RefreshValidation(); + + private static readonly HashSet StandardPlaceholderNames = new(StringComparer.Ordinal) + { + "Datum", "CurrentDate", "Empfaenger", "Anrede", "Brieftext", "LehrerName", + "Student.FirstName", "Student.LastName", "Contact.Name", "Contact.Address", "Contact.Street", + "Contact.PostalCode", "Contact.City", "Letter.Salutation", "Group.Name", "SchoolYear", + StudentAttendanceCalendarDrawingBuilder.PlaceholderName, StudentAbsenceDayListDrawingBuilder.PlaceholderName, + }; public AttendanceCalendarOptions GetAttendanceCalendarOptions() => _attendanceCalendarOptions; @@ -187,3 +228,33 @@ public sealed class LetterContactChoice(Contact model) { public Contact Model { 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"; } + +public sealed partial class LetterPlaceholderInput : ObservableObject +{ + public string Name { get; } + public PlaceholderType Type { get; } + public string Label => Type switch + { + PlaceholderType.Date => $"{Name} (Datum)", + PlaceholderType.Number => $"{Name} (Zahl)", + _ => Name, + }; + public bool IsTextType => Type == PlaceholderType.Text; + public bool IsMultilineType => Type == PlaceholderType.Multiline; + public bool IsDateType => Type == PlaceholderType.Date; + public bool IsNumberType => Type == PlaceholderType.Number; + + [ObservableProperty] private string _textValue = ""; + [ObservableProperty] private DateTimeOffset? _dateValue; + [ObservableProperty] private decimal? _numberValue; + + public LetterPlaceholderInput(string name, PlaceholderType type) { Name = name; Type = type; } + + public PlaceholderValue ToPlaceholderValue() => Type switch + { + PlaceholderType.Multiline => new MultilineValue(TextValue), + PlaceholderType.Date => new DateValue(DateValue.HasValue ? DateOnly.FromDateTime(DateValue.Value.LocalDateTime) : default), + PlaceholderType.Number => new NumberValue(NumberValue ?? 0), + _ => new TextValue(TextValue), + }; +} diff --git a/LehrerApp.Desktop/Views/Students/CreateLetterDialog.axaml b/LehrerApp.Desktop/Views/Students/CreateLetterDialog.axaml index 6762981..90e1a66 100644 --- a/LehrerApp.Desktop/Views/Students/CreateLetterDialog.axaml +++ b/LehrerApp.Desktop/Views/Students/CreateLetterDialog.axaml @@ -65,6 +65,26 @@ + + + + + + + + + + + + + + + + + diff --git a/LehrerApp.TemplateDesigner/DesignerViewModel.cs b/LehrerApp.TemplateDesigner/DesignerViewModel.cs index 68015c9..f10d462 100644 --- a/LehrerApp.TemplateDesigner/DesignerViewModel.cs +++ b/LehrerApp.TemplateDesigner/DesignerViewModel.cs @@ -49,6 +49,7 @@ public partial class DesignerViewModel : ObservableObject [ObservableProperty] private string _selectedPageTemplate = "first"; [ObservableProperty] private DesignerPreviewPage? _selectedPreviewPage; [ObservableProperty] private SpecialContentItem? _selectedSpecialContent; + [ObservableProperty] private bool _isLegacyContinuationSupported = true; public IReadOnlyList Units { get; } = ["mm", "cm", "pt", "in"]; public IReadOnlyList PlaceholderTypes { get; } = Enum.GetValues(); @@ -586,6 +587,8 @@ public partial class DesignerViewModel : ObservableObject try { var page = new LayoutParser().Parse(value); + IsLegacyContinuationSupported = !page.UsesPageTemplates; + if (page.UsesPageTemplates) UseContinuationLayout = false; OverlayPageWidth = page.Width; OverlayPageHeight = page.Height; OverlayPageUnit = page.Unit; var names = page.PageTemplates.Select(x => x.Name).ToList(); if (names.Count == 0) names.Add("legacy"); diff --git a/LehrerApp.TemplateDesigner/MainWindow.axaml b/LehrerApp.TemplateDesigner/MainWindow.axaml index 9b92347..628a136 100644 --- a/LehrerApp.TemplateDesigner/MainWindow.axaml +++ b/LehrerApp.TemplateDesigner/MainWindow.axaml @@ -271,12 +271,12 @@