From 83dcf5ba45a1a54772720f9001806936ae68a2ca Mon Sep 17 00:00:00 2001 From: Rkareko Date: Fri, 22 Nov 2024 10:33:26 +0300 Subject: [PATCH 01/21] Add delete draft workflow --- .../workflow/ApplicationWorkflow.kt | 3 ++ .../engine/src/main/res/values/strings.xml | 2 + .../quest/navigation/MainNavigationScreen.kt | 5 +++ .../quest/navigation/NavigationArg.kt | 1 + .../quest/ui/dialog/AlertDialogFragment.kt | 40 +++++++++++++++++++ .../quest/util/extensions/ConfigExtensions.kt | 7 ++++ .../res/navigation/application_nav_graph.xml | 8 ++++ 7 files changed, 66 insertions(+) create mode 100644 android/quest/src/main/java/org/smartregister/fhircore/quest/ui/dialog/AlertDialogFragment.kt diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/workflow/ApplicationWorkflow.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/workflow/ApplicationWorkflow.kt index ea58e771f56..de0ca594163 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/workflow/ApplicationWorkflow.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/workflow/ApplicationWorkflow.kt @@ -62,4 +62,7 @@ enum class ApplicationWorkflow { /** A workflow to launch pdf generation */ LAUNCH_PDF_GENERATION, + + /** A workflow to launch delete draft */ + LAUNCH_DELETE_DRAFT_FORM, } diff --git a/android/engine/src/main/res/values/strings.xml b/android/engine/src/main/res/values/strings.xml index 9003f2e8512..9df1f37f553 100644 --- a/android/engine/src/main/res/values/strings.xml +++ b/android/engine/src/main/res/values/strings.xml @@ -76,6 +76,8 @@ Given details have validation errors. Resolve errors and submit again Validation Failed OK + Open draft + Delete draft Username Password Forgot Password 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 a3ff11c5cc3..8c476e8eac5 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 @@ -70,5 +70,10 @@ sealed class MainNavigationScreen( route = org.smartregister.fhircore.quest.R.id.summaryBottomSheetFragment, ) + data object AlertDialogFragment : + MainNavigationScreen( + route = org.smartregister.fhircore.quest.R.id.alertDialogFragment, + ) + fun eventId(id: String) = route.toString() + "_" + id } diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/navigation/NavigationArg.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/navigation/NavigationArg.kt index 618bf1823bc..78429178312 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/navigation/NavigationArg.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/navigation/NavigationArg.kt @@ -28,4 +28,5 @@ object NavigationArg { const val REPORT_ID = "reportId" const val PARAMS = "params" const val TOOL_BAR_HOME_NAVIGATION = "toolBarHomeNavigation" + const val QUESTIONNAIRE_CONFIG = "questionnaireConfig" } diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/dialog/AlertDialogFragment.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/dialog/AlertDialogFragment.kt new file mode 100644 index 00000000000..59782682d07 --- /dev/null +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/dialog/AlertDialogFragment.kt @@ -0,0 +1,40 @@ +package org.smartregister.fhircore.quest.ui.dialog + +import android.app.Dialog +import android.os.Bundle +import androidx.fragment.app.DialogFragment +import androidx.navigation.fragment.navArgs +import org.smartregister.fhircore.engine.ui.base.AlertDialogue +import org.smartregister.fhircore.quest.ui.shared.QuestionnaireHandler + +class AlertDialogFragment() : DialogFragment() { + + private val alertDialogFragmentArgs by navArgs() + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + return AlertDialogue.showCancelAlert( + context = requireContext(), + message = + org.smartregister.fhircore.engine.R.string + .questionnaire_in_progress_alert_back_pressed_message, + title = org.smartregister.fhircore.engine.R.string.questionnaire_alert_back_pressed_title, + confirmButtonListener = { + if (requireContext() is QuestionnaireHandler) { + (requireContext() as QuestionnaireHandler).launchQuestionnaire( + context = requireContext(), + questionnaireConfig = alertDialogFragmentArgs.questionnaireConfig, + actionParams = listOf(), + ) + } + }, + confirmButtonText = + org.smartregister.fhircore.engine.R.string.questionnaire_alert_open_draft_button_title, + neutralButtonListener = {}, + neutralButtonText = + org.smartregister.fhircore.engine.R.string.questionnaire_alert_neutral_button_title, + negativeButtonListener = { this.dismiss() }, + negativeButtonText = + org.smartregister.fhircore.engine.R.string.questionnaire_alert_delete_draft_button_title, + ) + } +} \ No newline at end of file diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/util/extensions/ConfigExtensions.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/util/extensions/ConfigExtensions.kt index 19e1e44dbf8..c4a2c2c29d6 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/util/extensions/ConfigExtensions.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/util/extensions/ConfigExtensions.kt @@ -236,6 +236,13 @@ fun ActionConfig.handleClickEvent( val appCompatActivity = (navController.context as AppCompatActivity) PdfLauncherFragment.launch(appCompatActivity, interpolatedPdfConfig.encodeJson()) } + ApplicationWorkflow.LAUNCH_DELETE_DRAFT_FORM -> { + val args = + bundleOf( + NavigationArg.QUESTIONNAIRE_CONFIG to actionConfig.questionnaire, + ) + navController.navigate(MainNavigationScreen.AlertDialogFragment.route, args) + } else -> return } } diff --git a/android/quest/src/main/res/navigation/application_nav_graph.xml b/android/quest/src/main/res/navigation/application_nav_graph.xml index 21bf25e2a1f..f73639b7cc6 100644 --- a/android/quest/src/main/res/navigation/application_nav_graph.xml +++ b/android/quest/src/main/res/navigation/application_nav_graph.xml @@ -110,4 +110,12 @@ android:name="org.smartregister.fhircore.quest.ui.bottomsheet.SummaryBottomSheetFragment" > + + + From 3085211f63b871bf0a02f7f0ba7e1a53ec310686 Mon Sep 17 00:00:00 2001 From: Rkareko Date: Fri, 22 Nov 2024 15:28:49 +0300 Subject: [PATCH 02/21] Add view model --- .../fhircore/engine/ui/base/AlertDialogue.kt | 2 +- .../engine/ui/base/AlertDialogueTest.kt | 2 +- .../quest/ui/dialog/AlertDialogFragment.kt | 77 ++++++++++++------- .../quest/ui/dialog/AlertDialogViewModel.kt | 36 +++++++++ .../ui/questionnaire/QuestionnaireActivity.kt | 2 +- 5 files changed, 89 insertions(+), 30 deletions(-) create mode 100644 android/quest/src/main/java/org/smartregister/fhircore/quest/ui/dialog/AlertDialogViewModel.kt diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/ui/base/AlertDialogue.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/ui/base/AlertDialogue.kt index bee9d7febde..3bc8c724c04 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/ui/base/AlertDialogue.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/ui/base/AlertDialogue.kt @@ -169,7 +169,7 @@ object AlertDialogue { ) } - fun showCancelAlert( + fun showThreeButtonAlert( context: Context, @StringRes message: Int, @StringRes title: Int? = null, diff --git a/android/engine/src/test/java/org/smartregister/fhircore/engine/ui/base/AlertDialogueTest.kt b/android/engine/src/test/java/org/smartregister/fhircore/engine/ui/base/AlertDialogueTest.kt index 401ef8cc2cc..838eae71189 100644 --- a/android/engine/src/test/java/org/smartregister/fhircore/engine/ui/base/AlertDialogueTest.kt +++ b/android/engine/src/test/java/org/smartregister/fhircore/engine/ui/base/AlertDialogueTest.kt @@ -143,7 +143,7 @@ class AlertDialogueTest : ActivityRobolectricTest() { @Test fun testShowCancelAlertShowsWithCorrectData() { - AlertDialogue.showCancelAlert( + AlertDialogue.showThreeButtonAlert( context = context, message = R.string.questionnaire_in_progress_alert_back_pressed_message, title = R.string.questionnaire_alert_back_pressed_title, diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/dialog/AlertDialogFragment.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/dialog/AlertDialogFragment.kt index 59782682d07..d3b73e6bf78 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/dialog/AlertDialogFragment.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/dialog/AlertDialogFragment.kt @@ -1,40 +1,63 @@ +/* + * Copyright 2021-2024 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.dialog import android.app.Dialog import android.os.Bundle import androidx.fragment.app.DialogFragment +import androidx.fragment.app.viewModels import androidx.navigation.fragment.navArgs +import dagger.hilt.android.AndroidEntryPoint import org.smartregister.fhircore.engine.ui.base.AlertDialogue import org.smartregister.fhircore.quest.ui.shared.QuestionnaireHandler +@AndroidEntryPoint class AlertDialogFragment() : DialogFragment() { - private val alertDialogFragmentArgs by navArgs() + private val alertDialogFragmentArgs by navArgs() + private val alertDialogViewModel by viewModels() - override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { - return AlertDialogue.showCancelAlert( + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + return AlertDialogue.showThreeButtonAlert( + context = requireContext(), + message = + org.smartregister.fhircore.engine.R.string + .questionnaire_in_progress_alert_back_pressed_message, + title = org.smartregister.fhircore.engine.R.string.questionnaire_alert_back_pressed_title, + confirmButtonListener = { + if (requireContext() is QuestionnaireHandler) { + (requireContext() as QuestionnaireHandler).launchQuestionnaire( context = requireContext(), - message = - org.smartregister.fhircore.engine.R.string - .questionnaire_in_progress_alert_back_pressed_message, - title = org.smartregister.fhircore.engine.R.string.questionnaire_alert_back_pressed_title, - confirmButtonListener = { - if (requireContext() is QuestionnaireHandler) { - (requireContext() as QuestionnaireHandler).launchQuestionnaire( - context = requireContext(), - questionnaireConfig = alertDialogFragmentArgs.questionnaireConfig, - actionParams = listOf(), - ) - } - }, - confirmButtonText = - org.smartregister.fhircore.engine.R.string.questionnaire_alert_open_draft_button_title, - neutralButtonListener = {}, - neutralButtonText = - org.smartregister.fhircore.engine.R.string.questionnaire_alert_neutral_button_title, - negativeButtonListener = { this.dismiss() }, - negativeButtonText = - org.smartregister.fhircore.engine.R.string.questionnaire_alert_delete_draft_button_title, - ) - } -} \ No newline at end of file + questionnaireConfig = alertDialogFragmentArgs.questionnaireConfig, + actionParams = listOf(), + ) + } + }, + confirmButtonText = + org.smartregister.fhircore.engine.R.string.questionnaire_alert_open_draft_button_title, + neutralButtonListener = {}, + neutralButtonText = + org.smartregister.fhircore.engine.R.string.questionnaire_alert_neutral_button_title, + negativeButtonListener = { + alertDialogViewModel.deleteDraft(alertDialogFragmentArgs.questionnaireConfig) + this.dismiss() + }, + negativeButtonText = + org.smartregister.fhircore.engine.R.string.questionnaire_alert_delete_draft_button_title, + ) + } +} diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/dialog/AlertDialogViewModel.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/dialog/AlertDialogViewModel.kt new file mode 100644 index 00000000000..a9d1b6cfa01 --- /dev/null +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/dialog/AlertDialogViewModel.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2021-2024 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.dialog + +import androidx.lifecycle.ViewModel +import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject +import org.smartregister.fhircore.engine.configuration.QuestionnaireConfig +import org.smartregister.fhircore.engine.data.local.DefaultRepository +import org.smartregister.fhircore.engine.util.DispatcherProvider + +@HiltViewModel +class AlertDialogViewModel +@Inject +constructor( + val defaultRepository: DefaultRepository, + val dispatcherProvider: DispatcherProvider, +) : ViewModel() { + fun deleteDraft(questionnaireConfig: QuestionnaireConfig?) { + TODO("Add implementation") + } +} diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireActivity.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireActivity.kt index 2bb53a31e05..a5f33554d03 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireActivity.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireActivity.kt @@ -351,7 +351,7 @@ class QuestionnaireActivity : BaseMultiLanguageActivity() { if (questionnaireConfig.isReadOnly()) { finish() } else if (questionnaireConfig.saveDraft) { - AlertDialogue.showCancelAlert( + AlertDialogue.showThreeButtonAlert( context = this, message = org.smartregister.fhircore.engine.R.string From 8f0ab2f2a928a6e080ef37eb4961910d1399c055 Mon Sep 17 00:00:00 2001 From: Rkareko Date: Mon, 25 Nov 2024 08:39:08 +0300 Subject: [PATCH 03/21] Add logic for soft deleteing drafts --- .../engine/data/local/DefaultRepository.kt | 44 +++++++++++++++++++ .../quest/ui/dialog/AlertDialogViewModel.kt | 25 ++++++++++- 2 files changed, 67 insertions(+), 2 deletions(-) diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/data/local/DefaultRepository.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/data/local/DefaultRepository.kt index 63c0e4fc664..d457ef68d9a 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/data/local/DefaultRepository.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/data/local/DefaultRepository.kt @@ -62,6 +62,8 @@ import org.hl7.fhir.r4.model.Group import org.hl7.fhir.r4.model.IdType import org.hl7.fhir.r4.model.Location import org.hl7.fhir.r4.model.Patient +import org.hl7.fhir.r4.model.Questionnaire +import org.hl7.fhir.r4.model.QuestionnaireResponse import org.hl7.fhir.r4.model.Reference import org.hl7.fhir.r4.model.RelatedPerson import org.hl7.fhir.r4.model.Resource @@ -1292,6 +1294,48 @@ constructor( ) .mapTo(ArrayDeque()) { it.resource } + /** + * This function searches and returns the latest [QuestionnaireResponse] for the given + * [resourceId] that was extracted from the [Questionnaire] identified as [questionnaireId]. + * Returns null if non is found. + */ + suspend fun searchQuestionnaireResponse( + resourceId: String, + resourceType: ResourceType, + questionnaireId: String, + encounterId: String?, + questionnaireResponseStatus: String? = null, + ): QuestionnaireResponse? { + val search = + Search(ResourceType.QuestionnaireResponse).apply { + filter( + QuestionnaireResponse.SUBJECT, + { value = resourceId.asReference(resourceType).reference }, + ) + filter( + QuestionnaireResponse.QUESTIONNAIRE, + { value = questionnaireId.asReference(ResourceType.Questionnaire).reference }, + ) + if (!encounterId.isNullOrBlank()) { + filter( + QuestionnaireResponse.ENCOUNTER, + { + value = + encounterId.extractLogicalIdUuid().asReference(ResourceType.Encounter).reference + }, + ) + } + if (!questionnaireResponseStatus.isNullOrBlank()) { + filter( + QuestionnaireResponse.STATUS, + { value = of(questionnaireResponseStatus) }, + ) + } + } + val questionnaireResponses: List = search(search) + return questionnaireResponses.maxByOrNull { it.meta.lastUpdated } + } + /** * A wrapper data class to hold search results. All related resources are flattened into one Map * including the nested related resources as required by the Rules Engine facts. diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/dialog/AlertDialogViewModel.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/dialog/AlertDialogViewModel.kt index a9d1b6cfa01..bfa1a5b0052 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/dialog/AlertDialogViewModel.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/dialog/AlertDialogViewModel.kt @@ -19,6 +19,7 @@ package org.smartregister.fhircore.quest.ui.dialog import androidx.lifecycle.ViewModel import dagger.hilt.android.lifecycle.HiltViewModel import javax.inject.Inject +import org.hl7.fhir.r4.model.QuestionnaireResponse.QuestionnaireResponseStatus import org.smartregister.fhircore.engine.configuration.QuestionnaireConfig import org.smartregister.fhircore.engine.data.local.DefaultRepository import org.smartregister.fhircore.engine.util.DispatcherProvider @@ -30,7 +31,27 @@ constructor( val defaultRepository: DefaultRepository, val dispatcherProvider: DispatcherProvider, ) : ViewModel() { - fun deleteDraft(questionnaireConfig: QuestionnaireConfig?) { - TODO("Add implementation") + suspend fun deleteDraft(questionnaireConfig: QuestionnaireConfig?) { + if ( + questionnaireConfig == null || + questionnaireConfig.resourceIdentifier.isNullOrBlank() || + questionnaireConfig.resourceType == null + ) { + return + } + + val questionnaireResponse = + defaultRepository.searchQuestionnaireResponse( + resourceId = questionnaireConfig.resourceIdentifier!!, + resourceType = questionnaireConfig.resourceType!!, + questionnaireId = questionnaireConfig.id, + encounterId = null, + questionnaireResponseStatus = QuestionnaireResponseStatus.INPROGRESS.toCode(), + ) + + if (questionnaireResponse != null) { + questionnaireResponse.status = QuestionnaireResponseStatus.STOPPED + defaultRepository.update(questionnaireResponse) + } } } From 319f62d95eeadc8f916990458b738a453ee40643 Mon Sep 17 00:00:00 2001 From: Rkareko Date: Mon, 25 Nov 2024 13:03:39 +0300 Subject: [PATCH 04/21] Run the search for QuestinnaireResponse in view model scope --- .../quest/ui/dialog/AlertDialogViewModel.kt | 31 ++++++++++++------- 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/dialog/AlertDialogViewModel.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/dialog/AlertDialogViewModel.kt index bfa1a5b0052..77160a7ec4e 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/dialog/AlertDialogViewModel.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/dialog/AlertDialogViewModel.kt @@ -17,8 +17,11 @@ package org.smartregister.fhircore.quest.ui.dialog import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import javax.inject.Inject +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import org.hl7.fhir.r4.model.QuestionnaireResponse.QuestionnaireResponseStatus import org.smartregister.fhircore.engine.configuration.QuestionnaireConfig import org.smartregister.fhircore.engine.data.local.DefaultRepository @@ -31,7 +34,7 @@ constructor( val defaultRepository: DefaultRepository, val dispatcherProvider: DispatcherProvider, ) : ViewModel() { - suspend fun deleteDraft(questionnaireConfig: QuestionnaireConfig?) { + fun deleteDraft(questionnaireConfig: QuestionnaireConfig?) { if ( questionnaireConfig == null || questionnaireConfig.resourceIdentifier.isNullOrBlank() || @@ -40,18 +43,22 @@ constructor( return } - val questionnaireResponse = - defaultRepository.searchQuestionnaireResponse( - resourceId = questionnaireConfig.resourceIdentifier!!, - resourceType = questionnaireConfig.resourceType!!, - questionnaireId = questionnaireConfig.id, - encounterId = null, - questionnaireResponseStatus = QuestionnaireResponseStatus.INPROGRESS.toCode(), - ) + viewModelScope.launch { + withContext(dispatcherProvider.io()) { + val questionnaireResponse = + defaultRepository.searchQuestionnaireResponse( + resourceId = questionnaireConfig.resourceIdentifier!!, + resourceType = questionnaireConfig.resourceType!!, + questionnaireId = questionnaireConfig.id, + encounterId = null, + questionnaireResponseStatus = QuestionnaireResponseStatus.INPROGRESS.toCode(), + ) - if (questionnaireResponse != null) { - questionnaireResponse.status = QuestionnaireResponseStatus.STOPPED - defaultRepository.update(questionnaireResponse) + if (questionnaireResponse != null) { + questionnaireResponse.status = QuestionnaireResponseStatus.STOPPED + defaultRepository.update(questionnaireResponse) + } + } } } } From d734a22f4e3139dd84b7a1b763a44611accd4815 Mon Sep 17 00:00:00 2001 From: Rkareko Date: Tue, 26 Nov 2024 09:17:02 +0300 Subject: [PATCH 05/21] Move searchQuestionnaireResponse function to default repository --- .../data/local/DefaultRepositoryTest.kt | 123 ++++++++++++++++++ .../questionnaire/QuestionnaireViewModel.kt | 48 +------ .../QuestionnaireViewModelTest.kt | 98 +------------- 3 files changed, 128 insertions(+), 141 deletions(-) diff --git a/android/engine/src/test/java/org/smartregister/fhircore/engine/data/local/DefaultRepositoryTest.kt b/android/engine/src/test/java/org/smartregister/fhircore/engine/data/local/DefaultRepositoryTest.kt index 4dcbb39ec1e..f27b62db614 100644 --- a/android/engine/src/test/java/org/smartregister/fhircore/engine/data/local/DefaultRepositoryTest.kt +++ b/android/engine/src/test/java/org/smartregister/fhircore/engine/data/local/DefaultRepositoryTest.kt @@ -65,6 +65,9 @@ import org.hl7.fhir.r4.model.Organization import org.hl7.fhir.r4.model.Patient import org.hl7.fhir.r4.model.Period import org.hl7.fhir.r4.model.Procedure +import org.hl7.fhir.r4.model.Questionnaire +import org.hl7.fhir.r4.model.QuestionnaireResponse +import org.hl7.fhir.r4.model.QuestionnaireResponse.QuestionnaireResponseStatus import org.hl7.fhir.r4.model.Reference import org.hl7.fhir.r4.model.RelatedPerson import org.hl7.fhir.r4.model.Resource @@ -80,6 +83,7 @@ import org.junit.Test import org.smartregister.fhircore.engine.app.AppConfigService import org.smartregister.fhircore.engine.app.fakes.Faker import org.smartregister.fhircore.engine.configuration.ConfigurationRegistry +import org.smartregister.fhircore.engine.configuration.QuestionnaireConfig import org.smartregister.fhircore.engine.configuration.UniqueIdAssignmentConfig import org.smartregister.fhircore.engine.configuration.app.ConfigService import org.smartregister.fhircore.engine.configuration.event.EventTriggerCondition @@ -106,6 +110,7 @@ import org.smartregister.fhircore.engine.util.extension.generateMissingId import org.smartregister.fhircore.engine.util.extension.loadResource import org.smartregister.fhircore.engine.util.extension.plusDays import org.smartregister.fhircore.engine.util.extension.updateLastUpdated +import org.smartregister.fhircore.engine.util.extension.yesterday import org.smartregister.fhircore.engine.util.fhirpath.FhirPathDataExtractor @HiltAndroidTest @@ -134,6 +139,9 @@ class DefaultRepositoryTest : RobolectricTest() { private lateinit var dispatcherProvider: DefaultDispatcherProvider private lateinit var sharedPreferenceHelper: SharedPreferencesHelper private lateinit var defaultRepository: DefaultRepository + private lateinit var patient: Patient + private lateinit var questionnaireConfig: QuestionnaireConfig + private lateinit var samplePatientRegisterQuestionnaire: Questionnaire @Before fun setUp() { @@ -153,6 +161,25 @@ class DefaultRepositoryTest : RobolectricTest() { context = context, contentCache = contentCache, ) + patient = + Faker.buildPatient().apply { + address = + listOf( + Address().apply { + city = "Mombasa" + country = "Kenya" + }, + ) + } + questionnaireConfig = + QuestionnaireConfig( + id = "e5155788-8831-4916-a3f5-486915ce34b211", // Same as ID in + // sample_patient_registration.json + title = "Patient registration", + type = "DEFAULT", + ) + + samplePatientRegisterQuestionnaire = Questionnaire().apply { id = questionnaireConfig.id } } @Test @@ -1620,4 +1647,100 @@ class DefaultRepositoryTest : RobolectricTest() { Assert.assertEquals(2, location4SubLocations.size) Assert.assertEquals(location5.logicalId, location4SubLocations.last().logicalId) } + + @Test + fun testSearchLatestQuestionnaireResponseShouldReturnLatestQuestionnaireResponse() = + runTest(timeout = 90.seconds) { + Assert.assertNull( + defaultRepository.searchQuestionnaireResponse( + resourceId = patient.logicalId, + resourceType = ResourceType.Patient, + questionnaireId = questionnaireConfig.id, + encounterId = null, + ), + ) + + val questionnaireResponses = + listOf( + QuestionnaireResponse().apply { + id = "qr1" + meta.lastUpdated = Date() + subject = patient.asReference() + questionnaire = samplePatientRegisterQuestionnaire.asReference().reference + }, + QuestionnaireResponse().apply { + id = "qr2" + meta.lastUpdated = yesterday() + subject = patient.asReference() + questionnaire = samplePatientRegisterQuestionnaire.asReference().reference + }, + ) + + // Add QuestionnaireResponse to database + fhirEngine.create( + patient, + samplePatientRegisterQuestionnaire, + *questionnaireResponses.toTypedArray(), + ) + + val latestQuestionnaireResponse = + defaultRepository.searchQuestionnaireResponse( + resourceId = patient.logicalId, + resourceType = ResourceType.Patient, + questionnaireId = questionnaireConfig.id, + encounterId = null, + ) + Assert.assertNotNull(latestQuestionnaireResponse) + Assert.assertEquals("QuestionnaireResponse/qr1", latestQuestionnaireResponse?.id) + } + + @Test + fun testSearchLatestQuestionnaireResponseWhenSaveDraftIsTueShouldReturnLatestQuestionnaireResponse() = + runTest(timeout = 90.seconds) { + Assert.assertNull( + defaultRepository.searchQuestionnaireResponse( + resourceId = patient.logicalId, + resourceType = ResourceType.Patient, + questionnaireId = questionnaireConfig.id, + encounterId = null, + questionnaireResponseStatus = QuestionnaireResponseStatus.INPROGRESS.toCode(), + ), + ) + + val questionnaireResponses = + listOf( + QuestionnaireResponse().apply { + id = "qr1" + meta.lastUpdated = Date() + subject = patient.asReference() + questionnaire = samplePatientRegisterQuestionnaire.asReference().reference + status = QuestionnaireResponseStatus.INPROGRESS + }, + QuestionnaireResponse().apply { + id = "qr2" + meta.lastUpdated = yesterday() + subject = patient.asReference() + questionnaire = samplePatientRegisterQuestionnaire.asReference().reference + status = QuestionnaireResponseStatus.COMPLETED + }, + ) + + // Add QuestionnaireResponse to database + fhirEngine.create( + patient, + samplePatientRegisterQuestionnaire, + *questionnaireResponses.toTypedArray(), + ) + + val latestQuestionnaireResponse = + defaultRepository.searchQuestionnaireResponse( + resourceId = patient.logicalId, + resourceType = ResourceType.Patient, + questionnaireId = questionnaireConfig.id, + encounterId = null, + questionnaireResponseStatus = QuestionnaireResponseStatus.INPROGRESS.toCode(), + ) + Assert.assertNotNull(latestQuestionnaireResponse) + Assert.assertEquals("QuestionnaireResponse/qr1", latestQuestionnaireResponse?.id) + } } diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireViewModel.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireViewModel.kt index b2430fe56e2..256556a2c1f 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireViewModel.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireViewModel.kt @@ -571,7 +571,8 @@ constructor( !questionnaireConfig.resourceIdentifier.isNullOrEmpty() && subjectType != null ) { - searchQuestionnaireResponse( + defaultRepository + .searchQuestionnaireResponse( resourceId = questionnaireConfig.resourceIdentifier!!, resourceType = questionnaireConfig.resourceType ?: subjectType, questionnaireId = questionnaire.logicalId, @@ -1064,48 +1065,6 @@ constructor( } } - /** - * This function searches and returns the latest [QuestionnaireResponse] for the given - * [resourceId] that was extracted from the [Questionnaire] identified as [questionnaireId]. - * Returns null if non is found. - */ - suspend fun searchQuestionnaireResponse( - resourceId: String, - resourceType: ResourceType, - questionnaireId: String, - encounterId: String?, - questionnaireResponseStatus: String? = null, - ): QuestionnaireResponse? { - val search = - Search(ResourceType.QuestionnaireResponse).apply { - filter( - QuestionnaireResponse.SUBJECT, - { value = resourceId.asReference(resourceType).reference }, - ) - filter( - QuestionnaireResponse.QUESTIONNAIRE, - { value = questionnaireId.asReference(ResourceType.Questionnaire).reference }, - ) - if (!encounterId.isNullOrBlank()) { - filter( - QuestionnaireResponse.ENCOUNTER, - { - value = - encounterId.extractLogicalIdUuid().asReference(ResourceType.Encounter).reference - }, - ) - } - if (!questionnaireResponseStatus.isNullOrBlank()) { - filter( - QuestionnaireResponse.STATUS, - { value = of(questionnaireResponseStatus) }, - ) - } - } - val questionnaireResponses: List = defaultRepository.search(search) - return questionnaireResponses.maxByOrNull { it.meta.lastUpdated } - } - private suspend fun launchContextResources( subjectResourceType: ResourceType?, subjectResourceIdentifier: String?, @@ -1176,7 +1135,8 @@ constructor( questionnaireConfig.isReadOnly() || questionnaireConfig.saveDraft) ) { - searchQuestionnaireResponse( + defaultRepository + .searchQuestionnaireResponse( resourceId = resourceIdentifier, resourceType = resourceType, questionnaireId = questionnaire.logicalId, diff --git a/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireViewModelTest.kt b/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireViewModelTest.kt index 2360f2061c5..e2b95eaaa42 100644 --- a/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireViewModelTest.kt +++ b/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireViewModelTest.kt @@ -1375,102 +1375,6 @@ class QuestionnaireViewModelTest : RobolectricTest() { coVerify { defaultRepository.addOrUpdate(true, patient) } } - @Test - fun testSearchLatestQuestionnaireResponseShouldReturnLatestQuestionnaireResponse() = - runTest(timeout = 90.seconds) { - Assert.assertNull( - questionnaireViewModel.searchQuestionnaireResponse( - resourceId = patient.logicalId, - resourceType = ResourceType.Patient, - questionnaireId = questionnaireConfig.id, - encounterId = null, - ), - ) - - val questionnaireResponses = - listOf( - QuestionnaireResponse().apply { - id = "qr1" - meta.lastUpdated = Date() - subject = patient.asReference() - questionnaire = samplePatientRegisterQuestionnaire.asReference().reference - }, - QuestionnaireResponse().apply { - id = "qr2" - meta.lastUpdated = yesterday() - subject = patient.asReference() - questionnaire = samplePatientRegisterQuestionnaire.asReference().reference - }, - ) - - // Add QuestionnaireResponse to database - fhirEngine.create( - patient, - samplePatientRegisterQuestionnaire, - *questionnaireResponses.toTypedArray(), - ) - - val latestQuestionnaireResponse = - questionnaireViewModel.searchQuestionnaireResponse( - resourceId = patient.logicalId, - resourceType = ResourceType.Patient, - questionnaireId = questionnaireConfig.id, - encounterId = null, - ) - Assert.assertNotNull(latestQuestionnaireResponse) - Assert.assertEquals("QuestionnaireResponse/qr1", latestQuestionnaireResponse?.id) - } - - @Test - fun testSearchLatestQuestionnaireResponseWhenSaveDraftIsTueShouldReturnLatestQuestionnaireResponse() = - runTest(timeout = 90.seconds) { - Assert.assertNull( - questionnaireViewModel.searchQuestionnaireResponse( - resourceId = patient.logicalId, - resourceType = ResourceType.Patient, - questionnaireId = questionnaireConfig.id, - encounterId = null, - questionnaireResponseStatus = QuestionnaireResponseStatus.INPROGRESS.toCode(), - ), - ) - - val questionnaireResponses = - listOf( - QuestionnaireResponse().apply { - id = "qr1" - meta.lastUpdated = Date() - subject = patient.asReference() - questionnaire = samplePatientRegisterQuestionnaire.asReference().reference - status = QuestionnaireResponseStatus.INPROGRESS - }, - QuestionnaireResponse().apply { - id = "qr2" - meta.lastUpdated = yesterday() - subject = patient.asReference() - questionnaire = samplePatientRegisterQuestionnaire.asReference().reference - status = QuestionnaireResponseStatus.COMPLETED - }, - ) - - // Add QuestionnaireResponse to database - fhirEngine.create( - patient, - samplePatientRegisterQuestionnaire, - *questionnaireResponses.toTypedArray(), - ) - - val latestQuestionnaireResponse = - questionnaireViewModel.searchQuestionnaireResponse( - resourceId = patient.logicalId, - resourceType = ResourceType.Patient, - questionnaireId = questionnaireConfig.id, - encounterId = null, - questionnaireResponseStatus = QuestionnaireResponseStatus.INPROGRESS.toCode(), - ) - Assert.assertNotNull(latestQuestionnaireResponse) - Assert.assertEquals("QuestionnaireResponse/qr1", latestQuestionnaireResponse?.id) - } - @Test fun testRetrievePopulationResourcesReturnsListOfResourcesOrEmptyList() = runTest { val specimenId = "specimenId" @@ -1628,7 +1532,7 @@ class QuestionnaireViewModelTest : RobolectricTest() { } coEvery { - questionnaireViewModel.searchQuestionnaireResponse( + defaultRepository.searchQuestionnaireResponse( resourceId = patient.logicalId, resourceType = ResourceType.Patient, questionnaireId = questionnaireConfig.id, From d84263d2db7dbd59224640bdb2c250660e983b55 Mon Sep 17 00:00:00 2001 From: Rkareko Date: Tue, 26 Nov 2024 11:31:45 +0300 Subject: [PATCH 06/21] Use interpolated questionnaire config when launching delete draft fragment --- .../fhircore/quest/ui/dialog/AlertDialogFragment.kt | 7 ++++--- .../quest/ui/questionnaire/QuestionnaireViewModel.kt | 1 + .../fhircore/quest/util/extensions/ConfigExtensions.kt | 4 +++- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/dialog/AlertDialogFragment.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/dialog/AlertDialogFragment.kt index d3b73e6bf78..060ef66d2e7 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/dialog/AlertDialogFragment.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/dialog/AlertDialogFragment.kt @@ -23,6 +23,7 @@ import androidx.fragment.app.viewModels import androidx.navigation.fragment.navArgs import dagger.hilt.android.AndroidEntryPoint import org.smartregister.fhircore.engine.ui.base.AlertDialogue +import org.smartregister.fhircore.engine.util.extension.getActivity import org.smartregister.fhircore.quest.ui.shared.QuestionnaireHandler @AndroidEntryPoint @@ -39,9 +40,9 @@ class AlertDialogFragment() : DialogFragment() { .questionnaire_in_progress_alert_back_pressed_message, title = org.smartregister.fhircore.engine.R.string.questionnaire_alert_back_pressed_title, confirmButtonListener = { - if (requireContext() is QuestionnaireHandler) { - (requireContext() as QuestionnaireHandler).launchQuestionnaire( - context = requireContext(), + if (requireContext().getActivity() is QuestionnaireHandler) { + (requireContext().getActivity() as QuestionnaireHandler).launchQuestionnaire( + context = requireContext().getActivity()!!.baseContext, questionnaireConfig = alertDialogFragmentArgs.questionnaireConfig, actionParams = listOf(), ) diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireViewModel.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireViewModel.kt index 256556a2c1f..b6f85923184 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireViewModel.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireViewModel.kt @@ -1146,6 +1146,7 @@ constructor( ?.let { QuestionnaireResponse().apply { id = it.id + status = it.status item = it.item.removeUnAnsweredItems() // Clearing the text prompts the SDK to re-process the content, which includes HTML clearText() diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/util/extensions/ConfigExtensions.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/util/extensions/ConfigExtensions.kt index c4a2c2c29d6..c9a88ffe241 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/util/extensions/ConfigExtensions.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/util/extensions/ConfigExtensions.kt @@ -237,9 +237,11 @@ fun ActionConfig.handleClickEvent( PdfLauncherFragment.launch(appCompatActivity, interpolatedPdfConfig.encodeJson()) } ApplicationWorkflow.LAUNCH_DELETE_DRAFT_FORM -> { + val questionnaireConfigInterpolated = + actionConfig.questionnaire?.interpolate(computedValuesMap) val args = bundleOf( - NavigationArg.QUESTIONNAIRE_CONFIG to actionConfig.questionnaire, + NavigationArg.QUESTIONNAIRE_CONFIG to questionnaireConfigInterpolated, ) navController.navigate(MainNavigationScreen.AlertDialogFragment.route, args) } From 4c5ec7f8948ee7da565267467dcd1b56a0257c3b Mon Sep 17 00:00:00 2001 From: Rkareko Date: Wed, 27 Nov 2024 08:21:56 +0300 Subject: [PATCH 07/21] Ensure delete draft db calls complete before dialog is dismissed --- android/engine/src/main/res/values/strings.xml | 2 ++ .../fhircore/quest/ui/dialog/AlertDialogFragment.kt | 12 ++++++------ .../fhircore/quest/ui/dialog/AlertDialogViewModel.kt | 2 +- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/android/engine/src/main/res/values/strings.xml b/android/engine/src/main/res/values/strings.xml index 9df1f37f553..e22a4b54f1a 100644 --- a/android/engine/src/main/res/values/strings.xml +++ b/android/engine/src/main/res/values/strings.xml @@ -204,4 +204,6 @@ APPLY FILTER Save draft changes Do you want to save draft changes? + Open draft changes + You can reopen a saved draft form to continue or delete it diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/dialog/AlertDialogFragment.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/dialog/AlertDialogFragment.kt index 060ef66d2e7..1dec06561a9 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/dialog/AlertDialogFragment.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/dialog/AlertDialogFragment.kt @@ -22,6 +22,7 @@ import androidx.fragment.app.DialogFragment import androidx.fragment.app.viewModels import androidx.navigation.fragment.navArgs import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.runBlocking import org.smartregister.fhircore.engine.ui.base.AlertDialogue import org.smartregister.fhircore.engine.util.extension.getActivity import org.smartregister.fhircore.quest.ui.shared.QuestionnaireHandler @@ -35,10 +36,8 @@ class AlertDialogFragment() : DialogFragment() { override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { return AlertDialogue.showThreeButtonAlert( context = requireContext(), - message = - org.smartregister.fhircore.engine.R.string - .questionnaire_in_progress_alert_back_pressed_message, - title = org.smartregister.fhircore.engine.R.string.questionnaire_alert_back_pressed_title, + message = org.smartregister.fhircore.engine.R.string.open_draft_changes_message, + title = org.smartregister.fhircore.engine.R.string.open_draft_changes_title, confirmButtonListener = { if (requireContext().getActivity() is QuestionnaireHandler) { (requireContext().getActivity() as QuestionnaireHandler).launchQuestionnaire( @@ -54,8 +53,9 @@ class AlertDialogFragment() : DialogFragment() { neutralButtonText = org.smartregister.fhircore.engine.R.string.questionnaire_alert_neutral_button_title, negativeButtonListener = { - alertDialogViewModel.deleteDraft(alertDialogFragmentArgs.questionnaireConfig) - this.dismiss() + runBlocking { + alertDialogViewModel.deleteDraft(alertDialogFragmentArgs.questionnaireConfig) + } }, negativeButtonText = org.smartregister.fhircore.engine.R.string.questionnaire_alert_delete_draft_button_title, diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/dialog/AlertDialogViewModel.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/dialog/AlertDialogViewModel.kt index 77160a7ec4e..3dd7e0d0256 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/dialog/AlertDialogViewModel.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/dialog/AlertDialogViewModel.kt @@ -34,7 +34,7 @@ constructor( val defaultRepository: DefaultRepository, val dispatcherProvider: DispatcherProvider, ) : ViewModel() { - fun deleteDraft(questionnaireConfig: QuestionnaireConfig?) { + suspend fun deleteDraft(questionnaireConfig: QuestionnaireConfig?) { if ( questionnaireConfig == null || questionnaireConfig.resourceIdentifier.isNullOrBlank() || From 01417be2d15a067b32d4a0869089aeeea8747977 Mon Sep 17 00:00:00 2001 From: Rkareko Date: Wed, 27 Nov 2024 14:49:56 +0300 Subject: [PATCH 08/21] Add flag to indicate a drft has been deleted --- .../quest/ui/dialog/AlertDialogViewModel.kt | 81 +++++++++++++++---- 1 file changed, 64 insertions(+), 17 deletions(-) diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/dialog/AlertDialogViewModel.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/dialog/AlertDialogViewModel.kt index 3dd7e0d0256..625e8abfca0 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/dialog/AlertDialogViewModel.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/dialog/AlertDialogViewModel.kt @@ -17,15 +17,20 @@ package org.smartregister.fhircore.quest.ui.dialog import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel +import java.util.Date import javax.inject.Inject -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext +import org.hl7.fhir.r4.model.CodeableConcept +import org.hl7.fhir.r4.model.Coding +import org.hl7.fhir.r4.model.Flag +import org.hl7.fhir.r4.model.Identifier +import org.hl7.fhir.r4.model.Period +import org.hl7.fhir.r4.model.QuestionnaireResponse import org.hl7.fhir.r4.model.QuestionnaireResponse.QuestionnaireResponseStatus import org.smartregister.fhircore.engine.configuration.QuestionnaireConfig import org.smartregister.fhircore.engine.data.local.DefaultRepository import org.smartregister.fhircore.engine.util.DispatcherProvider +import org.smartregister.fhircore.engine.util.extension.asReference @HiltViewModel class AlertDialogViewModel @@ -43,22 +48,64 @@ constructor( return } - viewModelScope.launch { - withContext(dispatcherProvider.io()) { - val questionnaireResponse = - defaultRepository.searchQuestionnaireResponse( - resourceId = questionnaireConfig.resourceIdentifier!!, - resourceType = questionnaireConfig.resourceType!!, - questionnaireId = questionnaireConfig.id, - encounterId = null, - questionnaireResponseStatus = QuestionnaireResponseStatus.INPROGRESS.toCode(), - ) + val questionnaireResponse = + defaultRepository.searchQuestionnaireResponse( + resourceId = questionnaireConfig.resourceIdentifier!!, + resourceType = questionnaireConfig.resourceType!!, + questionnaireId = questionnaireConfig.id, + encounterId = null, + questionnaireResponseStatus = QuestionnaireResponseStatus.INPROGRESS.toCode(), + ) - if (questionnaireResponse != null) { - questionnaireResponse.status = QuestionnaireResponseStatus.STOPPED - defaultRepository.update(questionnaireResponse) + if (questionnaireResponse != null) { + questionnaireResponse.status = QuestionnaireResponseStatus.STOPPED + defaultRepository.update(questionnaireResponse) + defaultRepository.addOrUpdate( + resource = createDeleteDraftFlag(questionnaireConfig, questionnaireResponse), + ) + } + } + + fun createDeleteDraftFlag( + questionnaireConfig: QuestionnaireConfig, + questionnaireResponse: QuestionnaireResponse, + ): Flag { + return Flag().apply { + subject = + questionnaireConfig.resourceType?.let { + questionnaireConfig.resourceIdentifier?.asReference( + it, + ) + } + identifier = + listOf( + Identifier().apply { value = questionnaireResponse.id }, + ) + status = Flag.FlagStatus.ACTIVE + code = + CodeableConcept().apply { + coding = + listOf( + Coding().apply { + system = FLAG_SYSTEM + code = FLAG_CODE + display = FLAG_DISPLAY + }, + ) + text = FLAG_TEXT + } + period = + Period().apply { + start = Date() + end = Date() } - } } } + + companion object { + const val FLAG_SYSTEM = "http://smartregister.org/" + const val FLAG_CODE = "delete_draft" + const val FLAG_DISPLAY = "Delete Draft" + const val FLAG_TEXT = "QR Draft has been deleted" + } } From ca9c1592784d0c0ee48027bff3b0d60d5faaa132 Mon Sep 17 00:00:00 2001 From: Rkareko Date: Thu, 28 Nov 2024 12:36:57 +0300 Subject: [PATCH 09/21] Use aduti event to keep track of deleted drafts --- .../quest/ui/dialog/AlertDialogViewModel.kt | 77 +++++++++++-------- 1 file changed, 45 insertions(+), 32 deletions(-) diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/dialog/AlertDialogViewModel.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/dialog/AlertDialogViewModel.kt index 625e8abfca0..f97809991d5 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/dialog/AlertDialogViewModel.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/dialog/AlertDialogViewModel.kt @@ -20,25 +20,35 @@ import androidx.lifecycle.ViewModel import dagger.hilt.android.lifecycle.HiltViewModel import java.util.Date import javax.inject.Inject -import org.hl7.fhir.r4.model.CodeableConcept +import org.hl7.fhir.r4.model.AuditEvent +import org.hl7.fhir.r4.model.AuditEvent.AuditEventSourceComponent import org.hl7.fhir.r4.model.Coding -import org.hl7.fhir.r4.model.Flag -import org.hl7.fhir.r4.model.Identifier import org.hl7.fhir.r4.model.Period import org.hl7.fhir.r4.model.QuestionnaireResponse import org.hl7.fhir.r4.model.QuestionnaireResponse.QuestionnaireResponseStatus +import org.hl7.fhir.r4.model.Reference +import org.hl7.fhir.r4.model.ResourceType import org.smartregister.fhircore.engine.configuration.QuestionnaireConfig import org.smartregister.fhircore.engine.data.local.DefaultRepository -import org.smartregister.fhircore.engine.util.DispatcherProvider +import org.smartregister.fhircore.engine.util.SharedPreferenceKey +import org.smartregister.fhircore.engine.util.SharedPreferencesHelper import org.smartregister.fhircore.engine.util.extension.asReference +import org.smartregister.fhircore.engine.util.extension.extractLogicalIdUuid @HiltViewModel class AlertDialogViewModel @Inject constructor( val defaultRepository: DefaultRepository, - val dispatcherProvider: DispatcherProvider, + val sharedPreferencesHelper: SharedPreferencesHelper, ) : ViewModel() { + + private val practitionerId: String? by lazy { + sharedPreferencesHelper + .read(SharedPreferenceKey.PRACTITIONER_ID.name, null) + ?.extractLogicalIdUuid() + } + suspend fun deleteDraft(questionnaireConfig: QuestionnaireConfig?) { if ( questionnaireConfig == null || @@ -61,38 +71,42 @@ constructor( questionnaireResponse.status = QuestionnaireResponseStatus.STOPPED defaultRepository.update(questionnaireResponse) defaultRepository.addOrUpdate( - resource = createDeleteDraftFlag(questionnaireConfig, questionnaireResponse), + resource = createDeleteDraftAuditEvent(questionnaireConfig, questionnaireResponse), ) } } - fun createDeleteDraftFlag( + fun createDeleteDraftAuditEvent( questionnaireConfig: QuestionnaireConfig, questionnaireResponse: QuestionnaireResponse, - ): Flag { - return Flag().apply { - subject = - questionnaireConfig.resourceType?.let { - questionnaireConfig.resourceIdentifier?.asReference( - it, - ) + ): AuditEvent { + return AuditEvent().apply { + entity = + listOf( + AuditEvent.AuditEventEntityComponent().apply { + what = Reference(questionnaireResponse.id) + }, + ) + source = + AuditEventSourceComponent().apply { + observer = + questionnaireConfig.resourceType?.let { + questionnaireConfig.resourceIdentifier?.asReference( + it, + ) + } } - identifier = + agent = listOf( - Identifier().apply { value = questionnaireResponse.id }, + AuditEvent.AuditEventAgentComponent().apply { + who = practitionerId?.asReference(ResourceType.Practitioner) + }, ) - status = Flag.FlagStatus.ACTIVE - code = - CodeableConcept().apply { - coding = - listOf( - Coding().apply { - system = FLAG_SYSTEM - code = FLAG_CODE - display = FLAG_DISPLAY - }, - ) - text = FLAG_TEXT + type = + Coding().apply { + system = AUDIT_EVENT_SYSTEM + code = AUDIT_EVENT_CODE + display = AUDIT_EVENT_DISPLAY } period = Period().apply { @@ -103,9 +117,8 @@ constructor( } companion object { - const val FLAG_SYSTEM = "http://smartregister.org/" - const val FLAG_CODE = "delete_draft" - const val FLAG_DISPLAY = "Delete Draft" - const val FLAG_TEXT = "QR Draft has been deleted" + const val AUDIT_EVENT_SYSTEM = "http://smartregister.org/" + const val AUDIT_EVENT_CODE = "delete_draft" + const val AUDIT_EVENT_DISPLAY = "Delete Draft" } } From 3271332d62bd0e0c5820a474d65e759d30abdb69 Mon Sep 17 00:00:00 2001 From: Rkareko Date: Thu, 28 Nov 2024 12:42:39 +0300 Subject: [PATCH 10/21] Rename delete draft questionnaire workflow move delete draft classes to questionnaire package --- .../engine/configuration/workflow/ApplicationWorkflow.kt | 4 ++-- .../quest/ui/{dialog => questionnaire}/AlertDialogFragment.kt | 3 ++- .../ui/{dialog => questionnaire}/AlertDialogViewModel.kt | 2 +- .../fhircore/quest/util/extensions/ConfigExtensions.kt | 2 +- 4 files changed, 6 insertions(+), 5 deletions(-) rename android/quest/src/main/java/org/smartregister/fhircore/quest/ui/{dialog => questionnaire}/AlertDialogFragment.kt (95%) rename android/quest/src/main/java/org/smartregister/fhircore/quest/ui/{dialog => questionnaire}/AlertDialogViewModel.kt (98%) diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/workflow/ApplicationWorkflow.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/workflow/ApplicationWorkflow.kt index de0ca594163..68848c238ea 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/workflow/ApplicationWorkflow.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/workflow/ApplicationWorkflow.kt @@ -63,6 +63,6 @@ enum class ApplicationWorkflow { /** A workflow to launch pdf generation */ LAUNCH_PDF_GENERATION, - /** A workflow to launch delete draft */ - LAUNCH_DELETE_DRAFT_FORM, + /** A workflow to launch delete draft questionnaires */ + DELETE_DRAFT_QUESTIONNAIRE, } diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/dialog/AlertDialogFragment.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/AlertDialogFragment.kt similarity index 95% rename from android/quest/src/main/java/org/smartregister/fhircore/quest/ui/dialog/AlertDialogFragment.kt rename to android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/AlertDialogFragment.kt index 1dec06561a9..37ebe79f932 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/dialog/AlertDialogFragment.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/AlertDialogFragment.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.smartregister.fhircore.quest.ui.dialog +package org.smartregister.fhircore.quest.ui.questionnaire import android.app.Dialog import android.os.Bundle @@ -25,6 +25,7 @@ import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.runBlocking import org.smartregister.fhircore.engine.ui.base.AlertDialogue import org.smartregister.fhircore.engine.util.extension.getActivity +import org.smartregister.fhircore.quest.ui.dialog.AlertDialogFragmentArgs import org.smartregister.fhircore.quest.ui.shared.QuestionnaireHandler @AndroidEntryPoint diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/dialog/AlertDialogViewModel.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/AlertDialogViewModel.kt similarity index 98% rename from android/quest/src/main/java/org/smartregister/fhircore/quest/ui/dialog/AlertDialogViewModel.kt rename to android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/AlertDialogViewModel.kt index f97809991d5..bb6960926d2 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/dialog/AlertDialogViewModel.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/AlertDialogViewModel.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.smartregister.fhircore.quest.ui.dialog +package org.smartregister.fhircore.quest.ui.questionnaire import androidx.lifecycle.ViewModel import dagger.hilt.android.lifecycle.HiltViewModel diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/util/extensions/ConfigExtensions.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/util/extensions/ConfigExtensions.kt index c9a88ffe241..756274dcbec 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/util/extensions/ConfigExtensions.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/util/extensions/ConfigExtensions.kt @@ -236,7 +236,7 @@ fun ActionConfig.handleClickEvent( val appCompatActivity = (navController.context as AppCompatActivity) PdfLauncherFragment.launch(appCompatActivity, interpolatedPdfConfig.encodeJson()) } - ApplicationWorkflow.LAUNCH_DELETE_DRAFT_FORM -> { + ApplicationWorkflow.DELETE_DRAFT_QUESTIONNAIRE -> { val questionnaireConfigInterpolated = actionConfig.questionnaire?.interpolate(computedValuesMap) val args = From c050cc927eb87aa3da1b6a61d9d56b7abeab3c58 Mon Sep 17 00:00:00 2001 From: Rkareko Date: Thu, 28 Nov 2024 12:59:10 +0300 Subject: [PATCH 11/21] Rename draft dialog fragment and view model --- .../quest/navigation/MainNavigationScreen.kt | 2 +- ...agment.kt => QuestionnaireDraftDialogFragment.kt} | 12 ++++++------ ...Model.kt => QuestionnaireDraftDialogViewModel.kt} | 2 +- .../main/res/navigation/application_nav_graph.xml | 4 ++-- 4 files changed, 10 insertions(+), 10 deletions(-) rename android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/{AlertDialogFragment.kt => QuestionnaireDraftDialogFragment.kt} (83%) rename android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/{AlertDialogViewModel.kt => QuestionnaireDraftDialogViewModel.kt} (99%) 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 8c476e8eac5..989237f4f9a 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 @@ -72,7 +72,7 @@ sealed class MainNavigationScreen( data object AlertDialogFragment : MainNavigationScreen( - route = org.smartregister.fhircore.quest.R.id.alertDialogFragment, + route = org.smartregister.fhircore.quest.R.id.questionnaireDraftDialogFragment, ) fun eventId(id: String) = route.toString() + "_" + id diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/AlertDialogFragment.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireDraftDialogFragment.kt similarity index 83% rename from android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/AlertDialogFragment.kt rename to android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireDraftDialogFragment.kt index 37ebe79f932..34e2b930871 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/AlertDialogFragment.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireDraftDialogFragment.kt @@ -25,14 +25,14 @@ import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.runBlocking import org.smartregister.fhircore.engine.ui.base.AlertDialogue import org.smartregister.fhircore.engine.util.extension.getActivity -import org.smartregister.fhircore.quest.ui.dialog.AlertDialogFragmentArgs import org.smartregister.fhircore.quest.ui.shared.QuestionnaireHandler @AndroidEntryPoint -class AlertDialogFragment() : DialogFragment() { +class QuestionnaireDraftDialogFragment() : DialogFragment() { - private val alertDialogFragmentArgs by navArgs() - private val alertDialogViewModel by viewModels() + private val questionnaireDraftDialogFragmentArgs by + navArgs() + private val alertDialogViewModel by viewModels() override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { return AlertDialogue.showThreeButtonAlert( @@ -43,7 +43,7 @@ class AlertDialogFragment() : DialogFragment() { if (requireContext().getActivity() is QuestionnaireHandler) { (requireContext().getActivity() as QuestionnaireHandler).launchQuestionnaire( context = requireContext().getActivity()!!.baseContext, - questionnaireConfig = alertDialogFragmentArgs.questionnaireConfig, + questionnaireConfig = questionnaireDraftDialogFragmentArgs.questionnaireConfig, actionParams = listOf(), ) } @@ -55,7 +55,7 @@ class AlertDialogFragment() : DialogFragment() { org.smartregister.fhircore.engine.R.string.questionnaire_alert_neutral_button_title, negativeButtonListener = { runBlocking { - alertDialogViewModel.deleteDraft(alertDialogFragmentArgs.questionnaireConfig) + alertDialogViewModel.deleteDraft(questionnaireDraftDialogFragmentArgs.questionnaireConfig) } }, negativeButtonText = diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/AlertDialogViewModel.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireDraftDialogViewModel.kt similarity index 99% rename from android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/AlertDialogViewModel.kt rename to android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireDraftDialogViewModel.kt index bb6960926d2..b1d6c905fdd 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/AlertDialogViewModel.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireDraftDialogViewModel.kt @@ -36,7 +36,7 @@ import org.smartregister.fhircore.engine.util.extension.asReference import org.smartregister.fhircore.engine.util.extension.extractLogicalIdUuid @HiltViewModel -class AlertDialogViewModel +class QuestionnaireDraftDialogViewModel @Inject constructor( val defaultRepository: DefaultRepository, diff --git a/android/quest/src/main/res/navigation/application_nav_graph.xml b/android/quest/src/main/res/navigation/application_nav_graph.xml index f73639b7cc6..3c77f01c8ef 100644 --- a/android/quest/src/main/res/navigation/application_nav_graph.xml +++ b/android/quest/src/main/res/navigation/application_nav_graph.xml @@ -111,8 +111,8 @@ + android:id="@+id/questionnaireDraftDialogFragment" + android:name="org.smartregister.fhircore.quest.ui.questionnaire.QuestionnaireDraftDialogFragment" > Date: Fri, 29 Nov 2024 14:06:07 +0300 Subject: [PATCH 12/21] Add data class to hold alert dialog button properties Add ability to set alert dialog button color --- .../fhircore/engine/ui/base/AlertDialogue.kt | 101 ++++++++++++------ .../ui/geowidget/GeoWidgetLauncherFragment.kt | 23 ++-- .../fhircore/quest/ui/main/AppMainActivity.kt | 11 +- .../ui/questionnaire/QuestionnaireActivity.kt | 46 +++++--- .../QuestionnaireDraftDialogFragment.kt | 60 +++++++---- 5 files changed, 159 insertions(+), 82 deletions(-) diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/ui/base/AlertDialogue.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/ui/base/AlertDialogue.kt index 3bc8c724c04..bf61fd47170 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/ui/base/AlertDialogue.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/ui/base/AlertDialogue.kt @@ -43,6 +43,12 @@ enum class AlertIntent { data class AlertDialogListItem(val key: String, val value: String) +data class AlertDialogButton( + val listener: ((d: DialogInterface) -> Unit)? = null, + @StringRes val text: Int? = null, + val color: Int? = null, +) + object AlertDialogue { private val ITEMS_LIST_KEY = "alert_dialog_items_list" @@ -51,12 +57,9 @@ object AlertDialogue { alertIntent: AlertIntent, message: CharSequence, title: String? = null, - confirmButtonListener: ((d: DialogInterface) -> Unit)? = null, - @StringRes confirmButtonText: Int = R.string.questionnaire_alert_confirm_button_title, - neutralButtonListener: ((d: DialogInterface) -> Unit)? = null, - @StringRes neutralButtonText: Int = R.string.questionnaire_alert_neutral_button_title, - negativeButtonListener: ((d: DialogInterface) -> Unit)? = null, - @StringRes negativeButtonText: Int = R.string.questionnaire_alert_negative_button_title, + confirmButton: AlertDialogButton? = null, + neutralButton: AlertDialogButton? = null, + negativeButton: AlertDialogButton? = null, cancellable: Boolean = false, options: Array? = null, ): AlertDialog { @@ -67,22 +70,48 @@ object AlertDialogue { setView(view) title?.let { setTitle(it) } setCancelable(cancellable) - neutralButtonListener?.let { - setNeutralButton(neutralButtonText) { d, _ -> neutralButtonListener.invoke(d) } + neutralButton?.listener?.let { + setNeutralButton( + neutralButton.text ?: R.string.questionnaire_alert_neutral_button_title, + ) { d, _ -> + neutralButton.listener.invoke(d) + } } - confirmButtonListener?.let { - setPositiveButton(confirmButtonText) { d, _ -> confirmButtonListener.invoke(d) } + confirmButton?.listener?.let { + setPositiveButton( + confirmButton.text ?: R.string.questionnaire_alert_confirm_button_title, + ) { d, _ -> + confirmButton.listener.invoke(d) + } } - negativeButtonListener?.let { - setNegativeButton(negativeButtonText) { d, _ -> negativeButtonListener.invoke(d) } + negativeButton?.listener?.let { + setNegativeButton( + negativeButton.text ?: R.string.questionnaire_alert_negative_button_title, + ) { d, _ -> + negativeButton.listener.invoke(d) + } } options?.run { setSingleChoiceItems(options.map { it.value }.toTypedArray(), -1, null) } } .show() + val neutralButtonColor = neutralButton?.color ?: R.color.grey_text_color dialog .getButton(AlertDialog.BUTTON_NEUTRAL) - .setTextColor(ContextCompat.getColor(context, R.color.grey_text_color)) + .setTextColor(ContextCompat.getColor(context, neutralButtonColor)) + + if (confirmButton?.color != null) { + dialog + .getButton(AlertDialog.BUTTON_POSITIVE) + .setTextColor(ContextCompat.getColor(context, confirmButton.color)) + } + + if (negativeButton?.color != null) { + dialog + .getButton(AlertDialog.BUTTON_NEGATIVE) + .setTextColor(ContextCompat.getColor(context, negativeButton.color)) + } + dialog.findViewById(R.id.pr_circular)?.apply { if (alertIntent == AlertIntent.PROGRESS) { this.show() @@ -115,8 +144,11 @@ object AlertDialogue { alertIntent = AlertIntent.INFO, message = message, title = title, - confirmButtonListener = confirmButtonListener, - confirmButtonText = confirmButtonText, + confirmButton = + AlertDialogButton( + listener = confirmButtonListener, + text = confirmButtonText, + ), ) } @@ -126,8 +158,11 @@ object AlertDialogue { alertIntent = AlertIntent.ERROR, message = message, title = title, - confirmButtonListener = { d -> d.dismiss() }, - confirmButtonText = R.string.questionnaire_alert_ack_button_title, + confirmButton = + AlertDialogButton( + listener = { d -> d.dismiss() }, + text = R.string.questionnaire_alert_ack_button_title, + ), ) } @@ -160,10 +195,16 @@ object AlertDialogue { alertIntent = AlertIntent.CONFIRM, message = context.getString(message), title = title?.let { context.getString(it) }, - confirmButtonListener = confirmButtonListener, - confirmButtonText = confirmButtonText, - neutralButtonListener = { d -> d.dismiss() }, - neutralButtonText = R.string.questionnaire_alert_neutral_button_title, + confirmButton = + AlertDialogButton( + listener = confirmButtonListener, + text = confirmButtonText, + ), + neutralButton = + AlertDialogButton( + listener = { d -> d.dismiss() }, + text = R.string.questionnaire_alert_neutral_button_title, + ), cancellable = false, options = options?.toTypedArray(), ) @@ -173,12 +214,9 @@ object AlertDialogue { context: Context, @StringRes message: Int, @StringRes title: Int? = null, - confirmButtonListener: ((d: DialogInterface) -> Unit), - @StringRes confirmButtonText: Int, - neutralButtonListener: ((d: DialogInterface) -> Unit), - @StringRes neutralButtonText: Int, - negativeButtonListener: ((d: DialogInterface) -> Unit), - @StringRes negativeButtonText: Int, + confirmButton: AlertDialogButton? = null, + neutralButton: AlertDialogButton? = null, + negativeButton: AlertDialogButton? = null, cancellable: Boolean = true, options: List? = null, ): AlertDialog { @@ -187,12 +225,9 @@ object AlertDialogue { alertIntent = AlertIntent.CONFIRM, message = context.getString(message), title = title?.let { context.getString(it) }, - confirmButtonListener = confirmButtonListener, - confirmButtonText = confirmButtonText, - neutralButtonListener = neutralButtonListener, - neutralButtonText = neutralButtonText, - negativeButtonListener = negativeButtonListener, - negativeButtonText = negativeButtonText, + confirmButton = confirmButton, + neutralButton = neutralButton, + negativeButton = negativeButton, cancellable = cancellable, options = options?.toTypedArray(), ) diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/geowidget/GeoWidgetLauncherFragment.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/geowidget/GeoWidgetLauncherFragment.kt index 2b4d302b1e1..2eab074a82e 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/geowidget/GeoWidgetLauncherFragment.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/geowidget/GeoWidgetLauncherFragment.kt @@ -53,6 +53,7 @@ import org.smartregister.fhircore.engine.configuration.ConfigurationRegistry import org.smartregister.fhircore.engine.configuration.geowidget.GeoWidgetConfiguration import org.smartregister.fhircore.engine.sync.OnSyncListener import org.smartregister.fhircore.engine.sync.SyncListenerManager +import org.smartregister.fhircore.engine.ui.base.AlertDialogButton import org.smartregister.fhircore.engine.ui.base.AlertDialogue import org.smartregister.fhircore.engine.ui.base.AlertIntent import org.smartregister.fhircore.engine.ui.theme.AppTheme @@ -271,15 +272,21 @@ class GeoWidgetLauncherFragment : Fragment(), OnSyncListener { alertIntent = AlertIntent.INFO, message = geoWidgetConfiguration.noResults?.message!!, title = geoWidgetConfiguration.noResults?.title!!, - confirmButtonListener = { - geoWidgetConfiguration.noResults - ?.actionButton - ?.actions - ?.handleClickEvent(findNavController()) - }, - confirmButtonText = R.string.positive_button_location_set, + confirmButton = + AlertDialogButton( + listener = { + geoWidgetConfiguration.noResults + ?.actionButton + ?.actions + ?.handleClickEvent(findNavController()) + }, + text = R.string.positive_button_location_set, + ), cancellable = true, - neutralButtonListener = {}, + neutralButton = + AlertDialogButton( + listener = {}, + ), ) } } diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/main/AppMainActivity.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/main/AppMainActivity.kt index c1c47cdbfd3..3a239198b15 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/main/AppMainActivity.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/main/AppMainActivity.kt @@ -52,6 +52,7 @@ import org.smartregister.fhircore.engine.domain.model.LauncherType import org.smartregister.fhircore.engine.rulesengine.services.LocationCoordinate import org.smartregister.fhircore.engine.sync.OnSyncListener import org.smartregister.fhircore.engine.sync.SyncListenerManager +import org.smartregister.fhircore.engine.ui.base.AlertDialogButton import org.smartregister.fhircore.engine.ui.base.AlertDialogue import org.smartregister.fhircore.engine.ui.base.AlertIntent import org.smartregister.fhircore.engine.ui.base.BaseMultiLanguageActivity @@ -318,8 +319,14 @@ open class AppMainActivity : BaseMultiLanguageActivity(), QuestionnaireHandler, title = getString(R.string.exit_app), message = getString(R.string.exit_app_message), cancellable = false, - confirmButtonListener = { finish() }, - neutralButtonListener = { dialog -> dialog.dismiss() }, + confirmButton = + AlertDialogButton( + listener = { finish() }, + ), + neutralButton = + AlertDialogButton( + listener = { dialog -> dialog.dismiss() }, + ), ) } else navHostFragment.navController.navigateUp() } diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireActivity.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireActivity.kt index a5f33554d03..e0608a8062f 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireActivity.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireActivity.kt @@ -50,6 +50,7 @@ import org.smartregister.fhircore.engine.configuration.app.LocationLogOptions import org.smartregister.fhircore.engine.domain.model.ActionParameter import org.smartregister.fhircore.engine.domain.model.isReadOnly import org.smartregister.fhircore.engine.domain.model.isSummary +import org.smartregister.fhircore.engine.ui.base.AlertDialogButton import org.smartregister.fhircore.engine.ui.base.AlertDialogue import org.smartregister.fhircore.engine.ui.base.BaseMultiLanguageActivity import org.smartregister.fhircore.engine.util.DispatcherProvider @@ -357,23 +358,34 @@ class QuestionnaireActivity : BaseMultiLanguageActivity() { org.smartregister.fhircore.engine.R.string .questionnaire_in_progress_alert_back_pressed_message, title = org.smartregister.fhircore.engine.R.string.questionnaire_alert_back_pressed_title, - confirmButtonListener = { - lifecycleScope.launch { - retrieveQuestionnaireResponse()?.let { questionnaireResponse -> - viewModel.saveDraftQuestionnaire(questionnaireResponse, questionnaireConfig) - finish() - } - } - }, - confirmButtonText = - org.smartregister.fhircore.engine.R.string - .questionnaire_alert_back_pressed_save_draft_button_title, - neutralButtonListener = {}, - neutralButtonText = - org.smartregister.fhircore.engine.R.string.questionnaire_alert_neutral_button_title, - negativeButtonListener = { finish() }, - negativeButtonText = - org.smartregister.fhircore.engine.R.string.questionnaire_alert_negative_button_title, + confirmButton = + AlertDialogButton( + listener = { + lifecycleScope.launch { + retrieveQuestionnaireResponse()?.let { questionnaireResponse -> + viewModel.saveDraftQuestionnaire(questionnaireResponse, questionnaireConfig) + finish() + } + } + }, + text = + org.smartregister.fhircore.engine.R.string + .questionnaire_alert_back_pressed_save_draft_button_title, + color = org.smartregister.fhircore.engine.R.color.colorPrimary, + ), + neutralButton = + AlertDialogButton( + listener = {}, + text = + org.smartregister.fhircore.engine.R.string.questionnaire_alert_neutral_button_title, + ), + negativeButton = + AlertDialogButton( + listener = { finish() }, + text = + org.smartregister.fhircore.engine.R.string.questionnaire_alert_negative_button_title, + color = org.smartregister.fhircore.engine.R.color.colorPrimary, + ), ) } else { AlertDialogue.showConfirmAlert( diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireDraftDialogFragment.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireDraftDialogFragment.kt index 34e2b930871..7e6e277005c 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireDraftDialogFragment.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireDraftDialogFragment.kt @@ -23,6 +23,8 @@ import androidx.fragment.app.viewModels import androidx.navigation.fragment.navArgs import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.runBlocking +import org.smartregister.fhircore.engine.R +import org.smartregister.fhircore.engine.ui.base.AlertDialogButton import org.smartregister.fhircore.engine.ui.base.AlertDialogue import org.smartregister.fhircore.engine.util.extension.getActivity import org.smartregister.fhircore.quest.ui.shared.QuestionnaireHandler @@ -32,34 +34,48 @@ class QuestionnaireDraftDialogFragment() : DialogFragment() { private val questionnaireDraftDialogFragmentArgs by navArgs() - private val alertDialogViewModel by viewModels() + private val questionnaireDraftDialogViewModel by viewModels() override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { return AlertDialogue.showThreeButtonAlert( context = requireContext(), message = org.smartregister.fhircore.engine.R.string.open_draft_changes_message, title = org.smartregister.fhircore.engine.R.string.open_draft_changes_title, - confirmButtonListener = { - if (requireContext().getActivity() is QuestionnaireHandler) { - (requireContext().getActivity() as QuestionnaireHandler).launchQuestionnaire( - context = requireContext().getActivity()!!.baseContext, - questionnaireConfig = questionnaireDraftDialogFragmentArgs.questionnaireConfig, - actionParams = listOf(), - ) - } - }, - confirmButtonText = - org.smartregister.fhircore.engine.R.string.questionnaire_alert_open_draft_button_title, - neutralButtonListener = {}, - neutralButtonText = - org.smartregister.fhircore.engine.R.string.questionnaire_alert_neutral_button_title, - negativeButtonListener = { - runBlocking { - alertDialogViewModel.deleteDraft(questionnaireDraftDialogFragmentArgs.questionnaireConfig) - } - }, - negativeButtonText = - org.smartregister.fhircore.engine.R.string.questionnaire_alert_delete_draft_button_title, + confirmButton = + AlertDialogButton( + listener = { + if (requireContext().getActivity() is QuestionnaireHandler) { + (requireContext().getActivity() as QuestionnaireHandler).launchQuestionnaire( + context = requireContext().getActivity()!!.baseContext, + questionnaireConfig = questionnaireDraftDialogFragmentArgs.questionnaireConfig, + actionParams = listOf(), + ) + } + }, + text = + org.smartregister.fhircore.engine.R.string.questionnaire_alert_open_draft_button_title, + color = R.color.colorPrimary, + ), + neutralButton = + AlertDialogButton( + listener = {}, + text = + org.smartregister.fhircore.engine.R.string.questionnaire_alert_neutral_button_title, + ), + negativeButton = + AlertDialogButton( + listener = { + runBlocking { + questionnaireDraftDialogViewModel.deleteDraft( + questionnaireDraftDialogFragmentArgs.questionnaireConfig, + ) + } + }, + text = + org.smartregister.fhircore.engine.R.string + .questionnaire_alert_delete_draft_button_title, + color = R.color.colorError, + ), ) } } From 3952876c675b42aeb709c70e66e2683acf810200 Mon Sep 17 00:00:00 2001 From: Rkareko Date: Fri, 29 Nov 2024 14:52:20 +0300 Subject: [PATCH 13/21] Fix failing tests --- .../engine/ui/base/AlertDialogueTest.kt | 35 +++++++++++++------ 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/android/engine/src/test/java/org/smartregister/fhircore/engine/ui/base/AlertDialogueTest.kt b/android/engine/src/test/java/org/smartregister/fhircore/engine/ui/base/AlertDialogueTest.kt index 838eae71189..bc0aeead935 100644 --- a/android/engine/src/test/java/org/smartregister/fhircore/engine/ui/base/AlertDialogueTest.kt +++ b/android/engine/src/test/java/org/smartregister/fhircore/engine/ui/base/AlertDialogueTest.kt @@ -53,10 +53,16 @@ class AlertDialogueTest : ActivityRobolectricTest() { alertIntent = AlertIntent.ERROR, message = getString(R.string.questionnaire_alert_invalid_message), title = getString(R.string.questionnaire_alert_invalid_title), - confirmButtonText = R.string.questionnaire_alert_confirm_button_title, - confirmButtonListener = { confirmCalled.add(true) }, - neutralButtonText = R.string.questionnaire_alert_ack_button_title, - neutralButtonListener = { neutralCalled.add(true) }, + confirmButton = + AlertDialogButton( + text = R.string.questionnaire_alert_confirm_button_title, + listener = { confirmCalled.add(true) }, + ), + neutralButton = + AlertDialogButton( + text = R.string.questionnaire_alert_ack_button_title, + listener = { neutralCalled.add(true) }, + ), options = arrayOf(AlertDialogListItem("a", "A"), AlertDialogListItem("b", "B")), ) @@ -147,12 +153,21 @@ class AlertDialogueTest : ActivityRobolectricTest() { context = context, message = R.string.questionnaire_in_progress_alert_back_pressed_message, title = R.string.questionnaire_alert_back_pressed_title, - confirmButtonListener = {}, - confirmButtonText = R.string.questionnaire_alert_back_pressed_save_draft_button_title, - neutralButtonListener = {}, - neutralButtonText = R.string.questionnaire_alert_back_pressed_button_title, - negativeButtonListener = {}, - negativeButtonText = R.string.questionnaire_alert_negative_button_title, + confirmButton = + AlertDialogButton( + listener = {}, + text = R.string.questionnaire_alert_back_pressed_save_draft_button_title, + ), + neutralButton = + AlertDialogButton( + listener = {}, + text = R.string.questionnaire_alert_back_pressed_button_title, + ), + negativeButton = + AlertDialogButton( + listener = {}, + text = R.string.questionnaire_alert_negative_button_title, + ), ) val dialog = shadowOf(ShadowAlertDialog.getLatestAlertDialog()) From d81f079a3534c561aa9e4891a093a16308401f60 Mon Sep 17 00:00:00 2001 From: Rkareko <47570855+Rkareko@users.noreply.github.com> Date: Fri, 29 Nov 2024 15:08:05 +0300 Subject: [PATCH 14/21] Update CHANGELOG.md --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6b6de709108..c6b01eff179 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,7 +12,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 1. Added a new class (PdfGenerator) for generating PDF documents from HTML content using Android's WebView and PrintManager 2. Introduced a new class (HtmlPopulator) to populate HTML templates with data from a Questionnaire Response 3. Implemented functionality to launch PDF generation using a configuration setup -- Added Save draft MVP functionality +- Added Save draft MVP functionality +- Added Delete saved draft feature ## [1.1.0] - 2024-02-15 From 14121ce7a28ce75f4560ae96804c50be9ffe3cbb Mon Sep 17 00:00:00 2001 From: Rkareko Date: Mon, 2 Dec 2024 11:58:07 +0300 Subject: [PATCH 15/21] Verify filtering by encounter works when when searching for latest QR --- .../engine/data/local/DefaultRepositoryTest.kt | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/android/engine/src/test/java/org/smartregister/fhircore/engine/data/local/DefaultRepositoryTest.kt b/android/engine/src/test/java/org/smartregister/fhircore/engine/data/local/DefaultRepositoryTest.kt index f27b62db614..930e240264a 100644 --- a/android/engine/src/test/java/org/smartregister/fhircore/engine/data/local/DefaultRepositoryTest.kt +++ b/android/engine/src/test/java/org/smartregister/fhircore/engine/data/local/DefaultRepositoryTest.kt @@ -1651,12 +1651,17 @@ class DefaultRepositoryTest : RobolectricTest() { @Test fun testSearchLatestQuestionnaireResponseShouldReturnLatestQuestionnaireResponse() = runTest(timeout = 90.seconds) { + val sampleEncounter = + Encounter().apply { + id = "encounter-id-1" + subject = patient.asReference() + } Assert.assertNull( defaultRepository.searchQuestionnaireResponse( resourceId = patient.logicalId, resourceType = ResourceType.Patient, questionnaireId = questionnaireConfig.id, - encounterId = null, + encounterId = sampleEncounter.id, ), ) @@ -1667,6 +1672,7 @@ class DefaultRepositoryTest : RobolectricTest() { meta.lastUpdated = Date() subject = patient.asReference() questionnaire = samplePatientRegisterQuestionnaire.asReference().reference + encounter = sampleEncounter.asReference() }, QuestionnaireResponse().apply { id = "qr2" @@ -1688,10 +1694,14 @@ class DefaultRepositoryTest : RobolectricTest() { resourceId = patient.logicalId, resourceType = ResourceType.Patient, questionnaireId = questionnaireConfig.id, - encounterId = null, + encounterId = sampleEncounter.id, ) Assert.assertNotNull(latestQuestionnaireResponse) Assert.assertEquals("QuestionnaireResponse/qr1", latestQuestionnaireResponse?.id) + Assert.assertEquals( + "Encounter/encounter-id-1", + latestQuestionnaireResponse?.encounter?.reference + ) } @Test From 5507708f80be11d8c52c03684daee4738a9ffc01 Mon Sep 17 00:00:00 2001 From: Rkareko Date: Mon, 2 Dec 2024 12:06:32 +0300 Subject: [PATCH 16/21] Run spotless Apply --- .../fhircore/engine/data/local/DefaultRepositoryTest.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/android/engine/src/test/java/org/smartregister/fhircore/engine/data/local/DefaultRepositoryTest.kt b/android/engine/src/test/java/org/smartregister/fhircore/engine/data/local/DefaultRepositoryTest.kt index 930e240264a..402a60885ac 100644 --- a/android/engine/src/test/java/org/smartregister/fhircore/engine/data/local/DefaultRepositoryTest.kt +++ b/android/engine/src/test/java/org/smartregister/fhircore/engine/data/local/DefaultRepositoryTest.kt @@ -1700,7 +1700,7 @@ class DefaultRepositoryTest : RobolectricTest() { Assert.assertEquals("QuestionnaireResponse/qr1", latestQuestionnaireResponse?.id) Assert.assertEquals( "Encounter/encounter-id-1", - latestQuestionnaireResponse?.encounter?.reference + latestQuestionnaireResponse?.encounter?.reference, ) } From a7e3a83724dcbbf364dfbdc4aef9e23b9ea3e994 Mon Sep 17 00:00:00 2001 From: Rkareko <47570855+Rkareko@users.noreply.github.com> Date: Tue, 3 Dec 2024 08:26:07 +0300 Subject: [PATCH 17/21] Update test name Co-authored-by: Martin Ndegwa --- .../fhircore/engine/data/local/DefaultRepositoryTest.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/android/engine/src/test/java/org/smartregister/fhircore/engine/data/local/DefaultRepositoryTest.kt b/android/engine/src/test/java/org/smartregister/fhircore/engine/data/local/DefaultRepositoryTest.kt index 402a60885ac..04495a79628 100644 --- a/android/engine/src/test/java/org/smartregister/fhircore/engine/data/local/DefaultRepositoryTest.kt +++ b/android/engine/src/test/java/org/smartregister/fhircore/engine/data/local/DefaultRepositoryTest.kt @@ -1705,7 +1705,7 @@ class DefaultRepositoryTest : RobolectricTest() { } @Test - fun testSearchLatestQuestionnaireResponseWhenSaveDraftIsTueShouldReturnLatestQuestionnaireResponse() = + fun testSearchLatestQuestionnaireResponseWhenSaveDraftIsTrueShouldReturnLatestQuestionnaireResponse() = runTest(timeout = 90.seconds) { Assert.assertNull( defaultRepository.searchQuestionnaireResponse( From 661194b6137ecd3aa2c0257b142dfa33642a561b Mon Sep 17 00:00:00 2001 From: Rkareko Date: Tue, 3 Dec 2024 10:11:30 +0300 Subject: [PATCH 18/21] Add documentation for delete draft functionality --- .../configuring/forms/save-form-as-draft.mdx | 127 ++++++++++++++++-- 1 file changed, 116 insertions(+), 11 deletions(-) diff --git a/docs/engineering/app/configuring/forms/save-form-as-draft.mdx b/docs/engineering/app/configuring/forms/save-form-as-draft.mdx index c9d33123c45..8796b64cc91 100644 --- a/docs/engineering/app/configuring/forms/save-form-as-draft.mdx +++ b/docs/engineering/app/configuring/forms/save-form-as-draft.mdx @@ -16,19 +16,25 @@ This excludes forms such as "register client" or "register household". - A health care worker is doing a household visit and providing care to multiple household members. They want the ability to start a workflow and switch to another workflow without losing their data - A health care worker is required to collect data in both the app and on paper. They start a form in the app, but are under time pressure, so they fill out the paper form and plan to enter the data in the app later - +The save draft functionality can be configured using the `LAUNCH_QUESTIONNAIRE` or the `DELETE_DRAFT_QUESTIONNAIRE` workflow. The configuration is done on the `QuestionnaireConfig`. The sample below demonstrates the configs that are required in order to save a form as a draft ```json { - "questionnaire": { - "id": "add-family-member", - "title": "Add Family Member", - "resourceIdentifier": "sample-house-id", - "resourceType": "Group", - "saveDraft": true - } + "actions": [ + { + "trigger": "ON_CLICK", + "workflow": "LAUNCH_QUESTIONNAIRE", + "questionnaire": { + "id": "add-family-member", + "title": "Add Family Member", + "resourceIdentifier": "sample-house-id", + "resourceType": "Group", + "saveDraft": true + } + } + ] } ``` ## Config properties @@ -41,15 +47,114 @@ resourceIdentifier | Unique ID String for the subject of the form | resourceType | The String representation of the resource type for the subject of the form | yes | | saveDraft | Flag that determines whether the form can be saved as a draft | yes | false | -## UI/UX workflow +### UI/UX workflow for saving a form as draft When the form is opened, with the configurations in place, the save as draft functionality is triggered when the user clicks on the close button (X) at the top left of the screen. A dialog appears with 3 buttons i.e `Save as draft`, `Discard changes` and `Cancel`. The table below details what each of the buttons does. -### Alert dialog buttons descriptions +#### Alert dialog buttons descriptions |Button | Description | |--|--|:--:|:--:| Save as draft | Saves user input as a draft | Discard changes | Dismisses user input, and closes the form without saving the draft. | -Cancel | Dismisses the dialog so that the user can continue interacting with the form | \ No newline at end of file +Cancel | Dismisses the dialog so that the user can continue interacting with the form | + +## Launching save draft from DELETE_DRAFT_QUESTIONNAIRE workflow +The save draft functionality works the same as described above when launched using the `DELETE_DRAFT_QUESTIONNAIRE` workflow. +The workflow adds another dialog that allows the user to either open or delete the draft. +The sample below demonstrates the configs that are required in order to save a form as a draft and also delete the draft. +```json +{ + "actions": [ + { + "trigger": "ON_CLICK", + "workflow": "DELETE_DRAFT_QUESTIONNAIRE", + "questionnaire": { + "id": "add-family-member", + "title": "Add Family Member", + "resourceIdentifier": "sample-house-id", + "resourceType": "Group", + "saveDraft": true + } + } + ] +} +``` + +### UI/UX workflow for deleting a draft form +When the `DELETE_DRAFT_QUESTIONNAIRE` workflow is configured, a dialog appears when the call to action is triggered. +The dialog has 3 buttons i.e `Open draft`, `Delete draft` and `Cancel`. + +The table below details what each of the buttons does. + +|Button | Description | +|:--|:--| +Open draft | Opens the questionnaire pre-filled with the saved draft changes | +Delete draft | Does a soft delete of the draft i.e update the status of the `QuestionnaireResponse` to `stopped` | +Cancel | Dismisses the dialog | + +### Propagating deletes to other devices +Since the devices work offline, there is a chance that a draft that has been deleted on device A could have some local changes on a device B. +Due to the way conflict resolution works, at the moment, when device B syncs the changes that indicate the draft has been deleted will not reflect on device B. +With this in mind, Event Management is used to update the deleted drafts in the background. + +The following is a sample config that would be added to the `application_config.json` +``` +{ + "eventWorkflows": [ + { + "eventType": "RESOURCE_CLOSURE", + "triggerConditions": [ + { + "eventResourceId": "draftFormToBeClosed", + "matchAll": false, + "conditionalFhirPathExpressions": [ + "true" + ] + } + ], + "eventResources": [ + { + "id": "draftFormToBeClosed", + "resource": "AuditEvent", + "dataQueries": [ + { + "paramName": "type", + "filterCriteria": [ + { + "dataType": "CODE", + "value": { + "system": "http://smartregister.org/", + "code": "delete_draft" + } + } + ] + } + ], + "relatedResources": [ + { + "resource": "QuestionnaireResponse", + "searchParameter": "entity", + "isRevInclude": false + } + ] + } + ], + "updateValues": [ + { + "jsonPathExpression": "QuestionnaireResponse.status", + "value": "stopped", + "resourceType": "QuestionnaireResponse" + } + ], + "resourceFilterExpressions": [] + } + ] +} +``` + +An `AuditEvent` resource is used to keep track of deleted drafts. It has a reference to the `QuestionnaireResponse` in the `entity` field. +The event management functionality fetches all the `AuditEvents` that have the `type` = `delete_draft`. +Then fetches the related `QuestionnaireResponses` by doing a forward include search on the `QuestionnaireResponse.entity` field. +The status for the retrieved `QuestionnaireResponses` is then updated to `stopped` i.e the draft is soft deleted. \ No newline at end of file From 067f0ed9cf990bc949764b1083e2efa1098aaf3e Mon Sep 17 00:00:00 2001 From: Rkareko Date: Tue, 3 Dec 2024 17:01:58 +0300 Subject: [PATCH 19/21] Add qustionnaire draft dialog view tests --- .../QuestionnaireDraftDialogViewModelTest.kt | 197 ++++++++++++++++++ 1 file changed, 197 insertions(+) create mode 100644 android/quest/src/test/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireDraftDialogViewModelTest.kt diff --git a/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireDraftDialogViewModelTest.kt b/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireDraftDialogViewModelTest.kt new file mode 100644 index 00000000000..14fbdcfae7c --- /dev/null +++ b/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireDraftDialogViewModelTest.kt @@ -0,0 +1,197 @@ +/* + * Copyright 2021-2024 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.questionnaire + +import android.app.Application +import androidx.test.core.app.ApplicationProvider +import ca.uhn.fhir.parser.IParser +import com.google.android.fhir.FhirEngine +import com.google.android.fhir.get +import com.google.android.fhir.search.Search +import dagger.hilt.android.testing.HiltAndroidRule +import dagger.hilt.android.testing.HiltAndroidTest +import io.mockk.mockk +import io.mockk.spyk +import javax.inject.Inject +import kotlin.time.Duration.Companion.seconds +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.test.runTest +import org.hl7.fhir.r4.model.AuditEvent +import org.hl7.fhir.r4.model.QuestionnaireResponse +import org.hl7.fhir.r4.model.ResourceType +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.smartregister.fhircore.engine.configuration.QuestionnaireConfig +import org.smartregister.fhircore.engine.configuration.app.ConfigService +import org.smartregister.fhircore.engine.data.local.ContentCache +import org.smartregister.fhircore.engine.data.local.DefaultRepository +import org.smartregister.fhircore.engine.rulesengine.ConfigRulesExecutor +import org.smartregister.fhircore.engine.rulesengine.ResourceDataRulesExecutor +import org.smartregister.fhircore.engine.util.DispatcherProvider +import org.smartregister.fhircore.engine.util.SharedPreferenceKey +import org.smartregister.fhircore.engine.util.SharedPreferencesHelper +import org.smartregister.fhircore.engine.util.extension.asReference +import org.smartregister.fhircore.engine.util.fhirpath.FhirPathDataExtractor +import org.smartregister.fhircore.quest.app.fakes.Faker +import org.smartregister.fhircore.quest.robolectric.RobolectricTest +import org.smartregister.fhircore.quest.ui.questionnaire.QuestionnaireDraftDialogViewModel.Companion.AUDIT_EVENT_CODE +import org.smartregister.fhircore.quest.ui.questionnaire.QuestionnaireDraftDialogViewModel.Companion.AUDIT_EVENT_DISPLAY +import org.smartregister.fhircore.quest.ui.questionnaire.QuestionnaireDraftDialogViewModel.Companion.AUDIT_EVENT_SYSTEM + +@HiltAndroidTest +class QuestionnaireDraftDialogViewModelTest : RobolectricTest() { + + @get:Rule(order = 0) val hiltRule = HiltAndroidRule(this) + + @Inject lateinit var sharedPreferencesHelper: SharedPreferencesHelper + + @Inject lateinit var configService: ConfigService + + @Inject lateinit var resourceDataRulesExecutor: ResourceDataRulesExecutor + + @Inject lateinit var fhirPathDataExtractor: FhirPathDataExtractor + + @Inject lateinit var fhirEngine: FhirEngine + + @Inject lateinit var dispatcherProvider: DispatcherProvider + + @Inject lateinit var parser: IParser + + @Inject lateinit var contentCache: ContentCache + + private lateinit var questionnaireDraftDialogViewModel: QuestionnaireDraftDialogViewModel + lateinit var defaultRepository: DefaultRepository + private val configurationRegistry = Faker.buildTestConfigurationRegistry() + private val context: Application = ApplicationProvider.getApplicationContext() + private val configRulesExecutor: ConfigRulesExecutor = mockk() + lateinit var questionnaireConfig: QuestionnaireConfig + lateinit var questionnaireResponse: QuestionnaireResponse + private val practitionerId = "practitioner-id-1" + + @Before + @ExperimentalCoroutinesApi + fun setUp() { + hiltRule.inject() + // Write practitioner and organization to shared preferences + sharedPreferencesHelper.write( + SharedPreferenceKey.PRACTITIONER_ID.name, + practitionerId, + ) + defaultRepository = + spyk( + DefaultRepository( + fhirEngine = fhirEngine, + dispatcherProvider = dispatcherProvider, + sharedPreferencesHelper = sharedPreferencesHelper, + configurationRegistry = configurationRegistry, + configService = configService, + configRulesExecutor = configRulesExecutor, + fhirPathDataExtractor = fhirPathDataExtractor, + parser = parser, + context = context, + contentCache = contentCache, + ), + ) + questionnaireDraftDialogViewModel = + spyk( + QuestionnaireDraftDialogViewModel( + defaultRepository = defaultRepository, + sharedPreferencesHelper = sharedPreferencesHelper, + ), + ) + + questionnaireConfig = + QuestionnaireConfig( + id = "dc-clinic-medicines", + resourceType = ResourceType.Patient, + resourceIdentifier = "Patient-id-1", + ) + questionnaireResponse = + QuestionnaireResponse().apply { + id = "qr-id-1" + status = QuestionnaireResponse.QuestionnaireResponseStatus.INPROGRESS + subject = "Patient-id-1".asReference(ResourceType.Patient) + questionnaire = "Questionnaire/dc-clinic-medicines" + } + } + + @Test + fun testDeleteDraftUpdateQuestionnaireResponseStatusToStoppedAndAuditEvent() { + runTest(timeout = 90.seconds) { + // add QR to db + fhirEngine.create(questionnaireResponse) + val savedDraft = fhirEngine.get("qr-id-1") + assertEquals("QuestionnaireResponse/qr-id-1", savedDraft.id) + assertEquals("Patient/Patient-id-1", savedDraft.subject.reference) + assertEquals("Questionnaire/dc-clinic-medicines", savedDraft.questionnaire) + assertEquals("in-progress", savedDraft.status.toCode()) + + runBlocking { + questionnaireDraftDialogViewModel.deleteDraft(questionnaireConfig = questionnaireConfig) + } + + val deletedDraft = fhirEngine.get("qr-id-1") + assertEquals("QuestionnaireResponse/qr-id-1", deletedDraft.id) + assertEquals("Patient/Patient-id-1", deletedDraft.subject.reference) + assertEquals("Questionnaire/dc-clinic-medicines", deletedDraft.questionnaire) + assertEquals("stopped", deletedDraft.status.toCode()) + + val search = + Search(ResourceType.AuditEvent).apply { + filter( + AuditEvent.SOURCE, + { value = "Patient-id-1".asReference(ResourceType.Patient).reference }, + ) + filter( + AuditEvent.TYPE, + { value = of("delete_draft") }, + ) + } + + val createdAuditEventList = defaultRepository.search(search) + assertNotNull(createdAuditEventList) + assertEquals( + "QuestionnaireResponse/qr-id-1", + createdAuditEventList[0].entity[0].what.reference, + ) + assertEquals( + "Practitioner/practitioner-id-1", + createdAuditEventList[0].agent[0].who.reference, + ) + assertEquals("Patient/Patient-id-1", createdAuditEventList[0].source.observer.reference) + } + } + + @Test + fun testCreateDeleteDraftFlag() { + val auditEvent = + questionnaireDraftDialogViewModel.createDeleteDraftAuditEvent( + questionnaireConfig = questionnaireConfig, + questionnaireResponse = questionnaireResponse, + ) + + assertEquals("Patient/Patient-id-1", auditEvent.source.observer.reference) + assertEquals("Practitioner/practitioner-id-1", auditEvent.agent[0].who.reference) + assertEquals(AUDIT_EVENT_SYSTEM, auditEvent.type.system) + assertEquals(AUDIT_EVENT_CODE, auditEvent.type.code) + assertEquals(AUDIT_EVENT_DISPLAY, auditEvent.type.display) + } +} From bdeafa9fa71e8e92f09b6ca4ff5e21f12a80e860 Mon Sep 17 00:00:00 2001 From: Rkareko Date: Wed, 4 Dec 2024 07:27:58 +0300 Subject: [PATCH 20/21] Run questionnaire soft deletion and audit event creation in a transaction block --- .../questionnaire/QuestionnaireDraftDialogViewModel.kt | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireDraftDialogViewModel.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireDraftDialogViewModel.kt index b1d6c905fdd..360a74b426f 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireDraftDialogViewModel.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireDraftDialogViewModel.kt @@ -69,10 +69,12 @@ constructor( if (questionnaireResponse != null) { questionnaireResponse.status = QuestionnaireResponseStatus.STOPPED - defaultRepository.update(questionnaireResponse) - defaultRepository.addOrUpdate( - resource = createDeleteDraftAuditEvent(questionnaireConfig, questionnaireResponse), - ) + defaultRepository.applyDbTransaction { + defaultRepository.update(questionnaireResponse) + defaultRepository.addOrUpdate( + resource = createDeleteDraftAuditEvent(questionnaireConfig, questionnaireResponse), + ) + } } } From eb0de4c628eee7f829142f0f1bcabda42920e093 Mon Sep 17 00:00:00 2001 From: Rkareko Date: Wed, 4 Dec 2024 07:51:54 +0300 Subject: [PATCH 21/21] Update docs --- .../app/configuring/forms/save-form-as-draft.mdx | 7 ------- 1 file changed, 7 deletions(-) diff --git a/docs/engineering/app/configuring/forms/save-form-as-draft.mdx b/docs/engineering/app/configuring/forms/save-form-as-draft.mdx index 8796b64cc91..a6457a835bd 100644 --- a/docs/engineering/app/configuring/forms/save-form-as-draft.mdx +++ b/docs/engineering/app/configuring/forms/save-form-as-draft.mdx @@ -106,13 +106,6 @@ The following is a sample config that would be added to the `application_config. { "eventType": "RESOURCE_CLOSURE", "triggerConditions": [ - { - "eventResourceId": "draftFormToBeClosed", - "matchAll": false, - "conditionalFhirPathExpressions": [ - "true" - ] - } ], "eventResources": [ {