diff --git a/android/engine/build.gradle.kts b/android/engine/build.gradle.kts index 19b9ade052..532a607070 100644 --- a/android/engine/build.gradle.kts +++ b/android/engine/build.gradle.kts @@ -212,6 +212,8 @@ dependencies { api("com.squareup.retrofit2:retrofit-mock:$retrofitVersion") api("com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter:0.8.0") + implementation("ca.uhn.hapi.fhir:hapi-fhir-client-okhttp:${Dependencies.Versions.hapiFhir}") + val okhttpVersion = "4.12.0" api("com.squareup.okhttp3:okhttp:$okhttpVersion") api("com.squareup.okhttp3:logging-interceptor:$okhttpVersion") diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/data/local/register/dao/HivRegisterDao.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/data/local/register/dao/HivRegisterDao.kt index 46f20e9b93..8141495ec1 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/data/local/register/dao/HivRegisterDao.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/data/local/register/dao/HivRegisterDao.kt @@ -48,6 +48,7 @@ import org.smartregister.fhircore.engine.configuration.ConfigurationRegistry import org.smartregister.fhircore.engine.data.domain.Guardian import org.smartregister.fhircore.engine.data.domain.PregnancyStatus import org.smartregister.fhircore.engine.data.local.DefaultRepository +import org.smartregister.fhircore.engine.domain.model.CarePlanTask import org.smartregister.fhircore.engine.domain.model.HealthStatus import org.smartregister.fhircore.engine.domain.model.ProfileData import org.smartregister.fhircore.engine.domain.model.RegisterData @@ -245,7 +246,7 @@ constructor( fhirEngine.withTransaction { val patient = defaultRepository.loadResource(resourceId)!! val carePlan = patient.activeCarePlans(fhirEngine).firstOrNull() - + val (exists, activities) = fetchCarePlanActivities(carePlan) profileData = ProfileData.HivProfileData( logicalId = patient.logicalId, @@ -265,7 +266,8 @@ constructor( showIdentifierInProfile = true, currentCarePlan = carePlan, healthStatus = patient.extractHealthStatusFromMeta(patientTypeMetaTagCodingSystem), - tasks = fetchCarePlanActivities(carePlan), + tasks = activities, + hasMissingTasks = exists, conditions = defaultRepository.activePatientConditions(patient.logicalId), otherPatients = patient.otherChildren(), guardians = patient.guardians(), @@ -512,33 +514,49 @@ constructor( private suspend fun fetchCarePlanActivities( carePlan: CarePlan?, - ): List { - if (carePlan == null) return emptyList() - val activityOnList = mutableMapOf() - val tasksToFetch = mutableListOf() - for (planActivity in carePlan.activity) { - if (!planActivity.shouldShowOnProfile()) { - continue - } - val taskId = planActivity.outcomeReference.firstOrNull()?.extractId() - if (taskId != null) { - tasksToFetch.add(taskId) - activityOnList[taskId] = planActivity + ): Pair> { + if (carePlan == null) return Pair(false, emptyList()) + + val activityOnList = mutableMapOf() + val tasksToFetch = + carePlan.activity.mapNotNull { planActivity -> + if (planActivity.shouldShowOnProfile()) { + planActivity.outcomeReference.firstOrNull()?.extractId()?.also { taskId -> + activityOnList[taskId] = CarePlanTask(planActivity, false) + } + } else { + null + } } - } - if (tasksToFetch.isNotEmpty()) { - val tasks = fhirEngine.getResourcesByIds(tasksToFetch) - tasks.forEach { task -> - val planActivity: CarePlan.CarePlanActivityComponent? = activityOnList[task.logicalId] - if (planActivity != null) { - planActivity.detail?.status = task.taskStatusToCarePlanActivityStatus() - activityOnList[task.logicalId] = planActivity + + var hasMissingTask = false + val items: List = + if (tasksToFetch.isNotEmpty()) { + val tasks = fhirEngine.getResourcesByIds(tasksToFetch).associateBy { it.logicalId } + activityOnList.map { (taskId, carePlanTask) -> + println(taskId) + tasks[taskId]?.let { task -> + val updatedTask = + carePlanTask.task.apply { detail?.status = task.taskStatusToCarePlanActivityStatus() } + carePlanTask.copy(task = updatedTask, taskExists = true) + } + ?: run { + if (carePlanTask.task.detail?.status != CarePlan.CarePlanActivityStatus.SCHEDULED) { + hasMissingTask = true + } + carePlanTask.copy(taskExists = false) + } } + } else { + activityOnList.values.toList() } - } - return activityOnList.values.sortedWith( - compareBy(nullsLast()) { it.detail?.code?.text?.toBigIntegerOrNull() }, - ) + + val sortedItems = + items.sortedWith( + compareBy(nullsLast()) { it.task.detail?.code?.text?.toBigIntegerOrNull() }, + ) + + return Pair(hasMissingTask, sortedItems) } object ResourceValue { diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/data/remote/fhir/resource/ResourceFixerService.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/data/remote/fhir/resource/ResourceFixerService.kt new file mode 100644 index 0000000000..4631dd1df3 --- /dev/null +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/data/remote/fhir/resource/ResourceFixerService.kt @@ -0,0 +1,212 @@ +/* + * Copyright 2021 Ona Systems, Inc + * + * 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 org.smartregister.fhircore.engine.data.remote.fhir.resource + +import ca.uhn.fhir.rest.client.api.IGenericClient +import com.google.android.fhir.FhirEngine +import com.google.android.fhir.datacapture.extensions.logicalId +import com.google.android.fhir.get +import java.util.Date +import javax.inject.Inject +import org.hl7.fhir.r4.model.Bundle +import org.hl7.fhir.r4.model.CarePlan +import org.hl7.fhir.r4.model.CarePlan.CarePlanActivityComponent +import org.hl7.fhir.r4.model.Coding +import org.hl7.fhir.r4.model.Meta +import org.hl7.fhir.r4.model.Period +import org.hl7.fhir.r4.model.Reference +import org.hl7.fhir.r4.model.Resource +import org.hl7.fhir.r4.model.ResourceType +import org.hl7.fhir.r4.model.Task +import org.smartregister.fhircore.engine.util.SharedPreferenceKey +import org.smartregister.fhircore.engine.util.SharedPreferencesHelper +import org.smartregister.fhircore.engine.util.SystemConstants.CARE_PLAN_REFERENCE_SYSTEM +import org.smartregister.fhircore.engine.util.SystemConstants.QUESTIONNAIRE_REFERENCE_SYSTEM +import org.smartregister.fhircore.engine.util.SystemConstants.RESOURCE_CREATED_ON_TAG_SYSTEM +import org.smartregister.fhircore.engine.util.extension.extractId +import org.smartregister.fhircore.engine.util.extension.generateCreatedOn +import org.smartregister.fhircore.engine.util.extension.getResourcesByIds +import org.smartregister.fhircore.engine.util.extension.shouldShowOnProfile +import org.smartregister.fhircore.engine.util.extension.toTaskStatus +import timber.log.Timber + +class ResourceFixerService +@Inject +constructor( + private val fhirClient: IGenericClient, + private val fhirEngine: FhirEngine, + private val sharedPreferences: SharedPreferencesHelper, +) { + suspend fun fixCurrentCarePlan(patientId: String, carePlanId: String) { + val carePlan: CarePlan = fhirEngine.get(carePlanId) + val tasks = getMissingTasks(carePlan) + if (tasks.isEmpty()) { + return + } + Timber.i("Found missing tasks: ${tasks.size}") + val isOffline = sharedPreferences.read(SharedPreferenceKey.PATIENT_FIX_TYPE.name, false) + handleMissingTasks(patientId, carePlan, tasks, recreateAll = isOffline) + return + } + + private suspend fun getMissingTasks(carePlan: CarePlan): List { + val activityOnList = mutableMapOf() + val missingTasks = mutableListOf() + val tasksToFetch = + carePlan.activity.mapNotNull { planActivity -> + if (planActivity.shouldShowOnProfile()) { + planActivity.outcomeReference.firstOrNull()?.extractId()?.also { taskId -> + activityOnList[taskId] = planActivity + } + } else { + null + } + } + val tasks = fhirEngine.getResourcesByIds(tasksToFetch).associateBy { it.logicalId } + activityOnList.forEach { (taskId, activity) -> + if (activity.detail?.status != CarePlan.CarePlanActivityStatus.SCHEDULED) { + if (!tasks.containsKey(taskId)) { + missingTasks.add(activity) + } + } + } + return missingTasks + } + + private suspend fun handleMissingTasks( + patientId: String, + carePlan: CarePlan, + tasks: List, + recreateAll: Boolean, + ) { + val resourceToSave = mutableListOf() + + if (recreateAll) { + resourceToSave.addAll( + tasks.map { + createGenericTask( + patientId = patientId, + activity = it, + carePlan = carePlan, + ) + }, + ) + Timber.e("Recreating tasks: ${resourceToSave.size}") + } else { + val bundle = Bundle() + bundle.setType(Bundle.BundleType.TRANSACTION) + bundle.entry.addAll( + tasks.map { task -> + val taskId = task.outcomeReference.firstOrNull()?.extractId() + Bundle.BundleEntryComponent().apply { + request = + Bundle.BundleEntryRequestComponent().apply { + method = Bundle.HTTPVerb.GET + url = "${ResourceType.Task.name}/$taskId" + } + } + }, + ) + + val tasksToRecreate = mutableListOf() + val existingTasks = mutableListOf() + + val resBundle = fhirClient.transaction().withBundle(bundle).execute() + for ((index, entry) in resBundle.entry.withIndex()) { + if (entry.hasResource()) { + existingTasks.add(entry.resource as Task) + } else { + tasksToRecreate.add(tasks[index]) + } + } + resourceToSave.addAll(existingTasks) + if (tasksToRecreate.isNotEmpty()) { + resourceToSave.addAll( + tasksToRecreate.map { + createGenericTask( + patientId = patientId, + activity = it, + carePlan = carePlan, + ) + }, + ) + } + Timber.e("Task on server: ${existingTasks.size}, Tasks to recreate: ${tasksToRecreate.size}") + } + + fhirEngine.create(*resourceToSave.toTypedArray()) + } + + private fun createGenericTask( + patientId: String, + activity: CarePlanActivityComponent, + carePlan: CarePlan, + ): Task { + val questId = activity.detail.code.coding.firstOrNull()?.code + val patientReference = Reference().apply { reference = "Patient/$patientId" } + val questRef = Reference().apply { reference = questId } + + val period = + Period().apply { + start = Date() + end = Date() + } + + val task = + Task().apply { + id = activity.outcomeReference.firstOrNull()?.extractId() + status = activity.detail.status.toTaskStatus() + intent = Task.TaskIntent.PLAN + priority = Task.TaskPriority.ROUTINE + description = activity.detail.description + authoredOn = Date() + lastModified = Date() + `for` = patientReference + executionPeriod = period + requester = carePlan.author + owner = carePlan.author + reasonReference = questRef + } + + val meta = + Meta().apply { + tag = + carePlan.meta.tag + .filter { + it.system != RESOURCE_CREATED_ON_TAG_SYSTEM && it.system != CARE_PLAN_REFERENCE_SYSTEM + } + .toMutableList() + tag.add( + Coding().apply { + system = CARE_PLAN_REFERENCE_SYSTEM + code = "CarePlan/${carePlan.id}" + display = carePlan.title + }, + ) + tag.add( + Coding().apply { + system = QUESTIONNAIRE_REFERENCE_SYSTEM + code = questId + }, + ) + } + + task.meta = meta + task.generateCreatedOn() + return task + } +} diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/di/NetworkModule.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/di/NetworkModule.kt index 13712c20c2..20e5208fb7 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/di/NetworkModule.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/di/NetworkModule.kt @@ -17,7 +17,9 @@ package org.smartregister.fhircore.engine.di import ca.uhn.fhir.context.FhirContext +import ca.uhn.fhir.okhttp.client.OkHttpRestfulClientFactory import ca.uhn.fhir.parser.IParser +import ca.uhn.fhir.rest.client.api.IGenericClient import com.google.gson.Gson import com.google.gson.GsonBuilder import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory @@ -166,6 +168,19 @@ class NetworkModule { fun provideFhirResourceService(@RegularRetrofit retrofit: Retrofit): FhirResourceService = retrofit.create(FhirResourceService::class.java) + @Provides + fun providesGenericFhirClient( + @WithAuthorizationOkHttpClientQualifier okHttpClient: OkHttpClient, + configService: ConfigService, + ): IGenericClient { + val factory = OkHttpRestfulClientFactory() + val ctx = FhirContext.forR4() + factory.fhirContext = ctx + factory.setHttpClient(okHttpClient) + ctx.restfulClientFactory = factory + return ctx.newRestfulGenericClient(configService.provideAuthConfiguration().fhirServerBaseUrl) + } + companion object { const val TIMEOUT_DURATION = 120L const val AUTHORIZATION = "Authorization" diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/domain/model/CarePlanTask.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/domain/model/CarePlanTask.kt new file mode 100644 index 0000000000..7f6d27e6d6 --- /dev/null +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/domain/model/CarePlanTask.kt @@ -0,0 +1,24 @@ +/* + * Copyright 2021 Ona Systems, Inc + * + * 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 org.smartregister.fhircore.engine.domain.model + +import org.hl7.fhir.r4.model.CarePlan + +data class CarePlanTask( + val task: CarePlan.CarePlanActivityComponent, + val taskExists: Boolean, +) diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/domain/model/ProfileData.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/domain/model/ProfileData.kt index 474acee2b4..cb3846ab5a 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/domain/model/ProfileData.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/domain/model/ProfileData.kt @@ -96,7 +96,8 @@ sealed class ProfileData(open val logicalId: String, open val name: String) { val addressDistrict: String = "", val addressTracingCatchment: String = "", val addressPhysicalLocator: String = "", - val tasks: List = listOf(), + val hasMissingTasks: Boolean = false, + val tasks: List = listOf(), val chwAssigned: Reference, val healthStatus: HealthStatus, val phoneContacts: List = listOf(), diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/ui/settings/SettingsScreen.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/ui/settings/SettingsScreen.kt index 39bf3e3ddf..f4e6b3f8da 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/ui/settings/SettingsScreen.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/ui/settings/SettingsScreen.kt @@ -29,13 +29,11 @@ import androidx.compose.material.Scaffold import androidx.compose.material.Text import androidx.compose.material.TopAppBar import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.rounded.Logout import androidx.compose.material.icons.filled.ArrowBack -import androidx.compose.material.icons.outlined.Upload -import androidx.compose.material.icons.rounded.BugReport import androidx.compose.material.icons.rounded.CleaningServices -import androidx.compose.material.icons.rounded.Logout import androidx.compose.material.icons.rounded.Report -import androidx.compose.material.icons.rounded.Sync +import androidx.compose.material.icons.rounded.Task import androidx.compose.material.icons.rounded.Upload import androidx.compose.material.rememberModalBottomSheetState import androidx.compose.runtime.Composable @@ -53,6 +51,7 @@ import me.zhanghai.compose.preference.PreferenceCategory import me.zhanghai.compose.preference.ProvidePreferenceLocals import me.zhanghai.compose.preference.getPreferenceFlow import me.zhanghai.compose.preference.listPreference +import me.zhanghai.compose.preference.switchPreference import org.smartregister.fhircore.engine.R import org.smartregister.fhircore.engine.configuration.preferences.SyncUploadStrategy import org.smartregister.fhircore.engine.ui.settings.views.DevMenu @@ -117,12 +116,12 @@ fun SettingsScreen( // clickListener = settingsViewModel::fetchPractitionerDetails, // modifier = modifier, // ) - UserProfileRow( - icon = Icons.Rounded.Sync, - text = stringResource(id = R.string.sync), - clickListener = settingsViewModel::runSync, - modifier = modifier, - ) + // UserProfileRow( + // icon = Icons.Rounded.Sync, + // text = stringResource(id = R.string.sync), + // clickListener = settingsViewModel::runSync, + // modifier = modifier, + // ) UserProfileRow( icon = Icons.Rounded.Report, text = stringResource(R.string.reports), @@ -130,37 +129,17 @@ fun SettingsScreen( modifier = modifier, ) UserProfileRow( - icon = Icons.Rounded.BugReport, - text = stringResource(R.string.dev_menu), + icon = Icons.Rounded.Task, + text = stringResource(R.string.background_taks), clickListener = { scope.launch { devMenuSheetState.show() } }, modifier = modifier, ) - UserProfileRow( - icon = Icons.Rounded.Logout, - text = stringResource(id = R.string.logout), - clickListener = { settingsViewModel.logoutUser(context) }, - modifier = modifier, - ) - - val timestamp = settingsViewModel.sharedPreferences.read(LAST_PURGE_KEY.name, 0L) - val simpleDateFormat = - SimpleDateFormat(SYNC_TIMESTAMP_OUTPUT_FORMAT, Locale.getDefault()) - val text = context.resources.getString(R.string.last_purge) - val dateFormat = simpleDateFormat.format(timestamp) - - if (timestamp > 0L) { - UserProfileRow( - icon = Icons.Rounded.CleaningServices, - text = "$text: $dateFormat", - clickable = false, - modifier = modifier, - ) - } } item { Divider(color = DividerColor, modifier = Modifier.padding(vertical = 10.dp)) PreferenceCategory(title = { Text(text = "Preferences") }) } + listPreference( key = SharedPreferenceKey.SYNC_UPLOAD_STRATEGY.name, defaultValue = SyncUploadStrategy.Default.name, @@ -179,6 +158,41 @@ fun SettingsScreen( }, values = SyncUploadStrategy.entries.map { it.name }, ) + + switchPreference( + key = SharedPreferenceKey.PATIENT_FIX_TYPE.name, + defaultValue = false, + title = { Text(text = "Fix patients offline") }, + ) + + item { + Divider(color = DividerColor, modifier = Modifier.padding(vertical = 10.dp)) + PreferenceCategory(title = { Text(text = "Others") }) + } + item { + UserProfileRow( + icon = Icons.AutoMirrored.Rounded.Logout, + text = stringResource(id = R.string.logout), + clickListener = { settingsViewModel.logoutUser(context) }, + modifier = modifier, + ) + } + item { + val timestamp = settingsViewModel.sharedPreferences.read(LAST_PURGE_KEY.name, 0L) + val simpleDateFormat = + SimpleDateFormat(SYNC_TIMESTAMP_OUTPUT_FORMAT, Locale.getDefault()) + val text = context.resources.getString(R.string.last_purge) + val dateFormat = simpleDateFormat.format(timestamp) + + if (timestamp > 0L) { + UserProfileRow( + icon = Icons.Rounded.CleaningServices, + text = "$text: $dateFormat", + clickable = false, + modifier = modifier, + ) + } + } } } } diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/ui/settings/views/DevMenu.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/ui/settings/views/DevMenu.kt index 4d6726c851..0ccb06ef5f 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/ui/settings/views/DevMenu.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/ui/settings/views/DevMenu.kt @@ -31,8 +31,10 @@ import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.work.WorkInfo +import org.smartregister.fhircore.engine.R import org.smartregister.fhircore.engine.domain.util.DataLoadState import org.smartregister.fhircore.engine.ui.settings.DevViewModel import org.smartregister.fhircore.engine.util.annotation.ExcludeFromJacocoGeneratedReport @@ -45,12 +47,12 @@ fun DevMenu(viewModel: DevViewModel) { val appointmentList by viewModel.observeMissedAppointment(context).collectAsState(listOf()) val interruptedList by viewModel.observeInterrupted(context).collectAsState(listOf()) val resourcePurger by viewModel.observeResourcePurgerWorker(context).collectAsState(listOf()) - val cleanState by viewModel.cleanCorruptedState.collectAsState() + // val cleanState by viewModel.cleanCorruptedState.collectAsState() Column( modifier = Modifier.padding(16.dp).padding(vertical = 20.dp).fillMaxWidth(), ) { - SectionTitle(text = "Developer Options") + SectionTitle(text = stringResource(id = R.string.background_taks)) UserProfileRow( iconAlt = { WorkerStateIcon(states = missedTasks) }, text = "Run missed task worker", @@ -71,11 +73,11 @@ fun DevMenu(viewModel: DevViewModel) { text = "Run Resource Purger Worker", clickListener = @ExcludeFromJacocoGeneratedReport { viewModel.resourcePurger(context) }, ) - UserProfileRow( - iconAlt = { LoadableStateIcon(cleanState) }, - text = "Run Clean corrupted resources", - clickListener = @ExcludeFromJacocoGeneratedReport { viewModel.clearCorruptedEvents() }, - ) + // UserProfileRow( + // iconAlt = { LoadableStateIcon(cleanState) }, + // text = "Run Clean corrupted resources", + // clickListener = @ExcludeFromJacocoGeneratedReport { viewModel.clearCorruptedEvents() }, + // ) } } diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/util/SharedPreferenceKey.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/util/SharedPreferenceKey.kt index d9aa196d1a..2783f1da73 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/util/SharedPreferenceKey.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/util/SharedPreferenceKey.kt @@ -31,4 +31,5 @@ enum class SharedPreferenceKey { LAST_PURGE_KEY, USER_CLAIM_INFO, SYNC_UPLOAD_STRATEGY, + PATIENT_FIX_TYPE, } diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/util/extension/CarePlanExtension.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/util/extension/CarePlanExtension.kt index 8d54de656c..b9ad510b23 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/util/extension/CarePlanExtension.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/util/extension/CarePlanExtension.kt @@ -94,3 +94,16 @@ fun CarePlan.CarePlanStatus.toCoding() = Coding(this.system, this.toCode(), this fun CarePlan.isLastTask(task: Task) = this.activity.last()?.outcomeReference?.last()?.extractId() == task.logicalId + +fun CarePlan.CarePlanActivityStatus.toTaskStatus(): Task.TaskStatus { + return when (this) { + CarePlan.CarePlanActivityStatus.STOPPED -> Task.TaskStatus.FAILED + CarePlan.CarePlanActivityStatus.CANCELLED -> Task.TaskStatus.CANCELLED + CarePlan.CarePlanActivityStatus.NOTSTARTED -> Task.TaskStatus.READY + CarePlan.CarePlanActivityStatus.COMPLETED -> Task.TaskStatus.COMPLETED + CarePlan.CarePlanActivityStatus.ONHOLD -> Task.TaskStatus.ONHOLD + CarePlan.CarePlanActivityStatus.INPROGRESS -> Task.TaskStatus.INPROGRESS + CarePlan.CarePlanActivityStatus.ENTEREDINERROR -> Task.TaskStatus.ENTEREDINERROR + else -> Task.TaskStatus.NULL + } +} diff --git a/android/engine/src/main/res/values/strings.xml b/android/engine/src/main/res/values/strings.xml index caaa7e898d..a36eeb485c 100644 --- a/android/engine/src/main/res/values/strings.xml +++ b/android/engine/src/main/res/values/strings.xml @@ -169,4 +169,5 @@ Ok Opening form... Visit + Backgroung Tasks diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/navigation/MainNavigationScreen.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/navigation/MainNavigationScreen.kt index ec637ff748..bf55e32e40 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/navigation/MainNavigationScreen.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/navigation/MainNavigationScreen.kt @@ -77,6 +77,12 @@ sealed class MainNavigationScreen( route = "patientProfileRoute", ) + data object FixPatientProfile : + MainNavigationScreen( + titleResource = R.string.fix_patient, + route = "fixPatientProfileRoute", + ) + data object TracingProfile : MainNavigationScreen( titleResource = org.smartregister.fhircore.engine.R.string.profile, @@ -106,6 +112,7 @@ sealed class MainNavigationScreen( Reports, Settings, PatientProfile, + FixPatientProfile, PatientGuardians, FamilyProfile, ViewChildContacts, diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/main/AppMainScreen.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/main/AppMainScreen.kt index ee0dac75ca..e78a8310fb 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/main/AppMainScreen.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/main/AppMainScreen.kt @@ -25,9 +25,7 @@ import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.Scaffold import androidx.compose.material.rememberScaffoldState import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.hilt.navigation.compose.hiltViewModel @@ -49,6 +47,8 @@ import org.smartregister.fhircore.quest.ui.counters.CountersScreen import org.smartregister.fhircore.quest.ui.family.profile.FamilyProfileScreen import org.smartregister.fhircore.quest.ui.insights.InsightsScreen import org.smartregister.fhircore.quest.ui.main.components.AppDrawer +import org.smartregister.fhircore.quest.ui.patient.fix.FixPatientScreen +import org.smartregister.fhircore.quest.ui.patient.fix.FixPatientViewModel import org.smartregister.fhircore.quest.ui.patient.profile.PatientProfileScreen import org.smartregister.fhircore.quest.ui.patient.profile.childcontact.ChildContactsProfileScreen import org.smartregister.fhircore.quest.ui.patient.profile.guardians.GuardianRelatedPersonProfileScreen @@ -199,6 +199,14 @@ private fun AppMainNavigationGraph( ) { PatientProfileScreen(navController = navController, appMainViewModel = appMainViewModel) } + MainNavigationScreen.FixPatientProfile -> + composable( + route = + "${it.route}${NavigationArg.routePathsOf(includeCommonArgs = true, NavigationArg.PATIENT_ID, FixPatientViewModel.NAVIGATION_ARG_START, FixPatientViewModel.NAVIGATION_ARG_CARE_PLAN)}", + arguments = commonNavArgs.plus(patientIdNavArgument()), + ) { + FixPatientScreen(navController = navController, appMainViewModel = appMainViewModel) + } MainNavigationScreen.TracingProfile -> composable( route = diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/patient/fix/FixPatientScreen.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/patient/fix/FixPatientScreen.kt new file mode 100644 index 0000000000..651a8d0095 --- /dev/null +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/patient/fix/FixPatientScreen.kt @@ -0,0 +1,145 @@ +/* + * Copyright 2021 Ona Systems, Inc + * + * 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 org.smartregister.fhircore.quest.ui.patient.fix + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material.Button +import androidx.compose.material.CircularProgressIndicator +import androidx.compose.material.Icon +import androidx.compose.material.IconButton +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Scaffold +import androidx.compose.material.Text +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.navigation.NavHostController +import org.smartregister.fhircore.engine.domain.util.DataLoadState +import org.smartregister.fhircore.quest.R +import org.smartregister.fhircore.quest.ui.main.AppMainViewModel + +@Composable +fun FixPatientScreen( + viewModel: FixPatientViewModel = hiltViewModel(), + navController: NavHostController, + appMainViewModel: AppMainViewModel, +) { + val state by viewModel.screenState.collectAsState() + val fixState by viewModel.fixState.collectAsState() + + LaunchedEffect(fixState) { + if (fixState is DataLoadState.Success || fixState is DataLoadState.Error) { + appMainViewModel.onTaskComplete(System.currentTimeMillis().toString()) + } + } + + Scaffold( + topBar = { + Column( + modifier = Modifier.fillMaxWidth().background(MaterialTheme.colors.primary), + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.padding(vertical = 8.dp), + ) { + IconButton(onClick = { navController.popBackStack() }) { + Icon( + Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = "Back", + tint = Color.White, + ) + } + Text( + text = stringResource(id = R.string.fix_patient), + fontSize = 20.sp, + color = Color.White, + modifier = Modifier.weight(1f), + ) + } + } + }, + ) { innerPadding -> + Box(modifier = Modifier.padding(innerPadding)) { + if (state is FixPatientState.ActionStart) { + PatientFixActionStartContainer(fixState, { navController.navigateUp() }) { + viewModel.startFix() + } + } else { + PatientFixAllActionContainer() + } + } + } +} + +@Composable fun PatientFixAllActionContainer() {} + +@Composable +fun PatientFixActionStartContainer( + state: DataLoadState, + close: () -> Unit, + retry: () -> Unit, +) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + modifier = Modifier.fillMaxSize(), + ) { + when (state) { + is DataLoadState.Error -> + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + ) { + Text(text = "Something went wrong while fetching data..") + Button(onClick = retry) { Text(text = "Retry") } + } + is DataLoadState.Success -> { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + ) { + Text(text = "Completed!") + Button(onClick = close) { Text(text = "Close") } + } + } + else -> { + Text( + text = "Fixing Patient", + style = MaterialTheme.typography.h4.copy(color = Color.Gray), + ) + CircularProgressIndicator() + } + } + } +} diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/patient/fix/FixPatientState.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/patient/fix/FixPatientState.kt new file mode 100644 index 0000000000..1a6ed58479 --- /dev/null +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/patient/fix/FixPatientState.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2021 Ona Systems, Inc + * + * 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 org.smartregister.fhircore.quest.ui.patient.fix + +sealed class FixPatientState { + data object AllActions : FixPatientState() + + data object ActionStart : FixPatientState() +} diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/patient/fix/FixPatientViewModel.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/patient/fix/FixPatientViewModel.kt new file mode 100644 index 0000000000..748339a7bd --- /dev/null +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/patient/fix/FixPatientViewModel.kt @@ -0,0 +1,83 @@ +/* + * Copyright 2021 Ona Systems, Inc + * + * 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 org.smartregister.fhircore.quest.ui.patient.fix + +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.launch +import org.smartregister.fhircore.engine.data.remote.fhir.resource.ResourceFixerService +import org.smartregister.fhircore.engine.domain.util.DataLoadState +import org.smartregister.fhircore.quest.navigation.NavigationArg +import timber.log.Timber + +@HiltViewModel +class FixPatientViewModel +@Inject +constructor( + savedStateHandle: SavedStateHandle, + private val resourceFixerService: ResourceFixerService, +) : ViewModel() { + val patientId = savedStateHandle.get(NavigationArg.PATIENT_ID) ?: "" + private val carePlanId = savedStateHandle.get(NAVIGATION_ARG_CARE_PLAN) + private val startState = + savedStateHandle.get(NAVIGATION_ARG_START)?.let { FixStartState.valueOf(it) } + ?: FixStartState.All + val screenState = MutableStateFlow(FixPatientState.AllActions) + val fixState = MutableStateFlow>(DataLoadState.Idle) + + init { + viewModelScope.launch { + if (startState == FixStartState.StartFix) { + screenState.emit(FixPatientState.ActionStart) + startFix() + } else { + screenState.emit(FixPatientState.AllActions) + } + } + } + + fun startFix() { + viewModelScope.launch(Dispatchers.IO) { + try { + fixState.emit(DataLoadState.Loading) + if (carePlanId == null) { + return@launch + } + resourceFixerService.fixCurrentCarePlan(patientId, carePlanId) + fixState.emit(DataLoadState.Success(true)) + } catch (e: Exception) { + Timber.e(e) + fixState.value = DataLoadState.Error(e) + } + } + } + + companion object { + const val NAVIGATION_ARG_START = "fix_start_state" + const val NAVIGATION_ARG_CARE_PLAN = "fix_care_plan" + } +} + +enum class FixStartState { + All, + StartFix, +} diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/patient/profile/PatientProfileEvent.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/patient/profile/PatientProfileEvent.kt index 332e589cd7..4ac3319296 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/patient/profile/PatientProfileEvent.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/patient/profile/PatientProfileEvent.kt @@ -41,4 +41,6 @@ sealed class PatientProfileEvent { data class OpenChildProfile(val patientId: String, val navController: NavHostController) : PatientProfileEvent() + + data class OpenPatientFixer(val navController: NavHostController) : PatientProfileEvent() } diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/patient/profile/PatientProfileScreen.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/patient/profile/PatientProfileScreen.kt index b6dfed7b60..65184bdd17 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/patient/profile/PatientProfileScreen.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/patient/profile/PatientProfileScreen.kt @@ -77,6 +77,7 @@ import org.smartregister.fhircore.quest.ui.main.AppMainViewModel import org.smartregister.fhircore.quest.ui.patient.profile.components.PersonalData import org.smartregister.fhircore.quest.ui.patient.profile.components.ProfileActionableItem import org.smartregister.fhircore.quest.ui.patient.profile.components.ProfileCard +import org.smartregister.fhircore.quest.ui.patient.profile.components.ProfileErrorCard import org.smartregister.fhircore.quest.ui.shared.models.PatientProfileViewSection @Composable @@ -204,6 +205,10 @@ fun PatientProfileScreen( PersonalData(profileViewData) } + ProfileErrorCard(profileViewData) { + patientProfileViewModel.onEvent(PatientProfileEvent.OpenPatientFixer(navController)) + } + // Patient tasks: List of tasks for the patients if (profileViewData.tasks.isNotEmpty()) { val appointmentDate = profileViewData.currentCarePlan?.period?.end diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/patient/profile/PatientProfileViewModel.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/patient/profile/PatientProfileViewModel.kt index 746034a5b5..82350c62a4 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/patient/profile/PatientProfileViewModel.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/patient/profile/PatientProfileViewModel.kt @@ -29,6 +29,7 @@ import androidx.paging.Pager import androidx.paging.PagingConfig import androidx.paging.PagingData import androidx.paging.cachedIn +import com.google.android.fhir.datacapture.extensions.logicalId import com.google.android.fhir.sync.SyncJobStatus import dagger.hilt.android.lifecycle.HiltViewModel import javax.inject.Inject @@ -60,6 +61,8 @@ import org.smartregister.fhircore.quest.navigation.NavigationArg import org.smartregister.fhircore.quest.navigation.OverflowMenuFactory import org.smartregister.fhircore.quest.navigation.OverflowMenuHost import org.smartregister.fhircore.quest.ui.family.remove.member.RemoveFamilyMemberQuestionnaireActivity +import org.smartregister.fhircore.quest.ui.patient.fix.FixPatientViewModel +import org.smartregister.fhircore.quest.ui.patient.fix.FixStartState import org.smartregister.fhircore.quest.ui.patient.profile.childcontact.ChildContactPagingSource import org.smartregister.fhircore.quest.ui.patient.profile.tranfer.TransferOutActivity import org.smartregister.fhircore.quest.ui.shared.models.ProfileViewData @@ -173,7 +176,7 @@ constructor( hivPatientProfileData.copy( tasks = hivPatientProfileData.tasks.filter { - it.isGuardianVisit(applicationConfiguration.taskFilterTagViaMetaCodingSystem) + it.task.isGuardianVisit(applicationConfiguration.taskFilterTagViaMetaCodingSystem) }, ) _patientProfileViewDataFlow.value = @@ -356,6 +359,24 @@ constructor( route = MainNavigationScreen.PatientProfile.route + urlParams, ) } + is PatientProfileEvent.OpenPatientFixer -> { + if (patientProfileData != null && patientProfileData is ProfileData.HivProfileData) { + val urlParams = + NavigationArg.bindArgumentsOf( + Pair(NavigationArg.FEATURE, AppFeature.PatientManagement.name), + Pair(NavigationArg.HEALTH_MODULE, healthModule), + Pair(NavigationArg.PATIENT_ID, patientId), + Pair(FixPatientViewModel.NAVIGATION_ARG_START, FixStartState.StartFix.name), + Pair( + FixPatientViewModel.NAVIGATION_ARG_CARE_PLAN, + (patientProfileData as ProfileData.HivProfileData).currentCarePlan?.logicalId, + ), + ) + event.navController.navigate( + route = MainNavigationScreen.FixPatientProfile.route + urlParams, + ) + } + } } } diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/patient/profile/components/ProfileActionableItem.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/patient/profile/components/ProfileActionableItem.kt index 4d8e82d2e9..e4750f150d 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/patient/profile/components/ProfileActionableItem.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/patient/profile/components/ProfileActionableItem.kt @@ -30,9 +30,11 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.ButtonDefaults import androidx.compose.material.Divider import androidx.compose.material.Icon +import androidx.compose.material.MaterialTheme import androidx.compose.material.OutlinedButton import androidx.compose.material.Text import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Error import androidx.compose.material.icons.outlined.ChevronRight import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment @@ -69,7 +71,26 @@ fun ProfileActionableItem( verticalAlignment = Alignment.CenterVertically, ) { if (patientProfileRowItem.title.isEmpty() && patientProfileRowItem.subtitle.isEmpty()) { - ActionButton(patientProfileRowItem, modifier = modifier.fillMaxWidth(1f), onActionClick) + Row(verticalAlignment = Alignment.CenterVertically) { + if (!patientProfileRowItem.taskExists) { + Box( + modifier + .padding(end = 8.dp) + .clip(RoundedCornerShape(6.dp)) + .background(MaterialTheme.colors.error) + .padding(8.dp), + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(2.dp), + ) { + Icon(Icons.Filled.Error, contentDescription = "", tint = MaterialTheme.colors.onError) + Text(text = "Missing Task", color = MaterialTheme.colors.onError) + } + } + } + ActionButton(patientProfileRowItem, modifier = modifier.fillMaxWidth(1f), onActionClick) + } } else { Row(verticalAlignment = Alignment.CenterVertically) { if ( @@ -258,6 +279,20 @@ fun ProfileActionableItemForVisitPreview() { ), onActionClick = { _, _ -> }, ) + Divider() + ProfileActionableItem( + PatientProfileRowItem( + id = "3", + title = "", + titleIcon = R.drawable.ic_pregnant, + subtitle = "", + profileViewSection = PatientProfileViewSection.TASKS, + actionButtonColor = OverdueColor, + actionButtonText = "Malaria medicine", + taskExists = false, + ), + onActionClick = { _, _ -> }, + ) } } diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/patient/profile/components/ProfileErrorCard.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/patient/profile/components/ProfileErrorCard.kt new file mode 100644 index 0000000000..c74fc7d113 --- /dev/null +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/patient/profile/components/ProfileErrorCard.kt @@ -0,0 +1,65 @@ +/* + * Copyright 2021 Ona Systems, Inc + * + * 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 org.smartregister.fhircore.quest.ui.patient.profile.components + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material.Button +import androidx.compose.material.Card +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import org.smartregister.fhircore.quest.ui.shared.models.ProfileViewData + +@Composable +fun ProfileErrorCard( + profileViewData: ProfileViewData.PatientProfileViewData, + navigate: () -> Unit, +) { + if (profileViewData.hasMissingTasks) { + Card( + backgroundColor = MaterialTheme.colors.error, + modifier = Modifier.fillMaxWidth().padding(8.dp), + ) { + Column(verticalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier.padding(8.dp)) { + Text(text = "Error", style = MaterialTheme.typography.h6) + Text(text = "Seems like this patient has some errors") + Button(onClick = navigate) { Text(text = "Fix patient") } + } + } + } +} + +@Preview +@Composable +private fun ProfileErrorCardPreview() { + Box(Modifier.padding(16.dp)) { + ProfileErrorCard( + profileViewData = + ProfileViewData.PatientProfileViewData( + hasMissingTasks = true, + ), + navigate = {}, + ) + } +} diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/shared/models/PatientProfileRowItem.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/shared/models/PatientProfileRowItem.kt index ddb91c3d19..345e2c0cf1 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/shared/models/PatientProfileRowItem.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/shared/models/PatientProfileRowItem.kt @@ -38,4 +38,5 @@ data class PatientProfileRowItem( val actionButtonText: String? = null, val showAngleRightIcon: Boolean = false, val showDot: Boolean = false, + val taskExists: Boolean = true, ) diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/shared/models/ProfileViewData.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/shared/models/ProfileViewData.kt index 4ac5b33442..38433300a0 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/shared/models/ProfileViewData.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/shared/models/ProfileViewData.kt @@ -46,6 +46,7 @@ sealed class ProfileViewData( val age: String = "", val dob: String = "", val showListsHighlights: Boolean = true, + val hasMissingTasks: Boolean = false, val tasks: List = emptyList(), val forms: List = emptyList(), val medicalHistoryData: List = emptyList(), diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/util/mappers/ProfileViewDataMapper.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/util/mappers/ProfileViewDataMapper.kt index 8fe46ea4ea..ebec1ca9a1 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/util/mappers/ProfileViewDataMapper.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/util/mappers/ProfileViewDataMapper.kt @@ -94,26 +94,28 @@ class ProfileViewDataMapper @Inject constructor(@ApplicationContext val context: guardians = inputModel.guardians, currentCarePlan = inputModel.currentCarePlan, visitNumber = inputModel.currentCarePlan?.extractVisitNumber(), + hasMissingTasks = inputModel.hasMissingTasks, tasks = inputModel.tasks.map { PatientProfileRowItem( - id = it.outcomeReference.first().extractId(), - actionFormId = if (it.canBeCompleted()) it.getQuestionnaire() else null, + id = it.task.outcomeReference.first().extractId(), + actionFormId = if (it.task.canBeCompleted()) it.task.getQuestionnaire() else null, title = "", // it.description, subtitle = "", // context.getString(R.string.due_on, // it.executionPeriod.start.makeItReadable()), + taskExists = it.taskExists, profileViewSection = PatientProfileViewSection.TASKS, actionButtonIcon = - if (it.detail.status == CarePlan.CarePlanActivityStatus.COMPLETED) { + if (it.task.detail.status == CarePlan.CarePlanActivityStatus.COMPLETED) { Icons.Filled.Check } else Icons.Filled.Add, actionIconColor = - if (it.detail.status == CarePlan.CarePlanActivityStatus.COMPLETED) { + if (it.task.detail.status == CarePlan.CarePlanActivityStatus.COMPLETED) { SuccessColor - } else it.detail.status.retrieveColorCode(), - actionButtonColor = it.detail.status.retrieveColorCode(), - actionButtonText = it.getQuestionnaireName(), - subtitleStatus = it.detail.status.name, + } else it.task.detail.status.retrieveColorCode(), + actionButtonColor = it.task.detail.status.retrieveColorCode(), + actionButtonText = it.task.getQuestionnaireName(), + subtitleStatus = it.task.detail.status.name, ) }, practitioners = inputModel.practitioners, diff --git a/android/quest/src/main/res/values/strings.xml b/android/quest/src/main/res/values/strings.xml index caa8f98b81..4788da6523 100644 --- a/android/quest/src/main/res/values/strings.xml +++ b/android/quest/src/main/res/values/strings.xml @@ -146,4 +146,5 @@ Appointments Insights RAM Available + Fix Patient