Compare commits

...

5 Commits

Author SHA1 Message Date
niklas.ortmann 6a4a2c31c3 Merge pull request 'versionCode u. VersionName' (#24) from dev-Inhalte4 into dev
Reviewed-on: #24
2026-10-17 18:30:11 +02:00
niklas.ortmann c5bf5d3752 versionCode u. VersionName 2026-10-17 18:29:51 +02:00
niklas.ortmann dff64be964 Merge pull request 'Inhalte4' (#22) from dev-Inhalte4 into dev
Reviewed-on: #22
2026-10-17 15:56:58 +02:00
niklas.ortmann 7d48b4c838 Inhalte4
Änderungswünsche
2026-10-17 15:56:03 +02:00
niklas.ortmann af8fa9bf9e README.md aktualisiert 2026-10-15 17:54:19 +02:00
13 changed files with 578 additions and 100 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 unterstützt Fotos und digitale Unterschriften und erzeugt aus
abgeschlossenen Dokumentationen PDF-Dateien. abgeschlossenen Dokumentationen PDF-Dateien.
> **Projektstatus:** beta1.1 > **Projektstatus:** beta1.4
------------------------------------------------------------------------ ------------------------------------------------------------------------
@@ -398,12 +398,12 @@ werden kann.
### Phase 6 -- Qualität und Stabilität ### Phase 6 -- Qualität und Stabilität
- [ ] Unit Tests - [x] Unit Tests
- [ ] Room Tests - [x] Room Tests
- [ ] UI Tests - [x] UI Tests
- [ ] Fehlerbehandlung - [x] Fehlerbehandlung
- [ ] Tablet-Optimierung - [ ] Tablet-Optimierung
- [ ] Datenschutz-Review - [x] Datenschutz-Review
### Phase 7 -- Multi-Device-Synchronisation ### Phase 7 -- Multi-Device-Synchronisation
+2 -2
View File
@@ -61,8 +61,8 @@ android {
// im gesamten Projekt spürbar. // im gesamten Projekt spürbar.
minSdk = 26 minSdk = 26
targetSdk = 35 targetSdk = 35
versionCode = 1 versionCode = 5
versionName = "1.0" versionName = "1.5"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
} }
Binary file not shown.
Binary file not shown.
+37
View File
@@ -0,0 +1,37 @@
{
"version": 3,
"artifactType": {
"type": "APK",
"kind": "Directory"
},
"applicationId": "de.medapp.wunddoku",
"variantName": "release",
"elements": [
{
"type": "SINGLE",
"filters": [],
"attributes": [],
"versionCode": 5,
"versionName": "1.5",
"outputFile": "app-release.apk"
}
],
"elementType": "File",
"baselineProfiles": [
{
"minApi": 28,
"maxApi": 30,
"baselineProfiles": [
"baselineProfiles/1/app-release.dm"
]
},
{
"minApi": 31,
"maxApi": 2147483647,
"baselineProfiles": [
"baselineProfiles/0/app-release.dm"
]
}
],
"minSdkVersionForDexing": 26
}
@@ -28,11 +28,11 @@ import de.medapp.wunddoku.domain.model.InputSpec
* Abschnitt 2.3b. * Abschnitt 2.3b.
* *
* [CATALOG_VERSION] wird beim Erstellen einer neuen Dokumentation als rein informatives * [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 { 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, * 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_arteriosum", label = "Ulcus Cruris art.", order = 6),
CheckItemDefinition(id = "wundart_ulcus_cruris_venosum_arteriosum", label = "Ulcus Cruris ven.-art.", order = 7), 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_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_post_operative_wundheilungsstoerung", label = "Post-operative Wundheilungsstörung", order = 9),
CheckItemDefinition(id = "wundart_schuerfwunde", label = "Schürfwunde", order = 10), CheckItemDefinition(id = "wundart_dehiszenz", label = "Dehiszenz", order = 10),
CheckItemDefinition(id = "wundart_intertrigo", label = "Intertrigo / Wunde nach Intertrigo", order = 11), CheckItemDefinition(id = "wundart_schuerfwunde", label = "Schürfwunde", order = 11),
CheckItemDefinition(id = "wundart_iad", label = "IAD (Inkontinenz-assoziierte Dermatitis) / Wunde nach IAD", order = 12), CheckItemDefinition(id = "wundart_intertrigo", label = "Intertrigo / Wunde nach Intertrigo", order = 12),
CheckItemDefinition(id = "wundart_amputationswunde", label = "Amputationswunde", order = 13), CheckItemDefinition(id = "wundart_iad", label = "IAD (Inkontinenz-assoziierte Dermatitis) / Wunde nach IAD", order = 13),
CheckItemDefinition(id = "wundart_spalthautentnahmestelle", label = "Spalthautentnahmestelle", order = 14), CheckItemDefinition(id = "wundart_amputationswunde", label = "Amputationswunde", order = 14),
CheckItemDefinition(id = "wundart_spalthautentnahmestelle", label = "Spalthautentnahmestelle", order = 15),
CheckItemDefinition( CheckItemDefinition(
id = "wundart_transplantatverlust", id = "wundart_transplantatverlust",
label = "Wunde nach Transplantatverlust", label = "Wunde nach Transplantatverlust",
order = 15, order = 16,
inputSpec = InputSpec( inputSpec = InputSpec(
pdfTemplate = "Wunde nach Transplantatverlust, Spalthautdeckung ca. {0}%", pdfTemplate = "Wunde nach Transplantatverlust, Spalthautdeckung ca. {0}%",
fields = listOf(InputFieldDef.Number(label = "Spalthautdeckung ca. (%)", maxLength = 3)), 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_sekundaer_heilende_wunde", label = "Sekundär heilende Wunde", order = 17),
CheckItemDefinition(id = "wundart_tumorwunde", label = "Tumorwunde", order = 17), CheckItemDefinition(id = "wundart_tumorwunde", label = "Tumorwunde", order = 18),
CheckItemDefinition(id = "wundart_wunde_nach_abszess", label = "Wunde nach Abszess", order = 18), CheckItemDefinition(id = "wundart_wunde_nach_abszess", label = "Wunde nach Abszess", order = 19),
CheckItemDefinition(id = "wundart_verbrennung_verbruehung", label = "Verbrennung / Verbrühung", order = 19), CheckItemDefinition(id = "wundart_verbrennung_verbruehung", label = "Verbrennung / Verbrühung", order = 20),
CheckItemDefinition(id = "wundart_platzwunde", label = "Platzwunde", order = 20), CheckItemDefinition(id = "wundart_platzwunde", label = "Platzwunde", order = 21),
CheckItemDefinition(id = "wundart_schnittwunde", label = "Schnittwunde", order = 21), CheckItemDefinition(id = "wundart_schnittwunde", label = "Schnittwunde", order = 22),
CheckItemDefinition(id = "wundart_bisswunde", label = "Bisswunde", order = 22), CheckItemDefinition(id = "wundart_bisswunde", label = "Bisswunde", order = 23),
CheckItemDefinition(id = "wundart_chemisch_veraetzung", label = "Chemisch hervorgerufene Wunde / Verätzung", order = 23), CheckItemDefinition(id = "wundart_chemisch_veraetzung", label = "Chemisch hervorgerufene Wunde / Verätzung", order = 24),
CheckItemDefinition(id = "wundart_strahlenschaden", label = "Strahlenschaden", order = 24), CheckItemDefinition(id = "wundart_strahlenschaden", label = "Strahlenschaden", order = 25),
), ),
), ),
BlockDefinition( BlockDefinition(
@@ -165,24 +166,25 @@ object DocumentationCatalog {
items = listOf( items = listOf(
CheckItemDefinition(id = "wundgrund_wie_ausgestanzt", label = "Wie ausgestanzt", order = 1), CheckItemDefinition(id = "wundgrund_wie_ausgestanzt", label = "Wie ausgestanzt", order = 1),
CheckItemDefinition(id = "wundgrund_kraterfoermig", label = "Kraterförmig", order = 2), CheckItemDefinition(id = "wundgrund_kraterfoermig", label = "Kraterförmig", order = 2),
CheckItemDefinition(id = "wundgrund_unregelmaessig_landkartenartig", label = "Unregelmäßig (landkartenartig/amorph)", order = 3), CheckItemDefinition(id = "wundgrund_unregelmaessig", label = "Unregelmäßig", order = 3),
CheckItemDefinition(id = "wundgrund_linear_spaltfoermig", label = "Linear/spaltförmig", order = 4), CheckItemDefinition(id = "wundgrund_landkartenartig_amorph", label = "Landkartenartig/amorph", order = 4),
CheckItemDefinition(id = "wundgrund_rund", label = "Rund", order = 5), CheckItemDefinition(id = "wundgrund_linear_spaltfoermig", label = "Linear/spaltförmig", order = 5),
CheckItemDefinition(id = "wundgrund_oval_elliptisch", label = "Oval/elliptisch", order = 6), CheckItemDefinition(id = "wundgrund_rund", label = "Rund", order = 6),
CheckItemDefinition(id = "wundgrund_quadrangulaer", label = "Quadrangulär", order = 7), CheckItemDefinition(id = "wundgrund_oval_elliptisch", label = "Oval/elliptisch", order = 7),
CheckItemDefinition(id = "wundgrund_eben", label = "Eben", order = 8), CheckItemDefinition(id = "wundgrund_quadrangulaer", label = "Quadrangulär", order = 8),
CheckItemDefinition(id = "wundgrund_auf_niveau", label = "Auf Niveau", order = 9), CheckItemDefinition(id = "wundgrund_eben", label = "Eben", order = 9),
CheckItemDefinition(id = "wundgrund_ueber_niveau", label = "Über Niveau", order = 10), CheckItemDefinition(id = "wundgrund_auf_niveau", label = "Auf Niveau", order = 10),
CheckItemDefinition(id = "wundgrund_glaenzend", label = "Glänzend", order = 11), CheckItemDefinition(id = "wundgrund_ueber_niveau", label = "Über Niveau", order = 11),
CheckItemDefinition(id = "wundgrund_grob", label = "Grob", order = 12), CheckItemDefinition(id = "wundgrund_glaenzend", label = "Glänzend", order = 12),
CheckItemDefinition(id = "wundgrund_fein", label = "Fein", order = 13), CheckItemDefinition(id = "wundgrund_grob", label = "Grob", order = 13),
CheckItemDefinition(id = "wundgrund_glatt", label = "Glatt", order = 14), CheckItemDefinition(id = "wundgrund_fein", label = "Fein", order = 14),
CheckItemDefinition(id = "wundgrund_rot", label = "Rot", order = 15), CheckItemDefinition(id = "wundgrund_glatt", label = "Glatt", order = 15),
CheckItemDefinition(id = "wundgrund_blass", label = "Blass", order = 16), CheckItemDefinition(id = "wundgrund_rot", label = "Rot", order = 16),
CheckItemDefinition(id = "wundgrund_knochen_sehnen", label = "Knochen/Sehnen", order = 17), CheckItemDefinition(id = "wundgrund_blass", label = "Blass", order = 17),
CheckItemDefinition(id = "wundgrund_haemorrhagie", label = "Hämorrhagie", order = 18), CheckItemDefinition(id = "wundgrund_knochen_sehnen", label = "Knochen/Sehnen", order = 18),
CheckItemDefinition(id = "wundgrund_nicht_kaum_einsehbar", label = "Nicht/kaum einsehbar", order = 19), CheckItemDefinition(id = "wundgrund_haemorrhagie", label = "Hämorrhagie", order = 19),
CheckItemDefinition(id = "wundgrund_erscheinung_erscheinung", label = "Erscheinung", order = 20, isFreeText = true), CheckItemDefinition(id = "wundgrund_nicht_kaum_einsehbar", label = "Nicht/kaum einsehbar", order = 20),
CheckItemDefinition(id = "wundgrund_erscheinung_erscheinung", label = "Erscheinung", order = 21, isFreeText = true),
), ),
), ),
BlockDefinition( BlockDefinition(
@@ -498,61 +500,61 @@ object DocumentationCatalog {
CheckItemDefinition(id = "empfohlene_wundversorgung_sterile_kompresse", label = "Sterile Kompresse", order = 2), 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_nacl", label = "NaCl 0,9%", order = 3),
CheckItemDefinition(id = "empfohlene_wundversorgung_octenisept", label = "Octenisept", order = 4), CheckItemDefinition(id = "empfohlene_wundversorgung_octenisept", label = "Octenisept", order = 4),
// Granudacyn: Checkbox + direkt folgendes Freitextfeld, das nur bei angehakter // Granudacyn: einfache Checkbox. Der Freitext ist ein eigener, separat
// Checkbox aktiv ist (= vorhandenes isFreeText-Verhalten, s. FreeTextCheckItemRow). // anzuhakender Punkt direkt darunter (Bezeichnung "Freitext" wird in PDF/JPG
CheckItemDefinition(id = "empfohlene_wundversorgung_granudacyn", label = "Granudacyn", order = 5, isFreeText = true), // 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( CheckItemDefinition(
id = "empfohlene_wundversorgung_getraenkte_kompresse", id = "empfohlene_wundversorgung_getraenkte_kompresse",
label = "Getränkte Kompresse", label = "Getränkte Kompresse",
order = 6, order = 7,
inputSpec = InputSpec( inputSpec = InputSpec(
pdfTemplate = "Getränkte Kompresse für ca. {0} Min. auflegen", pdfTemplate = "Getränkte Kompresse für ca. {0} Min. auflegen",
fields = listOf(InputFieldDef.Choice(label = "Minuten", options = MINUTE_1_TO_15_OPTIONS)), 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_tupfend", label = "Tupfend", order = 8),
CheckItemDefinition(id = "empfohlene_wundversorgung_wischend", label = "Wischend", 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 = 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 = 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 = 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 = 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 = 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 = 14, isFreeText = true), CheckItemDefinition(id = "empfohlene_wundversorgung_nicht_ueber_wundrand_mit_freitext", label = FREE_TEXT_LABEL, order = 15, isFreeText = true),
CheckItemDefinition(id = "empfohlene_wundversorgung_hinweis_wundrandschutz", label = "Wundrandschutz:", order = 15, isStaticHint = true), // Wundrandschutz: EINE Checkbox mit eigenem Freitextfeld (kein Ja/Nein mehr).
CheckItemDefinition(id = "empfohlene_wundversorgung_wundrandschutz_ja", label = "Ja", order = 16), CheckItemDefinition(id = "empfohlene_wundversorgung_wundrandschutz", label = "Wundrandschutz", order = 16, isFreeText = true),
CheckItemDefinition(id = "empfohlene_wundversorgung_wundrandschutz_nein", label = "Nein", order = 17),
CheckItemDefinition(id = "empfohlene_wundversorgung_wundrandschutz_freitext", label = FREE_TEXT_LABEL, order = 18, isFreeText = true),
// "Wie oft pro Woche": eigene Checkbox; die Auswahl (1-7x) ist erst nach dem // "Wie oft pro Woche": eigene Checkbox; die Auswahl (1-7x) ist erst nach dem
// Anhaken aktiv (InputSpec.requiresOwnCheckbox). // Anhaken aktiv (InputSpec.requiresOwnCheckbox).
CheckItemDefinition( CheckItemDefinition(
id = "empfohlene_wundversorgung_wundrandschutz_tage_pro_woche", id = "empfohlene_wundversorgung_wundrandschutz_tage_pro_woche",
label = "Wie oft pro Woche", label = "Wie oft pro Woche",
order = 19, order = 17,
inputSpec = InputSpec( inputSpec = InputSpec(
pdfTemplate = "{0}x pro Woche", pdfTemplate = "{0}x pro Woche",
fields = listOf(InputFieldDef.Choice(label = "Tage pro Woche", options = WEEKDAY_COUNT_OPTIONS)), fields = listOf(InputFieldDef.Choice(label = "Tage pro Woche", options = WEEKDAY_COUNT_OPTIONS)),
requiresOwnCheckbox = true, requiresOwnCheckbox = true,
), ),
), ),
CheckItemDefinition(id = "empfohlene_wundversorgung_hinweis_sekundaere_wundauflage", label = "Sekundäre Wundauflage:", order = 20, 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 = 21), CheckItemDefinition(id = "empfohlene_wundversorgung_sekundaer_sterile_kompresse", label = "Sterile Kompresse", order = 19),
CheckItemDefinition(id = "empfohlene_wundversorgung_saugkompresse", label = "Saugkompresse", order = 22), CheckItemDefinition(id = "empfohlene_wundversorgung_saugkompresse", label = "Saugkompresse", order = 20),
CheckItemDefinition(id = "empfohlene_wundversorgung_sonstige_wundauflage", label = "Sonstiges (Wundauflage)", order = 23, isFreeText = true), CheckItemDefinition(id = "empfohlene_wundversorgung_sonstige_wundauflage", label = "Sonstiges (Wundauflage)", order = 21, isFreeText = true),
CheckItemDefinition(id = "empfohlene_wundversorgung_hinweis_fixieren_mit", label = "Fixieren mit:", order = 24, isStaticHint = true), CheckItemDefinition(id = "empfohlene_wundversorgung_hinweis_fixieren_mit", label = "Fixieren mit:", order = 22, isStaticHint = true),
CheckItemDefinition(id = "empfohlene_wundversorgung_mullbinde", label = "Mullbinde", order = 25), CheckItemDefinition(id = "empfohlene_wundversorgung_mullbinde", label = "Mullbinde", order = 23),
CheckItemDefinition(id = "empfohlene_wundversorgung_klebevlies", label = "Klebevlies", order = 26), CheckItemDefinition(id = "empfohlene_wundversorgung_klebevlies", label = "Klebevlies", order = 24),
CheckItemDefinition(id = "empfohlene_wundversorgung_folienverband", label = "Folienverband", order = 27), CheckItemDefinition(id = "empfohlene_wundversorgung_folienverband", label = "Folienverband", order = 25),
CheckItemDefinition(id = "empfohlene_wundversorgung_schlauchverband", label = "Schlauchverband", order = 28), CheckItemDefinition(id = "empfohlene_wundversorgung_schlauchverband", label = "Schlauchverband", order = 26),
CheckItemDefinition(id = "empfohlene_wundversorgung_kohaesive_fixierbinde", label = "Kohäsive Fixierbinde", order = 29), CheckItemDefinition(id = "empfohlene_wundversorgung_kohaesive_fixierbinde", label = "Kohäsive Fixierbinde", order = 27),
CheckItemDefinition(id = "empfohlene_wundversorgung_kohaesive_fixierbinde_freitext", label = FREE_TEXT_LABEL, order = 30, isFreeText = true), 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 = 31), CheckItemDefinition(id = "empfohlene_wundversorgung_kompressionsverband_system", label = "Kompressionsverband / System", order = 29),
CheckItemDefinition(id = "empfohlene_wundversorgung_kompressionsverband_system_freitext", label = FREE_TEXT_LABEL, order = 32, isFreeText = true), CheckItemDefinition(id = "empfohlene_wundversorgung_kompressionsverband_system_freitext", label = FREE_TEXT_LABEL, order = 30, isFreeText = true),
CheckItemDefinition(id = "empfohlene_wundversorgung_wechselintervall", label = "Wechselintervall", order = 33, isFreeText = true), CheckItemDefinition(id = "empfohlene_wundversorgung_wechselintervall", label = "Wechselintervall", order = 31, isFreeText = true),
CheckItemDefinition(id = "empfohlene_wundversorgung_hinweis_bedarf", label = "+ bei Bedarf", order = 34, isStaticHint = true), CheckItemDefinition(id = "empfohlene_wundversorgung_hinweis_bedarf", label = "+ bei Bedarf", order = 32, isStaticHint = true),
CheckItemDefinition( CheckItemDefinition(
id = "empfohlene_wundversorgung_hinweis_abklaerung", id = "empfohlene_wundversorgung_hinweis_abklaerung",
label = "Generell: bei akuter Verschlechterung sofort ärztliche Abklärung", label = "Generell: bei akuter Verschlechterung sofort ärztliche Abklärung",
order = 35, order = 33,
isStaticHint = true, isStaticHint = true,
), ),
), ),
@@ -571,9 +573,8 @@ object DocumentationCatalog {
CheckItemDefinition(id = "ziel_abheilung", label = "Abheilung", order = 7), CheckItemDefinition(id = "ziel_abheilung", label = "Abheilung", order = 7),
CheckItemDefinition(id = "ziel_geruchsreduzierung", label = "Geruchsreduzierung", order = 8), CheckItemDefinition(id = "ziel_geruchsreduzierung", label = "Geruchsreduzierung", order = 8),
CheckItemDefinition(id = "ziel_einfache_durchfuehrung", label = "Einfache Durchführung", order = 9), 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 = 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 = 11),
CheckItemDefinition(id = "ziel_selbstversorgung_foerdern", label = "Selbstversorgung der Wunde durch Patient/Angehörige unterstützen/fördern", order = 12),
), ),
), ),
BlockDefinition( BlockDefinition(
@@ -2,10 +2,13 @@ package de.medapp.wunddoku.pdf
import android.content.Context import android.content.Context
import android.graphics.Bitmap import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.Color import android.graphics.Color
import android.graphics.Paint import android.graphics.Paint
import android.graphics.Typeface import android.graphics.Typeface
import android.graphics.pdf.PdfDocument import android.graphics.pdf.PdfDocument
import android.text.Layout
import android.text.StaticLayout
import android.text.TextPaint import android.text.TextPaint
import de.medapp.wunddoku.domain.error.FailureCategory import de.medapp.wunddoku.domain.error.FailureCategory
import de.medapp.wunddoku.domain.model.InputSpecCodec import de.medapp.wunddoku.domain.model.InputSpecCodec
@@ -66,6 +69,8 @@ class AndroidPdfDocumentExporter(
private const val ITEM_TEXT_SIZE = 11f private const val ITEM_TEXT_SIZE = 11f
private const val FREE_TEXT_TEXT_SIZE = 10.5f private const val FREE_TEXT_TEXT_SIZE = 10.5f
private const val FREE_TEXT_INDENT = 20f 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 // PDF-003: Signaturbereich. Fachlich handschriftliche Unterschriften, keine
// kryptographische PDF-Signatur (s. Aufgabenstellung Abschnitt 5). // kryptographische PDF-Signatur (s. Aufgabenstellung Abschnitt 5).
@@ -160,6 +165,9 @@ class AndroidPdfDocumentExporter(
marginRight = MARGIN, marginRight = MARGIN,
marginTop = MARGIN, marginTop = MARGIN,
marginBottom = MARGIN, marginBottom = MARGIN,
pageHeaderDrawer = PageHeaderDrawer { canvas, pageNumber, left, top, width ->
drawRunningHeader(canvas, pageNumber, left, top, width, model, paints)
},
) )
drawHeader(cursor, model, paints) drawHeader(cursor, model, paints)
@@ -243,6 +251,32 @@ class AndroidPdfDocumentExporter(
textSize = FREE_TEXT_TEXT_SIZE textSize = FREE_TEXT_TEXT_SIZE
color = Color.DKGRAY 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. // PDF-003: Signaturbereich.
val signatureLabel = TextPaint().apply { val signatureLabel = TextPaint().apply {
@@ -286,6 +320,37 @@ class AndroidPdfDocumentExporter(
cursor.drawDivider(spacingAfter = 12f) 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 /** Überschriften erscheinen im PDF immer mit abschließendem Doppelpunkt (kein doppelter
* Doppelpunkt, falls der Titel bereits mit einem endet). */ * Doppelpunkt, falls der Titel bereits mit einem endet). */
private fun headingText(title: String): String { private fun headingText(title: String): String {
@@ -294,6 +359,8 @@ class AndroidPdfDocumentExporter(
} }
private fun drawBlock(cursor: PdfPageCursor, block: PdfExportBlock, paints: ExportPaints) { 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) cursor.drawText(headingText(block.title), paints.blockTitle, spacingAfter = 6f)
for (item in block.items) { for (item in block.items) {
@@ -330,14 +397,37 @@ class AndroidPdfDocumentExporter(
return 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 // Die Freitext-Option bekommt einen abschließenden Doppelpunkt, sofern tatsächlich Text
// folgt (z. B. "Granudacyn" ohne Text bleibt ohne hängenden Doppelpunkt), normale // folgt (z. B. "Granudacyn" ohne Text bleibt ohne hängenden Doppelpunkt), normale
// Optionen nie. // 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( cursor.drawText(
text = labelText, text = labelText,
paint = paints.item, paint = labelPaint,
indent = ITEM_INDENT, indent = ITEM_INDENT,
spacingAfter = 3f, spacingAfter = 3f,
) )
@@ -1,6 +1,9 @@
package de.medapp.wunddoku.pdf 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.Documentation
import de.medapp.wunddoku.domain.model.DocumentationBlock
import de.medapp.wunddoku.domain.usecase.BlockEditedPolicy import de.medapp.wunddoku.domain.usecase.BlockEditedPolicy
import java.time.ZoneId import java.time.ZoneId
@@ -27,22 +30,24 @@ object DocumentationToPdfExportModelMapper {
.sortedBy { it.order } .sortedBy { it.order }
.map { block -> .map { block ->
PdfExportBlock( PdfExportBlock(
definitionId = block.definitionId,
title = block.title, title = block.title,
// Änderungswunsch: nur angehakte Optionen erscheinen im PDF - nicht startsOnNewPage = block.definitionId in BLOCKS_ON_OWN_PAGE,
// angehakte Checkboxen werden komplett weggelassen (nicht nur unausgefüllt // Nur angehakte Optionen erscheinen im PDF. Hinweistexte (isStaticHint) sind
// dargestellt). Ein leeres Ergebnis ist hier ausgeschlossen: BlockEditedPolicy // 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. // (Filter oben) lässt nur Blöcke mit mindestens einer angehakten Option durch.
items = block.items items = exportedItems(block).map { item ->
.filter { it.checked } PdfExportItem(
.sortedBy { it.order } label = item.label,
.map { item -> isFreeText = item.isFreeText,
PdfExportItem( freeTextValue = item.freeTextValue,
label = item.label, inputSpec = item.inputSpec,
isFreeText = item.isFreeText, // Generische "Freitext"-Bezeichnung nie drucken, nur den Text.
freeTextValue = item.freeTextValue, showLabel = !(item.isFreeText && item.label == DocumentationCatalog.FREE_TEXT_LABEL),
inputSpec = item.inputSpec, style = styleFor(item),
) )
}, },
) )
} }
@@ -80,4 +85,67 @@ object DocumentationToPdfExportModelMapper {
photos = photos, 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( data class PdfExportBlock(
/** Stabile Definitions-ID des Blocks (z. B. "empfohlene_wundversorgung"). */
val definitionId: String = "",
val title: 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 /** Bereits gefiltert (NUR angehakte Optionen, s. Mapper - Änderungswunsch) und sortiert
* (itemOrder). Nicht angehakte Optionen erscheinen im PDF überhaupt nicht mehr. */ * (itemOrder). Nicht angehakte Optionen erscheinen im PDF überhaupt nicht mehr. */
val items: List<PdfExportItem>, val items: List<PdfExportItem>,
@@ -45,8 +50,23 @@ data class PdfExportItem(
val isFreeText: Boolean, val isFreeText: Boolean,
val freeTextValue: String, val freeTextValue: String,
val inputSpec: 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] - * 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 * `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.StaticLayout
import android.text.TextPaint 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 * Verwaltet Seiten, Zeichenfläche und die aktuelle vertikale Schreibposition ("Cursor") beim
* Erzeugen eines [PdfDocument]. Kapselt die komplette Seitenumbruch-Logik an einer Stelle, damit * 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 marginRight: Float,
private val marginTop: Float, private val marginTop: Float,
private val marginBottom: 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 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 currentPage: PdfDocument.Page? = null
private var canvas: Canvas? = null private var canvas: Canvas? = null
private var cursorY: Float = marginTop private var cursorY: Float = marginTop
@@ -74,7 +88,15 @@ internal class PdfPageCursor(
val page = pdfDocument.startPage(info) val page = pdfDocument.startPage(info)
currentPage = page currentPage = page
canvas = page.canvas 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() { private fun finishCurrentPageIfOpen() {
@@ -98,6 +120,15 @@ internal class PdfPageCursor(
startNewPage() 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 * Zeichnet [bitmap] proportional eingepasst (kein Zuschneiden, keine Verzerrung) horizontal
* über die volle Inhaltsbreite und vertikal zentriert im aktuell verbleibenden Platz der * ü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.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import de.medapp.wunddoku.R 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.CheckItem
import de.medapp.wunddoku.domain.model.InputFieldDef import de.medapp.wunddoku.domain.model.InputFieldDef
import de.medapp.wunddoku.domain.model.InputSpecCodec import de.medapp.wunddoku.domain.model.InputSpecCodec
@@ -907,19 +908,25 @@ private fun FreeTextCheckItemRow(
onCheckedChanged: (checkItemStateId: String, checked: Boolean) -> Unit, onCheckedChanged: (checkItemStateId: String, checked: Boolean) -> Unit,
onFreeTextChanged: (checkItemStateId: String, text: String) -> 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 { Column {
Row(verticalAlignment = Alignment.CenterVertically) { Row(verticalAlignment = Alignment.CenterVertically) {
Checkbox( Checkbox(
checked = item.checked, checked = item.checked,
onCheckedChange = { checked -> onCheckedChanged(item.checkItemStateId, checked) }, onCheckedChange = { checked -> onCheckedChanged(item.checkItemStateId, checked) },
) )
Text(item.label) if (!hideName) Text(item.label)
} }
OutlinedTextField( OutlinedTextField(
value = item.freeTextValue, value = item.freeTextValue,
onValueChange = { text -> onFreeTextChanged(item.checkItemStateId, text) }, onValueChange = { text -> onFreeTextChanged(item.checkItemStateId, text) },
enabled = item.checked, enabled = item.checked,
label = { Text(stringResource(R.string.documentation_edit_free_text_label)) }, label = fieldLabel,
modifier = Modifier.fillMaxWidth().padding(start = 40.dp), modifier = Modifier.fillMaxWidth().padding(start = 40.dp),
minLines = 2, 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 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]) 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 ---------------------------------------------------------------------- // --- Sortierung ----------------------------------------------------------------------
@Test @Test