From 8a430d5089660972f6249044332dfc5a32cd6542 Mon Sep 17 00:00:00 2001 From: NOrtmann1 Date: Sat, 3 Oct 2026 00:34:59 +0200 Subject: [PATCH] neue Boxinhalte nach Kundenwunsch --- .../6.json | 635 ++++++++++++++++++ .../wunddoku/data/database/AppDatabase.kt | 9 +- .../wunddoku/data/database/Migrations.kt | 17 + .../data/entity/CheckItemStateEntity.kt | 12 +- .../data/mapper/DocumentationMapper.kt | 1 + .../repository/DocumentationRepository.kt | 35 +- .../domain/catalog/DocumentationCatalog.kt | 401 ++++++++--- .../medapp/wunddoku/domain/model/CheckItem.kt | 6 + .../medapp/wunddoku/domain/model/InputSpec.kt | 155 +++++ .../pdf/AndroidPdfDocumentExporter.kt | 17 + .../DocumentationToPdfExportModelMapper.kt | 1 + .../de/medapp/wunddoku/pdf/PdfExportModel.kt | 8 +- .../edit/DocumentationEditScreen.kt | 108 +++ 13 files changed, 1288 insertions(+), 117 deletions(-) create mode 100644 app/schemas/de.medapp.wunddoku.data.database.AppDatabase/6.json create mode 100644 app/src/main/java/de/medapp/wunddoku/domain/model/InputSpec.kt diff --git a/app/schemas/de.medapp.wunddoku.data.database.AppDatabase/6.json b/app/schemas/de.medapp.wunddoku.data.database.AppDatabase/6.json new file mode 100644 index 0000000..6db2ec0 --- /dev/null +++ b/app/schemas/de.medapp.wunddoku.data.database.AppDatabase/6.json @@ -0,0 +1,635 @@ +{ + "formatVersion": 1, + "database": { + "version": 6, + "identityHash": "5e5d96efeef00d1ff7d404166a900974", + "entities": [ + { + "tableName": "patients", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `firstName` TEXT NOT NULL, `lastName` TEXT NOT NULL, `birthDate` TEXT NOT NULL, `street` TEXT NOT NULL, `postalCode` TEXT NOT NULL, `city` TEXT NOT NULL, `phone1` TEXT NOT NULL, `phone2` TEXT, `phone3` TEXT, `createdAt` INTEGER NOT NULL, `updatedAt` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "firstName", + "columnName": "firstName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastName", + "columnName": "lastName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "birthDate", + "columnName": "birthDate", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "street", + "columnName": "street", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "postalCode", + "columnName": "postalCode", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "city", + "columnName": "city", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "phone1", + "columnName": "phone1", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "phone2", + "columnName": "phone2", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "phone3", + "columnName": "phone3", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "updatedAt", + "columnName": "updatedAt", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_patients_lastName_firstName", + "unique": false, + "columnNames": [ + "lastName", + "firstName" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_patients_lastName_firstName` ON `${TABLE_NAME}` (`lastName`, `firstName`)" + }, + { + "name": "index_patients_birthDate", + "unique": false, + "columnNames": [ + "birthDate" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_patients_birthDate` ON `${TABLE_NAME}` (`birthDate`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "documentations", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `patientId` TEXT NOT NULL, `status` TEXT NOT NULL, `createdAt` INTEGER NOT NULL, `updatedAt` INTEGER NOT NULL, `continuedFromDocumentationId` TEXT, `pdfLastGeneratedAt` INTEGER, `catalogVersion` INTEGER NOT NULL, `patientFirstNameSnapshot` TEXT NOT NULL, `patientLastNameSnapshot` TEXT NOT NULL, `patientBirthDateSnapshot` TEXT NOT NULL, `patientStreetSnapshot` TEXT NOT NULL, `patientPostalCodeSnapshot` TEXT NOT NULL, `patientCitySnapshot` TEXT NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`patientId`) REFERENCES `patients`(`id`) ON UPDATE NO ACTION ON DELETE RESTRICT , FOREIGN KEY(`continuedFromDocumentationId`) REFERENCES `documentations`(`id`) ON UPDATE NO ACTION ON DELETE SET NULL )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "patientId", + "columnName": "patientId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "status", + "columnName": "status", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "updatedAt", + "columnName": "updatedAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "continuedFromDocumentationId", + "columnName": "continuedFromDocumentationId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pdfLastGeneratedAt", + "columnName": "pdfLastGeneratedAt", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "catalogVersion", + "columnName": "catalogVersion", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "patientFirstNameSnapshot", + "columnName": "patientFirstNameSnapshot", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "patientLastNameSnapshot", + "columnName": "patientLastNameSnapshot", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "patientBirthDateSnapshot", + "columnName": "patientBirthDateSnapshot", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "patientStreetSnapshot", + "columnName": "patientStreetSnapshot", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "patientPostalCodeSnapshot", + "columnName": "patientPostalCodeSnapshot", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "patientCitySnapshot", + "columnName": "patientCitySnapshot", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_documentations_patientId_createdAt", + "unique": false, + "columnNames": [ + "patientId", + "createdAt" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_documentations_patientId_createdAt` ON `${TABLE_NAME}` (`patientId`, `createdAt`)" + }, + { + "name": "index_documentations_status", + "unique": false, + "columnNames": [ + "status" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_documentations_status` ON `${TABLE_NAME}` (`status`)" + }, + { + "name": "index_documentations_continuedFromDocumentationId", + "unique": false, + "columnNames": [ + "continuedFromDocumentationId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_documentations_continuedFromDocumentationId` ON `${TABLE_NAME}` (`continuedFromDocumentationId`)" + } + ], + "foreignKeys": [ + { + "table": "patients", + "onDelete": "RESTRICT", + "onUpdate": "NO ACTION", + "columns": [ + "patientId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "documentations", + "onDelete": "SET NULL", + "onUpdate": "NO ACTION", + "columns": [ + "continuedFromDocumentationId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "block_states", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `documentationId` TEXT NOT NULL, `blockDefinitionId` TEXT NOT NULL, `titleSnapshot` TEXT NOT NULL, `blockOrder` INTEGER NOT NULL, `updatedAt` INTEGER NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`documentationId`) REFERENCES `documentations`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "documentationId", + "columnName": "documentationId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "blockDefinitionId", + "columnName": "blockDefinitionId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "titleSnapshot", + "columnName": "titleSnapshot", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "blockOrder", + "columnName": "blockOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "updatedAt", + "columnName": "updatedAt", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_block_states_documentationId", + "unique": false, + "columnNames": [ + "documentationId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_block_states_documentationId` ON `${TABLE_NAME}` (`documentationId`)" + }, + { + "name": "index_block_states_documentationId_blockDefinitionId", + "unique": true, + "columnNames": [ + "documentationId", + "blockDefinitionId" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_block_states_documentationId_blockDefinitionId` ON `${TABLE_NAME}` (`documentationId`, `blockDefinitionId`)" + } + ], + "foreignKeys": [ + { + "table": "documentations", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "documentationId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "check_item_states", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `blockStateId` TEXT NOT NULL, `checkItemDefinitionId` TEXT NOT NULL, `labelSnapshot` TEXT NOT NULL, `itemOrder` INTEGER NOT NULL, `checked` INTEGER NOT NULL, `isFreeText` INTEGER NOT NULL, `freeTextValue` TEXT NOT NULL, `isStaticHint` INTEGER NOT NULL, `inputSpec` TEXT NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`blockStateId`) REFERENCES `block_states`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "blockStateId", + "columnName": "blockStateId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "checkItemDefinitionId", + "columnName": "checkItemDefinitionId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "labelSnapshot", + "columnName": "labelSnapshot", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "itemOrder", + "columnName": "itemOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "checked", + "columnName": "checked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isFreeText", + "columnName": "isFreeText", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "freeTextValue", + "columnName": "freeTextValue", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isStaticHint", + "columnName": "isStaticHint", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "inputSpec", + "columnName": "inputSpec", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_check_item_states_blockStateId", + "unique": false, + "columnNames": [ + "blockStateId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_check_item_states_blockStateId` ON `${TABLE_NAME}` (`blockStateId`)" + }, + { + "name": "index_check_item_states_blockStateId_checkItemDefinitionId", + "unique": true, + "columnNames": [ + "blockStateId", + "checkItemDefinitionId" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_check_item_states_blockStateId_checkItemDefinitionId` ON `${TABLE_NAME}` (`blockStateId`, `checkItemDefinitionId`)" + } + ], + "foreignKeys": [ + { + "table": "block_states", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "blockStateId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "photos", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `documentationId` TEXT NOT NULL, `fileName` TEXT NOT NULL, `orderIndex` INTEGER NOT NULL, `createdAt` INTEGER NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`documentationId`) REFERENCES `documentations`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "documentationId", + "columnName": "documentationId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "fileName", + "columnName": "fileName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "orderIndex", + "columnName": "orderIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_photos_documentationId_orderIndex", + "unique": false, + "columnNames": [ + "documentationId", + "orderIndex" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_photos_documentationId_orderIndex` ON `${TABLE_NAME}` (`documentationId`, `orderIndex`)" + } + ], + "foreignKeys": [ + { + "table": "documentations", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "documentationId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "signatures", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `documentationId` TEXT NOT NULL, `signerName` TEXT NOT NULL, `fileName` TEXT NOT NULL, `orderIndex` INTEGER NOT NULL, `createdAt` INTEGER NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`documentationId`) REFERENCES `documentations`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "documentationId", + "columnName": "documentationId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "signerName", + "columnName": "signerName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "fileName", + "columnName": "fileName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "orderIndex", + "columnName": "orderIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_signatures_documentationId", + "unique": false, + "columnNames": [ + "documentationId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_signatures_documentationId` ON `${TABLE_NAME}` (`documentationId`)" + } + ], + "foreignKeys": [ + { + "table": "documentations", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "documentationId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "wound_carers", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `signatureFileName` TEXT NOT NULL, `updatedAt` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "signatureFileName", + "columnName": "signatureFileName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "updatedAt", + "columnName": "updatedAt", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '5e5d96efeef00d1ff7d404166a900974')" + ] + } +} \ No newline at end of file diff --git a/app/src/main/java/de/medapp/wunddoku/data/database/AppDatabase.kt b/app/src/main/java/de/medapp/wunddoku/data/database/AppDatabase.kt index 2c8c2c1..2e593a2 100644 --- a/app/src/main/java/de/medapp/wunddoku/data/database/AppDatabase.kt +++ b/app/src/main/java/de/medapp/wunddoku/data/database/AppDatabase.kt @@ -48,6 +48,11 @@ import de.medapp.wunddoku.data.entity.converter.DocumentationStatusConverter * Version 5 (vorher 4, finale fachliche Katalog-Inhalte): `check_item_states.isStaticHint` neu * (nicht editierbarer Hinweistext, z. B. "+ bei Bedarf") - rein additive Spalte mit * Default-Wert ([MIGRATION_4_5]), keine bestehenden Daten verändert. + * + * Version 6 (vorher 5, Änderungswunsch): `check_item_states.inputSpec` neu (strukturierte + * Eingabefelder wie Zahl/Auswahlliste statt Freitext, s. [de.medapp.wunddoku.domain.model.InputSpec]) + * - ebenfalls rein additive Spalte mit Default-Wert ([MIGRATION_5_6]), keine bestehenden Daten + * verändert. */ @Database( entities = [ @@ -59,7 +64,7 @@ import de.medapp.wunddoku.data.entity.converter.DocumentationStatusConverter SignatureEntity::class, WoundCarerEntity::class, ], - version = 5, + version = 6, exportSchema = true, ) @TypeConverters(DocumentationStatusConverter::class) @@ -87,7 +92,7 @@ abstract class AppDatabase : RoomDatabase() { DATABASE_NAME, ) // Kein fallbackToDestructiveMigration() mehr ab Version 3 - s. Klassenkommentar. - .addMigrations(MIGRATION_2_3, MIGRATION_3_4, MIGRATION_4_5) + .addMigrations(MIGRATION_2_3, MIGRATION_3_4, MIGRATION_4_5, MIGRATION_5_6) .build().also { instance = it } } } diff --git a/app/src/main/java/de/medapp/wunddoku/data/database/Migrations.kt b/app/src/main/java/de/medapp/wunddoku/data/database/Migrations.kt index 94c4706..9d0a48c 100644 --- a/app/src/main/java/de/medapp/wunddoku/data/database/Migrations.kt +++ b/app/src/main/java/de/medapp/wunddoku/data/database/Migrations.kt @@ -131,3 +131,20 @@ val MIGRATION_4_5 = object : Migration(4, 5) { ) } } + +/** + * Migration von Schema-Version 5 auf 6 (Änderungswunsch: strukturierte Eingabefelder statt + * Freitext, z. B. "Länge/Breite/Tiefe" bei "Größe" oder "Von ... bis ... Uhr" bei "Fistel / + * Tasche" - s. [de.medapp.wunddoku.domain.model.InputSpec]). + * + * Wie schon bei Migration 4->5 rein additiv: eine neue NOT NULL-Spalte mit Default `''` (leerer + * String = "dieser Eintrag hat keine strukturierten Felder", entspricht dem bisherigen + * Verhalten aller bestehenden Zeilen unverändert). Kein Tabellenneubau nötig. + */ +val MIGRATION_5_6 = object : Migration(5, 6) { + override fun migrate(db: SupportSQLiteDatabase) { + db.execSQL( + "ALTER TABLE `check_item_states` ADD COLUMN `inputSpec` TEXT NOT NULL DEFAULT ''" + ) + } +} diff --git a/app/src/main/java/de/medapp/wunddoku/data/entity/CheckItemStateEntity.kt b/app/src/main/java/de/medapp/wunddoku/data/entity/CheckItemStateEntity.kt index 25ec104..a8515bf 100644 --- a/app/src/main/java/de/medapp/wunddoku/data/entity/CheckItemStateEntity.kt +++ b/app/src/main/java/de/medapp/wunddoku/data/entity/CheckItemStateEntity.kt @@ -20,13 +20,22 @@ import androidx.room.PrimaryKey * nachrichtlich an") - technisch identisch zur automatisch angehängten Option, nur mit eigener, * fachlich benannter [labelSnapshot] statt des generischen "Freitext"-Labels. * - * [isStaticHint]: NEU. Markiert eine Zeile als reinen, nicht editierbaren Hinweistext (z. B. + * [isStaticHint]: Markiert eine Zeile als reinen, nicht editierbaren Hinweistext (z. B. * "+ bei Bedarf") - keine Checkbox in der UI, kein Umschalten möglich. Solche Zeilen werden * beim Anlegen (s. DocumentationRepository.insertBlocksFromCatalog) mit `checked = true` * fest verankert, damit die bestehende, unverändert gebliebene "nur angehakte Elemente"-Regel * im PDF-Export sie automatisch mit anzeigt, ohne dass diese Regel selbst angepasst werden * musste. [BlockEditedPolicy] schließt `isStaticHint`-Zeilen dagegen explizit von der * "bearbeitet"-Prüfung aus, da ein reiner Hinweistext keine Nutzereingabe darstellt. + * + * [inputSpec]: NEU. Nicht-leer, wenn dieser Eintrag statt eines einfachen Häkchens ein oder + * mehrere strukturierte Eingabefelder besitzt (Zahl oder Auswahlliste, z. B. "Von [3] bis [5] + * Uhr" oder "Länge/Breite/Tiefe"), s. [de.medapp.wunddoku.domain.model.InputSpec] und + * [de.medapp.wunddoku.domain.model.InputSpecCodec]. Die eingegebenen Werte selbst liegen - + * mehrere durch ein Trennzeichen kombiniert - weiterhin in [freeTextValue]; es wird also KEINE + * weitere Spalte pro Feld benötigt. [checked] wird bei einem solchen Eintrag automatisch von der + * UI gesetzt, sobald mindestens ein Feld einen Wert hat - dieselbe Checkbox-Semantik wie überall + * sonst, keine zweite "bearbeitet"-Logik. */ @Entity( tableName = "check_item_states", @@ -53,4 +62,5 @@ data class CheckItemStateEntity( val isFreeText: Boolean = false, val freeTextValue: String = "", val isStaticHint: Boolean = false, + val inputSpec: String = "", ) diff --git a/app/src/main/java/de/medapp/wunddoku/data/mapper/DocumentationMapper.kt b/app/src/main/java/de/medapp/wunddoku/data/mapper/DocumentationMapper.kt index a0121c2..6ca86c7 100644 --- a/app/src/main/java/de/medapp/wunddoku/data/mapper/DocumentationMapper.kt +++ b/app/src/main/java/de/medapp/wunddoku/data/mapper/DocumentationMapper.kt @@ -72,6 +72,7 @@ fun CheckItemStateEntity.toDomain(): CheckItem = CheckItem( checked = checked, isFreeText = isFreeText, isStaticHint = isStaticHint, + inputSpec = inputSpec, freeTextValue = freeTextValue, ) diff --git a/app/src/main/java/de/medapp/wunddoku/data/repository/DocumentationRepository.kt b/app/src/main/java/de/medapp/wunddoku/data/repository/DocumentationRepository.kt index ab7933d..9650ce0 100644 --- a/app/src/main/java/de/medapp/wunddoku/data/repository/DocumentationRepository.kt +++ b/app/src/main/java/de/medapp/wunddoku/data/repository/DocumentationRepository.kt @@ -21,6 +21,7 @@ import de.medapp.wunddoku.domain.catalog.DocumentationCatalog import de.medapp.wunddoku.domain.model.Documentation import de.medapp.wunddoku.domain.model.DocumentationStatus import de.medapp.wunddoku.domain.model.DocumentationSummary +import de.medapp.wunddoku.domain.model.InputSpecCodec import de.medapp.wunddoku.domain.model.Photo import de.medapp.wunddoku.domain.model.Signature import de.medapp.wunddoku.media.ImageDownsampler @@ -191,6 +192,7 @@ class DocumentationRepository( isFreeText = item.isFreeText, isStaticHint = item.isStaticHint, freeTextValue = item.freeTextValue, + inputSpec = item.inputSpec, ) } ) @@ -399,10 +401,11 @@ class DocumentationRepository( * nachrichtlich an") - so bestätigt und akzeptiert, auch wenn das für einzelne Blöcke eine * zusätzliche, generisch benannte Freitext-Checkbox neben den fachlichen Einträgen bedeutet. * - * Katalog-Einträge werden 1:1 aus [BlockDefinition.items] übernommen: `isFreeText` und - * `isStaticHint` steuern direkt das Verhalten der jeweiligen Zeile (normale Checkbox, - * eigene benannte Freitext-Checkbox, oder nicht editierbarer Hinweistext) - keine - * block-spezifische Sonderlogik nötig, die Katalogdefinition allein entscheidet. + * Katalog-Einträge werden 1:1 aus [BlockDefinition.items] übernommen: `isFreeText`, + * `isStaticHint` und `inputSpec` steuern direkt das Verhalten der jeweiligen Zeile (normale + * Checkbox, eigene benannte Freitext-Checkbox, nicht editierbarer Hinweistext, oder + * strukturierte Eingabefelder) - keine block-spezifische Sonderlogik nötig, die + * Katalogdefinition allein entscheidet. * * Hinweistext-Einträge (`isStaticHint = true`) werden bereits HIER mit `checked = true` * angelegt (statt änderbar per UI-Checkbox) - dadurch greift die bestehende, unveränderte @@ -410,11 +413,14 @@ class DocumentationRepository( * Regel selbst angepasst werden musste. [de.medapp.wunddoku.domain.usecase.BlockEditedPolicy] * schließt solche Zeilen dagegen explizit aus der "bearbeitet"-Prüfung aus. * - * Die generische Freitext-Option erscheint als ERSTES Element im Block (itemOrder = 0, - * niedriger als jeder Katalog-Eintrag, dessen `order` bei 1 beginnt). Betrifft ausschließlich - * NEU erstellte Dokumentationen: bereits bestehende BlockState-/CheckItemState-Zeilen - * behalten ihre historisch gespeicherte itemOrder unverändert (Snapshot-Prinzip, s. - * Klassendokumentation zu titleSnapshot/labelSnapshot). + * Änderungswunsch: Die generische Freitext-Option erscheint jetzt als LETZTES Element im + * Block (itemOrder = höchste vorkommende Katalog-`order` + 1) - zuvor war sie das ERSTE + * Element (itemOrder = 0). Vom Katalog selbst definierte, eigens benannte Freitext-Einträge + * (isFreeText = true) behalten dagegen ihre im Katalog festgelegte Position, sofern der + * jeweilige Block das explizit so vorsieht. Betrifft ausschließlich NEU erstellte + * Dokumentationen: bereits bestehende BlockState-/CheckItemState-Zeilen behalten ihre + * historisch gespeicherte itemOrder unverändert (Snapshot-Prinzip, s. Klassendokumentation + * zu titleSnapshot/labelSnapshot). */ private suspend fun insertBlocksFromCatalog(documentationId: String, now: Long) { for (blockDef in DocumentationCatalog.blocks) { @@ -440,24 +446,29 @@ class DocumentationRepository( labelSnapshot = itemDef.label, itemOrder = itemDef.order, // Hinweistexte starten bereits "angehakt" (s. Methodenkommentar) - alle - // anderen Einträge (Checkbox oder eigene Freitext-Checkbox) starten - // unangehakt/leer, wie bisher. + // anderen Einträge (Checkbox, eigene Freitext-Checkbox oder strukturierte + // Eingabe) starten unangehakt/leer, wie bisher. checked = itemDef.isStaticHint, isFreeText = itemDef.isFreeText, isStaticHint = itemDef.isStaticHint, freeTextValue = "", + inputSpec = itemDef.inputSpec?.let(InputSpecCodec::encode) ?: "", ) } + // Änderungswunsch: generische Freitext-Option jetzt NACH allen Katalog-Einträgen + // dieses Blocks (s. Methodenkommentar) statt davor. + val genericFreeTextOrder = (blockDef.items.maxOfOrNull { it.order } ?: 0) + 1 val genericFreeTextItem = CheckItemStateEntity( id = UUID.randomUUID().toString(), blockStateId = blockStateId, checkItemDefinitionId = blockDef.freeTextDefinitionId, labelSnapshot = DocumentationCatalog.FREE_TEXT_LABEL, - itemOrder = 0, + itemOrder = genericFreeTextOrder, checked = false, isFreeText = true, isStaticHint = false, freeTextValue = "", + inputSpec = "", ) checkItemStateDao.insertAll(catalogItems + genericFreeTextItem) } diff --git a/app/src/main/java/de/medapp/wunddoku/domain/catalog/DocumentationCatalog.kt b/app/src/main/java/de/medapp/wunddoku/domain/catalog/DocumentationCatalog.kt index 063e1c2..0e163dc 100644 --- a/app/src/main/java/de/medapp/wunddoku/domain/catalog/DocumentationCatalog.kt +++ b/app/src/main/java/de/medapp/wunddoku/domain/catalog/DocumentationCatalog.kt @@ -1,64 +1,103 @@ package de.medapp.wunddoku.domain.catalog +import de.medapp.wunddoku.domain.model.InputFieldDef +import de.medapp.wunddoku.domain.model.InputSpec + /** * Zentrale, statische Definition aller Dokumentationsblöcke und ihrer Checkboxen. * * Enthält die mit dem Kunden final abgestimmten Dokumentationsabschnitte. Basis ist die - * handschriftlich vorgegebene 22-Punkte-Reihenfolge, ergänzt/verfeinert nach Abgleich mit dem - * ursprünglichen Papierformular ("Wunddokumentation" von Harald Klock) - dabei wurden: + * handschriftlich vorgegebene 22-Punkte-Reihenfolge, zunächst ergänzt/verfeinert nach Abgleich + * mit dem ursprünglichen Papierformular, und in dieser Fassung ein weiteres Mal überarbeitet + * nach konkreten Änderungswünschen (u. a. neue Checkbox-Listen für Wundart/Vitalwerte/ + * Auswirkungen aus Fotos, strukturierte Eingabefelder statt Freitext für Maße/Uhrzeiten/Tiefen, + * ein neuer Abschnitt "Wundheilung beeinflussende Faktoren"). * - * - mehrere Abschnitte, die im Chat/der handschriftlichen Liste nur als Freitext geführt waren, - * auf dem Papierbogen aber eine differenzierte Checkbox-Struktur zeigen, entsprechend - * erweitert (Wundgrund, Belag, Wundrand, Exsudat, Größe, Fistel/Tasche, Lokalisation, - * Empfohlene Wundversorgung), - * - zwei auf dem Papierbogen vorhandene, aber in der handschriftlichen Liste fehlende - * Abschnitte wieder aufgenommen ("Weitere Überlegung" - vermutlich versehentlich in der - * handschriftlichen Liste ausgelassen; "Nächster Termin" - auf dem Bogen als eigenes Feld - * vorhanden), - * - "Verlauf" bewusst NICHT um die alten Papier-Checkboxen erweitert (explizit vom Kunden - * bestätigt: bleibt reiner Freitext, die alte Checkbox-Auswahl wurde bewusst verworfen), - * - "Wundumgebung" und "Auswirkungen auf das alltägliche Leben" um die Papier-Checkbox-Listen - * ergänzt, jeweils MIT vorangestelltem freiem Textfeld (die ohnehin an jedem Block - * automatisch vorhandene generische Freitext-Option deckt das ab, s. [FREE_TEXT_LABEL]), - * - "Wundart" um die Papier-Checkbox "nicht bekannt/diagnostiziert" ergänzt (zusätzlich zum - * Freitext - NICHT Teil der früher verworfenen Wundarten-Liste, sondern ein eigenständiges - * Status-Flag). + * WICHTIGE GENERELLE REGEL (Änderungswunsch): Die automatisch an jeden Block angehängte + * generische Freitext-Option (s. [FREE_TEXT_LABEL]) erscheint jetzt als LETZTES Element eines + * Blocks (vorher: als erstes) - siehe [de.medapp.wunddoku.data.repository.DocumentationRepository.insertBlocksFromCatalog]. + * Vom Katalog selbst eigens benannte Freitext-Einträge (`isFreeText = true` an einem konkreten + * [CheckItemDefinition]) behalten dagegen ihre hier festgelegte Position. * * Frühere, im Kundenchat zwischenzeitlich diskutierte, aber später ausdrücklich verworfene - * Inhalte (Wundart-Typenliste, Verlauf-Checkboxen, "Schmerzempfinden 1-10") sind weiterhin - * bewusst NICHT enthalten. + * Inhalte (Wortlaut-Variante der alten Wundart-Checkboxen, "Schmerzempfinden 1-10") sind + * weiterhin bewusst NICHT enthalten. * * Diese Definitionen sind bewusst reiner Kotlin-Code und keine Room-Tabellen: Sie ändern sich * nur durch neue App-Versionen, nicht durch Nutzerinteraktion. Siehe Architektur-Dokument * Abschnitt 2.3b. * * [CATALOG_VERSION] wird beim Erstellen einer neuen Dokumentation als rein informatives - * Metadatum in Documentation.catalogVersion übernommen. Erhöhe diesen Wert, wenn sich Blöcke - * oder Checkboxen inhaltlich ändern (nicht bei rein redaktionellen Korrekturen wie - * Tippfehlern) – rein zu Nachvollziehbarkeitszwecken, nicht sicherheitsrelevant für die - * historische Korrektheit (das übernehmen die Label-/Titel-Snapshots). Auf 3 erhöht mit dem - * Abgleich gegen das ursprüngliche Papierformular. + * Metadatum in Documentation.catalogVersion übernommen. Auf 4 erhöht mit diesem Umbau. */ object DocumentationCatalog { - const val CATALOG_VERSION: Int = 3 + const val CATALOG_VERSION: Int = 4 /** - * Label der automatisch an jeden Block angehängten Freitext-Option (Änderungswunsch: - * "Freitext soll selbst wie eine weitere auswählbare Option behandelt werden"). Zentral - * hier definiert, damit Repository (Erstellung/Fortsetzen) und PDF-Export dieselbe - * Bezeichnung verwenden. Gilt weiterhin unverändert für ALLE Blöcke, auch für solche, die - * fachlich "nur Checkboxen" wären - ausdrücklich bestätigt und kein Fehler. + * Label der automatisch an jeden Block angehängten Freitext-Option. Zentral hier definiert, + * damit Repository (Erstellung/Fortsetzen) und PDF-Export dieselbe Bezeichnung verwenden. + * Gilt weiterhin unverändert für ALLE Blöcke, auch für solche, die fachlich "nur + * Checkboxen" wären - ausdrücklich bestätigt und kein Fehler. */ const val FREE_TEXT_LABEL: String = "Freitext" + // Wiederverwendete Auswahllisten für strukturierte Eingabefelder (s. InputFieldDef.Choice). + private val HOUR_OPTIONS: List = (1..12).map { it.toString() } + private val WEEKDAY_COUNT_OPTIONS: List = (1..7).map { it.toString() } + private val MINUTE_1_TO_15_OPTIONS: List = (1..15).map { it.toString() } + private val DEPTH_HALF_CM_OPTIONS: List = listOf( + "0,5 cm", "1 cm", "1,5 cm", "2 cm", "2,5 cm", "3 cm", "3,5 cm", "4 cm", + "4,5 cm", "5 cm", "5,5 cm", "6 cm", "6,5 cm", "7 cm", "7,5 cm", "8 cm", ">8 cm", + ) + private val DECUBITUS_GRADE_OPTIONS: List = listOf("I", "II", "III", "IV") + val blocks: List = listOf( BlockDefinition( id = "wundart", title = "Wundart", order = 1, items = listOf( - CheckItemDefinition(id = "wundart_nicht_bekannt_diagnostiziert", label = "nicht bekannt/diagnostiziert", order = 1), + CheckItemDefinition(id = "wundart_nicht_bekannt_diagnostiziert", label = "Noch nicht diagnostiziert / nicht bekannt", order = 1), + CheckItemDefinition(id = "wundart_erscheint_wie", label = "Erscheint wie", order = 2, isFreeText = true), + CheckItemDefinition( + id = "wundart_decubitus", + label = "Decubitus", + order = 3, + inputSpec = InputSpec( + pdfTemplate = "Decubitus Grad {0}", + fields = listOf(InputFieldDef.Choice(label = "Grad", options = DECUBITUS_GRADE_OPTIONS)), + ), + ), + CheckItemDefinition(id = "wundart_ulcus_cruris_unbezeichnet", label = "Ulcus Cruris (nicht näher bezeichnet)", order = 4), + CheckItemDefinition(id = "wundart_ulcus_cruris_venosum", label = "Ulcus Cruris ven.", order = 5), + 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_transplantatverlust", + label = "Wunde nach Transplantatverlust", + order = 15, + 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), ), ), BlockDefinition( @@ -74,18 +113,37 @@ object DocumentationCatalog { title = "Größe", order = 3, items = listOf( - CheckItemDefinition(id = "groesse_multiple_wunden_max", label = "multiple Wunden mit max. (cm)", order = 1, isFreeText = true), - CheckItemDefinition(id = "groesse_wunde", label = "Wunde", order = 2), - CheckItemDefinition(id = "groesse_areal", label = "Areal", order = 3), - CheckItemDefinition(id = "groesse_masse", label = "Maße (Länge x Breite, ca. cm)", order = 4, isFreeText = true), - CheckItemDefinition(id = "groesse_vertikale_tiefe", label = "Vertikale Tiefe (ca. cm)", order = 5, isFreeText = true), + CheckItemDefinition(id = "groesse_hinweis_masse", label = "Maße", order = 1, isStaticHint = true), + CheckItemDefinition( + id = "groesse_laenge_breite_tiefe", + label = "Maße (Länge / Breite / Tiefe)", + order = 2, + inputSpec = InputSpec( + pdfTemplate = "Länge {0} cm, Breite {1} cm, Tiefe {2} cm", + fields = listOf( + InputFieldDef.Number(label = "Länge (cm)", maxLength = 5, allowDecimal = true), + InputFieldDef.Number(label = "Breite (cm)", maxLength = 5, allowDecimal = true), + InputFieldDef.Number(label = "Tiefe (cm)", maxLength = 5, allowDecimal = true), + ), + ), + ), + CheckItemDefinition(id = "groesse_freitext_masse", label = "Freitext für die Maße", order = 3, isFreeText = true), + CheckItemDefinition(id = "groesse_multiple_wunden_max", label = "Multiple Wunden mit max. (cm)", order = 4, isFreeText = true), + CheckItemDefinition(id = "groesse_hinweis_areal", label = "Zu versorgendes Areal", order = 5, isStaticHint = true), + CheckItemDefinition(id = "groesse_areal", label = "Zu versorgendes Areal", order = 6), + CheckItemDefinition(id = "groesse_freitext_areal", label = "Freitext für zu versorgendes Areal", order = 7, isFreeText = true), ), ), BlockDefinition( id = "verlauf", title = "Verlauf", order = 4, - items = emptyList(), + items = listOf( + CheckItemDefinition(id = "verlauf_positiv", label = "Positiv", order = 1), + CheckItemDefinition(id = "verlauf_negativ", label = "Negativ", order = 2), + CheckItemDefinition(id = "verlauf_stagnierend", label = "Stagnierend", order = 3), + CheckItemDefinition(id = "verlauf_nicht_bekannt", label = "Nicht bekannt", order = 4), + ), ), BlockDefinition( id = "exsudat", @@ -114,21 +172,26 @@ object DocumentationCatalog { title = "Wundgrund + Erscheinung", order = 6, items = listOf( - CheckItemDefinition(id = "wundgrund_eben", label = "Eben", order = 1), - CheckItemDefinition(id = "wundgrund_unregelmaessig", label = "Unregelmäßig", order = 2), - CheckItemDefinition(id = "wundgrund_knospig", label = "Knospig", order = 3), - CheckItemDefinition(id = "wundgrund_auf_niveau", label = "Auf Niveau", order = 4), - CheckItemDefinition(id = "wundgrund_ueber_niveau", label = "Über Niveau", order = 5), - CheckItemDefinition(id = "wundgrund_glaenzend", label = "Glänzend", order = 6), - CheckItemDefinition(id = "wundgrund_grob", label = "Grob", order = 7), - CheckItemDefinition(id = "wundgrund_fein", label = "Fein", order = 8), - CheckItemDefinition(id = "wundgrund_glatt", label = "Glatt", order = 9), - CheckItemDefinition(id = "wundgrund_rot", label = "Rot", order = 10), - CheckItemDefinition(id = "wundgrund_blass", label = "Blass", order = 11), - CheckItemDefinition(id = "wundgrund_knochen_sehnen", label = "Knochen/Sehnen", order = 12), - CheckItemDefinition(id = "wundgrund_haemorrhagie", label = "Hämorrhagie", order = 13), - CheckItemDefinition(id = "wundgrund_nicht_kaum_einsehbar", label = "Nicht/kaum einsehbar", order = 14), - CheckItemDefinition(id = "wundgrund_erscheinung_erscheinung", label = "Erscheinung", order = 15, isFreeText = true), + 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), ), ), BlockDefinition( @@ -142,13 +205,14 @@ object DocumentationCatalog { CheckItemDefinition(id = "belag_fremdkoerper", label = "Fremdkörper", order = 4), CheckItemDefinition(id = "belag_nekrose", label = "Nekrose", order = 5), CheckItemDefinition(id = "belag_kruste", label = "Kruste", order = 6), - CheckItemDefinition(id = "belag_mit_fibrinsaum", label = "Mit Fibrinsaum", order = 7), - CheckItemDefinition(id = "belag_trocken", label = "Trocken", order = 8), - CheckItemDefinition(id = "belag_feucht", label = "Feucht", order = 9), - CheckItemDefinition(id = "belag_100_prozent", label = "100%", order = 10), - CheckItemDefinition(id = "belag_75_prozent", label = "75%", order = 11), - CheckItemDefinition(id = "belag_50_prozent", label = "50%", order = 12), - CheckItemDefinition(id = "belag_25_prozent_plus", label = "25%+<", order = 13), + CheckItemDefinition(id = "belag_biofilm", label = "Biofilm", order = 7), + CheckItemDefinition(id = "belag_mit_fibrinsaum", label = "Mit Fibrinsaum", order = 8), + CheckItemDefinition(id = "belag_trocken", label = "Trocken", order = 9), + CheckItemDefinition(id = "belag_feucht", label = "Feucht", order = 10), + CheckItemDefinition(id = "belag_100_prozent", label = "100%", order = 11), + CheckItemDefinition(id = "belag_75_prozent", label = "75%", order = 12), + CheckItemDefinition(id = "belag_50_prozent", label = "50%", order = 13), + CheckItemDefinition(id = "belag_25_prozent_plus", label = "25%+<", order = 14), ), ), BlockDefinition( @@ -190,10 +254,11 @@ object DocumentationCatalog { CheckItemDefinition(id = "wundumgebung_blaeulich", label = "Bläulich", order = 8), CheckItemDefinition(id = "wundumgebung_blass", label = "Blass", order = 9), CheckItemDefinition(id = "wundumgebung_hyperkeratose", label = "Hyperkeratose", order = 10), - CheckItemDefinition(id = "wundumgebung_pergamenthaut", label = "Pergamenthaut", order = 11), - CheckItemDefinition(id = "wundumgebung_rissig_kratzspuren", label = "Rissig/Kratzspuren", order = 12), - CheckItemDefinition(id = "wundumgebung_spannungsblasen", label = "Spannungsblasen", order = 13), - CheckItemDefinition(id = "wundumgebung_haematome", label = "Hämatome", order = 14), + CheckItemDefinition(id = "wundumgebung_sehr_trockene_haut", label = "Sehr trockene Haut", order = 11), + CheckItemDefinition(id = "wundumgebung_pergamenthaut", label = "Pergamenthaut", order = 12), + CheckItemDefinition(id = "wundumgebung_rissig_kratzspuren", label = "Rissig/Kratzspuren", order = 13), + CheckItemDefinition(id = "wundumgebung_spannungsblasen", label = "Spannungsblasen", order = 14), + CheckItemDefinition(id = "wundumgebung_haematome", label = "Hämatome", order = 15), ), ), BlockDefinition( @@ -204,8 +269,27 @@ object DocumentationCatalog { CheckItemDefinition(id = "fistel_tasche_nein", label = "Nein", order = 1), CheckItemDefinition(id = "fistel_tasche_ja", label = "Ja", order = 2), CheckItemDefinition(id = "fistel_tasche_zirkulaer", label = "Zirkulär", order = 3), - CheckItemDefinition(id = "fistel_tasche_von_bis_uhr", label = "Von ... bis ... Uhr", order = 4, isFreeText = true), - CheckItemDefinition(id = "fistel_tasche_tiefe", label = "Tiefe (ca. cm)", order = 5, isFreeText = true), + CheckItemDefinition( + id = "fistel_tasche_von_bis_uhr", + label = "Von ... bis ... Uhr", + order = 4, + inputSpec = InputSpec( + pdfTemplate = "Von {0} bis {1} Uhr", + fields = listOf( + InputFieldDef.Choice(label = "Von", options = HOUR_OPTIONS), + InputFieldDef.Choice(label = "Bis", options = HOUR_OPTIONS), + ), + ), + ), + CheckItemDefinition( + id = "fistel_tasche_tiefe", + label = "Tiefe", + order = 5, + inputSpec = InputSpec( + pdfTemplate = "Tiefe: {0}", + fields = listOf(InputFieldDef.Choice(label = "Tiefe", options = DEPTH_HALF_CM_OPTIONS)), + ), + ), ), ), BlockDefinition( @@ -264,28 +348,118 @@ object DocumentationCatalog { id = "vitalwerte", title = "Vitalwerte", order = 15, - items = emptyList(), + items = listOf( + CheckItemDefinition(id = "vitalwerte_arteria_tibialis_posterior_links", label = "Arteria tibialis posterior links", order = 1), + CheckItemDefinition(id = "vitalwerte_arteria_dorsalis_pedis_links", label = "Arteria dorsalis pedis links", order = 2), + CheckItemDefinition(id = "vitalwerte_arteria_tibialis_posterior_rechts", label = "Arteria tibialis posterior rechts", order = 3), + CheckItemDefinition(id = "vitalwerte_arteria_dorsalis_pedis_rechts", label = "Arteria dorsalis pedis rechts", order = 4), + CheckItemDefinition(id = "vitalwerte_per_doppler_taschendoppler", label = "per Doppler (Taschendoppler)", order = 5), + CheckItemDefinition(id = "vitalwerte_per_palpation", label = "per Palpation", order = 6), + CheckItemDefinition(id = "vitalwerte_darstellbar", label = "Darstellbar", order = 7), + CheckItemDefinition(id = "vitalwerte_abgeschwaecht_veraendert", label = "Abgeschwächt/verändert", order = 8), + CheckItemDefinition(id = "vitalwerte_nicht_darstellbar", label = "Nicht darstellbar", order = 9), + CheckItemDefinition( + id = "vitalwerte_rr", + label = "RR (systolisch/diastolisch)", + order = 10, + inputSpec = InputSpec( + pdfTemplate = "RR {0}/{1} mmHg", + fields = listOf( + InputFieldDef.Number(label = "RR systolisch", maxLength = 3), + InputFieldDef.Number(label = "RR diastolisch", maxLength = 3), + ), + ), + ), + CheckItemDefinition( + id = "vitalwerte_puls", + label = "Puls (BPM)", + order = 11, + inputSpec = InputSpec( + pdfTemplate = "Puls {0} BPM", + fields = listOf(InputFieldDef.Number(label = "Puls (BPM)", maxLength = 3)), + ), + ), + CheckItemDefinition( + id = "vitalwerte_blutzucker", + label = "Blutzucker (mg/dl)", + order = 12, + inputSpec = InputSpec( + pdfTemplate = "Blutzucker {0} mg/dl", + fields = listOf(InputFieldDef.Number(label = "Blutzucker (mg/dl)", maxLength = 3)), + ), + ), + CheckItemDefinition( + id = "vitalwerte_spo2", + label = "SpO2 (%)", + order = 13, + inputSpec = InputSpec( + pdfTemplate = "SpO2 {0}%", + fields = listOf(InputFieldDef.Number(label = "SpO2 (%)", maxLength = 3)), + ), + ), + CheckItemDefinition( + id = "vitalwerte_temperatur", + label = "Temperatur (°C)", + order = 14, + inputSpec = InputSpec( + pdfTemplate = "Temp. {0} °C", + fields = listOf(InputFieldDef.Number(label = "Temperatur (°C)", maxLength = 4, allowDecimal = true)), + ), + ), + ), + ), + BlockDefinition( + id = "wundheilung_beeinflussende_faktoren", + title = "Wundheilung beeinflussende Faktoren", + order = 16, + items = listOf( + CheckItemDefinition(id = "wbf_infektion_keimbesiedelung", label = "Infektion/Keimbesiedelung", order = 1), + CheckItemDefinition(id = "wbf_alter", label = "Alter", order = 2), + CheckItemDefinition(id = "wbf_diabetes_mellitus", label = "Diabetes mellitus", order = 3), + CheckItemDefinition(id = "wbf_geschwaechtes_immunsystem", label = "Geschwächtes Immunsystem", order = 4), + CheckItemDefinition(id = "wbf_ernaehrungszustand_dehydration", label = "Ernährungszustand/Dehydration", order = 5), + CheckItemDefinition(id = "wbf_medikamente", label = "Medikamente", order = 6), + CheckItemDefinition(id = "wbf_rauchen", label = "Rauchen", order = 7), + CheckItemDefinition(id = "wbf_adipositas", label = "Adipositas", order = 8), + CheckItemDefinition(id = "wbf_psych_stress_schlafmangel", label = "Psych. Stress/Schlafmangel", order = 9), + CheckItemDefinition(id = "wbf_mobilitaet", label = "Mobilität", order = 10), + CheckItemDefinition(id = "wbf_lage_der_wunde", label = "Lage der Wunde", order = 11), + ), ), BlockDefinition( id = "auswirkungen_alltag", title = "Auswirkungen auf das alltägliche Leben", - order = 16, + order = 17, items = listOf( - CheckItemDefinition(id = "auswirkungen_alltag_ja", label = "Ja", order = 1), - CheckItemDefinition(id = "auswirkungen_alltag_nein", label = "Nein", order = 2), - CheckItemDefinition(id = "auswirkungen_alltag_nicht_ermittelbar", label = "Nicht ermittelbar", order = 3), + CheckItemDefinition(id = "auswirkungen_alltag_nein", label = "Nein", order = 1), + CheckItemDefinition(id = "auswirkungen_alltag_schmerz", label = "Schmerz", order = 2), + CheckItemDefinition(id = "auswirkungen_alltag_eingeschraenkte_mobilitaet", label = "Eingeschränkte Mobilität", order = 3), + CheckItemDefinition(id = "auswirkungen_alltag_schlafstoerung", label = "Schlafstörung", order = 4), + CheckItemDefinition(id = "auswirkungen_alltag_koerperbildstoerung", label = "Körperbildstörung", order = 5), + CheckItemDefinition(id = "auswirkungen_alltag_geruch", label = "Geruch", order = 6), + CheckItemDefinition(id = "auswirkungen_alltag_sozialer_rueckzug", label = "Sozialer Rückzug", order = 7), + CheckItemDefinition(id = "auswirkungen_alltag_zeitaufwand", label = "Zeitaufwand", order = 8), + CheckItemDefinition(id = "auswirkungen_alltag_einschraenkung_kleidung_hygiene", label = "Einschränkung Kleidung/Hygiene", order = 9), + CheckItemDefinition(id = "auswirkungen_alltag_abhaengigkeit_von_dritten", label = "Abhängigkeit von Dritten (Pflegedienst/Ärzte/Angehörige)", order = 10), + CheckItemDefinition(id = "auswirkungen_alltag_angst", label = "Angst", order = 11), + CheckItemDefinition(id = "auswirkungen_alltag_hobbyverlust", label = "Hobbyverlust", order = 12), + CheckItemDefinition(id = "auswirkungen_alltag_berufliche_ausfaelle", label = "Berufliche Ausfälle", order = 13), + CheckItemDefinition(id = "auswirkungen_alltag_finanzielle_zusatzbelastung", label = "Finanzielle Zusatzbelastung", order = 14), ), ), BlockDefinition( id = "bekannte_allergie", title = "Bekannte Allergie", - order = 17, - items = emptyList(), + order = 18, + items = listOf( + CheckItemDefinition(id = "bekannte_allergie_ja", label = "Ja", order = 1), + CheckItemDefinition(id = "bekannte_allergie_nein", label = "Nein", order = 2), + ), ), BlockDefinition( id = "beratung_erfolgte", title = "Beratung erfolgte", - order = 18, + order = 19, items = listOf( CheckItemDefinition(id = "beratung_verbandwechsel", label = "Verbandwechsel", order = 1), CheckItemDefinition(id = "beratung_hygiene", label = "Hygiene", order = 2), @@ -298,7 +472,7 @@ object DocumentationCatalog { BlockDefinition( id = "weitere_ueberlegung", title = "Weitere Überlegung", - order = 19, + order = 20, items = listOf( CheckItemDefinition(id = "weitere_ueberlegung_hausarztvisite", label = "Hausarztvisite", order = 1), CheckItemDefinition(id = "weitere_ueberlegung_kompressionstherapie", label = "Kompressionstherapie", order = 2), @@ -321,20 +495,28 @@ object DocumentationCatalog { BlockDefinition( id = "sonstiges", title = "Sonstiges", - order = 20, + order = 21, items = emptyList(), ), BlockDefinition( id = "empfohlene_wundversorgung", title = "Empfohlene Wundversorgung", - order = 21, + order = 22, items = listOf( CheckItemDefinition(id = "empfohlene_wundversorgung_hinweis_reinigen_mit", label = "Reinigen mit:", order = 1, isStaticHint = true), 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), CheckItemDefinition(id = "empfohlene_wundversorgung_granudacyn", label = "Granudacyn", order = 5), - CheckItemDefinition(id = "empfohlene_wundversorgung_getraenkte_kompresse", label = "Getränkte Kompresse für ca. ... Min. auflegen", order = 6, isFreeText = true), + CheckItemDefinition( + id = "empfohlene_wundversorgung_getraenkte_kompresse", + label = "Getränkte Kompresse", + order = 6, + 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), @@ -343,24 +525,34 @@ object DocumentationCatalog { 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, isFreeText = true), CheckItemDefinition(id = "empfohlene_wundversorgung_hinweis_wundrandschutz_mit", label = "Wundrandschutz mit (bis 3x Wo.):", order = 14, isStaticHint = true), - CheckItemDefinition(id = "empfohlene_wundversorgung_secura", label = "Secura", order = 15), - CheckItemDefinition(id = "empfohlene_wundversorgung_cavilon", label = "Cavilon", order = 16), - CheckItemDefinition(id = "empfohlene_wundversorgung_hinweis_sekundaere_wundauflage", label = "Sekundäre Wundauflage:", order = 17, isStaticHint = true), - CheckItemDefinition(id = "empfohlene_wundversorgung_sekundaer_sterile_kompresse", label = "Sterile Kompresse", order = 18), - CheckItemDefinition(id = "empfohlene_wundversorgung_saugkompresse", label = "Saugkompresse", order = 19), - CheckItemDefinition(id = "empfohlene_wundversorgung_hinweis_fixieren_mit", label = "Fixieren mit:", order = 20, isStaticHint = true), - CheckItemDefinition(id = "empfohlene_wundversorgung_mullbinde", label = "Mullbinde", order = 21), - CheckItemDefinition(id = "empfohlene_wundversorgung_klebevlies", label = "Klebevlies", order = 22), - CheckItemDefinition(id = "empfohlene_wundversorgung_folienverband", label = "Folienverband", order = 23), - CheckItemDefinition(id = "empfohlene_wundversorgung_schlauchverband", label = "Schlauchverband", order = 24), - CheckItemDefinition(id = "empfohlene_wundversorgung_kohaesive_fixierbinde", label = "Kohäsive Fixierbinde", order = 25), - CheckItemDefinition(id = "empfohlene_wundversorgung_kompressionsverband", label = "Kompressionsverband", order = 26), - CheckItemDefinition(id = "empfohlene_wundversorgung_wechselintervall", label = "Wechselintervall", order = 27, isFreeText = true), - CheckItemDefinition(id = "empfohlene_wundversorgung_hinweis_bedarf", label = "+ bei Bedarf", order = 28, isStaticHint = true), + CheckItemDefinition(id = "empfohlene_wundversorgung_wundrandschutz_ja", label = "Ja", order = 15), + CheckItemDefinition(id = "empfohlene_wundversorgung_wundrandschutz_nein", label = "Nein", order = 16), + CheckItemDefinition( + id = "empfohlene_wundversorgung_wundrandschutz_tage_pro_woche", + label = "Wie oft pro Woche?", + order = 17, + inputSpec = InputSpec( + pdfTemplate = "{0}x pro Woche", + fields = listOf(InputFieldDef.Choice(label = "Tage pro Woche", options = WEEKDAY_COUNT_OPTIONS)), + ), + ), + 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, isFreeText = true), + CheckItemDefinition(id = "empfohlene_wundversorgung_kompressionsverband_system", label = "Kompressionsverband / System", order = 28), + CheckItemDefinition(id = "empfohlene_wundversorgung_wechselintervall", label = "Wechselintervall", order = 29, isFreeText = true), + CheckItemDefinition(id = "empfohlene_wundversorgung_hinweis_bedarf", label = "+ bei Bedarf", order = 30, isStaticHint = true), CheckItemDefinition( id = "empfohlene_wundversorgung_hinweis_abklaerung", label = "Generell: bei akuter Verschlechterung sofort ärztliche Abklärung", - order = 29, + order = 31, isStaticHint = true, ), ), @@ -368,7 +560,7 @@ object DocumentationCatalog { BlockDefinition( id = "ziel", title = "Ziel", - order = 22, + order = 23, items = listOf( CheckItemDefinition(id = "ziel_sauberes_wundmilieu", label = "Sauberes Wundmilieu", order = 1), CheckItemDefinition(id = "ziel_infektionsprophylaxe", label = "Infektionsprophylaxe", order = 2), @@ -381,12 +573,13 @@ object DocumentationCatalog { 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), ), ), BlockDefinition( id = "kopie_nachrichtlich_an", title = "Kopie nachrichtlich an", - order = 23, + order = 24, items = listOf( CheckItemDefinition(id = "kopie_nachrichtlich_an_arzt", label = "Ärztin / Arzt", order = 1, isFreeText = true), CheckItemDefinition(id = "kopie_nachrichtlich_an_pflege", label = "Pflege", order = 2, isFreeText = true), @@ -397,7 +590,7 @@ object DocumentationCatalog { BlockDefinition( id = "naechster_termin", title = "Nächster Termin", - order = 24, + order = 25, items = emptyList(), ), ) @@ -425,7 +618,7 @@ data class BlockDefinition( /** * Ein einzelner Katalog-Eintrag innerhalb eines Blocks. * - * Normalfall: reine Checkbox ([isFreeText] = false, [isStaticHint] = false). + * Normalfall: reine Checkbox ([isFreeText] = false, [isStaticHint] = false, [inputSpec] = null). * * [isFreeText] = true: eine vom Katalog selbst definierte, fachlich eigenständig benannte * Freitext-Checkbox (z. B. "Erscheinung", "Wechselintervall", oder ein Empfänger bei "Kopie @@ -437,10 +630,15 @@ data class BlockDefinition( * optische Zwischenüberschrift wie "Reinigen mit:") - [label] ist hier direkt der anzuzeigende * Text selbst, keine Checkbox-Beschriftung. * - * [isFreeText] und [isStaticHint] schließen sich fachlich gegenseitig aus (ein Eintrag ist - * entweder normale Checkbox, Freitext-Checkbox oder Hinweistext) - es gibt aber bewusst keine - * Laufzeitprüfung dafür, um die Katalogdefinition einfach zu halten; beide gleichzeitig auf - * `true` zu setzen ergibt schlicht keinen sinnvollen Katalogeintrag. + * [inputSpec] != null: dieser Eintrag besitzt statt eines einfachen Häkchens ein oder mehrere + * strukturierte Eingabefelder (Zahl oder Auswahlliste, z. B. "Von [ ] bis [ ] Uhr" oder + * "Länge/Breite/Tiefe") - s. [de.medapp.wunddoku.domain.model.InputSpec]. + * + * [isFreeText], [isStaticHint] und [inputSpec] schließen sich fachlich gegenseitig aus (ein + * Eintrag ist entweder normale Checkbox, Freitext-Checkbox, Hinweistext oder strukturierte + * Eingabe) - es gibt aber bewusst keine Laufzeitprüfung dafür, um die Katalogdefinition einfach + * zu halten; mehrere davon gleichzeitig zu setzen ergibt schlicht keinen sinnvollen + * Katalogeintrag. */ data class CheckItemDefinition( val id: String, @@ -448,4 +646,5 @@ data class CheckItemDefinition( val order: Int, val isFreeText: Boolean = false, val isStaticHint: Boolean = false, + val inputSpec: InputSpec? = null, ) diff --git a/app/src/main/java/de/medapp/wunddoku/domain/model/CheckItem.kt b/app/src/main/java/de/medapp/wunddoku/domain/model/CheckItem.kt index 83a9df5..98ca776 100644 --- a/app/src/main/java/de/medapp/wunddoku/domain/model/CheckItem.kt +++ b/app/src/main/java/de/medapp/wunddoku/domain/model/CheckItem.kt @@ -20,6 +20,11 @@ package de.medapp.wunddoku.domain.model * [isStaticHint]: reiner, nicht editierbarer Hinweistext (z. B. "+ bei Bedarf") - keine * Checkbox in der UI, [checked] ist bei solchen Zeilen technisch immer `true` (s. Entity- * Kommentar), zählt aber NICHT als Bearbeitung (s. [de.medapp.wunddoku.domain.usecase.BlockEditedPolicy]). + * + * [inputSpec]: nicht-leer, wenn dieser Eintrag statt eines einfachen Häkchens strukturierte + * Eingabefelder besitzt (Zahl/Auswahlliste) - roher, codierter String, s. [InputSpecCodec.decode]. + * Die eingegebenen Werte liegen weiterhin in [freeTextValue] (mehrere Felder durch ein + * Trennzeichen kombiniert, s. [InputSpec.decodeValues]/[InputSpec.encodeValues]). */ data class CheckItem( val checkItemStateId: String, @@ -30,4 +35,5 @@ data class CheckItem( val isFreeText: Boolean = false, val freeTextValue: String = "", val isStaticHint: Boolean = false, + val inputSpec: String = "", ) diff --git a/app/src/main/java/de/medapp/wunddoku/domain/model/InputSpec.kt b/app/src/main/java/de/medapp/wunddoku/domain/model/InputSpec.kt new file mode 100644 index 0000000..c34277b --- /dev/null +++ b/app/src/main/java/de/medapp/wunddoku/domain/model/InputSpec.kt @@ -0,0 +1,155 @@ +package de.medapp.wunddoku.domain.model + +/** + * Ein einzelnes Eingabefeld innerhalb eines strukturierten Katalog-Eintrags (s. [InputSpec]). + * + * Bewusst nur zwei Feldarten - genau das, was die Kundenvorgaben bisher benötigen: + * - [Number]: Zahleneingabe mit maximaler Zeichenzahl (z. B. Länge/Breite/Tiefe, Blutdruck) + * - [Choice]: Auswahl aus einer festen Liste (z. B. Uhrzeit-Position 1-12, Tiefe in 0,5-cm-Schritten) + */ +sealed interface InputFieldDef { + val label: String + + /** + * @param maxLength maximale Zeichenzahl (bei [allowDecimal] inklusive des Dezimaltrennzeichens) + * @param allowDecimal erlaubt genau ein Dezimalkomma (Punkt wird beim Tippen zu Komma) + */ + data class Number( + override val label: String, + val maxLength: Int, + val allowDecimal: Boolean = false, + ) : InputFieldDef + + data class Choice( + override val label: String, + val options: List, + ) : InputFieldDef +} + +/** + * Beschreibt einen Katalog-Eintrag, der statt eines einfachen Häkchens (oder Freitexts) ein oder + * mehrere strukturierte Eingabefelder besitzt - z. B. "Von [3] bis [5] Uhr" oder + * "Länge [ ] Breite [ ] Tiefe [ ]". + * + * Der Eintrag bleibt trotzdem eine ganz normale Checkbox-Zeile (s. CheckItem.checked): Sobald in + * einem Feld etwas eingegeben/gewählt wird, wird das Häkchen automatisch gesetzt (s. UI). Dadurch + * greifen BlockEditedPolicy und die "nur angehakte Elemente"-Regel im PDF-Export unverändert - + * es gibt keine zweite "bearbeitet"-Logik für diesen Eintragstyp. + * + * [pdfTemplate] bestimmt, wie der Eintrag im PDF ausgegeben wird; `{0}`, `{1}` ... werden durch die + * Werte der Felder (in Reihenfolge von [fields]) ersetzt, z. B. `"Von {0} bis {1} Uhr"`. + * + * Die Beschreibung wird beim Anlegen einer Dokumentation zusammen mit dem Eintrag gespeichert + * (Snapshot, s. [InputSpecCodec]) - spätere Katalogänderungen (andere Auswahlwerte, andere + * Textvorlage) verändern bestehende Dokumentationen dadurch nicht rückwirkend. + */ +data class InputSpec( + val pdfTemplate: String, + val fields: List, +) { + + /** Zerlegt den gespeicherten Gesamtwert (s. [encodeValues]) in genau [fields].size Einzelwerte. */ + fun decodeValues(stored: String): List { + val parts = if (stored.isEmpty()) emptyList() else stored.split(VALUE_SEPARATOR) + return List(fields.size) { index -> parts.getOrElse(index) { "" } } + } + + /** Fasst die Einzelwerte der Felder zu dem einen String zusammen, der in der Datenbank steht. */ + fun encodeValues(values: List): String = values.joinToString(VALUE_SEPARATOR.toString()) + + /** true, sobald mindestens ein Feld einen Wert enthält. */ + fun hasAnyValue(stored: String): Boolean = decodeValues(stored).any { it.isNotBlank() } + + /** + * Text für den PDF-Export: [pdfTemplate] mit den eingesetzten Feldwerten. Ein noch leeres + * Feld erscheint als "–", damit erkennbar bleibt, dass dort bewusst nichts gewählt wurde. + */ + fun formatForPdf(stored: String): String { + var result = pdfTemplate + decodeValues(stored).forEachIndexed { index, value -> + result = result.replace("{$index}", value.ifBlank { EMPTY_VALUE_PLACEHOLDER }) + } + return result + } + + companion object { + /** Trennzeichen zwischen den Werten mehrerer Felder (Steuerzeichen, kommt in Zahlen/Auswahlwerten nie vor). */ + const val VALUE_SEPARATOR: Char = '\u001F' + private const val EMPTY_VALUE_PLACEHOLDER = "–" + } +} + +/** + * (De-)Serialisierung von [InputSpec] in einen einzelnen String für die Datenbankspalte + * `check_item_states.inputSpec`. Bewusst ein schlichtes eigenes Format mit Steuerzeichen als + * Trennern statt JSON: reines Kotlin ohne Android-Klassen (jederzeit in normalen JUnit-Tests + * prüfbar) und ohne zusätzliche Abhängigkeit. Die verwendeten Trennzeichen kommen in Labels, + * Textvorlagen und Auswahlwerten nicht vor. + * + * Aufbau: `pdfTemplate ␞ feld1 ␞ feld2 ...`, jedes Feld `N␟label␟maxLength␟decimal` bzw. + * `C␟label␟option␝option␝...`. + */ +object InputSpecCodec { + private const val RECORD_SEPARATOR = '\u001E' + private const val UNIT_SEPARATOR = '\u001F' + private const val GROUP_SEPARATOR = '\u001D' + + fun encode(spec: InputSpec): String { + val records = mutableListOf(spec.pdfTemplate) + for (field in spec.fields) { + records += when (field) { + is InputFieldDef.Number -> listOf( + "N", + field.label, + field.maxLength.toString(), + if (field.allowDecimal) "1" else "0", + ).joinToString(UNIT_SEPARATOR.toString()) + + is InputFieldDef.Choice -> listOf( + "C", + field.label, + field.options.joinToString(GROUP_SEPARATOR.toString()), + ).joinToString(UNIT_SEPARATOR.toString()) + } + } + return records.joinToString(RECORD_SEPARATOR.toString()) + } + + /** Liefert `null` für einen leeren String (Eintrag ohne strukturierte Felder) oder bei nicht lesbaren Daten. */ + fun decode(encoded: String): InputSpec? { + if (encoded.isEmpty()) return null + val records = encoded.split(RECORD_SEPARATOR) + if (records.size < 2) return null + + val fields = records.drop(1).mapNotNull { record -> + val attributes = record.split(UNIT_SEPARATOR) + when (attributes.firstOrNull()) { + "N" -> { + val maxLength = attributes.getOrNull(2)?.toIntOrNull() + if (attributes.size >= 4 && maxLength != null) { + InputFieldDef.Number( + label = attributes[1], + maxLength = maxLength, + allowDecimal = attributes[3] == "1", + ) + } else { + null + } + } + + "C" -> if (attributes.size >= 3) { + InputFieldDef.Choice( + label = attributes[1], + options = attributes[2].split(GROUP_SEPARATOR), + ) + } else { + null + } + + else -> null + } + } + if (fields.isEmpty()) return null + return InputSpec(pdfTemplate = records.first(), fields = fields) + } +} diff --git a/app/src/main/java/de/medapp/wunddoku/pdf/AndroidPdfDocumentExporter.kt b/app/src/main/java/de/medapp/wunddoku/pdf/AndroidPdfDocumentExporter.kt index a32f2b1..8ec41bf 100644 --- a/app/src/main/java/de/medapp/wunddoku/pdf/AndroidPdfDocumentExporter.kt +++ b/app/src/main/java/de/medapp/wunddoku/pdf/AndroidPdfDocumentExporter.kt @@ -6,6 +6,7 @@ import android.graphics.Color import android.graphics.Paint import android.graphics.pdf.PdfDocument import android.text.TextPaint +import de.medapp.wunddoku.domain.model.InputSpecCodec import de.medapp.wunddoku.media.PhotoStorage import de.medapp.wunddoku.signature.SignatureStorage import kotlinx.coroutines.Dispatchers @@ -281,6 +282,22 @@ class AndroidPdfDocumentExporter( * angehakte Optionen enthält (s. Mapper), wäre ein "immer angekreuztes" Kästchen ohnehin * redundant - stattdessen ein einfacher, leicht eingerückter Label-Text je Option. */ private fun drawItem(cursor: PdfPageCursor, item: PdfExportItem, paints: ExportPaints) { + // Änderungswunsch: strukturierte Eingabefelder (Zahl/Auswahlliste, z. B. "Von ... bis + // ... Uhr") werden als EIN fertig formatierter Satz ausgegeben (s. + // InputSpec.formatForPdf) - kein Label+Doppelpunkt+separate Zusatzzeile wie bei + // Freitext, da die Textvorlage im Katalog bereits den vollständigen, lesbaren Satz + // vorgibt. + val decodedInputSpec = if (item.inputSpec.isNotEmpty()) InputSpecCodec.decode(item.inputSpec) else null + if (decodedInputSpec != null) { + cursor.drawText( + text = decodedInputSpec.formatForPdf(item.freeTextValue), + paint = paints.item, + indent = ITEM_INDENT, + spacingAfter = 3f, + ) + return + } + // Die Freitext-Option bekommt einen abschließenden Doppelpunkt, normale Optionen nicht. val labelText = if (item.isFreeText) "${item.label}:" else item.label diff --git a/app/src/main/java/de/medapp/wunddoku/pdf/DocumentationToPdfExportModelMapper.kt b/app/src/main/java/de/medapp/wunddoku/pdf/DocumentationToPdfExportModelMapper.kt index ea91483..269e5dd 100644 --- a/app/src/main/java/de/medapp/wunddoku/pdf/DocumentationToPdfExportModelMapper.kt +++ b/app/src/main/java/de/medapp/wunddoku/pdf/DocumentationToPdfExportModelMapper.kt @@ -40,6 +40,7 @@ object DocumentationToPdfExportModelMapper { label = item.label, isFreeText = item.isFreeText, freeTextValue = item.freeTextValue, + inputSpec = item.inputSpec, ) }, ) diff --git a/app/src/main/java/de/medapp/wunddoku/pdf/PdfExportModel.kt b/app/src/main/java/de/medapp/wunddoku/pdf/PdfExportModel.kt index 4dadbbf..94d0ae8 100644 --- a/app/src/main/java/de/medapp/wunddoku/pdf/PdfExportModel.kt +++ b/app/src/main/java/de/medapp/wunddoku/pdf/PdfExportModel.kt @@ -34,11 +34,17 @@ data class PdfExportBlock( ) /** Kein `checked`-Feld mehr: Da [PdfExportBlock.items] bereits ausschließlich angehakte - * Optionen enthält, wäre es immer `true` und damit bedeutungslos - s. Mapper-Filter. */ + * Optionen enthält, wäre es immer `true` und damit bedeutungslos - s. Mapper-Filter. + * + * [inputSpec]: nicht-leer, wenn dieser Eintrag strukturierte Eingabefelder besitzt (s. + * [de.medapp.wunddoku.domain.model.InputSpec]) - der Exporter formatiert in diesem Fall + * [freeTextValue] über [de.medapp.wunddoku.domain.model.InputSpec.formatForPdf] statt es als + * einfachen Zusatztext unter [label] auszugeben. */ data class PdfExportItem( val label: String, val isFreeText: Boolean, val freeTextValue: String, + val inputSpec: String = "", ) /** diff --git a/app/src/main/java/de/medapp/wunddoku/ui/documentation/edit/DocumentationEditScreen.kt b/app/src/main/java/de/medapp/wunddoku/ui/documentation/edit/DocumentationEditScreen.kt index 22637a5..073c005 100644 --- a/app/src/main/java/de/medapp/wunddoku/ui/documentation/edit/DocumentationEditScreen.kt +++ b/app/src/main/java/de/medapp/wunddoku/ui/documentation/edit/DocumentationEditScreen.kt @@ -10,6 +10,7 @@ import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row @@ -41,6 +42,8 @@ import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.OutlinedButton import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Scaffold @@ -67,6 +70,7 @@ import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Dialog import androidx.lifecycle.Lifecycle @@ -76,6 +80,8 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel import de.medapp.wunddoku.R import de.medapp.wunddoku.domain.model.CheckItem +import de.medapp.wunddoku.domain.model.InputFieldDef +import de.medapp.wunddoku.domain.model.InputSpecCodec import de.medapp.wunddoku.domain.model.DocumentationBlock import de.medapp.wunddoku.domain.model.Photo import de.medapp.wunddoku.domain.model.Signature @@ -771,6 +777,11 @@ private fun BlockAccordionItem( for (item in block.items) { when { item.isStaticHint -> StaticHintRow(item = item) + item.inputSpec.isNotEmpty() -> StructuredInputRow( + item = item, + onCheckedChanged = onCheckedChanged, + onFreeTextChanged = onFreeTextChanged, + ) item.isFreeText -> FreeTextCheckItemRow( item = item, onCheckedChanged = onCheckedChanged, @@ -844,3 +855,100 @@ private fun StaticHintRow(item: CheckItem) { color = MaterialTheme.colorScheme.onSurfaceVariant, ) } + +/** + * Ein Eintrag mit strukturierten Eingabefeldern statt eines einfachen Häkchens (Änderungswunsch, + * z. B. "Von [ ] bis [ ] Uhr" oder "Länge/Breite/Tiefe") - s. [de.medapp.wunddoku.domain.model.InputSpec]. + * Anders als bei [FreeTextCheckItemRow] gibt es hier KEINE eigene Checkbox zum Freischalten: die + * Felder sind immer direkt ausfüllbar, `checked` wird automatisch gesetzt, sobald mindestens ein + * Feld einen Wert hat - dieselbe Checkbox-Semantik wie überall sonst (BlockEditedPolicy, PDF- + * Filter), nur ohne zusätzlichen Tap zum "Freischalten" der Felder. + */ +@Composable +private fun StructuredInputRow( + item: CheckItem, + onCheckedChanged: (checkItemStateId: String, checked: Boolean) -> Unit, + onFreeTextChanged: (checkItemStateId: String, text: String) -> Unit, +) { + val spec = remember(item.inputSpec) { InputSpecCodec.decode(item.inputSpec) } ?: return + val values = remember(item.freeTextValue, spec) { spec.decodeValues(item.freeTextValue) } + + fun updateValue(fieldIndex: Int, newValue: String) { + val newValues = values.toMutableList().apply { this[fieldIndex] = newValue } + val encoded = spec.encodeValues(newValues) + onFreeTextChanged(item.checkItemStateId, encoded) + onCheckedChanged(item.checkItemStateId, spec.hasAnyValue(encoded)) + } + + Column(modifier = Modifier.fillMaxWidth()) { + Text(item.label, style = MaterialTheme.typography.bodyMedium) + Spacer(Modifier.height(4.dp)) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + spec.fields.forEachIndexed { index, field -> + Box(modifier = Modifier.weight(1f)) { + when (field) { + is InputFieldDef.Number -> OutlinedTextField( + value = values.getOrElse(index) { "" }, + onValueChange = { raw -> + var seenComma = false + val filtered = buildString { + for (c in raw) { + when { + c.isDigit() -> append(c) + field.allowDecimal && (c == ',' || c == '.') && !seenComma -> { + append(',') + seenComma = true + } + } + } + }.take(field.maxLength) + updateValue(index, filtered) + }, + label = { Text(field.label) }, + singleLine = true, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), + modifier = Modifier.fillMaxWidth(), + ) + + is InputFieldDef.Choice -> ChoiceField( + field = field, + selected = values.getOrElse(index) { "" }, + onSelected = { updateValue(index, it) }, + ) + } + } + } + } + } +} + +/** Einfache Auswahlliste (Tippen öffnet ein Dropdown) für [InputFieldDef.Choice]-Felder - bewusst + * ein schlichter Button + [DropdownMenu] statt `ExposedDropdownMenuBox`, um ohne zusätzliche + * experimentelle Material3-APIs auszukommen. */ +@Composable +private fun ChoiceField( + field: InputFieldDef.Choice, + selected: String, + onSelected: (String) -> Unit, +) { + var expanded by remember { mutableStateOf(false) } + Box(modifier = Modifier.fillMaxWidth()) { + OutlinedButton(onClick = { expanded = true }, modifier = Modifier.fillMaxWidth()) { + Text(selected.ifBlank { field.label }, maxLines = 1) + } + DropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) { + field.options.forEach { option -> + DropdownMenuItem( + text = { Text(option) }, + onClick = { + onSelected(option) + expanded = false + }, + ) + } + } + } +} -- 2.47.3