Beta 1.0 Release #11

Merged
niklas.ortmann merged 24 commits from dev into main 2026-10-03 00:37:20 +02:00
10 changed files with 975 additions and 57 deletions
Showing only changes of commit 7948d2c3c6 - Show all commits
@@ -0,0 +1,629 @@
{
"formatVersion": 1,
"database": {
"version": 5,
"identityHash": "12448571f5ffcf00398fa1d8260c9b2b",
"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, 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
}
],
"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, '12448571f5ffcf00398fa1d8260c9b2b')"
]
}
}
@@ -44,6 +44,10 @@ import de.medapp.wunddoku.data.entity.converter.DocumentationStatusConverter
* Version 4 (vorher 3, Änderungswunsch): neue, eigenständige Tabelle `wound_carers`
* (Namensvorschläge + Standard-Vorbelegung im Signatur-Dialog, s. [WoundCarerEntity]) - rein
* additiv ([MIGRATION_3_4]), keine bestehenden Tabellen betroffen.
*
* 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.
*/
@Database(
entities = [
@@ -55,7 +59,7 @@ import de.medapp.wunddoku.data.entity.converter.DocumentationStatusConverter
SignatureEntity::class,
WoundCarerEntity::class,
],
version = 4,
version = 5,
exportSchema = true,
)
@TypeConverters(DocumentationStatusConverter::class)
@@ -83,7 +87,7 @@ abstract class AppDatabase : RoomDatabase() {
DATABASE_NAME,
)
// Kein fallbackToDestructiveMigration() mehr ab Version 3 - s. Klassenkommentar.
.addMigrations(MIGRATION_2_3, MIGRATION_3_4)
.addMigrations(MIGRATION_2_3, MIGRATION_3_4, MIGRATION_4_5)
.build().also { instance = it }
}
}
@@ -111,3 +111,23 @@ val MIGRATION_3_4 = object : Migration(3, 4) {
)
}
}
/**
* Migration von Schema-Version 4 auf 5 (finale fachliche Katalog-Inhalte: neuer Content-Typ
* "statischer Hinweistext", z. B. "+ bei Bedarf" bei "Empfohlene Wundversorgung").
*
* Rein additiv - eine einzelne neue, NOT NULL-Spalte mit Default `false` (0) auf einer
* bestehenden Tabelle. Anders als bei der Signaturen-Migration (2->3, dort war ein
* NULL->NOT-NULL-Wechsel auf einer bereits befüllten Spalte nötig) ist hier ein einfaches
* `ALTER TABLE ... ADD COLUMN` mit Default-Wert ausreichend und von SQLite direkt unterstützt -
* kein Tabellenneubau nötig. Bestehende Zeilen erhalten automatisch `isStaticHint = 0`
* (entspricht ihrem bisherigen, unveränderten Verhalten als normale Checkbox- bzw.
* Freitext-Zeile).
*/
val MIGRATION_4_5 = object : Migration(4, 5) {
override fun migrate(db: SupportSQLiteDatabase) {
db.execSQL(
"ALTER TABLE `check_item_states` ADD COLUMN `isStaticHint` INTEGER NOT NULL DEFAULT 0"
)
}
}
@@ -15,6 +15,18 @@ import androidx.room.PrimaryKey
* zusätzliche CheckItemStateEntity mit `isFreeText = true` - das ist die Freitext-Option des
* Blocks (Änderungswunsch: Freitext als weitere auswählbare Option statt eigenes Feld am
* Block). Für normale Checkbox-Optionen bleibt `isFreeText = false` und `freeTextValue` leer.
* Derselbe Mechanismus wird jetzt auch für vom KATALOG selbst definierte Freitext-Checkboxen
* verwendet (z. B. "Wundgrund"/"Erscheinung" oder die vier Empfänger-Freitexte bei "Kopie
* 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.
* "+ 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.
*/
@Entity(
tableName = "check_item_states",
@@ -40,4 +52,5 @@ data class CheckItemStateEntity(
val checked: Boolean,
val isFreeText: Boolean = false,
val freeTextValue: String = "",
val isStaticHint: Boolean = false,
)
@@ -71,6 +71,7 @@ fun CheckItemStateEntity.toDomain(): CheckItem = CheckItem(
order = itemOrder,
checked = checked,
isFreeText = isFreeText,
isStaticHint = isStaticHint,
freeTextValue = freeTextValue,
)
@@ -185,6 +185,7 @@ class DocumentationRepository(
itemOrder = item.itemOrder,
checked = item.checked,
isFreeText = item.isFreeText,
isStaticHint = item.isStaticHint,
freeTextValue = item.freeTextValue,
)
}
@@ -376,16 +377,30 @@ class DocumentationRepository(
/**
* Legt für jeden Katalog-Block einen BlockState an, sowie je ein CheckItemState pro
* Katalog-Checkbox PLUS eine zusätzliche, automatisch angehängte Freitext-Option
* (isFreeText = true) - siehe DocumentationCatalog.FREE_TEXT_LABEL und
* BlockDefinition.freeTextDefinitionId.
* Katalog-Eintrag PLUS eine zusätzliche, automatisch angehängte generische Freitext-Option
* (isFreeText = true, Label "Freitext") - siehe DocumentationCatalog.FREE_TEXT_LABEL und
* BlockDefinition.freeTextDefinitionId. Diese generische Option wird WEITERHIN für JEDEN
* Block angehängt, unabhängig davon, ob der Block selbst bereits eigene, fachlich benannte
* `isFreeText`-Einträge definiert (z. B. "Wundgrund + Erscheinung" oder "Kopie
* 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.
*
* Änderungswunsch: Die Freitext-Option erscheint als ERSTES Element im Block (itemOrder = 0,
* niedriger als jede Katalog-Checkbox, deren `order` bei 1 beginnt) - Vorbereitung dafür,
* dass echte Multiple-Choice-Varianten später als weitere Optionen NACH dem Freitext folgen
* können. 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).
* 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.
*
* Hinweistext-Einträge (`isStaticHint = true`) werden bereits HIER mit `checked = true`
* angelegt (statt änderbar per UI-Checkbox) - dadurch greift die bestehende, unveränderte
* "nur angehakte Elemente"-Regel im PDF-Export automatisch auch für sie, ohne dass diese
* 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).
*/
private suspend fun insertBlocksFromCatalog(documentationId: String, now: Long) {
for (blockDef in DocumentationCatalog.blocks) {
@@ -403,19 +418,23 @@ class DocumentationRepository(
)
)
val normalItems = blockDef.items.map { itemDef ->
val catalogItems = blockDef.items.map { itemDef ->
CheckItemStateEntity(
id = UUID.randomUUID().toString(),
blockStateId = blockStateId,
checkItemDefinitionId = itemDef.id,
labelSnapshot = itemDef.label,
itemOrder = itemDef.order,
checked = false,
isFreeText = false,
// Hinweistexte starten bereits "angehakt" (s. Methodenkommentar) - alle
// anderen Einträge (Checkbox oder eigene Freitext-Checkbox) starten
// unangehakt/leer, wie bisher.
checked = itemDef.isStaticHint,
isFreeText = itemDef.isFreeText,
isStaticHint = itemDef.isStaticHint,
freeTextValue = "",
)
}
val freeTextItem = CheckItemStateEntity(
val genericFreeTextItem = CheckItemStateEntity(
id = UUID.randomUUID().toString(),
blockStateId = blockStateId,
checkItemDefinitionId = blockDef.freeTextDefinitionId,
@@ -423,9 +442,10 @@ class DocumentationRepository(
itemOrder = 0,
checked = false,
isFreeText = true,
isStaticHint = false,
freeTextValue = "",
)
checkItemStateDao.insertAll(normalItems + freeTextItem)
checkItemStateDao.insertAll(catalogItems + genericFreeTextItem)
}
}
}
@@ -3,10 +3,11 @@ package de.medapp.wunddoku.domain.catalog
/**
* Zentrale, statische Definition aller Dokumentationsblöcke und ihrer Checkboxen.
*
* WICHTIG: Die hier verwendeten Inhalte sind ausschließlich neutrale Platzhalter
* (Vorgabe aus der Aufgabenstellung, Abschnitt 25: "Erfinde keine medizinischen Inhalte").
* Die späteren echten Inhalte (ca. 20 fachliche Blöcke) werden ausschließlich in dieser
* Datei ergänzt – keine andere Stelle im Code (UI, Datenbank) muss dafür geändert werden.
* Enthält die mit dem Kunden final abgestimmten 22 Dokumentationsabschnitte in der
* verbindlichen, handschriftlich vorgegebenen Reihenfolge (nicht alphabetisch, nicht die
* frühere Chat-Reihenfolge). Frühere, im Kundenchat zwischenzeitlich diskutierte, aber später
* ausdrücklich verworfene Inhalte (u. a. Wundart-Checkboxen, Verlauf-Checkboxen,
* "Schmerzempfinden 1-10", "Weitere Überlegungen") sind 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
@@ -16,51 +17,230 @@ package de.medapp.wunddoku.domain.catalog
* 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).
* historische Korrektheit (das übernehmen die Label-/Titel-Snapshots). Auf 2 erhöht mit
* Einführung dieser finalen fachlichen Inhalte (vorher: neutrale Platzhalter "Block 1"-"Block 3").
*/
object DocumentationCatalog {
const val CATALOG_VERSION: Int = 1
const val CATALOG_VERSION: Int = 2
/**
* 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 ggf. der spätere PDF-Export
* dieselbe Bezeichnung verwenden.
* 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
* laut Kundenvorgabe fachlich "nur Checkboxen" wären (z. B. Entzündungszeichen, Geruch,
* Schmerz, Juckreiz) - das wurde ausdrücklich so bestätigt und ist kein Fehler.
*/
const val FREE_TEXT_LABEL: String = "Freitext"
val blocks: List<BlockDefinition> = listOf(
BlockDefinition(
id = "block_01",
title = "Block 1",
id = "wundart",
title = "Wundart",
order = 1,
items = listOf(
CheckItemDefinition(id = "block_01_a", label = "Option A", order = 1),
CheckItemDefinition(id = "block_01_b", label = "Option B", order = 2),
),
items = emptyList(),
),
BlockDefinition(
id = "block_02",
title = "Block 2",
id = "lokalisation",
title = "Lokalisation",
order = 2,
items = emptyList(),
),
BlockDefinition(
id = "groesse",
title = "Größe",
order = 3,
items = emptyList(),
),
BlockDefinition(
id = "verlauf",
title = "Verlauf",
order = 4,
items = emptyList(),
),
BlockDefinition(
id = "exsudat",
title = "Exsudat",
order = 5,
items = listOf(
CheckItemDefinition(id = "block_02_a", label = "Option A", order = 1),
CheckItemDefinition(id = "block_02_b", label = "Option B", order = 2),
CheckItemDefinition(id = "block_02_c", label = "Option C", order = 3),
CheckItemDefinition(id = "exsudat_kein", label = "Kein", order = 1),
CheckItemDefinition(id = "exsudat_wenig", label = "Wenig", order = 2),
CheckItemDefinition(id = "exsudat_maessig", label = "Mäßig", order = 3),
CheckItemDefinition(id = "exsudat_viel", label = "Viel", order = 4),
CheckItemDefinition(id = "exsudat_uebermaessig", label = "Übermäßig", order = 5),
),
),
BlockDefinition(
id = "block_03",
title = "Block 3",
order = 3,
id = "wundgrund_erscheinung",
title = "Wundgrund + Erscheinung",
order = 6,
items = listOf(
CheckItemDefinition(id = "block_03_a", label = "Option A", order = 1),
CheckItemDefinition(id = "block_03_b", label = "Option B", order = 2),
CheckItemDefinition(id = "wundgrund_erscheinung_wundgrund", label = "Wundgrund", order = 1, isFreeText = true),
CheckItemDefinition(id = "wundgrund_erscheinung_erscheinung", label = "Erscheinung", order = 2, isFreeText = true),
),
),
BlockDefinition(
id = "belag",
title = "Belag",
order = 7,
items = emptyList(),
),
BlockDefinition(
id = "wundrand",
title = "Wundrand",
order = 8,
items = emptyList(),
),
BlockDefinition(
id = "wundumgebung",
title = "Wundumgebung",
order = 9,
items = emptyList(),
),
BlockDefinition(
id = "fistel_tasche",
title = "Fistel / Tasche",
order = 10,
items = emptyList(),
),
BlockDefinition(
id = "entzuendungszeichen",
title = "Entzündungszeichen",
order = 11,
items = listOf(
CheckItemDefinition(id = "entzuendungszeichen_nein", label = "Nein", order = 1),
CheckItemDefinition(id = "entzuendungszeichen_temperatur_erhoeht", label = "Temperatur erhöht", order = 2),
CheckItemDefinition(id = "entzuendungszeichen_schwellung", label = "Schwellung", order = 3),
CheckItemDefinition(id = "entzuendungszeichen_roetung", label = "Rötung", order = 4),
CheckItemDefinition(id = "entzuendungszeichen_funktionsstoerung", label = "Funktionsstörung", order = 5),
CheckItemDefinition(id = "entzuendungszeichen_schmerz", label = "Schmerz", order = 6),
),
),
BlockDefinition(
id = "geruch",
title = "Geruch",
order = 12,
items = listOf(
CheckItemDefinition(id = "geruch_ja", label = "Ja", order = 1),
CheckItemDefinition(id = "geruch_nein", label = "Nein", order = 2),
CheckItemDefinition(id = "geruch_gering", label = "Gering", order = 3),
CheckItemDefinition(id = "geruch_deutlich_wahrnehmbar", label = "Deutlich wahrnehmbar", order = 4),
),
),
BlockDefinition(
id = "schmerz",
title = "Schmerz",
order = 13,
items = listOf(
CheckItemDefinition(id = "schmerz_ja", label = "Ja", order = 1),
CheckItemDefinition(id = "schmerz_nein", label = "Nein", order = 2),
CheckItemDefinition(id = "schmerz_immer", label = "Immer", order = 3),
CheckItemDefinition(id = "schmerz_zeitweise", label = "Zeitweise", order = 4),
CheckItemDefinition(id = "schmerz_bei_verbandwechsel", label = "Bei Verbandwechsel", order = 5),
CheckItemDefinition(id = "schmerz_vorwiegend_nachts", label = "Vorwiegend nachts", order = 6),
CheckItemDefinition(id = "schmerz_wunde", label = "Wunde", order = 7),
CheckItemDefinition(id = "schmerz_umgebung", label = "Umgebung", order = 8),
),
),
BlockDefinition(
id = "juckreiz",
title = "Juckreiz",
order = 14,
items = listOf(
CheckItemDefinition(id = "juckreiz_ja", label = "Ja", order = 1),
CheckItemDefinition(id = "juckreiz_nein", label = "Nein", order = 2),
CheckItemDefinition(id = "juckreiz_wunde", label = "Wunde", order = 3),
CheckItemDefinition(id = "juckreiz_umgebung", label = "Umgebung", order = 4),
CheckItemDefinition(id = "juckreiz_immer_oft", label = "Immer / oft", order = 5),
CheckItemDefinition(id = "juckreiz_zeitweise", label = "Zeitweise", order = 6),
),
),
BlockDefinition(
id = "vitalwerte",
title = "Vitalwerte",
order = 15,
items = emptyList(),
),
BlockDefinition(
id = "auswirkungen_alltag",
title = "Auswirkungen auf das alltägliche Leben",
order = 16,
items = emptyList(),
),
BlockDefinition(
id = "bekannte_allergie",
title = "Bekannte Allergie",
order = 17,
items = emptyList(),
),
BlockDefinition(
id = "beratung_erfolgte",
title = "Beratung erfolgte",
order = 18,
items = listOf(
CheckItemDefinition(id = "beratung_verbandwechsel", label = "Verbandwechsel", order = 1),
CheckItemDefinition(id = "beratung_hygiene", label = "Hygiene", order = 2),
CheckItemDefinition(id = "beratung_ernaehrung", label = "Ernährung", order = 3),
CheckItemDefinition(id = "beratung_lagerung_druckentlastung", label = "Lagerung / Druckentlastung", order = 4),
CheckItemDefinition(id = "beratung_kleidung_schuhe", label = "Kleidung / Schuhe", order = 5),
CheckItemDefinition(id = "beratung_hautpflege", label = "Hautpflege", order = 6),
),
),
BlockDefinition(
id = "sonstiges",
title = "Sonstiges",
order = 19,
items = emptyList(),
),
BlockDefinition(
id = "empfohlene_wundversorgung",
title = "Empfohlene Wundversorgung",
order = 20,
items = listOf(
CheckItemDefinition(
id = "empfohlene_wundversorgung_hinweis_bedarf",
label = "+ bei Bedarf",
order = 1,
isStaticHint = true,
),
CheckItemDefinition(
id = "empfohlene_wundversorgung_hinweis_abklaerung",
label = "Generell: bei akuter Verschlechterung sofort ärztliche Abklärung",
order = 2,
isStaticHint = true,
),
),
),
BlockDefinition(
id = "ziel",
title = "Ziel",
order = 21,
items = listOf(
CheckItemDefinition(id = "ziel_sauberes_wundmilieu", label = "Sauberes Wundmilieu", order = 1),
CheckItemDefinition(id = "ziel_infektionsprophylaxe", label = "Infektionsprophylaxe", order = 2),
CheckItemDefinition(id = "ziel_abpolsterung", label = "Abpolsterung", order = 3),
CheckItemDefinition(id = "ziel_schmerzreduktion", label = "Schmerzreduktion", order = 4),
CheckItemDefinition(id = "ziel_angepasstes_exsudatmanagement", label = "Angepasstes Exsudatmanagement", order = 5),
CheckItemDefinition(id = "ziel_stabile_wundverhaeltnisse", label = "Stabile Wundverhältnisse", order = 6),
CheckItemDefinition(id = "ziel_abheilung", label = "Abheilung", order = 7),
CheckItemDefinition(id = "ziel_geruchsreduzierung", label = "Geruchsreduzierung", order = 8),
CheckItemDefinition(id = "ziel_einfache_durchfuehrung", label = "Einfache Durchführung", order = 9),
CheckItemDefinition(id = "ziel_wundrandschutz", label = "Wundrandschutz", order = 10),
CheckItemDefinition(id = "ziel_aufbruch_vorhandenes_material", label = "Aufbruch des vorhandenen Materials", order = 11),
),
),
BlockDefinition(
id = "kopie_nachrichtlich_an",
title = "Kopie nachrichtlich an",
order = 22,
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),
CheckItemDefinition(id = "kopie_nachrichtlich_an_patient", label = "Patient", order = 3, isFreeText = true),
CheckItemDefinition(id = "kopie_nachrichtlich_an_sonstige", label = "Sonstige", order = 4, isFreeText = true),
),
),
// Weitere Blöcke (insgesamt ca. 20) werden hier ergänzt, sobald die fachlichen
// Inhalte geliefert werden. Struktur und Reihenfolge bleiben identisch.
)
fun findBlock(blockDefinitionId: String): BlockDefinition? =
@@ -74,16 +254,38 @@ data class BlockDefinition(
val items: List<CheckItemDefinition>,
) {
/**
* Stabile, aus der Block-ID abgeleitete ID für die automatisch angehängte Freitext-Option
* dieses Blocks (siehe DocumentationCatalog.FREE_TEXT_LABEL). Kein Bestandteil von [items],
* da die Freitext-Option kein fachlicher Katalog-Eintrag ist, sondern strukturell zu jedem
* Block gehört.
* Stabile, aus der Block-ID abgeleitete ID für die automatisch angehängte, generische
* Freitext-Option dieses Blocks (siehe DocumentationCatalog.FREE_TEXT_LABEL). Kein
* Bestandteil von [items], da diese generische Freitext-Option kein fachlicher
* Katalog-Eintrag ist, sondern strukturell zu JEDEM Block gehört - unabhängig davon, ob
* der Block selbst bereits eigene `isFreeText`-Einträge in [items] definiert.
*/
val freeTextDefinitionId: String get() = "${id}_freetext"
}
/**
* Ein einzelner Katalog-Eintrag innerhalb eines Blocks.
*
* Normalfall: reine Checkbox ([isFreeText] = false, [isStaticHint] = false).
*
* [isFreeText] = true: eine vom Katalog selbst definierte, fachlich eigenständig benannte
* Freitext-Checkbox (z. B. "Wundgrund", "Erscheinung", oder ein Empfänger bei "Kopie
* nachrichtlich an") - technisch identisch zur automatisch an JEDEN Block angehängten
* generischen Freitext-Option (s. DocumentationRepository.insertBlocksFromCatalog), nur mit
* eigenem Label statt "Freitext". Ein Block kann beliebig viele solcher Einträge definieren.
*
* [isStaticHint] = true: reiner, nicht editierbarer Hinweistext (z. B. "+ bei Bedarf") - [label]
* ist hier direkt der anzuzeigende Hinweistext 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.
*/
data class CheckItemDefinition(
val id: String,
val label: String,
val order: Int,
val isFreeText: Boolean = false,
val isStaticHint: Boolean = false,
)
@@ -9,12 +9,17 @@ package de.medapp.wunddoku.domain.model
* historische Dokumentationen unabhängig von späteren Katalog-Änderungen unverändert korrekt
* dargestellt werden.
*
* [isFreeText]/[freeTextValue]: Der Freitext eines Blocks wird NICHT mehr als separates Feld am
* Block geführt, sondern als eine weitere, besondere Checkbox-Option behandelt - jeder Block
* bekommt automatisch genau ein CheckItem mit `isFreeText = true`. Ist [isFreeText] `false`,
* ist [freeTextValue] stets leer und ohne Bedeutung. Diese Struktur erlaubt es dem späteren
* PDF-Export, normale Optionen, die Freitext-Option und deren Inhalt eindeutig zu
* unterscheiden, ohne eine Sonderlogik außerhalb des bestehenden Checkbox-Modells zu benötigen.
* [isFreeText]/[freeTextValue]: Der Freitext eines Blocks wird NICHT als separates Feld am
* Block geführt, sondern als eine weitere, besondere Checkbox-Option behandelt. Jeder Block
* bekommt automatisch genau ein CheckItem mit `isFreeText = true`; zusätzlich kann der Katalog
* selbst beliebig viele weitere `isFreeText = true`-Optionen mit eigenem, fachlichem Label
* definieren (z. B. "Wundgrund", "Erscheinung", oder die vier Empfänger bei "Kopie
* nachrichtlich an") - technisch identisch, nur mit eigenem Label statt des generischen
* "Freitext". Ist [isFreeText] `false`, ist [freeTextValue] stets leer und ohne Bedeutung.
*
* [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]).
*/
data class CheckItem(
val checkItemStateId: String,
@@ -24,4 +29,5 @@ data class CheckItem(
val checked: Boolean,
val isFreeText: Boolean = false,
val freeTextValue: String = "",
val isStaticHint: Boolean = false,
)
@@ -23,8 +23,15 @@ object BlockEditedPolicy {
* nötig: Das Freitextfeld ist in der UI deaktiviert, solange seine Checkbox nicht angehakt
* ist, ein Ankreuzen zählt also bereits als bewusste Bearbeitung, unabhängig davon, ob schon
* Text eingegeben wurde.
*
* EINZIGE Ausnahme: Hinweistext-Zeilen (CheckItem.isStaticHint = true) werden hierbei
* IGNORIERT, obwohl ihr `checked` technisch immer `true` ist (s. Entity-Kommentar zu
* isStaticHint) - ein reiner, nicht editierbarer Hinweistext ist keine Nutzereingabe und
* darf einen Block nicht allein dadurch als "bearbeitet" erscheinen lassen (z. B.
* "Empfohlene Wundversorgung" enthält zwei feste Hinweiszeilen, soll aber nur dann als
* bearbeitet gelten, wenn tatsächlich eine Empfehlung eingetragen wurde).
*/
fun isEdited(block: DocumentationBlock): Boolean {
return block.items.any { it.checked }
return block.items.any { it.checked && !it.isStaticHint }
}
}
@@ -59,6 +59,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
@@ -675,14 +676,14 @@ private fun BlockAccordionItem(
verticalArrangement = Arrangement.spacedBy(12.dp),
) {
for (item in block.items) {
if (item.isFreeText) {
FreeTextCheckItemRow(
when {
item.isStaticHint -> StaticHintRow(item = item)
item.isFreeText -> FreeTextCheckItemRow(
item = item,
onCheckedChanged = onCheckedChanged,
onFreeTextChanged = onFreeTextChanged,
)
} else {
CheckItemRow(item = item, onCheckedChanged = onCheckedChanged)
else -> CheckItemRow(item = item, onCheckedChanged = onCheckedChanged)
}
}
}
@@ -735,3 +736,18 @@ private fun FreeTextCheckItemRow(
)
}
}
/**
* Ein rein anzeigender, nicht editierbarer Hinweistext (z. B. "+ bei Bedarf" bei "Empfohlene
* Wundversorgung") - keine Checkbox, keine Interaktion, [item.label] ist direkt der
* anzuzeigende Text selbst. Optisch dezent (kursiv, gedämpfte Farbe) abgesetzt von echten,
* ausfüllbaren Optionen, damit auf den ersten Blick klar ist, dass hier nichts einzugeben ist.
*/
@Composable
private fun StaticHintRow(item: CheckItem) {
Text(
item.label,
style = MaterialTheme.typography.bodyMedium.copy(fontStyle = FontStyle.Italic),
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
}