diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1bed383d..e83ee28e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -75,25 +75,6 @@ jobs: :ohs-player-reference-app:lintDebug kotlinDaemonHeap: '-Xmx6g -XX:+UseG1GC' extraArgs: '' - - id: js - label: Kotlin/JS - # jsBrowserTest is disabled in build.gradle.kts, but requesting - # it still runs the compile prerequisites (compileKotlinJs + - # compileTestDevelopmentExecutableKotlinJs) — which is the work - # we want covered. - tasks: ':ohs-player-reference-app:jsBrowserTest' - kotlinDaemonHeap: '-Xmx10g -XX:+UseG1GC' - extraArgs: '' - - id: wasm - label: Kotlin/Wasm - tasks: >- - :ohs-player-library:wasmJsBrowserTest - :ohs-player-reference-app:wasmJsBrowserTest - kotlinDaemonHeap: '-Xmx10g -XX:+UseG1GC' - # Serialize the library + reference-app wasm compiles so they - # don't run concurrently as workers and double the daemon heap - # demand even at 10g. - extraArgs: '--max-workers=1' steps: - uses: actions/checkout@v4 diff --git a/.gitignore b/.gitignore index 84965540..c9f24e44 100644 --- a/.gitignore +++ b/.gitignore @@ -53,3 +53,6 @@ AGENT.MD CLAUDE.MD .skills +# Git worktrees used for isolated agent workspaces +.worktrees/ + diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 9be9eb55..587ee760 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -15,6 +15,7 @@ composeMaterialIcons = "1.7.3" fhirDataCapture = "2.0.0-alpha02" junit = "4.13.2" kermit = "2.1.0" +koinBom = "4.1.1" kotlin = "2.3.21" kotlinx-coroutines = "1.11.0" kotlinpoet = "2.3.0" @@ -25,6 +26,7 @@ navigation-compose = "2.9.2" material3 = "1.10.0-alpha05" kotlinxSerializationJson = "1.11.0" spotless = "8.6.0" +ohsFhirEngine = "2.0.0-alpha01" ohsFhirModel = "1.0.0-beta05" ohsFhirPath = "1.0.0-beta03" kotlinxDatetime = "0.8.0" @@ -33,6 +35,10 @@ ionspin-bignum = "0.3.10" [libraries] fhir-data-capture = { module = "dev.ohs.fhir:fhir-data-capture", version.ref = "fhirDataCapture" } kermit = { module = "co.touchlab:kermit", version.ref = "kermit" } +koin-bom = { module = "io.insert-koin:koin-bom", version.ref = "koinBom" } +koin-core = { module = "io.insert-koin:koin-core" } +koin-compose = { module = "io.insert-koin:koin-compose" } +koin-composeViewmodel = { module = "io.insert-koin:koin-compose-viewmodel" } kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" } kotlin-testJunit = { module = "org.jetbrains.kotlin:kotlin-test-junit", version.ref = "kotlin" } junit = { module = "junit:junit", version.ref = "junit" } @@ -59,6 +65,7 @@ kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version kotlinpoet = { module = "com.squareup:kotlinpoet", version.ref = "kotlinpoet" } kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" } navigation-compose = { module = "org.jetbrains.androidx.navigation:navigation-compose", version.ref = "navigation-compose" } +ohs-fhir-engine = { module = "dev.ohs.fhir:fhir-engine", version.ref = "ohsFhirEngine" } ohs-fhir-model = { module = "dev.ohs.fhir:fhir-model", version.ref = "ohsFhirModel" } ohs-fhir-path = { module = "dev.ohs.fhir:fhir-path", version.ref = "ohsFhirPath" } ionspin-bignum = { module = "com.ionspin.kotlin:bignum", version.ref = "ionspin-bignum" } diff --git a/ohs-player-reference-app/build.gradle.kts b/ohs-player-reference-app/build.gradle.kts index 0f1f6903..470f3c57 100644 --- a/ohs-player-reference-app/build.gradle.kts +++ b/ohs-player-reference-app/build.gradle.kts @@ -73,6 +73,11 @@ kotlin { implementation(libs.kotlinx.serialization.json) implementation(libs.kotlinx.datetime) implementation(libs.navigation.compose) + implementation(project.dependencies.platform(libs.koin.bom)) + implementation(libs.koin.core) + implementation(libs.koin.compose) + implementation(libs.koin.composeViewmodel) + implementation(libs.ohs.fhir.engine) implementation(libs.ohs.fhir.model) implementation(libs.ohs.fhir.path) implementation(libs.fhir.data.capture) diff --git a/ohs-player-reference-app/src/androidMain/AndroidManifest.xml b/ohs-player-reference-app/src/androidMain/AndroidManifest.xml index 26403a75..a36bd34e 100644 --- a/ohs-player-reference-app/src/androidMain/AndroidManifest.xml +++ b/ohs-player-reference-app/src/androidMain/AndroidManifest.xml @@ -2,6 +2,7 @@ { FhirEngineProvider.getInstance(applicationContext) } }) + DataCapture.initialize(applicationContext) + } +} diff --git a/ohs-player-reference-app/src/androidMain/kotlin/dev/ohs/player/reference/app/data/repository/PlatformRepositorySnapshotStore.android.kt b/ohs-player-reference-app/src/androidMain/kotlin/dev/ohs/player/reference/app/data/repository/PlatformRepositorySnapshotStore.android.kt deleted file mode 100644 index 2982c4bb..00000000 --- a/ohs-player-reference-app/src/androidMain/kotlin/dev/ohs/player/reference/app/data/repository/PlatformRepositorySnapshotStore.android.kt +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright 2026 Open Health Stack Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package dev.ohs.player.reference.app.data.repository - -import android.content.Context -import java.io.File - -private const val SNAPSHOT_FILE_NAME = "fhir-repository.json" - -internal object AndroidAppContextHolder { - var applicationContext: Context? = null -} - -actual object PlatformRepositorySnapshotStore : RepositorySnapshotStore { - actual override suspend fun read(): String? = - snapshotFile().takeIf(File::exists)?.readText().takeUnless { it.isNullOrBlank() } - - actual override suspend fun write(snapshot: String) { - val file = snapshotFile() - file.parentFile?.mkdirs() - file.writeText(snapshot) - } - - private fun snapshotFile(): File { - val contextDirectory = AndroidAppContextHolder.applicationContext?.filesDir - val fallbackDirectory = File(System.getProperty("java.io.tmpdir").orEmpty().ifBlank { "." }) - return File(contextDirectory ?: fallbackDirectory, SNAPSHOT_FILE_NAME) - } -} diff --git a/ohs-player-reference-app/src/commonMain/composeResources/files/SampleResourcesBundle.json b/ohs-player-reference-app/src/commonMain/composeResources/files/SampleResourcesBundle.json deleted file mode 100644 index 164990f5..00000000 --- a/ohs-player-reference-app/src/commonMain/composeResources/files/SampleResourcesBundle.json +++ /dev/null @@ -1,5711 +0,0 @@ -{ - "resourceType": "Bundle", - "id": "ips-sample-data", - "type": "collection", - "entry": [ - { - "resource": { - "resourceType": "Patient", - "id": "p1", - "identifier": [ - { - "type": { - "coding": [ - { - "code": "MR" - } - ] - }, - "value": "MRN-2015-0342" - } - ], - "name": [ - { - "family": "Diallo", - "given": [ - "Amina" - ] - } - ], - "gender": "female", - "birthDate": "1990-03-14", - "active": true, - "telecom": [ - { - "system": "phone", - "value": "+221 77 123 4567" - }, - { - "system": "email", - "value": "amina.diallo@example.com" - } - ], - "contact": [ - { - "relationship": [ - { - "coding": [ - { - "system": "http://terminology.hl7.org/CodeSystem/v2-0131", - "code": "C", - "display": "Emergency Contact" - } - ] - } - ], - "name": { - "family": "Diallo", - "given": [ - "Oumar" - ] - }, - "telecom": [ - { - "system": "phone", - "value": "+221 77 987 0001" - } - ] - }, - { - "relationship": [ - { - "coding": [ - { - "system": "http://terminology.hl7.org/CodeSystem/v2-0131", - "code": "N", - "display": "Next-of-kin" - } - ] - } - ], - "name": { - "family": "Diallo", - "given": [ - "Fatoumata" - ] - }, - "telecom": [ - { - "system": "phone", - "value": "+221 77 987 0002" - } - ] - } - ] - } - }, - { - "resource": { - "resourceType": "Patient", - "id": "p2", - "identifier": [ - { - "type": { - "coding": [ - { - "code": "MR" - } - ] - }, - "value": "MRN-2010-0789" - } - ], - "name": [ - { - "family": "Mensah", - "given": [ - "Kofi" - ] - } - ], - "gender": "male", - "birthDate": "1985-11-02", - "active": true, - "telecom": [ - { - "system": "phone", - "value": "+233 24 567 8901" - }, - { - "system": "email", - "value": "kwame.mensah@example.com" - } - ], - "contact": [ - { - "relationship": [ - { - "coding": [ - { - "system": "http://terminology.hl7.org/CodeSystem/v2-0131", - "code": "N", - "display": "Next-of-kin" - } - ] - } - ], - "name": { - "family": "Mensah", - "given": [ - "Kofi" - ] - }, - "telecom": [ - { - "system": "phone", - "value": "+233 24 111 2222" - } - ] - } - ] - } - }, - { - "resource": { - "resourceType": "Patient", - "id": "p3", - "identifier": [ - { - "type": { - "coding": [ - { - "code": "MR" - } - ] - }, - "value": "MRN-2020-1234" - } - ], - "name": [ - { - "family": "Diallo", - "given": [ - "Yuki" - ] - } - ], - "gender": "female", - "birthDate": "2000-07-19", - "active": false, - "telecom": [ - { - "system": "phone", - "value": "+254 722 345 678" - }, - { - "system": "email", - "value": "aisha.osei@example.com" - } - ], - "contact": [ - { - "relationship": [ - { - "coding": [ - { - "system": "http://terminology.hl7.org/CodeSystem/v2-0131", - "code": "C", - "display": "Emergency Contact" - } - ] - } - ], - "name": { - "family": "Osei", - "given": [ - "Grace" - ] - }, - "telecom": [ - { - "system": "phone", - "value": "+254 700 333 444" - } - ] - }, - { - "relationship": [ - { - "coding": [ - { - "system": "http://terminology.hl7.org/CodeSystem/v3-RoleCode", - "code": "SPS", - "display": "Spouse" - } - ] - } - ] - } - ] - } - }, - { - "resource": { - "resourceType": "Patient", - "id": "p4", - "identifier": [ - { - "type": { - "coding": [ - { - "code": "MR" - } - ] - }, - "value": "MRN-2022-0055" - } - ], - "name": [ - { - "family": "Diallo", - "given": [ - "Lamine" - ] - } - ], - "gender": "male", - "birthDate": "2015-11-08", - "active": true, - "telecom": [ - { - "system": "email", - "value": "chidi.diallo@example.com" - } - ], - "contact": [ - { - "relationship": [ - { - "coding": [ - { - "system": "http://terminology.hl7.org/CodeSystem/v3-RoleCode", - "code": "CHILD", - "display": "Child" - } - ] - } - ] - } - ] - } - }, - { - "resource": { - "resourceType": "Patient", - "id": "p5", - "identifier": [ - { - "type": { - "coding": [ - { - "code": "MR" - } - ] - }, - "value": "MRN-2011-0321" - } - ], - "name": [ - { - "family": "Mensah", - "given": [ - "Ama" - ] - } - ], - "gender": "female", - "birthDate": "1988-04-23", - "active": true, - "telecom": [ - { - "system": "phone", - "value": "+233 20 987 6543" - }, - { - "system": "email", - "value": "ama.mensah@example.com" - } - ], - "contact": [ - { - "relationship": [ - { - "coding": [ - { - "system": "http://terminology.hl7.org/CodeSystem/v2-0131", - "code": "C", - "display": "Emergency Contact" - } - ] - } - ], - "name": { - "family": "Mensah", - "given": [ - "Emmanuel" - ] - }, - "telecom": [ - { - "system": "phone", - "value": "+233 20 555 6666" - } - ] - }, - { - "relationship": [ - { - "coding": [ - { - "system": "http://terminology.hl7.org/CodeSystem/v3-RoleCode", - "code": "SPS", - "display": "Spouse" - } - ] - } - ] - } - ] - } - }, - { - "resource": { - "resourceType": "Patient", - "id": "p6", - "identifier": [ - { - "type": { - "coding": [ - { - "code": "MR" - } - ] - }, - "value": "MRN-1978-0610" - } - ], - "name": [ - { - "family": "Okonkwo", - "given": [ - "Emmanuel" - ] - } - ], - "gender": "male", - "birthDate": "1978-05-14", - "active": true, - "telecom": [ - { - "system": "phone", - "value": "+234 802 123 4567" - }, - { - "system": "email", - "value": "adaeze.okonkwo@example.com" - } - ], - "contact": [ - { - "relationship": [ - { - "coding": [ - { - "system": "http://terminology.hl7.org/CodeSystem/v2-0131", - "code": "N", - "display": "Next-of-kin" - } - ] - } - ], - "name": { - "family": "Okonkwo", - "given": [ - "Ngozi" - ] - }, - "telecom": [ - { - "system": "phone", - "value": "+234 802 777 888" - } - ] - } - ] - } - }, - { - "resource": { - "resourceType": "Patient", - "id": "p7", - "identifier": [ - { - "type": { - "coding": [ - { - "code": "MR" - } - ] - }, - "value": "MRN-2005-0785" - } - ], - "name": [ - { - "family": "Okonkwo", - "given": [ - "Chioma" - ] - } - ], - "gender": "female", - "birthDate": "2005-03-22", - "active": true, - "telecom": [ - { - "system": "phone", - "value": "+234 803 234 5678" - }, - { - "system": "email", - "value": "emeka.okonkwo@example.com" - } - ], - "contact": [ - { - "relationship": [ - { - "coding": [ - { - "system": "http://terminology.hl7.org/CodeSystem/v2-0131", - "code": "C", - "display": "Emergency Contact" - } - ] - } - ], - "name": { - "family": "Okonkwo", - "given": [ - "Emeka" - ] - }, - "telecom": [ - { - "system": "phone", - "value": "+234 803 999 000" - } - ] - }, - { - "relationship": [ - { - "coding": [ - { - "system": "http://terminology.hl7.org/CodeSystem/v3-RoleCode", - "code": "CHILD", - "display": "Child" - } - ] - } - ] - } - ] - } - }, - { - "resource": { - "resourceType": "Patient", - "id": "p8", - "identifier": [ - { - "type": { - "coding": [ - { - "code": "MR" - } - ] - }, - "value": "MRN-2008-1122" - } - ], - "name": [ - { - "family": "Okonkwo", - "given": [ - "Olumide" - ] - } - ], - "gender": "male", - "birthDate": "2008-09-07", - "active": true, - "telecom": [ - { - "system": "email", - "value": "chiamaka.okonkwo@example.com" - } - ], - "contact": [ - { - "relationship": [ - { - "coding": [ - { - "system": "http://terminology.hl7.org/CodeSystem/v3-RoleCode", - "code": "CHILD", - "display": "Child" - } - ] - } - ] - } - ] - } - }, - { - "resource": { - "resourceType": "Patient", - "id": "p9", - "identifier": [ - { - "type": { - "coding": [ - { - "code": "MR" - } - ] - }, - "value": "MRN-1975-0403" - } - ], - "name": [ - { - "family": "Nkosi", - "given": [ - "Thabo" - ] - } - ], - "gender": "male", - "birthDate": "1975-07-30", - "active": true, - "telecom": [ - { - "system": "phone", - "value": "+27 82 345 6789" - }, - { - "system": "email", - "value": "sipho.nkosi@example.com" - } - ], - "contact": [ - { - "relationship": [ - { - "coding": [ - { - "system": "http://terminology.hl7.org/CodeSystem/v2-0131", - "code": "C", - "display": "Emergency Contact" - } - ] - } - ], - "name": { - "family": "Nkosi", - "given": [ - "Zanele" - ] - }, - "telecom": [ - { - "system": "phone", - "value": "+27 82 111 2222" - } - ] - } - ] - } - }, - { - "resource": { - "resourceType": "Patient", - "id": "p10", - "identifier": [ - { - "type": { - "coding": [ - { - "code": "MR" - } - ] - }, - "value": "MRN-1978-1204" - } - ], - "name": [ - { - "family": "Nkosi", - "given": [ - "Nomvula" - ] - } - ], - "gender": "female", - "birthDate": "1978-12-15", - "active": true, - "telecom": [ - { - "system": "phone", - "value": "+27 83 456 7890" - }, - { - "system": "email", - "value": "nomvula.nkosi@example.com" - } - ], - "contact": [ - { - "relationship": [ - { - "coding": [ - { - "system": "http://terminology.hl7.org/CodeSystem/v3-RoleCode", - "code": "SPS", - "display": "Spouse" - } - ] - } - ] - } - ] - } - }, - { - "resource": { - "resourceType": "Patient", - "id": "p11", - "identifier": [ - { - "type": { - "coding": [ - { - "code": "MR" - } - ] - }, - "value": "MRN-1965-0234" - } - ], - "name": [ - { - "family": "Traoré", - "given": [ - "Ibrahim" - ] - } - ], - "gender": "male", - "birthDate": "1965-03-10", - "active": true, - "telecom": [ - { - "system": "phone", - "value": "+223 76 123 456" - }, - { - "system": "email", - "value": "mamadou.traore@example.com" - } - ], - "contact": [ - { - "relationship": [ - { - "coding": [ - { - "system": "http://terminology.hl7.org/CodeSystem/v2-0131", - "code": "N", - "display": "Next-of-kin" - } - ] - } - ], - "name": { - "family": "Traoré", - "given": [ - "Ibrahim" - ] - }, - "telecom": [ - { - "system": "phone", - "value": "+223 76 333 444" - } - ] - } - ] - } - }, - { - "resource": { - "resourceType": "Patient", - "id": "p12", - "identifier": [ - { - "type": { - "coding": [ - { - "code": "MR" - } - ] - }, - "value": "MRN-1968-0987" - } - ], - "name": [ - { - "family": "Traoré", - "given": [ - "Fatoumata" - ] - } - ], - "gender": "female", - "birthDate": "1968-08-25", - "active": true, - "telecom": [ - { - "system": "phone", - "value": "+223 77 234 567" - }, - { - "system": "email", - "value": "mariam.traore@example.com" - } - ], - "contact": [ - { - "relationship": [ - { - "coding": [ - { - "system": "http://terminology.hl7.org/CodeSystem/v3-RoleCode", - "code": "SPS", - "display": "Spouse" - } - ] - } - ] - } - ] - } - }, - { - "resource": { - "resourceType": "Patient", - "id": "p13", - "identifier": [ - { - "type": { - "coding": [ - { - "code": "MR" - } - ] - }, - "value": "MRN-2000-1456" - } - ], - "name": [ - { - "family": "Traoré", - "given": [ - "Moussa" - ] - } - ], - "gender": "male", - "birthDate": "2000-11-18", - "active": true, - "telecom": [ - { - "system": "email", - "value": "boubacar.traore@example.com" - } - ], - "contact": [ - { - "relationship": [ - { - "coding": [ - { - "system": "http://terminology.hl7.org/CodeSystem/v3-RoleCode", - "code": "CHILD", - "display": "Child" - } - ] - } - ] - } - ] - } - }, - { - "resource": { - "resourceType": "Patient", - "id": "p14", - "identifier": [ - { - "type": { - "coding": [ - { - "code": "MR" - } - ] - }, - "value": "MRN-1982-0563" - } - ], - "name": [ - { - "family": "Kamau", - "given": [ - "James" - ] - } - ], - "gender": "male", - "birthDate": "1982-06-12", - "active": true, - "telecom": [ - { - "system": "phone", - "value": "+254 722 123 456" - } - ] - } - }, - { - "resource": { - "resourceType": "Patient", - "id": "p15", - "identifier": [ - { - "type": { - "coding": [ - { - "code": "MR" - } - ] - }, - "value": "MRN-1985-0234" - } - ], - "name": [ - { - "family": "Kamau", - "given": [ - "Grace" - ] - } - ], - "gender": "female", - "birthDate": "1985-02-28", - "active": true, - "contact": [ - { - "relationship": [ - { - "coding": [ - { - "system": "http://terminology.hl7.org/CodeSystem/v3-RoleCode", - "code": "SPS", - "display": "Spouse" - } - ] - } - ] - } - ] - } - }, - { - "resource": { - "resourceType": "Patient", - "id": "p16", - "identifier": [ - { - "type": { - "coding": [ - { - "code": "MR" - } - ] - }, - "value": "MRN-1990-0891" - } - ], - "name": [ - { - "family": "Mwangi", - "given": [ - "Peter" - ] - } - ], - "gender": "male", - "birthDate": "1990-09-19", - "active": true, - "telecom": [ - { - "system": "phone", - "value": "+254 710 234 567" - } - ] - } - }, - { - "resource": { - "resourceType": "Patient", - "id": "p17", - "identifier": [ - { - "type": { - "coding": [ - { - "code": "MR" - } - ] - }, - "value": "MRN-1992-0345" - } - ], - "name": [ - { - "family": "Mwangi", - "given": [ - "Mary" - ] - } - ], - "gender": "female", - "birthDate": "1992-04-03", - "active": true, - "contact": [ - { - "relationship": [ - { - "coding": [ - { - "system": "http://terminology.hl7.org/CodeSystem/v3-RoleCode", - "code": "SPS", - "display": "Spouse" - } - ] - } - ] - } - ] - } - }, - { - "resource": { - "resourceType": "Patient", - "id": "p18", - "identifier": [ - { - "type": { - "coding": [ - { - "code": "MR" - } - ] - }, - "value": "MRN-2018-0678" - } - ], - "name": [ - { - "family": "Mwangi", - "given": [ - "Samuel" - ] - } - ], - "gender": "male", - "birthDate": "2018-12-01", - "active": true, - "contact": [ - { - "relationship": [ - { - "coding": [ - { - "system": "http://terminology.hl7.org/CodeSystem/v3-RoleCode", - "code": "CHILD", - "display": "Child" - } - ] - } - ] - } - ] - } - }, - { - "resource": { - "resourceType": "Patient", - "id": "p19", - "identifier": [ - { - "type": { - "coding": [ - { - "code": "MR" - } - ] - }, - "value": "MRN-1955-0123" - } - ], - "name": [ - { - "family": "Achebe", - "given": [ - "Chinwe" - ] - } - ], - "gender": "female", - "birthDate": "1955-01-17", - "active": true, - "telecom": [ - { - "system": "phone", - "value": "+234 805 345 6789" - } - ] - } - }, - { - "resource": { - "resourceType": "Patient", - "id": "p20", - "identifier": [ - { - "type": { - "coding": [ - { - "code": "MR" - } - ] - }, - "value": "MRN-1958-0567" - } - ], - "name": [ - { - "family": "Achebe", - "given": [ - "Emeka" - ] - } - ], - "gender": "male", - "birthDate": "1958-10-22", - "active": true, - "telecom": [ - { - "system": "phone", - "value": "+234 806 456 7890" - } - ], - "contact": [ - { - "relationship": [ - { - "coding": [ - { - "system": "http://terminology.hl7.org/CodeSystem/v3-RoleCode", - "code": "SPS", - "display": "Spouse" - } - ] - } - ] - } - ] - } - }, - { - "resource": { - "resourceType": "Patient", - "id": "p21", - "identifier": [ - { - "type": { - "coding": [ - { - "code": "MR" - } - ] - }, - "value": "MRN-1972-0456" - } - ], - "name": [ - { - "family": "Osei", - "given": [ - "Kwame" - ] - } - ], - "gender": "male", - "birthDate": "1972-08-11", - "active": true, - "telecom": [ - { - "system": "phone", - "value": "+233 24 678 9012" - } - ] - } - }, - { - "resource": { - "resourceType": "Patient", - "id": "p22", - "identifier": [ - { - "type": { - "coding": [ - { - "code": "MR" - } - ] - }, - "value": "MRN-1975-0234" - } - ], - "name": [ - { - "family": "Osei", - "given": [ - "Abena" - ] - } - ], - "gender": "female", - "birthDate": "1975-05-07", - "active": true, - "telecom": [ - { - "system": "phone", - "value": "+233 20 789 0123" - } - ], - "contact": [ - { - "relationship": [ - { - "coding": [ - { - "system": "http://terminology.hl7.org/CodeSystem/v3-RoleCode", - "code": "SPS", - "display": "Spouse" - } - ] - } - ] - } - ] - } - }, - { - "resource": { - "resourceType": "Patient", - "id": "p23", - "identifier": [ - { - "type": { - "coding": [ - { - "code": "MR" - } - ] - }, - "value": "MRN-2002-0891" - } - ], - "name": [ - { - "family": "Osei", - "given": [ - "Yaw" - ] - } - ], - "gender": "male", - "birthDate": "2002-03-29", - "active": true, - "contact": [ - { - "relationship": [ - { - "coding": [ - { - "system": "http://terminology.hl7.org/CodeSystem/v3-RoleCode", - "code": "CHILD", - "display": "Child" - } - ] - } - ] - } - ] - } - }, - { - "resource": { - "resourceType": "Patient", - "id": "p24", - "identifier": [ - { - "type": { - "coding": [ - { - "code": "MR" - } - ] - }, - "value": "MRN-1988-0734" - } - ], - "name": [ - { - "family": "Banda", - "given": [ - "Moses" - ] - } - ], - "gender": "male", - "birthDate": "1988-07-16", - "active": true, - "telecom": [ - { - "system": "phone", - "value": "+265 99 123 456" - } - ] - } - }, - { - "resource": { - "resourceType": "Patient", - "id": "p25", - "identifier": [ - { - "type": { - "coding": [ - { - "code": "MR" - } - ] - }, - "value": "MRN-1990-0567" - } - ], - "name": [ - { - "family": "Banda", - "given": [ - "Chisomo" - ] - } - ], - "gender": "female", - "birthDate": "1990-12-04", - "active": true, - "contact": [ - { - "relationship": [ - { - "coding": [ - { - "system": "http://terminology.hl7.org/CodeSystem/v3-RoleCode", - "code": "SPS", - "display": "Spouse" - } - ] - } - ] - } - ] - } - }, - { - "resource": { - "resourceType": "Patient", - "id": "p26", - "identifier": [ - { - "type": { - "coding": [ - { - "code": "MR" - } - ] - }, - "value": "MRN-1960-0345" - } - ], - "name": [ - { - "family": "Touré", - "given": [ - "Mamadou" - ] - } - ], - "gender": "male", - "birthDate": "1960-02-28", - "active": true, - "telecom": [ - { - "system": "phone", - "value": "+224 622 123 456" - } - ] - } - }, - { - "resource": { - "resourceType": "Patient", - "id": "p27", - "identifier": [ - { - "type": { - "coding": [ - { - "code": "MR" - } - ] - }, - "value": "MRN-1963-0678" - } - ], - "name": [ - { - "family": "Touré", - "given": [ - "Aissatou" - ] - } - ], - "gender": "female", - "birthDate": "1963-09-14", - "active": true, - "telecom": [ - { - "system": "phone", - "value": "+224 623 234 567" - } - ], - "contact": [ - { - "relationship": [ - { - "coding": [ - { - "system": "http://terminology.hl7.org/CodeSystem/v3-RoleCode", - "code": "SPS", - "display": "Spouse" - } - ] - } - ] - } - ] - } - }, - { - "resource": { - "resourceType": "Patient", - "id": "p28", - "identifier": [ - { - "type": { - "coding": [ - { - "code": "MR" - } - ] - }, - "value": "MRN-1995-0234" - } - ], - "name": [ - { - "family": "Dlamini", - "given": [ - "Sibusiso" - ] - } - ], - "gender": "male", - "birthDate": "1995-04-22", - "active": true, - "telecom": [ - { - "system": "phone", - "value": "+268 7600 1234" - } - ] - } - }, - { - "resource": { - "resourceType": "Patient", - "id": "p29", - "identifier": [ - { - "type": { - "coding": [ - { - "code": "MR" - } - ] - }, - "value": "MRN-1997-0567" - } - ], - "name": [ - { - "family": "Dlamini", - "given": [ - "Nompilo" - ] - } - ], - "gender": "female", - "birthDate": "1997-11-09", - "active": true, - "contact": [ - { - "relationship": [ - { - "coding": [ - { - "system": "http://terminology.hl7.org/CodeSystem/v3-RoleCode", - "code": "SPS", - "display": "Spouse" - } - ] - } - ] - } - ] - } - }, - { - "resource": { - "resourceType": "Patient", - "id": "p30", - "identifier": [ - { - "type": { - "coding": [ - { - "code": "MR" - } - ] - }, - "value": "MRN-2022-0234" - } - ], - "name": [ - { - "family": "Dlamini", - "given": [ - "Lihle" - ] - } - ], - "gender": "female", - "birthDate": "2022-03-15", - "active": true, - "contact": [ - { - "relationship": [ - { - "coding": [ - { - "system": "http://terminology.hl7.org/CodeSystem/v3-RoleCode", - "code": "CHILD", - "display": "Child" - } - ] - } - ] - } - ] - } - }, - { - "resource": { - "resourceType": "Patient", - "id": "p31", - "identifier": [ - { - "type": { - "coding": [ - { - "code": "MR" - } - ] - }, - "value": "MRN-1963-0456" - } - ], - "name": [ - { - "family": "Adeyemi", - "given": [ - "Adewale" - ] - } - ], - "gender": "male", - "birthDate": "1963-06-05", - "active": true, - "telecom": [ - { - "system": "phone", - "value": "+234 807 567 8901" - } - ] - } - }, - { - "resource": { - "resourceType": "Patient", - "id": "p32", - "identifier": [ - { - "type": { - "coding": [ - { - "code": "MR" - } - ] - }, - "value": "MRN-1967-0789" - } - ], - "name": [ - { - "family": "Adeyemi", - "given": [ - "Folake" - ] - } - ], - "gender": "female", - "birthDate": "1967-01-20", - "active": true, - "telecom": [ - { - "system": "phone", - "value": "+234 808 678 9012" - } - ], - "contact": [ - { - "relationship": [ - { - "coding": [ - { - "system": "http://terminology.hl7.org/CodeSystem/v3-RoleCode", - "code": "SPS", - "display": "Spouse" - } - ] - } - ] - } - ] - } - }, - { - "resource": { - "resourceType": "Patient", - "id": "p33", - "identifier": [ - { - "type": { - "coding": [ - { - "code": "MR" - } - ] - }, - "value": "MRN-1980-0345" - } - ], - "name": [ - { - "family": "Owusu", - "given": [ - "Nana" - ] - } - ], - "gender": "female", - "birthDate": "1980-10-13", - "active": true, - "telecom": [ - { - "system": "phone", - "value": "+233 20 901 2345" - } - ] - } - }, - { - "resource": { - "resourceType": "Patient", - "id": "p34", - "identifier": [ - { - "type": { - "coding": [ - { - "code": "MR" - } - ] - }, - "value": "MRN-1978-0234" - } - ], - "name": [ - { - "family": "Owusu", - "given": [ - "Kweku" - ] - } - ], - "gender": "male", - "birthDate": "1978-04-27", - "active": true, - "telecom": [ - { - "system": "phone", - "value": "+233 24 012 3456" - } - ], - "contact": [ - { - "relationship": [ - { - "coding": [ - { - "system": "http://terminology.hl7.org/CodeSystem/v3-RoleCode", - "code": "SPS", - "display": "Spouse" - } - ] - } - ] - } - ] - } - }, - { - "resource": { - "resourceType": "Patient", - "id": "p35", - "identifier": [ - { - "type": { - "coding": [ - { - "code": "MR" - } - ] - }, - "value": "MRN-2010-0678" - } - ], - "name": [ - { - "family": "Owusu", - "given": [ - "Ama" - ] - } - ], - "gender": "female", - "birthDate": "2010-07-08", - "active": true, - "contact": [ - { - "relationship": [ - { - "coding": [ - { - "system": "http://terminology.hl7.org/CodeSystem/v3-RoleCode", - "code": "CHILD", - "display": "Child" - } - ] - } - ] - } - ] - } - }, - { - "resource": { - "resourceType": "Patient", - "id": "p36", - "identifier": [ - { - "type": { - "coding": [ - { - "code": "MR" - } - ] - }, - "value": "MRN-1970-0789" - } - ], - "name": [ - { - "family": "Kabila", - "given": [ - "Jean-Pierre" - ] - } - ], - "gender": "male", - "birthDate": "1970-12-01", - "active": true, - "telecom": [ - { - "system": "phone", - "value": "+243 99 123 456" - } - ] - } - }, - { - "resource": { - "resourceType": "Patient", - "id": "p37", - "identifier": [ - { - "type": { - "coding": [ - { - "code": "MR" - } - ] - }, - "value": "MRN-1973-0345" - } - ], - "name": [ - { - "family": "Kabila", - "given": [ - "Marie" - ] - } - ], - "gender": "female", - "birthDate": "1973-08-19", - "active": true, - "contact": [ - { - "relationship": [ - { - "coding": [ - { - "system": "http://terminology.hl7.org/CodeSystem/v3-RoleCode", - "code": "SPS", - "display": "Spouse" - } - ] - } - ] - } - ] - } - }, - { - "resource": { - "resourceType": "Patient", - "id": "p38", - "identifier": [ - { - "type": { - "coding": [ - { - "code": "MR" - } - ] - }, - "value": "MRN-1985-0456" - } - ], - "name": [ - { - "family": "Ndiaye", - "given": [ - "Abdoulaye" - ] - } - ], - "gender": "male", - "birthDate": "1985-05-23", - "active": true, - "telecom": [ - { - "system": "phone", - "value": "+221 77 345 6789" - } - ] - } - }, - { - "resource": { - "resourceType": "Patient", - "id": "p39", - "identifier": [ - { - "type": { - "coding": [ - { - "code": "MR" - } - ] - }, - "value": "MRN-1988-0234" - } - ], - "name": [ - { - "family": "Ndiaye", - "given": [ - "Mariama" - ] - } - ], - "gender": "female", - "birthDate": "1988-02-11", - "active": true, - "contact": [ - { - "relationship": [ - { - "coding": [ - { - "system": "http://terminology.hl7.org/CodeSystem/v3-RoleCode", - "code": "SPS", - "display": "Spouse" - } - ] - } - ] - } - ] - } - }, - { - "resource": { - "resourceType": "Patient", - "id": "p40", - "identifier": [ - { - "type": { - "coding": [ - { - "code": "MR" - } - ] - }, - "value": "MRN-1975-0567" - } - ], - "name": [ - { - "family": "Mutua", - "given": [ - "Josephat" - ] - } - ], - "gender": "male", - "birthDate": "1975-11-07", - "active": true, - "telecom": [ - { - "system": "phone", - "value": "+254 711 123 456" - } - ] - } - }, - { - "resource": { - "resourceType": "Patient", - "id": "p41", - "identifier": [ - { - "type": { - "coding": [ - { - "code": "MR" - } - ] - }, - "value": "MRN-1978-0567" - } - ], - "name": [ - { - "family": "Mutua", - "given": [ - "Catherine" - ] - } - ], - "gender": "female", - "birthDate": "1978-06-30", - "active": true, - "telecom": [ - { - "system": "phone", - "value": "+254 712 234 567" - } - ], - "contact": [ - { - "relationship": [ - { - "coding": [ - { - "system": "http://terminology.hl7.org/CodeSystem/v3-RoleCode", - "code": "SPS", - "display": "Spouse" - } - ] - } - ] - } - ] - } - }, - { - "resource": { - "resourceType": "Patient", - "id": "p42", - "identifier": [ - { - "type": { - "coding": [ - { - "code": "MR" - } - ] - }, - "value": "MRN-2005-0901" - } - ], - "name": [ - { - "family": "Mutua", - "given": [ - "Kevin" - ] - } - ], - "gender": "male", - "birthDate": "2005-09-14", - "active": true, - "contact": [ - { - "relationship": [ - { - "coding": [ - { - "system": "http://terminology.hl7.org/CodeSystem/v3-RoleCode", - "code": "CHILD", - "display": "Child" - } - ] - } - ] - } - ] - } - }, - { - "resource": { - "resourceType": "Patient", - "id": "p43", - "identifier": [ - { - "type": { - "coding": [ - { - "code": "MR" - } - ] - }, - "value": "MRN-1968-0345" - } - ], - "name": [ - { - "family": "Appiah", - "given": [ - "Kofi" - ] - } - ], - "gender": "male", - "birthDate": "1968-03-25", - "active": true, - "telecom": [ - { - "system": "phone", - "value": "+233 24 234 5678" - } - ] - } - }, - { - "resource": { - "resourceType": "Patient", - "id": "p44", - "identifier": [ - { - "type": { - "coding": [ - { - "code": "MR" - } - ] - }, - "value": "MRN-1970-0456" - } - ], - "name": [ - { - "family": "Appiah", - "given": [ - "Akosua" - ] - } - ], - "gender": "female", - "birthDate": "1970-10-18", - "active": true, - "contact": [ - { - "relationship": [ - { - "coding": [ - { - "system": "http://terminology.hl7.org/CodeSystem/v3-RoleCode", - "code": "SPS", - "display": "Spouse" - } - ] - } - ] - } - ] - } - }, - { - "resource": { - "resourceType": "Patient", - "id": "p45", - "identifier": [ - { - "type": { - "coding": [ - { - "code": "MR" - } - ] - }, - "value": "MRN-1992-0678" - } - ], - "name": [ - { - "family": "Oduya", - "given": [ - "Sunday" - ] - } - ], - "gender": "male", - "birthDate": "1992-01-31", - "active": true, - "telecom": [ - { - "system": "phone", - "value": "+234 809 789 0123" - } - ] - } - }, - { - "resource": { - "resourceType": "Patient", - "id": "p46", - "identifier": [ - { - "type": { - "coding": [ - { - "code": "MR" - } - ] - }, - "value": "MRN-1995-0567" - } - ], - "name": [ - { - "family": "Oduya", - "given": [ - "Blessing" - ] - } - ], - "gender": "female", - "birthDate": "1995-08-16", - "active": true, - "contact": [ - { - "relationship": [ - { - "coding": [ - { - "system": "http://terminology.hl7.org/CodeSystem/v3-RoleCode", - "code": "SPS", - "display": "Spouse" - } - ] - } - ] - } - ] - } - }, - { - "resource": { - "resourceType": "Patient", - "id": "p47", - "identifier": [ - { - "type": { - "coding": [ - { - "code": "MR" - } - ] - }, - "value": "MRN-1950-0123" - } - ], - "name": [ - { - "family": "Nakamura", - "given": [ - "Hiroshi" - ] - } - ], - "gender": "male", - "birthDate": "1950-07-04", - "active": true, - "telecom": [ - { - "system": "phone", - "value": "+81 3 1234 5678" - } - ] - } - }, - { - "resource": { - "resourceType": "Group", - "id": "g1", - "type": "person", - "actual": true, - "name": "Diallo Household", - "member": [ - { - "entity": { - "reference": "Patient/p1" - } - }, - { - "entity": { - "reference": "Patient/p3" - } - }, - { - "entity": { - "reference": "Patient/p4" - } - } - ] - } - }, - { - "resource": { - "resourceType": "Group", - "id": "g2", - "type": "person", - "actual": true, - "name": "Mensah Household", - "member": [ - { - "entity": { - "reference": "Patient/p2" - } - }, - { - "entity": { - "reference": "Patient/p5" - } - } - ] - } - }, - { - "resource": { - "resourceType": "Group", - "id": "g3", - "type": "person", - "actual": true, - "name": "Okonkwo Household", - "member": [ - { - "entity": { - "reference": "Patient/p6" - } - }, - { - "entity": { - "reference": "Patient/p7" - } - }, - { - "entity": { - "reference": "Patient/p8" - } - } - ] - } - }, - { - "resource": { - "resourceType": "Group", - "id": "g4", - "type": "person", - "actual": true, - "name": "Nkosi Household", - "member": [ - { - "entity": { - "reference": "Patient/p9" - } - }, - { - "entity": { - "reference": "Patient/p10" - } - } - ] - } - }, - { - "resource": { - "resourceType": "Group", - "id": "g5", - "type": "person", - "actual": true, - "name": "Traoré Household", - "member": [ - { - "entity": { - "reference": "Patient/p11" - } - }, - { - "entity": { - "reference": "Patient/p12" - } - }, - { - "entity": { - "reference": "Patient/p13" - } - } - ] - } - }, - { - "resource": { - "resourceType": "Group", - "id": "g6", - "type": "person", - "actual": true, - "name": "Kamau Household", - "member": [ - { - "entity": { - "reference": "Patient/p14" - } - }, - { - "entity": { - "reference": "Patient/p15" - } - } - ] - } - }, - { - "resource": { - "resourceType": "Group", - "id": "g7", - "type": "person", - "actual": true, - "name": "Mwangi Household", - "member": [ - { - "entity": { - "reference": "Patient/p16" - } - }, - { - "entity": { - "reference": "Patient/p17" - } - }, - { - "entity": { - "reference": "Patient/p18" - } - } - ] - } - }, - { - "resource": { - "resourceType": "Group", - "id": "g8", - "type": "person", - "actual": true, - "name": "Achebe Household", - "member": [ - { - "entity": { - "reference": "Patient/p19" - } - }, - { - "entity": { - "reference": "Patient/p20" - } - } - ] - } - }, - { - "resource": { - "resourceType": "Group", - "id": "g9", - "type": "person", - "actual": true, - "name": "Osei Household", - "member": [ - { - "entity": { - "reference": "Patient/p21" - } - }, - { - "entity": { - "reference": "Patient/p22" - } - }, - { - "entity": { - "reference": "Patient/p23" - } - } - ] - } - }, - { - "resource": { - "resourceType": "Group", - "id": "g10", - "type": "person", - "actual": true, - "name": "Banda Household", - "member": [ - { - "entity": { - "reference": "Patient/p24" - } - }, - { - "entity": { - "reference": "Patient/p25" - } - } - ] - } - }, - { - "resource": { - "resourceType": "Group", - "id": "g11", - "type": "person", - "actual": true, - "name": "Touré Household", - "member": [ - { - "entity": { - "reference": "Patient/p26" - } - }, - { - "entity": { - "reference": "Patient/p27" - } - } - ] - } - }, - { - "resource": { - "resourceType": "Group", - "id": "g12", - "type": "person", - "actual": true, - "name": "Dlamini Household", - "member": [ - { - "entity": { - "reference": "Patient/p28" - } - }, - { - "entity": { - "reference": "Patient/p29" - } - }, - { - "entity": { - "reference": "Patient/p30" - } - } - ] - } - }, - { - "resource": { - "resourceType": "Group", - "id": "g13", - "type": "person", - "actual": true, - "name": "Adeyemi Household", - "member": [ - { - "entity": { - "reference": "Patient/p31" - } - }, - { - "entity": { - "reference": "Patient/p32" - } - } - ] - } - }, - { - "resource": { - "resourceType": "Group", - "id": "g14", - "type": "person", - "actual": true, - "name": "Owusu Household", - "member": [ - { - "entity": { - "reference": "Patient/p33" - } - }, - { - "entity": { - "reference": "Patient/p34" - } - }, - { - "entity": { - "reference": "Patient/p35" - } - } - ] - } - }, - { - "resource": { - "resourceType": "Group", - "id": "g15", - "type": "person", - "actual": true, - "name": "Kabila Household", - "member": [ - { - "entity": { - "reference": "Patient/p36" - } - }, - { - "entity": { - "reference": "Patient/p37" - } - } - ] - } - }, - { - "resource": { - "resourceType": "Group", - "id": "g16", - "type": "person", - "actual": true, - "name": "Ndiaye Household", - "member": [ - { - "entity": { - "reference": "Patient/p38" - } - }, - { - "entity": { - "reference": "Patient/p39" - } - } - ] - } - }, - { - "resource": { - "resourceType": "Group", - "id": "g17", - "type": "person", - "actual": true, - "name": "Mutua Household", - "member": [ - { - "entity": { - "reference": "Patient/p40" - } - }, - { - "entity": { - "reference": "Patient/p41" - } - }, - { - "entity": { - "reference": "Patient/p42" - } - } - ] - } - }, - { - "resource": { - "resourceType": "Group", - "id": "g18", - "type": "person", - "actual": true, - "name": "Appiah Household", - "member": [ - { - "entity": { - "reference": "Patient/p43" - } - }, - { - "entity": { - "reference": "Patient/p44" - } - } - ] - } - }, - { - "resource": { - "resourceType": "Group", - "id": "g19", - "type": "person", - "actual": true, - "name": "Oduya Household", - "member": [ - { - "entity": { - "reference": "Patient/p45" - } - }, - { - "entity": { - "reference": "Patient/p46" - } - } - ] - } - }, - { - "resource": { - "resourceType": "Group", - "id": "g20", - "type": "person", - "actual": true, - "name": "Nakamura Household", - "member": [ - { - "entity": { - "reference": "Patient/p47" - } - } - ] - } - }, - { - "resource": { - "resourceType": "AllergyIntolerance", - "id": "ai-p1-a", - "patient": { - "reference": "Patient/p1" - }, - "code": { - "coding": [ - { - "display": "Penicillin" - } - ] - }, - "criticality": "high", - "clinicalStatus": { - "coding": [ - { - "system": "http://terminology.hl7.org/CodeSystem/allergyintolerance-clinical", - "code": "active" - } - ] - }, - "reaction": [ - { - "manifestation": [ - { - "coding": [ - { - "system": "http://snomed.info/sct", - "code": "247472004", - "display": "Urticaria" - } - ] - } - ], - "severity": "moderate" - }, - { - "manifestation": [ - { - "coding": [ - { - "system": "http://snomed.info/sct", - "code": "39579001", - "display": "Anaphylaxis" - } - ] - } - ], - "severity": "severe" - } - ] - } - }, - { - "resource": { - "resourceType": "AllergyIntolerance", - "id": "ai-p1-b", - "patient": { - "reference": "Patient/p1" - }, - "code": { - "coding": [ - { - "display": "Latex" - } - ] - }, - "criticality": "low", - "clinicalStatus": { - "coding": [ - { - "system": "http://terminology.hl7.org/CodeSystem/allergyintolerance-clinical", - "code": "active" - } - ] - }, - "reaction": [ - { - "manifestation": [ - { - "coding": [ - { - "system": "http://snomed.info/sct", - "code": "271807003", - "display": "Skin rash" - } - ] - } - ], - "severity": "mild" - } - ] - } - }, - { - "resource": { - "resourceType": "AllergyIntolerance", - "id": "ai-p2-a", - "patient": { - "reference": "Patient/p2" - }, - "code": { - "coding": [ - { - "display": "Sulfonamides" - } - ] - }, - "criticality": "high", - "clinicalStatus": { - "coding": [ - { - "system": "http://terminology.hl7.org/CodeSystem/allergyintolerance-clinical", - "code": "active" - } - ] - }, - "reaction": [ - { - "manifestation": [ - { - "coding": [ - { - "system": "http://snomed.info/sct", - "code": "247472004", - "display": "Urticaria" - } - ] - } - ], - "severity": "moderate" - } - ] - } - }, - { - "resource": { - "resourceType": "AllergyIntolerance", - "id": "ai-p2-b", - "patient": { - "reference": "Patient/p2" - }, - "code": { - "coding": [ - { - "display": "Ibuprofen" - } - ] - }, - "criticality": "low", - "clinicalStatus": { - "coding": [ - { - "system": "http://terminology.hl7.org/CodeSystem/allergyintolerance-clinical", - "code": "active" - } - ] - }, - "reaction": [ - { - "manifestation": [ - { - "coding": [ - { - "system": "http://snomed.info/sct", - "code": "422587007", - "display": "Nausea" - } - ] - } - ], - "severity": "mild" - } - ] - } - }, - { - "resource": { - "resourceType": "AllergyIntolerance", - "id": "ai-p3-a", - "patient": { - "reference": "Patient/p3" - }, - "code": { - "coding": [ - { - "display": "Tree nut allergy" - } - ] - }, - "criticality": "high", - "clinicalStatus": { - "coding": [ - { - "system": "http://terminology.hl7.org/CodeSystem/allergyintolerance-clinical", - "code": "active" - } - ] - }, - "reaction": [ - { - "manifestation": [ - { - "coding": [ - { - "system": "http://snomed.info/sct", - "code": "41291007", - "display": "Angioedema" - } - ] - } - ], - "severity": "moderate" - } - ] - } - }, - { - "resource": { - "resourceType": "AllergyIntolerance", - "id": "ai-p3-b", - "patient": { - "reference": "Patient/p3" - }, - "code": { - "coding": [ - { - "display": "Codeine" - } - ] - }, - "criticality": "low", - "clinicalStatus": { - "coding": [ - { - "system": "http://terminology.hl7.org/CodeSystem/allergyintolerance-clinical", - "code": "inactive" - } - ] - }, - "reaction": [ - { - "manifestation": [ - { - "coding": [ - { - "system": "http://snomed.info/sct", - "code": "271807003", - "display": "Skin rash" - } - ] - } - ], - "severity": "mild" - } - ] - } - }, - { - "resource": { - "resourceType": "AllergyIntolerance", - "id": "ai-p6-a", - "patient": { - "reference": "Patient/p6" - }, - "code": { - "coding": [ - { - "display": "Penicillin" - } - ] - }, - "criticality": "high", - "clinicalStatus": { - "coding": [ - { - "system": "http://terminology.hl7.org/CodeSystem/allergyintolerance-clinical", - "code": "active" - } - ] - }, - "reaction": [ - { - "manifestation": [ - { - "coding": [ - { - "system": "http://snomed.info/sct", - "code": "230145002", - "display": "Difficulty breathing" - } - ] - } - ], - "severity": "severe" - } - ] - } - }, - { - "resource": { - "resourceType": "AllergyIntolerance", - "id": "ai-p6-b", - "patient": { - "reference": "Patient/p6" - }, - "code": { - "coding": [ - { - "display": "Dust mites" - } - ] - }, - "criticality": "low", - "clinicalStatus": { - "coding": [ - { - "system": "http://terminology.hl7.org/CodeSystem/allergyintolerance-clinical", - "code": "active" - } - ] - }, - "reaction": [ - { - "manifestation": [ - { - "coding": [ - { - "system": "http://snomed.info/sct", - "code": "422400008", - "display": "Vomiting" - } - ] - } - ], - "severity": "mild" - } - ] - } - }, - { - "resource": { - "resourceType": "AllergyIntolerance", - "id": "ai-p9-a", - "patient": { - "reference": "Patient/p9" - }, - "code": { - "coding": [ - { - "display": "Sulfonamides" - } - ] - }, - "criticality": "high", - "clinicalStatus": { - "coding": [ - { - "system": "http://terminology.hl7.org/CodeSystem/allergyintolerance-clinical", - "code": "active" - } - ] - }, - "reaction": [ - { - "manifestation": [ - { - "coding": [ - { - "system": "http://snomed.info/sct", - "code": "418290006", - "display": "Pruritus" - } - ] - } - ], - "severity": "mild" - } - ] - } - }, - { - "resource": { - "resourceType": "AllergyIntolerance", - "id": "ai-p11-a", - "patient": { - "reference": "Patient/p11" - }, - "code": { - "coding": [ - { - "display": "Aspirin" - } - ] - }, - "criticality": "high", - "clinicalStatus": { - "coding": [ - { - "system": "http://terminology.hl7.org/CodeSystem/allergyintolerance-clinical", - "code": "active" - } - ] - }, - "reaction": [ - { - "manifestation": [ - { - "coding": [ - { - "system": "http://snomed.info/sct", - "code": "247472004", - "display": "Urticaria" - } - ] - } - ], - "severity": "moderate" - } - ] - } - }, - { - "resource": { - "resourceType": "AllergyIntolerance", - "id": "ai-p19-a", - "patient": { - "reference": "Patient/p19" - }, - "code": { - "coding": [ - { - "display": "Penicillin" - } - ] - }, - "criticality": "high", - "clinicalStatus": { - "coding": [ - { - "system": "http://terminology.hl7.org/CodeSystem/allergyintolerance-clinical", - "code": "active" - } - ] - } - } - }, - { - "resource": { - "resourceType": "AllergyIntolerance", - "id": "ai-p23-a", - "patient": { - "reference": "Patient/p23" - }, - "code": { - "coding": [ - { - "display": "Aspirin" - } - ] - }, - "criticality": "unable-to-assess", - "clinicalStatus": { - "coding": [ - { - "system": "http://terminology.hl7.org/CodeSystem/allergyintolerance-clinical", - "code": "active" - } - ] - } - } - }, - { - "resource": { - "resourceType": "AllergyIntolerance", - "id": "ai-p26-a", - "patient": { - "reference": "Patient/p26" - }, - "code": { - "coding": [ - { - "display": "Codeine" - } - ] - }, - "criticality": "low", - "clinicalStatus": { - "coding": [ - { - "system": "http://terminology.hl7.org/CodeSystem/allergyintolerance-clinical", - "code": "inactive" - } - ] - } - } - }, - { - "resource": { - "resourceType": "AllergyIntolerance", - "id": "ai-p31-a", - "patient": { - "reference": "Patient/p31" - }, - "code": { - "coding": [ - { - "display": "Aspirin" - } - ] - }, - "criticality": "high", - "clinicalStatus": { - "coding": [ - { - "system": "http://terminology.hl7.org/CodeSystem/allergyintolerance-clinical", - "code": "active" - } - ] - } - } - }, - { - "resource": { - "resourceType": "AllergyIntolerance", - "id": "ai-p33-a", - "patient": { - "reference": "Patient/p33" - }, - "code": { - "coding": [ - { - "display": "Penicillin" - } - ] - }, - "criticality": "low", - "clinicalStatus": { - "coding": [ - { - "system": "http://terminology.hl7.org/CodeSystem/allergyintolerance-clinical", - "code": "active" - } - ] - } - } - }, - { - "resource": { - "resourceType": "AllergyIntolerance", - "id": "ai-p41-a", - "patient": { - "reference": "Patient/p41" - }, - "code": { - "coding": [ - { - "display": "Shellfish" - } - ] - }, - "criticality": "high", - "clinicalStatus": { - "coding": [ - { - "system": "http://terminology.hl7.org/CodeSystem/allergyintolerance-clinical", - "code": "active" - } - ] - } - } - }, - { - "resource": { - "resourceType": "AllergyIntolerance", - "id": "ai-p43-a", - "patient": { - "reference": "Patient/p43" - }, - "code": { - "coding": [ - { - "display": "Latex" - } - ] - }, - "criticality": "high", - "clinicalStatus": { - "coding": [ - { - "system": "http://terminology.hl7.org/CodeSystem/allergyintolerance-clinical", - "code": "active" - } - ] - } - } - }, - { - "resource": { - "resourceType": "AllergyIntolerance", - "id": "ai-p47-a", - "patient": { - "reference": "Patient/p47" - }, - "code": { - "coding": [ - { - "display": "Penicillin" - } - ] - }, - "criticality": "high", - "clinicalStatus": { - "coding": [ - { - "system": "http://terminology.hl7.org/CodeSystem/allergyintolerance-clinical", - "code": "active" - } - ] - } - } - }, - { - "resource": { - "resourceType": "MedicationRequest", - "id": "med-p1-a", - "status": "active", - "intent": "order", - "subject": { - "reference": "Patient/p1" - }, - "medicationCodeableConcept": { - "coding": [ - { - "display": "Salbutamol 100mcg inhaler" - } - ] - }, - "dosageInstruction": [ - { - "text": "100mcg PRN" - } - ] - } - }, - { - "resource": { - "resourceType": "MedicationRequest", - "id": "med-p1-b", - "status": "active", - "intent": "order", - "subject": { - "reference": "Patient/p1" - }, - "medicationCodeableConcept": { - "coding": [ - { - "display": "Prednisolone 5mg" - } - ] - }, - "dosageInstruction": [ - { - "text": "5mg OD" - } - ] - } - }, - { - "resource": { - "resourceType": "MedicationRequest", - "id": "med-p2-a", - "status": "active", - "intent": "order", - "subject": { - "reference": "Patient/p2" - }, - "medicationCodeableConcept": { - "coding": [ - { - "display": "Metformin 500mg" - } - ] - }, - "dosageInstruction": [ - { - "text": "500mg BD" - } - ] - } - }, - { - "resource": { - "resourceType": "MedicationRequest", - "id": "med-p2-b", - "status": "active", - "intent": "order", - "subject": { - "reference": "Patient/p2" - }, - "medicationCodeableConcept": { - "coding": [ - { - "display": "Lisinopril 10mg" - } - ] - }, - "dosageInstruction": [ - { - "text": "10mg OD" - } - ] - } - }, - { - "resource": { - "resourceType": "MedicationRequest", - "id": "med-p3-a", - "status": "on-hold", - "intent": "order", - "subject": { - "reference": "Patient/p3" - }, - "medicationCodeableConcept": { - "coding": [ - { - "display": "Cetirizine 10mg" - } - ] - }, - "dosageInstruction": [ - { - "text": "10mg OD" - } - ] - } - }, - { - "resource": { - "resourceType": "MedicationRequest", - "id": "med-p3-b", - "status": "stopped", - "intent": "order", - "subject": { - "reference": "Patient/p3" - }, - "medicationCodeableConcept": { - "coding": [ - { - "display": "Loratadine 10mg" - } - ] - }, - "dosageInstruction": [ - { - "text": "10mg OD" - } - ] - } - }, - { - "resource": { - "resourceType": "MedicationRequest", - "id": "med-p6-a", - "status": "active", - "intent": "order", - "subject": { - "reference": "Patient/p6" - }, - "medicationCodeableConcept": { - "coding": [ - { - "display": "Tenofovir/Lamivudine/Dolutegravir 300/300/50mg" - } - ] - }, - "dosageInstruction": [ - { - "text": "1 tablet OD" - } - ] - } - }, - { - "resource": { - "resourceType": "MedicationRequest", - "id": "med-p6-b", - "status": "active", - "intent": "order", - "subject": { - "reference": "Patient/p6" - }, - "medicationCodeableConcept": { - "coding": [ - { - "display": "Cotrimoxazole 960mg" - } - ] - }, - "dosageInstruction": [ - { - "text": "960mg OD" - } - ] - } - }, - { - "resource": { - "resourceType": "MedicationRequest", - "id": "med-p9-a", - "status": "active", - "intent": "order", - "subject": { - "reference": "Patient/p9" - }, - "medicationCodeableConcept": { - "coding": [ - { - "display": "Metformin 500mg" - } - ] - }, - "dosageInstruction": [ - { - "text": "500mg BD" - } - ] - } - }, - { - "resource": { - "resourceType": "MedicationRequest", - "id": "med-p9-b", - "status": "active", - "intent": "order", - "subject": { - "reference": "Patient/p9" - }, - "medicationCodeableConcept": { - "coding": [ - { - "display": "Salbutamol 100mcg inhaler" - } - ] - }, - "dosageInstruction": [ - { - "text": "100mcg PRN" - } - ] - } - }, - { - "resource": { - "resourceType": "MedicationRequest", - "id": "med-p11-a", - "status": "active", - "intent": "order", - "subject": { - "reference": "Patient/p11" - }, - "medicationCodeableConcept": { - "coding": [ - { - "display": "Lisinopril 10mg" - } - ] - }, - "dosageInstruction": [ - { - "text": "10mg OD" - } - ] - } - }, - { - "resource": { - "resourceType": "MedicationRequest", - "id": "med-p11-b", - "status": "active", - "intent": "order", - "subject": { - "reference": "Patient/p11" - }, - "medicationCodeableConcept": { - "coding": [ - { - "display": "Hydrochlorothiazide 12.5mg" - } - ] - }, - "dosageInstruction": [ - { - "text": "12.5mg OD" - } - ] - } - }, - { - "resource": { - "resourceType": "MedicationRequest", - "id": "med-p12-a", - "status": "active", - "intent": "order", - "subject": { - "reference": "Patient/p12" - }, - "medicationCodeableConcept": { - "coding": [ - { - "display": "Ferrous sulfate 200mg" - } - ] - }, - "dosageInstruction": [ - { - "text": "200mg BD" - } - ] - } - }, - { - "resource": { - "resourceType": "MedicationRequest", - "id": "med-p17-a", - "status": "active", - "intent": "order", - "subject": { - "reference": "Patient/p17" - }, - "medicationCodeableConcept": { - "coding": [ - { - "display": "Folic acid 5mg" - } - ] - }, - "dosageInstruction": [ - { - "text": "5mg OD" - } - ] - } - }, - { - "resource": { - "resourceType": "MedicationRequest", - "id": "med-p19-a", - "status": "active", - "intent": "order", - "subject": { - "reference": "Patient/p19" - }, - "medicationCodeableConcept": { - "coding": [ - { - "display": "Lisinopril 10mg" - } - ] - }, - "dosageInstruction": [ - { - "text": "10mg OD" - } - ] - } - }, - { - "resource": { - "resourceType": "MedicationRequest", - "id": "med-p20-a", - "status": "active", - "intent": "order", - "subject": { - "reference": "Patient/p20" - }, - "medicationCodeableConcept": { - "coding": [ - { - "display": "Metformin 500mg" - } - ] - }, - "dosageInstruction": [ - { - "text": "500mg BD" - } - ] - } - }, - { - "resource": { - "resourceType": "MedicationRequest", - "id": "med-p22-a", - "status": "active", - "intent": "order", - "subject": { - "reference": "Patient/p22" - }, - "medicationCodeableConcept": { - "coding": [ - { - "display": "Amlodipine 5mg" - } - ] - }, - "dosageInstruction": [ - { - "text": "5mg OD" - } - ] - } - }, - { - "resource": { - "resourceType": "MedicationRequest", - "id": "med-p23-a", - "status": "active", - "intent": "order", - "subject": { - "reference": "Patient/p23" - }, - "medicationCodeableConcept": { - "coding": [ - { - "display": "Salbutamol 100mcg inhaler" - } - ] - }, - "dosageInstruction": [ - { - "text": "100mcg PRN" - } - ] - } - }, - { - "resource": { - "resourceType": "MedicationRequest", - "id": "med-p24-a", - "status": "active", - "intent": "order", - "subject": { - "reference": "Patient/p24" - }, - "medicationCodeableConcept": { - "coding": [ - { - "display": "Tenofovir/Lamivudine/Dolutegravir 300/300/50mg" - } - ] - }, - "dosageInstruction": [ - { - "text": "1 tablet OD" - } - ] - } - }, - { - "resource": { - "resourceType": "MedicationRequest", - "id": "med-p24-b", - "status": "active", - "intent": "order", - "subject": { - "reference": "Patient/p24" - }, - "medicationCodeableConcept": { - "coding": [ - { - "display": "Cotrimoxazole 960mg" - } - ] - }, - "dosageInstruction": [ - { - "text": "960mg OD" - } - ] - } - }, - { - "resource": { - "resourceType": "MedicationRequest", - "id": "med-p26-a", - "status": "active", - "intent": "order", - "subject": { - "reference": "Patient/p26" - }, - "medicationCodeableConcept": { - "coding": [ - { - "display": "Lisinopril 20mg" - } - ] - }, - "dosageInstruction": [ - { - "text": "20mg OD" - } - ] - } - }, - { - "resource": { - "resourceType": "MedicationRequest", - "id": "med-p26-b", - "status": "active", - "intent": "order", - "subject": { - "reference": "Patient/p26" - }, - "medicationCodeableConcept": { - "coding": [ - { - "display": "Furosemide 40mg" - } - ] - }, - "dosageInstruction": [ - { - "text": "40mg OD" - } - ] - } - }, - { - "resource": { - "resourceType": "MedicationRequest", - "id": "med-p27-a", - "status": "active", - "intent": "order", - "subject": { - "reference": "Patient/p27" - }, - "medicationCodeableConcept": { - "coding": [ - { - "display": "Metformin 500mg" - } - ] - }, - "dosageInstruction": [ - { - "text": "500mg BD" - } - ] - } - }, - { - "resource": { - "resourceType": "MedicationRequest", - "id": "med-p27-b", - "status": "active", - "intent": "order", - "subject": { - "reference": "Patient/p27" - }, - "medicationCodeableConcept": { - "coding": [ - { - "display": "Insulin Glargine" - } - ] - }, - "dosageInstruction": [ - { - "text": "10 units OD at bedtime" - } - ] - } - }, - { - "resource": { - "resourceType": "MedicationRequest", - "id": "med-p29-a", - "status": "active", - "intent": "order", - "subject": { - "reference": "Patient/p29" - }, - "medicationCodeableConcept": { - "coding": [ - { - "display": "Artemether-Lumefantrine 80/480mg" - } - ] - }, - "dosageInstruction": [ - { - "text": "4 tablets BD for 3 days" - } - ] - } - }, - { - "resource": { - "resourceType": "MedicationRequest", - "id": "med-p31-a", - "status": "active", - "intent": "order", - "subject": { - "reference": "Patient/p31" - }, - "medicationCodeableConcept": { - "coding": [ - { - "display": "Amlodipine 5mg" - } - ] - }, - "dosageInstruction": [ - { - "text": "5mg OD" - } - ] - } - }, - { - "resource": { - "resourceType": "MedicationRequest", - "id": "med-p31-b", - "status": "active", - "intent": "order", - "subject": { - "reference": "Patient/p31" - }, - "medicationCodeableConcept": { - "coding": [ - { - "display": "Metformin 1g" - } - ] - }, - "dosageInstruction": [ - { - "text": "1g BD" - } - ] - } - }, - { - "resource": { - "resourceType": "MedicationRequest", - "id": "med-p32-a", - "status": "active", - "intent": "order", - "subject": { - "reference": "Patient/p32" - }, - "medicationCodeableConcept": { - "coding": [ - { - "display": "Levothyroxine 50mcg" - } - ] - }, - "dosageInstruction": [ - { - "text": "50mcg OD fasting" - } - ] - } - }, - { - "resource": { - "resourceType": "MedicationRequest", - "id": "med-p33-a", - "status": "active", - "intent": "order", - "subject": { - "reference": "Patient/p33" - }, - "medicationCodeableConcept": { - "coding": [ - { - "display": "Salbutamol 100mcg inhaler" - } - ] - }, - "dosageInstruction": [ - { - "text": "100mcg PRN" - } - ] - } - }, - { - "resource": { - "resourceType": "MedicationRequest", - "id": "med-p36-a", - "status": "active", - "intent": "order", - "subject": { - "reference": "Patient/p36" - }, - "medicationCodeableConcept": { - "coding": [ - { - "display": "Artemether-Lumefantrine 80/480mg" - } - ] - }, - "dosageInstruction": [ - { - "text": "4 tablets BD for 3 days" - } - ] - } - }, - { - "resource": { - "resourceType": "MedicationRequest", - "id": "med-p36-b", - "status": "active", - "intent": "order", - "subject": { - "reference": "Patient/p36" - }, - "medicationCodeableConcept": { - "coding": [ - { - "display": "Rifampicin 600mg" - } - ] - }, - "dosageInstruction": [ - { - "text": "600mg OD" - } - ] - } - }, - { - "resource": { - "resourceType": "MedicationRequest", - "id": "med-p37-a", - "status": "active", - "intent": "order", - "subject": { - "reference": "Patient/p37" - }, - "medicationCodeableConcept": { - "coding": [ - { - "display": "Ferrous sulfate 200mg" - } - ] - }, - "dosageInstruction": [ - { - "text": "200mg BD" - } - ] - } - }, - { - "resource": { - "resourceType": "MedicationRequest", - "id": "med-p39-a", - "status": "active", - "intent": "order", - "subject": { - "reference": "Patient/p39" - }, - "medicationCodeableConcept": { - "coding": [ - { - "display": "Lisinopril 10mg" - } - ] - }, - "dosageInstruction": [ - { - "text": "10mg OD" - } - ] - } - }, - { - "resource": { - "resourceType": "MedicationRequest", - "id": "med-p40-a", - "status": "active", - "intent": "order", - "subject": { - "reference": "Patient/p40" - }, - "medicationCodeableConcept": { - "coding": [ - { - "display": "Tenofovir/Lamivudine/Dolutegravir 300/300/50mg" - } - ] - }, - "dosageInstruction": [ - { - "text": "1 tablet OD" - } - ] - } - }, - { - "resource": { - "resourceType": "MedicationRequest", - "id": "med-p40-b", - "status": "active", - "intent": "order", - "subject": { - "reference": "Patient/p40" - }, - "medicationCodeableConcept": { - "coding": [ - { - "display": "Cotrimoxazole 960mg" - } - ] - }, - "dosageInstruction": [ - { - "text": "960mg OD" - } - ] - } - }, - { - "resource": { - "resourceType": "MedicationRequest", - "id": "med-p41-a", - "status": "active", - "intent": "order", - "subject": { - "reference": "Patient/p41" - }, - "medicationCodeableConcept": { - "coding": [ - { - "display": "Amlodipine 5mg" - } - ] - }, - "dosageInstruction": [ - { - "text": "5mg OD" - } - ] - } - }, - { - "resource": { - "resourceType": "MedicationRequest", - "id": "med-p43-a", - "status": "active", - "intent": "order", - "subject": { - "reference": "Patient/p43" - }, - "medicationCodeableConcept": { - "coding": [ - { - "display": "Metformin 1g" - } - ] - }, - "dosageInstruction": [ - { - "text": "1g BD" - } - ] - } - }, - { - "resource": { - "resourceType": "MedicationRequest", - "id": "med-p43-b", - "status": "active", - "intent": "order", - "subject": { - "reference": "Patient/p43" - }, - "medicationCodeableConcept": { - "coding": [ - { - "display": "Insulin NPH" - } - ] - }, - "dosageInstruction": [ - { - "text": "20 units BD" - } - ] - } - }, - { - "resource": { - "resourceType": "MedicationRequest", - "id": "med-p43-c", - "status": "active", - "intent": "order", - "subject": { - "reference": "Patient/p43" - }, - "medicationCodeableConcept": { - "coding": [ - { - "display": "Lisinopril 10mg" - } - ] - }, - "dosageInstruction": [ - { - "text": "10mg OD" - } - ] - } - }, - { - "resource": { - "resourceType": "MedicationRequest", - "id": "med-p46-a", - "status": "active", - "intent": "order", - "subject": { - "reference": "Patient/p46" - }, - "medicationCodeableConcept": { - "coding": [ - { - "display": "Folic acid 5mg" - } - ] - }, - "dosageInstruction": [ - { - "text": "5mg OD" - } - ] - } - }, - { - "resource": { - "resourceType": "MedicationRequest", - "id": "med-p46-b", - "status": "active", - "intent": "order", - "subject": { - "reference": "Patient/p46" - }, - "medicationCodeableConcept": { - "coding": [ - { - "display": "Hydroxyurea 500mg" - } - ] - }, - "dosageInstruction": [ - { - "text": "500mg OD" - } - ] - } - }, - { - "resource": { - "resourceType": "MedicationRequest", - "id": "med-p47-a", - "status": "active", - "intent": "order", - "subject": { - "reference": "Patient/p47" - }, - "medicationCodeableConcept": { - "coding": [ - { - "display": "Metformin 500mg" - } - ] - }, - "dosageInstruction": [ - { - "text": "500mg BD" - } - ] - } - }, - { - "resource": { - "resourceType": "MedicationRequest", - "id": "med-p47-b", - "status": "active", - "intent": "order", - "subject": { - "reference": "Patient/p47" - }, - "medicationCodeableConcept": { - "coding": [ - { - "display": "Amlodipine 5mg" - } - ] - }, - "dosageInstruction": [ - { - "text": "5mg OD" - } - ] - } - }, - { - "resource": { - "resourceType": "Condition", - "id": "cond-p1-a", - "subject": { - "reference": "Patient/p1" - }, - "code": { - "coding": [ - { - "display": "Asthma" - } - ] - }, - "clinicalStatus": { - "coding": [ - { - "system": "http://terminology.hl7.org/CodeSystem/condition-clinical", - "code": "active" - } - ] - }, - "onsetDateTime": "2015-06-01" - } - }, - { - "resource": { - "resourceType": "Condition", - "id": "cond-p1-b", - "subject": { - "reference": "Patient/p1" - }, - "code": { - "coding": [ - { - "display": "Allergic rhinitis" - } - ] - }, - "clinicalStatus": { - "coding": [ - { - "system": "http://terminology.hl7.org/CodeSystem/condition-clinical", - "code": "active" - } - ] - }, - "onsetDateTime": "2016-03-20" - } - }, - { - "resource": { - "resourceType": "Condition", - "id": "cond-p2-a", - "subject": { - "reference": "Patient/p2" - }, - "code": { - "coding": [ - { - "display": "Type 2 diabetes mellitus" - } - ] - }, - "clinicalStatus": { - "coding": [ - { - "system": "http://terminology.hl7.org/CodeSystem/condition-clinical", - "code": "active" - } - ] - }, - "onsetDateTime": "2018-01-15" - } - }, - { - "resource": { - "resourceType": "Condition", - "id": "cond-p2-b", - "subject": { - "reference": "Patient/p2" - }, - "code": { - "coding": [ - { - "display": "Essential hypertension" - } - ] - }, - "clinicalStatus": { - "coding": [ - { - "system": "http://terminology.hl7.org/CodeSystem/condition-clinical", - "code": "active" - } - ] - }, - "onsetDateTime": "2019-07-22" - } - }, - { - "resource": { - "resourceType": "Condition", - "id": "cond-p3-a", - "subject": { - "reference": "Patient/p3" - }, - "code": { - "coding": [ - { - "display": "Atopic dermatitis" - } - ] - }, - "clinicalStatus": { - "coding": [ - { - "system": "http://terminology.hl7.org/CodeSystem/condition-clinical", - "code": "active" - } - ] - }, - "onsetDateTime": "2020-02-10" - } - }, - { - "resource": { - "resourceType": "Condition", - "id": "cond-p3-b", - "subject": { - "reference": "Patient/p3" - }, - "code": { - "coding": [ - { - "display": "Food allergy" - } - ] - }, - "clinicalStatus": { - "coding": [ - { - "system": "http://terminology.hl7.org/CodeSystem/condition-clinical", - "code": "active" - } - ] - }, - "onsetDateTime": "2019-11-05" - } - }, - { - "resource": { - "resourceType": "Condition", - "id": "cond-p6-a", - "subject": { - "reference": "Patient/p6" - }, - "code": { - "coding": [ - { - "display": "HIV disease" - } - ] - }, - "clinicalStatus": { - "coding": [ - { - "system": "http://terminology.hl7.org/CodeSystem/condition-clinical", - "code": "active" - } - ] - }, - "onsetDateTime": "2010-01-15" - } - }, - { - "resource": { - "resourceType": "Condition", - "id": "cond-p6-b", - "subject": { - "reference": "Patient/p6" - }, - "code": { - "coding": [ - { - "display": "Essential hypertension" - } - ] - }, - "clinicalStatus": { - "coding": [ - { - "system": "http://terminology.hl7.org/CodeSystem/condition-clinical", - "code": "active" - } - ] - }, - "onsetDateTime": "2015-03-10" - } - }, - { - "resource": { - "resourceType": "Condition", - "id": "cond-p9-a", - "subject": { - "reference": "Patient/p9" - }, - "code": { - "coding": [ - { - "display": "Type 2 diabetes mellitus" - } - ] - }, - "clinicalStatus": { - "coding": [ - { - "system": "http://terminology.hl7.org/CodeSystem/condition-clinical", - "code": "active" - } - ] - }, - "onsetDateTime": "2012-05-20" - } - }, - { - "resource": { - "resourceType": "Condition", - "id": "cond-p9-b", - "subject": { - "reference": "Patient/p9" - }, - "code": { - "coding": [ - { - "display": "Asthma" - } - ] - }, - "clinicalStatus": { - "coding": [ - { - "system": "http://terminology.hl7.org/CodeSystem/condition-clinical", - "code": "active" - } - ] - }, - "onsetDateTime": "2003-08-14" - } - }, - { - "resource": { - "resourceType": "Condition", - "id": "cond-p11-a", - "subject": { - "reference": "Patient/p11" - }, - "code": { - "coding": [ - { - "display": "Essential hypertension" - } - ] - }, - "clinicalStatus": { - "coding": [ - { - "system": "http://terminology.hl7.org/CodeSystem/condition-clinical", - "code": "active" - } - ] - }, - "onsetDateTime": "2008-11-03" - } - }, - { - "resource": { - "resourceType": "Condition", - "id": "cond-p12-a", - "subject": { - "reference": "Patient/p12" - }, - "code": { - "coding": [ - { - "display": "Iron deficiency anaemia" - } - ] - }, - "clinicalStatus": { - "coding": [ - { - "system": "http://terminology.hl7.org/CodeSystem/condition-clinical", - "code": "active" - } - ] - }, - "onsetDateTime": "2020-06-18" - } - }, - { - "resource": { - "resourceType": "Condition", - "id": "cond-p17-a", - "subject": { - "reference": "Patient/p17" - }, - "code": { - "coding": [ - { - "display": "Sickle cell disease" - } - ] - }, - "clinicalStatus": { - "coding": [ - { - "system": "http://terminology.hl7.org/CodeSystem/condition-clinical", - "code": "active" - } - ] - }, - "onsetDateTime": "2000-01-01" - } - }, - { - "resource": { - "resourceType": "Condition", - "id": "cond-p17-b", - "subject": { - "reference": "Patient/p17" - }, - "code": { - "coding": [ - { - "display": "Malaria" - } - ] - }, - "clinicalStatus": { - "coding": [ - { - "system": "http://terminology.hl7.org/CodeSystem/condition-clinical", - "code": "active" - } - ] - }, - "onsetDateTime": "2024-11-05" - } - }, - { - "resource": { - "resourceType": "Condition", - "id": "cond-p19-a", - "subject": { - "reference": "Patient/p19" - }, - "code": { - "coding": [ - { - "display": "Essential hypertension" - } - ] - }, - "clinicalStatus": { - "coding": [ - { - "system": "http://terminology.hl7.org/CodeSystem/condition-clinical", - "code": "active" - } - ] - }, - "onsetDateTime": "2005-07-22" - } - }, - { - "resource": { - "resourceType": "Condition", - "id": "cond-p20-a", - "subject": { - "reference": "Patient/p20" - }, - "code": { - "coding": [ - { - "display": "Type 2 diabetes mellitus" - } - ] - }, - "clinicalStatus": { - "coding": [ - { - "system": "http://terminology.hl7.org/CodeSystem/condition-clinical", - "code": "active" - } - ] - }, - "onsetDateTime": "2014-09-11" - } - }, - { - "resource": { - "resourceType": "Condition", - "id": "cond-p22-a", - "subject": { - "reference": "Patient/p22" - }, - "code": { - "coding": [ - { - "display": "Essential hypertension" - } - ] - }, - "clinicalStatus": { - "coding": [ - { - "system": "http://terminology.hl7.org/CodeSystem/condition-clinical", - "code": "active" - } - ] - }, - "onsetDateTime": "2018-03-07" - } - }, - { - "resource": { - "resourceType": "Condition", - "id": "cond-p23-a", - "subject": { - "reference": "Patient/p23" - }, - "code": { - "coding": [ - { - "display": "Asthma" - } - ] - }, - "clinicalStatus": { - "coding": [ - { - "system": "http://terminology.hl7.org/CodeSystem/condition-clinical", - "code": "active" - } - ] - }, - "onsetDateTime": "2010-06-14" - } - }, - { - "resource": { - "resourceType": "Condition", - "id": "cond-p24-a", - "subject": { - "reference": "Patient/p24" - }, - "code": { - "coding": [ - { - "display": "HIV disease" - } - ] - }, - "clinicalStatus": { - "coding": [ - { - "system": "http://terminology.hl7.org/CodeSystem/condition-clinical", - "code": "active" - } - ] - }, - "onsetDateTime": "2016-04-20" - } - }, - { - "resource": { - "resourceType": "Condition", - "id": "cond-p26-a", - "subject": { - "reference": "Patient/p26" - }, - "code": { - "coding": [ - { - "display": "Essential hypertension" - } - ] - }, - "clinicalStatus": { - "coding": [ - { - "system": "http://terminology.hl7.org/CodeSystem/condition-clinical", - "code": "active" - } - ] - }, - "onsetDateTime": "1995-02-14" - } - }, - { - "resource": { - "resourceType": "Condition", - "id": "cond-p26-b", - "subject": { - "reference": "Patient/p26" - }, - "code": { - "coding": [ - { - "display": "Heart failure" - } - ] - }, - "clinicalStatus": { - "coding": [ - { - "system": "http://terminology.hl7.org/CodeSystem/condition-clinical", - "code": "active" - } - ] - }, - "onsetDateTime": "2020-08-30" - } - }, - { - "resource": { - "resourceType": "Condition", - "id": "cond-p27-a", - "subject": { - "reference": "Patient/p27" - }, - "code": { - "coding": [ - { - "display": "Type 2 diabetes mellitus" - } - ] - }, - "clinicalStatus": { - "coding": [ - { - "system": "http://terminology.hl7.org/CodeSystem/condition-clinical", - "code": "active" - } - ] - }, - "onsetDateTime": "2005-11-22" - } - }, - { - "resource": { - "resourceType": "Condition", - "id": "cond-p29-a", - "subject": { - "reference": "Patient/p29" - }, - "code": { - "coding": [ - { - "display": "Malaria" - } - ] - }, - "clinicalStatus": { - "coding": [ - { - "system": "http://terminology.hl7.org/CodeSystem/condition-clinical", - "code": "active" - } - ] - }, - "onsetDateTime": "2024-10-18" - } - }, - { - "resource": { - "resourceType": "Condition", - "id": "cond-p31-a", - "subject": { - "reference": "Patient/p31" - }, - "code": { - "coding": [ - { - "display": "Essential hypertension" - } - ] - }, - "clinicalStatus": { - "coding": [ - { - "system": "http://terminology.hl7.org/CodeSystem/condition-clinical", - "code": "active" - } - ] - }, - "onsetDateTime": "2000-06-05" - } - }, - { - "resource": { - "resourceType": "Condition", - "id": "cond-p31-b", - "subject": { - "reference": "Patient/p31" - }, - "code": { - "coding": [ - { - "display": "Type 2 diabetes mellitus" - } - ] - }, - "clinicalStatus": { - "coding": [ - { - "system": "http://terminology.hl7.org/CodeSystem/condition-clinical", - "code": "active" - } - ] - }, - "onsetDateTime": "2008-03-17" - } - }, - { - "resource": { - "resourceType": "Condition", - "id": "cond-p32-a", - "subject": { - "reference": "Patient/p32" - }, - "code": { - "coding": [ - { - "display": "Hypothyroidism" - } - ] - }, - "clinicalStatus": { - "coding": [ - { - "system": "http://terminology.hl7.org/CodeSystem/condition-clinical", - "code": "active" - } - ] - }, - "onsetDateTime": "2015-09-08" - } - }, - { - "resource": { - "resourceType": "Condition", - "id": "cond-p33-a", - "subject": { - "reference": "Patient/p33" - }, - "code": { - "coding": [ - { - "display": "Asthma" - } - ] - }, - "clinicalStatus": { - "coding": [ - { - "system": "http://terminology.hl7.org/CodeSystem/condition-clinical", - "code": "active" - } - ] - }, - "onsetDateTime": "2005-04-22" - } - }, - { - "resource": { - "resourceType": "Condition", - "id": "cond-p36-a", - "subject": { - "reference": "Patient/p36" - }, - "code": { - "coding": [ - { - "display": "Malaria" - } - ] - }, - "clinicalStatus": { - "coding": [ - { - "system": "http://terminology.hl7.org/CodeSystem/condition-clinical", - "code": "active" - } - ] - }, - "onsetDateTime": "2024-12-01" - } - }, - { - "resource": { - "resourceType": "Condition", - "id": "cond-p36-b", - "subject": { - "reference": "Patient/p36" - }, - "code": { - "coding": [ - { - "display": "Pulmonary tuberculosis" - } - ] - }, - "clinicalStatus": { - "coding": [ - { - "system": "http://terminology.hl7.org/CodeSystem/condition-clinical", - "code": "active" - } - ] - }, - "onsetDateTime": "2023-06-15" - } - }, - { - "resource": { - "resourceType": "Condition", - "id": "cond-p37-a", - "subject": { - "reference": "Patient/p37" - }, - "code": { - "coding": [ - { - "display": "Iron deficiency anaemia" - } - ] - }, - "clinicalStatus": { - "coding": [ - { - "system": "http://terminology.hl7.org/CodeSystem/condition-clinical", - "code": "active" - } - ] - }, - "onsetDateTime": "2022-07-04" - } - }, - { - "resource": { - "resourceType": "Condition", - "id": "cond-p39-a", - "subject": { - "reference": "Patient/p39" - }, - "code": { - "coding": [ - { - "display": "Essential hypertension" - } - ] - }, - "clinicalStatus": { - "coding": [ - { - "system": "http://terminology.hl7.org/CodeSystem/condition-clinical", - "code": "active" - } - ] - }, - "onsetDateTime": "2021-04-16" - } - }, - { - "resource": { - "resourceType": "Condition", - "id": "cond-p40-a", - "subject": { - "reference": "Patient/p40" - }, - "code": { - "coding": [ - { - "display": "HIV disease" - } - ] - }, - "clinicalStatus": { - "coding": [ - { - "system": "http://terminology.hl7.org/CodeSystem/condition-clinical", - "code": "active" - } - ] - }, - "onsetDateTime": "2012-07-19" - } - }, - { - "resource": { - "resourceType": "Condition", - "id": "cond-p41-a", - "subject": { - "reference": "Patient/p41" - }, - "code": { - "coding": [ - { - "display": "Essential hypertension" - } - ] - }, - "clinicalStatus": { - "coding": [ - { - "system": "http://terminology.hl7.org/CodeSystem/condition-clinical", - "code": "active" - } - ] - }, - "onsetDateTime": "2019-11-25" - } - }, - { - "resource": { - "resourceType": "Condition", - "id": "cond-p43-a", - "subject": { - "reference": "Patient/p43" - }, - "code": { - "coding": [ - { - "display": "Type 2 diabetes mellitus" - } - ] - }, - "clinicalStatus": { - "coding": [ - { - "system": "http://terminology.hl7.org/CodeSystem/condition-clinical", - "code": "active" - } - ] - }, - "onsetDateTime": "2003-08-01" - } - }, - { - "resource": { - "resourceType": "Condition", - "id": "cond-p43-b", - "subject": { - "reference": "Patient/p43" - }, - "code": { - "coding": [ - { - "display": "Essential hypertension" - } - ] - }, - "clinicalStatus": { - "coding": [ - { - "system": "http://terminology.hl7.org/CodeSystem/condition-clinical", - "code": "active" - } - ] - }, - "onsetDateTime": "2010-02-14" - } - }, - { - "resource": { - "resourceType": "Condition", - "id": "cond-p46-a", - "subject": { - "reference": "Patient/p46" - }, - "code": { - "coding": [ - { - "display": "Sickle cell disease" - } - ] - }, - "clinicalStatus": { - "coding": [ - { - "system": "http://terminology.hl7.org/CodeSystem/condition-clinical", - "code": "active" - } - ] - }, - "onsetDateTime": "2000-01-01" - } - }, - { - "resource": { - "resourceType": "Condition", - "id": "cond-p46-b", - "subject": { - "reference": "Patient/p46" - }, - "code": { - "coding": [ - { - "display": "Malaria" - } - ] - }, - "clinicalStatus": { - "coding": [ - { - "system": "http://terminology.hl7.org/CodeSystem/condition-clinical", - "code": "active" - } - ] - }, - "onsetDateTime": "2024-09-12" - } - }, - { - "resource": { - "resourceType": "Condition", - "id": "cond-p47-a", - "subject": { - "reference": "Patient/p47" - }, - "code": { - "coding": [ - { - "display": "Type 2 diabetes mellitus" - } - ] - }, - "clinicalStatus": { - "coding": [ - { - "system": "http://terminology.hl7.org/CodeSystem/condition-clinical", - "code": "active" - } - ] - }, - "onsetDateTime": "2005-09-20" - } - }, - { - "resource": { - "resourceType": "Condition", - "id": "cond-p47-b", - "subject": { - "reference": "Patient/p47" - }, - "code": { - "coding": [ - { - "display": "Essential hypertension" - } - ] - }, - "clinicalStatus": { - "coding": [ - { - "system": "http://terminology.hl7.org/CodeSystem/condition-clinical", - "code": "active" - } - ] - }, - "onsetDateTime": "2010-03-08" - } - }, - { - "resource": { - "resourceType": "Immunization", - "id": "imm-p1-a", - "status": "completed", - "vaccineCode": { - "coding": [ - { - "display": "BCG vaccine" - } - ] - }, - "patient": { - "reference": "Patient/p1" - }, - "occurrenceDateTime": "1990-03-14" - } - }, - { - "resource": { - "resourceType": "Immunization", - "id": "imm-p1-b", - "status": "completed", - "vaccineCode": { - "coding": [ - { - "display": "COVID-19 mRNA vaccine" - } - ] - }, - "patient": { - "reference": "Patient/p1" - }, - "occurrenceDateTime": "2021-09-15" - } - }, - { - "resource": { - "resourceType": "Immunization", - "id": "imm-p2-a", - "status": "completed", - "vaccineCode": { - "coding": [ - { - "display": "Hepatitis B vaccine" - } - ] - }, - "patient": { - "reference": "Patient/p2" - }, - "occurrenceDateTime": "1985-11-02" - } - }, - { - "resource": { - "resourceType": "Immunization", - "id": "imm-p2-b", - "status": "completed", - "vaccineCode": { - "coding": [ - { - "display": "Yellow fever vaccine" - } - ] - }, - "patient": { - "reference": "Patient/p2" - }, - "occurrenceDateTime": "2010-04-12" - } - }, - { - "resource": { - "resourceType": "Immunization", - "id": "imm-p3-a", - "status": "completed", - "vaccineCode": { - "coding": [ - { - "display": "MMR vaccine" - } - ] - }, - "patient": { - "reference": "Patient/p3" - }, - "occurrenceDateTime": "2001-07-19" - } - }, - { - "resource": { - "resourceType": "Immunization", - "id": "imm-p3-b", - "status": "completed", - "vaccineCode": { - "coding": [ - { - "display": "HPV vaccine" - } - ] - }, - "patient": { - "reference": "Patient/p3" - }, - "occurrenceDateTime": "2015-08-30" - } - }, - { - "resource": { - "resourceType": "Immunization", - "id": "imm-p6-a", - "status": "completed", - "vaccineCode": { - "coding": [ - { - "display": "BCG vaccine" - } - ] - }, - "patient": { - "reference": "Patient/p6" - }, - "occurrenceDateTime": "1978-05-14" - } - }, - { - "resource": { - "resourceType": "Immunization", - "id": "imm-p6-b", - "status": "completed", - "vaccineCode": { - "coding": [ - { - "display": "Hepatitis B vaccine" - } - ] - }, - "patient": { - "reference": "Patient/p6" - }, - "occurrenceDateTime": "2010-01-15" - } - }, - { - "resource": { - "resourceType": "Immunization", - "id": "imm-p16-a", - "status": "completed", - "vaccineCode": { - "coding": [ - { - "display": "COVID-19 mRNA vaccine" - } - ] - }, - "patient": { - "reference": "Patient/p16" - }, - "occurrenceDateTime": "2021-08-20" - } - }, - { - "resource": { - "resourceType": "Immunization", - "id": "imm-p18-a", - "status": "completed", - "vaccineCode": { - "coding": [ - { - "display": "Rotavirus vaccine" - } - ] - }, - "patient": { - "reference": "Patient/p18" - }, - "occurrenceDateTime": "2019-02-01" - } - }, - { - "resource": { - "resourceType": "Immunization", - "id": "imm-p18-b", - "status": "completed", - "vaccineCode": { - "coding": [ - { - "display": "Oral polio vaccine" - } - ] - }, - "patient": { - "reference": "Patient/p18" - }, - "occurrenceDateTime": "2019-03-15" - } - }, - { - "resource": { - "resourceType": "Immunization", - "id": "imm-p19-a", - "status": "completed", - "vaccineCode": { - "coding": [ - { - "display": "BCG vaccine" - } - ] - }, - "patient": { - "reference": "Patient/p19" - }, - "occurrenceDateTime": "1955-01-17" - } - }, - { - "resource": { - "resourceType": "Immunization", - "id": "imm-p24-a", - "status": "completed", - "vaccineCode": { - "coding": [ - { - "display": "BCG vaccine" - } - ] - }, - "patient": { - "reference": "Patient/p24" - }, - "occurrenceDateTime": "1988-07-16" - } - }, - { - "resource": { - "resourceType": "Immunization", - "id": "imm-p28-a", - "status": "completed", - "vaccineCode": { - "coding": [ - { - "display": "HPV vaccine" - } - ] - }, - "patient": { - "reference": "Patient/p28" - }, - "occurrenceDateTime": "2013-04-22" - } - }, - { - "resource": { - "resourceType": "Immunization", - "id": "imm-p30-a", - "status": "completed", - "vaccineCode": { - "coding": [ - { - "display": "BCG vaccine" - } - ] - }, - "patient": { - "reference": "Patient/p30" - }, - "occurrenceDateTime": "2022-03-15" - } - }, - { - "resource": { - "resourceType": "Immunization", - "id": "imm-p30-b", - "status": "completed", - "vaccineCode": { - "coding": [ - { - "display": "Rotavirus vaccine" - } - ] - }, - "patient": { - "reference": "Patient/p30" - }, - "occurrenceDateTime": "2022-04-15" - } - }, - { - "resource": { - "resourceType": "Immunization", - "id": "imm-p33-a", - "status": "completed", - "vaccineCode": { - "coding": [ - { - "display": "COVID-19 mRNA vaccine" - } - ] - }, - "patient": { - "reference": "Patient/p33" - }, - "occurrenceDateTime": "2021-07-10" - } - }, - { - "resource": { - "resourceType": "Immunization", - "id": "imm-p35-a", - "status": "completed", - "vaccineCode": { - "coding": [ - { - "display": "MMR vaccine" - } - ] - }, - "patient": { - "reference": "Patient/p35" - }, - "occurrenceDateTime": "2012-07-08" - } - }, - { - "resource": { - "resourceType": "Immunization", - "id": "imm-p36-a", - "status": "completed", - "vaccineCode": { - "coding": [ - { - "display": "BCG vaccine" - } - ] - }, - "patient": { - "reference": "Patient/p36" - }, - "occurrenceDateTime": "1970-12-01" - } - }, - { - "resource": { - "resourceType": "Immunization", - "id": "imm-p40-a", - "status": "completed", - "vaccineCode": { - "coding": [ - { - "display": "Yellow fever vaccine" - } - ] - }, - "patient": { - "reference": "Patient/p40" - }, - "occurrenceDateTime": "2005-06-15" - } - }, - { - "resource": { - "resourceType": "Immunization", - "id": "imm-p47-a", - "status": "completed", - "vaccineCode": { - "coding": [ - { - "display": "BCG vaccine" - } - ] - }, - "patient": { - "reference": "Patient/p47" - }, - "occurrenceDateTime": "1950-07-04" - } - }, - { - "resource": { - "resourceType": "Immunization", - "id": "imm-p47-b", - "status": "completed", - "vaccineCode": { - "coding": [ - { - "display": "Hepatitis B vaccine" - } - ] - }, - "patient": { - "reference": "Patient/p47" - }, - "occurrenceDateTime": "2000-09-01" - } - }, - { - "resource": { - "resourceType": "Immunization", - "id": "imm-p47-c", - "status": "completed", - "vaccineCode": { - "coding": [ - { - "display": "COVID-19 mRNA vaccine" - } - ] - }, - "patient": { - "reference": "Patient/p47" - }, - "occurrenceDateTime": "2021-11-15" - } - } - ] -} diff --git a/ohs-player-reference-app/src/commonMain/kotlin/dev/ohs/player/reference/app/data/datasource/SampleDataStore.kt b/ohs-player-reference-app/src/commonMain/kotlin/dev/ohs/player/reference/app/data/datasource/SampleDataStore.kt index 607a95aa..f7e94564 100644 --- a/ohs-player-reference-app/src/commonMain/kotlin/dev/ohs/player/reference/app/data/datasource/SampleDataStore.kt +++ b/ohs-player-reference-app/src/commonMain/kotlin/dev/ohs/player/reference/app/data/datasource/SampleDataStore.kt @@ -23,14 +23,12 @@ import dev.ohs.fhir.model.r4.MedicationRequest import dev.ohs.fhir.model.r4.Patient import dev.ohs.fhir.model.r4.Resource import dev.ohs.player.library.model.SearchResult -import dev.ohs.player.reference.app.data.AppDependencies import dev.ohs.player.reference.app.data.patientIdFromReference import dev.ohs.player.reference.app.data.repository.FhirRepository /** Returns all patient IDs — used by the patient list screen. */ -suspend fun allPatientIds( - repository: FhirRepository = AppDependencies.fhirRepository -): List = repository.all("Patient").mapNotNull { (it as? Patient)?.id } +suspend fun allPatientIds(repository: FhirRepository): List = + repository.all("Patient").mapNotNull { (it as? Patient)?.id } /** * Patient list: root = Patient only. No clinical resources needed — the list card shows summary @@ -38,7 +36,7 @@ suspend fun allPatientIds( */ suspend fun patientSummarySearchResult( patientId: String, - repository: FhirRepository = AppDependencies.fhirRepository, + repository: FhirRepository, ): SearchResult? { val patient = repository.get("Patient", patientId) as? Patient ?: return null return SearchResult(resource = patient) @@ -50,7 +48,7 @@ suspend fun patientSummarySearchResult( */ suspend fun patientProfileSearchResult( patientId: String, - repository: FhirRepository = AppDependencies.fhirRepository, + repository: FhirRepository, ): SearchResult? { val patient = repository.get("Patient", patientId) as? Patient ?: return null val revIncluded = buildMap { @@ -90,9 +88,7 @@ suspend fun patientProfileSearchResult( * Group list: root = Group only. Member count is derived from `Group.member.size` on the resource * itself — no additional includes needed. */ -suspend fun groupListSearchResults( - repository: FhirRepository = AppDependencies.fhirRepository -): List> = +suspend fun groupListSearchResults(repository: FhirRepository): List> = repository.all("Group").filterIsInstance().map { group -> SearchResult(resource = group) } /** @@ -103,7 +99,7 @@ suspend fun groupListSearchResults( */ suspend fun groupProfileSearchResult( groupId: String, - repository: FhirRepository = AppDependencies.fhirRepository, + repository: FhirRepository, ): SearchResult? { val group = repository.get("Group", groupId) as? Group ?: return null val memberPatients = diff --git a/ohs-player-reference-app/src/commonMain/kotlin/dev/ohs/player/reference/app/data/di/AppModule.kt b/ohs-player-reference-app/src/commonMain/kotlin/dev/ohs/player/reference/app/data/di/AppModule.kt new file mode 100644 index 00000000..bf47ce14 --- /dev/null +++ b/ohs-player-reference-app/src/commonMain/kotlin/dev/ohs/player/reference/app/data/di/AppModule.kt @@ -0,0 +1,60 @@ +/* + * Copyright 2026 Open Health Stack Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package dev.ohs.player.reference.app.data.di + +import dev.ohs.player.reference.app.data.repository.FhirEngineRepository +import dev.ohs.player.reference.app.data.repository.FhirRepository +import dev.ohs.player.reference.app.data.repository.GroupRepository +import dev.ohs.player.reference.app.data.repository.PatientRepository +import dev.ohs.player.reference.app.feature.group.list.GroupListViewModel +import dev.ohs.player.reference.app.feature.group.profile.GroupProfileViewModel +import dev.ohs.player.reference.app.feature.patient.list.PatientListViewModel +import dev.ohs.player.reference.app.feature.patient.profile.PatientProfileViewModel +import dev.ohs.player.reference.app.feature.questionnaire.QuestionnaireHostViewModel +import dev.ohs.player.reference.app.feature.questionnaire.QuestionnaireLaunchContext +import dev.ohs.player.reference.app.feature.questionnaire.QuestionnaireService +import org.koin.core.module.dsl.viewModel +import org.koin.dsl.module + +/** + * Binds the production [FhirRepository], backed by [FhirEngineRepository]. Requires a + * [dev.ohs.fhir.FhirEngine] binding, supplied by each platform's module (see `initKoin` callers). + */ +internal val fhirEngineRepositoryModule = module { + single { FhirEngineRepository(get()) } +} + +/** + * Repositories that only depend on [FhirRepository] — kept separate from + * [fhirEngineRepositoryModule] so tests can swap in a fake [FhirRepository] without redeclaring + * these bindings. + */ +internal val repositoryModule = module { + single { PatientRepository(get()) } + single { GroupRepository(get()) } +} + +internal val serviceModule = module { factory { QuestionnaireService(get()) } } + +internal val viewModelModule = module { + viewModel { PatientListViewModel(get()) } + viewModel { (patientId: String) -> PatientProfileViewModel(patientId, get()) } + viewModel { GroupListViewModel(get()) } + viewModel { (groupId: String) -> GroupProfileViewModel(groupId, get()) } + viewModel { (questionnaireId: String, launchContext: QuestionnaireLaunchContext) -> + QuestionnaireHostViewModel(questionnaireId, launchContext, get()) + } +} diff --git a/ohs-player-reference-app/src/commonMain/kotlin/dev/ohs/player/reference/app/data/AppDependencies.kt b/ohs-player-reference-app/src/commonMain/kotlin/dev/ohs/player/reference/app/data/di/InitKoin.kt similarity index 55% rename from ohs-player-reference-app/src/commonMain/kotlin/dev/ohs/player/reference/app/data/AppDependencies.kt rename to ohs-player-reference-app/src/commonMain/kotlin/dev/ohs/player/reference/app/data/di/InitKoin.kt index 0dbf08c7..7f4aef16 100644 --- a/ohs-player-reference-app/src/commonMain/kotlin/dev/ohs/player/reference/app/data/AppDependencies.kt +++ b/ohs-player-reference-app/src/commonMain/kotlin/dev/ohs/player/reference/app/data/di/InitKoin.kt @@ -13,15 +13,23 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package dev.ohs.player.reference.app.data +package dev.ohs.player.reference.app.data.di -import dev.ohs.player.reference.app.data.repository.FhirRepository -import dev.ohs.player.reference.app.data.repository.InMemoryFhirRepository +import org.koin.core.context.startKoin +import org.koin.core.module.Module /** - * TODO: Replace this service locator with a proper dependency injection setup, such as Koin. This - * should go away once dependencies are wired explicitly through a DI framework. + * Starts the single global Koin instance for the app. [platformModule] supplies the + * platform-specific `FhirEngine` binding that [fhirEngineRepositoryModule] depends on. */ -object AppDependencies { - var fhirRepository: FhirRepository = InMemoryFhirRepository() +fun initKoin(platformModule: Module) { + startKoin { + modules( + platformModule, + fhirEngineRepositoryModule, + repositoryModule, + serviceModule, + viewModelModule, + ) + } } diff --git a/ohs-player-reference-app/src/commonMain/kotlin/dev/ohs/player/reference/app/data/repository/FhirEngineRepository.kt b/ohs-player-reference-app/src/commonMain/kotlin/dev/ohs/player/reference/app/data/repository/FhirEngineRepository.kt new file mode 100644 index 00000000..60ea2820 --- /dev/null +++ b/ohs-player-reference-app/src/commonMain/kotlin/dev/ohs/player/reference/app/data/repository/FhirEngineRepository.kt @@ -0,0 +1,155 @@ +/* + * Copyright 2026 Open Health Stack Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package dev.ohs.player.reference.app.data.repository + +import dev.ohs.fhir.FhirEngine +import dev.ohs.fhir.db.ResourceNotFoundException +import dev.ohs.fhir.model.r4.Bundle +import dev.ohs.fhir.model.r4.Resource +import dev.ohs.fhir.model.r4.terminologies.ResourceType +import dev.ohs.fhir.resourceType +import dev.ohs.fhir.search.Search +import dev.ohs.player.reference.app.generateId +import dev.ohs.player.reference.app.util.FhirJson +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.jsonObject + +/** + * [FhirRepository] backed by a real on-disk database via [FhirEngine]. + * + * Bundle submissions resolve entry ids and rewrite internal references — see + * [normalizeBundleResources] (added in a later change). + */ +class FhirEngineRepository(private val fhirEngine: FhirEngine) : FhirRepository { + + private val json = FhirJson.instance + private val _revision = MutableStateFlow(0L) + + override val revision: StateFlow = _revision + + override suspend fun upsert(resource: Resource) { + upsertResource(resource) + _revision.value += 1 + } + + override suspend fun upsert(bundle: Bundle): Int { + val normalized = normalizeBundleResources(bundle) + if (normalized.isEmpty()) return 0 + fhirEngine.withTransaction { normalized.forEach { upsertResource(it) } } + _revision.value += 1 + return normalized.size + } + + override suspend fun get(resourceType: String, id: String): Resource? { + return runCatching { fhirEngine.get(ResourceType.valueOf(resourceType), id) } + .getOrElse { if (it is ResourceNotFoundException) null else throw it } + } + + override suspend fun all(resourceType: String): List { + return fhirEngine.search(Search(ResourceType.valueOf(resourceType))).map { + it.resource + } + } + + private suspend fun upsertResource(resource: Resource) { + val withId = if (resource.id == null) resource.withId(generateId()) else resource + val type = ResourceType.valueOf(withId.resourceType) + val exists = runCatching { fhirEngine.get(type, withId.id!!) }.isSuccess + if (exists) fhirEngine.update(withId) else fhirEngine.create(withId) + } + + private fun normalizeBundleResources(bundle: Bundle): List { + val drafts = + bundle.entry.mapNotNull { entry -> + val resource = entry.resource ?: return@mapNotNull null + val resolvedId = + resource.id + ?: idFromFullUrl(entry.fullUrl?.value) + ?: idFromRequestUrl(entry.request?.url?.value, resource.resourceType) + ?: generateId() + entry.fullUrl?.value to resource.withId(resolvedId) + } + + val referenceMap = + drafts + .mapNotNull { (fullUrl, resource) -> + fullUrl?.let { it to "${resource.resourceType}/${resource.id}" } + } + .toMap() + + return drafts.map { (_, resource) -> rewriteReferences(resource, referenceMap) } + } + + private fun rewriteReferences(resource: Resource, referenceMap: Map): Resource { + val rewritten = + rewriteReferencesInElement( + json.encodeToJsonElement(Resource.serializer(), resource), + referenceMap, + ) + return json.decodeFromJsonElement(Resource.serializer(), rewritten) + } + + private fun rewriteReferencesInElement( + element: JsonElement, + referenceMap: Map, + ): JsonElement = + when (element) { + is JsonObject -> + JsonObject( + element.mapValues { (key, value) -> + if (key == "reference" && value is JsonPrimitive) { + referenceMap[value.content]?.let(::JsonPrimitive) ?: value + } else { + rewriteReferencesInElement(value, referenceMap) + } + } + ) + + is JsonArray -> JsonArray(element.map { rewriteReferencesInElement(it, referenceMap) }) + + else -> element + } + + private fun idFromFullUrl(fullUrl: String?): String? = + when { + fullUrl.isNullOrBlank() -> null + fullUrl.startsWith("urn:uuid:") -> fullUrl.substringAfterLast(':').ifBlank { null } + fullUrl.contains('/') -> fullUrl.substringAfterLast('/').substringBefore('?').ifBlank { null } + else -> null + } + + private fun idFromRequestUrl(url: String?, resourceType: String): String? { + if (url.isNullOrBlank()) return null + val candidate = + url.substringAfterLast('/').substringBefore('?').ifBlank { + return null + } + return candidate.takeUnless { it == resourceType } + } + + private fun Resource.withId(newId: String): Resource { + val obj = json.encodeToJsonElement(Resource.serializer(), this).jsonObject + return json.decodeFromJsonElement( + Resource.serializer(), + JsonObject(obj + ("id" to JsonPrimitive(newId))), + ) + } +} diff --git a/ohs-player-reference-app/src/commonMain/kotlin/dev/ohs/player/reference/app/data/repository/FhirRepository.kt b/ohs-player-reference-app/src/commonMain/kotlin/dev/ohs/player/reference/app/data/repository/FhirRepository.kt index c78760d5..6557a11e 100644 --- a/ohs-player-reference-app/src/commonMain/kotlin/dev/ohs/player/reference/app/data/repository/FhirRepository.kt +++ b/ohs-player-reference-app/src/commonMain/kotlin/dev/ohs/player/reference/app/data/repository/FhirRepository.kt @@ -19,14 +19,15 @@ import dev.ohs.fhir.model.r4.Bundle import dev.ohs.fhir.model.r4.Resource import kotlinx.coroutines.flow.StateFlow -/** - * Minimal persistence seam for FHIR resources. - * - * The reference app keeps data in memory today so questionnaire submissions can immediately feed - * the demo UI. When the engine-backed database lands, the implementation behind this interface can - * be swapped without changing the feature layer. - */ +/** Minimal persistence seam for FHIR resources, backed by [FhirEngineRepository] in production. */ interface FhirRepository { + /** + * Incremented on every successful [upsert]. Implementers must bump this after each write so that + * observers (e.g. [PatientRepository], [GroupRepository]) know to re-query. + * + * TODO: To be deleted, once https://github.com/ohs-foundation/kotlin-fhir-engine/issues/65 is + * done + */ val revision: StateFlow suspend fun upsert(resource: Resource) diff --git a/ohs-player-reference-app/src/commonMain/kotlin/dev/ohs/player/reference/app/data/repository/GroupRepository.kt b/ohs-player-reference-app/src/commonMain/kotlin/dev/ohs/player/reference/app/data/repository/GroupRepository.kt index 4120f1f9..3580630f 100644 --- a/ohs-player-reference-app/src/commonMain/kotlin/dev/ohs/player/reference/app/data/repository/GroupRepository.kt +++ b/ohs-player-reference-app/src/commonMain/kotlin/dev/ohs/player/reference/app/data/repository/GroupRepository.kt @@ -18,7 +18,6 @@ package dev.ohs.player.reference.app.data.repository import dev.ohs.player.generated.state.GroupHeaderState import dev.ohs.player.generated.state.GroupListState import dev.ohs.player.generated.state.GroupMemberState -import dev.ohs.player.reference.app.data.AppDependencies import dev.ohs.player.reference.app.data.Extraction.extractor import dev.ohs.player.reference.app.data.datasource.groupListSearchResults import dev.ohs.player.reference.app.data.datasource.groupProfileSearchResult @@ -28,52 +27,51 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map import kotlinx.coroutines.withContext -object GroupRepository { +class GroupRepository(private val fhirRepository: FhirRepository) { // FhirPathEvaluator is not concurrent-safe. limitedParallelism(1) serializes all extraction on a // single background thread without any explicit locking. private val extractorDispatcher = Dispatchers.Default.limitedParallelism(1) - fun observeGroups(): Flow> = - AppDependencies.fhirRepository.revision.map { getGroups() } + fun observeGroups(): Flow> = fhirRepository.revision.map { getGroups() } suspend fun getGroups(): List = withContext(extractorDispatcher) { - groupListSearchResults().flatMap { extractor.extract(it) } + groupListSearchResults(fhirRepository).flatMap { extractor.extract(it) } } fun observeGroupProfile(groupId: String): Flow = - AppDependencies.fhirRepository.revision.map { getGroupProfile(groupId) } + fhirRepository.revision.map { getGroupProfile(groupId) } suspend fun getGroupProfile(groupId: String): GroupProfileUiState = withContext(extractorDispatcher) { - val result = groupProfileSearchResult(groupId) ?: return@withContext GroupProfileUiState() + val result = + groupProfileSearchResult(groupId, fhirRepository) + ?: return@withContext GroupProfileUiState() buildGroupProfileUiState( groupHeader = extractor.extract(result).firstOrNull(), extractedMembers = extractor.extract(result), ) } +} - internal fun buildGroupProfileUiState( - groupHeader: GroupHeaderState?, - extractedMembers: List, - ): GroupProfileUiState { - if (extractedMembers.isEmpty()) { - return GroupProfileUiState(groupHeader = groupHeader) - } +internal fun buildGroupProfileUiState( + groupHeader: GroupHeaderState?, + extractedMembers: List, +): GroupProfileUiState { + if (extractedMembers.isEmpty()) { + return GroupProfileUiState(groupHeader = groupHeader) + } - val headIndex = extractedMembers.indexOfFirst { it.relationshipCode.isNullOrBlank() } - val resolvedHeadIndex = if (headIndex >= 0) headIndex else 0 - val head = extractedMembers[resolvedHeadIndex] + val headIndex = extractedMembers.indexOfFirst { it.relationshipCode.isNullOrBlank() } + val resolvedHeadIndex = if (headIndex >= 0) headIndex else 0 + val head = extractedMembers[resolvedHeadIndex] - return GroupProfileUiState( - groupHeader = - (groupHeader ?: GroupHeaderState()).copy( - headGivenName = head.memberGivenName, - headFamilyName = head.memberFamilyName, - ), - // Keep every Group.member navigable. In FHIR the head is still a legitimate member entity, - // and hiding that row leaves single-person households with no path into the patient profile. - members = extractedMembers, - ) - } + return GroupProfileUiState( + groupHeader = + (groupHeader ?: GroupHeaderState()).copy( + headGivenName = head.memberGivenName, + headFamilyName = head.memberFamilyName, + ), + members = extractedMembers, + ) } diff --git a/ohs-player-reference-app/src/commonMain/kotlin/dev/ohs/player/reference/app/data/repository/InMemoryFhirRepository.kt b/ohs-player-reference-app/src/commonMain/kotlin/dev/ohs/player/reference/app/data/repository/InMemoryFhirRepository.kt deleted file mode 100644 index 21af5620..00000000 --- a/ohs-player-reference-app/src/commonMain/kotlin/dev/ohs/player/reference/app/data/repository/InMemoryFhirRepository.kt +++ /dev/null @@ -1,353 +0,0 @@ -/* - * Copyright 2026 Open Health Stack Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package dev.ohs.player.reference.app.data.repository - -import dev.ohs.fhir.model.r4.Bundle -import dev.ohs.fhir.model.r4.Group -import dev.ohs.fhir.model.r4.Patient -import dev.ohs.fhir.model.r4.Resource -import dev.ohs.player.reference.app.data.patientIdFromReference -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.sync.withLock -import kotlinx.serialization.builtins.ListSerializer -import kotlinx.serialization.json.Json -import kotlinx.serialization.json.JsonArray -import kotlinx.serialization.json.JsonElement -import kotlinx.serialization.json.JsonObject -import kotlinx.serialization.json.JsonPrimitive -import kotlinx.serialization.json.contentOrNull -import kotlinx.serialization.json.jsonObject -import kotlinx.serialization.json.jsonPrimitive -import ohsplayerreferenceclientapp.ohs_player_reference_app.generated.resources.Res -import org.jetbrains.compose.resources.ExperimentalResourceApi - -/** - * TODO(#58): Temporary workaround until FHIREngine integration is available. Remove this once - * FHIREngine owns persistence and reference handling; that should simplify this implementation. - */ -class InMemoryFhirRepository( - private val seedResourcesLoader: suspend () -> List = ::loadBundledSampleResources, - private val snapshotStore: RepositorySnapshotStore? = null, -) : FhirRepository { - private val mutex = Mutex() - private val resourcesByType = mutableMapOf>() - private val fhirJson = Json { ignoreUnknownKeys = true } - private var initialized = false - private var generatedIdCounter = 0L - private val _revision = MutableStateFlow(0L) - - override val revision: StateFlow = _revision - - override suspend fun upsert(resource: Resource) { - ensureInitialized() - mutex.withLock { - val normalized = normalizeResource(resource) - if (storeResource(normalized) != null) { - repairStoredGroups() - persistSnapshotLocked() - _revision.value += 1 - } - } - } - - override suspend fun upsert(bundle: Bundle): Int { - ensureInitialized() - return mutex.withLock { - val normalizedResources = normalizeBundleResources(bundle) - normalizedResources.forEach(::storeResource) - if (normalizedResources.isNotEmpty()) { - repairStoredGroups() - persistSnapshotLocked() - _revision.value += 1 - } - normalizedResources.size - } - } - - override suspend fun get(resourceType: String, id: String): Resource? { - ensureInitialized() - return mutex.withLock { resourcesByType[resourceType]?.get(id) } - } - - override suspend fun all(resourceType: String): List { - ensureInitialized() - return mutex.withLock { resourcesByType[resourceType]?.values?.toList().orEmpty() } - } - - private suspend fun ensureInitialized() { - if (initialized) return - mutex.withLock { - if (initialized) return - seedResourcesLoader().forEach(::storeResource) - loadPersistedResources().forEach(::storeResource) - if (repairStoredGroups()) { - persistSnapshotLocked() - } - initialized = true - } - } - - private fun storeResource(resource: Resource): Resource? { - val resourceJson = fhirJson.encodeToJsonElement(Resource.serializer(), resource).jsonObject - val resourceType = resourceJson["resourceType"]?.jsonPrimitive?.contentOrNull ?: return null - val id = resourceJson["id"]?.jsonPrimitive?.contentOrNull ?: return null - val bucket = resourcesByType.getOrPut(resourceType) { mutableMapOf() } - bucket[id] = resource - return resource - } - - private fun normalizeResource(resource: Resource): Resource { - val resourceJson = fhirJson.encodeToJsonElement(Resource.serializer(), resource).jsonObject - val resourceType = resourceJson["resourceType"]?.jsonPrimitive?.contentOrNull ?: return resource - val id = resourceJson["id"]?.jsonPrimitive?.contentOrNull ?: generateResourceId(resourceType) - return decodeResource(JsonObject(resourceJson + ("id" to JsonPrimitive(id)))) - } - - private fun normalizeBundleResources(bundle: Bundle): List { - val drafts = - bundle.entry.mapNotNull { entry -> - val resource = entry.resource ?: return@mapNotNull null - val resourceJson = fhirJson.encodeToJsonElement(Resource.serializer(), resource).jsonObject - val resourceType = - resourceJson["resourceType"]?.jsonPrimitive?.contentOrNull ?: return@mapNotNull null - val normalizedId = - resourceJson["id"]?.jsonPrimitive?.contentOrNull - ?: idFromFullUrl(entry.fullUrl?.value) - ?: idFromRequestUrl(entry.request?.url?.value, resourceType) - ?: generateResourceId(resourceType) - ResourceDraft( - resourceType = resourceType, - normalizedId = normalizedId, - fullUrl = entry.fullUrl?.value, - resourceJson = JsonObject(resourceJson + ("id" to JsonPrimitive(normalizedId))), - ) - } - - val referenceMap = - drafts - .mapNotNull { draft -> - draft.fullUrl?.let { fullUrl -> fullUrl to "${draft.resourceType}/${draft.normalizedId}" } - } - .toMap() - - return drafts.map { draft -> - decodeResource(rewriteReferences(draft.resourceJson, referenceMap).jsonObject) - } - } - - private fun decodeResource(resourceJson: JsonObject): Resource = - fhirJson.decodeFromJsonElement(Resource.serializer(), JsonObject(resourceJson)) - - private suspend fun loadPersistedResources(): List { - val snapshot = snapshotStore?.read().orEmpty() - if (snapshot.isBlank()) return emptyList() - return runCatching { - fhirJson.decodeFromString(ListSerializer(Resource.serializer()), snapshot) - } - .getOrElse { emptyList() } - } - - private suspend fun persistSnapshotLocked() { - val snapshotStore = snapshotStore ?: return - val snapshot = - fhirJson.encodeToString( - ListSerializer(Resource.serializer()), - resourcesByType.values - .flatMap { it.values } - .sortedWith(compareBy({ resourceTypeName(it) }, { resourceId(it).orEmpty() })), - ) - snapshotStore.write(snapshot) - } - - private fun rewriteReferences( - element: JsonElement, - referenceMap: Map, - ): JsonElement = - when (element) { - is JsonObject -> - JsonObject( - element.mapValues { (key, value) -> - if (key == "reference" && value is JsonPrimitive) { - referenceMap[value.content]?.let(::JsonPrimitive) ?: value - } else { - rewriteReferences(value, referenceMap) - } - } - ) - - is JsonArray -> JsonArray(element.map { rewriteReferences(it, referenceMap) }) - else -> element - } - - private fun idFromFullUrl(fullUrl: String?): String? = - when { - fullUrl.isNullOrBlank() -> null - fullUrl.startsWith("urn:uuid:") -> fullUrl.substringAfterLast(':').ifBlank { null } - fullUrl.contains('/') -> fullUrl.substringAfterLast('/').substringBefore('?').ifBlank { null } - - else -> null - } - - private fun idFromRequestUrl(url: String?, resourceType: String): String? { - if (url.isNullOrBlank()) return null - val candidate = - url.substringAfterLast('/').substringBefore('?').ifBlank { - return null - } - return candidate.takeUnless { it == resourceType } - } - - private fun generateResourceId(resourceType: String): String { - generatedIdCounter += 1 - return "${resourceType.lowercase()}-$generatedIdCounter" - } - - private fun repairStoredGroups(): Boolean { - val groupBucket = resourcesByType["Group"] ?: return false - var repairedAny = false - - groupBucket.entries.toList().forEach { (groupId, resource) -> - val repaired = repairGroupResource(resource) - if (repaired != resource) { - groupBucket[groupId] = repaired - repairedAny = true - } - } - - return repairedAny - } - - private fun repairGroupResource(resource: Resource): Resource { - val group = resource as? Group ?: return resource - val groupJson = fhirJson.encodeToJsonElement(Group.serializer(), group).jsonObject - val repairedMemberArray = repairMemberReferences(groupJson["member"] as? JsonArray) - val normalizedGroupJson = - if (repairedMemberArray != null) JsonObject(groupJson + ("member" to repairedMemberArray)) - else groupJson - - val currentName = normalizedGroupJson["name"]?.jsonPrimitive?.contentOrNull?.trim() - val repairedName = - if (!currentName.isNullOrBlank() && !looksLikeModelDump(currentName)) { - currentName - } else { - inferredHouseholdName(normalizedGroupJson) - } - - val updates = - buildMap { - repairedMemberArray?.let { put("member", it) } - repairedName?.let { put("name", JsonPrimitive(it)) } - } - if (updates.isEmpty()) return resource - - val repairedJson = JsonObject(normalizedGroupJson + updates) - return fhirJson.decodeFromJsonElement(Group.serializer(), repairedJson) - } - - private fun repairMemberReferences(memberArray: JsonArray?): JsonArray? { - memberArray ?: return null - var repairedAny = false - - val repairedMembers = - memberArray.map { memberElement -> - val memberJson = memberElement as? JsonObject ?: return@map memberElement - val entityJson = memberJson["entity"] as? JsonObject ?: return@map memberElement - val existingReference = entityJson["reference"]?.jsonPrimitive?.contentOrNull - - if (!existingReference.isNullOrBlank()) return@map memberElement - - val malformedReferenceId = - (((entityJson["_reference"] as? JsonObject)?.get("id")) as? JsonPrimitive) - ?.contentOrNull - ?.trim() - .orEmpty() - if (malformedReferenceId.isBlank()) return@map memberElement - - repairedAny = true - JsonObject( - memberJson + - ("entity" to - JsonObject(entityJson + ("reference" to JsonPrimitive(malformedReferenceId)))) - ) - } - - return if (repairedAny) JsonArray(repairedMembers) else null - } - - private fun inferredHouseholdName(groupJson: JsonObject): String? { - val memberArray = groupJson["member"] as? JsonArray ?: return null - return memberArray.firstNotNullOfOrNull { memberElement -> - val memberJson = memberElement as? JsonObject ?: return@firstNotNullOfOrNull null - val reference = - ((memberJson["entity"] as? JsonObject)?.get("reference") as? JsonPrimitive)?.contentOrNull - val patientId = patientIdFromReference(reference) ?: return@firstNotNullOfOrNull null - val patient = - resourcesByType["Patient"]?.get(patientId) as? Patient ?: return@firstNotNullOfOrNull null - val patientJson = fhirJson.encodeToJsonElement(Patient.serializer(), patient).jsonObject - val firstName = (patientJson["name"] as? JsonArray)?.firstOrNull() as? JsonObject - val familyName = firstName?.get("family")?.jsonPrimitive?.contentOrNull?.trim().orEmpty() - val givenName = - ((firstName?.get("given") as? JsonArray)?.firstOrNull() as? JsonPrimitive) - ?.contentOrNull - ?.trim() - .orEmpty() - - when { - familyName.isNotBlank() -> "$familyName Household" - givenName.isNotBlank() -> "$givenName Household" - else -> null - } - } - } - - private fun looksLikeModelDump(value: String): Boolean = - value.startsWith("HumanName(") || - value.contains("dev.ohs.fhir.model.r4.") || - value.contains("id=null") || - value == "null" - - private fun resourceTypeName(resource: Resource): String = - fhirJson - .encodeToJsonElement(Resource.serializer(), resource) - .jsonObject["resourceType"] - ?.jsonPrimitive - ?.contentOrNull - .orEmpty() - - private fun resourceId(resource: Resource): String? = - fhirJson - .encodeToJsonElement(Resource.serializer(), resource) - .jsonObject["id"] - ?.jsonPrimitive - ?.contentOrNull -} - -private data class ResourceDraft( - val resourceType: String, - val normalizedId: String, - val fullUrl: String?, - val resourceJson: JsonObject, -) - -@OptIn(ExperimentalResourceApi::class) -private suspend fun loadBundledSampleResources(): List { - val fhirJson = Json { ignoreUnknownKeys = true } - val bundleJson = Res.readBytes("files/SampleResourcesBundle.json").decodeToString() - val bundle = fhirJson.decodeFromString(Bundle.serializer(), bundleJson) - return bundle.entry.mapNotNull { it.resource } -} diff --git a/ohs-player-reference-app/src/commonMain/kotlin/dev/ohs/player/reference/app/data/repository/PatientRepository.kt b/ohs-player-reference-app/src/commonMain/kotlin/dev/ohs/player/reference/app/data/repository/PatientRepository.kt index 04b20a22..3cf7bee0 100644 --- a/ohs-player-reference-app/src/commonMain/kotlin/dev/ohs/player/reference/app/data/repository/PatientRepository.kt +++ b/ohs-player-reference-app/src/commonMain/kotlin/dev/ohs/player/reference/app/data/repository/PatientRepository.kt @@ -23,7 +23,6 @@ import dev.ohs.player.generated.state.PatientImmunizationState import dev.ohs.player.generated.state.PatientMedicationState import dev.ohs.player.generated.state.PatientSummaryState import dev.ohs.player.generated.state.PatientTelecomState -import dev.ohs.player.reference.app.data.AppDependencies import dev.ohs.player.reference.app.data.Extraction.extractor import dev.ohs.player.reference.app.data.datasource.allPatientIds import dev.ohs.player.reference.app.data.datasource.patientProfileSearchResult @@ -34,12 +33,7 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map import kotlinx.coroutines.withContext -/** - * TODO(#58): Temporary implementation until FHIREngine integration is in place. Delete this once - * FHIREngine handles patient storage, retrieval, and related queries; that should simplify this - * repository. - */ -object PatientRepository { +class PatientRepository(private val fhirRepository: FhirRepository) { // FhirPathEvaluator holds mutable state is not concurrent-safe. // limitedParallelism(1) serializes all extraction on a single background thread without any @@ -47,23 +41,24 @@ object PatientRepository { private val extractorDispatcher = Dispatchers.Default.limitedParallelism(1) fun observePatients(): Flow> = - AppDependencies.fhirRepository.revision.map { getPatients() } + fhirRepository.revision.map { getPatients() } suspend fun getPatients(): List = withContext(extractorDispatcher) { - allPatientIds().mapNotNull { id -> - patientSummarySearchResult(id)?.let { + allPatientIds(fhirRepository).mapNotNull { id -> + patientSummarySearchResult(id, fhirRepository)?.let { extractor.extract(it).firstOrNull() } } } fun observePatientProfile(patientId: String): Flow = - AppDependencies.fhirRepository.revision.map { getPatientProfile(patientId) } + fhirRepository.revision.map { getPatientProfile(patientId) } suspend fun getPatientProfile(patientId: String): ProfileUiState = withContext(extractorDispatcher) { - val result = patientProfileSearchResult(patientId) ?: return@withContext ProfileUiState() + val result = + patientProfileSearchResult(patientId, fhirRepository) ?: return@withContext ProfileUiState() ProfileUiState( patient = extractor.extract(result).firstOrNull(), allergies = extractor.extract(result), diff --git a/ohs-player-reference-app/src/commonMain/kotlin/dev/ohs/player/reference/app/feature/group/list/GroupListScreen.kt b/ohs-player-reference-app/src/commonMain/kotlin/dev/ohs/player/reference/app/feature/group/list/GroupListScreen.kt index b2451b4a..f797dd17 100644 --- a/ohs-player-reference-app/src/commonMain/kotlin/dev/ohs/player/reference/app/feature/group/list/GroupListScreen.kt +++ b/ohs-player-reference-app/src/commonMain/kotlin/dev/ohs/player/reference/app/feature/group/list/GroupListScreen.kt @@ -34,16 +34,16 @@ import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.lifecycle.viewmodel.compose.viewModel import dev.ohs.player.generated.state.GroupListState import dev.ohs.player.generated.viewtype.ViewTypeCS import dev.ohs.player.library.layout.VerticalListRenderer import dev.ohs.player.library.scaffold.ListScaffold +import org.koin.compose.viewmodel.koinViewModel @OptIn(ExperimentalMaterial3Api::class) @Composable fun GroupListScreen(onGroupClick: (String) -> Unit, onDataCaptureClick: () -> Unit) { - val viewModel: GroupListViewModel = viewModel { GroupListViewModel() } + val viewModel: GroupListViewModel = koinViewModel() val groups by viewModel.groups.collectAsStateWithLifecycle() if (groups == null) { diff --git a/ohs-player-reference-app/src/commonMain/kotlin/dev/ohs/player/reference/app/feature/group/list/GroupListViewModel.kt b/ohs-player-reference-app/src/commonMain/kotlin/dev/ohs/player/reference/app/feature/group/list/GroupListViewModel.kt index f4702e53..ad7d9568 100644 --- a/ohs-player-reference-app/src/commonMain/kotlin/dev/ohs/player/reference/app/feature/group/list/GroupListViewModel.kt +++ b/ohs-player-reference-app/src/commonMain/kotlin/dev/ohs/player/reference/app/feature/group/list/GroupListViewModel.kt @@ -25,13 +25,13 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.collect import kotlinx.coroutines.launch -class GroupListViewModel : ViewModel() { +class GroupListViewModel(private val groupRepository: GroupRepository) : ViewModel() { private val _groups = MutableStateFlow?>(null) val groups: StateFlow?> = _groups.asStateFlow() init { viewModelScope.launch { - GroupRepository.observeGroups().collect { _groups.value = it.reversed() } + groupRepository.observeGroups().collect { _groups.value = it.reversed() } } } } diff --git a/ohs-player-reference-app/src/commonMain/kotlin/dev/ohs/player/reference/app/feature/group/profile/GroupProfileScreen.kt b/ohs-player-reference-app/src/commonMain/kotlin/dev/ohs/player/reference/app/feature/group/profile/GroupProfileScreen.kt index ab432c71..f80bae1a 100644 --- a/ohs-player-reference-app/src/commonMain/kotlin/dev/ohs/player/reference/app/feature/group/profile/GroupProfileScreen.kt +++ b/ohs-player-reference-app/src/commonMain/kotlin/dev/ohs/player/reference/app/feature/group/profile/GroupProfileScreen.kt @@ -41,7 +41,6 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.lifecycle.viewmodel.compose.viewModel import dev.ohs.player.generated.state.GroupHeaderState import dev.ohs.player.generated.state.GroupMemberState import dev.ohs.player.generated.viewtype.ViewTypeCS @@ -49,6 +48,8 @@ import dev.ohs.player.library.registry.LocalViewRegistry import dev.ohs.player.library.registry.componentRenderer import dev.ohs.player.library.registry.layoutRenderer import dev.ohs.player.library.renderer.RenderOptions +import org.koin.compose.viewmodel.koinViewModel +import org.koin.core.parameter.parametersOf @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -58,7 +59,7 @@ fun GroupProfileScreen( onMemberClick: (String) -> Unit, onAddMembers: () -> Unit, ) { - val viewModel = viewModel(key = groupId) { GroupProfileViewModel(groupId) } + val viewModel = koinViewModel(key = groupId) { parametersOf(groupId) } val state by viewModel.uiState.collectAsStateWithLifecycle() val registry = LocalViewRegistry.current diff --git a/ohs-player-reference-app/src/commonMain/kotlin/dev/ohs/player/reference/app/feature/group/profile/GroupProfileViewModel.kt b/ohs-player-reference-app/src/commonMain/kotlin/dev/ohs/player/reference/app/feature/group/profile/GroupProfileViewModel.kt index 2b802b9c..f2fbeb0e 100644 --- a/ohs-player-reference-app/src/commonMain/kotlin/dev/ohs/player/reference/app/feature/group/profile/GroupProfileViewModel.kt +++ b/ohs-player-reference-app/src/commonMain/kotlin/dev/ohs/player/reference/app/feature/group/profile/GroupProfileViewModel.kt @@ -31,13 +31,13 @@ data class GroupProfileUiState( val members: List = emptyList(), ) -class GroupProfileViewModel(groupId: String) : ViewModel() { +class GroupProfileViewModel(groupId: String, groupRepository: GroupRepository) : ViewModel() { private val _uiState = MutableStateFlow(null) val uiState: StateFlow = _uiState.asStateFlow() init { viewModelScope.launch { - GroupRepository.observeGroupProfile(groupId).collect { _uiState.value = it } + groupRepository.observeGroupProfile(groupId).collect { _uiState.value = it } } } } diff --git a/ohs-player-reference-app/src/commonMain/kotlin/dev/ohs/player/reference/app/feature/patient/list/PatientListScreen.kt b/ohs-player-reference-app/src/commonMain/kotlin/dev/ohs/player/reference/app/feature/patient/list/PatientListScreen.kt index af0e696e..108f636f 100644 --- a/ohs-player-reference-app/src/commonMain/kotlin/dev/ohs/player/reference/app/feature/patient/list/PatientListScreen.kt +++ b/ohs-player-reference-app/src/commonMain/kotlin/dev/ohs/player/reference/app/feature/patient/list/PatientListScreen.kt @@ -28,16 +28,16 @@ import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.lifecycle.viewmodel.compose.viewModel import dev.ohs.player.generated.state.PatientSummaryState import dev.ohs.player.generated.viewtype.ViewTypeCS import dev.ohs.player.library.layout.VerticalListRenderer import dev.ohs.player.library.scaffold.ListScaffold +import org.koin.compose.viewmodel.koinViewModel @OptIn(ExperimentalMaterial3Api::class) @Composable fun PatientListScreen(onPatientClick: (String) -> Unit) { - val viewModel: PatientListViewModel = viewModel { PatientListViewModel() } + val viewModel: PatientListViewModel = koinViewModel() val patients by viewModel.patients.collectAsStateWithLifecycle() if (patients == null) { diff --git a/ohs-player-reference-app/src/commonMain/kotlin/dev/ohs/player/reference/app/feature/patient/list/PatientListViewModel.kt b/ohs-player-reference-app/src/commonMain/kotlin/dev/ohs/player/reference/app/feature/patient/list/PatientListViewModel.kt index 93df68fc..8f2a3938 100644 --- a/ohs-player-reference-app/src/commonMain/kotlin/dev/ohs/player/reference/app/feature/patient/list/PatientListViewModel.kt +++ b/ohs-player-reference-app/src/commonMain/kotlin/dev/ohs/player/reference/app/feature/patient/list/PatientListViewModel.kt @@ -25,11 +25,11 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.collect import kotlinx.coroutines.launch -class PatientListViewModel : ViewModel() { +class PatientListViewModel(private val patientRepository: PatientRepository) : ViewModel() { private val _patients = MutableStateFlow?>(null) val patients: StateFlow?> = _patients.asStateFlow() init { - viewModelScope.launch { PatientRepository.observePatients().collect { _patients.value = it } } + viewModelScope.launch { patientRepository.observePatients().collect { _patients.value = it } } } } diff --git a/ohs-player-reference-app/src/commonMain/kotlin/dev/ohs/player/reference/app/feature/patient/profile/PatientProfileScreen.kt b/ohs-player-reference-app/src/commonMain/kotlin/dev/ohs/player/reference/app/feature/patient/profile/PatientProfileScreen.kt index 8f9d5115..1407ea95 100644 --- a/ohs-player-reference-app/src/commonMain/kotlin/dev/ohs/player/reference/app/feature/patient/profile/PatientProfileScreen.kt +++ b/ohs-player-reference-app/src/commonMain/kotlin/dev/ohs/player/reference/app/feature/patient/profile/PatientProfileScreen.kt @@ -44,7 +44,6 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.lifecycle.viewmodel.compose.viewModel import dev.ohs.player.generated.state.AllergyReactionState import dev.ohs.player.generated.state.PatientAllergyState import dev.ohs.player.generated.state.PatientConditionState @@ -58,11 +57,14 @@ import dev.ohs.player.library.registry.LocalViewRegistry import dev.ohs.player.library.registry.componentRenderer import dev.ohs.player.library.registry.layoutRenderer import dev.ohs.player.library.renderer.RenderOptions +import org.koin.compose.viewmodel.koinViewModel +import org.koin.core.parameter.parametersOf @OptIn(ExperimentalMaterial3Api::class) @Composable fun PatientProfileScreen(patientId: String, onBack: () -> Unit, onAddClinicalData: () -> Unit) { - val viewModel = viewModel(key = patientId) { IpsPatientProfileViewModel(patientId) } + val viewModel = + koinViewModel(key = patientId) { parametersOf(patientId) } val state by viewModel.uiState.collectAsStateWithLifecycle() val registry = LocalViewRegistry.current diff --git a/ohs-player-reference-app/src/commonMain/kotlin/dev/ohs/player/reference/app/feature/patient/profile/PatientProfileViewModel.kt b/ohs-player-reference-app/src/commonMain/kotlin/dev/ohs/player/reference/app/feature/patient/profile/PatientProfileViewModel.kt index 2353212d..674bb430 100644 --- a/ohs-player-reference-app/src/commonMain/kotlin/dev/ohs/player/reference/app/feature/patient/profile/PatientProfileViewModel.kt +++ b/ohs-player-reference-app/src/commonMain/kotlin/dev/ohs/player/reference/app/feature/patient/profile/PatientProfileViewModel.kt @@ -21,16 +21,16 @@ import dev.ohs.player.reference.app.data.repository.PatientRepository import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.collect import kotlinx.coroutines.launch -class IpsPatientProfileViewModel(patientId: String) : ViewModel() { +class PatientProfileViewModel(patientId: String, patientRepository: PatientRepository) : + ViewModel() { private val _uiState = MutableStateFlow(null) val uiState: StateFlow = _uiState.asStateFlow() init { viewModelScope.launch { - PatientRepository.observePatientProfile(patientId).collect { _uiState.value = it } + patientRepository.observePatientProfile(patientId).collect { _uiState.value = it } } } } diff --git a/ohs-player-reference-app/src/commonMain/kotlin/dev/ohs/player/reference/app/feature/questionnaire/QuestionnaireHostScreen.kt b/ohs-player-reference-app/src/commonMain/kotlin/dev/ohs/player/reference/app/feature/questionnaire/QuestionnaireHostScreen.kt index f7e45861..d895a71c 100644 --- a/ohs-player-reference-app/src/commonMain/kotlin/dev/ohs/player/reference/app/feature/questionnaire/QuestionnaireHostScreen.kt +++ b/ohs-player-reference-app/src/commonMain/kotlin/dev/ohs/player/reference/app/feature/questionnaire/QuestionnaireHostScreen.kt @@ -45,15 +45,15 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp -import androidx.lifecycle.viewmodel.compose.viewModel import dev.ohs.fhir.datacapture.Questionnaire import dev.ohs.fhir.datacapture.QuestionnaireConfig import dev.ohs.fhir.datacapture.QuestionnaireItemViewFactoryMatcher import dev.ohs.fhir.datacapture.QuestionnaireItemViewFactoryMatchersProvider -import dev.ohs.player.reference.app.data.AppDependencies import kotlin.time.Duration.Companion.milliseconds import kotlinx.coroutines.delay import kotlinx.coroutines.launch +import org.koin.compose.viewmodel.koinViewModel +import org.koin.core.parameter.parametersOf @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -74,12 +74,8 @@ fun QuestionnaireHostScreen( } val viewModel = - viewModel(key = "$questionnaireId:$patientId:$groupId") { - QuestionnaireHostViewModel( - questionnaireId = questionnaireId, - launchContext = launchContext, - questionnaireService = QuestionnaireService(repository = AppDependencies.fhirRepository), - ) + koinViewModel(key = "$questionnaireId:$patientId:$groupId") { + parametersOf(questionnaireId, launchContext) } val uiState by viewModel.uiState.collectAsState() val coroutineScope = rememberCoroutineScope() diff --git a/ohs-player-reference-app/src/commonMain/kotlin/dev/ohs/player/reference/app/data/repository/RepositorySnapshotStore.kt b/ohs-player-reference-app/src/commonMain/kotlin/dev/ohs/player/reference/app/feature/questionnaire/QuestionnaireHostUiState.kt similarity index 53% rename from ohs-player-reference-app/src/commonMain/kotlin/dev/ohs/player/reference/app/data/repository/RepositorySnapshotStore.kt rename to ohs-player-reference-app/src/commonMain/kotlin/dev/ohs/player/reference/app/feature/questionnaire/QuestionnaireHostUiState.kt index 207ed104..c1bce350 100644 --- a/ohs-player-reference-app/src/commonMain/kotlin/dev/ohs/player/reference/app/data/repository/RepositorySnapshotStore.kt +++ b/ohs-player-reference-app/src/commonMain/kotlin/dev/ohs/player/reference/app/feature/questionnaire/QuestionnaireHostUiState.kt @@ -13,21 +13,17 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package dev.ohs.player.reference.app.data.repository +package dev.ohs.player.reference.app.feature.questionnaire -interface RepositorySnapshotStore { - suspend fun read(): String? +sealed interface QuestionnaireHostUiState { + data object Loading : QuestionnaireHostUiState - suspend fun write(snapshot: String) -} + data class Ready(val questionnaireJson: String, val title: String?) : QuestionnaireHostUiState -/** - * TODO(#58): Temporary snapshot persistence for the in-memory repository. Delete this once - * FHIREngine-backed persistence is implemented; that should make these platform-specific stores - * unnecessary. - */ -expect object PlatformRepositorySnapshotStore : RepositorySnapshotStore { - override suspend fun read(): String? + data class Submitting(val questionnaireJson: String, val title: String?) : + QuestionnaireHostUiState + + data class Submitted(val result: QuestionnaireSubmissionResult) : QuestionnaireHostUiState - override suspend fun write(snapshot: String) + data class Error(val message: String) : QuestionnaireHostUiState } diff --git a/ohs-player-reference-app/src/commonMain/kotlin/dev/ohs/player/reference/app/feature/questionnaire/QuestionnaireHostViewModel.kt b/ohs-player-reference-app/src/commonMain/kotlin/dev/ohs/player/reference/app/feature/questionnaire/QuestionnaireHostViewModel.kt index 3efb74f4..ac5cbccc 100644 --- a/ohs-player-reference-app/src/commonMain/kotlin/dev/ohs/player/reference/app/feature/questionnaire/QuestionnaireHostViewModel.kt +++ b/ohs-player-reference-app/src/commonMain/kotlin/dev/ohs/player/reference/app/feature/questionnaire/QuestionnaireHostViewModel.kt @@ -23,30 +23,6 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch -import kotlinx.serialization.json.Json - -/** Single shared [Json] instance/configuration for encoding and decoding FHIR resources. */ -object FhirJson { - val instance: Json = Json { - prettyPrint = true - explicitNulls = false - encodeDefaults = false - ignoreUnknownKeys = true - } -} - -sealed interface QuestionnaireHostUiState { - data object Loading : QuestionnaireHostUiState - - data class Ready(val questionnaireJson: String, val title: String?) : QuestionnaireHostUiState - - data class Submitting(val questionnaireJson: String, val title: String?) : - QuestionnaireHostUiState - - data class Submitted(val result: QuestionnaireSubmissionResult) : QuestionnaireHostUiState - - data class Error(val message: String) : QuestionnaireHostUiState -} /** * Screen-level state holder for hosting a single questionnaire. All FHIR work is delegated to diff --git a/ohs-player-reference-app/src/commonMain/kotlin/dev/ohs/player/reference/app/feature/questionnaire/QuestionnaireService.kt b/ohs-player-reference-app/src/commonMain/kotlin/dev/ohs/player/reference/app/feature/questionnaire/QuestionnaireService.kt index 160f828e..8411addd 100644 --- a/ohs-player-reference-app/src/commonMain/kotlin/dev/ohs/player/reference/app/feature/questionnaire/QuestionnaireService.kt +++ b/ohs-player-reference-app/src/commonMain/kotlin/dev/ohs/player/reference/app/feature/questionnaire/QuestionnaireService.kt @@ -30,6 +30,7 @@ import dev.ohs.fhir.model.r4.Reference import dev.ohs.fhir.model.r4.String as FhirString import dev.ohs.player.reference.app.data.repository.FhirRepository import dev.ohs.player.reference.app.generateId +import dev.ohs.player.reference.app.util.FhirJson import kotlinx.serialization.json.JsonArray import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.JsonPrimitive diff --git a/ohs-player-reference-app/src/webMain/kotlin/dev/ohs/player/reference/app/data/repository/PlatformRepositorySnapshotStore.web.kt b/ohs-player-reference-app/src/commonMain/kotlin/dev/ohs/player/reference/app/util/FhirJson.kt similarity index 57% rename from ohs-player-reference-app/src/webMain/kotlin/dev/ohs/player/reference/app/data/repository/PlatformRepositorySnapshotStore.web.kt rename to ohs-player-reference-app/src/commonMain/kotlin/dev/ohs/player/reference/app/util/FhirJson.kt index 989ef9f2..b35834bc 100644 --- a/ohs-player-reference-app/src/webMain/kotlin/dev/ohs/player/reference/app/data/repository/PlatformRepositorySnapshotStore.web.kt +++ b/ohs-player-reference-app/src/commonMain/kotlin/dev/ohs/player/reference/app/util/FhirJson.kt @@ -13,16 +13,16 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package dev.ohs.player.reference.app.data.repository +package dev.ohs.player.reference.app.util -import kotlinx.browser.window +import kotlinx.serialization.json.Json -private const val SNAPSHOT_STORAGE_KEY = "ohs-player-reference-app.fhir-repository" - -actual object PlatformRepositorySnapshotStore : RepositorySnapshotStore { - actual override suspend fun read(): String? = window.localStorage.getItem(SNAPSHOT_STORAGE_KEY) - - actual override suspend fun write(snapshot: String) { - window.localStorage.setItem(SNAPSHOT_STORAGE_KEY, snapshot) +/** Single shared [Json] instance/configuration for encoding and decoding FHIR resources. */ +object FhirJson { + val instance: Json = Json { + prettyPrint = true + explicitNulls = false + encodeDefaults = false + ignoreUnknownKeys = true } } diff --git a/ohs-player-reference-app/src/commonTest/kotlin/dev/ohs/player/reference/app/data/repository/GroupRepositoryTest.kt b/ohs-player-reference-app/src/commonTest/kotlin/dev/ohs/player/reference/app/data/repository/GroupRepositoryTest.kt index a71f70b6..13e048b5 100644 --- a/ohs-player-reference-app/src/commonTest/kotlin/dev/ohs/player/reference/app/data/repository/GroupRepositoryTest.kt +++ b/ohs-player-reference-app/src/commonTest/kotlin/dev/ohs/player/reference/app/data/repository/GroupRepositoryTest.kt @@ -24,7 +24,7 @@ class GroupRepositoryTest { @Test fun buildGroupProfileUiState_usesRelationshipFreePatientAsHead() { val result = - GroupRepository.buildGroupProfileUiState( + buildGroupProfileUiState( groupHeader = GroupHeaderState(groupId = "group-1", groupName = "Doe Household", memberCount = "3"), extractedMembers = @@ -61,7 +61,7 @@ class GroupRepositoryTest { @Test fun buildGroupProfileUiState_fallsBackToFirstMemberWhenEveryMemberHasRelationship() { val result = - GroupRepository.buildGroupProfileUiState( + buildGroupProfileUiState( groupHeader = GroupHeaderState( groupId = "group-1", @@ -96,7 +96,7 @@ class GroupRepositoryTest { @Test fun buildGroupProfileUiState_keepsHeadNavigableForSingleMemberHousehold() { val result = - GroupRepository.buildGroupProfileUiState( + buildGroupProfileUiState( groupHeader = GroupHeaderState(groupId = "group-1", groupName = "Solo Household", memberCount = "1"), extractedMembers = diff --git a/ohs-player-reference-app/src/commonTest/kotlin/dev/ohs/player/reference/app/data/repository/InMemoryFhirRepositoryTest.kt b/ohs-player-reference-app/src/commonTest/kotlin/dev/ohs/player/reference/app/data/repository/InMemoryFhirRepositoryTest.kt deleted file mode 100644 index f5e75723..00000000 --- a/ohs-player-reference-app/src/commonTest/kotlin/dev/ohs/player/reference/app/data/repository/InMemoryFhirRepositoryTest.kt +++ /dev/null @@ -1,284 +0,0 @@ -/* - * Copyright 2026 Open Health Stack Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package dev.ohs.player.reference.app.data.repository - -import dev.ohs.fhir.model.r4.Bundle -import dev.ohs.fhir.model.r4.Group -import dev.ohs.fhir.model.r4.Patient -import dev.ohs.player.generated.state.GroupListState -import dev.ohs.player.reference.app.data.Extraction -import dev.ohs.player.reference.app.data.datasource.allPatientIds -import dev.ohs.player.reference.app.data.datasource.groupListSearchResults -import dev.ohs.player.reference.app.data.datasource.groupProfileSearchResult -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertNotNull -import kotlin.test.assertTrue -import kotlinx.coroutines.test.runTest -import kotlinx.serialization.json.Json - -class InMemoryFhirRepositoryTest { - private val json = Json { ignoreUnknownKeys = true } - - @Test - fun upsertBundle_persistsExtractedResourcesForUiQueries() = runTest { - val repository = InMemoryFhirRepository(seedResourcesLoader = { emptyList() }) - val bundle = json.decodeFromString(Bundle.serializer(), extractedBundleJson()) - - val storedCount = repository.upsert(bundle) - - assertEquals(1L, repository.revision.value) - assertEquals(2, storedCount) - assertEquals(listOf("patient-100"), allPatientIds(repository)) - - val groupResults = groupListSearchResults(repository) - assertEquals(listOf("group-100"), groupResults.map { it.resource.id }) - - val groupProfile = assertNotNull(groupProfileSearchResult("group-100", repository)) - val includedPatients = - groupProfile.included?.get("member").orEmpty().filterIsInstance() - assertEquals(listOf("patient-100"), includedPatients.mapNotNull(Patient::id)) - assertEquals(null, groupProfile.revIncluded) - assertTrue(repository.all("QuestionnaireResponse").isEmpty()) - } - - @Test - fun groupProfileSearchResult_resolvesAbsolutePatientReferences() = runTest { - val repository = InMemoryFhirRepository(seedResourcesLoader = { emptyList() }) - val patient = - json.decodeFromString( - Patient.serializer(), - """ - { - "resourceType": "Patient", - "id": "patient-200", - "active": true, - "name": [ - { - "family": "Otieno", - "given": ["Akinyi"] - } - ] - } - """ - .trimIndent(), - ) - val group = - json.decodeFromString( - Group.serializer(), - """ - { - "resourceType": "Group", - "id": "group-200", - "type": "person", - "actual": true, - "member": [ - { - "entity": { - "reference": "https://example.org/fhir/Patient/patient-200/_history/3" - } - } - ] - } - """ - .trimIndent(), - ) - - repository.upsert(patient) - repository.upsert(group) - - val groupProfile = assertNotNull(groupProfileSearchResult("group-200", repository)) - val includedPatients = - groupProfile.included?.get("member").orEmpty().filterIsInstance() - - assertEquals(listOf("patient-200"), includedPatients.mapNotNull(Patient::id)) - } - - @Test - fun groupProfileSearchResult_repairsMalformedReferenceElementIds() = runTest { - val repository = InMemoryFhirRepository(seedResourcesLoader = { emptyList() }) - val patient = - json.decodeFromString( - Patient.serializer(), - """ - { - "resourceType": "Patient", - "id": "patient-250", - "active": true, - "name": [ - { - "family": "Njeri", - "given": ["Wanjiku"] - } - ] - } - """ - .trimIndent(), - ) - val group = - json.decodeFromString( - Group.serializer(), - """ - { - "resourceType": "Group", - "id": "group-250", - "type": "person", - "actual": true, - "member": [ - { - "entity": { - "_reference": { - "id": "Patient/patient-250" - } - } - } - ] - } - """ - .trimIndent(), - ) - - repository.upsert(patient) - repository.upsert(group) - - val groupProfile = assertNotNull(groupProfileSearchResult("group-250", repository)) - val includedPatients = - groupProfile.included?.get("member").orEmpty().filterIsInstance() - - assertEquals(listOf("patient-250"), includedPatients.mapNotNull(Patient::id)) - } - - @Test - fun upsert_repairsMalformedHouseholdNameFromLinkedPatient() = runTest { - val repository = InMemoryFhirRepository(seedResourcesLoader = { emptyList() }) - val patient = - json.decodeFromString( - Patient.serializer(), - """ - { - "resourceType": "Patient", - "id": "patient-300", - "active": true, - "name": [ - { - "family": "Otieno", - "given": ["Akinyi"] - } - ] - } - """ - .trimIndent(), - ) - val group = - json.decodeFromString( - Group.serializer(), - """ - { - "resourceType": "Group", - "id": "group-300", - "type": "person", - "actual": true, - "name": "HumanName(id=null, extension=[], use=null)", - "member": [ - { - "entity": { - "reference": "Patient/patient-300" - } - } - ] - } - """ - .trimIndent(), - ) - - repository.upsert(patient) - repository.upsert(group) - - val states = - groupListSearchResults(repository).flatMap { result -> - Extraction.extractor.extract(result) - } - - assertEquals(listOf("Otieno Household"), states.mapNotNull { it.groupName }) - } - - @Test - fun snapshotStore_rehydratesResourcesAcrossRepositoryInstances() = runTest { - val snapshotStore = MemorySnapshotStore() - val firstRepository = - InMemoryFhirRepository(seedResourcesLoader = { emptyList() }, snapshotStore = snapshotStore) - - firstRepository.upsert(json.decodeFromString(Bundle.serializer(), extractedBundleJson())) - - val secondRepository = - InMemoryFhirRepository(seedResourcesLoader = { emptyList() }, snapshotStore = snapshotStore) - - assertEquals(listOf("patient-100"), allPatientIds(secondRepository)) - val groups = groupListSearchResults(secondRepository) - assertEquals(listOf("group-100"), groups.map { it.resource.id }) - } - - private fun extractedBundleJson(): String = - """ - { - "resourceType": "Bundle", - "type": "collection", - "entry": [ - { - "fullUrl": "urn:uuid:patient-100", - "resource": { - "resourceType": "Patient", - "active": true, - "name": [ - { - "family": "Otieno", - "given": ["Akinyi"] - } - ], - "gender": "female" - } - }, - { - "fullUrl": "urn:uuid:group-100", - "resource": { - "resourceType": "Group", - "type": "person", - "actual": true, - "active": true, - "member": [ - { - "entity": { - "reference": "urn:uuid:patient-100" - } - } - ] - } - } - ] - } - """ - .trimIndent() -} - -private class MemorySnapshotStore : RepositorySnapshotStore { - private var snapshot: String? = null - - override suspend fun read(): String? = snapshot - - override suspend fun write(snapshot: String) { - this.snapshot = snapshot - } -} diff --git a/ohs-player-reference-app/src/commonTest/kotlin/dev/ohs/player/reference/app/data/repository/InMemorySampleFhirRepository.kt b/ohs-player-reference-app/src/commonTest/kotlin/dev/ohs/player/reference/app/data/repository/InMemorySampleFhirRepository.kt new file mode 100644 index 00000000..2021fa46 --- /dev/null +++ b/ohs-player-reference-app/src/commonTest/kotlin/dev/ohs/player/reference/app/data/repository/InMemorySampleFhirRepository.kt @@ -0,0 +1,55 @@ +/* + * Copyright 2026 Open Health Stack Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package dev.ohs.player.reference.app.data.repository + +import dev.ohs.fhir.model.r4.Bundle +import dev.ohs.fhir.model.r4.Resource +import dev.ohs.fhir.resourceType +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow + +/** [FhirRepository] test double for UI tests. */ +class InMemorySampleFhirRepository : FhirRepository { + private val resourcesByType = mutableMapOf>() + private val _revision = MutableStateFlow(0L) + + override val revision: StateFlow = _revision + + override suspend fun upsert(resource: Resource) { + store(resource) + _revision.value += 1 + } + + override suspend fun upsert(bundle: Bundle): Int { + val resources = bundle.entry.mapNotNull { it.resource } + resources.forEach(::store) + if (resources.isNotEmpty()) _revision.value += 1 + return resources.size + } + + override suspend fun get(resourceType: String, id: String): Resource? { + return resourcesByType[resourceType]?.get(id) + } + + override suspend fun all(resourceType: String): List { + return resourcesByType[resourceType]?.values?.toList().orEmpty() + } + + private fun store(resource: Resource) { + val id = resource.id ?: return + resourcesByType.getOrPut(resource.resourceType) { mutableMapOf() }[id] = resource + } +} diff --git a/ohs-player-reference-app/src/commonTest/kotlin/dev/ohs/player/reference/app/data/repository/SamplePatientFixture.kt b/ohs-player-reference-app/src/commonTest/kotlin/dev/ohs/player/reference/app/data/repository/SamplePatientFixture.kt new file mode 100644 index 00000000..db2b511e --- /dev/null +++ b/ohs-player-reference-app/src/commonTest/kotlin/dev/ohs/player/reference/app/data/repository/SamplePatientFixture.kt @@ -0,0 +1,105 @@ +/* + * Copyright 2026 Open Health Stack Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package dev.ohs.player.reference.app.data.repository + +import dev.ohs.fhir.model.r4.Resource +import dev.ohs.player.reference.app.util.FhirJson + +/** Patient "p1" (Amina Diallo) and related clinical resources, for UI tests. */ +object SamplePatientFixture { + val resources: List = + listOf( + """ + { + "resourceType": "Patient", + "id": "p1", + "name": [{"family": "Diallo", "given": ["Amina"]}], + "gender": "female", + "birthDate": "1990-03-14", + "active": true, + "telecom": [ + {"system": "phone", "value": "+221 77 123 4567"}, + {"system": "email", "value": "amina.diallo@example.com"} + ] + } + """, + """ + { + "resourceType": "AllergyIntolerance", + "id": "ai-p1-a", + "patient": {"reference": "Patient/p1"}, + "code": {"coding": [{"display": "Penicillin"}]}, + "criticality": "high", + "clinicalStatus": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/allergyintolerance-clinical", + "code": "active" + } + ] + }, + "reaction": [ + { + "manifestation": [ + { + "coding": [ + {"system": "http://snomed.info/sct", "code": "247472004", "display": "Urticaria"} + ] + } + ], + "severity": "moderate" + } + ] + } + """, + """ + { + "resourceType": "MedicationRequest", + "id": "med-p1-a", + "status": "active", + "intent": "order", + "subject": {"reference": "Patient/p1"}, + "medicationCodeableConcept": {"coding": [{"display": "Salbutamol 100mcg inhaler"}]}, + "dosageInstruction": [{"text": "100mcg PRN"}] + } + """, + """ + { + "resourceType": "Condition", + "id": "cond-p1-a", + "subject": {"reference": "Patient/p1"}, + "code": {"coding": [{"display": "Asthma"}]}, + "clinicalStatus": { + "coding": [ + {"system": "http://terminology.hl7.org/CodeSystem/condition-clinical", "code": "active"} + ] + }, + "onsetDateTime": "2015-06-01" + } + """, + """ + { + "resourceType": "Immunization", + "id": "imm-p1-a", + "status": "completed", + "vaccineCode": {"coding": [{"display": "BCG vaccine"}]}, + "patient": {"reference": "Patient/p1"}, + "occurrenceDateTime": "1990-03-14" + } + """, + ) + .map { FhirJson.instance.decodeFromString(Resource.serializer(), it.trimIndent()) } +} diff --git a/ohs-player-reference-app/src/commonTest/kotlin/dev/ohs/player/reference/app/feature/patient/list/PatientListScreenTest.kt b/ohs-player-reference-app/src/commonTest/kotlin/dev/ohs/player/reference/app/feature/patient/list/PatientListScreenTest.kt index f1c14211..9dc6ce8d 100644 --- a/ohs-player-reference-app/src/commonTest/kotlin/dev/ohs/player/reference/app/feature/patient/list/PatientListScreenTest.kt +++ b/ohs-player-reference-app/src/commonTest/kotlin/dev/ohs/player/reference/app/feature/patient/list/PatientListScreenTest.kt @@ -24,12 +24,34 @@ import androidx.compose.ui.test.performClick import androidx.compose.ui.test.runComposeUiTest import dev.ohs.player.library.registry.LocalViewRegistry import dev.ohs.player.reference.app.buildAppViewRegistry +import dev.ohs.player.reference.app.data.di.repositoryModule +import dev.ohs.player.reference.app.data.di.viewModelModule +import dev.ohs.player.reference.app.data.repository.FhirRepository +import dev.ohs.player.reference.app.data.repository.InMemorySampleFhirRepository +import dev.ohs.player.reference.app.data.repository.SamplePatientFixture +import kotlin.test.AfterTest +import kotlin.test.BeforeTest import kotlin.test.Test import kotlin.test.assertEquals +import kotlinx.coroutines.test.runTest +import org.koin.core.context.startKoin +import org.koin.core.context.stopKoin +import org.koin.dsl.module @OptIn(ExperimentalTestApi::class) class PatientListScreenTest { + @BeforeTest + fun setUp() = runTest { + val repository = InMemorySampleFhirRepository() + SamplePatientFixture.resources.forEach { repository.upsert(it) } + startKoin { + modules(module { single { repository } }, repositoryModule, viewModelModule) + } + } + + @AfterTest fun tearDown() = stopKoin() + @Test fun tappingPatient_invokesOnPatientClickWithMatchingId() = runComposeUiTest { val registry = buildAppViewRegistry() diff --git a/ohs-player-reference-app/src/commonTest/kotlin/dev/ohs/player/reference/app/feature/patient/profile/PatientProfileScreenTest.kt b/ohs-player-reference-app/src/commonTest/kotlin/dev/ohs/player/reference/app/feature/patient/profile/PatientProfileScreenTest.kt index 27b76225..5ef3a036 100644 --- a/ohs-player-reference-app/src/commonTest/kotlin/dev/ohs/player/reference/app/feature/patient/profile/PatientProfileScreenTest.kt +++ b/ohs-player-reference-app/src/commonTest/kotlin/dev/ohs/player/reference/app/feature/patient/profile/PatientProfileScreenTest.kt @@ -25,12 +25,34 @@ import androidx.compose.ui.test.performScrollToNode import androidx.compose.ui.test.runComposeUiTest import dev.ohs.player.library.registry.LocalViewRegistry import dev.ohs.player.reference.app.buildAppViewRegistry +import dev.ohs.player.reference.app.data.di.repositoryModule +import dev.ohs.player.reference.app.data.di.viewModelModule +import dev.ohs.player.reference.app.data.repository.FhirRepository +import dev.ohs.player.reference.app.data.repository.InMemorySampleFhirRepository +import dev.ohs.player.reference.app.data.repository.SamplePatientFixture +import kotlin.test.AfterTest +import kotlin.test.BeforeTest import kotlin.test.Test import kotlin.test.assertTrue +import kotlinx.coroutines.test.runTest +import org.koin.core.context.startKoin +import org.koin.core.context.stopKoin +import org.koin.dsl.module @OptIn(ExperimentalTestApi::class) class PatientProfileScreenTest { + @BeforeTest + fun setUp() = runTest { + val repository = InMemorySampleFhirRepository() + SamplePatientFixture.resources.forEach { repository.upsert(it) } + startKoin { + modules(module { single { repository } }, repositoryModule, viewModelModule) + } + } + + @AfterTest fun tearDown() = stopKoin() + @Test fun knownPatient_rendersNameAndClinicalSections() = runComposeUiTest { val registry = buildAppViewRegistry() diff --git a/ohs-player-reference-app/src/iosMain/kotlin/dev/ohs/player/reference/app/MainViewController.kt b/ohs-player-reference-app/src/iosMain/kotlin/dev/ohs/player/reference/app/MainViewController.kt index 8de9753d..b0a9609a 100644 --- a/ohs-player-reference-app/src/iosMain/kotlin/dev/ohs/player/reference/app/MainViewController.kt +++ b/ohs-player-reference-app/src/iosMain/kotlin/dev/ohs/player/reference/app/MainViewController.kt @@ -16,12 +16,14 @@ package dev.ohs.player.reference.app import androidx.compose.ui.window.ComposeUIViewController -import dev.ohs.player.reference.app.data.AppDependencies -import dev.ohs.player.reference.app.data.repository.InMemoryFhirRepository -import dev.ohs.player.reference.app.data.repository.PlatformRepositorySnapshotStore +import dev.ohs.fhir.FhirEngine +import dev.ohs.fhir.FhirEngineConfiguration +import dev.ohs.fhir.FhirEngineProvider +import dev.ohs.player.reference.app.data.di.initKoin +import org.koin.dsl.module -fun MainViewController() = - InMemoryFhirRepository(snapshotStore = PlatformRepositorySnapshotStore).let { repository -> - AppDependencies.fhirRepository = repository - ComposeUIViewController { App() } - } +fun MainViewController() = run { + FhirEngineProvider.init(FhirEngineConfiguration()) + initKoin(module { single { FhirEngineProvider.getInstance() } }) + ComposeUIViewController { App() } +} diff --git a/ohs-player-reference-app/src/iosMain/kotlin/dev/ohs/player/reference/app/data/repository/PlatformRepositorySnapshotStore.ios.kt b/ohs-player-reference-app/src/iosMain/kotlin/dev/ohs/player/reference/app/data/repository/PlatformRepositorySnapshotStore.ios.kt deleted file mode 100644 index 6bd090b8..00000000 --- a/ohs-player-reference-app/src/iosMain/kotlin/dev/ohs/player/reference/app/data/repository/PlatformRepositorySnapshotStore.ios.kt +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Copyright 2026 Open Health Stack Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package dev.ohs.player.reference.app.data.repository - -import kotlin.experimental.ExperimentalNativeApi -import kotlinx.cinterop.ExperimentalForeignApi -import kotlinx.cinterop.addressOf -import kotlinx.cinterop.usePinned -import platform.Foundation.NSDocumentDirectory -import platform.Foundation.NSSearchPathForDirectoriesInDomains -import platform.Foundation.NSTemporaryDirectory -import platform.Foundation.NSUserDomainMask -import platform.posix.SEEK_END -import platform.posix.fclose -import platform.posix.fopen -import platform.posix.fread -import platform.posix.fseek -import platform.posix.ftell -import platform.posix.fwrite -import platform.posix.rewind - -private const val SNAPSHOT_FILE_NAME = "fhir-repository.json" - -@OptIn(ExperimentalForeignApi::class, ExperimentalNativeApi::class) -actual object PlatformRepositorySnapshotStore : RepositorySnapshotStore { - actual override suspend fun read(): String? { - val file = fopen(snapshotPath(), "rb") ?: return null - return try { - fseek(file, 0, SEEK_END) - val size = ftell(file) - if (size <= 0L) return null - rewind(file) - - val bytes = ByteArray(size.toInt()) - val bytesRead = - bytes.usePinned { pinned -> - fread(pinned.addressOf(0), 1u, bytes.size.toULong(), file).toInt() - } - bytes.decodeToString(endIndex = bytesRead).takeIf { it.isNotBlank() } - } finally { - fclose(file) - } - } - - actual override suspend fun write(snapshot: String) { - val file = fopen(snapshotPath(), "wb") ?: return - try { - val bytes = snapshot.encodeToByteArray() - bytes.usePinned { pinned -> fwrite(pinned.addressOf(0), 1u, bytes.size.toULong(), file) } - } finally { - fclose(file) - } - } - - private fun snapshotPath(): String { - val directory = - (NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, true) - .firstOrNull() as? String) ?: NSTemporaryDirectory() - return "$directory/$SNAPSHOT_FILE_NAME" - } -} diff --git a/ohs-player-reference-app/src/jvmMain/kotlin/dev/ohs/player/reference/app/data/repository/PlatformRepositorySnapshotStore.jvm.kt b/ohs-player-reference-app/src/jvmMain/kotlin/dev/ohs/player/reference/app/data/repository/PlatformRepositorySnapshotStore.jvm.kt deleted file mode 100644 index ac889fe6..00000000 --- a/ohs-player-reference-app/src/jvmMain/kotlin/dev/ohs/player/reference/app/data/repository/PlatformRepositorySnapshotStore.jvm.kt +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright 2026 Open Health Stack Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package dev.ohs.player.reference.app.data.repository - -import java.io.File - -private const val SNAPSHOT_FILE_NAME = "fhir-repository.json" - -actual object PlatformRepositorySnapshotStore : RepositorySnapshotStore { - actual override suspend fun read(): String? = - snapshotFile().takeIf(File::exists)?.readText().takeUnless { it.isNullOrBlank() } - - actual override suspend fun write(snapshot: String) { - val file = snapshotFile() - file.parentFile?.mkdirs() - file.writeText(snapshot) - } - - private fun snapshotFile(): File { - val userHome = System.getProperty("user.home").orEmpty().ifBlank { "." } - return File(File(userHome, ".ohs-player-reference-app"), SNAPSHOT_FILE_NAME) - } -} diff --git a/ohs-player-reference-app/src/jvmMain/kotlin/dev/ohs/player/reference/app/main.kt b/ohs-player-reference-app/src/jvmMain/kotlin/dev/ohs/player/reference/app/main.kt index 1972eaf3..288b297e 100644 --- a/ohs-player-reference-app/src/jvmMain/kotlin/dev/ohs/player/reference/app/main.kt +++ b/ohs-player-reference-app/src/jvmMain/kotlin/dev/ohs/player/reference/app/main.kt @@ -17,12 +17,17 @@ package dev.ohs.player.reference.app import androidx.compose.ui.window.Window import androidx.compose.ui.window.application -import dev.ohs.player.reference.app.data.AppDependencies -import dev.ohs.player.reference.app.data.repository.InMemoryFhirRepository -import dev.ohs.player.reference.app.data.repository.PlatformRepositorySnapshotStore +import dev.ohs.fhir.FhirEngine +import dev.ohs.fhir.FhirEngineConfiguration +import dev.ohs.fhir.FhirEngineProvider +import dev.ohs.player.reference.app.data.di.initKoin +import java.io.File +import org.koin.dsl.module fun main() = application { - AppDependencies.fhirRepository = - InMemoryFhirRepository(snapshotStore = PlatformRepositorySnapshotStore) + val userHome = System.getProperty("user.home").orEmpty().ifBlank { "." } + val storageDirectory = File(userHome, ".ohs-player-reference-app").absolutePath + FhirEngineProvider.init(FhirEngineConfiguration(storageDirectory = storageDirectory)) + initKoin(module { single { FhirEngineProvider.getInstance() } }) Window(onCloseRequest = ::exitApplication, title = "OHS Player Reference App") { App() } } diff --git a/ohs-player-reference-app/src/jvmTest/kotlin/dev/ohs/player/reference/app/data/repository/FhirEngineRepositoryTest.kt b/ohs-player-reference-app/src/jvmTest/kotlin/dev/ohs/player/reference/app/data/repository/FhirEngineRepositoryTest.kt new file mode 100644 index 00000000..ed9ce518 --- /dev/null +++ b/ohs-player-reference-app/src/jvmTest/kotlin/dev/ohs/player/reference/app/data/repository/FhirEngineRepositoryTest.kt @@ -0,0 +1,227 @@ +/* + * Copyright 2026 Open Health Stack Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package dev.ohs.player.reference.app.data.repository + +import dev.ohs.fhir.FhirEngine +import dev.ohs.fhir.FhirEngineConfiguration +import dev.ohs.fhir.FhirEngineProvider +import dev.ohs.fhir.model.r4.Bundle +import dev.ohs.fhir.model.r4.Group +import dev.ohs.fhir.model.r4.Patient +import java.nio.file.Files +import kotlin.test.BeforeTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNull +import kotlinx.coroutines.test.runTest +import kotlinx.serialization.json.Json + +class FhirEngineRepositoryTest { + private val json = Json { ignoreUnknownKeys = true } + private lateinit var fhirEngine: FhirEngine + + @BeforeTest + fun setUp() = runTest { + if (FhirEngineProvider.isNotInitialized()) { + FhirEngineProvider.init( + FhirEngineConfiguration( + storageDirectory = Files.createTempDirectory("fhir-engine-repository-test").toString() + ) + ) + } + fhirEngine = FhirEngineProvider.getInstance() + fhirEngine.clearDatabase() + } + + @Test + fun upsertResource_thenGet_returnsStoredResource() = runTest { + val repository = FhirEngineRepository(fhirEngine) + val patient = + json.decodeFromString( + Patient.serializer(), + """ + { + "resourceType": "Patient", + "id": "patient-1", + "active": true, + "name": [{"family": "Otieno", "given": ["Akinyi"]}] + } + """ + .trimIndent(), + ) + + repository.upsert(patient) + + val stored = repository.get("Patient", "patient-1") as? Patient + assertEquals("patient-1", stored?.id) + assertEquals(1L, repository.revision.value) + } + + @Test + fun upsertResource_calledTwiceWithSameId_updatesInPlace() = runTest { + val repository = FhirEngineRepository(fhirEngine) + val original = + json.decodeFromString( + Patient.serializer(), + """{"resourceType": "Patient", "id": "patient-2", "active": true}""", + ) + val updated = + json.decodeFromString( + Patient.serializer(), + """{"resourceType": "Patient", "id": "patient-2", "active": false}""", + ) + + repository.upsert(original) + repository.upsert(updated) + + val stored = repository.get("Patient", "patient-2") as? Patient + assertEquals(false, stored?.active?.value) + assertEquals(listOf("patient-2"), repository.all("Patient").mapNotNull { it.id }) + assertEquals(2L, repository.revision.value) + } + + @Test + fun get_missingResource_returnsNull() = runTest { + val repository = FhirEngineRepository(fhirEngine) + + assertNull(repository.get("Patient", "does-not-exist")) + } + + @Test + fun upsertBundle_resolvesFullUrlIdsAndRewritesReferences() = runTest { + val repository = FhirEngineRepository(fhirEngine) + val bundle = + json.decodeFromString( + Bundle.serializer(), + """ + { + "resourceType": "Bundle", + "type": "collection", + "entry": [ + { + "fullUrl": "urn:uuid:patient-100", + "resource": { + "resourceType": "Patient", + "active": true, + "name": [{"family": "Otieno", "given": ["Akinyi"]}] + } + }, + { + "fullUrl": "urn:uuid:group-100", + "resource": { + "resourceType": "Group", + "type": "person", + "actual": true, + "member": [ + {"entity": {"reference": "urn:uuid:patient-100"}} + ] + } + } + ] + } + """ + .trimIndent(), + ) + + val storedCount = repository.upsert(bundle) + + assertEquals(2, storedCount) + assertEquals(1L, repository.revision.value) + val patients = repository.all("Patient").filterIsInstance() + assertEquals(1, patients.size) + val patientId = patients.first().id.orEmpty() + val groups = repository.all("Group").filterIsInstance() + assertEquals("Patient/$patientId", groups.first().member.first().entity.reference?.value) + } + + @Test + fun upsertBundle_resolvesIdsViaRequestUrlAbsoluteFullUrlAndGeneratedFallback() = runTest { + val repository = FhirEngineRepository(fhirEngine) + val bundle = + json.decodeFromString( + Bundle.serializer(), + """ + { + "resourceType": "Bundle", + "type": "collection", + "entry": [ + { + "fullUrl": "https://example.org/fhir/Patient/patient-abs-1", + "resource": { + "resourceType": "Patient", + "name": [{"family": "Abs"}] + } + }, + { + "request": {"method": "PUT", "url": "Patient/patient-req-1"}, + "resource": { + "resourceType": "Patient", + "name": [{"family": "Req"}] + } + }, + { + "resource": { + "resourceType": "Patient", + "name": [{"family": "Generated"}] + } + }, + { + "fullUrl": "urn:uuid:group-200", + "resource": { + "resourceType": "Group", + "type": "person", + "actual": true, + "member": [ + {"entity": {"reference": "https://example.org/fhir/Patient/patient-abs-1"}} + ] + } + }, + { + "fullUrl": "urn:uuid:dropped-entry" + } + ] + } + """ + .trimIndent(), + ) + + val storedCount = repository.upsert(bundle) + + // 4 stored: absolute-fullUrl patient, request-url patient, generated-id patient, and the + // group. The 5th entry (resource == null) is silently dropped and does not count. + assertEquals(4, storedCount) + assertEquals(1L, repository.revision.value) + + val patients = repository.all("Patient").filterIsInstance() + assertEquals(3, patients.size) + val patientIds = patients.mapNotNull { it.id } + + // idFromFullUrl: absolute-URL fullUrl resolves to the last path segment. + assertEquals(true, patientIds.contains("patient-abs-1")) + // idFromRequestUrl: no fullUrl, so id comes from request.url's last path segment. + assertEquals(true, patientIds.contains("patient-req-1")) + // generateId(): neither fullUrl nor request present, so a random id is generated. It's + // whichever id isn't one of the two known ones above. + val generatedId = patientIds.first { it != "patient-abs-1" && it != "patient-req-1" } + assertEquals(true, generatedId.isNotBlank()) + + // Reference rewriting: the group's reference to the absolute-fullUrl patient is rewritten to + // "Patient/", proving the plain-path idFromFullUrl branch feeds the referenceMap. + val groups = repository.all("Group").filterIsInstance() + assertEquals(1, groups.size) + assertEquals("Patient/patient-abs-1", groups.first().member.first().entity.reference?.value) + } +} diff --git a/ohs-player-reference-app/src/webMain/kotlin/dev/ohs/player/reference/app/main.kt b/ohs-player-reference-app/src/webMain/kotlin/dev/ohs/player/reference/app/main.kt index 1d79d952..23398291 100644 --- a/ohs-player-reference-app/src/webMain/kotlin/dev/ohs/player/reference/app/main.kt +++ b/ohs-player-reference-app/src/webMain/kotlin/dev/ohs/player/reference/app/main.kt @@ -17,13 +17,15 @@ package dev.ohs.player.reference.app import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.window.ComposeViewport -import dev.ohs.player.reference.app.data.AppDependencies -import dev.ohs.player.reference.app.data.repository.InMemoryFhirRepository -import dev.ohs.player.reference.app.data.repository.PlatformRepositorySnapshotStore +import dev.ohs.fhir.FhirEngine +import dev.ohs.fhir.FhirEngineConfiguration +import dev.ohs.fhir.FhirEngineProvider +import dev.ohs.player.reference.app.data.di.initKoin +import org.koin.dsl.module @OptIn(ExperimentalComposeUiApi::class) fun main() { - AppDependencies.fhirRepository = - InMemoryFhirRepository(snapshotStore = PlatformRepositorySnapshotStore) + FhirEngineProvider.init(FhirEngineConfiguration()) + initKoin(module { single { FhirEngineProvider.getInstance() } }) ComposeViewport { App() } }