using LehrerApp.Core.Models; using LehrerApp.Templating; namespace LehrerApp.Desktop.Services; /// /// Baut die Standard-Platzhalterwerte für Elternbrief-Vorlagen (Datum, Anrede, Kontaktadresse, /// Schülerdaten, Brieftext, ...). Ursprünglich Teil von /// , hierher extrahiert, damit /// LetterTemplateTools (MCP, siehe Planungsdokument) dieselbe Logik nutzt statt sie zu /// duplizieren — beide Aufrufer müssen exakt dieselben Platzhalternamen befüllen, sonst driften /// Dialog-generierte und KI-generierte Briefe unbemerkt auseinander. /// public static class LetterPlaceholderBuilder { public static Dictionary BuildStandardValues( Student student, Contact? contact, LearningGroup? group, DateOnly date, string letterText, string teacherName, DrawingValue? attendanceCalendar = null, DrawingValue? absenceDays = null) { var address = FormatAddress(contact); return new Dictionary(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), ["Student.Name"] = new TextValue(student.FullName), ["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 ?? ""), [StudentAttendanceCalendarDrawingBuilder.PlaceholderName] = attendanceCalendar ?? new DrawingValue([], 0), [StudentAbsenceDayListDrawingBuilder.PlaceholderName] = absenceDays ?? new DrawingValue([], 0), }; } /// Mehrzeilige Anschrift (Name/Straße/PLZ Ort) für Adressvorschau im Dialog und /// den Contact.Address-Platzhalter - eine Formatierung für beide Verwendungen. public static string FormatAddress(Contact? contact) { var cityLine = string.Join(" ", new[] { contact?.PostalCode, contact?.City }.Where(x => !string.IsNullOrWhiteSpace(x))); return string.Join(Environment.NewLine, new[] { contact?.Name, contact?.Street, cityLine }.Where(x => !string.IsNullOrWhiteSpace(x))); } public static bool IsEmpty(PlaceholderValue value) => value switch { TextValue x => string.IsNullOrWhiteSpace(x.Value), MultilineValue x => string.IsNullOrWhiteSpace(x.Value), DrawingValue x => x.ContentHeight <= 0 || x.Commands.Count == 0, _ => false, }; /// Tatsächliche DRAWBOX/FLOWDRAWBOX-Größe eines Platzhalters (in der Maßeinheit der /// Vorlage) plus dem Umrechnungsfaktor "1mm in dieser Maßeinheit". DrawingValue-Builder wie /// entwerfen ihr Raster in Millimetern; /// ohne MillimeterScale bliebe das bei einer in "pt" (statt "mm") deklarierten Vorlage viel zu /// klein (1 "mm-Einheit" würde als 1pt ≈ 0.35mm gerendert). IsFixed unterscheidet DRAWBOX (feste, /// nicht umbrechende Box - Inhalt, der die Höhe sprengt, wird von QuestTemplateRenderer /// stillschweigend am unteren Rand abgeschnitten statt umzubrechen) von FLOWDRAWBOX (fließt bei /// Bedarf automatisch auf weitere Seiten, siehe DrawingElementRenderer.Slice) - nur bei /// IsFixed=true darf/muss ein Builder seine Höhe an Height anpassen. public readonly record struct DeclaredDrawingBox(float Width, float Height, float MillimeterScale, bool IsFixed); public static DeclaredDrawingBox? FindDeclaredDrawingBox(LoadedTemplate template, string placeholderName) => FindDeclaredDrawingBox(template.Layout, placeholderName) ?? (template.ContinuationLayout is not null ? FindDeclaredDrawingBox(template.ContinuationLayout, placeholderName) : null); private static DeclaredDrawingBox? FindDeclaredDrawingBox(TemplateLayout layout, string placeholderName) { var box = layout.Elements.Concat(layout.PageTemplates.SelectMany(p => p.Elements)) .Concat(layout.ContentFlows.SelectMany(f => f.Elements)) .Select(e => e switch { DrawBoxElement draw when draw.Placeholder == placeholderName => ((float Width, float Height, bool IsFixed)?)(draw.Width, draw.Height, true), FlowDrawBoxElement flow when flow.Placeholder == placeholderName => (flow.Width, flow.Height, false), _ => null, }) .FirstOrDefault(b => b is not null); if (box is null) return null; var millimeterScale = UnitConverter.Points(1, "mm") / UnitConverter.Points(1, layout.Unit); return new DeclaredDrawingBox(box.Value.Width, box.Value.Height, millimeterScale, box.Value.IsFixed); } }