fix(orion): correct report numbering toc and first page

This commit is contained in:
Schubert Ferenc 2026-07-11 12:48:01 +02:00
parent 503b343070
commit 47f54d3461
7 changed files with 283 additions and 137 deletions

View file

@ -27,6 +27,22 @@ def render_template_text(content: str, context: ReportContext) -> str:
return rendered
class NumberedComponent(ReportComponent):
anchor = ""
title = ""
def __init__(self, anchor: str | None = None, title: str | None = None, number: str | None = None) -> None:
if anchor is not None:
self.anchor = anchor
if title is not None:
self.title = title
self.number = number
@property
def bookmark_label(self) -> str:
return f"{self.number} {self.title}" if self.number else self.title
class CoverComponent(ReportComponent):
anchor = "cover"
title = "Deckblatt"
@ -63,7 +79,7 @@ class CoverComponent(ReportComponent):
'<div class="company-address">SCHUBAMED<br>Validation Suite<br>Medizintechnik und Validierung</div>'
f'<img class="cover-logo" src="{logo_uri}" alt="SCHUBAMED Validation Suite">'
"</div>"
"<h1>PRUEFBERICHT ZUR VALIDIERUNG</h1>"
"<h1>PRÜFBERICHT ZUR VALIDIERUNG</h1>"
'<p class="cover-subtitle">Funktions- und Leistungsqualifikation Klein-Sterilisator</p>'
f'<p class="cover-subtitle">{text(context.customer.name)}</p>'
f"{rows}"
@ -76,17 +92,27 @@ class TocComponent(ReportComponent):
anchor = "toc"
title = "Inhaltsverzeichnis"
def __init__(self, components: list[ReportComponent]) -> None:
self.components = components
def __init__(self, sections: list[dict]) -> None:
self.sections = sections
def render(self, context: ReportContext) -> str:
links = "".join(
f'<li><a href="#{component.anchor}">{text(component.title)}</a></li>'
for component in self.components
if component.anchor not in {"cover", "toc"}
self._toc_item(item)
for item in self.sections
if item.get("toc")
)
return section(self.anchor, self.title, f'<ol class="toc-list">{links}</ol>')
def _toc_item(self, item: dict) -> str:
number = item.get("number")
title = item.get("title")
label = f"{number} {title}" if number else str(title)
depth = str(number).count(".") + 1 if number else 2
return (
f'<li class="toc-level-{depth}"><a href="#{text(item.get("key"))}">'
f'<span class="toc-label">{text(label)}</span></a></li>'
)
class SummaryComponent(ReportComponent):
anchor = "summary"
@ -121,17 +147,29 @@ class SummaryComponent(ReportComponent):
class StaticTextComponent(ReportComponent):
def __init__(self, anchor: str, title: str, body: str = "nicht erfasst", block_key: str | None = None) -> None:
def __init__(
self,
anchor: str,
title: str,
body: str = "nicht erfasst",
block_key: str | None = None,
number: str | None = None,
) -> None:
self.anchor = anchor
self.title = title
self.body = body
self.block_key = block_key
self.number = number
@property
def bookmark_label(self) -> str:
return f"{self.number} {self.title}" if self.number else self.title
def render(self, context: ReportContext) -> str:
if self.block_key and self.block_key in context.text_blocks:
content = render_template_text(context.text_blocks[self.block_key].content, context)
return section(self.anchor, self.title, f"<p>{paragraph(content)}</p>")
return section(self.anchor, self.title, f"<p>{paragraph(self.body)}</p>")
return section(self.anchor, self.title, f"<p>{paragraph(content)}</p>", self.bookmark_label)
return section(self.anchor, self.title, f"<p>{paragraph(self.body)}</p>", self.bookmark_label)
class CustomerComponent(ReportComponent):
@ -182,14 +220,14 @@ class CustomerComponent(ReportComponent):
return section(self.anchor, self.title, body)
class DeviceComponent(ReportComponent):
class DeviceComponent(NumberedComponent):
anchor = "device"
title = "Geraet"
def render(self, context: ReportContext) -> str:
device = context.device
if device is None:
return section(self.anchor, self.title, "")
return section(self.anchor, self.title, "<p>nicht erfasst</p>", self.bookmark_label)
body = definition_list(
[
("Hersteller", device.manufacturer),
@ -212,10 +250,10 @@ class DeviceComponent(ReportComponent):
("Lieferant", device.supplier),
]
)
return section(self.anchor, self.title, body)
return section(self.anchor, self.title, body, self.bookmark_label)
class EquipmentComponent(ReportComponent):
class EquipmentComponent(NumberedComponent):
anchor = "equipment"
title = "Pruefmittel"
@ -247,10 +285,11 @@ class EquipmentComponent(ReportComponent):
],
rows,
),
self.bookmark_label,
)
class EnvironmentComponent(ReportComponent):
class EnvironmentComponent(NumberedComponent):
anchor = "environment"
title = "Umgebungsbedingungen"
@ -269,10 +308,10 @@ class EnvironmentComponent(ReportComponent):
]
if rows:
body += table(["Pruefpunkt", "Bewertung", "Kommentar"], rows)
return section(self.anchor, self.title, body)
return section(self.anchor, self.title, body, self.bookmark_label)
class ChecklistComponent(ReportComponent):
class ChecklistComponent(NumberedComponent):
anchor = "checklists"
title = "Dokumentations- und Leistungschecklisten"
@ -286,7 +325,7 @@ class ChecklistComponent(ReportComponent):
performance = context.validation.performance_checklist or []
body = "<h3>Dokumentation</h3>" + self._render_items(documentation)
body += "<h3>Leistung</h3>" + self._render_items(performance)
return section(self.anchor, self.title, body)
return section(self.anchor, self.title, body, self.bookmark_label)
def _render_items(self, items: list[dict]) -> str:
rows = [
@ -296,7 +335,7 @@ class ChecklistComponent(ReportComponent):
return table(["Nr.", "Pruefpunkt", "Bewertung", "Kommentar"], rows)
class ProgramComponent(ReportComponent):
class ProgramComponent(NumberedComponent):
anchor = "programs"
title = "Programme"
@ -306,10 +345,10 @@ class ProgramComponent(ReportComponent):
[index + 1, item.get("name"), "Eigenes Programm" if item.get("custom") else "Standard"]
for index, item in enumerate(programs)
]
return section(self.anchor, self.title, table(["Nr.", "Programm", "Typ"], rows))
return section(self.anchor, self.title, table(["Nr.", "Programm", "Typ"], rows), self.bookmark_label)
class LoadingComponent(ReportComponent):
class LoadingComponent(NumberedComponent):
anchor = "loading"
title = "Beladungsmuster"
@ -327,10 +366,11 @@ class LoadingComponent(ReportComponent):
self.anchor,
self.title,
table(["Testlauf", "Beladungsmuster", "Beschreibung", "Bilder"], rows),
self.bookmark_label,
)
class MeasurementComponent(ReportComponent):
class MeasurementComponent(NumberedComponent):
anchor = "measurements"
title = "Messdaten"
@ -351,6 +391,7 @@ class MeasurementComponent(ReportComponent):
self.anchor,
self.title,
table(["Testlauf", "Messwert", "Wert", "Einheit", "Quelle", "Sicherheit"], rows, "compact"),
self.bookmark_label,
)
rows = [
[
@ -394,10 +435,10 @@ class MeasurementComponent(ReportComponent):
if winlog_rows:
body += "<h3>Winlog-Dateien</h3>" + table(["Bereich", "Datei", "Typ"], winlog_rows)
body += "<p>Messdaten noch nicht bestätigt.</p>"
return section(self.anchor, self.title, body)
return section(self.anchor, self.title, body, self.bookmark_label)
class DryingComponent(ReportComponent):
class DryingComponent(NumberedComponent):
anchor = "drying"
title = "Trocknung"
@ -412,10 +453,10 @@ class DryingComponent(ReportComponent):
("Bemerkung", data.get("comment")),
]
)
return section(self.anchor, self.title, body)
return section(self.anchor, self.title, body, self.bookmark_label)
class RecommendationComponent(ReportComponent):
class RecommendationComponent(NumberedComponent):
anchor = "recommendations"
title = "Empfehlungen und Auflagen"
@ -424,10 +465,10 @@ class RecommendationComponent(ReportComponent):
[item.get("number"), item.get("text"), item.get("deadline"), item.get("status")]
for item in context.validation.recommendations or []
]
return section(self.anchor, self.title, table(["Nr.", "Text", "Frist", "Status"], rows))
return section(self.anchor, self.title, table(["Nr.", "Text", "Frist", "Status"], rows), self.bookmark_label)
class AttachmentComponent(ReportComponent):
class AttachmentComponent(NumberedComponent):
anchor = "attachments"
title = "Bilder und Anlagen"
@ -456,7 +497,7 @@ class AttachmentComponent(ReportComponent):
body = table(["Reihenfolge", "Kategorie", "Datei", "Beschreibung"], rows)
if figures:
body += '<div class="figure-grid">' + "".join(figures) + "</div>"
return section(self.anchor, self.title, body)
return section(self.anchor, self.title, body, self.bookmark_label)
def _image_source(self, item: dict) -> str | None:
content_type = str(item.get("content_type") or "")

View file

@ -41,5 +41,12 @@ def table(headers: list[str], rows: list[list[Any]], css_class: str = "") -> str
return f"<table class=\"data-table {css_class}\"><thead><tr>{head}</tr></thead><tbody>{body}</tbody></table>"
def section(chapter_id: str, title: str, body: str) -> str:
return f"<section class=\"chapter\" id=\"{text(chapter_id)}\"><h2>{text(title)}</h2>{body}</section>"
def section(chapter_id: str, title: str, body: str, bookmark_label: str | None = None) -> str:
bookmark_attr = (
f' data-bookmark-label="{text(bookmark_label)}"' if bookmark_label is not None else ""
)
heading = bookmark_label or title
return (
f'<section class="chapter" id="{text(chapter_id)}">'
f'<h2 class="chapter-title"{bookmark_attr}>{text(heading)}</h2>{body}</section>'
)

View file

@ -26,6 +26,7 @@ from app.modules.orion.components import (
)
from app.modules.orion.context import OrionContextBuilder, ReportContext
from app.modules.orion.templates.report import render_document
from app.modules.orion.template_service import REPORT_SECTIONS
logger = logging.getLogger(__name__)
@ -52,51 +53,35 @@ class OrionReportService:
return output_path
def _components(self) -> list[ReportComponent]:
chapters: list[ReportComponent] = [
StaticTextComponent("bq", "1 Funktionsqualifikation (BQ)", "nicht erfasst"),
StaticTextComponent("bq-goal", "1.1 Anlass und Ziel der Prüfung", block_key="bq_goal"),
StaticTextComponent("legal", "1.2 Gesetzliche Grundlagen", block_key="legal"),
DeviceComponent(),
StaticTextComponent("performance", "1.4 Leistungsüberprüfung", block_key="performance"),
ChecklistComponent(),
StaticTextComponent("documentation", "1.6 Dokumentation / Kontrolle"),
StaticTextComponent("work-instructions", "1.7 Arbeitsanweisungen"),
EnvironmentComponent(),
StaticTextComponent("batch-control", "1.9 Chargenkontrolle"),
ProgramComponent(),
LoadingComponent(),
StaticTextComponent("reference-load", "1.12 Referenzbeladung Sterilisator"),
StaticTextComponent("equipment", "2 Eingesetzte Prüfmittel"),
EquipmentComponent(),
StaticTextComponent("equipment-thermo", "2.2 Prüfmittel zur thermoelektrischen Untersuchung"),
StaticTextComponent("configuration", "2.3 Prüfkonfiguration"),
StaticTextComponent("lq", "3 Leistungsqualifikation (LQ)"),
StaticTextComponent("thermo-tests", "3.1 Leistungsbeurteilung Thermoelektrische Prüfungen"),
MeasurementComponent(),
StaticTextComponent("run-1", "3.2 Standardbeladung Testlauf 1"),
StaticTextComponent("test-1", "3.2.1 Test 1"),
StaticTextComponent("run-2", "3.3 Standardbeladung Testlauf 2"),
StaticTextComponent("test-2", "3.3.1 Test 2"),
StaticTextComponent("run-3", "3.4 Standardbeladung Testlauf 3"),
StaticTextComponent("test-3", "3.4.1 Test 3"),
StaticTextComponent("results", "4 Ergebnisse der Validierung"),
StaticTextComponent("results-vacuum", "4.1 Vakuumtest"),
StaticTextComponent("results-runs", "4.1.2 Testläufe 1 bis 3"),
DryingComponent(),
RecommendationComponent(),
StaticTextComponent("appendix", "5 Anhang"),
AttachmentComponent(),
StaticTextComponent("winlog", "5.1 Winlog-Auswertungen"),
StaticTextComponent("release-docs", "5.2 Freigabedokumentation / Routineprüfungen"),
StaticTextComponent("indicators", "5.3 Nachweis der umgeschlagenen Indikatoren"),
StaticTextComponent("cycles", "6 Programmabläufe / Zyklen"),
StaticTextComponent("practice-certificates", "6.1 Zertifikate Praxis"),
StaticTextComponent("risk", "7 Risikoeinstufung nach RKI"),
StaticTextComponent("closing-talk", "7.1 Abschlussgespräch"),
StaticTextComponent("certificates", "8. Zertifikate"),
StaticTextComponent("calibration-certificates", "9. Werkskalibrierzertifikate Sensoren"),
]
return [CoverComponent(), SummaryComponent(), TocComponent(chapters), CustomerComponent(), *chapters]
chapters = [self._component_for_section(item) for item in REPORT_SECTIONS]
return [CoverComponent(), SummaryComponent(), TocComponent(REPORT_SECTIONS), CustomerComponent(), *chapters]
def _component_for_section(self, item: dict) -> ReportComponent:
component = item.get("component")
key = str(item["key"])
title = str(item["title"])
number = item.get("number")
if component == "device":
return DeviceComponent(key, title, number)
if component == "checklist":
return ChecklistComponent(key, title, number)
if component == "environment":
return EnvironmentComponent(key, title, number)
if component == "programs":
return ProgramComponent(key, title, number)
if component == "loading":
return LoadingComponent(key, title, number)
if component == "equipment":
return EquipmentComponent(key, title, number)
if component == "measurements":
return MeasurementComponent(key, title, number)
if component == "drying":
return DryingComponent(key, title, number)
if component == "recommendations":
return RecommendationComponent(key, title, number)
if component == "attachments":
return AttachmentComponent(key, title, number)
return StaticTextComponent(key, title, block_key=item.get("block_key"), number=number)
def _append_pdf_attachments(self, context: ReportContext, output_path: Path) -> None:
pdf_paths = []

View file

@ -19,48 +19,50 @@ TEMPLATE_NAME = "Erstvalidierung Klein-Sterilisator"
TEMPLATE_VERSION = "1.0"
REFERENCE_PATH = "docs/reference/reports/Erstvalidierung_Dr.Durmaz_Steri_12-25.docx"
SECTION_DEFINITIONS: list[tuple[str, str | None, str, bool]] = [
("bq", "1", "Funktionsqualifikation (BQ)", True),
("bq_goal", "1.1", "Anlass und Ziel der Prüfung", False),
("legal", "1.2", "Gesetzliche Grundlagen", False),
("device", "1.3", "Angaben zum Gerät", False),
("performance", "1.4", "Leistungsüberprüfung", False),
("performance_checklist", "1.5", "Checkliste Leistungsanforderung", False),
("documentation", "1.6", "Dokumentation / Kontrolle", False),
("work_instructions", "1.7", "Arbeitsanweisungen", False),
("environment", "1.8", "Umgebungsbedingungen", False),
("batch_control", "1.9", "Chargenkontrolle", False),
("programs", "1.10", "Beschreibung der verwendeten Programme", False),
("loading", "1.11", "Beladungsbeschreibungen", False),
("reference_load", "1.12", "Referenzbeladung Sterilisator", False),
("equipment", "2", "Eingesetzte Prüfmittel", True),
("measurement_devices", "2.1", "Beschreibung der Messgeräte", False),
("thermo_equipment", "2.2", "Prüfmittel zur thermoelektrischen Untersuchung", False),
("configuration", "2.3", "Prüfkonfiguration", False),
("lq", "3", "Leistungsqualifikation (LQ)", True),
("thermo_tests", "3.1", "Leistungsbeurteilung Thermoelektrische Prüfungen", False),
("vacuum_test", "3.1.1", "Vakuumtest", False),
("run_1", "3.2", "Standardbeladung Testlauf 1", False),
("test_1", "3.2.1", "Test 1", False),
("run_2", "3.3", "Standardbeladung Testlauf 2", False),
("test_2", "3.3.1", "Test 2", False),
("run_3", "3.4", "Standardbeladung Testlauf 3", False),
("test_3", "3.4.1", "Test 3", False),
("results", "4", "Ergebnisse der Validierung", True),
("results_vacuum", "4.1", "Vakuumtest", False),
("results_runs", "4.1.2", "Testläufe 1 bis 3", False),
("drying", None, "Nachweis der Trocknungseigenschaften", False),
("recommendations", "4.2", "Empfehlungen und Auflagen", False),
("attachments", "5", "Anhang", True),
("winlog", "5.1", "Winlog-Auswertungen", False),
("release_docs", "5.2", "Freigabedokumentation / Routineprüfungen", False),
("indicators", "5.3", "Nachweis der umgeschlagenen Indikatoren", False),
("cycles", "6", "Programmabläufe / Zyklen", True),
("practice_certificates", "6.1", "Zertifikate Praxis", False),
("risk", "7", "Risikoeinstufung nach RKI", True),
("closing_talk", "7.1", "Abschlussgespräch", False),
("certificates", "8", "Zertifikate", True),
("calibration_certificates", "9", "Werkskalibrierzertifikate Sensoren", True),
REPORT_SECTIONS: list[dict[str, Any]] = [
{"key": "functional_qualification", "number": "1", "title": "Funktionsqualifikation (BQ)", "toc": True, "component": "static", "block_key": None},
{"key": "purpose", "number": "1.1", "title": "Anlass und Ziel der Prüfung", "toc": True, "component": "static", "block_key": "bq_goal"},
{"key": "legal", "number": "1.2", "title": "Gesetzliche Grundlagen", "toc": True, "component": "static", "block_key": "legal"},
{"key": "device", "number": "1.3", "title": "Angaben zum Gerät", "toc": True, "component": "device"},
{"key": "performance", "number": "1.4", "title": "Leistungsüberprüfung", "toc": True, "component": "static", "block_key": "performance"},
{"key": "performance_checklist", "number": "1.5", "title": "Checkliste Leistungsanforderung", "toc": True, "component": "checklist"},
{"key": "documentation", "number": "1.6", "title": "Dokumentation / Kontrolle", "toc": True, "component": "static"},
{"key": "work_instructions", "number": "1.7", "title": "Arbeitsanweisungen", "toc": True, "component": "static"},
{"key": "environment", "number": "1.8", "title": "Umgebungsbedingungen", "toc": True, "component": "environment"},
{"key": "batch_control", "number": "1.9", "title": "Chargenkontrolle", "toc": True, "component": "static"},
{"key": "programs", "number": "1.10", "title": "Beschreibung der verwendeten Programme", "toc": True, "component": "programs"},
{"key": "loading", "number": "1.11", "title": "Beladungsbeschreibungen", "toc": True, "component": "loading"},
{"key": "reference_load", "number": "1.12", "title": "Referenzbeladung Sterilisator bei Validierung", "toc": True, "component": "static"},
{"key": "equipment_root", "number": "2", "title": "Eingesetzte Prüfmittel", "toc": True, "component": "static"},
{"key": "measurement_devices", "number": "2.1", "title": "Beschreibung der Messgeräte", "toc": True, "component": "equipment"},
{"key": "thermo_equipment", "number": "2.2", "title": "Prüfmittel zur thermoelektrischen Untersuchung", "toc": True, "component": "static"},
{"key": "configuration", "number": "2.3", "title": "Prüfkonfiguration", "toc": True, "component": "static"},
{"key": "lq", "number": "3", "title": "Leistungsqualifikation (LQ)", "toc": True, "component": "static"},
{"key": "thermo_tests", "number": "3.1", "title": "Leistungsbeurteilung Thermoelektrische Prüfungen", "toc": True, "component": "static"},
{"key": "vacuum_test", "number": "3.1.1", "title": "Vakuumtest", "toc": True, "component": "measurements"},
{"key": "run_1", "number": "3.2", "title": "Standardbeladung (1. Durchlauf)", "toc": True, "component": "static"},
{"key": "test_1", "number": "3.2.1", "title": "Test 1", "toc": True, "component": "static"},
{"key": "run_2", "number": "3.3", "title": "Standardbeladung (2. Durchlauf)", "toc": True, "component": "static"},
{"key": "test_2", "number": "3.3.1", "title": "Test 2", "toc": True, "component": "static"},
{"key": "run_3", "number": "3.4", "title": "Standardbeladung (3. Durchlauf)", "toc": True, "component": "static"},
{"key": "test_3", "number": "3.4.1", "title": "Test 3", "toc": True, "component": "static"},
{"key": "results", "number": "4", "title": "Ergebnisse der Validierung", "toc": True, "component": "static"},
{"key": "results_vacuum", "number": "4.1", "title": "Vakuumtest", "toc": True, "component": "static"},
{"key": "results_runs", "number": "4.1.2", "title": "Testläufe 1 bis 3 Programm 134 °C B", "toc": True, "component": "static"},
{"key": "drying", "number": None, "title": "Nachweis der Trocknungseigenschaften Testläufe 1 bis 3", "toc": True, "component": "drying"},
{"key": "recommendations", "number": "4.2", "title": "Empfehlungen und Auflagen", "toc": True, "component": "recommendations"},
{"key": "appendix", "number": "5", "title": "Anhang", "toc": True, "component": "static"},
{"key": "winlog", "number": "5.1", "title": "Vakuumtest / Winlog-Auswertungen", "toc": True, "component": "attachments"},
{"key": "bd_empty_chamber", "number": None, "title": "Testlauf Leerkammerprofil / Bowie-Dick", "toc": True, "component": "static"},
{"key": "batch_release_docs", "number": None, "title": "Chargen- und Freigabedokumentation", "toc": True, "component": "static"},
{"key": "release_docs", "number": "5.2", "title": "Freigabedokumentation / Routineprüfungen", "toc": True, "component": "static"},
{"key": "indicators", "number": "5.3", "title": "Nachweis der umgeschlagenen Indikatoren", "toc": True, "component": "static"},
{"key": "cycles", "number": "6", "title": "Programmabläufe / Zyklen des Sterilisators", "toc": True, "component": "static"},
{"key": "practice_certificates", "number": "6.1", "title": "Zertifikate Praxis", "toc": True, "component": "static"},
{"key": "risk", "number": "7", "title": "Risikoeinstufung der Praxis nach RKI", "toc": True, "component": "static"},
{"key": "closing_talk", "number": "7.1", "title": "Abschlussgespräch zur Validierung", "toc": True, "component": "static"},
{"key": "certificates", "number": "8", "title": "Zertifikate", "toc": True, "component": "static"},
{"key": "calibration_certificates", "number": "9", "title": "Werkskalibrierzertifikate Sensoren", "toc": True, "component": "static"},
]
TEXT_BLOCK_HEADINGS = {
@ -252,15 +254,15 @@ class ReportTemplateService:
def _create_children(self, template: ReportTemplate) -> None:
paragraphs = self._read_docx_paragraphs()
blocks = self._extract_blocks(paragraphs)
for index, (section_key, number, title, page_break) in enumerate(SECTION_DEFINITIONS, start=1):
for index, section in enumerate(REPORT_SECTIONS, start=1):
self.session.add(
ReportSection(
template_id=template.id,
section_key=section_key,
number=number,
title=title,
section_key=str(section["key"]),
number=section["number"],
title=str(section["title"]),
order_index=index,
page_break_before=page_break,
page_break_before=section["number"] in {"1", "2", "3", "4", "5", "6", "7", "8", "9"},
active=True,
)
)

View file

@ -1,16 +1,19 @@
@page {
size: A4;
margin: 34mm 18mm 24mm 18mm;
@top-left { content: element(report-header); }
@bottom-left { content: element(report-footer); }
}
@page:first {
@page cover {
margin: 20mm 18mm 20mm 18mm;
@top-left { content: ""; }
@bottom-left { content: ""; }
}
@page report {
margin: 34mm 18mm 24mm 18mm;
@top-left { content: element(report-header); }
@bottom-left { content: element(report-footer); }
}
* {
box-sizing: border-box;
}
@ -27,6 +30,7 @@ body {
}
.report-meta {
display: none;
string-set: report-number attr(data-report-number), report-version attr(data-report-version), report-date attr(data-report-date);
}
@ -130,11 +134,16 @@ body {
}
.cover-page {
page: cover;
min-height: 245mm;
display: flex;
flex-direction: column;
justify-content: flex-start;
page-break-after: always;
break-after: page;
}
.report-content {
page: report;
}
.cover-top {
@ -207,6 +216,12 @@ h2 {
line-height: 1.15;
margin: 0 0 7mm 0;
padding-bottom: 4mm;
bookmark-level: none;
}
h2[data-bookmark-label] {
bookmark-label: attr(data-bookmark-label);
bookmark-level: 1;
}
.chapter + .chapter {
@ -286,9 +301,26 @@ dd {
.toc-list a {
color: #2E3B40;
display: grid;
gap: 4mm;
grid-template-columns: 1fr 14mm;
text-decoration: none;
}
.toc-level-2 {
padding-left: 6mm;
}
.toc-level-3,
.toc-level-4 {
padding-left: 12mm;
}
.toc-list a::after {
content: target-counter(attr(href), page);
text-align: right;
}
.figure-grid {
display: grid;
gap: 8mm;

View file

@ -38,7 +38,8 @@ def render_document(context: ReportContext, chapters: list[str]) -> str:
css = (Path(__file__).resolve().parent / "report.css").read_text(encoding="utf-8")
logo_uri = schubamed_logo_uri()
title = f"Validierungsbericht {context.validation.report_number}"
chapter_markup = "\n".join(chapters)
cover_markup = chapters[0] if chapters else ""
content_markup = "\n".join(chapters[1:])
return f"""<!doctype html>
<html lang="de">
<head>
@ -49,9 +50,12 @@ def render_document(context: ReportContext, chapters: list[str]) -> str:
</head>
<body>
<article class="report-document">
<div class="report-meta" data-report-number="{text(context.validation.report_number)}" data-report-version="{text(context.validation.version)}" data-report-date="{text(context.validation.updated_at)}"></div>
{render_report_chrome(context, logo_uri)}
{chapter_markup}
{cover_markup}
<section class="report-content">
<div class="report-meta" data-report-number="{text(context.validation.report_number)}" data-report-version="{text(context.validation.version)}" data-report-date="{text(context.validation.updated_at)}"></div>
{render_report_chrome(context, logo_uri)}
{content_markup}
</section>
</article>
</body>
</html>"""