Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 732c5b73c0 | |||
| 8a430d5089 | |||
| 0c7eaabbb7 | |||
| 63bc93ebd2 | |||
| 0f4ce2d2e1 | |||
| 08091a15a2 | |||
| 4a62d195ac |
@@ -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')"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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 }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 ''"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 = "",
|
||||
)
|
||||
|
||||
@@ -72,6 +72,7 @@ fun CheckItemStateEntity.toDomain(): CheckItem = CheckItem(
|
||||
checked = checked,
|
||||
isFreeText = isFreeText,
|
||||
isStaticHint = isStaticHint,
|
||||
inputSpec = inputSpec,
|
||||
freeTextValue = freeTextValue,
|
||||
)
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -29,10 +30,13 @@ import de.medapp.wunddoku.pdf.DocumentationPdfExporter
|
||||
import de.medapp.wunddoku.pdf.DocumentationToPdfExportModelMapper
|
||||
import de.medapp.wunddoku.pdf.PdfExportError
|
||||
import de.medapp.wunddoku.pdf.PdfExportResult
|
||||
import de.medapp.wunddoku.pdf.PdfToImagesConverter
|
||||
import de.medapp.wunddoku.pdf.PdfToImagesResult
|
||||
import de.medapp.wunddoku.signature.SignatureBitmapValidation
|
||||
import de.medapp.wunddoku.signature.SignatureStorage
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import java.io.File
|
||||
import java.time.Instant
|
||||
import java.util.UUID
|
||||
|
||||
@@ -60,6 +64,7 @@ class DocumentationRepository(
|
||||
private val signatureStorage: SignatureStorage,
|
||||
private val woundCarerRepository: WoundCarerRepository,
|
||||
private val pdfExporter: DocumentationPdfExporter,
|
||||
private val pdfToImagesConverter: PdfToImagesConverter,
|
||||
private val db: AppDatabase,
|
||||
) {
|
||||
|
||||
@@ -187,6 +192,7 @@ class DocumentationRepository(
|
||||
isFreeText = item.isFreeText,
|
||||
isStaticHint = item.isStaticHint,
|
||||
freeTextValue = item.freeTextValue,
|
||||
inputSpec = item.inputSpec,
|
||||
)
|
||||
}
|
||||
)
|
||||
@@ -284,6 +290,16 @@ class DocumentationRepository(
|
||||
return pdfExporter.export(model)
|
||||
}
|
||||
|
||||
/**
|
||||
* Rendert eine bereits erzeugte PDF-Datei (s. [exportPdf]) seitenweise in JPEG-Dateien
|
||||
* (Änderungswunsch: "PDF zusätzlich als JPEG speichern/senden können"). Nimmt bewusst die
|
||||
* fertige [pdfFile] entgegen, statt die Dokumentation erneut zu laden/zu mappen - der
|
||||
* Aufrufer hat sie direkt aus einem vorangegangenen [exportPdf]-Aufruf zur Verfügung, ein
|
||||
* erneutes Laden wäre unnötige Doppelarbeit.
|
||||
*/
|
||||
suspend fun convertPdfToImages(documentationId: String, pdfFile: File): PdfToImagesResult =
|
||||
pdfToImagesConverter.convert(documentationId, pdfFile)
|
||||
|
||||
/**
|
||||
* Liefert eine speicherschonende Vorschauversion eines Fotos für die Anzeige im Fotogitter
|
||||
* der Dokumentationsbearbeitung (kein Laden der vollen gespeicherten Auflösung). Wird von
|
||||
@@ -385,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
|
||||
@@ -396,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) {
|
||||
@@ -426,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)
|
||||
}
|
||||
|
||||
@@ -8,7 +8,9 @@ import de.medapp.wunddoku.data.repository.WoundCarerRepository
|
||||
import de.medapp.wunddoku.media.InternalPhotoStorage
|
||||
import de.medapp.wunddoku.media.PhotoStorage
|
||||
import de.medapp.wunddoku.pdf.AndroidPdfDocumentExporter
|
||||
import de.medapp.wunddoku.pdf.AndroidPdfToImagesConverter
|
||||
import de.medapp.wunddoku.pdf.DocumentationPdfExporter
|
||||
import de.medapp.wunddoku.pdf.PdfToImagesConverter
|
||||
import de.medapp.wunddoku.signature.InternalSignatureStorage
|
||||
import de.medapp.wunddoku.signature.SignatureStorage
|
||||
|
||||
@@ -38,6 +40,7 @@ class AppContainer(context: Context) {
|
||||
private val pdfExporter: DocumentationPdfExporter by lazy {
|
||||
AndroidPdfDocumentExporter(context, photoStorage, signatureStorage)
|
||||
}
|
||||
private val pdfToImagesConverter: PdfToImagesConverter by lazy { AndroidPdfToImagesConverter(context) }
|
||||
|
||||
val patientRepository: PatientRepository by lazy {
|
||||
PatientRepository(patientDao = database.patientDao())
|
||||
@@ -62,6 +65,7 @@ class AppContainer(context: Context) {
|
||||
signatureStorage = signatureStorage,
|
||||
woundCarerRepository = woundCarerRepository,
|
||||
pdfExporter = pdfExporter,
|
||||
pdfToImagesConverter = pdfToImagesConverter,
|
||||
db = database,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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<String> = (1..12).map { it.toString() }
|
||||
private val WEEKDAY_COUNT_OPTIONS: List<String> = (1..7).map { it.toString() }
|
||||
private val MINUTE_1_TO_15_OPTIONS: List<String> = (1..15).map { it.toString() }
|
||||
private val DEPTH_HALF_CM_OPTIONS: List<String> = 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<String> = listOf("I", "II", "III", "IV")
|
||||
|
||||
val blocks: List<BlockDefinition> = 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(
|
||||
@@ -66,7 +105,7 @@ object DocumentationCatalog {
|
||||
title = "Lokalisation",
|
||||
order = 2,
|
||||
items = listOf(
|
||||
CheckItemDefinition(id = "lokalisation_multiple_wunden", label = "multiple Wunden im Bereich", order = 1),
|
||||
CheckItemDefinition(id = "lokalisation_multiple_wunden", label = "Multiple Wunden im Bereich", order = 1),
|
||||
),
|
||||
),
|
||||
BlockDefinition(
|
||||
@@ -74,18 +113,41 @@ 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),
|
||||
// Bewusst OHNE separate Hinweis-Überschrift (anders als bei "Maße" oben): Die
|
||||
// Checkbox selbst trägt mit "Zu versorgendes Areal" bereits den fachlich
|
||||
// gewünschten Namen - eine zusätzliche, wortgleiche Überschrift direkt darüber
|
||||
// würde im PDF als zwei identische, nicht unterscheidbare Zeilen hintereinander
|
||||
// erscheinen (Hinweistext und Checkbox-Label werden dort gleich dargestellt).
|
||||
CheckItemDefinition(id = "groesse_areal", label = "Zu versorgendes Areal", order = 5),
|
||||
CheckItemDefinition(id = "groesse_freitext_areal", label = "Freitext für zu versorgendes Areal", order = 6, 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 +176,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 +209,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 +258,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 +273,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 +352,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 +476,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 +499,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 +529,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 +564,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 +577,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 +594,7 @@ object DocumentationCatalog {
|
||||
BlockDefinition(
|
||||
id = "naechster_termin",
|
||||
title = "Nächster Termin",
|
||||
order = 24,
|
||||
order = 25,
|
||||
items = emptyList(),
|
||||
),
|
||||
)
|
||||
@@ -425,7 +622,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 +634,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 +650,5 @@ data class CheckItemDefinition(
|
||||
val order: Int,
|
||||
val isFreeText: Boolean = false,
|
||||
val isStaticHint: Boolean = false,
|
||||
val inputSpec: InputSpec? = null,
|
||||
)
|
||||
|
||||
@@ -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 = "",
|
||||
)
|
||||
|
||||
@@ -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<String>,
|
||||
) : 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<InputFieldDef>,
|
||||
) {
|
||||
|
||||
/** Zerlegt den gespeicherten Gesamtwert (s. [encodeValues]) in genau [fields].size Einzelwerte. */
|
||||
fun decodeValues(stored: String): List<String> {
|
||||
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>): 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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -40,6 +40,7 @@ object DocumentationToPdfExportModelMapper {
|
||||
label = item.label,
|
||||
isFreeText = item.isFreeText,
|
||||
freeTextValue = item.freeTextValue,
|
||||
inputSpec = item.inputSpec,
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
@@ -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 = "",
|
||||
)
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
package de.medapp.wunddoku.pdf
|
||||
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.ClipData
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import androidx.core.content.FileProvider
|
||||
import java.io.File
|
||||
|
||||
/**
|
||||
* Übergibt alle als JPEG gerenderten PDF-Seiten (s. [PdfToImagesConverter]) GEMEINSAM an den
|
||||
* Android-"Senden an"-Dialog (`ACTION_SEND_MULTIPLE`) - Änderungswunsch: "alle generierten
|
||||
* Bilder über 'Senden an' direkt verschicken können", nicht Seite für Seite einzeln.
|
||||
*
|
||||
* Bewusst getrennt von [PdfToImagesConverter] (reine Datei-Logik): das Starten einer Activity
|
||||
* ist eine UI-/Plattform-Aktion, keine Repository-Aufgabe - exakt dieselbe Trennung wie bei
|
||||
* [DocumentationPdfExporter] (Erzeugung) vs. [PdfOpener] (Anzeige).
|
||||
*
|
||||
* VulneraDoc implementiert ausdrücklich KEINEN eigenen Versand-Mechanismus; welche konkrete App
|
||||
* (Mail, Messenger, Cloud-Speicher, ...) die Bilder entgegennimmt, entscheidet ausschließlich
|
||||
* Android/der Nutzer über den System-Dialog.
|
||||
*/
|
||||
interface PdfImageSharer {
|
||||
fun share(imageFiles: List<File>): PdfImageShareResult
|
||||
}
|
||||
|
||||
sealed interface PdfImageShareResult {
|
||||
data object Success : PdfImageShareResult
|
||||
|
||||
sealed interface Failure : PdfImageShareResult {
|
||||
/** Es wurden keine Bilddateien übergeben - nichts zum Teilen vorhanden. */
|
||||
data object NoImages : Failure
|
||||
|
||||
/** FileProvider konnte für mindestens eine Datei keine content://-Uri erzeugen. */
|
||||
data class UriResolutionFailed(val message: String) : Failure
|
||||
|
||||
/** Keine installierte App kann Bilder (image/jpeg) entgegennehmen. */
|
||||
data object NoAppAvailable : Failure
|
||||
|
||||
data class Unexpected(val message: String) : Failure
|
||||
}
|
||||
}
|
||||
|
||||
fun PdfImageShareResult.Failure.toUserMessage(): String = when (this) {
|
||||
PdfImageShareResult.Failure.NoImages ->
|
||||
"Es sind keine Bilder zum Versenden vorhanden."
|
||||
is PdfImageShareResult.Failure.UriResolutionFailed ->
|
||||
"Die Bilder konnten nicht zum Versenden freigegeben werden."
|
||||
PdfImageShareResult.Failure.NoAppAvailable ->
|
||||
"Auf diesem Gerät ist keine App zum Versenden von Bildern installiert."
|
||||
is PdfImageShareResult.Failure.Unexpected ->
|
||||
"Die Bilder konnten nicht versendet werden."
|
||||
}
|
||||
|
||||
class AndroidPdfImageSharer(private val context: Context) : PdfImageSharer {
|
||||
|
||||
override fun share(imageFiles: List<File>): PdfImageShareResult {
|
||||
if (imageFiles.isEmpty()) return PdfImageShareResult.Failure.NoImages
|
||||
|
||||
val authority = "${context.packageName}.fileprovider"
|
||||
val uris = try {
|
||||
imageFiles.map { file -> FileProvider.getUriForFile(context, authority, file) }
|
||||
} catch (e: IllegalArgumentException) {
|
||||
// Wird von FileProvider geworfen, wenn eine Datei außerhalb der in file_paths.xml
|
||||
// freigegebenen Verzeichnisse liegt oder die Authority nicht passt.
|
||||
return PdfImageShareResult.Failure.UriResolutionFailed(e.message ?: "Uri konnte nicht erzeugt werden.")
|
||||
}
|
||||
|
||||
val intent = Intent(Intent.ACTION_SEND_MULTIPLE).apply {
|
||||
type = "image/jpeg"
|
||||
putParcelableArrayListExtra(Intent.EXTRA_STREAM, ArrayList(uris))
|
||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
// Zusätzlich zum Flag oben wird die Leseberechtigung explizit über ClipData an JEDE
|
||||
// einzelne Uri gehängt - bei ACTION_SEND_MULTIPLE mit mehreren content://-Uris ist
|
||||
// das der zuverlässige Weg, damit wirklich ALLE empfangenden Apps Zugriff auf ALLE
|
||||
// Bilder bekommen (nicht nur auf die im Intent-Datenfeld primär referenzierte).
|
||||
clipData = ClipData.newUri(context.contentResolver, "Wunddokumentation", uris.first()).apply {
|
||||
for (uri in uris.drop(1)) {
|
||||
addItem(ClipData.Item(uri))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val chooser = Intent.createChooser(intent, "Bilder senden an")
|
||||
|
||||
return try {
|
||||
context.startActivity(chooser)
|
||||
PdfImageShareResult.Success
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
// Bewusst KEIN vorheriges intent.resolveActivity(...) - s. Begründung in
|
||||
// AndroidPdfOpener.open() (Package-Visibility-Einschränkungen ab Android 11).
|
||||
PdfImageShareResult.Failure.NoAppAvailable
|
||||
} catch (e: SecurityException) {
|
||||
PdfImageShareResult.Failure.Unexpected(e.message ?: "Zugriff auf die Bilddateien wurde verweigert.")
|
||||
} catch (e: Exception) {
|
||||
PdfImageShareResult.Failure.Unexpected(e.message ?: "Unbekannter Fehler beim Versenden der Bilder.")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package de.medapp.wunddoku.pdf
|
||||
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import androidx.core.content.FileProvider
|
||||
import java.io.File
|
||||
|
||||
/**
|
||||
* Übergibt die fertige PDF-Datei selbst (statt in JPEG-Seiten umgewandelt, s.
|
||||
* [PdfToImagesConverter]/[PdfImageSharer]) an den Android-"Senden an"-Dialog
|
||||
* (`ACTION_SEND`, `application/pdf`) - Änderungswunsch: Nutzer soll beim Antippen des
|
||||
* Teilen-Buttons wählen können, ob als JPG ODER als PDF geteilt wird.
|
||||
*
|
||||
* Bewusst getrennt von [PdfOpener] (öffnet dieselbe Datei stattdessen per `ACTION_VIEW` in
|
||||
* einem PDF-Reader) - unterschiedliche Nutzerabsicht (ansehen vs. weitergeben), auch wenn
|
||||
* beide dieselbe Datei über denselben FileProvider-Pfad freigeben.
|
||||
*/
|
||||
interface PdfSharer {
|
||||
fun share(pdfFile: File): PdfShareResult
|
||||
}
|
||||
|
||||
sealed interface PdfShareResult {
|
||||
data object Success : PdfShareResult
|
||||
|
||||
sealed interface Failure : PdfShareResult {
|
||||
data class UriResolutionFailed(val message: String) : Failure
|
||||
data object NoAppAvailable : Failure
|
||||
data class Unexpected(val message: String) : Failure
|
||||
}
|
||||
}
|
||||
|
||||
fun PdfShareResult.Failure.toUserMessage(): String = when (this) {
|
||||
is PdfShareResult.Failure.UriResolutionFailed ->
|
||||
"Die PDF-Datei konnte nicht zum Versenden freigegeben werden."
|
||||
PdfShareResult.Failure.NoAppAvailable ->
|
||||
"Auf diesem Gerät ist keine App zum Versenden von PDFs installiert."
|
||||
is PdfShareResult.Failure.Unexpected ->
|
||||
"Die PDF-Datei konnte nicht versendet werden."
|
||||
}
|
||||
|
||||
class AndroidPdfSharer(private val context: Context) : PdfSharer {
|
||||
|
||||
override fun share(pdfFile: File): PdfShareResult {
|
||||
val authority = "${context.packageName}.fileprovider"
|
||||
val uri = try {
|
||||
FileProvider.getUriForFile(context, authority, pdfFile)
|
||||
} catch (e: IllegalArgumentException) {
|
||||
return PdfShareResult.Failure.UriResolutionFailed(e.message ?: "Uri konnte nicht erzeugt werden.")
|
||||
}
|
||||
|
||||
val intent = Intent(Intent.ACTION_SEND).apply {
|
||||
type = "application/pdf"
|
||||
putExtra(Intent.EXTRA_STREAM, uri)
|
||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
}
|
||||
val chooser = Intent.createChooser(intent, "PDF senden an")
|
||||
|
||||
return try {
|
||||
context.startActivity(chooser)
|
||||
PdfShareResult.Success
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
// Bewusst KEIN vorheriges intent.resolveActivity(...) - s. Begründung in
|
||||
// AndroidPdfOpener.open() (Package-Visibility-Einschränkungen ab Android 11).
|
||||
PdfShareResult.Failure.NoAppAvailable
|
||||
} catch (e: SecurityException) {
|
||||
PdfShareResult.Failure.Unexpected(e.message ?: "Zugriff auf die PDF-Datei wurde verweigert.")
|
||||
} catch (e: Exception) {
|
||||
PdfShareResult.Failure.Unexpected(e.message ?: "Unbekannter Fehler beim Versenden der PDF-Datei.")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
package de.medapp.wunddoku.pdf
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Color
|
||||
import android.graphics.pdf.PdfRenderer
|
||||
import android.os.ParcelFileDescriptor
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
/**
|
||||
* Rendert eine bereits fertig erzeugte PDF-Datei (s. [DocumentationPdfExporter]) seitenweise in
|
||||
* einzelne JPEG-Dateien - Änderungswunsch: "PDF zusätzlich als JPEG speichern/senden können".
|
||||
*
|
||||
* Bewusst als eigener Schritt NACH dem PDF-Export statt als Alternative dazu: Es gibt weiterhin
|
||||
* genau eine Quelle der Wahrheit für das Seitenlayout (der bestehende [PdfPageCursor]-basierte
|
||||
* Export) - dieser Konverter rastert lediglich das bereits fertige Ergebnis, statt eine zweite,
|
||||
* parallele Layout-Logik zu pflegen.
|
||||
*
|
||||
* Verwendet [android.graphics.pdf.PdfRenderer] (Android-Bordmittel seit API 21, unproblematisch
|
||||
* bei minSdk 26) - keine zusätzliche Bibliothek nötig. Eine mehrseitige Wunddokumentation (Kopf/
|
||||
* Blöcke, Signaturen, ggf. 30+ Fotoseiten) ergibt dabei bewusst EIN JPEG PRO SEITE, kein
|
||||
* einzelnes Sammelbild - beim späteren Teilen (s. [PdfImageSharer]) werden alle Seiten
|
||||
* gemeinsam als Bilder-Mehrfachauswahl übergeben.
|
||||
*/
|
||||
interface PdfToImagesConverter {
|
||||
suspend fun convert(documentationId: String, pdfFile: File): PdfToImagesResult
|
||||
}
|
||||
|
||||
sealed interface PdfToImagesResult {
|
||||
data class Success(val imageFiles: List<File>) : PdfToImagesResult
|
||||
|
||||
sealed interface Failure : PdfToImagesResult {
|
||||
/** Die übergebene PDF-Datei existiert nicht (mehr). */
|
||||
data object FileNotFound : Failure
|
||||
|
||||
/** PdfRenderer meldet 0 Seiten - sollte bei einem selbst erzeugten PDF nie vorkommen. */
|
||||
data object EmptyDocument : Failure
|
||||
|
||||
data class Unexpected(val message: String) : Failure
|
||||
}
|
||||
}
|
||||
|
||||
fun PdfToImagesResult.Failure.toUserMessage(): String = when (this) {
|
||||
PdfToImagesResult.Failure.FileNotFound ->
|
||||
"Die PDF-Datei wurde nicht gefunden. Bitte zuerst die Dokumentation erneut exportieren."
|
||||
PdfToImagesResult.Failure.EmptyDocument ->
|
||||
"Die PDF-Datei enthält keine Seiten."
|
||||
is PdfToImagesResult.Failure.Unexpected ->
|
||||
"Die PDF-Datei konnte nicht in Bilder umgewandelt werden."
|
||||
}
|
||||
|
||||
class AndroidPdfToImagesConverter(private val context: Context) : PdfToImagesConverter {
|
||||
|
||||
override suspend fun convert(documentationId: String, pdfFile: File): PdfToImagesResult =
|
||||
withContext(Dispatchers.IO) {
|
||||
if (!pdfFile.exists()) return@withContext PdfToImagesResult.Failure.FileNotFound
|
||||
|
||||
val outputDir = File(File(context.filesDir, "pdf_images"), documentationId)
|
||||
// Bilder eines vorherigen Exports zuerst entfernen, damit bei einer inzwischen
|
||||
// kürzeren PDF (z. B. Fotos gelöscht) keine veralteten, zu vielen Seiten-Dateien
|
||||
// liegen bleiben.
|
||||
outputDir.deleteRecursively()
|
||||
outputDir.mkdirs()
|
||||
|
||||
try {
|
||||
ParcelFileDescriptor.open(pdfFile, ParcelFileDescriptor.MODE_READ_ONLY).use { fd ->
|
||||
PdfRenderer(fd).use { renderer ->
|
||||
if (renderer.pageCount == 0) {
|
||||
return@withContext PdfToImagesResult.Failure.EmptyDocument
|
||||
}
|
||||
|
||||
val imageFiles = mutableListOf<File>()
|
||||
for (pageIndex in 0 until renderer.pageCount) {
|
||||
renderer.openPage(pageIndex).use { page ->
|
||||
val scale = RENDER_DPI / POINTS_PER_INCH
|
||||
val width = (page.width * scale).roundToInt().coerceAtLeast(1)
|
||||
val height = (page.height * scale).roundToInt().coerceAtLeast(1)
|
||||
|
||||
val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
|
||||
// WICHTIG: PdfRenderer zeichnet nur die tatsächlich im PDF
|
||||
// vorhandenen Inhalte - unbedeckte Flächen bleiben transparent
|
||||
// (Alpha 0). JPEG kennt keine Transparenz; ohne dieses
|
||||
// Vorab-Weißfüllen würde jede unbedeckte Fläche beim
|
||||
// Komprimieren als SCHWARZ statt als weißer Seitenhintergrund
|
||||
// enden (bekannte PdfRenderer-Falle, s. offizielle Android-
|
||||
// Beispiele).
|
||||
bitmap.eraseColor(Color.WHITE)
|
||||
page.render(bitmap, null, null, PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY)
|
||||
|
||||
val imageFile = File(outputDir, "seite_%02d.jpg".format(pageIndex + 1))
|
||||
FileOutputStream(imageFile).use { out ->
|
||||
bitmap.compress(Bitmap.CompressFormat.JPEG, JPEG_QUALITY, out)
|
||||
}
|
||||
bitmap.recycle()
|
||||
imageFiles.add(imageFile)
|
||||
}
|
||||
}
|
||||
PdfToImagesResult.Success(imageFiles)
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
PdfToImagesResult.Failure.Unexpected(e.message ?: "Unbekannter Fehler bei der Bildkonvertierung.")
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
/** PDF-Koordinaten sind in Punkten (1/72 Zoll) angegeben - diese Ziel-Auflösung ergibt
|
||||
* ein für Ausdruck/Weitergabe ausreichend scharfes, aber nicht unnötig großes Bild. */
|
||||
private const val RENDER_DPI = 200f
|
||||
private const val POINTS_PER_INCH = 72f
|
||||
private const val JPEG_QUALITY = 90
|
||||
}
|
||||
}
|
||||
+204
-3
@@ -10,12 +10,14 @@ 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
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
@@ -30,6 +32,7 @@ import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material.icons.filled.ExpandLess
|
||||
import androidx.compose.material.icons.filled.ExpandMore
|
||||
import androidx.compose.material.icons.filled.PictureAsPdf
|
||||
import androidx.compose.material.icons.filled.Share
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Checkbox
|
||||
@@ -39,12 +42,15 @@ 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
|
||||
import androidx.compose.material3.SnackbarHost
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.SnackbarResult
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.TopAppBar
|
||||
@@ -64,7 +70,9 @@ 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
|
||||
import androidx.lifecycle.LifecycleEventObserver
|
||||
import androidx.lifecycle.compose.LocalLifecycleOwner
|
||||
@@ -72,11 +80,17 @@ 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
|
||||
import de.medapp.wunddoku.pdf.AndroidPdfImageSharer
|
||||
import de.medapp.wunddoku.pdf.AndroidPdfOpener
|
||||
import de.medapp.wunddoku.pdf.AndroidPdfSharer
|
||||
import de.medapp.wunddoku.pdf.PdfImageShareResult
|
||||
import de.medapp.wunddoku.pdf.PdfOpenResult
|
||||
import de.medapp.wunddoku.pdf.PdfShareResult
|
||||
import de.medapp.wunddoku.pdf.toUserMessage
|
||||
import de.medapp.wunddoku.ui.documentation.edit.signature.AddSignatureDialog
|
||||
|
||||
@@ -137,6 +151,8 @@ fun DocumentationEditScreen(
|
||||
// Starten einer externen Activity ist eine UI-/Plattform-Aktion, keine Repository-Aufgabe.
|
||||
val context = LocalContext.current
|
||||
val pdfOpener = remember(context) { AndroidPdfOpener(context) }
|
||||
val pdfImageSharer = remember(context) { AndroidPdfImageSharer(context) }
|
||||
val pdfSharer = remember(context) { AndroidPdfSharer(context) }
|
||||
val snackbarHostState = remember { SnackbarHostState() }
|
||||
LaunchedEffect(uiState.pdfExportMessage) {
|
||||
val message = uiState.pdfExportMessage ?: return@LaunchedEffect
|
||||
@@ -157,6 +173,34 @@ fun DocumentationEditScreen(
|
||||
viewModel.dismissPdfExportMessage()
|
||||
}
|
||||
|
||||
// Änderungswunsch: erzeugte Bilder direkt über den Android-"Senden an"-Dialog verschicken
|
||||
// können. Sobald das ViewModel frische JPEG-Seiten bereitstellt (s.
|
||||
// DocumentationEditViewModel.shareAsImages), öffnet dieser Effekt den System-
|
||||
// Share-Dialog - Intent/Activity-Start ist bewusst UI-Aufgabe, nicht Sache des ViewModels.
|
||||
LaunchedEffect(uiState.imagesToShare) {
|
||||
val images = uiState.imagesToShare ?: return@LaunchedEffect
|
||||
when (val shareResult = pdfImageSharer.share(images)) {
|
||||
PdfImageShareResult.Success -> Unit
|
||||
is PdfImageShareResult.Failure -> snackbarHostState.showSnackbar(shareResult.toUserMessage())
|
||||
}
|
||||
viewModel.onImageShareHandled()
|
||||
}
|
||||
// Analog zum Bilderversand oben, nur für die Format-Auswahl "PDF" (s.
|
||||
// DocumentationEditViewModel.shareAsPdf).
|
||||
LaunchedEffect(uiState.pdfToShare) {
|
||||
val pdfFile = uiState.pdfToShare ?: return@LaunchedEffect
|
||||
when (val shareResult = pdfSharer.share(pdfFile)) {
|
||||
PdfShareResult.Success -> Unit
|
||||
is PdfShareResult.Failure -> snackbarHostState.showSnackbar(shareResult.toUserMessage())
|
||||
}
|
||||
viewModel.onPdfShareHandled()
|
||||
}
|
||||
LaunchedEffect(uiState.shareMessage) {
|
||||
val message = uiState.shareMessage ?: return@LaunchedEffect
|
||||
snackbarHostState.showSnackbar(message)
|
||||
viewModel.dismissShareMessage()
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
snackbarHost = { SnackbarHost(snackbarHostState) },
|
||||
topBar = {
|
||||
@@ -175,9 +219,8 @@ fun DocumentationEditScreen(
|
||||
}
|
||||
},
|
||||
actions = {
|
||||
// PDF-005 (Öffnen)/PDF-006 (Teilen) folgen später - dieser Button erzeugt
|
||||
// ausschließlich die Datei im internen Speicher (PDF-001/002), als
|
||||
// minimaler Trigger zum manuellen Testen der Export-Funktionalität.
|
||||
// PDF-005 (Öffnen) ist umgesetzt (s. LaunchedEffect oben); dieser Button
|
||||
// erzeugt die Datei im internen Speicher (PDF-001/002).
|
||||
if (uiState.pdfExportInProgress) {
|
||||
CircularProgressIndicator(modifier = Modifier.size(24.dp), strokeWidth = 2.dp)
|
||||
} else {
|
||||
@@ -185,6 +228,16 @@ fun DocumentationEditScreen(
|
||||
Icon(Icons.Filled.PictureAsPdf, contentDescription = "PDF erzeugen")
|
||||
}
|
||||
}
|
||||
// Änderungswunsch: PDF zusätzlich als JPEG speichern/versenden, wahlweise
|
||||
// auch als PDF selbst - Format wird per Dialog abgefragt (s. weiter unten),
|
||||
// BEVOR irgendetwas exportiert wird.
|
||||
if (uiState.shareInProgress) {
|
||||
CircularProgressIndicator(modifier = Modifier.size(24.dp), strokeWidth = 2.dp)
|
||||
} else {
|
||||
IconButton(onClick = viewModel::requestShareFormatChoice) {
|
||||
Icon(Icons.Filled.Share, contentDescription = "Teilen")
|
||||
}
|
||||
}
|
||||
// "Fertig" beendet die Bearbeitung bereits jetzt (Inhalte sind ohnehin per
|
||||
// Autosave gesichert, kein expliziter Speichern-Button nötig).
|
||||
Button(onClick = onFinished) { Text("Fertig") }
|
||||
@@ -315,6 +368,52 @@ fun DocumentationEditScreen(
|
||||
onDismiss = { viewedPhoto = null },
|
||||
)
|
||||
}
|
||||
|
||||
// Änderungswunsch: Format-Auswahl beim Antippen des Teilen-Buttons.
|
||||
if (uiState.showShareFormatDialog) {
|
||||
ShareFormatDialog(
|
||||
onDismiss = viewModel::dismissShareFormatDialog,
|
||||
onChooseImages = viewModel::shareAsImages,
|
||||
onChoosePdf = viewModel::shareAsPdf,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fragt beim Antippen des Teilen-Buttons ab, ob als JPG (Bilder, eine Datei pro Seite) oder als
|
||||
* PDF (die Datei selbst) geteilt werden soll (Änderungswunsch). Erst NACH dieser Auswahl wird
|
||||
* überhaupt exportiert (s. [DocumentationEditViewModel.shareAsImages]/[DocumentationEditViewModel.shareAsPdf]) -
|
||||
* kein unnötiger Export, falls der Nutzer den Dialog stattdessen abbricht.
|
||||
*/
|
||||
@Composable
|
||||
private fun ShareFormatDialog(
|
||||
onDismiss: () -> Unit,
|
||||
onChooseImages: () -> Unit,
|
||||
onChoosePdf: () -> Unit,
|
||||
) {
|
||||
Dialog(onDismissRequest = onDismiss) {
|
||||
Surface(shape = RoundedCornerShape(16.dp), color = MaterialTheme.colorScheme.surface) {
|
||||
Column(modifier = Modifier.fillMaxWidth().padding(20.dp)) {
|
||||
Text("Teilen als", style = MaterialTheme.typography.titleMedium)
|
||||
Spacer(Modifier.height(8.dp))
|
||||
Text(
|
||||
"Als Bilder (JPG) oder als PDF teilen?",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
Spacer(Modifier.height(16.dp))
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.End,
|
||||
) {
|
||||
TextButton(onClick = onDismiss) { Text(stringResource(R.string.action_cancel)) }
|
||||
Spacer(Modifier.width(8.dp))
|
||||
OutlinedButton(onClick = onChooseImages) { Text("JPG") }
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Button(onClick = onChoosePdf) { Text("PDF") }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -678,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,
|
||||
@@ -751,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
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+138
@@ -15,6 +15,8 @@ import de.medapp.wunddoku.domain.model.Documentation
|
||||
import de.medapp.wunddoku.domain.model.WoundCarer
|
||||
import de.medapp.wunddoku.pdf.PdfExportError
|
||||
import de.medapp.wunddoku.pdf.PdfExportResult
|
||||
import de.medapp.wunddoku.pdf.PdfToImagesResult
|
||||
import de.medapp.wunddoku.pdf.toUserMessage
|
||||
import de.medapp.wunddoku.ui.navigation.Screen
|
||||
import kotlinx.coroutines.FlowPreview
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
@@ -63,6 +65,28 @@ data class DocumentationEditUiState(
|
||||
* referenziert werden kann, während ein neuer Export läuft.
|
||||
*/
|
||||
val lastExportedPdfFile: File? = null,
|
||||
/** true, während PDF-Export + Konvertierung für den Versand laufen - gemeinsamer
|
||||
* Ladezustand für BEIDE Formate (JPG über [DocumentationEditViewModel.shareAsImages],
|
||||
* PDF über [DocumentationEditViewModel.shareAsPdf]), da immer zuerst frisch exportiert
|
||||
* wird. */
|
||||
val shareInProgress: Boolean = false,
|
||||
/** Fehlermeldung, falls Export oder Konvertierung fehlgeschlagen sind - ein erfolgreicher
|
||||
* Export löst dagegen KEINE Meldung aus, sondern direkt [imagesToShare] bzw. [pdfToShare]. */
|
||||
val shareMessage: String? = null,
|
||||
/** true, solange die Auswahl "Als JPG oder PDF teilen?" angezeigt wird (Änderungswunsch:
|
||||
* Format-Auswahl beim Antippen des Teilen-Buttons, bevor irgendetwas exportiert wird). */
|
||||
val showShareFormatDialog: Boolean = false,
|
||||
/**
|
||||
* Frisch erzeugte JPEG-Seiten, bereit zum Teilen (Änderungswunsch: "alle generierten Bilder
|
||||
* über 'Senden an' direkt verschicken können"). Wird vom Screen per LaunchedEffect
|
||||
* beobachtet, der daraufhin den System-"Senden an"-Dialog öffnet und anschließend
|
||||
* [DocumentationEditViewModel.onImageShareHandled] aufruft - das Teilen selbst (Intent/
|
||||
* Activity-Start) ist bewusst UI-Aufgabe, nicht Sache des ViewModels.
|
||||
*/
|
||||
val imagesToShare: List<File>? = null,
|
||||
/** Analog [imagesToShare], nur für die Variante "als PDF teilen" - Gegenstück
|
||||
* [DocumentationEditViewModel.onPdfShareHandled]. */
|
||||
val pdfToShare: File? = null,
|
||||
) {
|
||||
val blockCount: Int get() = documentation?.blocks?.size ?: 0
|
||||
|
||||
@@ -357,6 +381,120 @@ class DocumentationEditViewModel(
|
||||
_uiState.update { it.copy(pdfExportMessage = null) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Öffnet die Auswahl "Als JPG oder PDF teilen?" (Änderungswunsch: Format-Auswahl beim
|
||||
* Antippen des Teilen-Buttons, BEVOR irgendetwas exportiert wird - vorher wurde beim
|
||||
* Antippen sofort und ausschließlich als JPG geteilt).
|
||||
*/
|
||||
fun requestShareFormatChoice() {
|
||||
_uiState.update { it.copy(showShareFormatDialog = true) }
|
||||
}
|
||||
|
||||
fun dismissShareFormatDialog() {
|
||||
_uiState.update { it.copy(showShareFormatDialog = false) }
|
||||
}
|
||||
|
||||
/** Nutzer hat sich für "Als JPG teilen" entschieden. */
|
||||
fun shareAsImages() {
|
||||
_uiState.update { it.copy(showShareFormatDialog = false) }
|
||||
exportAndShareAsImages()
|
||||
}
|
||||
|
||||
/** Nutzer hat sich für "Als PDF teilen" entschieden. */
|
||||
fun shareAsPdf() {
|
||||
_uiState.update { it.copy(showShareFormatDialog = false) }
|
||||
exportAndSharePdf()
|
||||
}
|
||||
|
||||
/**
|
||||
* Exportiert die Dokumentation frisch als PDF und wandelt das Ergebnis anschließend
|
||||
* seitenweise in JPEG-Bilder um (Änderungswunsch: "PDF zusätzlich als JPEG speichern/senden
|
||||
* können"). Bewusst ein eigener, vollständiger Export-Lauf statt Wiederverwendung von
|
||||
* [uiState.lastExportedPdfFile]: so spiegeln die Bilder IMMER den aktuellen Stand wider,
|
||||
* unabhängig davon, ob/wann zuvor schon einmal "PDF erzeugen" angetippt wurde.
|
||||
*
|
||||
* Das eigentliche Teilen (Android-"Senden an"-Dialog) übernimmt NICHT diese Methode, sondern
|
||||
* der Screen anhand von [DocumentationEditUiState.imagesToShare] - Intent/Activity-Start ist
|
||||
* UI-Aufgabe (s. [de.medapp.wunddoku.pdf.PdfImageSharer]).
|
||||
*/
|
||||
private fun exportAndShareAsImages() {
|
||||
viewModelScope.launch {
|
||||
_uiState.update {
|
||||
it.copy(shareInProgress = true, shareMessage = null, imagesToShare = null)
|
||||
}
|
||||
|
||||
val exportResult = repository.exportPdf(documentationId)
|
||||
val pdfFile = when (exportResult) {
|
||||
is PdfExportResult.Success -> exportResult.file
|
||||
is PdfExportResult.Failure -> {
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
shareInProgress = false,
|
||||
shareMessage = "PDF-Erzeugung fehlgeschlagen: ${exportResult.error.toUserMessage()}",
|
||||
)
|
||||
}
|
||||
return@launch
|
||||
}
|
||||
}
|
||||
|
||||
when (val imagesResult = repository.convertPdfToImages(documentationId, pdfFile)) {
|
||||
is PdfToImagesResult.Success -> _uiState.update {
|
||||
it.copy(shareInProgress = false, imagesToShare = imagesResult.imageFiles)
|
||||
}
|
||||
|
||||
is PdfToImagesResult.Failure -> _uiState.update {
|
||||
it.copy(
|
||||
shareInProgress = false,
|
||||
shareMessage = "Bildexport fehlgeschlagen: ${imagesResult.toUserMessage()}",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Exportiert die Dokumentation frisch als PDF und stellt sie zum Teilen bereit
|
||||
* (Änderungswunsch: Format-Auswahl "PDF" statt "JPG") - ohne die JPEG-Konvertierung, die
|
||||
* geteilte Datei ist die PDF selbst. Das eigentliche Teilen übernimmt der Screen anhand von
|
||||
* [DocumentationEditUiState.pdfToShare] (s. [de.medapp.wunddoku.pdf.PdfSharer]).
|
||||
*/
|
||||
private fun exportAndSharePdf() {
|
||||
viewModelScope.launch {
|
||||
_uiState.update {
|
||||
it.copy(shareInProgress = true, shareMessage = null, pdfToShare = null)
|
||||
}
|
||||
|
||||
when (val exportResult = repository.exportPdf(documentationId)) {
|
||||
is PdfExportResult.Success -> _uiState.update {
|
||||
it.copy(shareInProgress = false, pdfToShare = exportResult.file)
|
||||
}
|
||||
|
||||
is PdfExportResult.Failure -> _uiState.update {
|
||||
it.copy(
|
||||
shareInProgress = false,
|
||||
shareMessage = "PDF-Erzeugung fehlgeschlagen: ${exportResult.error.toUserMessage()}",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Wird vom Screen aufgerufen, nachdem der "Senden an"-Dialog für [DocumentationEditUiState.imagesToShare]
|
||||
* geöffnet wurde (erfolgreich oder nicht) - verhindert ein erneutes Öffnen bei der nächsten
|
||||
* Recomposition, solange keine neuen Bilder erzeugt wurden. */
|
||||
fun onImageShareHandled() {
|
||||
_uiState.update { it.copy(imagesToShare = null) }
|
||||
}
|
||||
|
||||
/** Analog [onImageShareHandled], nur für [DocumentationEditUiState.pdfToShare]. */
|
||||
fun onPdfShareHandled() {
|
||||
_uiState.update { it.copy(pdfToShare = null) }
|
||||
}
|
||||
|
||||
fun dismissShareMessage() {
|
||||
_uiState.update { it.copy(shareMessage = null) }
|
||||
}
|
||||
|
||||
private fun PdfExportError.toUserMessage(): String = when (this) {
|
||||
PdfExportError.DocumentationNotFound -> "Die Dokumentation wurde nicht gefunden."
|
||||
is PdfExportError.DirectoryCreationFailed -> "Zielverzeichnis konnte nicht angelegt werden."
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
-->
|
||||
<paths xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<files-path name="pdf_exports" path="pdf/" />
|
||||
<files-path name="pdf_images" path="pdf_images/" />
|
||||
<files-path name="photos" path="photos/" />
|
||||
<files-path name="signatures" path="signatures/" />
|
||||
</paths>
|
||||
|
||||
Reference in New Issue
Block a user