From 03a5e0dd9c63fc5c0fe2f16c5243dfc562ac6069 Mon Sep 17 00:00:00 2001 From: Baddi86 Date: Wed, 16 Sep 2026 00:58:09 +0200 Subject: [PATCH] fix: Fehlzeitenkalender in Templates, Datenbezug und Layout --- .../UntisNameMatchingTests.cs | 14 ++++++++ ...StudentAttendanceCalendarDrawingBuilder.cs | 36 +++++++++++-------- 2 files changed, 36 insertions(+), 14 deletions(-) diff --git a/LehrerApp.Desktop.Tests/UntisNameMatchingTests.cs b/LehrerApp.Desktop.Tests/UntisNameMatchingTests.cs index 37dc141..8ed6059 100644 --- a/LehrerApp.Desktop.Tests/UntisNameMatchingTests.cs +++ b/LehrerApp.Desktop.Tests/UntisNameMatchingTests.cs @@ -18,4 +18,18 @@ public sealed class UntisNameMatchingTests [InlineData("Ben Schmidt", "")] public void NamesMatch_LehntUnterschiedlicheOderFehlendeNamenAb(string? a, string b) => Assert.False(UntisNameMatching.NamesMatch(a, b)); + + // Regression: Student.FullName liefert "Nachname, Vorname" (mit Komma) für Anzeigezwecke. + // Wird dieser String direkt an NamesMatch übergeben, bleibt das Komma am Wort kleben + // ("gerste," != "gerste") und der Abgleich gegen WebUntis-Namen (immer ohne Komma) schlägt + // fehl - genau das ließ den Anwesenheitskalender/die Fehlzeitenliste im Elternbrief leer + // bleiben, obwohl echte Fehlzeiten vorlagen. Aufrufer müssen deshalb "Vorname Nachname" ohne + // Komma bilden (siehe StudentAttendanceCalendarDrawingBuilder.StudentAttendanceCalendarService + // und ClassTeacherOverviewViewModel.cs:1173), statt Student.FullName direkt zu verwenden. + [Fact] + public void NamesMatch_KommaGetrennterAnzeigename_PasstNichtOhneUmformung() + { + Assert.False(UntisNameMatching.NamesMatch("Gerste, Amelia", "Gerste Amelia")); + Assert.True(UntisNameMatching.NamesMatch("Amelia Gerste", "Gerste Amelia")); + } } diff --git a/LehrerApp.Desktop/Services/StudentAttendanceCalendarDrawingBuilder.cs b/LehrerApp.Desktop/Services/StudentAttendanceCalendarDrawingBuilder.cs index 298900f..575d983 100644 --- a/LehrerApp.Desktop/Services/StudentAttendanceCalendarDrawingBuilder.cs +++ b/LehrerApp.Desktop/Services/StudentAttendanceCalendarDrawingBuilder.cs @@ -38,11 +38,12 @@ public static class StudentAttendanceCalendarDrawingBuilder _ => .85f, }; var contentWidth = width * scale; - var cellWidth = contentWidth / 7; var cellHeight = 10 * scale; - var monthGap = 6 * scale; + var monthGapX = 6 * scale; var first = options.NormalizedStartMonth; var monthCount = options.NormalizedMonthCount; + var monthWidth = (contentWidth - monthGapX * (monthCount - 1)) / monthCount; + var cellWidth = monthWidth / 7; var commands = new List(); var y = 0f; commands.Add(new DrawStringEx(0, y, 14 * scale, contentWidth, "Anwesenheit", @@ -52,6 +53,8 @@ public static class StudentAttendanceCalendarDrawingBuilder DrawingTextAlignment.AlignLeft, 8 * scale, Color: "#6B7280")); y += 10 * scale + 2 * scale; var weekdays = new[] { "Mo", "Di", "Mi", "Do", "Fr", "Sa", "So" }; + var gridStartY = y; + var maxWeeks = 0; for (var monthIndex = 0; monthIndex < monthCount; monthIndex++) { var current = first.AddMonths(monthIndex); @@ -59,30 +62,33 @@ public static class StudentAttendanceCalendarDrawingBuilder DateOnly.FromDateTime(DateTime.Today), studentName); var offset = ((int)current.DayOfWeek + 6) % 7; var weeks = (int)Math.Ceiling((offset + days.Count) / 7d); - commands.Add(new DrawStringEx(0, y, 11 * scale, contentWidth, + maxWeeks = Math.Max(maxWeeks, weeks); + var xOffset = monthIndex * (monthWidth + monthGapX); + var monthY = gridStartY; + commands.Add(new DrawStringEx(xOffset, monthY, 11 * scale, monthWidth, current.ToString("MMMM yyyy", CultureInfo.GetCultureInfo("de-DE")), DrawingTextAlignment.AlignLeft, 9 * scale, Color: "#374151", Bold: true)); - y += 11 * scale; + monthY += 11 * scale; for (var column = 0; column < 7; column++) - commands.Add(new DrawStringEx(column * cellWidth, y, 9 * scale, cellWidth, weekdays[column], - DrawingTextAlignment.AlignCenter, 7 * scale, Color: "#6B7280", Bold: true)); - y += 9 * scale; + commands.Add(new DrawStringEx(xOffset + column * cellWidth, monthY, 9 * scale, cellWidth, + weekdays[column], DrawingTextAlignment.AlignCenter, 7 * scale, Color: "#6B7280", Bold: true)); + monthY += 9 * scale; foreach (var day in days) { var index = offset + day.Date.Day - 1; var column = index % 7; var row = index / 7; - var x = column * cellWidth; - var cellY = y + row * cellHeight; + var x = xOffset + column * cellWidth; + var cellY = monthY + row * cellHeight; commands.Add(new DrawRectangle(x + scale, cellY, cellWidth - 2 * scale, cellHeight - scale, "#D1D5DB", .35f, day.HasSignal ? day.SignalColorHex : "#FFFFFF")); commands.Add(new DrawStringEx(x, cellY + scale, cellHeight - 2 * scale, cellWidth, day.HasSignal ? day.SignalCode : day.DayNumber, DrawingTextAlignment.AlignCenter, 7 * scale, Color: day.HasSignal ? "#FFFFFF" : "#374151", Bold: day.HasSignal)); } - y += weeks * cellHeight + monthGap; } + y = gridStartY + 11 * scale + 9 * scale + maxWeeks * cellHeight + monthGapX; commands.Add(new DrawStringEx(0, y, 8 * scale, contentWidth, "U unentschuldigt · A abwesend · V verspätet · E entschuldigt · ! Klassenbuch", @@ -190,9 +196,10 @@ public sealed class StudentAttendanceCalendarService( var first = options.NormalizedStartMonth; var last = first.AddMonths(options.NormalizedMonthCount).AddDays(-1); + var matchName = $"{student.FirstName} {student.LastName}"; var rosterName = rosterCache.GetByClass(className) - .FirstOrDefault(r => UntisNameMatching.NamesMatch(r.DisplayName, student.FullName))?.DisplayName - ?? student.FullName; + .FirstOrDefault(r => UntisNameMatching.NamesMatch(r.DisplayName, matchName))?.DisplayName + ?? matchName; var start = first.Year * 10000 + first.Month * 100 + first.Day; var end = last.Year * 10000 + last.Month * 100 + last.Day; var absences = absenceCache.GetByClassAndRange(className, start, end) @@ -215,9 +222,10 @@ public sealed class StudentAttendanceCalendarService( var first = options.NormalizedStartMonth; var last = first.AddMonths(options.NormalizedMonthCount).AddDays(-1); + var matchName = $"{student.FirstName} {student.LastName}"; var rosterName = rosterCache.GetByClass(className) - .FirstOrDefault(r => UntisNameMatching.NamesMatch(r.DisplayName, student.FullName))?.DisplayName - ?? student.FullName; + .FirstOrDefault(r => UntisNameMatching.NamesMatch(r.DisplayName, matchName))?.DisplayName + ?? matchName; var start = first.Year * 10000 + first.Month * 100 + first.Day; var end = last.Year * 10000 + last.Month * 100 + last.Day; var absences = absenceCache.GetByClassAndRange(className, start, end)