Lesson bekommt dieselbe Anhang-Infrastruktur wie Documentation (Material, Arbeitsblätter, Experimentunterlagen), samt Fix einer Sync-Lücke, die Anhang- Dateibytes bisher nur für Documentation statt generisch übertragen hat (IHasAttachments). Sitzplan-Tab bekommt einen "Plätze mischen"-Button für Klausursitzpläne. Neu: mehrschrittiger Gefährdungsbeurteilungs-Assistent mit optionalem KI-Entwurf (ai-backend/gbu.php) und PDF-Export, Format bewusst als JSON-Anhang statt eigener Datenbank-Entität. Details und Architekturentscheidungen in TODO.md (4.2, 7.1.5, 10.1.8). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
536 lines
24 KiB
C#
536 lines
24 KiB
C#
using QuestPDF.Fluent;
|
|
using QuestPDF.Helpers;
|
|
using QuestPDF.Infrastructure;
|
|
|
|
namespace LehrerApp.Desktop.Services;
|
|
|
|
// ── Druckdaten (bewusst schlanke DTOs statt ViewModels: die PDF-Erzeugung bleibt dadurch ohne
|
|
// Avalonia-/MVVM-Bezug testbar, und die Views entscheiden selbst, was gedruckt wird) ────────────
|
|
|
|
/// <param name="Seats">Alle Plätze des Rasters in Zeilen-Hauptordnung, auch ausgeblendete
|
|
/// (IsHidden) - sie belegen ihre Rasterposition weiterhin, werden aber leer gedruckt
|
|
/// (gleiche Logik wie SeatingPlanPanel/SeatCellViewModel.ShowSeat).</param>
|
|
public sealed record SeatingPlanPrintData(
|
|
string PlanTitle,
|
|
string PlanSubtitle,
|
|
int Columns,
|
|
bool IsBoardAtBottom,
|
|
IReadOnlyList<double> ColumnGapWidths,
|
|
IReadOnlyList<SeatPrintCell> Seats);
|
|
|
|
public sealed record SeatPrintCell(int Row, int Column, string? StudentName, bool IsHidden);
|
|
|
|
public sealed record GradeListPrintData(
|
|
string GroupLabel,
|
|
string SchoolYear,
|
|
string PeriodLabel,
|
|
IReadOnlyList<string> ColumnHeaders,
|
|
IReadOnlyList<GradeListPrintRow> Rows);
|
|
|
|
public sealed record GradeListPrintRow(string Name, IReadOnlyList<string> Cells, string Total);
|
|
|
|
public sealed record LabelValue(string Label, string Value);
|
|
|
|
public sealed record ExamStatisticsPrintData(
|
|
string ExamTitle,
|
|
string DateLabel,
|
|
string NiveauLabel,
|
|
IReadOnlyList<LabelValue> Stats,
|
|
IReadOnlyList<GradeDistributionPrintRow> Distribution,
|
|
IReadOnlyList<TaskAnalysisPrintRow> TaskAnalysis);
|
|
|
|
public sealed record GradeDistributionPrintRow(string Grade, int Count, double Percent);
|
|
|
|
public sealed record TaskAnalysisPrintRow(string Label, double AvgPercent, bool IsWeak);
|
|
|
|
public sealed record CompetencyReportPrintData(
|
|
string GroupLabel,
|
|
string StudentName,
|
|
string Summary,
|
|
string ThresholdLabel,
|
|
IReadOnlyList<CompetencyReportPrintRow> Rows);
|
|
|
|
public sealed record CompetencyReportPrintRow(
|
|
string Domain,
|
|
string Code,
|
|
string Description,
|
|
string InstructionCoverage,
|
|
string ExamCoverage,
|
|
string GroupResult,
|
|
string StudentResult,
|
|
string Recommendation);
|
|
|
|
public sealed record StudentDocumentationPrintData(
|
|
string StudentName,
|
|
IReadOnlyList<DocumentationPrintEntry> Entries);
|
|
|
|
public sealed record DocumentationPrintEntry(
|
|
string DateDisplay,
|
|
string TypeLabel,
|
|
string Title,
|
|
string Content,
|
|
IReadOnlyList<string> Tags,
|
|
bool IsConfidential,
|
|
bool IsDraft);
|
|
|
|
public sealed record HazardAssessmentPrintData(
|
|
string Title,
|
|
string GroupLabel,
|
|
string DateDisplay,
|
|
string KindLabel,
|
|
string Procedure,
|
|
IReadOnlyList<HazardSubstancePrintRow> Substances,
|
|
IReadOnlyList<string> Hazards,
|
|
IReadOnlyList<string> ProtectiveMeasures,
|
|
string FirstAid,
|
|
string Disposal,
|
|
string Notes,
|
|
bool IsAiAssisted);
|
|
|
|
public sealed record HazardSubstancePrintRow(
|
|
string Name, string Amount, string Pictograms, string HStatements, string PStatements);
|
|
|
|
/// <summary>
|
|
/// PDF-Erzeugung über QuestPDF (11.3). Liefert fertige Bytes - der Speicherdialog läuft wie bei
|
|
/// allen Exporten über <see cref="ExportService"/>. Lebt bewusst in LehrerApp.Desktop statt Core:
|
|
/// QuestPDF zieht SkiaSharp-Native-Binaries mit, die im Server-Image (LehrerApp.Api, referenziert
|
|
/// Core transitiv über Sync) nichts verloren haben.
|
|
/// </summary>
|
|
public sealed class PdfExportService
|
|
{
|
|
static PdfExportService() => QuestPDF.Settings.License = LicenseType.Community;
|
|
|
|
// Bildschirm-Pixel (Tischabstände, 0-300) → Druckpunkte. 0.4 hält auch den Maximalabstand
|
|
// (120pt ≈ 4cm) auf einer A4-Querseite praktikabel.
|
|
private const float GapPixelToPoint = 0.4f;
|
|
|
|
public byte[] BuildSeatingPlanPdf(SeatingPlanPrintData data) =>
|
|
Document.Create(container => container.Page(page =>
|
|
{
|
|
page.Size(PageSizes.A4.Landscape());
|
|
page.Margin(36);
|
|
page.DefaultTextStyle(style => style.FontSize(10));
|
|
|
|
page.Header().Element(header => Header(header, data.PlanTitle, data.PlanSubtitle));
|
|
|
|
page.Content().PaddingVertical(10).Column(column =>
|
|
{
|
|
column.Spacing(10);
|
|
if (!data.IsBoardAtBottom) column.Item().Element(BoardBanner);
|
|
column.Item().Element(grid => SeatGrid(grid, data));
|
|
if (data.IsBoardAtBottom) column.Item().Element(BoardBanner);
|
|
});
|
|
|
|
page.Footer().Element(Footer);
|
|
})).GeneratePdf();
|
|
|
|
public byte[] BuildGradeListPdf(GradeListPrintData data) =>
|
|
Document.Create(container => container.Page(page =>
|
|
{
|
|
page.Size(PageSizes.A4.Landscape());
|
|
page.Margin(36);
|
|
page.DefaultTextStyle(style => style.FontSize(8));
|
|
|
|
page.Header().Element(header => Header(header,
|
|
$"Notenliste {data.GroupLabel}",
|
|
$"{data.SchoolYear} · {data.PeriodLabel}"));
|
|
|
|
page.Content().PaddingVertical(10).Table(table =>
|
|
{
|
|
table.ColumnsDefinition(columns =>
|
|
{
|
|
columns.RelativeColumn(2.4f);
|
|
foreach (var _ in data.ColumnHeaders) columns.RelativeColumn();
|
|
columns.RelativeColumn(0.8f);
|
|
});
|
|
|
|
table.Header(header =>
|
|
{
|
|
header.Cell().Element(HeaderCell).Text("Name").SemiBold();
|
|
foreach (var columnHeader in data.ColumnHeaders)
|
|
header.Cell().Element(HeaderCell).Text(columnHeader).SemiBold();
|
|
header.Cell().Element(HeaderCell).AlignRight().Text("Schnitt").SemiBold();
|
|
});
|
|
|
|
foreach (var row in data.Rows)
|
|
{
|
|
table.Cell().Element(BodyCell).Text(row.Name);
|
|
foreach (var cell in row.Cells)
|
|
table.Cell().Element(BodyCell).AlignCenter().Text(cell);
|
|
table.Cell().Element(BodyCell).AlignRight().Text(row.Total).SemiBold();
|
|
}
|
|
});
|
|
|
|
page.Footer().Element(Footer);
|
|
})).GeneratePdf();
|
|
|
|
public byte[] BuildExamStatisticsPdf(ExamStatisticsPrintData data) =>
|
|
Document.Create(container => container.Page(page =>
|
|
{
|
|
page.Size(PageSizes.A4);
|
|
page.Margin(36);
|
|
page.DefaultTextStyle(style => style.FontSize(10));
|
|
|
|
var subtitle = string.IsNullOrWhiteSpace(data.NiveauLabel)
|
|
? data.DateLabel
|
|
: $"{data.DateLabel} · Niveau {data.NiveauLabel}";
|
|
page.Header().Element(header => Header(header, data.ExamTitle, subtitle));
|
|
|
|
page.Content().PaddingVertical(10).Column(column =>
|
|
{
|
|
column.Spacing(14);
|
|
|
|
column.Item().Table(table =>
|
|
{
|
|
table.ColumnsDefinition(columns =>
|
|
{
|
|
columns.RelativeColumn(2);
|
|
columns.RelativeColumn();
|
|
});
|
|
foreach (var stat in data.Stats)
|
|
{
|
|
table.Cell().Element(BodyCell).Text(stat.Label);
|
|
table.Cell().Element(BodyCell).AlignRight().Text(stat.Value).SemiBold();
|
|
}
|
|
});
|
|
|
|
column.Item().Text("Notenspiegel").FontSize(12).SemiBold();
|
|
column.Item().Table(table =>
|
|
{
|
|
table.ColumnsDefinition(columns =>
|
|
{
|
|
columns.ConstantColumn(60);
|
|
columns.ConstantColumn(60);
|
|
columns.ConstantColumn(60);
|
|
columns.RelativeColumn();
|
|
});
|
|
table.Header(header =>
|
|
{
|
|
header.Cell().Element(HeaderCell).Text("Note").SemiBold();
|
|
header.Cell().Element(HeaderCell).AlignRight().Text("Anzahl").SemiBold();
|
|
header.Cell().Element(HeaderCell).AlignRight().Text("Anteil").SemiBold();
|
|
header.Cell().Element(HeaderCell);
|
|
});
|
|
var maxCount = data.Distribution.Count == 0 ? 0 : data.Distribution.Max(d => d.Count);
|
|
foreach (var entry in data.Distribution)
|
|
{
|
|
table.Cell().Element(BodyCell).Text(entry.Grade);
|
|
table.Cell().Element(BodyCell).AlignRight().Text(entry.Count.ToString());
|
|
table.Cell().Element(BodyCell).AlignRight().Text($"{entry.Percent:0.#} %");
|
|
table.Cell().Element(BodyCell).AlignMiddle().Element(bar =>
|
|
Bar(bar, maxCount == 0 ? 0 : entry.Count * 100.0 / maxCount, Colors.Blue.Medium));
|
|
}
|
|
});
|
|
|
|
if (data.TaskAnalysis.Count > 0)
|
|
{
|
|
column.Item().Text("Aufgabenanalyse").FontSize(12).SemiBold();
|
|
column.Item().Table(table =>
|
|
{
|
|
table.ColumnsDefinition(columns =>
|
|
{
|
|
columns.RelativeColumn(2);
|
|
columns.ConstantColumn(80);
|
|
columns.RelativeColumn();
|
|
});
|
|
table.Header(header =>
|
|
{
|
|
header.Cell().Element(HeaderCell).Text("Aufgabe").SemiBold();
|
|
header.Cell().Element(HeaderCell).AlignRight().Text("Ø Erfüllung").SemiBold();
|
|
header.Cell().Element(HeaderCell);
|
|
});
|
|
foreach (var task in data.TaskAnalysis)
|
|
{
|
|
table.Cell().Element(BodyCell).Text(text =>
|
|
{
|
|
text.Span(task.Label);
|
|
if (task.IsWeak) text.Span(" auffällig schwach")
|
|
.FontColor(Colors.Red.Darken1).FontSize(8);
|
|
});
|
|
table.Cell().Element(BodyCell).AlignRight().Text($"{task.AvgPercent:0.#} %");
|
|
table.Cell().Element(BodyCell).AlignMiddle().Element(bar =>
|
|
Bar(bar, task.AvgPercent, task.IsWeak ? Colors.Red.Medium : Colors.Green.Medium));
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
page.Footer().Element(Footer);
|
|
})).GeneratePdf();
|
|
|
|
public byte[] BuildCompetencyReportPdf(CompetencyReportPrintData data) =>
|
|
Document.Create(container => container.Page(page =>
|
|
{
|
|
page.Size(PageSizes.A4.Landscape());
|
|
page.Margin(36);
|
|
page.DefaultTextStyle(style => style.FontSize(8));
|
|
|
|
page.Header().Element(header => Header(header,
|
|
$"Kompetenzbericht · {data.StudentName}", $"{data.GroupLabel} · {data.Summary}"));
|
|
|
|
page.Content().PaddingVertical(10).Column(column =>
|
|
{
|
|
column.Spacing(8);
|
|
column.Item().Text(data.ThresholdLabel).FontSize(8).FontColor(Colors.Grey.Darken1);
|
|
column.Item().Table(table =>
|
|
{
|
|
table.ColumnsDefinition(columns =>
|
|
{
|
|
columns.RelativeColumn(1.4f); // Bereich
|
|
columns.RelativeColumn(0.5f); // Code
|
|
columns.RelativeColumn(2.3f); // Beschreibung
|
|
columns.RelativeColumn(1.1f); // Unterricht
|
|
columns.RelativeColumn(1.0f); // Klausuren
|
|
columns.RelativeColumn(1.4f); // Gruppe
|
|
columns.RelativeColumn(1.1f); // Schüler
|
|
// "Wiederholungsbedarf" (längster Wert) muss ohne Wortumbruch passen.
|
|
columns.ConstantColumn(88); // Empfehlung
|
|
});
|
|
table.Header(header =>
|
|
{
|
|
header.Cell().Element(HeaderCell).Text("Bereich").SemiBold();
|
|
header.Cell().Element(HeaderCell).Text("Code").SemiBold();
|
|
header.Cell().Element(HeaderCell).Text("Kompetenz").SemiBold();
|
|
header.Cell().Element(HeaderCell).Text("Unterricht").SemiBold();
|
|
header.Cell().Element(HeaderCell).Text("Klausuren").SemiBold();
|
|
header.Cell().Element(HeaderCell).Text("Gruppenergebnis").SemiBold();
|
|
header.Cell().Element(HeaderCell).Text("Schüler").SemiBold();
|
|
header.Cell().Element(HeaderCell).Text("Empfehlung").SemiBold();
|
|
});
|
|
foreach (var row in data.Rows)
|
|
{
|
|
table.Cell().Element(BodyCell).Text(row.Domain);
|
|
table.Cell().Element(BodyCell).Text(row.Code);
|
|
table.Cell().Element(BodyCell).Text(row.Description);
|
|
table.Cell().Element(BodyCell).Text(row.InstructionCoverage);
|
|
table.Cell().Element(BodyCell).Text(row.ExamCoverage);
|
|
table.Cell().Element(BodyCell).Text(row.GroupResult);
|
|
table.Cell().Element(BodyCell).Text(row.StudentResult).SemiBold();
|
|
table.Cell().Element(BodyCell).Text(row.Recommendation)
|
|
.FontColor(row.Recommendation == "Wiederholungsbedarf"
|
|
? Colors.Red.Darken1
|
|
: row.Recommendation == "Stand solide"
|
|
? Colors.Green.Darken1
|
|
: Colors.Grey.Darken1);
|
|
}
|
|
});
|
|
});
|
|
|
|
page.Footer().Element(Footer);
|
|
})).GeneratePdf();
|
|
|
|
public byte[] BuildStudentDocumentationPdf(StudentDocumentationPrintData data) =>
|
|
Document.Create(container => container.Page(page =>
|
|
{
|
|
page.Size(PageSizes.A4);
|
|
page.Margin(36);
|
|
page.DefaultTextStyle(style => style.FontSize(9));
|
|
|
|
page.Header().Element(header => Header(header,
|
|
$"Dokumentation · {data.StudentName}",
|
|
data.Entries.Count == 1 ? "1 Eintrag" : $"{data.Entries.Count} Einträge"));
|
|
|
|
page.Content().PaddingVertical(10).Column(column =>
|
|
{
|
|
column.Spacing(10);
|
|
foreach (var entry in data.Entries)
|
|
column.Item().Border(0.8f).BorderColor(Colors.Grey.Lighten2).Padding(8)
|
|
.Column(block =>
|
|
{
|
|
block.Spacing(3);
|
|
block.Item().Row(row =>
|
|
{
|
|
row.RelativeItem().Text(text =>
|
|
{
|
|
text.Span(entry.Title).FontSize(10).SemiBold();
|
|
if (entry.IsConfidential)
|
|
text.Span(" vertraulich").FontSize(8).FontColor(Colors.Red.Darken1);
|
|
if (entry.IsDraft)
|
|
text.Span(" Entwurf").FontSize(8).FontColor(Colors.Orange.Darken2);
|
|
});
|
|
row.ConstantItem(130).AlignRight()
|
|
.Text($"{entry.TypeLabel} · {entry.DateDisplay}")
|
|
.FontSize(8).FontColor(Colors.Grey.Darken1);
|
|
});
|
|
if (!string.IsNullOrWhiteSpace(entry.Content))
|
|
block.Item().Text(entry.Content);
|
|
if (entry.Tags.Count > 0)
|
|
block.Item().Text(string.Join(" · ", entry.Tags))
|
|
.FontSize(8).FontColor(Colors.Grey.Darken1);
|
|
});
|
|
});
|
|
|
|
page.Footer().Element(Footer);
|
|
})).GeneratePdf();
|
|
|
|
public byte[] BuildHazardAssessmentPdf(HazardAssessmentPrintData data) =>
|
|
Document.Create(container => container.Page(page =>
|
|
{
|
|
page.Size(PageSizes.A4);
|
|
page.Margin(36);
|
|
page.DefaultTextStyle(style => style.FontSize(9));
|
|
|
|
page.Header().Element(header => Header(header, $"Gefährdungsbeurteilung · {data.Title}",
|
|
$"{data.GroupLabel} · {data.KindLabel} · {data.DateDisplay}"));
|
|
|
|
page.Content().PaddingVertical(10).Column(column =>
|
|
{
|
|
column.Spacing(10);
|
|
if (data.IsAiAssisted) column.Item().Element(AiDisclaimerBanner);
|
|
if (!string.IsNullOrWhiteSpace(data.Procedure))
|
|
column.Item().Element(c => TextSection(c, "Durchführung", data.Procedure));
|
|
if (data.Substances.Count > 0)
|
|
column.Item().Element(c => SubstanceTable(c, data.Substances));
|
|
if (data.Hazards.Count > 0)
|
|
column.Item().Element(c => BulletSection(c, "Gefährdungen", data.Hazards));
|
|
if (data.ProtectiveMeasures.Count > 0)
|
|
column.Item().Element(c => BulletSection(c, "Schutzmaßnahmen", data.ProtectiveMeasures));
|
|
if (!string.IsNullOrWhiteSpace(data.FirstAid))
|
|
column.Item().Element(c => TextSection(c, "Erste Hilfe", data.FirstAid));
|
|
if (!string.IsNullOrWhiteSpace(data.Disposal))
|
|
column.Item().Element(c => TextSection(c, "Entsorgung", data.Disposal));
|
|
if (!string.IsNullOrWhiteSpace(data.Notes))
|
|
column.Item().Element(c => TextSection(c, "Weitere Hinweise", data.Notes));
|
|
});
|
|
|
|
page.Footer().Element(Footer);
|
|
})).GeneratePdf();
|
|
|
|
// ── Bausteine ───────────────────────────────────────────────────────────────
|
|
|
|
private static void AiDisclaimerBanner(IContainer container) =>
|
|
container.Background(Colors.Orange.Lighten4).Padding(8).Text(
|
|
"⚠️ Enthält KI-generierte Anteile — kein rechtssicheres Dokument. Vor Verwendung " +
|
|
"eigenverantwortlich prüfen, insbesondere H-/P-Sätze und Mengenangaben gegen das " +
|
|
"Sicherheitsdatenblatt.").FontSize(9).FontColor(Colors.Orange.Darken3).SemiBold();
|
|
|
|
private static void TextSection(IContainer container, string title, string text) =>
|
|
container.Column(column =>
|
|
{
|
|
column.Item().Text(title).FontSize(11).SemiBold();
|
|
column.Item().Text(text);
|
|
});
|
|
|
|
private static void BulletSection(IContainer container, string title, IReadOnlyList<string> items) =>
|
|
container.Column(column =>
|
|
{
|
|
column.Item().Text(title).FontSize(11).SemiBold();
|
|
foreach (var item in items)
|
|
column.Item().Text($"• {item}");
|
|
});
|
|
|
|
private static void SubstanceTable(IContainer container, IReadOnlyList<HazardSubstancePrintRow> rows) =>
|
|
container.Column(column =>
|
|
{
|
|
column.Item().Text("Gefahrstoffe").FontSize(11).SemiBold();
|
|
column.Item().Table(table =>
|
|
{
|
|
table.ColumnsDefinition(columns =>
|
|
{
|
|
columns.RelativeColumn(2f);
|
|
columns.RelativeColumn(1f);
|
|
columns.RelativeColumn(1.3f);
|
|
columns.RelativeColumn(2f);
|
|
columns.RelativeColumn(2f);
|
|
});
|
|
table.Header(header =>
|
|
{
|
|
header.Cell().Element(HeaderCell).Text("Stoff").SemiBold();
|
|
header.Cell().Element(HeaderCell).Text("Menge").SemiBold();
|
|
header.Cell().Element(HeaderCell).Text("GHS").SemiBold();
|
|
header.Cell().Element(HeaderCell).Text("H-Sätze").SemiBold();
|
|
header.Cell().Element(HeaderCell).Text("P-Sätze").SemiBold();
|
|
});
|
|
foreach (var row in rows)
|
|
{
|
|
table.Cell().Element(BodyCell).Text(row.Name);
|
|
table.Cell().Element(BodyCell).Text(row.Amount);
|
|
table.Cell().Element(BodyCell).Text(row.Pictograms);
|
|
table.Cell().Element(BodyCell).Text(row.HStatements);
|
|
table.Cell().Element(BodyCell).Text(row.PStatements);
|
|
}
|
|
});
|
|
});
|
|
|
|
/// <summary>Horizontaler Balken, 0-100 % der verfügbaren Zellenbreite (max. 150pt).</summary>
|
|
private static void Bar(IContainer container, double percent, string color) =>
|
|
container.MaxWidth(150).Height(7).Background(Colors.Grey.Lighten3)
|
|
.AlignLeft().Width((float)(Math.Clamp(percent, 0, 100) * 1.5)).Background(color);
|
|
|
|
private static void Header(IContainer container, string title, string subtitle) =>
|
|
container.BorderBottom(1).BorderColor(Colors.Grey.Lighten2).PaddingBottom(6).Row(row =>
|
|
{
|
|
row.RelativeItem().Column(column =>
|
|
{
|
|
column.Item().Text(title).FontSize(16).SemiBold();
|
|
if (!string.IsNullOrWhiteSpace(subtitle))
|
|
column.Item().Text(subtitle).FontSize(10).FontColor(Colors.Grey.Darken1);
|
|
});
|
|
row.ConstantItem(110).AlignRight().AlignBottom()
|
|
.Text($"Stand: {DateTime.Now:dd.MM.yyyy}").FontSize(9).FontColor(Colors.Grey.Darken1);
|
|
});
|
|
|
|
private static void Footer(IContainer container) =>
|
|
container.AlignCenter().Text(text =>
|
|
{
|
|
text.DefaultTextStyle(style => style.FontSize(8).FontColor(Colors.Grey.Darken1));
|
|
text.Span("Seite ");
|
|
text.CurrentPageNumber();
|
|
text.Span(" von ");
|
|
text.TotalPages();
|
|
});
|
|
|
|
private static void BoardBanner(IContainer container) =>
|
|
container.AlignCenter().Background(Colors.Grey.Lighten3).PaddingVertical(4)
|
|
.PaddingHorizontal(40).Text("Tafel / Vorderseite").FontSize(9).SemiBold()
|
|
.FontColor(Colors.Grey.Darken2);
|
|
|
|
private static void SeatGrid(IContainer container, SeatingPlanPrintData data) =>
|
|
container.Table(table =>
|
|
{
|
|
// Tischabstände als eigene, schmale Leerspalten zwischen den Sitzspalten - dieselbe
|
|
// Idee wie SeatingPlanPanel, nur mit Tabellenspalten statt Panel-Arithmetik. Merkt
|
|
// sich je Sitzspalte den 1-basierten Tabellenspalten-Index.
|
|
var tableColumnOfSeatColumn = new int[Math.Max(1, data.Columns)];
|
|
table.ColumnsDefinition(columns =>
|
|
{
|
|
var next = 1;
|
|
for (var c = 0; c < data.Columns; c++)
|
|
{
|
|
columns.RelativeColumn();
|
|
tableColumnOfSeatColumn[c] = next++;
|
|
var gap = c < data.ColumnGapWidths.Count ? data.ColumnGapWidths[c] : 0;
|
|
if (c < data.Columns - 1 && gap > 0)
|
|
{
|
|
columns.ConstantColumn((float)gap * GapPixelToPoint);
|
|
next++;
|
|
}
|
|
}
|
|
});
|
|
|
|
foreach (var seat in data.Seats)
|
|
{
|
|
var cell = table.Cell()
|
|
.Row((uint)(seat.Row + 1))
|
|
.Column((uint)tableColumnOfSeatColumn[seat.Column])
|
|
.Padding(3);
|
|
// Ausgeblendete Plätze (kein Tisch im Raum) bleiben als leere Rasterposition
|
|
// erhalten, damit die übrigen Plätze der Reihe nicht verrutschen.
|
|
if (seat.IsHidden) { cell.MinHeight(38); continue; }
|
|
var box = cell.MinHeight(38).Border(0.8f)
|
|
.BorderColor(seat.StudentName is null ? Colors.Grey.Lighten2 : Colors.Grey.Darken1)
|
|
.Padding(4).AlignCenter().AlignMiddle();
|
|
if (seat.StudentName is null)
|
|
box.Text("frei").FontSize(8).FontColor(Colors.Grey.Lighten1);
|
|
else
|
|
box.Text(seat.StudentName).FontSize(9).SemiBold();
|
|
}
|
|
});
|
|
|
|
private static IContainer HeaderCell(IContainer container) => container
|
|
.BorderBottom(1).BorderColor(Colors.Grey.Darken1).PaddingVertical(3).PaddingHorizontal(2);
|
|
|
|
private static IContainer BodyCell(IContainer container) => container
|
|
.BorderBottom(0.5f).BorderColor(Colors.Grey.Lighten2).PaddingVertical(2.5f).PaddingHorizontal(2);
|
|
}
|