From 2cdc66851d290464eff02ab5b6e3c91680e5a300 Mon Sep 17 00:00:00 2001 From: Elly Kitoto Date: Wed, 9 Oct 2024 10:04:03 +0300 Subject: [PATCH 1/3] Support search by dynamic queries Signed-off-by: Elly Kitoto --- .../register/RegisterContentConfig.kt | 1 + .../ui/register/RegisterScreenTest.kt | 18 +- .../quest/ui/register/RegisterScreen.kt | 2 +- .../quest/ui/register/RegisterUiState.kt | 3 +- .../quest/ui/register/RegisterViewModel.kt | 218 ++++++++++++++++-- 5 files changed, 207 insertions(+), 35 deletions(-) diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/register/RegisterContentConfig.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/register/RegisterContentConfig.kt index 16d6f2a1a5..4dc64dd3e8 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/register/RegisterContentConfig.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/register/RegisterContentConfig.kt @@ -27,4 +27,5 @@ data class RegisterContentConfig( val visible: Boolean? = null, val computedRules: List? = null, val searchByQrCode: Boolean? = null, + val dataFilterFields: List = emptyList(), ) diff --git a/android/quest/src/androidTest/java/org/smartregister/fhircore/quest/integration/ui/register/RegisterScreenTest.kt b/android/quest/src/androidTest/java/org/smartregister/fhircore/quest/integration/ui/register/RegisterScreenTest.kt index 79f6f5ec2d..62627c8e33 100644 --- a/android/quest/src/androidTest/java/org/smartregister/fhircore/quest/integration/ui/register/RegisterScreenTest.kt +++ b/android/quest/src/androidTest/java/org/smartregister/fhircore/quest/integration/ui/register/RegisterScreenTest.kt @@ -131,7 +131,7 @@ class RegisterScreenTest { pagesCount = 0, progressPercentage = flowOf(0), isSyncUpload = flowOf(false), - params = emptyMap(), + params = emptyList(), ) val searchText = mutableStateOf(SearchQuery.emptyText) val currentPage = mutableStateOf(0) @@ -172,7 +172,7 @@ class RegisterScreenTest { pagesCount = 1, progressPercentage = flowOf(0), isSyncUpload = flowOf(false), - params = emptyMap(), + params = emptyList(), ) val searchText = mutableStateOf(SearchQuery.emptyText) val currentPage = mutableStateOf(0) @@ -216,7 +216,7 @@ class RegisterScreenTest { pagesCount = 1, progressPercentage = flowOf(0), isSyncUpload = flowOf(false), - params = emptyMap(), + params = emptyList(), ) val searchText = mutableStateOf(SearchQuery.emptyText) val currentPage = mutableStateOf(0) @@ -260,7 +260,7 @@ class RegisterScreenTest { pagesCount = 1, progressPercentage = flowOf(0), isSyncUpload = flowOf(false), - params = emptyMap(), + params = emptyList(), ) val searchText = mutableStateOf(SearchQuery.emptyText) val currentPage = mutableStateOf(0) @@ -305,7 +305,7 @@ class RegisterScreenTest { pagesCount = 0, progressPercentage = flowOf(0), isSyncUpload = flowOf(false), - params = emptyMap(), + params = emptyList(), ) val searchText = mutableStateOf(SearchQuery.emptyText) val currentPage = mutableStateOf(0) @@ -351,7 +351,7 @@ class RegisterScreenTest { pagesCount = 0, progressPercentage = flowOf(0), isSyncUpload = flowOf(false), - params = emptyMap(), + params = emptyList(), ) val searchText = mutableStateOf(SearchQuery.emptyText) val currentPage = mutableStateOf(0) @@ -436,7 +436,7 @@ class RegisterScreenTest { ), ), ), - params = emptyMap(), + params = emptyList(), ) val searchText = mutableStateOf(SearchQuery.emptyText) val currentPage = mutableStateOf(0) @@ -488,7 +488,7 @@ class RegisterScreenTest { progressPercentage = flowOf(100), isSyncUpload = flowOf(false), currentSyncJobStatus = flowOf(CurrentSyncJobStatus.Succeeded(OffsetDateTime.now())), - params = emptyMap(), + params = emptyList(), ) val searchText = mutableStateOf(SearchQuery.emptyText) val currentPage = mutableStateOf(0) @@ -539,7 +539,7 @@ class RegisterScreenTest { progressPercentage = flowOf(100), isSyncUpload = flowOf(false), currentSyncJobStatus = flowOf(CurrentSyncJobStatus.Succeeded(OffsetDateTime.now())), - params = emptyMap(), + params = emptyList(), ) val searchText = mutableStateOf(SearchQuery.emptyText) val currentPage = mutableStateOf(0) diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/register/RegisterScreen.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/register/RegisterScreen.kt index 78ecbd3b93..be92daf11f 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/register/RegisterScreen.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/register/RegisterScreen.kt @@ -292,7 +292,7 @@ fun RegisterScreenWithDataPreview() { pagesCount = 1, progressPercentage = flowOf(0), isSyncUpload = flowOf(false), - params = emptyMap(), + params = emptyList(), ) val searchText = remember { mutableStateOf(SearchQuery.emptyText) } val currentPage = remember { mutableIntStateOf(0) } diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/register/RegisterUiState.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/register/RegisterUiState.kt index 56f8ee43da..49698fa760 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/register/RegisterUiState.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/register/RegisterUiState.kt @@ -20,6 +20,7 @@ import com.google.android.fhir.sync.CurrentSyncJobStatus import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flowOf import org.smartregister.fhircore.engine.configuration.register.RegisterConfiguration +import org.smartregister.fhircore.engine.domain.model.ActionParameter data class RegisterUiState( val screenTitle: String = "", @@ -32,5 +33,5 @@ data class RegisterUiState( val progressPercentage: Flow = flowOf(0), val isSyncUpload: Flow = flowOf(false), val currentSyncJobStatus: Flow = flowOf(null), - val params: Map = emptyMap(), + val params: List = emptyList(), ) diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/register/RegisterViewModel.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/register/RegisterViewModel.kt index 1aa74b6290..82389aa8f4 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/register/RegisterViewModel.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/register/RegisterViewModel.kt @@ -38,9 +38,22 @@ import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch +import org.hl7.fhir.r4.model.CodeType +import org.hl7.fhir.r4.model.CodeableConcept import org.hl7.fhir.r4.model.Coding +import org.hl7.fhir.r4.model.DateTimeType +import org.hl7.fhir.r4.model.DateType +import org.hl7.fhir.r4.model.DecimalType import org.hl7.fhir.r4.model.Enumerations.DataType +import org.hl7.fhir.r4.model.IntegerType +import org.hl7.fhir.r4.model.Quantity import org.hl7.fhir.r4.model.QuestionnaireResponse +import org.hl7.fhir.r4.model.QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent +import org.hl7.fhir.r4.model.Reference +import org.hl7.fhir.r4.model.StringType +import org.hl7.fhir.r4.model.TimeType +import org.hl7.fhir.r4.model.UriType +import org.hl7.fhir.r4.model.UrlType import org.smartregister.fhircore.engine.configuration.ConfigType import org.smartregister.fhircore.engine.configuration.ConfigurationRegistry import org.smartregister.fhircore.engine.configuration.app.ApplicationConfiguration @@ -127,7 +140,7 @@ constructor( resourceDataRulesExecutor = resourceDataRulesExecutor, ruleConfigs = ruleConfigs, fhirResourceConfig = registerFilterState.value.fhirResourceConfig, - actionParameters = registerUiState.value.params, + actionParameters = registerUiState.value.params.toTypedArray().toParamDataMap(), ) .apply { setPatientPagingSourceState( @@ -149,7 +162,11 @@ constructor( // Ensures register configuration is initialized once if (!::registerConfiguration.isInitialized) { registerConfiguration = - configurationRegistry.retrieveConfiguration(ConfigType.Register, registerId, paramMap) + configurationRegistry.retrieveConfiguration( + ConfigType.Register, + registerId, + paramMap, + ) } return registerConfiguration } @@ -171,10 +188,20 @@ constructor( is RegisterEvent.SearchRegister -> { if (event.searchQuery.isBlank()) { val regConfig = retrieveRegisterConfiguration(registerId) - if (regConfig.infiniteScroll) { - registerData.value = retrieveCompleteRegisterData(registerId, false) - } else { - paginateRegisterData(registerId) + val searchByDynamicQueries = !regConfig.searchBar?.dataFilterFields.isNullOrEmpty() + if (searchByDynamicQueries) { + registerFilterState.value = RegisterFilterState() // Reset queries + } + when { + regConfig.infiniteScroll -> + registerData.value = retrieveCompleteRegisterData(registerId, searchByDynamicQueries) + else -> + retrieveRegisterUiState( + registerId = registerId, + screenTitle = registerUiState.value.screenTitle, + params = registerUiState.value.params.toTypedArray(), + clearCache = searchByDynamicQueries, + ) } } else { filterRegisterData(event.searchQuery.query) @@ -194,23 +221,157 @@ constructor( fun filterRegisterData(searchText: String) { val searchBar = registerUiState.value.registerConfiguration?.searchBar - // computedRules (names of pre-computed rules) must be provided for search to work. - if (searchBar?.computedRules != null) { + val registerId = registerUiState.value.registerId + if (!searchBar?.dataFilterFields.isNullOrEmpty()) { + val dataFilterFields = searchBar?.dataFilterFields + updateRegisterFilterState( + registerId = registerId, + questionnaireResponse = + constructSearchQuestionnaireResponse( + searchText = searchText, + dataFilterFields = searchBar?.dataFilterFields ?: emptyList(), + ), + dataFilterFields = dataFilterFields, + ) + paginateRegisterData(registerId = registerId, loadAll = true, clearCache = true) + } else if (searchBar?.computedRules != null) { registerData.value = - retrieveCompleteRegisterData(registerUiState.value.registerId, false).map { - pagingData: PagingData -> - pagingData.filter { resourceData: ResourceData -> - searchBar.computedRules!!.any { ruleName -> - // if ruleName not found in map return {-1}; check always return false hence no data - val value = resourceData.computedValuesMap[ruleName]?.toString() ?: "{-1}" - value.contains(other = searchText, ignoreCase = true) + retrieveCompleteRegisterData( + registerId = registerId, + forceRefresh = false, + ) + .map { pagingData: PagingData, + -> + pagingData.filter { resourceData: ResourceData -> + searchBar.computedRules!!.any { ruleName -> + // if ruleName not found in map return {-1}; check always return false hence no data + val value = resourceData.computedValuesMap[ruleName]?.toString() ?: "{-1}" + value.contains(other = searchText, ignoreCase = true) + } } } - } } } - fun updateRegisterFilterState(registerId: String, questionnaireResponse: QuestionnaireResponse) { + private fun constructSearchQuestionnaireResponse( + searchText: String, + dataFilterFields: List, + ): QuestionnaireResponse { + val questionnaireResponse = QuestionnaireResponse() + dataFilterFields.forEach { + it.dataQueries.mapToQRItems(questionnaireResponse, searchText) + it.nestedSearchResources?.forEach { nestedSearchConfig -> + nestedSearchConfig.dataQueries.mapToQRItems(questionnaireResponse, searchText) + } + } + return questionnaireResponse + } + + private fun List?.mapToQRItems( + questionnaireResponse: QuestionnaireResponse, + searchText: String, + ) { + this?.forEach { dataQuery -> + dataQuery.filterCriteria.map { filterCriterionConfig -> + questionnaireResponse.addItem( + QuestionnaireResponse.QuestionnaireResponseItemComponent( + StringType(filterCriterionConfig.dataFilterLinkId), + ) + .apply { + when (filterCriterionConfig.dataType) { + DataType.QUANTITY -> + addAnswer( + QuestionnaireResponseItemAnswerComponent().apply { + value = Quantity(searchText.toDouble()) + }, + ) + DataType.DATETIME -> + addAnswer( + QuestionnaireResponseItemAnswerComponent().apply { + value = DateTimeType(searchText) + }, + ) + DataType.DATE -> + addAnswer( + QuestionnaireResponseItemAnswerComponent().apply { + value = DateType(searchText) + }, + ) + DataType.TIME -> + addAnswer( + QuestionnaireResponseItemAnswerComponent().apply { + value = TimeType(searchText) + }, + ) + DataType.DECIMAL -> + addAnswer( + QuestionnaireResponseItemAnswerComponent().apply { + value = DecimalType(searchText) + }, + ) + DataType.INTEGER -> + addAnswer( + QuestionnaireResponseItemAnswerComponent().apply { + value = IntegerType(searchText) + }, + ) + DataType.STRING -> + addAnswer( + QuestionnaireResponseItemAnswerComponent().apply { + value = StringType(searchText) + }, + ) + DataType.URI -> + addAnswer( + QuestionnaireResponseItemAnswerComponent().apply { + value = UriType(searchText) + }, + ) + DataType.URL -> + addAnswer( + QuestionnaireResponseItemAnswerComponent().apply { + value = UrlType(searchText) + }, + ) + DataType.REFERENCE -> + addAnswer( + QuestionnaireResponseItemAnswerComponent().apply { + value = Reference(searchText) + }, + ) + DataType.CODING -> + addAnswer( + QuestionnaireResponseItemAnswerComponent().apply { + value = Coding("", searchText, "") + }, + ) + DataType.CODEABLECONCEPT -> + addAnswer( + QuestionnaireResponseItemAnswerComponent().apply { + value = CodeableConcept(Coding("", searchText, "")) + }, + ) + DataType.CODE -> + addAnswer( + QuestionnaireResponseItemAnswerComponent().apply { + value = CodeType(searchText) + }, + ) + else -> { + // Type cannot be used in search query + } + } + }, + ) + } + } + } + + fun updateRegisterFilterState( + registerId: String, + questionnaireResponse: QuestionnaireResponse, + dataFilterFields: List? = null, + ) { // Reset filter state if no answer is provided for all the fields if (questionnaireResponse.item.all { !it.hasAnswer() }) { registerFilterState.value = @@ -227,8 +388,7 @@ constructor( val qrItemMap = questionnaireResponse.item.groupBy { it.linkId }.mapValues { it.value.first() } val registerDataFilterFieldsMap = - registerConfiguration.registerFilter - ?.dataFilterFields + (dataFilterFields ?: registerConfiguration.registerFilter?.dataFilterFields) ?.groupBy { it.filterId } ?.mapValues { it.value.first() } @@ -337,7 +497,10 @@ constructor( val answerComponent = qrItemMap[filterCriterionConfig.dataFilterLinkId] answerComponent?.answer?.forEach { itemAnswerComponent -> val criterion = - convertAnswerToFilterCriterion(itemAnswerComponent, filterCriterionConfig) + convertAnswerToFilterCriterion( + itemAnswerComponent, + filterCriterionConfig, + ) if (criterion != null) newFilterCriteria.add(criterion) } } else { @@ -348,7 +511,7 @@ constructor( } private fun convertAnswerToFilterCriterion( - answerComponent: QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent, + answerComponent: QuestionnaireResponseItemAnswerComponent, oldFilterCriterion: FilterCriterionConfig, ): FilterCriterionConfig? = when { @@ -461,7 +624,10 @@ constructor( retrieveCompleteRegisterData(currentRegisterConfiguration.id, clearCache) } else { _totalRecordsCount.longValue = - registerRepository.countRegisterData(registerId = registerId, paramsMap = paramsMap) + registerRepository.countRegisterData( + registerId = registerId, + paramsMap = paramsMap, + ) // Only count filtered data when queries are updated if (registerFilterState.value.fhirResourceConfig != null) { @@ -472,7 +638,11 @@ constructor( fhirResourceConfig = registerFilterState.value.fhirResourceConfig, ) } - paginateRegisterData(registerId = registerId, loadAll = false, clearCache = clearCache) + paginateRegisterData( + registerId = registerId, + loadAll = false, + clearCache = clearCache, + ) } registerUiState.value = @@ -505,7 +675,7 @@ constructor( progressPercentage = _percentageProgress, isSyncUpload = _isUploadSync, currentSyncJobStatus = _currentSyncJobStatusFlow, - params = paramsMap, + params = params?.toList() ?: emptyList(), ) } } From 196b9c531a5a7b883e7a7787fb767d582551cf8d Mon Sep 17 00:00:00 2001 From: Rkareko Date: Fri, 18 Oct 2024 08:43:16 +0300 Subject: [PATCH 2/3] Add custom search param for Patient name and identifier --- .../engine/configuration/app/ConfigService.kt | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/app/ConfigService.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/app/ConfigService.kt index b2a856415e..502cb2cde8 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/app/ConfigService.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/app/ConfigService.kt @@ -109,10 +109,22 @@ interface ConfigService { description = "Search the sort field" } + val patientSearchParameter = + SearchParameter().apply { + url = "http://smartregister.org/SearchParameter/patient-search" + addBase("Patient") + name = SEARCH_PARAM + code = SEARCH_PARAM + type = Enumerations.SearchParamType.STRING + expression = "Patient.name.text | Patient.identifier.value" + description = "Search patients by name and identifier fields" + } + return listOf( activeGroupSearchParameter, flagStatusSearchParameter, medicationSortSearchParameter, + patientSearchParameter, ) } @@ -121,6 +133,7 @@ interface ConfigService { const val APP_VERSION = "AppVersion" const val STATUS_SEARCH_PARAM = "status" const val SORT_SEARCH_PARAM = "sort" + const val SEARCH_PARAM = "search" const val MEDICATION_SORT_URL = "http://smartregister.org/SearchParameter/medication-sort" } } From 1757cac7ecf20b00387931c32028b8c9056964b8 Mon Sep 17 00:00:00 2001 From: Rkareko Date: Fri, 18 Oct 2024 12:11:39 +0300 Subject: [PATCH 3/3] Fix failing test --- .../fhircore/quest/ui/register/RegisterViewModelTest.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/register/RegisterViewModelTest.kt b/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/register/RegisterViewModelTest.kt index a8e7bf5ee7..3746f047c9 100644 --- a/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/register/RegisterViewModelTest.kt +++ b/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/register/RegisterViewModelTest.kt @@ -156,7 +156,7 @@ class RegisterViewModelTest : RobolectricTest() { mutableStateOf(RegisterUiState(registerId = registerId)) // Search with empty string should paginate the data registerViewModel.onEvent(RegisterEvent.SearchRegister(SearchQuery.emptyText)) - verify { registerViewModel.paginateRegisterData(any(), any()) } + verify { registerViewModel.retrieveRegisterUiState(any(), any(), any(), any()) } // Search for the word 'Khan' should call the filterRegisterData function registerViewModel.onEvent(RegisterEvent.SearchRegister(SearchQuery("Khan")))