beta1.5 Release #23

Merged
niklas.ortmann merged 3 commits from dev into main 2026-10-17 15:59:27 +02:00
9 changed files with 539 additions and 98 deletions
+6 -6
View File
@@ -7,7 +7,7 @@ Die App ermöglicht die Verwaltung von Patienten und Wunddokumentationen,
unterstützt Fotos und digitale Unterschriften und erzeugt aus
abgeschlossenen Dokumentationen PDF-Dateien.
> **Projektstatus:** beta1.1
> **Projektstatus:** beta1.4
------------------------------------------------------------------------
@@ -398,12 +398,12 @@ werden kann.
### Phase 6 -- Qualität und Stabilität
- [ ] Unit Tests
- [ ] Room Tests
- [ ] UI Tests
- [ ] Fehlerbehandlung
- [x] Unit Tests
- [x] Room Tests
- [x] UI Tests
- [x] Fehlerbehandlung
- [ ] Tablet-Optimierung
- [ ] Datenschutz-Review
- [x] Datenschutz-Review
### Phase 7 -- Multi-Device-Synchronisation
@@ -28,11 +28,11 @@ import de.medapp.wunddoku.domain.model.InputSpec
* Abschnitt 2.3b.
*
* [CATALOG_VERSION] wird beim Erstellen einer neuen Dokumentation als rein informatives
* Metadatum in Documentation.catalogVersion übernommen. Auf 5 erhöht (Decubitus als einfache Checkbox, Umbau "Empfohlene Wundversorgung").
* Metadatum in Documentation.catalogVersion übernommen. Auf 6 erhöht (Wundart/Wundgrund geteilt, Wundrandschutz neu geordnet, Granudacyn und Freitext entkoppelt).
*/
object DocumentationCatalog {
const val CATALOG_VERSION: Int = 5
const val CATALOG_VERSION: Int = 6
/**
* Label der automatisch an jeden Block angehängten Freitext-Option. Zentral hier definiert,
@@ -65,30 +65,31 @@ object DocumentationCatalog {
CheckItemDefinition(id = "wundart_ulcus_cruris_arteriosum", label = "Ulcus Cruris art.", order = 6),
CheckItemDefinition(id = "wundart_ulcus_cruris_venosum_arteriosum", label = "Ulcus Cruris ven.-art.", order = 7),
CheckItemDefinition(id = "wundart_diabetisches_fusssyndrom", label = "Diab. Fuß Syndrom", order = 8),
CheckItemDefinition(id = "wundart_post_operative_wundheilungsstoerung", label = "Post-operative Wundheilungsstörung / Dehiszenz", order = 9),
CheckItemDefinition(id = "wundart_schuerfwunde", label = "Schürfwunde", order = 10),
CheckItemDefinition(id = "wundart_intertrigo", label = "Intertrigo / Wunde nach Intertrigo", order = 11),
CheckItemDefinition(id = "wundart_iad", label = "IAD (Inkontinenz-assoziierte Dermatitis) / Wunde nach IAD", order = 12),
CheckItemDefinition(id = "wundart_amputationswunde", label = "Amputationswunde", order = 13),
CheckItemDefinition(id = "wundart_spalthautentnahmestelle", label = "Spalthautentnahmestelle", order = 14),
CheckItemDefinition(id = "wundart_post_operative_wundheilungsstoerung", label = "Post-operative Wundheilungsstörung", order = 9),
CheckItemDefinition(id = "wundart_dehiszenz", label = "Dehiszenz", order = 10),
CheckItemDefinition(id = "wundart_schuerfwunde", label = "Schürfwunde", order = 11),
CheckItemDefinition(id = "wundart_intertrigo", label = "Intertrigo / Wunde nach Intertrigo", order = 12),
CheckItemDefinition(id = "wundart_iad", label = "IAD (Inkontinenz-assoziierte Dermatitis) / Wunde nach IAD", order = 13),
CheckItemDefinition(id = "wundart_amputationswunde", label = "Amputationswunde", order = 14),
CheckItemDefinition(id = "wundart_spalthautentnahmestelle", label = "Spalthautentnahmestelle", order = 15),
CheckItemDefinition(
id = "wundart_transplantatverlust",
label = "Wunde nach Transplantatverlust",
order = 15,
order = 16,
inputSpec = InputSpec(
pdfTemplate = "Wunde nach Transplantatverlust, Spalthautdeckung ca. {0}%",
fields = listOf(InputFieldDef.Number(label = "Spalthautdeckung ca. (%)", maxLength = 3)),
),
),
CheckItemDefinition(id = "wundart_sekundaer_heilende_wunde", label = "Sekundär heilende Wunde", order = 16),
CheckItemDefinition(id = "wundart_tumorwunde", label = "Tumorwunde", order = 17),
CheckItemDefinition(id = "wundart_wunde_nach_abszess", label = "Wunde nach Abszess", order = 18),
CheckItemDefinition(id = "wundart_verbrennung_verbruehung", label = "Verbrennung / Verbrühung", order = 19),
CheckItemDefinition(id = "wundart_platzwunde", label = "Platzwunde", order = 20),
CheckItemDefinition(id = "wundart_schnittwunde", label = "Schnittwunde", order = 21),
CheckItemDefinition(id = "wundart_bisswunde", label = "Bisswunde", order = 22),
CheckItemDefinition(id = "wundart_chemisch_veraetzung", label = "Chemisch hervorgerufene Wunde / Verätzung", order = 23),
CheckItemDefinition(id = "wundart_strahlenschaden", label = "Strahlenschaden", order = 24),
CheckItemDefinition(id = "wundart_sekundaer_heilende_wunde", label = "Sekundär heilende Wunde", order = 17),
CheckItemDefinition(id = "wundart_tumorwunde", label = "Tumorwunde", order = 18),
CheckItemDefinition(id = "wundart_wunde_nach_abszess", label = "Wunde nach Abszess", order = 19),
CheckItemDefinition(id = "wundart_verbrennung_verbruehung", label = "Verbrennung / Verbrühung", order = 20),
CheckItemDefinition(id = "wundart_platzwunde", label = "Platzwunde", order = 21),
CheckItemDefinition(id = "wundart_schnittwunde", label = "Schnittwunde", order = 22),
CheckItemDefinition(id = "wundart_bisswunde", label = "Bisswunde", order = 23),
CheckItemDefinition(id = "wundart_chemisch_veraetzung", label = "Chemisch hervorgerufene Wunde / Verätzung", order = 24),
CheckItemDefinition(id = "wundart_strahlenschaden", label = "Strahlenschaden", order = 25),
),
),
BlockDefinition(
@@ -165,24 +166,25 @@ object DocumentationCatalog {
items = listOf(
CheckItemDefinition(id = "wundgrund_wie_ausgestanzt", label = "Wie ausgestanzt", order = 1),
CheckItemDefinition(id = "wundgrund_kraterfoermig", label = "Kraterförmig", order = 2),
CheckItemDefinition(id = "wundgrund_unregelmaessig_landkartenartig", label = "Unregelmäßig (landkartenartig/amorph)", order = 3),
CheckItemDefinition(id = "wundgrund_linear_spaltfoermig", label = "Linear/spaltförmig", order = 4),
CheckItemDefinition(id = "wundgrund_rund", label = "Rund", order = 5),
CheckItemDefinition(id = "wundgrund_oval_elliptisch", label = "Oval/elliptisch", order = 6),
CheckItemDefinition(id = "wundgrund_quadrangulaer", label = "Quadrangulär", order = 7),
CheckItemDefinition(id = "wundgrund_eben", label = "Eben", order = 8),
CheckItemDefinition(id = "wundgrund_auf_niveau", label = "Auf Niveau", order = 9),
CheckItemDefinition(id = "wundgrund_ueber_niveau", label = "Über Niveau", order = 10),
CheckItemDefinition(id = "wundgrund_glaenzend", label = "Glänzend", order = 11),
CheckItemDefinition(id = "wundgrund_grob", label = "Grob", order = 12),
CheckItemDefinition(id = "wundgrund_fein", label = "Fein", order = 13),
CheckItemDefinition(id = "wundgrund_glatt", label = "Glatt", order = 14),
CheckItemDefinition(id = "wundgrund_rot", label = "Rot", order = 15),
CheckItemDefinition(id = "wundgrund_blass", label = "Blass", order = 16),
CheckItemDefinition(id = "wundgrund_knochen_sehnen", label = "Knochen/Sehnen", order = 17),
CheckItemDefinition(id = "wundgrund_haemorrhagie", label = "Hämorrhagie", order = 18),
CheckItemDefinition(id = "wundgrund_nicht_kaum_einsehbar", label = "Nicht/kaum einsehbar", order = 19),
CheckItemDefinition(id = "wundgrund_erscheinung_erscheinung", label = "Erscheinung", order = 20, isFreeText = true),
CheckItemDefinition(id = "wundgrund_unregelmaessig", label = "Unregelmäßig", order = 3),
CheckItemDefinition(id = "wundgrund_landkartenartig_amorph", label = "Landkartenartig/amorph", order = 4),
CheckItemDefinition(id = "wundgrund_linear_spaltfoermig", label = "Linear/spaltförmig", order = 5),
CheckItemDefinition(id = "wundgrund_rund", label = "Rund", order = 6),
CheckItemDefinition(id = "wundgrund_oval_elliptisch", label = "Oval/elliptisch", order = 7),
CheckItemDefinition(id = "wundgrund_quadrangulaer", label = "Quadrangulär", order = 8),
CheckItemDefinition(id = "wundgrund_eben", label = "Eben", order = 9),
CheckItemDefinition(id = "wundgrund_auf_niveau", label = "Auf Niveau", order = 10),
CheckItemDefinition(id = "wundgrund_ueber_niveau", label = "Über Niveau", order = 11),
CheckItemDefinition(id = "wundgrund_glaenzend", label = "Glänzend", order = 12),
CheckItemDefinition(id = "wundgrund_grob", label = "Grob", order = 13),
CheckItemDefinition(id = "wundgrund_fein", label = "Fein", order = 14),
CheckItemDefinition(id = "wundgrund_glatt", label = "Glatt", order = 15),
CheckItemDefinition(id = "wundgrund_rot", label = "Rot", order = 16),
CheckItemDefinition(id = "wundgrund_blass", label = "Blass", order = 17),
CheckItemDefinition(id = "wundgrund_knochen_sehnen", label = "Knochen/Sehnen", order = 18),
CheckItemDefinition(id = "wundgrund_haemorrhagie", label = "Hämorrhagie", order = 19),
CheckItemDefinition(id = "wundgrund_nicht_kaum_einsehbar", label = "Nicht/kaum einsehbar", order = 20),
CheckItemDefinition(id = "wundgrund_erscheinung_erscheinung", label = "Erscheinung", order = 21, isFreeText = true),
),
),
BlockDefinition(
@@ -498,61 +500,61 @@ object DocumentationCatalog {
CheckItemDefinition(id = "empfohlene_wundversorgung_sterile_kompresse", label = "Sterile Kompresse", order = 2),
CheckItemDefinition(id = "empfohlene_wundversorgung_nacl", label = "NaCl 0,9%", order = 3),
CheckItemDefinition(id = "empfohlene_wundversorgung_octenisept", label = "Octenisept", order = 4),
// Granudacyn: Checkbox + direkt folgendes Freitextfeld, das nur bei angehakter
// Checkbox aktiv ist (= vorhandenes isFreeText-Verhalten, s. FreeTextCheckItemRow).
CheckItemDefinition(id = "empfohlene_wundversorgung_granudacyn", label = "Granudacyn", order = 5, isFreeText = true),
// Granudacyn: einfache Checkbox. Der Freitext ist ein eigener, separat
// anzuhakender Punkt direkt darunter (Bezeichnung "Freitext" wird in PDF/JPG
// und, sobald angehakt, auch in der App nicht angezeigt).
CheckItemDefinition(id = "empfohlene_wundversorgung_granudacyn", label = "Granudacyn", order = 5),
CheckItemDefinition(id = "empfohlene_wundversorgung_granudacyn_freitext", label = FREE_TEXT_LABEL, order = 6, isFreeText = true),
CheckItemDefinition(
id = "empfohlene_wundversorgung_getraenkte_kompresse",
label = "Getränkte Kompresse",
order = 6,
order = 7,
inputSpec = InputSpec(
pdfTemplate = "Getränkte Kompresse für ca. {0} Min. auflegen",
fields = listOf(InputFieldDef.Choice(label = "Minuten", options = MINUTE_1_TO_15_OPTIONS)),
),
),
CheckItemDefinition(id = "empfohlene_wundversorgung_tupfend", label = "Tupfend", order = 7),
CheckItemDefinition(id = "empfohlene_wundversorgung_wischend", label = "Wischend", order = 8),
CheckItemDefinition(id = "empfohlene_wundversorgung_nachspuelen_nacl", label = "Nachspülen mit NaCl 0,9%", order = 9),
CheckItemDefinition(id = "empfohlene_wundversorgung_umgebung_trocken_tupfen", label = "Umgebung trocken tupfen", order = 10),
CheckItemDefinition(id = "empfohlene_wundversorgung_primaer_abdecken_austamponieren", label = "Primär Wunde abdecken/austamponieren", order = 11),
CheckItemDefinition(id = "empfohlene_wundversorgung_ueber_wundrand_hinaus", label = "Über Wundrand hinaus", order = 12),
CheckItemDefinition(id = "empfohlene_wundversorgung_nicht_ueber_wundrand_mit", label = "Nicht über Wundrand mit", order = 13),
CheckItemDefinition(id = "empfohlene_wundversorgung_nicht_ueber_wundrand_mit_freitext", label = FREE_TEXT_LABEL, order = 14, isFreeText = true),
CheckItemDefinition(id = "empfohlene_wundversorgung_hinweis_wundrandschutz", label = "Wundrandschutz:", order = 15, isStaticHint = true),
CheckItemDefinition(id = "empfohlene_wundversorgung_wundrandschutz_ja", label = "Ja", order = 16),
CheckItemDefinition(id = "empfohlene_wundversorgung_wundrandschutz_nein", label = "Nein", order = 17),
CheckItemDefinition(id = "empfohlene_wundversorgung_wundrandschutz_freitext", label = FREE_TEXT_LABEL, order = 18, isFreeText = true),
CheckItemDefinition(id = "empfohlene_wundversorgung_tupfend", label = "Tupfend", order = 8),
CheckItemDefinition(id = "empfohlene_wundversorgung_wischend", label = "Wischend", order = 9),
CheckItemDefinition(id = "empfohlene_wundversorgung_nachspuelen_nacl", label = "Nachspülen mit NaCl 0,9%", order = 10),
CheckItemDefinition(id = "empfohlene_wundversorgung_umgebung_trocken_tupfen", label = "Umgebung trocken tupfen", order = 11),
CheckItemDefinition(id = "empfohlene_wundversorgung_primaer_abdecken_austamponieren", label = "Primär Wunde abdecken/austamponieren", order = 12),
CheckItemDefinition(id = "empfohlene_wundversorgung_ueber_wundrand_hinaus", label = "Über Wundrand hinaus", order = 13),
CheckItemDefinition(id = "empfohlene_wundversorgung_nicht_ueber_wundrand_mit", label = "Nicht über Wundrand mit", order = 14),
CheckItemDefinition(id = "empfohlene_wundversorgung_nicht_ueber_wundrand_mit_freitext", label = FREE_TEXT_LABEL, order = 15, isFreeText = true),
// Wundrandschutz: EINE Checkbox mit eigenem Freitextfeld (kein Ja/Nein mehr).
CheckItemDefinition(id = "empfohlene_wundversorgung_wundrandschutz", label = "Wundrandschutz", order = 16, isFreeText = true),
// "Wie oft pro Woche": eigene Checkbox; die Auswahl (1-7x) ist erst nach dem
// Anhaken aktiv (InputSpec.requiresOwnCheckbox).
CheckItemDefinition(
id = "empfohlene_wundversorgung_wundrandschutz_tage_pro_woche",
label = "Wie oft pro Woche",
order = 19,
order = 17,
inputSpec = InputSpec(
pdfTemplate = "{0}x pro Woche",
fields = listOf(InputFieldDef.Choice(label = "Tage pro Woche", options = WEEKDAY_COUNT_OPTIONS)),
requiresOwnCheckbox = true,
),
),
CheckItemDefinition(id = "empfohlene_wundversorgung_hinweis_sekundaere_wundauflage", label = "Sekundäre Wundauflage:", order = 20, isStaticHint = true),
CheckItemDefinition(id = "empfohlene_wundversorgung_sekundaer_sterile_kompresse", label = "Sterile Kompresse", order = 21),
CheckItemDefinition(id = "empfohlene_wundversorgung_saugkompresse", label = "Saugkompresse", order = 22),
CheckItemDefinition(id = "empfohlene_wundversorgung_sonstige_wundauflage", label = "Sonstiges (Wundauflage)", order = 23, isFreeText = true),
CheckItemDefinition(id = "empfohlene_wundversorgung_hinweis_fixieren_mit", label = "Fixieren mit:", order = 24, isStaticHint = true),
CheckItemDefinition(id = "empfohlene_wundversorgung_mullbinde", label = "Mullbinde", order = 25),
CheckItemDefinition(id = "empfohlene_wundversorgung_klebevlies", label = "Klebevlies", order = 26),
CheckItemDefinition(id = "empfohlene_wundversorgung_folienverband", label = "Folienverband", order = 27),
CheckItemDefinition(id = "empfohlene_wundversorgung_schlauchverband", label = "Schlauchverband", order = 28),
CheckItemDefinition(id = "empfohlene_wundversorgung_kohaesive_fixierbinde", label = "Kohäsive Fixierbinde", order = 29),
CheckItemDefinition(id = "empfohlene_wundversorgung_kohaesive_fixierbinde_freitext", label = FREE_TEXT_LABEL, order = 30, isFreeText = true),
CheckItemDefinition(id = "empfohlene_wundversorgung_kompressionsverband_system", label = "Kompressionsverband / System", order = 31),
CheckItemDefinition(id = "empfohlene_wundversorgung_kompressionsverband_system_freitext", label = FREE_TEXT_LABEL, order = 32, isFreeText = true),
CheckItemDefinition(id = "empfohlene_wundversorgung_wechselintervall", label = "Wechselintervall", order = 33, isFreeText = true),
CheckItemDefinition(id = "empfohlene_wundversorgung_hinweis_bedarf", label = "+ bei Bedarf", order = 34, isStaticHint = true),
CheckItemDefinition(id = "empfohlene_wundversorgung_hinweis_sekundaere_wundauflage", label = "Sekundäre Wundauflage:", order = 18, isStaticHint = true),
CheckItemDefinition(id = "empfohlene_wundversorgung_sekundaer_sterile_kompresse", label = "Sterile Kompresse", order = 19),
CheckItemDefinition(id = "empfohlene_wundversorgung_saugkompresse", label = "Saugkompresse", order = 20),
CheckItemDefinition(id = "empfohlene_wundversorgung_sonstige_wundauflage", label = "Sonstiges (Wundauflage)", order = 21, isFreeText = true),
CheckItemDefinition(id = "empfohlene_wundversorgung_hinweis_fixieren_mit", label = "Fixieren mit:", order = 22, isStaticHint = true),
CheckItemDefinition(id = "empfohlene_wundversorgung_mullbinde", label = "Mullbinde", order = 23),
CheckItemDefinition(id = "empfohlene_wundversorgung_klebevlies", label = "Klebevlies", order = 24),
CheckItemDefinition(id = "empfohlene_wundversorgung_folienverband", label = "Folienverband", order = 25),
CheckItemDefinition(id = "empfohlene_wundversorgung_schlauchverband", label = "Schlauchverband", order = 26),
CheckItemDefinition(id = "empfohlene_wundversorgung_kohaesive_fixierbinde", label = "Kohäsive Fixierbinde", order = 27),
CheckItemDefinition(id = "empfohlene_wundversorgung_kohaesive_fixierbinde_freitext", label = FREE_TEXT_LABEL, order = 28, isFreeText = true),
CheckItemDefinition(id = "empfohlene_wundversorgung_kompressionsverband_system", label = "Kompressionsverband / System", order = 29),
CheckItemDefinition(id = "empfohlene_wundversorgung_kompressionsverband_system_freitext", label = FREE_TEXT_LABEL, order = 30, isFreeText = true),
CheckItemDefinition(id = "empfohlene_wundversorgung_wechselintervall", label = "Wechselintervall", order = 31, isFreeText = true),
CheckItemDefinition(id = "empfohlene_wundversorgung_hinweis_bedarf", label = "+ bei Bedarf", order = 32, isStaticHint = true),
CheckItemDefinition(
id = "empfohlene_wundversorgung_hinweis_abklaerung",
label = "Generell: bei akuter Verschlechterung sofort ärztliche Abklärung",
order = 35,
order = 33,
isStaticHint = true,
),
),
@@ -571,9 +573,8 @@ object DocumentationCatalog {
CheckItemDefinition(id = "ziel_abheilung", label = "Abheilung", order = 7),
CheckItemDefinition(id = "ziel_geruchsreduzierung", label = "Geruchsreduzierung", order = 8),
CheckItemDefinition(id = "ziel_einfache_durchfuehrung", label = "Einfache Durchführung", order = 9),
CheckItemDefinition(id = "ziel_wundrandschutz", label = "Wundrandschutz", order = 10),
CheckItemDefinition(id = "ziel_aufbruch_vorhandenes_material", label = "Aufbruch des vorhandenen Materials", order = 11),
CheckItemDefinition(id = "ziel_selbstversorgung_foerdern", label = "Selbstversorgung der Wunde durch Patient/Angehörige unterstützen/fördern", order = 12),
CheckItemDefinition(id = "ziel_aufbruch_vorhandenes_material", label = "Aufbruch des vorhandenen Materials", order = 10),
CheckItemDefinition(id = "ziel_selbstversorgung_foerdern", label = "Selbstversorgung der Wunde durch Patient/Angehörige unterstützen/fördern", order = 11),
),
),
BlockDefinition(
@@ -2,10 +2,13 @@ package de.medapp.wunddoku.pdf
import android.content.Context
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.Typeface
import android.graphics.pdf.PdfDocument
import android.text.Layout
import android.text.StaticLayout
import android.text.TextPaint
import de.medapp.wunddoku.domain.error.FailureCategory
import de.medapp.wunddoku.domain.model.InputSpecCodec
@@ -66,6 +69,8 @@ class AndroidPdfDocumentExporter(
private const val ITEM_TEXT_SIZE = 11f
private const val FREE_TEXT_TEXT_SIZE = 10.5f
private const val FREE_TEXT_INDENT = 20f
private const val RUNNING_HEADER_TEXT_SIZE = 9.5f
private const val RUNNING_HEADER_SPACING_AFTER = 10f
// PDF-003: Signaturbereich. Fachlich handschriftliche Unterschriften, keine
// kryptographische PDF-Signatur (s. Aufgabenstellung Abschnitt 5).
@@ -160,6 +165,9 @@ class AndroidPdfDocumentExporter(
marginRight = MARGIN,
marginTop = MARGIN,
marginBottom = MARGIN,
pageHeaderDrawer = PageHeaderDrawer { canvas, pageNumber, left, top, width ->
drawRunningHeader(canvas, pageNumber, left, top, width, model, paints)
},
)
drawHeader(cursor, model, paints)
@@ -243,6 +251,32 @@ class AndroidPdfDocumentExporter(
textSize = FREE_TEXT_TEXT_SIZE
color = Color.DKGRAY
}
// Zwischenüberschriften innerhalb eines Blocks: kursiv + unterstrichen.
val itemSubheading = TextPaint().apply {
isAntiAlias = true
textSize = ITEM_TEXT_SIZE
typeface = Typeface.create(Typeface.DEFAULT, Typeface.ITALIC)
isUnderlineText = true
color = Color.BLACK
}
// Wichtiger Hinweis: fett + unterstrichen.
val itemWarning = TextPaint().apply {
isAntiAlias = true
textSize = ITEM_TEXT_SIZE
typeface = Typeface.create(Typeface.DEFAULT, Typeface.BOLD)
isUnderlineText = true
color = Color.BLACK
}
// Seitenkopf ab Seite 2 (Patient + Erstelldatum).
val runningHeader = TextPaint().apply {
isAntiAlias = true
textSize = RUNNING_HEADER_TEXT_SIZE
color = Color.DKGRAY
}
val runningHeaderLine = Paint().apply {
color = Color.LTGRAY
strokeWidth = 1f
}
// PDF-003: Signaturbereich.
val signatureLabel = TextPaint().apply {
@@ -286,6 +320,37 @@ class AndroidPdfDocumentExporter(
cursor.drawDivider(spacingAfter = 12f)
}
/**
* Seitenkopf für alle Seiten AB Seite 2 (Änderungswunsch: auf jeder Seite müssen Name des
* Patienten und Erstellungsdatum stehen): Seite 1 trägt bereits den großen Dokumentkopf
* ([drawHeader]). Gilt auch für Unterschriften- und Fotoseiten. Verwendet ausschließlich
* Snapshot-Daten aus dem [PdfExportModel]. Liefert die belegte Höhe zurück.
*/
private fun drawRunningHeader(
canvas: Canvas,
pageNumber: Int,
left: Float,
top: Float,
width: Float,
model: PdfExportModel,
paints: ExportPaints,
): Float {
if (pageNumber <= 1) return 0f
val text = "Patient: ${model.patientFullName}, geb. ${model.patientBirthDate.format(HEADER_DATE_FORMATTER)}" +
" · ${model.patientDisplayAddress} · Erstellt am: ${model.createdAt.format(HEADER_DATE_FORMATTER)}"
val layout = StaticLayout.Builder.obtain(text, 0, text.length, paints.runningHeader, width.toInt().coerceAtLeast(1))
.setAlignment(Layout.Alignment.ALIGN_NORMAL)
.setIncludePad(false)
.build()
canvas.save()
canvas.translate(left, top)
layout.draw(canvas)
canvas.restore()
val lineY = top + layout.height + 4f
canvas.drawLine(left, lineY, left + width, lineY, paints.runningHeaderLine)
return layout.height + 4f + 1f + RUNNING_HEADER_SPACING_AFTER
}
/** Überschriften erscheinen im PDF immer mit abschließendem Doppelpunkt (kein doppelter
* Doppelpunkt, falls der Titel bereits mit einem endet). */
private fun headingText(title: String): String {
@@ -294,6 +359,8 @@ class AndroidPdfDocumentExporter(
}
private fun drawBlock(cursor: PdfPageCursor, block: PdfExportBlock, paints: ExportPaints) {
// Änderungswunsch: "Empfohlene Wundversorgung" beginnt immer auf einer eigenen Seite.
if (block.startsOnNewPage) cursor.startNewPageUnlessEmpty()
cursor.drawText(headingText(block.title), paints.blockTitle, spacingAfter = 6f)
for (item in block.items) {
@@ -330,14 +397,37 @@ class AndroidPdfDocumentExporter(
return
}
// Generische "Freitext"-Einträge (Änderungswunsch): Bezeichnung wird NICHT gedruckt, nur
// der eingegebene Text (eingerückt unter dem vorhergehenden Eintrag). Ein angehakter,
// aber leerer Freitext erzeugt keine Ausgabe.
if (!item.showLabel) {
if (item.freeTextValue.isNotBlank()) {
cursor.drawText(
text = item.freeTextValue,
paint = paints.item,
indent = FREE_TEXT_INDENT,
spacingAfter = 6f,
)
}
return
}
// Die Freitext-Option bekommt einen abschließenden Doppelpunkt, sofern tatsächlich Text
// folgt (z. B. "Granudacyn" ohne Text bleibt ohne hängenden Doppelpunkt), normale
// Optionen nie.
val labelText = if (item.isFreeText && item.freeTextValue.isNotBlank()) "${item.label}:" else item.label
val baseLabel = if (item.isFreeText && item.freeTextValue.isNotBlank()) "${item.label}:" else item.label
// Änderungswunsch: Zwischenüberschriften kursiv + unterstrichen, wichtiger Hinweis fett +
// unterstrichen mit Ausrufezeichen davor und dahinter.
val (labelText, labelPaint) = when (item.style) {
PdfItemStyle.SUBHEADING -> baseLabel to paints.itemSubheading
PdfItemStyle.WARNING -> "! $baseLabel !" to paints.itemWarning
PdfItemStyle.NORMAL -> baseLabel to paints.item
}
cursor.drawText(
text = labelText,
paint = paints.item,
paint = labelPaint,
indent = ITEM_INDENT,
spacingAfter = 3f,
)
@@ -1,6 +1,9 @@
package de.medapp.wunddoku.pdf
import de.medapp.wunddoku.domain.catalog.DocumentationCatalog
import de.medapp.wunddoku.domain.model.CheckItem
import de.medapp.wunddoku.domain.model.Documentation
import de.medapp.wunddoku.domain.model.DocumentationBlock
import de.medapp.wunddoku.domain.usecase.BlockEditedPolicy
import java.time.ZoneId
@@ -27,20 +30,22 @@ object DocumentationToPdfExportModelMapper {
.sortedBy { it.order }
.map { block ->
PdfExportBlock(
definitionId = block.definitionId,
title = block.title,
// Änderungswunsch: nur angehakte Optionen erscheinen im PDF - nicht
// angehakte Checkboxen werden komplett weggelassen (nicht nur unausgefüllt
// dargestellt). Ein leeres Ergebnis ist hier ausgeschlossen: BlockEditedPolicy
startsOnNewPage = block.definitionId in BLOCKS_ON_OWN_PAGE,
// Nur angehakte Optionen erscheinen im PDF. Hinweistexte (isStaticHint) sind
// technisch immer angehakt und werden gruppenabhängig gefiltert, s.
// [exportedItems]. Ein leeres Ergebnis ist ausgeschlossen: BlockEditedPolicy
// (Filter oben) lässt nur Blöcke mit mindestens einer angehakten Option durch.
items = block.items
.filter { it.checked }
.sortedBy { it.order }
.map { item ->
items = exportedItems(block).map { item ->
PdfExportItem(
label = item.label,
isFreeText = item.isFreeText,
freeTextValue = item.freeTextValue,
inputSpec = item.inputSpec,
// Generische "Freitext"-Bezeichnung nie drucken, nur den Text.
showLabel = !(item.isFreeText && item.label == DocumentationCatalog.FREE_TEXT_LABEL),
style = styleFor(item),
)
},
)
@@ -80,4 +85,67 @@ object DocumentationToPdfExportModelMapper {
photos = photos,
)
}
/** Blöcke, die im PDF/JPG immer auf einer neuen Seite beginnen. */
private val BLOCKS_ON_OWN_PAGE: Set<String> = setOf("empfohlene_wundversorgung")
/** Zwischenüberschriften (kursiv + unterstrichen), anhand der stabilen Definitions-IDs. */
private val SUBHEADING_DEFINITION_IDS: Set<String> = setOf(
"empfohlene_wundversorgung_hinweis_reinigen_mit",
"empfohlene_wundversorgung_primaer_abdecken_austamponieren",
"empfohlene_wundversorgung_wundrandschutz",
"empfohlene_wundversorgung_hinweis_sekundaere_wundauflage",
"empfohlene_wundversorgung_hinweis_fixieren_mit",
"empfohlene_wundversorgung_wechselintervall",
)
/** Wichtige Hinweise (fett + unterstrichen, mit Ausrufezeichen). */
private val WARNING_DEFINITION_IDS: Set<String> = setOf(
"empfohlene_wundversorgung_hinweis_abklaerung",
)
private fun styleFor(item: CheckItem): PdfItemStyle = when (item.definitionId) {
in WARNING_DEFINITION_IDS -> PdfItemStyle.WARNING
in SUBHEADING_DEFINITION_IDS -> PdfItemStyle.SUBHEADING
else -> PdfItemStyle.NORMAL
}
/**
* Bestimmt die zu exportierenden Einträge eines Blocks (sortiert nach order):
* - normale Einträge: nur wenn angehakt
* - Hinweistexte (isStaticHint, technisch immer angehakt): Ein Hinweis, dem im Block
* eigene Einträge folgen (bis zum nächsten Hinweis), ist eine Gruppenüberschrift und
* wird nur gedruckt, wenn mindestens einer dieser Einträge angehakt ist (z. B. "Zu
* versorgendes Areal" bei Größe). Hinweise ohne folgende Einträge ("+ bei Bedarf",
* "Generell: ...") bleiben immer erhalten. Der automatisch angehängte generische
* Freitext eines Blocks gehört zu keiner Gruppe. Trägt der erste angehakte Eintrag der
* Gruppe dieselbe Bezeichnung wie der Hinweis, entfällt der Hinweis (keine Doppelung).
*
* Ausschließlich Snapshot-Daten, kein Zugriff auf den aktuellen Katalog.
*/
private fun exportedItems(block: DocumentationBlock): List<CheckItem> {
val sorted = block.items.sortedBy { it.order }
val genericFreeTextId = "${block.definitionId}_freetext"
val result = mutableListOf<CheckItem>()
for ((index, item) in sorted.withIndex()) {
if (!item.checked) continue
if (!item.isStaticHint) {
result.add(item)
continue
}
val groupItems = sorted
.drop(index + 1)
.takeWhile { !it.isStaticHint }
.filter { it.definitionId != genericFreeTextId }
val keep = when {
groupItems.isEmpty() -> true
else -> {
val firstChecked = groupItems.firstOrNull { it.checked }
firstChecked != null && !firstChecked.label.trim().equals(item.label.trim(), ignoreCase = true)
}
}
if (keep) result.add(item)
}
return result
}
}
@@ -27,7 +27,12 @@ data class PdfExportModel(
)
data class PdfExportBlock(
/** Stabile Definitions-ID des Blocks (z. B. "empfohlene_wundversorgung"). */
val definitionId: String = "",
val title: String,
/** `true`: der Block beginnt im PDF/JPG zwingend auf einer neuen Seite (Änderungswunsch:
* "Empfohlene Wundversorgung" auf eigener Seite). */
val startsOnNewPage: Boolean = false,
/** Bereits gefiltert (NUR angehakte Optionen, s. Mapper - Änderungswunsch) und sortiert
* (itemOrder). Nicht angehakte Optionen erscheinen im PDF überhaupt nicht mehr. */
val items: List<PdfExportItem>,
@@ -45,8 +50,23 @@ data class PdfExportItem(
val isFreeText: Boolean,
val freeTextValue: String,
val inputSpec: String = "",
/** `false`: die Bezeichnung wird nicht gedruckt, nur der eingegebene Text (generische
* "Freitext"-Einträge, Änderungswunsch: Schriftzug "Freitext" im PDF/JPG nicht anzeigen). */
val showLabel: Boolean = true,
val style: PdfItemStyle = PdfItemStyle.NORMAL,
)
/** Darstellung der Bezeichnung eines Eintrags im PDF (rein layoutbezogen). */
enum class PdfItemStyle {
NORMAL,
/** Zwischenüberschrift innerhalb eines Blocks: kursiv und unterstrichen. */
SUBHEADING,
/** Wichtiger Hinweis: fett, unterstrichen, mit Ausrufezeichen davor und dahinter. */
WARNING,
}
/**
* Minimale, für den Fotoanhang benötigte Referenz. Enthält bewusst nur [fileName] -
* `documentationId` steht bereits am [PdfExportModel] selbst zur Verfügung (nötig, um über
@@ -10,6 +10,15 @@ import android.text.Layout
import android.text.StaticLayout
import android.text.TextPaint
/**
* Zeichnet den Seitenkopf einer (neuen) Seite und liefert die dafür belegte Höhe zurück
* (0f, wenn auf dieser Seite kein Seitenkopf gezeichnet wird, z. B. Seite 1 mit dem großen
* Dokumentkopf). [left]/[top] sind die obere linke Ecke des Inhaltsbereichs, [width] dessen Breite.
*/
internal fun interface PageHeaderDrawer {
fun draw(canvas: Canvas, pageNumber: Int, left: Float, top: Float, width: Float): Float
}
/**
* Verwaltet Seiten, Zeichenfläche und die aktuelle vertikale Schreibposition ("Cursor") beim
* Erzeugen eines [PdfDocument]. Kapselt die komplette Seitenumbruch-Logik an einer Stelle, damit
@@ -33,8 +42,13 @@ internal class PdfPageCursor(
private val marginRight: Float,
private val marginTop: Float,
private val marginBottom: Float,
/** Optionaler Seitenkopf, der auf JEDER neu begonnenen Seite gezeichnet wird (Änderungswunsch:
* Patientenname und Erstellungsdatum auf jeder Seite). */
private val pageHeaderDrawer: PageHeaderDrawer? = null,
) {
private var pageNumber = 0
/** Y-Position, ab der auf der aktuellen Seite Inhalt beginnt (nach einem evtl. Seitenkopf). */
private var pageContentTop: Float = marginTop
private var currentPage: PdfDocument.Page? = null
private var canvas: Canvas? = null
private var cursorY: Float = marginTop
@@ -74,7 +88,15 @@ internal class PdfPageCursor(
val page = pdfDocument.startPage(info)
currentPage = page
canvas = page.canvas
cursorY = marginTop
val headerHeight = pageHeaderDrawer?.draw(
canvas = page.canvas,
pageNumber = pageNumber,
left = marginLeft,
top = marginTop,
width = pageWidth - marginLeft - marginRight,
) ?: 0f
pageContentTop = marginTop + headerHeight
cursorY = pageContentTop
}
private fun finishCurrentPageIfOpen() {
@@ -98,6 +120,15 @@ internal class PdfPageCursor(
startNewPage()
}
/**
* Beginnt eine neue Seite, außer die aktuelle Seite ist noch völlig leer (Cursor steht am
* Inhaltsanfang) - so entstehen keine Leerseiten, wenn ein Block, der immer auf einer
* eigenen Seite beginnt, ohnehin ganz oben stünde.
*/
fun startNewPageUnlessEmpty() {
if (currentPage == null || cursorY > pageContentTop) startNewPage()
}
/**
* Zeichnet [bitmap] proportional eingepasst (kein Zuschneiden, keine Verzerrung) horizontal
* über die volle Inhaltsbreite und vertikal zentriert im aktuell verbleibenden Platz der
@@ -85,6 +85,7 @@ import androidx.lifecycle.compose.LocalLifecycleOwner
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import de.medapp.wunddoku.R
import de.medapp.wunddoku.domain.catalog.DocumentationCatalog
import de.medapp.wunddoku.domain.model.CheckItem
import de.medapp.wunddoku.domain.model.InputFieldDef
import de.medapp.wunddoku.domain.model.InputSpecCodec
@@ -907,19 +908,25 @@ private fun FreeTextCheckItemRow(
onCheckedChanged: (checkItemStateId: String, checked: Boolean) -> Unit,
onFreeTextChanged: (checkItemStateId: String, text: String) -> Unit,
) {
// Änderungswunsch: Ist ein generischer "Freitext"-Eintrag angehakt, wird der Schriftzug
// "Freitext" nicht mehr angezeigt (nur Häkchenkästchen + Eingabefeld). Fachlich benannte
// Freitext-Einträge (z. B. "Wundrandschutz", "Wechselintervall") behalten ihre Bezeichnung.
val hideName = item.checked && item.label == DocumentationCatalog.FREE_TEXT_LABEL
val fieldLabel: (@Composable () -> Unit)? =
if (hideName) null else ({ Text(stringResource(R.string.documentation_edit_free_text_label)) })
Column {
Row(verticalAlignment = Alignment.CenterVertically) {
Checkbox(
checked = item.checked,
onCheckedChange = { checked -> onCheckedChanged(item.checkItemStateId, checked) },
)
Text(item.label)
if (!hideName) Text(item.label)
}
OutlinedTextField(
value = item.freeTextValue,
onValueChange = { text -> onFreeTextChanged(item.checkItemStateId, text) },
enabled = item.checked,
label = { Text(stringResource(R.string.documentation_edit_free_text_label)) },
label = fieldLabel,
modifier = Modifier.fillMaxWidth().padding(start = 40.dp),
minLines = 2,
)
@@ -0,0 +1,78 @@
package de.medapp.wunddoku.domain.catalog
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Test
/**
* Strukturprüfungen für den statischen Katalog sowie die fachlichen Änderungswünsche
* (Wundart/Wundgrund geteilt, Wundrandschutz, Granudacyn).
*/
class DocumentationCatalogTest {
private fun items(blockId: String) = DocumentationCatalog.findBlock(blockId)!!.items
@Test
fun alle_checkitem_ids_sind_katalogweit_eindeutig() {
val ids = DocumentationCatalog.blocks.flatMap { b -> b.items.map { it.id } }
assertEquals(ids.size, ids.toSet().size)
}
@Test
fun order_ist_je_block_eindeutig_und_aufsteigend() {
for (block in DocumentationCatalog.blocks) {
val orders = block.items.map { it.order }
assertEquals("Block ${block.id}", orders.sorted(), orders)
assertEquals("Block ${block.id}", orders.size, orders.toSet().size)
}
}
@Test
fun wundart_postoperativ_und_dehiszenz_sind_getrennte_punkte() {
val wundart = items("wundart")
assertEquals("Post-operative Wundheilungsstörung", wundart.single { it.id == "wundart_post_operative_wundheilungsstoerung" }.label)
assertEquals("Dehiszenz", wundart.single { it.id == "wundart_dehiszenz" }.label)
assertTrue(wundart.none { it.label.contains("/ Dehiszenz") })
}
@Test
fun wundgrund_unregelmaessig_und_landkartenartig_sind_getrennte_punkte() {
val wundgrund = items("wundgrund_erscheinung")
assertEquals("Unregelmäßig", wundgrund.single { it.id == "wundgrund_unregelmaessig" }.label)
assertEquals("Landkartenartig/amorph", wundgrund.single { it.id == "wundgrund_landkartenartig_amorph" }.label)
assertNull(wundgrund.firstOrNull { it.id == "wundgrund_unregelmaessig_landkartenartig" })
}
@Test
fun wundrandschutz_ist_aus_ziel_entfernt() {
assertTrue(items("ziel").none { it.label.contains("Wundrandschutz") })
}
@Test
fun wundrandschutz_ist_eine_einzelne_freitext_box_ohne_ja_nein() {
val ev = items("empfohlene_wundversorgung")
val wrs = ev.single { it.id == "empfohlene_wundversorgung_wundrandschutz" }
assertEquals("Wundrandschutz", wrs.label)
assertTrue(wrs.isFreeText)
assertNull(ev.firstOrNull { it.id == "empfohlene_wundversorgung_hinweis_wundrandschutz" })
assertNull(ev.firstOrNull { it.id == "empfohlene_wundversorgung_wundrandschutz_ja" })
assertNull(ev.firstOrNull { it.id == "empfohlene_wundversorgung_wundrandschutz_nein" })
// "Wie oft pro Woche" bleibt als eigene Checkbox erhalten.
assertNotNull(ev.firstOrNull { it.id == "empfohlene_wundversorgung_wundrandschutz_tage_pro_woche" })
}
@Test
fun granudacyn_ist_einfache_checkbox_und_freitext_ein_eigener_punkt_direkt_darunter() {
val ev = items("empfohlene_wundversorgung")
val granudacyn = ev.single { it.id == "empfohlene_wundversorgung_granudacyn" }
val freitext = ev.single { it.id == "empfohlene_wundversorgung_granudacyn_freitext" }
assertFalse(granudacyn.isFreeText)
assertTrue(freitext.isFreeText)
assertEquals(DocumentationCatalog.FREE_TEXT_LABEL, freitext.label)
assertEquals(granudacyn.order + 1, freitext.order)
}
}
@@ -172,10 +172,156 @@ class DocumentationToPdfExportModelMapperTest {
val items = DocumentationToPdfExportModelMapper.map(doc).blocks.single().items
assertEquals(PdfExportItem("Freitext", isFreeText = true, freeTextValue = "Rötung", inputSpec = ""), items[0])
// Generischer "Freitext": Bezeichnung wird nicht gedruckt (showLabel = false).
assertEquals(
PdfExportItem("Freitext", isFreeText = true, freeTextValue = "Rötung", inputSpec = "", showLabel = false),
items[0],
)
assertEquals(PdfExportItem("Maße", isFreeText = false, freeTextValue = "3|4", inputSpec = "SPEC"), items[1])
}
// --- Änderungswünsche: Freitext, Hinweisgruppen, Stile, eigene Seite ------------------
@Test
fun benannter_freitext_behaelt_seine_bezeichnung() {
val doc = documentation(
listOf(
block("a", 1, items = listOf(
item("w", 1, checked = true, label = "Wundrandschutz", isFreeText = true, freeTextValue = "Cavilon"),
)),
)
)
assertTrue(DocumentationToPdfExportModelMapper.map(doc).blocks.single().items.single().showLabel)
}
@Test
fun gruppenhinweis_ohne_angehakten_eintrag_der_gruppe_entfaellt() {
// Größe: "Zu versorgendes Areal" darf nicht erscheinen, wenn nur die Maße ausgefüllt sind.
val doc = documentation(
listOf(
block("groesse", 1, items = listOf(
item("h_masse", 1, checked = true, label = "Maße", isStaticHint = true),
item("masse", 2, checked = true, label = "Maße (Länge / Breite / Tiefe)", freeTextValue = "3|4|1", inputSpec = "SPEC"),
item("h_areal", 3, checked = true, label = "Zu versorgendes Areal", isStaticHint = true),
item("areal", 4, checked = false, label = "Zu versorgendes Areal"),
item("areal_ft", 5, checked = false, label = "Freitext für zu versorgendes Areal", isFreeText = true),
// generischer Freitext des Blocks gehört zu keiner Gruppe
item("groesse_freetext", 6, checked = false, label = "Freitext", isFreeText = true),
)),
)
)
val labels = DocumentationToPdfExportModelMapper.map(doc).blocks.single().items.map { it.label }
assertEquals(listOf("Maße", "Maße (Länge / Breite / Tiefe)"), labels)
}
@Test
fun generischer_freitext_laesst_gruppenhinweis_nicht_erscheinen() {
val doc = documentation(
listOf(
block("groesse", 1, items = listOf(
item("masse", 1, checked = true, label = "Maße", freeTextValue = "3|4|1", inputSpec = "SPEC"),
item("h_areal", 2, checked = true, label = "Zu versorgendes Areal", isStaticHint = true),
item("areal", 3, checked = false, label = "Areal"),
item("groesse_freetext", 4, checked = true, label = "Freitext", isFreeText = true, freeTextValue = "x"),
)),
)
)
val labels = DocumentationToPdfExportModelMapper.map(doc).blocks.single().items.map { it.label }
assertEquals(listOf("Maße", "Freitext"), labels)
}
@Test
fun gruppenhinweis_mit_angehaktem_eintrag_bleibt_erhalten() {
val doc = documentation(
listOf(
block("a", 1, items = listOf(
item("h", 1, checked = true, label = "Fixieren mit:", isStaticHint = true),
item("m", 2, checked = true, label = "Mullbinde"),
item("k", 3, checked = false, label = "Klebevlies"),
)),
)
)
val labels = DocumentationToPdfExportModelMapper.map(doc).blocks.single().items.map { it.label }
assertEquals(listOf("Fixieren mit:", "Mullbinde"), labels)
}
@Test
fun gruppenhinweis_mit_gleichnamigem_angehaktem_eintrag_wird_nicht_doppelt_gedruckt() {
val doc = documentation(
listOf(
block("a", 1, items = listOf(
item("h", 1, checked = true, label = "Zu versorgendes Areal", isStaticHint = true),
item("areal", 2, checked = true, label = "Zu versorgendes Areal"),
)),
)
)
val labels = DocumentationToPdfExportModelMapper.map(doc).blocks.single().items.map { it.label }
assertEquals(listOf("Zu versorgendes Areal"), labels)
}
@Test
fun abschliessende_hinweise_ohne_folgende_eintraege_bleiben_immer_erhalten() {
val doc = documentation(
listOf(
block("empfohlene_wundversorgung", 1, items = listOf(
item("x", 1, checked = true, label = "Mullbinde"),
item("empfohlene_wundversorgung_hinweis_bedarf", 2, checked = true, label = "+ bei Bedarf", isStaticHint = true),
item("empfohlene_wundversorgung_hinweis_abklaerung", 3, checked = true, label = "Generell: ...", isStaticHint = true),
item("empfohlene_wundversorgung_freetext", 4, checked = false, label = "Freitext", isFreeText = true),
)),
)
)
val labels = DocumentationToPdfExportModelMapper.map(doc).blocks.single().items.map { it.label }
assertEquals(listOf("Mullbinde", "+ bei Bedarf", "Generell: ..."), labels)
}
@Test
fun stile_werden_anhand_der_definitions_id_vergeben() {
val doc = documentation(
listOf(
block("empfohlene_wundversorgung", 1, items = listOf(
item("empfohlene_wundversorgung_hinweis_reinigen_mit", 1, checked = true, label = "Reinigen mit:", isStaticHint = true),
item("empfohlene_wundversorgung_nacl", 2, checked = true, label = "NaCl 0,9%"),
item("empfohlene_wundversorgung_primaer_abdecken_austamponieren", 3, checked = true, label = "Primär Wunde abdecken/austamponieren"),
item("empfohlene_wundversorgung_hinweis_abklaerung", 4, checked = true, label = "Generell: ...", isStaticHint = true),
)),
)
)
val styles = DocumentationToPdfExportModelMapper.map(doc).blocks.single().items.map { it.style }
assertEquals(
listOf(PdfItemStyle.SUBHEADING, PdfItemStyle.NORMAL, PdfItemStyle.SUBHEADING, PdfItemStyle.WARNING),
styles,
)
}
@Test
fun empfohlene_wundversorgung_beginnt_auf_eigener_seite_andere_bloecke_nicht() {
val doc = documentation(
listOf(
block("weitere_ueberlegung", 1, items = listOf(item("a", 1, checked = true))),
block("empfohlene_wundversorgung", 2, items = listOf(item("b", 1, checked = true))),
block("ziel", 3, items = listOf(item("c", 1, checked = true))),
)
)
val blocks = DocumentationToPdfExportModelMapper.map(doc).blocks
assertEquals(listOf(false, true, false), blocks.map { it.startsOnNewPage })
}
// --- Sortierung ----------------------------------------------------------------------
@Test