diff --git a/LehrerApp.Desktop.Tests/CreateLetterDialogViewModelTests.cs b/LehrerApp.Desktop.Tests/CreateLetterDialogViewModelTests.cs index 243e269..1f1fe95 100644 --- a/LehrerApp.Desktop.Tests/CreateLetterDialogViewModelTests.cs +++ b/LehrerApp.Desktop.Tests/CreateLetterDialogViewModelTests.cs @@ -62,7 +62,7 @@ public sealed class CreateLetterDialogViewModelTests : IDisposable StudentAttendanceCalendarDrawingBuilder.PlaceholderName, PlaceholderType.Drawing, true)); var drawing = StudentAttendanceCalendarDrawingBuilder.Build("Lena Beispiel", new DateOnly(2026, 9, 1), [], []); var vm = new CreateLetterDialogViewModel(StudentWithContact("Sehr geehrte Frau Muster,"), store, - new QuestTemplateRenderer(), new FakeMemberships([]), new FakeGroups([]), _ => drawing); + new QuestTemplateRenderer(), new FakeMemberships([]), new FakeGroups([]), (_, _, _) => drawing); var output = Path.Combine(_directory, "Kalender.pdf"); Assert.True(vm.UsesAttendanceCalendar); @@ -76,6 +76,67 @@ public sealed class CreateLetterDialogViewModelTests : IDisposable Assert.Equal("%PDF", System.Text.Encoding.ASCII.GetString(File.ReadAllBytes(output), 0, 4)); } + // Regression: Der DrawingValue-Builder hat früher immer eine feste Breite (170) angenommen, + // unabhängig davon, wie breit die DRAWBOX in der jeweiligen Vorlage tatsächlich deklariert war - + // bei breiteren (oder in "pt" statt "mm" deklarierten) Boxen blieb dadurch ein Großteil der + // eigentlich verfügbaren Fläche ungenutzt leer (sichtbar in echten Elternbriefen als "Luft" + // rechts/unterhalb des Kalenders). Der Dialog muss die real deklarierte Breite ermitteln und an + // den Builder weiterreichen. + [Fact] + public void AdvancedContent_Anwesenheitskalender_NutztDieImLayoutDeklarierteBoxbreite() + { + var manifest = new TemplateManifest + { + Id = $"brief-{Guid.NewGuid():N}", Name = "Elternbrief", + Placeholders = [new(StudentAttendanceCalendarDrawingBuilder.PlaceholderName, PlaceholderType.Drawing, true)], + }; + var source = Path.Combine(_directory, $"{Guid.NewGuid():N}.lavorlage"); + TemplatePackage.Create(source, manifest, + $"PAGE 210 297 mm\nDRAWBOX 20 20 250 80 ${StudentAttendanceCalendarDrawingBuilder.PlaceholderName}", + new Dictionary()); + var store = new TemplateStore(Path.Combine(_directory, $"store-{Guid.NewGuid():N}")); + store.Import(source); + float? receivedWidth = null; float? receivedMillimeterScale = null; + + var vm = new CreateLetterDialogViewModel(StudentWithContact("Sehr geehrte Frau Muster,"), store, + new QuestTemplateRenderer(), new FakeMemberships([]), new FakeGroups([]), + (_, width, millimeterScale) => { receivedWidth = width; receivedMillimeterScale = millimeterScale; return new DrawingValue([], 0); }); + + Assert.True(vm.UsesAttendanceCalendar); + Assert.Equal(250, receivedWidth); + Assert.Equal(1, receivedMillimeterScale); + } + + // Regression: Eine reale, aus einem Word-Dokument übernommene Vorlage deklariert die Seite in + // "pt" statt "mm" (PAGE ... pt). Ohne Umrechnung würde das mm-entworfene Kalenderraster dort nur + // rund ein Drittel der vorgesehenen physischen Größe erreichen (1 "mm-Einheit" würde als 1pt statt + // als ~2.83pt gerendert). + [Fact] + public void AdvancedContent_Anwesenheitskalender_RechnetBoxbreiteBeiPunktBasierterVorlageUm() + { + var manifest = new TemplateManifest + { + Id = $"brief-{Guid.NewGuid():N}", Name = "Elternbrief", + Placeholders = [new(StudentAttendanceCalendarDrawingBuilder.PlaceholderName, PlaceholderType.Drawing, true)], + }; + var source = Path.Combine(_directory, $"{Guid.NewGuid():N}.lavorlage"); + TemplatePackage.Create(source, manifest, + $"PAGE 595.32 841.92 pt\nDRAWBOX 71 350 487 177 ${StudentAttendanceCalendarDrawingBuilder.PlaceholderName}", + new Dictionary()); + var store = new TemplateStore(Path.Combine(_directory, $"store-{Guid.NewGuid():N}")); + store.Import(source); + float? receivedWidth = null; float? receivedMillimeterScale = null; + + var vm = new CreateLetterDialogViewModel(StudentWithContact("Sehr geehrte Frau Muster,"), store, + new QuestTemplateRenderer(), new FakeMemberships([]), new FakeGroups([]), + (_, width, millimeterScale) => { receivedWidth = width; receivedMillimeterScale = millimeterScale; return new DrawingValue([], 0); }); + + Assert.True(vm.UsesAttendanceCalendar); + Assert.Equal(487, receivedWidth); + Assert.NotNull(receivedMillimeterScale); + Assert.Equal(2.8346f, receivedMillimeterScale!.Value, 0.001f); + } + [Fact] public async Task AttendanceKalenderKonfigurieren_LoestGezieltenDatenAbrufFuerDenZeitraumAus() { @@ -84,7 +145,7 @@ public sealed class CreateLetterDialogViewModelTests : IDisposable AttendanceCalendarOptions? requested = null; var vm = new CreateLetterDialogViewModel(StudentWithContact("Sehr geehrte Frau Muster,"), store, new QuestTemplateRenderer(), new FakeMemberships([]), new FakeGroups([]), - _ => new DrawingValue([], 0), attendanceDataRefresher: (options, _) => + (_, _, _) => new DrawingValue([], 0), attendanceDataRefresher: (options, _) => { requested = options; return Task.CompletedTask; }); await vm.SetAttendanceCalendarOptionsAsync(new AttendanceCalendarOptions( @@ -104,7 +165,7 @@ public sealed class CreateLetterDialogViewModelTests : IDisposable StudentAttendanceCalendarDrawingBuilder.PlaceholderName, PlaceholderType.Drawing, true)); var vm = new CreateLetterDialogViewModel(StudentWithContact("Sehr geehrte Frau Muster,"), store, new QuestTemplateRenderer(), new FakeMemberships([]), new FakeGroups([]), - _ => new DrawingValue([], 0), + (_, _, _) => new DrawingValue([], 0), attendanceDataRefresher: (_, _) => throw new WebUntisIntegrationException("Keine Verbindung.")); await vm.SetAttendanceCalendarOptionsAsync(new AttendanceCalendarOptions( @@ -124,7 +185,7 @@ public sealed class CreateLetterDialogViewModelTests : IDisposable new AttendanceCalendarOptions(new DateOnly(2026, 9, 1), 1), []); var vm = new CreateLetterDialogViewModel(StudentWithContact("Sehr geehrte Frau Muster,"), store, new QuestTemplateRenderer(), new FakeMemberships([]), new FakeGroups([]), - attendanceCalendarFactory: null, absenceDayListFactory: _ => drawing); + attendanceCalendarFactory: null, absenceDayListFactory: (_, _, _) => drawing); Assert.False(vm.UsesAttendanceCalendar); Assert.True(vm.UsesAbsenceDayList); diff --git a/LehrerApp.Desktop/Services/LetterDialogs.cs b/LehrerApp.Desktop/Services/LetterDialogs.cs index 825673f..a1e0aac 100644 --- a/LehrerApp.Desktop/Services/LetterDialogs.cs +++ b/LehrerApp.Desktop/Services/LetterDialogs.cs @@ -20,9 +20,10 @@ public static class LetterDialogs App.Services.GetRequiredService(), App.Services.GetRequiredService(), App.Services.GetRequiredService(), - options => App.Services.GetRequiredService().Build(student, options), - options => App.Services.GetRequiredService() - .BuildAbsenceDayList(student, options), + (options, contentWidth, millimeterScale) => App.Services.GetRequiredService() + .Build(student, options, contentWidth, millimeterScale), + (options, contentWidth, millimeterScale) => App.Services.GetRequiredService() + .BuildAbsenceDayList(student, options, contentWidth, millimeterScale), RefreshAttendanceDataAsync); var dialog = new CreateLetterDialog { DataContext = vm }; var path = await dialog.ShowDialog(owner); diff --git a/LehrerApp.Desktop/Services/LetterPlaceholderBuilder.cs b/LehrerApp.Desktop/Services/LetterPlaceholderBuilder.cs index aea0bb0..8596a24 100644 --- a/LehrerApp.Desktop/Services/LetterPlaceholderBuilder.cs +++ b/LehrerApp.Desktop/Services/LetterPlaceholderBuilder.cs @@ -50,4 +50,31 @@ public static class LetterPlaceholderBuilder DrawingValue x => x.ContentHeight <= 0 || x.Commands.Count == 0, _ => false, }; + + /// Tatsächliche DRAWBOX/FLOWDRAWBOX-Breite 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). + public readonly record struct DeclaredDrawingBox(float Width, float MillimeterScale); + + 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 width = 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?)draw.Width, + FlowDrawBoxElement flow when flow.Placeholder == placeholderName => (float?)flow.Width, + _ => null, + }) + .FirstOrDefault(w => w is not null); + if (width is null) return null; + var millimeterScale = UnitConverter.Points(1, "mm") / UnitConverter.Points(1, layout.Unit); + return new DeclaredDrawingBox(width.Value, millimeterScale); + } } diff --git a/LehrerApp.Desktop/Services/Mcp/Tools/LetterTemplateTools.cs b/LehrerApp.Desktop/Services/Mcp/Tools/LetterTemplateTools.cs index 099fdae..429bfb2 100644 --- a/LehrerApp.Desktop/Services/Mcp/Tools/LetterTemplateTools.cs +++ b/LehrerApp.Desktop/Services/Mcp/Tools/LetterTemplateTools.cs @@ -74,9 +74,15 @@ public class LetterTemplateTools( var group = groupId is { } gid ? groups.GetById(gid) : null; var date = letterDate ?? DateOnly.FromDateTime(DateTime.Today); + var calendarBox = LetterPlaceholderBuilder.FindDeclaredDrawingBox(loaded, + StudentAttendanceCalendarDrawingBuilder.PlaceholderName) ?? new(170, 1); + var absenceDayListBox = LetterPlaceholderBuilder.FindDeclaredDrawingBox(loaded, + StudentAbsenceDayListDrawingBuilder.PlaceholderName) ?? new(170, 1); var values = LetterPlaceholderBuilder.BuildStandardValues(student, contact, group, date, letterText, - teacherName, attendanceCalendars?.Build(student, date), - attendanceCalendars?.BuildAbsenceDayList(student, new AttendanceCalendarOptions(date, 1))); + teacherName, attendanceCalendars?.Build(student, new AttendanceCalendarOptions(date, 1), + calendarBox.Width, calendarBox.MillimeterScale), + attendanceCalendars?.BuildAbsenceDayList(student, new AttendanceCalendarOptions(date, 1), + absenceDayListBox.Width, absenceDayListBox.MillimeterScale)); foreach (var (name, raw) in extraValues ?? []) { var definition = loaded.Manifest.Placeholders.FirstOrDefault(p => p.Name == name); diff --git a/LehrerApp.Desktop/Services/StudentAttendanceCalendarDrawingBuilder.cs b/LehrerApp.Desktop/Services/StudentAttendanceCalendarDrawingBuilder.cs index a2dc478..4552c83 100644 --- a/LehrerApp.Desktop/Services/StudentAttendanceCalendarDrawingBuilder.cs +++ b/LehrerApp.Desktop/Services/StudentAttendanceCalendarDrawingBuilder.cs @@ -28,42 +28,48 @@ public static class StudentAttendanceCalendarDrawingBuilder public static DrawingValue Build(string studentName, AttendanceCalendarOptions options, IReadOnlyList absences, - IReadOnlyList registerEntries) + IReadOnlyList registerEntries, float contentWidth = 170, + float millimeterScale = 1) { - const float width = 170; var scale = options.Size switch { AttendanceCalendarSize.Small => .7f, AttendanceCalendarSize.Large => 1f, _ => .85f, }; - // Die Breite richtet sich nach der DRAWBOX-Deklaration im Layout-Skript (fest, unabhängig - // von der Größenwahl) - nur Schrift/Zellenhöhe skalieren mit "Größe". Würde die Breite mit - // skalieren, würde "Groß" (scale=1) exakt die Skript-Box ausfüllen, "Klein"/"Normal" aber - // nur einen Teil davon - und eine größere Skalierung als 1 liefe über die Box hinaus und - // würde am rechten Rand abgeschnitten (SVG overflow="hidden"). - var contentWidth = width; - var cellHeight = 10 * scale; - // War 10 - bei 3 Monaten nebeneinander größer als eine einzelne Tageskachel und zog damit + // contentWidth/millimeterScale kommen aus der tatsächlichen DRAWBOX/FLOWDRAWBOX-Deklaration + // der jeweiligen Vorlage (siehe LetterPlaceholderBuilder.FindDeclaredDrawingBox). Das Raster + // ist in Millimetern entworfen; millimeterScale rechnet das in die Koordinaten-Einheit der + // Vorlage um (1, wenn die Vorlage schon "mm" nutzt; ≈2.83 bei "pt") - ohne das würde das + // Raster bei einer in "pt" deklarierten Vorlage nur rund ein Drittel der vorgesehenen Größe + // erreichen. Schriftgrößen (FontSize) sind davon unabhängig immer echte Punktgrößen. + // contentWidth ist unabhängig von der Größenwahl fest - nur Schrift/Zellenhöhe skalieren mit + // "Größe". Würde die Breite mit skalieren, würde "Groß" (scale=1) exakt die Skript-Box + // ausfüllen, "Klein"/"Normal" aber nur einen Teil davon - und eine größere Skalierung als 1 + // liefe über die Box hinaus und würde am rechten Rand abgeschnitten (SVG overflow="hidden"). + // Die Defaults (170/1) greifen nur, wenn die Vorlage nicht ermittelt werden kann (z.B. + // Vorschau ohne Kontext). + var cellHeight = 10 * scale * millimeterScale; + // War 10mm - bei 3 Monaten nebeneinander größer als eine einzelne Tageskachel und zog damit // spürbar Platz von den Kacheln ab, ohne selbst als Inhalt wahrgenommen zu werden. - var monthGapX = 6f; + var monthGapX = 6f * millimeterScale; var first = options.NormalizedStartMonth; var monthCount = options.NormalizedMonthCount; var monthWidth = (contentWidth - monthGapX * (monthCount - 1)) / monthCount; var cellWidth = monthWidth / 7; - // Kachel-Zwischenraum (Trennung zu Nachbarzellen) deutlich knapper als vorher (war "scale", - // also bis zu 1mm auf jeder Seite - bei einer ~7mm breiten Zelle ein gutes Viertel reiner - // Leerraum). Die Kachel selbst füllt dadurch ihren Rasterplatz sichtbar besser aus. - var cellInset = scale * .4f; + // Kachel-Zwischenraum (Trennung zu Nachbarzellen) deutlich knapper als vorher (war "scale" + // mm, also bis zu 1mm auf jeder Seite - bei einer ~7mm breiten Zelle ein gutes Viertel + // reiner Leerraum). Die Kachel selbst füllt dadurch ihren Rasterplatz sichtbar besser aus. + var cellInset = scale * .4f * millimeterScale; var cellCornerRadius = Math.Min(cellWidth, cellHeight) * .2f; var commands = new List(); var y = 0f; - commands.Add(new DrawStringEx(0, y, 14 * scale, contentWidth, "Anwesenheit", + commands.Add(new DrawStringEx(0, y, 14 * scale * millimeterScale, contentWidth, "Anwesenheit", DrawingTextAlignment.AlignLeft, 11 * scale, Color: "#1F2937", Bold: true)); - y += 14 * scale; - commands.Add(new DrawStringEx(0, y, 10 * scale, contentWidth, studentName, + y += 14 * scale * millimeterScale; + commands.Add(new DrawStringEx(0, y, 10 * scale * millimeterScale, contentWidth, studentName, DrawingTextAlignment.AlignLeft, 8 * scale, Color: "#6B7280")); - y += 10 * scale + 2 * scale; + y += 10 * scale * millimeterScale + 2 * scale * millimeterScale; var weekdays = new[] { "Mo", "Di", "Mi", "Do", "Fr", "Sa", "So" }; var gridStartY = y; var maxWeeks = 0; @@ -77,14 +83,15 @@ public static class StudentAttendanceCalendarDrawingBuilder maxWeeks = Math.Max(maxWeeks, weeks); var xOffset = monthIndex * (monthWidth + monthGapX); var monthY = gridStartY; - commands.Add(new DrawStringEx(xOffset, monthY, 11 * scale, monthWidth, + commands.Add(new DrawStringEx(xOffset, monthY, 11 * scale * millimeterScale, monthWidth, current.ToString("MMMM yyyy", CultureInfo.GetCultureInfo("de-DE")), DrawingTextAlignment.AlignLeft, 9 * scale, Color: "#374151", Bold: true)); - monthY += 11 * scale; + monthY += 11 * scale * millimeterScale; for (var column = 0; column < 7; column++) - commands.Add(new DrawStringEx(xOffset + column * cellWidth, monthY, 9 * scale, cellWidth, - weekdays[column], DrawingTextAlignment.AlignCenter, 7 * scale, Color: "#6B7280", Bold: true)); - monthY += 9 * scale; + commands.Add(new DrawStringEx(xOffset + column * cellWidth, monthY, 9 * scale * millimeterScale, + cellWidth, weekdays[column], DrawingTextAlignment.AlignCenter, 7 * scale, Color: "#6B7280", + Bold: true)); + monthY += 9 * scale * millimeterScale; foreach (var day in days) { @@ -96,17 +103,18 @@ public static class StudentAttendanceCalendarDrawingBuilder commands.Add(new DrawRoundedRectangle(x + cellInset, cellY, cellWidth - 2 * cellInset, cellHeight - cellInset, cellCornerRadius, "#D1D5DB", .35f, day.HasSignal ? day.SignalColorHex : "#FFFFFF")); - commands.Add(new DrawStringEx(x, cellY + scale, cellHeight - 2 * scale, cellWidth, + commands.Add(new DrawStringEx(x, cellY + scale * millimeterScale, + cellHeight - 2 * scale * millimeterScale, cellWidth, day.HasSignal ? day.SignalCode : day.DayNumber, DrawingTextAlignment.AlignCenter, 7 * scale, Color: day.HasSignal ? "#FFFFFF" : "#374151", Bold: day.HasSignal)); } } - y = gridStartY + 11 * scale + 9 * scale + maxWeeks * cellHeight + monthGapX; + y = gridStartY + 11 * scale * millimeterScale + 9 * scale * millimeterScale + maxWeeks * cellHeight + monthGapX; - commands.Add(new DrawStringEx(0, y, 8 * scale, contentWidth, + commands.Add(new DrawStringEx(0, y, 8 * scale * millimeterScale, contentWidth, "U unentschuldigt · A abwesend · V verspätet · E entschuldigt · ! Klassenbuch", DrawingTextAlignment.AlignLeft, 6.5f * scale, Color: "#6B7280")); - return new DrawingValue(commands, y + 9 * scale); + return new DrawingValue(commands, y + 9 * scale * millimeterScale); } } @@ -116,19 +124,19 @@ public static class StudentAbsenceDayListDrawingBuilder public const string PlaceholderName = "Student.AbsenceDays"; public static DrawingValue Build(string studentName, AttendanceCalendarOptions options, - IReadOnlyList absences) + IReadOnlyList absences, float contentWidth = 170, + float millimeterScale = 1) { - const float width = 170; var scale = options.Size switch { AttendanceCalendarSize.Small => .75f, AttendanceCalendarSize.Large => 1f, _ => .88f, }; - // Breite bleibt an die DRAWBOX-Deklaration im Layout-Skript gebunden (siehe - // StudentAttendanceCalendarDrawingBuilder) - nur Zeilenhöhe/Schrift skalieren mit "Größe". - var contentWidth = width; - var rowHeight = 12 * scale; + // contentWidth/millimeterScale: siehe StudentAttendanceCalendarDrawingBuilder - contentWidth + // bleibt an die tatsächliche DRAWBOX/FLOWDRAWBOX-Deklaration im Layout-Skript gebunden, + // millimeterScale rechnet das mm-entworfene Raster in die Koordinaten-Einheit der Vorlage um. + var rowHeight = 12 * scale * millimeterScale; var start = options.NormalizedStartMonth; var end = start.AddMonths(options.NormalizedMonthCount).AddDays(-1); var rows = absences @@ -138,33 +146,33 @@ public static class StudentAbsenceDayListDrawingBuilder .ToList(); var commands = new List(); var y = 0f; - commands.Add(new DrawStringEx(0, y, 14 * scale, contentWidth, "Fehltage", + commands.Add(new DrawStringEx(0, y, 14 * scale * millimeterScale, contentWidth, "Fehltage", DrawingTextAlignment.AlignLeft, 11 * scale, Color: "#1F2937", Bold: true)); - y += 14 * scale; - commands.Add(new DrawStringEx(0, y, 10 * scale, contentWidth, studentName, + y += 14 * scale * millimeterScale; + commands.Add(new DrawStringEx(0, y, 10 * scale * millimeterScale, contentWidth, studentName, DrawingTextAlignment.AlignLeft, 8 * scale, Color: "#6B7280")); - y += 10 * scale + 2 * scale; + y += 10 * scale * millimeterScale + 2 * scale * millimeterScale; // Spaltenbreiten so gewählt, dass auch die Datenzeilen (nicht nur die kurzen Kopfzeilen- // Labels) hineinpassen - "Datum" als Kopfzeile ist kürzer als "dd.MM.yyyy" und wurde bei // 31 zu schmal bemessen, wodurch das Datum am rechten Rand abgeschnitten wurde. - const float dateColumnX = 2, dateColumnWidth = 38; - const float extentColumnX = 44, extentColumnWidth = 66; - const float statusColumnX = 114, statusColumnWidth = 54; + var dateColumnX = 2 * millimeterScale; var dateColumnWidth = 38 * millimeterScale; + var extentColumnX = 44 * millimeterScale; var extentColumnWidth = 66 * millimeterScale; + var statusColumnX = 114 * millimeterScale; var statusColumnWidth = 54 * millimeterScale; commands.Add(new DrawRectangle(0, y, contentWidth, rowHeight, "#CBD5E1", .4f, "#F3F4F6")); - commands.Add(new DrawStringEx(dateColumnX, y + scale, rowHeight - scale, dateColumnWidth, "Datum", - DrawingTextAlignment.AlignLeft, 7.5f * scale, Color: "#374151", Bold: true)); - commands.Add(new DrawStringEx(extentColumnX, y + scale, rowHeight - scale, extentColumnWidth, "Umfang", - DrawingTextAlignment.AlignLeft, 7.5f * scale, Color: "#374151", Bold: true)); - commands.Add(new DrawStringEx(statusColumnX, y + scale, rowHeight - scale, statusColumnWidth, "Status", - DrawingTextAlignment.AlignLeft, 7.5f * scale, Color: "#374151", Bold: true)); + commands.Add(new DrawStringEx(dateColumnX, y + scale * millimeterScale, rowHeight - scale * millimeterScale, + dateColumnWidth, "Datum", DrawingTextAlignment.AlignLeft, 7.5f * scale, Color: "#374151", Bold: true)); + commands.Add(new DrawStringEx(extentColumnX, y + scale * millimeterScale, rowHeight - scale * millimeterScale, + extentColumnWidth, "Umfang", DrawingTextAlignment.AlignLeft, 7.5f * scale, Color: "#374151", Bold: true)); + commands.Add(new DrawStringEx(statusColumnX, y + scale * millimeterScale, rowHeight - scale * millimeterScale, + statusColumnWidth, "Status", DrawingTextAlignment.AlignLeft, 7.5f * scale, Color: "#374151", Bold: true)); y += rowHeight; if (rows.Count == 0) { - commands.Add(new DrawStringEx(2, y + 2 * scale, rowHeight, contentWidth - 4, - "Keine Fehltage im gewählten Zeitraum", DrawingTextAlignment.AlignLeft, 8 * scale, - Color: "#6B7280", Italic: true)); - y += rowHeight + 3 * scale; + commands.Add(new DrawStringEx(2 * millimeterScale, y + 2 * scale * millimeterScale, rowHeight, + contentWidth - 4 * millimeterScale, "Keine Fehltage im gewählten Zeitraum", + DrawingTextAlignment.AlignLeft, 8 * scale, Color: "#6B7280", Italic: true)); + y += rowHeight + 3 * scale * millimeterScale; } else { @@ -184,12 +192,15 @@ public static class StudentAbsenceDayListDrawingBuilder : row.FriendlyStatusLabel; var statusColor = row.IsUnexcused ? "#C62828" : "#2E7D32"; commands.Add(new DrawRectangle(0, y, contentWidth, rowHeight, "#E5E7EB", .3f, fill)); - commands.Add(new DrawStringEx(dateColumnX, y + scale, rowHeight - scale, dateColumnWidth, + commands.Add(new DrawStringEx(dateColumnX, y + scale * millimeterScale, + rowHeight - scale * millimeterScale, dateColumnWidth, row.Date.ToString("dd.MM.yyyy"), DrawingTextAlignment.AlignLeft, 7.5f * scale, Color: "#374151")); - commands.Add(new DrawStringEx(extentColumnX, y + scale, rowHeight - scale, extentColumnWidth, + commands.Add(new DrawStringEx(extentColumnX, y + scale * millimeterScale, + rowHeight - scale * millimeterScale, extentColumnWidth, extent, DrawingTextAlignment.AlignLeft, 7.5f * scale, Color: "#374151")); - commands.Add(new DrawStringEx(statusColumnX, y + scale, rowHeight - scale, statusColumnWidth, + commands.Add(new DrawStringEx(statusColumnX, y + scale * millimeterScale, + rowHeight - scale * millimeterScale, statusColumnWidth, status, DrawingTextAlignment.AlignLeft, 7.5f * scale, Color: statusColor, Bold: row.IsUnexcused)); y += rowHeight; @@ -210,7 +221,8 @@ public sealed class StudentAttendanceCalendarService( public DrawingValue Build(Student student, DateOnly month) => Build(student, new AttendanceCalendarOptions(month, 1)); - public DrawingValue Build(Student student, AttendanceCalendarOptions options) + public DrawingValue Build(Student student, AttendanceCalendarOptions options, float contentWidth = 170, + float millimeterScale = 1) { var className = settings.HomeroomClassName; if (string.IsNullOrWhiteSpace(className)) return new DrawingValue([], 0); @@ -233,10 +245,11 @@ public sealed class StudentAttendanceCalendarService( e.TeacherUsername, e.CategoryName, e.CategoryGroup, e.Text)) .ToList(); return StudentAttendanceCalendarDrawingBuilder.Build(rosterName, options, - ClassAbsenceDaySummaryRow.GroupByStudentAndDay(absences), register); + ClassAbsenceDaySummaryRow.GroupByStudentAndDay(absences), register, contentWidth, millimeterScale); } - public DrawingValue BuildAbsenceDayList(Student student, AttendanceCalendarOptions options) + public DrawingValue BuildAbsenceDayList(Student student, AttendanceCalendarOptions options, float contentWidth = 170, + float millimeterScale = 1) { var className = settings.HomeroomClassName; if (string.IsNullOrWhiteSpace(className)) return new DrawingValue([], 0); @@ -254,6 +267,6 @@ public sealed class StudentAttendanceCalendarService( e.AbsentPeriods, e.AbsentMinutes, e.TeacherUsernames, e.Subject, e.AbsenceReason, e.Note, e.EntryId, e.HandledOn, e.Counts, e.ExcuseNote, e.PeriodNumber, e.Status, e.CountsAsFullDay)); return StudentAbsenceDayListDrawingBuilder.Build(rosterName, options, - ClassAbsenceDaySummaryRow.GroupByStudentAndDay(absences)); + ClassAbsenceDaySummaryRow.GroupByStudentAndDay(absences), contentWidth, millimeterScale); } } diff --git a/LehrerApp.Desktop/ViewModels/Students/CreateLetterDialogViewModel.cs b/LehrerApp.Desktop/ViewModels/Students/CreateLetterDialogViewModel.cs index 2b928e1..873334b 100644 --- a/LehrerApp.Desktop/ViewModels/Students/CreateLetterDialogViewModel.cs +++ b/LehrerApp.Desktop/ViewModels/Students/CreateLetterDialogViewModel.cs @@ -12,8 +12,8 @@ public partial class CreateLetterDialogViewModel : ObservableObject private readonly Student _student; private readonly TemplateStore _templates; private readonly ITemplateRenderer _renderer; - private readonly Func? _attendanceCalendarFactory; - private readonly Func? _absenceDayListFactory; + private readonly Func? _attendanceCalendarFactory; + private readonly Func? _absenceDayListFactory; private readonly Func? _attendanceDataRefresher; private AttendanceCalendarOptions _attendanceCalendarOptions = new( new DateOnly(DateTime.Today.Year, DateTime.Today.Month, 1), 1); @@ -52,8 +52,8 @@ public partial class CreateLetterDialogViewModel : ObservableObject public CreateLetterDialogViewModel(Student student, TemplateStore templates, ITemplateRenderer renderer, IGroupMembershipRepository memberships, IGroupRepository groups, - Func? attendanceCalendarFactory = null, - Func? absenceDayListFactory = null, + Func? attendanceCalendarFactory = null, + Func? absenceDayListFactory = null, Func? attendanceDataRefresher = null) { _student = student; _templates = templates; _renderer = renderer; @@ -137,16 +137,32 @@ public partial class CreateLetterDialogViewModel : ObservableObject private IReadOnlyDictionary BuildValues() { + var calendarBox = DeclaredDrawingBox(StudentAttendanceCalendarDrawingBuilder.PlaceholderName); + var absenceDayListBox = DeclaredDrawingBox(StudentAbsenceDayListDrawingBuilder.PlaceholderName); 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); + UsesAttendanceCalendar ? _attendanceCalendarFactory?.Invoke(_attendanceCalendarOptions, + calendarBox.Width, calendarBox.MillimeterScale) : null, + UsesAbsenceDayList ? _absenceDayListFactory?.Invoke(_attendanceCalendarOptions, + absenceDayListBox.Width, absenceDayListBox.MillimeterScale) : null); values["Anrede"] = new TextValue(Anrede); values["Letter.Salutation"] = new TextValue(Anrede); foreach (var custom in CustomPlaceholders) values[custom.Name] = custom.ToPlaceholderValue(); return values; } + /// Tatsächliche DRAWBOX/FLOWDRAWBOX-Größe der aktuell gewählten Vorlage für diesen + /// Platzhalter, damit der Kalender/die Fehltagesliste die real verfügbare Fläche ausfüllen statt + /// eine feste Breite zu raten (siehe LetterPlaceholderBuilder.FindDeclaredDrawingBox). Fällt auf + /// 170mm zurück, wenn keine Vorlage gewählt ist oder die Box nicht gefunden wird. + private LetterPlaceholderBuilder.DeclaredDrawingBox DeclaredDrawingBox(string placeholderName) + { + var fallback = new LetterPlaceholderBuilder.DeclaredDrawingBox(170, 1); + if (SelectedTemplate is null) return fallback; + try { return LetterPlaceholderBuilder.FindDeclaredDrawingBox(_templates.Load(SelectedTemplate.Model), placeholderName) ?? fallback; } + catch (Exception ex) when (ex is IOException or InvalidDataException or TemplateValidationException) { return fallback; } + } + private void RebuildCustomPlaceholders(LetterTemplateChoice? choice) { foreach (var existing in CustomPlaceholders) existing.PropertyChanged -= OnCustomPlaceholderChanged;