diff --git a/LehrerApp.TemplateDesigner.Tests/ProjectLifecycleTests.cs b/LehrerApp.TemplateDesigner.Tests/ProjectLifecycleTests.cs index 7f6e6e4..7b70561 100644 --- a/LehrerApp.TemplateDesigner.Tests/ProjectLifecycleTests.cs +++ b/LehrerApp.TemplateDesigner.Tests/ProjectLifecycleTests.cs @@ -80,6 +80,40 @@ public sealed class ProjectLifecycleTests Assert.True(viewModel.HasNoSelectedPlaceholder); } + [Theory] + [InlineData("Anwesenheitskalender", "Student.AttendanceCalendar", "DRAWBOX", "90")] + [InlineData("Fehltage nach Datum", "Student.AbsenceDays", "FLOWDRAWBOX", "140")] + public void Sonderinhalt_AusKatalog_BereitetPlatzhalterUndElementVor( + string displayName, string placeholderName, string elementType, string height) + { + var viewModel = new DesignerViewModel(); + viewModel.Placeholders.Clear(); + viewModel.SelectedSpecialContent = viewModel.SpecialContents.Single(x => x.DisplayName == displayName); + + viewModel.PrepareSelectedSpecialContent(); + + var placeholder = Assert.Single(viewModel.Placeholders); + Assert.Equal(placeholderName, placeholder.Name); + Assert.Equal(LehrerApp.Templating.PlaceholderType.Drawing, placeholder.Type); + Assert.Same(placeholder, viewModel.SelectedPlaceholder); + Assert.Equal(elementType, viewModel.NewElementType); + Assert.Equal("$" + placeholderName, viewModel.NewContent); + Assert.Equal("170", viewModel.NewWidth); + Assert.Equal(height, viewModel.NewHeight); + } + + [Fact] + public void Sonderinhalt_WirdNichtDoppeltDeklariert() + { + var viewModel = new DesignerViewModel(); + viewModel.SelectedSpecialContent = viewModel.SpecialContents.Single(x => + x.PlaceholderName == "Student.AttendanceCalendar"); + + viewModel.PrepareSelectedSpecialContent(); + + Assert.Single(viewModel.Placeholders, x => x.Name == "Student.AttendanceCalendar"); + } + [Fact] public void DoppeltePlatzhalternamen_WerdenVerstaendlichAbgelehnt() { diff --git a/LehrerApp.TemplateDesigner/DesignerViewModel.cs b/LehrerApp.TemplateDesigner/DesignerViewModel.cs index 5debefb..68015c9 100644 --- a/LehrerApp.TemplateDesigner/DesignerViewModel.cs +++ b/LehrerApp.TemplateDesigner/DesignerViewModel.cs @@ -48,12 +48,14 @@ public partial class DesignerViewModel : ObservableObject [ObservableProperty] private string _overlayPageUnit = "mm"; [ObservableProperty] private string _selectedPageTemplate = "first"; [ObservableProperty] private DesignerPreviewPage? _selectedPreviewPage; + [ObservableProperty] private SpecialContentItem? _selectedSpecialContent; public IReadOnlyList Units { get; } = ["mm", "cm", "pt", "in"]; public IReadOnlyList PlaceholderTypes { get; } = Enum.GetValues(); public IReadOnlyList ElementTypes { get; } = ["TEXT", "TEXTBOX", "FLOWBOX", "DRAWBOX", "FLOWDRAWBOX", "IMG", "TABLE", "CHART"]; public IReadOnlyList ElementScopes { get; } = ["Seitenvorlage (fest)", "Content-Flow"]; + public IReadOnlyList SpecialContents { get; } = SpecialContentCatalog.Items; public ObservableCollection Placeholders { get; } = [ new("Datum", PlaceholderType.Date, true, DateTime.Today.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture)), @@ -78,7 +80,11 @@ public partial class DesignerViewModel : ObservableObject public bool HasSelectedPlaceholder => SelectedPlaceholder is not null; public bool HasNoSelectedPlaceholder => SelectedPlaceholder is null; - public DesignerViewModel() => SelectedPlaceholder = Placeholders.FirstOrDefault(); + public DesignerViewModel() + { + SelectedPlaceholder = Placeholders.FirstOrDefault(); + SelectedSpecialContent = SpecialContents.FirstOrDefault(); + } partial void OnSelectedPlaceholderChanged(DesignerPlaceholder? value) { @@ -198,6 +204,32 @@ public partial class DesignerViewModel : ObservableObject return placeholder; } + /// Übernimmt einen lesbar benannten LehrerApp-Sonderinhalt in das normale + /// Elementformular und legt seine Drawing-Deklaration an, falls sie noch fehlt. + public void PrepareSelectedSpecialContent() + { + if (SelectedSpecialContent is not { } special) + throw new InvalidOperationException("Bitte zuerst einen Sonderinhalt auswählen."); + var placeholder = Placeholders.FirstOrDefault(p => p.Name == special.PlaceholderName); + if (placeholder is not null && placeholder.Type != PlaceholderType.Drawing) + throw new InvalidDataException( + $"Der vorhandene Platzhalter „{special.PlaceholderName}“ hat nicht den Typ Drawing."); + if (placeholder is null) + { + placeholder = new DesignerPlaceholder(special.PlaceholderName, PlaceholderType.Drawing, + false, special.Description); + Placeholders.Add(placeholder); + } + SelectedPlaceholder = placeholder; + NewElementType = special.RecommendedElementType; + NewContent = "$" + special.PlaceholderName; + NewWidth = special.RecommendedWidth; + NewHeight = special.RecommendedHeight; + NewAttributes = ""; + CanExport = false; + SetStatus($"„{special.DisplayName}“ vorbereitet. Position und Größe prüfen, dann ins Layout übernehmen.", false); + } + public void RemoveSelectedPlaceholder() { if (SelectedPlaceholder is not { } selected) return; @@ -703,6 +735,48 @@ public sealed record DesignerPreviewPage(int Number, Bitmap Image) public string Display => $"Dokumentseite {Number}"; } +public sealed record SpecialContentItem(string DisplayName, string PlaceholderName, string Description, + string RecommendedElementType, string RecommendedWidth, string RecommendedHeight); + +public static class SpecialContentCatalog +{ + public static IReadOnlyList Items { get; } = + [ + new("Anwesenheitskalender", "Student.AttendanceCalendar", + "Monatskalender mit farbigen Anwesenheitsmarkern; Zeitraum und Größe werden beim Erstellen des Briefs gewählt.", + "DRAWBOX", "170", "90"), + new("Fehltage nach Datum", "Student.AbsenceDays", + "Chronologische Liste mit Datum, Fehlzeit oder ganzem Fehltag sowie Entschuldigungsstatus.", + "FLOWDRAWBOX", "170", "140"), + ]; + + public static DrawingValue SampleDrawing(string placeholderName) => placeholderName switch + { + "Student.AttendanceCalendar" => new DrawingValue( + [ + new DrawString(2, 2, "Anwesenheit · Erika Beispiel", 10, "#1F2937", Bold: true), + new DrawRectangle(2, 14, 22, 12, "#D1D5DB", .4f, "#FFFFFF"), + new DrawString(9, 16, "12", 7, "#374151"), + new DrawRectangle(26, 14, 22, 12, "#C62828", .4f, "#C62828"), + new DrawString(34, 16, "U", 7, "#FFFFFF", Bold: true), + new DrawRectangle(50, 14, 22, 12, "#2E7D32", .4f, "#2E7D32"), + new DrawString(58, 16, "E", 7, "#FFFFFF", Bold: true), + ], 28), + "Student.AbsenceDays" => new DrawingValue( + [ + new DrawString(2, 2, "Fehltage · Erika Beispiel", 10, "#1F2937", Bold: true), + new DrawRectangle(2, 14, 166, 11, "#CBD5E1", .4f, "#F3F4F6"), + new DrawString(4, 15, "Datum", 7, "#374151", Bold: true), + new DrawString(42, 15, "Umfang", 7, "#374151", Bold: true), + new DrawString(116, 15, "Status", 7, "#374151", Bold: true), + new DrawString(4, 27, "03.09.2026", 7, "#374151"), + new DrawString(42, 27, "Ganzer Fehltag", 7, "#374151"), + new DrawString(116, 27, "Entschuldigt", 7, "#2E7D32"), + ], 38), + _ => DesignerPlaceholder.GenericSampleDrawing(), + }; +} + public partial class DesignerAsset(string name, int pixelWidth, int pixelHeight, long byteCount) : ObservableObject { [ObservableProperty] private int _usageCount; @@ -757,7 +831,7 @@ public partial class DesignerPlaceholder : ObservableObject PlaceholderType.Image => new ImageValue([], "image/png"), PlaceholderType.Table => ParseTable(Sample), PlaceholderType.Chart => ParseChart(Sample), - PlaceholderType.Drawing => SampleDrawing(), + PlaceholderType.Drawing => SpecialContentCatalog.SampleDrawing(Name), _ => new TextValue(Sample), }; public static string SampleFor(PlaceholderType type) => type switch @@ -772,7 +846,7 @@ public partial class DesignerPlaceholder : ObservableObject } private static ChartValue ParseChart(string value) => new([new("Werte", value.Split(';', StringSplitOptions.RemoveEmptyEntries) .Select((x, i) => { var parts = x.Split(':', 2); return new ChartPoint(parts[0], parts.Length == 2 && decimal.TryParse(parts[1], CultureInfo.InvariantCulture, out var y) ? y : i + 1); }).ToList())]); - private static DrawingValue SampleDrawing() => new DrawingValue( + internal static DrawingValue GenericSampleDrawing() => new DrawingValue( [new DrawRectangle(0, 0, 80, 24, "#2563EB", 0.8f, "#EFF6FF"), new DrawString(4, 4, "Dynamischer Inhalt", 10, "#1E3A8A", Bold: true), new MoveTo(4, 19), new LineTo(76, 19, "#93C5FD", 0.6f)], 24); diff --git a/LehrerApp.TemplateDesigner/MainWindow.axaml b/LehrerApp.TemplateDesigner/MainWindow.axaml index 1416ac7..9b92347 100644 --- a/LehrerApp.TemplateDesigner/MainWindow.axaml +++ b/LehrerApp.TemplateDesigner/MainWindow.axaml @@ -213,6 +213,21 @@ + + + + + + +