Files
LehrerApp/LehrerApp.Templating/StudentReport.cs
T
2026-10-22 11:24:43 +02:00

115 lines
6.7 KiB
C#

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<string> 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<string, byte[]>());
}
public sealed record ReportChartPoint(DateOnly Date, double Value);
public sealed record ReportChartSeries(string Label, string Color, IReadOnlyList<ReportChartPoint> Points);
public static class ParticipationReportDrawing
{
public static DrawingValue Build(IReadOnlyList<ReportChartSeries> 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<DrawingCommand>();
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<DrawStringEx>().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);
}
}