feat: Anhänge je Stunde, Klausur-Sitzplan mischen, Gefährdungsbeurteilungs-Assistent
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>
This commit is contained in:
@@ -73,6 +73,23 @@ public sealed record DocumentationPrintEntry(
|
||||
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:
|
||||
@@ -346,8 +363,95 @@ public sealed class PdfExportService
|
||||
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)
|
||||
|
||||
Reference in New Issue
Block a user