feat: Fehltage-Liste zweispaltig bei ausreichend Zeilen und Boxbreite
Chronologische Liste splittet ab 4 Zeilen in zwei Spalten (erste Hälfte links, Rest rechts), sofern die Box mindestens zwei komfortable Spalten zulässt (>= 2x Referenzbreite + Zwischenraum) - sonst blieben Datum/Umfang/ Status pro Hälfte zu schmal. Halbiert die benötigte Höhe bei längeren Listen etwa, was der vom Nutzer beobachteten unerklärlichen Leerseite bei einer sehr langen einspaltigen FLOWDRAWBOX entgegenwirken sollte. Bei der Gelegenheit einen Bug aus dem vorherigen Commit gefixt: der Refactor auf die gemeinsame DrawRows()-Hilfsfunktion hatte eine doppelte, inzwischen tote Kopfzeilen-Zeichnung übrig gelassen (sichtbar als doppeltes "Datum" in den Draw-Befehlen). Hinweis: die reale Vorlage dieses Nutzers deklariert die FLOWDRAWBOX für Student.AbsenceDays aktuell nur mit 170 statt z.B. 400+ Einheiten Breite - dort bleibt es also weiterhin bewusst einspaltig, bis die Box verbreitert wird. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
3c74e3873a
commit
f1cf01ff88
2 files changed
+140
-35
No files matched your search
@@ -226,6 +226,58 @@ public sealed class ClassTeacherViewModelsTests
|
||||
Assert.All(rectangles, c => Assert.True(c.X + c.Width <= contentWidth + 0.01f));
|
||||
}
|
||||
|
||||
private static ClassAbsenceDaySummaryRow[] BuildFehltage(int count) =>
|
||||
[.. Enumerable.Range(1, count).Select(day => new ClassAbsenceDaySummaryRow(
|
||||
new DateOnly(2026, 9, day), "Ada Müller", 1, 0, 0,
|
||||
["Deu"], [1], ["nicht entsch."], ["Absent"], null, null, true))];
|
||||
|
||||
// Regression: eine sehr lange einspaltige Fehlzeitenliste (FLOWDRAWBOX ohne automatischen
|
||||
// Seitenumbruch-Schutz) konnte in der echten Vorlage zu einer unerklärlichen Leerseite führen.
|
||||
// Ab ausreichend Zeilen UND ausreichend breiter Box wird deshalb zweispaltig (chronologisch:
|
||||
// erste Hälfte links, Rest rechts) gerendert - das halbiert die benötigte Höhe ungefähr.
|
||||
[Fact]
|
||||
public void ElternbriefFehltage_RendertZweispaltigWennBoxBreitGenugIst()
|
||||
{
|
||||
var options = new AttendanceCalendarOptions(new DateOnly(2026, 9, 1), 1);
|
||||
var absences = BuildFehltage(10);
|
||||
|
||||
var einspaltig = StudentAbsenceDayListDrawingBuilder.Build("Ada Müller", options, absences,
|
||||
contentWidth: 170);
|
||||
var zweispaltig = StudentAbsenceDayListDrawingBuilder.Build("Ada Müller", options, absences,
|
||||
contentWidth: 400);
|
||||
|
||||
// Zweispaltig braucht ungefähr die Hälfte der Höhe der einspaltigen Variante für dieselben
|
||||
// Daten (5 Zeilen je Spalte statt 10 Zeilen in einer Spalte, jeweils plus eine Kopfzeile).
|
||||
Assert.True(zweispaltig.ContentHeight < einspaltig.ContentHeight * .7f,
|
||||
$"Zweispaltig ({zweispaltig.ContentHeight}) sollte deutlich niedriger sein als einspaltig ({einspaltig.ContentHeight}).");
|
||||
var texte = zweispaltig.Commands.OfType<DrawStringEx>().Select(c => c.Text).ToList();
|
||||
Assert.Equal(2, texte.Count(t => t == "Datum")); // eine Kopfzeile je Spalte
|
||||
Assert.Contains("01.09.2026", texte);
|
||||
Assert.Contains("10.09.2026", texte);
|
||||
var rechteckeUndTexte = zweispaltig.Commands.Select(c => c switch
|
||||
{
|
||||
DrawRectangle r => (float?)(r.X + r.Width), DrawStringEx s => s.X + s.Width, _ => null,
|
||||
}).Where(x => x is not null);
|
||||
Assert.All(rechteckeUndTexte, x => Assert.True(x <= 400.01f));
|
||||
}
|
||||
|
||||
// Regression: bei einer zu schmalen Box (wie der echten Vorlage dieses Nutzers, deren
|
||||
// FLOWDRAWBOX für die Fehltagesliste aktuell nur 170 Einheiten breit ist) müssen zwei
|
||||
// nebeneinanderliegende Spalten zu schmal für Datum/Umfang/Status werden - deshalb bleibt es
|
||||
// dort bewusst einspaltig, auch bei vielen Zeilen.
|
||||
[Fact]
|
||||
public void ElternbriefFehltage_BleibtEinspaltigWennBoxZuSchmalFuerZweiSpaltenIst()
|
||||
{
|
||||
var options = new AttendanceCalendarOptions(new DateOnly(2026, 9, 1), 1);
|
||||
var absences = BuildFehltage(10);
|
||||
|
||||
var drawing = StudentAbsenceDayListDrawingBuilder.Build("Ada Müller", options, absences,
|
||||
contentWidth: 170);
|
||||
var texte = drawing.Commands.OfType<DrawStringEx>().Select(c => c.Text).ToList();
|
||||
|
||||
Assert.Equal(1, texte.Count(t => t == "Datum"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GroupByStudentAndDay_FasstFehlstundenProSchuelerUndTagZusammen()
|
||||
{
|
||||
|
||||
@@ -200,17 +200,59 @@ public static class StudentAbsenceDayListDrawingBuilder
|
||||
var extentColumnWidth = contentWidth * (66f / referenceWidth);
|
||||
var statusColumnX = contentWidth * (114f / referenceWidth);
|
||||
var statusColumnWidth = contentWidth * (54f / referenceWidth);
|
||||
commands.Add(new DrawRectangle(0, y, contentWidth, rowHeight, "#CBD5E1", .4f, "#F3F4F6"));
|
||||
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;
|
||||
|
||||
(string Extent, string Status, string StatusColor) Format(ClassAbsenceDaySummaryRow row)
|
||||
{
|
||||
var extent = row.CountsAsFullDay
|
||||
? "Ganzer Fehltag"
|
||||
: row.TotalAbsentPeriods > 0
|
||||
? $"Fehlzeit · {row.TotalAbsentPeriods} Std."
|
||||
: $"Fehlzeit · {row.TotalAbsentMinutes} Min.";
|
||||
var status = row.IsUnexcused
|
||||
? "Unentschuldigt"
|
||||
: row.FriendlyStatusLabel.Contains("Entschuldigt", StringComparison.OrdinalIgnoreCase)
|
||||
? "Entschuldigt"
|
||||
: row.FriendlyStatusLabel;
|
||||
return (extent, status, row.IsUnexcused ? "#C62828" : "#2E7D32");
|
||||
}
|
||||
|
||||
void DrawRows(float columnX, float columnWidth, float dColumnX, float dColumnWidth, float eColumnX,
|
||||
float eColumnWidth, float sColumnX, float sColumnWidth,
|
||||
IReadOnlyList<ClassAbsenceDaySummaryRow> columnRows)
|
||||
{
|
||||
var rowY = y;
|
||||
commands.Add(new DrawRectangle(columnX, rowY, columnWidth, rowHeight, "#CBD5E1", .4f, "#F3F4F6"));
|
||||
commands.Add(new DrawStringEx(dColumnX, rowY + scale * millimeterScale, rowHeight - scale * millimeterScale,
|
||||
dColumnWidth, "Datum", DrawingTextAlignment.AlignLeft, 7.5f * scale, Color: "#374151", Bold: true));
|
||||
commands.Add(new DrawStringEx(eColumnX, rowY + scale * millimeterScale, rowHeight - scale * millimeterScale,
|
||||
eColumnWidth, "Umfang", DrawingTextAlignment.AlignLeft, 7.5f * scale, Color: "#374151", Bold: true));
|
||||
commands.Add(new DrawStringEx(sColumnX, rowY + scale * millimeterScale, rowHeight - scale * millimeterScale,
|
||||
sColumnWidth, "Status", DrawingTextAlignment.AlignLeft, 7.5f * scale, Color: "#374151", Bold: true));
|
||||
rowY += rowHeight;
|
||||
for (var index = 0; index < columnRows.Count; index++)
|
||||
{
|
||||
var row = columnRows[index];
|
||||
var (extent, status, statusColor) = Format(row);
|
||||
var fill = index % 2 == 0 ? "#FFFFFF" : "#F9FAFB";
|
||||
commands.Add(new DrawRectangle(columnX, rowY, columnWidth, rowHeight, "#E5E7EB", .3f, fill));
|
||||
commands.Add(new DrawStringEx(dColumnX, rowY + scale * millimeterScale,
|
||||
rowHeight - scale * millimeterScale, dColumnWidth,
|
||||
row.Date.ToString("dd.MM.yyyy"), DrawingTextAlignment.AlignLeft, 7.5f * scale, Color: "#374151"));
|
||||
commands.Add(new DrawStringEx(eColumnX, rowY + scale * millimeterScale,
|
||||
rowHeight - scale * millimeterScale, eColumnWidth,
|
||||
extent, DrawingTextAlignment.AlignLeft, 7.5f * scale, Color: "#374151"));
|
||||
commands.Add(new DrawStringEx(sColumnX, rowY + scale * millimeterScale,
|
||||
rowHeight - scale * millimeterScale, sColumnWidth,
|
||||
status, DrawingTextAlignment.AlignLeft, 7.5f * scale, Color: statusColor, Bold: row.IsUnexcused));
|
||||
rowY += rowHeight;
|
||||
}
|
||||
}
|
||||
|
||||
if (rows.Count == 0)
|
||||
{
|
||||
DrawRows(0, contentWidth, dateColumnX, dateColumnWidth, extentColumnX, extentColumnWidth,
|
||||
statusColumnX, statusColumnWidth, []);
|
||||
y += rowHeight;
|
||||
commands.Add(new DrawStringEx(dateColumnX, y + 2 * scale * millimeterScale, rowHeight,
|
||||
contentWidth - 2 * dateColumnX, "Keine Fehltage im gewählten Zeitraum",
|
||||
DrawingTextAlignment.AlignLeft, 8 * scale, Color: "#6B7280", Italic: true));
|
||||
@@ -218,34 +260,45 @@ public static class StudentAbsenceDayListDrawingBuilder
|
||||
}
|
||||
else
|
||||
{
|
||||
for (var index = 0; index < rows.Count; index++)
|
||||
// Ab genug Zeilen zweispaltig (chronologisch: erste Hälfte links, Rest rechts) - spart bei
|
||||
// längeren Listen spürbar Höhe. Nur wenn die Box auch wirklich breit genug für zwei volle
|
||||
// Spalten ist (>= 2x die Einspalten-Referenzbreite + Zwischenraum) - sonst blieben Datum/
|
||||
// Umfang/Status in jeder Hälfte zu schmal und würden abgeschnitten, siehe die reale
|
||||
// Vorlage dieses Nutzers, deren FLOWDRAWBOX für diesen Platzhalter aktuell nur 170 statt
|
||||
// z.B. 400+ Einheiten breit ist (dort bleibt es also bewusst einspaltig).
|
||||
const float columnGapDesign = 8f;
|
||||
var minTwoColumnWidth = 2 * referenceWidth + columnGapDesign;
|
||||
if (rows.Count >= 4 && contentWidth >= minTwoColumnWidth)
|
||||
{
|
||||
var row = rows[index];
|
||||
var fill = index % 2 == 0 ? "#FFFFFF" : "#F9FAFB";
|
||||
var extent = row.CountsAsFullDay
|
||||
? "Ganzer Fehltag"
|
||||
: row.TotalAbsentPeriods > 0
|
||||
? $"Fehlzeit · {row.TotalAbsentPeriods} Std."
|
||||
: $"Fehlzeit · {row.TotalAbsentMinutes} Min.";
|
||||
var status = row.IsUnexcused
|
||||
? "Unentschuldigt"
|
||||
: row.FriendlyStatusLabel.Contains("Entschuldigt", StringComparison.OrdinalIgnoreCase)
|
||||
? "Entschuldigt"
|
||||
: 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 * 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 * millimeterScale,
|
||||
rowHeight - scale * millimeterScale, extentColumnWidth,
|
||||
extent, DrawingTextAlignment.AlignLeft, 7.5f * scale, Color: "#374151"));
|
||||
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;
|
||||
var columnGap = contentWidth * (columnGapDesign / referenceWidth);
|
||||
var columnWidth = (contentWidth - columnGap) / 2;
|
||||
var leftCount = (rows.Count + 1) / 2;
|
||||
var left = rows.Take(leftCount).ToList();
|
||||
var right = rows.Skip(leftCount).ToList();
|
||||
// Spaltenanteile innerhalb EINER Hälfte, gegen eine 81-Einheiten-Referenz austariert:
|
||||
// 81 + columnGapDesign(8) + 81 = 170 = referenceWidth, dieselbe Gesamtbreite wie das
|
||||
// einspaltige Layout, nur eben in zwei Hälften aufgeteilt.
|
||||
const float halfReferenceWidth = 81f;
|
||||
(float X, float Width) Fraction(float leftEdge, float x, float width) =>
|
||||
(leftEdge + columnWidth * (x / halfReferenceWidth), columnWidth * (width / halfReferenceWidth));
|
||||
var (leftDateX, leftDateWidth) = Fraction(0, 1, 19);
|
||||
var (leftExtentX, leftExtentWidth) = Fraction(0, 21, 32);
|
||||
var (leftStatusX, leftStatusWidth) = Fraction(0, 54, 26);
|
||||
var rightEdge = columnWidth + columnGap;
|
||||
var (rightDateX, rightDateWidth) = Fraction(rightEdge, 1, 19);
|
||||
var (rightExtentX, rightExtentWidth) = Fraction(rightEdge, 21, 32);
|
||||
var (rightStatusX, rightStatusWidth) = Fraction(rightEdge, 54, 26);
|
||||
DrawRows(0, columnWidth, leftDateX, leftDateWidth, leftExtentX, leftExtentWidth, leftStatusX,
|
||||
leftStatusWidth, left);
|
||||
DrawRows(rightEdge, columnWidth, rightDateX, rightDateWidth, rightExtentX, rightExtentWidth,
|
||||
rightStatusX, rightStatusWidth, right);
|
||||
y += rowHeight * (1 + Math.Max(left.Count, right.Count));
|
||||
}
|
||||
else
|
||||
{
|
||||
DrawRows(0, contentWidth, dateColumnX, dateColumnWidth, extentColumnX, extentColumnWidth,
|
||||
statusColumnX, statusColumnWidth, rows);
|
||||
y += rowHeight * (1 + rows.Count);
|
||||
}
|
||||
}
|
||||
return new DrawingValue(commands, y);
|
||||
|
||||
Reference in new issue
Block a user