namespace LehrerApp.Templating; public static class StudentReportTemplate { public const string Participation = "Student.ParticipationChart"; public const string Competencies = "Student.Competencies"; public const string Homework = "Student.Homework"; public const string Documentation = "Student.Documentation"; public static IReadOnlyList Drawings { get; } = [ReportTextDrawing.InterimGrades, Participation, Competencies, Homework, Documentation]; public const string Layout = """ PAGE 210 297 mm #pragma format-version 3 #pragma page-template first TEXT 20 12 "Schülerbericht: $Student.Name" size=15 TEXT 20 22 "Lerngruppe: $Group.Name · $Report.Period" size=10 #pragma flow-slot body x=20 y=34 w=170 h=240 #pragma end-page-template #pragma content-flow body FLOWDRAWBOX $Student.InterimGrades w=170 h=230 fit=content gap=5 FLOWDRAWBOX $Student.ParticipationChart w=170 h=230 fit=content gap=5 FLOWDRAWBOX $Student.Competencies w=170 h=230 fit=content gap=5 FLOWDRAWBOX $Student.Homework w=170 h=230 fit=content gap=5 FLOWDRAWBOX $Student.Documentation w=170 h=230 fit=content gap=5 FLOWBOX $Report.Comment w=170 size=10 #pragma end-content-flow """; public static DrawingValue SampleDrawing(string name, DeclaredDrawingBox? declared = null) { var box = declared ?? new DeclaredDrawingBox(170, 240, 1, false); var from = new DateOnly(2026, 1, 1); if (name == Participation) return ParticipationReportDrawing.Build([new("Gewichtete Mitarbeit", "#6C5CE7", [new(from, -.5), new(from.AddDays(7), .5), new(from.AddDays(14), 1.2)])], from, from.AddDays(20), box.Width, box.MillimeterScale); var (title, text) = name switch { ReportTextDrawing.RepeatInfo => ("Wiederholungsarbeit", new[] { "Erste Arbeit: Atombau vom 18.12.2025", "Note der ersten Arbeit: 4", "Für die Bewertung zählt die bessere Note aus beiden Arbeiten." }), ReportTextDrawing.InterimGrades => ("Vorläufiger Notenzwischenstand", new[] { "Stichtag: 15.01.2026 · Sprechtag", "Mündlich: 2", "Schriftlich: 3", "Vorläufige Zeugnisnote: 2-3" }), Competencies => ("Kompetenzen", new[] { "CH1 Aufbau der Atome beschreiben: 75 % (4 Bewertungen)" }), Homework => ("Hausaufgaben", new[] { "12.01.2026: Hausaufgabe gemacht", "14.01.2026: Hausaufgabe nachgereicht" }), _ => ("Klassenbucheinträge und Unterrichtsnotizen", new[] { "15.01.2026 · Gespräch: Lernentwicklung", "Erika arbeitet inzwischen regelmäßig mit." }), }; return ReportTextDrawing.Build(title, text, box.Width, box.MillimeterScale, box.Height, box.IsFixed); } public static LoadedTemplate Create() => new(new TemplateManifest { Id = "student-report-standard", Name = "Schülerbericht Standard", Metadata = new() { [TemplateMetadataKeys.ReportType] = TemplateReportTypes.StudentReport }, Placeholders = new[] { new PlaceholderDefinition("Student.Name", PlaceholderType.Text), new PlaceholderDefinition("Group.Name", PlaceholderType.Text), new PlaceholderDefinition("Report.Period", PlaceholderType.Text), new PlaceholderDefinition("Report.Comment", PlaceholderType.Multiline), }.Concat(Drawings.Select(n => new PlaceholderDefinition(n, PlaceholderType.Drawing))).ToList(), }, new LayoutParser().Parse(Layout), new Dictionary()); } public sealed record ReportChartPoint(DateOnly Date, double Value); public sealed record ReportChartSeries(string Label, string Color, IReadOnlyList Points); public static class ParticipationReportDrawing { public static DrawingValue Build(IReadOnlyList series, DateOnly from, DateOnly to, float width = 170, float scale = 1) { if (from > to || width < 50 * scale || scale <= 0 || !float.IsFinite(width) || !float.IsFinite(scale)) throw new InvalidDataException("Der Mitarbeitsgraph benötigt mindestens 50 mm Breite und einen gültigen Zeitraum."); if (series.All(s => s.Points.Count == 0)) return ReportTextDrawing.Build("Mitarbeitsentwicklung", ["Keine bewerteten Sitzungen im gewählten Zeitraum."], width, scale); var commands = new List(); void Text(float x, float y, string text, string color = "#334155", bool bold = false) => commands.Add(new DrawString(x * scale, y * scale, text, 9, color, Bold: bold)); var right = width / scale - 4; const float left = 12, top = 14, height = 40; Text(1, 0, "Mitarbeitsentwicklung (-2 bis +2)", bold: true); for (var value = -2; value <= 2; value++) { var y = top + (2 - value) / 4f * height; Text(1, y - 2, value.ToString(System.Globalization.CultureInfo.InvariantCulture)); commands.Add(new DrawLine(left * scale, y * scale, right * scale, y * scale, "#CBD5E1", .2f * scale)); } var days = Math.Max(1, to.DayNumber - from.DayNumber); foreach (var item in series) { (float X, float Y)? previous = null; foreach (var point in item.Points.Where(p => p.Date >= from && p.Date <= to).OrderBy(p => p.Date)) { var x = (left + (point.Date.DayNumber - from.DayNumber) / (float)days * (right - left)) * scale; var y = (top + (2 - (float)Math.Clamp(point.Value, -2, 2)) / 4 * height) * scale; if (previous is { } prev) commands.Add(new DrawLine(prev.X, prev.Y, x, y, item.Color, .5f * scale)); commands.Add(new DrawCircle(x, y, .8f * scale, item.Color, .2f * scale, item.Color)); previous = (x, y); } } Text(left, 57, from.ToString("dd.MM.yyyy")); Text(Math.Max(left, right - 26), 57, to.ToString("dd.MM.yyyy")); var legendY = 64f; foreach (var item in series) { // Jede Kurve bekommt eine eigene beschriftete Legendenzeile. commands.Add(new DrawLine(left * scale, (legendY + 2) * scale, (left + 6) * scale, (legendY + 2) * scale, item.Color, .6f * scale)); var legend = ReportTextDrawing.Build("", [item.Label], (right - left - 10) * scale, scale); // Legendentexte konservativ umbrechen, auch bei individuell langen Aspektnamen. foreach (var line in legend.Commands.OfType().Skip(1)) commands.Add(line with { X = (left + 9) * scale, Y = line.Y - 5 * scale + legendY * scale, Color = item.Color }); legendY += legend.ContentHeight / scale - 5; } return new DrawingValue(commands, (legendY + 2) * scale); } }