Skip to content

Commit

Permalink
Filter register data via configuration (#2780)
Browse files Browse the repository at this point in the history
* Filter register data via configuration

Signed-off-by: Elly Kitoto <[email protected]>

* Add missing related resources

Signed-off-by: Elly Kitoto <[email protected]>

* Implement data filters for related resources

Signed-off-by: Elly Kitoto <[email protected]>

* Implement data filtered register count badge

Signed-off-by: Elly Kitoto <[email protected]>

* Fix failing tests

Signed-off-by: Elly Kitoto <[email protected]>

* Test load paging source data with FhirResourceConfig

Signed-off-by: Elly Kitoto <[email protected]>

* Test that refreshData is invoked with QuestionnaireResponse param

Signed-off-by: Elly Kitoto <[email protected]>

* Write test for RegisterViewModel#updateRegisterFilterState

Signed-off-by: Elly Kitoto <[email protected]>

* Implement clear all UI

Signed-off-by: Elly Kitoto <[email protected]>

* Apply no filter if no data field answer is provided

Also fix rendering of badge count. Filtered data can return 0.
Reset default value to -1.

Signed-off-by: Elly Kitoto <[email protected]>

---------

Signed-off-by: Elly Kitoto <[email protected]>
  • Loading branch information
ellykits authored Sep 27, 2023
1 parent da81d9d commit 0ea392b
Show file tree
Hide file tree
Showing 23 changed files with 792 additions and 55 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ data class QuestionnaireConfig(
null,
val saveQuestionnaireResponse: Boolean = true,
val generateCarePlanWithWorkflowApi: Boolean = false,
val showClearAll: Boolean = false,
) : java.io.Serializable, Parcelable {

fun interpolate(computedValuesMap: Map<String, Any>) =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ import org.hl7.fhir.r4.model.ResourceType
import org.smartregister.fhircore.engine.configuration.ConfigType
import org.smartregister.fhircore.engine.configuration.Configuration
import org.smartregister.fhircore.engine.configuration.navigation.NavigationMenuConfig
import org.smartregister.fhircore.engine.domain.model.ActionConfig
import org.smartregister.fhircore.engine.domain.model.FhirResourceConfig
import org.smartregister.fhircore.engine.domain.model.RuleConfig

Expand All @@ -33,7 +32,6 @@ data class RegisterConfiguration(
val registerTitle: String? = null,
val fhirResource: FhirResourceConfig,
val secondaryResources: List<FhirResourceConfig>? = null,
val filter: RegisterContentConfig? = null,
val searchBar: RegisterContentConfig? = null,
val registerCard: RegisterCardConfig = RegisterCardConfig(),
val fabActions: List<NavigationMenuConfig> = emptyList(),
Expand All @@ -45,5 +43,5 @@ data class RegisterConfiguration(
ActiveResourceFilterConfig(resourceType = ResourceType.Group, active = true),
),
val configRules: List<RuleConfig>? = null,
val filterActions: List<ActionConfig> = emptyList(),
val registerFilter: RegisterFilterConfig? = null,
) : Configuration()
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
* Copyright 2021-2023 Ona Systems, Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.smartregister.fhircore.engine.configuration.register

import kotlinx.serialization.Serializable
import org.smartregister.fhircore.engine.domain.model.ActionConfig
import org.smartregister.fhircore.engine.domain.model.DataQuery

@Serializable
data class RegisterFilterConfig(
val dataFilterActions: List<ActionConfig>? = null,
val dataFilterFields: List<RegisterFilterField> = emptyList(),
) : java.io.Serializable

@Serializable
data class RegisterFilterField(
val dataQueries: List<DataQuery>,
val filterId: String,
) : java.io.Serializable
Original file line number Diff line number Diff line change
Expand Up @@ -67,12 +67,13 @@ constructor(
override suspend fun loadRegisterData(
currentPage: Int,
registerId: String,
fhirResourceConfig: FhirResourceConfig?,
paramsMap: Map<String, String>?,
): List<RepositoryResourceData> {
val registerConfiguration = retrieveRegisterConfiguration(registerId, paramsMap)
return searchResourcesRecursively(
filterActiveResources = registerConfiguration.activeResourceFilters,
fhirResourceConfig = registerConfiguration.fhirResource,
fhirResourceConfig = fhirResourceConfig ?: registerConfiguration.fhirResource,
secondaryResourceConfigs = registerConfiguration.secondaryResources,
currentPage = currentPage,
pageSize = registerConfiguration.pageSize,
Expand Down Expand Up @@ -159,10 +160,12 @@ constructor(
/** Count register data for the provided [registerId]. Use the configured base resource filters */
override suspend fun countRegisterData(
registerId: String,
fhirResourceConfig: FhirResourceConfig?,
paramsMap: Map<String, String>?,
): Long {
val registerConfiguration = retrieveRegisterConfiguration(registerId, paramsMap)
val baseResourceConfig = registerConfiguration.fhirResource.baseResource
val fhirResource = fhirResourceConfig ?: registerConfiguration.fhirResource
val baseResourceConfig = fhirResource.baseResource
val configComputedRuleValues = registerConfiguration.configRules.configRulesComputedValues()
val search =
Search(baseResourceConfig.resource).apply {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,14 +41,17 @@ sealed class FilterCriterionConfig : Parcelable, java.io.Serializable {

abstract val dataType: DataType
abstract val computedRule: String?
abstract val dataFilterLinkId: String?
abstract val value: Any?

@Serializable
@Parcelize
data class QuantityFilterCriterionConfig(
override val dataType: DataType = DataType.QUANTITY,
override val computedRule: String? = null,
override val dataFilterLinkId: String? = null,
@Serializable(with = BigDecimalSerializer::class) override val value: BigDecimal? = null,
val prefix: ParamPrefixEnum? = null,
@Serializable(with = BigDecimalSerializer::class) val value: BigDecimal? = null,
val system: String? = null,
val unit: String? = null,
) : FilterCriterionConfig(), Parcelable, java.io.Serializable
Expand All @@ -58,8 +61,9 @@ sealed class FilterCriterionConfig : Parcelable, java.io.Serializable {
data class DateFilterCriterionConfig(
override val dataType: DataType = DataType.DATETIME,
override val computedRule: String? = null,
override val dataFilterLinkId: String? = null,
override val value: String? = null,
val prefix: ParamPrefixEnum = ParamPrefixEnum.GREATERTHAN_OR_EQUALS,
val value: String? = null,
val valueAsDateTime: Boolean = false,
) : FilterCriterionConfig(), Parcelable, java.io.Serializable

Expand All @@ -68,40 +72,45 @@ sealed class FilterCriterionConfig : Parcelable, java.io.Serializable {
data class NumberFilterCriterionConfig(
override val dataType: DataType = DataType.DECIMAL,
override val computedRule: String? = null,
override val dataFilterLinkId: String? = null,
@Serializable(with = BigDecimalSerializer::class) override val value: BigDecimal? = null,
val prefix: ParamPrefixEnum = ParamPrefixEnum.EQUAL,
@Serializable(with = BigDecimalSerializer::class) val value: BigDecimal? = null,
) : FilterCriterionConfig(), Parcelable, java.io.Serializable

@Serializable
@Parcelize
data class StringFilterCriterionConfig(
override val dataType: DataType = DataType.STRING,
override val computedRule: String? = null,
override val dataFilterLinkId: String? = null,
override val value: String? = null,
val modifier: StringFilterModifier = StringFilterModifier.STARTS_WITH,
val value: String? = null,
) : FilterCriterionConfig(), Parcelable, java.io.Serializable

@Serializable
@Parcelize
data class UriFilterCriterionConfig(
override val dataType: DataType = DataType.URI,
override val computedRule: String? = null,
val value: String? = null,
override val dataFilterLinkId: String? = null,
override val value: String? = null,
) : FilterCriterionConfig(), Parcelable, java.io.Serializable

@Serializable
@Parcelize
data class ReferenceFilterCriterionConfig(
override val dataType: DataType = DataType.REFERENCE,
override val computedRule: String? = null,
val value: String? = null,
override val dataFilterLinkId: String? = null,
override val value: String? = null,
) : FilterCriterionConfig(), Parcelable, java.io.Serializable

@Serializable
@Parcelize
data class TokenFilterCriterionConfig(
override val dataType: DataType = DataType.CODE,
override val computedRule: String? = null,
val value: Code? = null,
override val dataFilterLinkId: String? = null,
override val value: Code? = null,
) : FilterCriterionConfig(), Parcelable, java.io.Serializable
}
Original file line number Diff line number Diff line change
Expand Up @@ -73,12 +73,16 @@ data class FhirResourceConfig(
* [CountResultConfig.sumCounts] is set to true, all the related resources counts are computed once
* via one query. However there may be scenarios to return count for each related resource e.g. for
* every Patient in a Group, return their Tasks count.
*
* [filterId] Refers to a unique ID used to identify the Resource in data filter screen (The data
* filter screen renders a questionnaire with the linkIds for the content to be filtered)
*/
@Serializable
@Parcelize
data class ResourceConfig(
val id: String? = null,
val resource: ResourceType,
val filterId: String? = null,
val searchParameter: String? = null,
val isRevInclude: Boolean = true,
val dataQueries: List<DataQuery>? = null,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ interface Repository {
suspend fun loadRegisterData(
currentPage: Int,
registerId: String,
fhirResourceConfig: FhirResourceConfig? = null,
paramsMap: Map<String, String>? = emptyMap(),
): List<RepositoryResourceData>

Expand All @@ -41,6 +42,7 @@ interface Repository {
*/
suspend fun countRegisterData(
registerId: String,
fhirResourceConfig: FhirResourceConfig? = null,
paramsMap: Map<String, String>? = emptyMap(),
): Long

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,8 @@ class RegisterRepositoryTest : RobolectricTest() {
val paramsMap = emptyMap<String, String>()
val searchSlot = slot<Search>()
coEvery { fhirEngine.count(capture(searchSlot)) } returns 20
val recordsCount = registerRepository.countRegisterData(PATIENT_REGISTER, paramsMap)
val recordsCount =
registerRepository.countRegisterData(registerId = PATIENT_REGISTER, paramsMap = paramsMap)
Assert.assertEquals(ResourceType.Patient, searchSlot.captured.type)
Assert.assertEquals(20, recordsCount)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import android.database.SQLException
import androidx.paging.PagingSource
import androidx.paging.PagingState
import org.smartregister.fhircore.engine.data.local.register.RegisterRepository
import org.smartregister.fhircore.engine.domain.model.FhirResourceConfig
import org.smartregister.fhircore.engine.domain.model.ResourceData
import org.smartregister.fhircore.engine.domain.model.RuleConfig
import org.smartregister.fhircore.engine.rulesengine.ResourceDataRulesExecutor
Expand All @@ -34,6 +35,7 @@ class RegisterPagingSource(
private val registerRepository: RegisterRepository,
private val resourceDataRulesExecutor: ResourceDataRulesExecutor,
private val ruleConfigs: List<RuleConfig>,
private val fhirResourceConfig: FhirResourceConfig?,
private val actionParameters: Map<String, String>?,
) : PagingSource<Int, ResourceData>() {

Expand All @@ -58,6 +60,7 @@ class RegisterPagingSource(
registerRepository.loadRegisterData(
currentPage = currentPage,
registerId = _registerPagingSourceState.registerId,
fhirResourceConfig = fhirResourceConfig,
)

val prevKey =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,10 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Badge
import androidx.compose.material.BadgedBox
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.MaterialTheme
Expand All @@ -41,6 +44,7 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import org.smartregister.fhircore.engine.R
Expand Down Expand Up @@ -68,6 +72,7 @@ fun TopScreenSection(
modifier: Modifier = Modifier,
title: String,
searchText: String,
filteredRecordsCount: Long? = null,
searchPlaceholder: String? = null,
toolBarHomeNavigation: ToolBarHomeNavigation = ToolBarHomeNavigation.OPEN_DRAWER,
onSearchTextChanged: (String) -> Unit,
Expand Down Expand Up @@ -100,12 +105,26 @@ fun TopScreenSection(
)
if (isFilterIconEnabled) {
IconButton(onClick = { onClick.invoke(ToolbarClickEvent.FilterData) }) {
Icon(
Icons.Filled.FilterAlt,
contentDescription = FILTER,
tint = Color.White,
modifier = modifier.testTag(TOP_ROW_FILTER_ICON_TEST_TAG),
)
BadgedBox(
badge = {
if (filteredRecordsCount != null && filteredRecordsCount > -1) {
Badge(modifier = Modifier.size(18.dp)) {
Text(
text = filteredRecordsCount.toString(),
overflow = TextOverflow.Clip,
maxLines = 1,
)
}
}
},
) {
Icon(
imageVector = Icons.Default.FilterAlt,
contentDescription = FILTER,
tint = Color.White,
modifier = modifier.testTag(TOP_ROW_FILTER_ICON_TEST_TAG),
)
}
}
}
}
Expand Down Expand Up @@ -161,6 +180,7 @@ fun TopScreenSectionPreview() {
TopScreenSection(
title = "All Clients",
searchText = "Eddy",
filteredRecordsCount = 8,
onSearchTextChanged = {},
toolBarHomeNavigation = ToolBarHomeNavigation.NAVIGATE_BACK,
isFilterIconEnabled = true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import android.app.AlertDialog
import android.content.Intent
import android.os.Bundle
import android.os.Parcelable
import android.view.View
import androidx.activity.OnBackPressedCallback
import androidx.activity.viewModels
import androidx.core.os.bundleOf
Expand Down Expand Up @@ -114,10 +115,18 @@ class QuestionnaireActivity : BaseMultiLanguageActivity() {
lifecycleScope.launch {
if (supportFragmentManager.findFragmentByTag(QUESTIONNAIRE_FRAGMENT_TAG) == null) {
viewModel.setProgressState(QuestionnaireProgressState.QuestionnaireLaunch(true))
viewBinding.questionnaireToolbar.apply {
title = questionnaireConfig.title
setNavigationIcon(R.drawable.ic_arrow_back)
setNavigationOnClickListener { handleBackPress() }
with(viewBinding) {
questionnaireToolbar.apply {
setNavigationIcon(R.drawable.ic_arrow_back)
setNavigationOnClickListener { handleBackPress() }
}
questionnaireTitle.apply { text = questionnaireConfig.title }
clearAll.apply {
visibility = if (questionnaireConfig.showClearAll) View.VISIBLE else View.GONE
setOnClickListener {
// TODO Clear current QuestionnaireResponse items -> SDK
}
}
}

questionnaire = viewModel.retrieveQuestionnaire(questionnaireConfig, actionParameters)
Expand Down Expand Up @@ -227,6 +236,7 @@ class QuestionnaireActivity : BaseMultiLanguageActivity() {
context = this@QuestionnaireActivity,
) { idTypes, questionnaireResponse ->
// Dismiss progress indicator dialog, submit result then finish activity
// TODO Ensure this dialog is dismissed even when an exception is encountered
setProgressState(QuestionnaireProgressState.ExtractionInProgress(false))
setResult(
Activity.RESULT_OK,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@ constructor(
context = context,
)

if (!questionnaireResponseValid) {
if (questionnaireConfig.saveQuestionnaireResponse && !questionnaireResponseValid) {
Timber.e("Invalid questionnaire response")
context.showToast(context.getString(R.string.questionnaire_response_invalid))
return@launch
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,6 @@ sealed class RegisterEvent {
object MoveToNextPage : RegisterEvent()

object MoveToPreviousPage : RegisterEvent()

object ResetFilterRecordsCount : RegisterEvent()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*
* Copyright 2021-2023 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.register

import org.hl7.fhir.r4.model.QuestionnaireResponse
import org.smartregister.fhircore.engine.domain.model.FhirResourceConfig

data class RegisterFilterState(
val fhirResourceConfig: FhirResourceConfig? = null,
val questionnaireResponse: QuestionnaireResponse? = null,
)
Loading

0 comments on commit 0ea392b

Please sign in to comment.