diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/ConfigurationRegistry.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/ConfigurationRegistry.kt index 2c099a831a..f5d22bb5d0 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/ConfigurationRegistry.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/ConfigurationRegistry.kt @@ -58,6 +58,7 @@ import org.smartregister.fhircore.engine.configuration.app.ApplicationConfigurat import org.smartregister.fhircore.engine.configuration.app.ConfigService import org.smartregister.fhircore.engine.data.remote.fhir.resource.FhirResourceDataSource import org.smartregister.fhircore.engine.di.NetworkModule +import org.smartregister.fhircore.engine.domain.model.MultiSelectViewAction import org.smartregister.fhircore.engine.util.DispatcherProvider import org.smartregister.fhircore.engine.util.SharedPreferenceKey import org.smartregister.fhircore.engine.util.SharedPreferencesHelper @@ -72,7 +73,7 @@ import org.smartregister.fhircore.engine.util.extension.generateMissingId import org.smartregister.fhircore.engine.util.extension.interpolate import org.smartregister.fhircore.engine.util.extension.referenceValue import org.smartregister.fhircore.engine.util.extension.retrieveCompositionSections -import org.smartregister.fhircore.engine.util.extension.retrieveRelatedEntitySyncLocationIds +import org.smartregister.fhircore.engine.util.extension.retrieveRelatedEntitySyncLocationState import org.smartregister.fhircore.engine.util.extension.searchCompositionByIdentifier import org.smartregister.fhircore.engine.util.extension.updateLastUpdated import org.smartregister.fhircore.engine.util.helper.LocalizationHelper @@ -743,7 +744,10 @@ constructor( configService.defineResourceTags().find { it.type == ResourceType.Organization.name } val mandatoryTags = configService.provideResourceTags(sharedPreferencesHelper) - val locationIds = context.retrieveRelatedEntitySyncLocationIds() + val locationIds = + context.retrieveRelatedEntitySyncLocationState(MultiSelectViewAction.SYNC_DATA).map { + it.locationId + } syncConfig.parameter .map { it.resource as SearchParameter } diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/view/ImageProperties.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/view/ImageProperties.kt index 3571c42b58..34e347cc4d 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/view/ImageProperties.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/view/ImageProperties.kt @@ -44,7 +44,7 @@ data class ImageProperties( val tint: String? = null, val text: String? = null, val imageConfig: ImageConfig? = null, - val size: Int? = null, + val size: Int? = 22, val shape: ImageShape? = null, val textColor: String? = null, val actions: List = emptyList(), 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 abccc74501..9bad212293 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 @@ -75,6 +75,7 @@ import org.smartregister.fhircore.engine.configuration.register.ActiveResourceFi import org.smartregister.fhircore.engine.domain.model.Code import org.smartregister.fhircore.engine.domain.model.DataQuery import org.smartregister.fhircore.engine.domain.model.FhirResourceConfig +import org.smartregister.fhircore.engine.domain.model.MultiSelectViewAction import org.smartregister.fhircore.engine.domain.model.RelatedResourceCount import org.smartregister.fhircore.engine.domain.model.RepositoryResourceData import org.smartregister.fhircore.engine.domain.model.ResourceConfig @@ -93,7 +94,7 @@ import org.smartregister.fhircore.engine.util.extension.filterBy import org.smartregister.fhircore.engine.util.extension.filterByResourceTypeId import org.smartregister.fhircore.engine.util.extension.generateMissingId import org.smartregister.fhircore.engine.util.extension.loadResource -import org.smartregister.fhircore.engine.util.extension.retrieveRelatedEntitySyncLocationIds +import org.smartregister.fhircore.engine.util.extension.retrieveRelatedEntitySyncLocationState import org.smartregister.fhircore.engine.util.extension.updateFrom import org.smartregister.fhircore.engine.util.extension.updateLastUpdated import org.smartregister.fhircore.engine.util.fhirpath.FhirPathDataExtractor @@ -934,7 +935,11 @@ constructor( configComputedRuleValues: Map, ) = if (filterByRelatedEntityLocation) { - val syncLocationIds = context.retrieveRelatedEntitySyncLocationIds() + val syncLocationIds = + context.retrieveRelatedEntitySyncLocationState(MultiSelectViewAction.FILTER_DATA).map { + it.locationId + } + val locationIds = syncLocationIds .map { retrieveFlattenedSubLocations(it).map { subLocation -> subLocation.logicalId } } @@ -1017,7 +1022,10 @@ constructor( val configComputedRuleValues = configRules.configRulesComputedValues() if (filterByRelatedEntityLocationMetaTag) { - val syncLocationIds = context.retrieveRelatedEntitySyncLocationIds() + val syncLocationIds = + context.retrieveRelatedEntitySyncLocationState(MultiSelectViewAction.FILTER_DATA).map { + it.locationId + } val locationIds = syncLocationIds .map { retrieveFlattenedSubLocations(it).map { subLocation -> subLocation.logicalId } } diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/datastore/ProtoDataStore.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/datastore/ProtoDataStore.kt index 1c40595597..bbea209802 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/datastore/ProtoDataStore.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/datastore/ProtoDataStore.kt @@ -36,10 +36,9 @@ import timber.log.Timber private const val PRACTITIONER_DETAILS_DATASTORE_JSON = "practitioner_details.json" private const val USER_INFO_DATASTORE_JSON = "user_info.json" - private const val LOCATION_COORDINATES_DATASTORE_JSON = "location_coordinates.json" - private const val SYNC_LOCATION_IDS = "sync_location_ids.json" +private const val DATA_FILTER_LOCATION_IDS = "data_filter_location_ids.json" val Context.practitionerProtoStore: DataStore by dataStore( @@ -64,6 +63,11 @@ val Context.syncLocationIdsProtoStore: DataStore> fileName = SYNC_LOCATION_IDS, serializer = SyncLocationIdDataStoreSerializer, ) +val Context.dataFilterLocationIdsProtoStore: DataStore> by + dataStore( + fileName = DATA_FILTER_LOCATION_IDS, + serializer = SyncLocationIdDataStoreSerializer, + ) @Singleton class ProtoDataStore @Inject constructor(@ApplicationContext val context: Context) { diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/domain/model/MultiSelectViewConfig.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/domain/model/MultiSelectViewConfig.kt index 3da938c5fd..f878256987 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/domain/model/MultiSelectViewConfig.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/domain/model/MultiSelectViewConfig.kt @@ -29,6 +29,9 @@ import kotlinx.serialization.Serializable * @property rootNodeFhirPathExpression A key value pair containing a FHIRPath expression for * extracting the value used to identify if the current resource is Root. The key is the FHIRPath * expression while value is the content to compare against. + * @property viewActions The actions to be performed when the multiselect action button is pressed + * @property mutuallyExclusive Setup the multi choice checkbox such that only a single (root level) + * selection can be performed at a time. */ @Serializable @Parcelize @@ -37,4 +40,11 @@ data class MultiSelectViewConfig( val parentIdFhirPathExpression: String, val contentFhirPathExpression: String, val rootNodeFhirPathExpression: KeyValueConfig, + val viewActions: List = listOf(MultiSelectViewAction.FILTER_DATA), + val mutuallyExclusive: Boolean = true, ) : java.io.Serializable, Parcelable + +enum class MultiSelectViewAction { + SYNC_DATA, + FILTER_DATA, +} diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/ui/multiselect/MultiSelectView.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/ui/multiselect/MultiSelectView.kt index 59bb1305d8..0f5bc9a66f 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/ui/multiselect/MultiSelectView.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/ui/multiselect/MultiSelectView.kt @@ -46,6 +46,7 @@ fun ColumnScope.MultiSelectView( rootTreeNode: TreeNode, syncLocationStateMap: MutableMap, depth: Int = 0, + onChecked: () -> Unit, content: @Composable (TreeNode) -> Unit, ) { val collapsedState = remember { mutableStateOf(false) } @@ -55,6 +56,7 @@ fun ColumnScope.MultiSelectView( depth = depth, content = content, collapsedState = collapsedState, + onChecked = onChecked, ) if (collapsedState.value) { rootTreeNode.children.forEach { @@ -63,18 +65,20 @@ fun ColumnScope.MultiSelectView( syncLocationStateMap = syncLocationStateMap, depth = depth + 16, content = content, + onChecked = onChecked, ) } } } @Composable -fun MultiSelectCheckbox( +private fun MultiSelectCheckbox( syncLocationStateMap: MutableMap, currentTreeNode: TreeNode, depth: Int, content: @Composable (TreeNode) -> Unit, collapsedState: MutableState, + onChecked: () -> Unit, ) { val checked = remember { mutableStateOf(false) } Column { @@ -134,19 +138,12 @@ fun MultiSelectCheckbox( parent = parent.parent } - // Select all the nested checkboxes - val treeNodeArrayDeque = ArrayDeque(currentTreeNode.children) - - while (treeNodeArrayDeque.isNotEmpty()) { - val currentNode = treeNodeArrayDeque.removeFirst() - syncLocationStateMap[currentNode.id] = - SyncLocationState( - currentNode.id, - currentNode.parent?.id, - ToggleableState(checked.value), - ) - currentNode.children.forEach { treeNodeArrayDeque.addLast(it) } - } + updateNestedCheckboxState( + currentTreeNode = currentTreeNode, + syncLocationStateMap = syncLocationStateMap, + checked = checked.value, + ) + onChecked() }, modifier = Modifier.padding(0.dp), colors = CheckboxDefaults.colors(checkedColor = MaterialTheme.colors.primary), @@ -156,3 +153,26 @@ fun MultiSelectCheckbox( } } } + +/** + * This function selects/deselects all the children for the [currentTreeNode] based on the value for + * the [checked] parameter. The states for the [MultiSelectCheckbox] is updated in the + * [syncLocationStateMap]. + */ +fun updateNestedCheckboxState( + currentTreeNode: TreeNode, + syncLocationStateMap: MutableMap, + checked: Boolean, +) { + val treeNodeArrayDeque = ArrayDeque(currentTreeNode.children) + while (treeNodeArrayDeque.isNotEmpty()) { + val currentNode = treeNodeArrayDeque.removeFirst() + syncLocationStateMap[currentNode.id] = + SyncLocationState( + locationId = currentNode.id, + parentLocationId = currentNode.parent?.id, + toggleableState = ToggleableState(checked), + ) + currentNode.children.forEach { treeNodeArrayDeque.addLast(it) } + } +} diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/util/extension/AndroidExtensions.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/util/extension/AndroidExtensions.kt index 7d873a8d40..6b7156ae96 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/util/extension/AndroidExtensions.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/util/extension/AndroidExtensions.kt @@ -39,8 +39,13 @@ import androidx.core.view.WindowInsetsCompat import androidx.core.view.updatePadding import java.io.Serializable import java.util.Locale +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.withContext +import org.smartregister.fhircore.engine.datastore.dataFilterLocationIdsProtoStore import org.smartregister.fhircore.engine.datastore.syncLocationIdsProtoStore +import org.smartregister.fhircore.engine.domain.model.MultiSelectViewAction +import org.smartregister.fhircore.engine.domain.model.SyncLocationState import org.smartregister.fhircore.engine.ui.theme.DangerColor import org.smartregister.fhircore.engine.ui.theme.DefaultColor import org.smartregister.fhircore.engine.ui.theme.InfoColor @@ -221,13 +226,25 @@ inline fun Intent.parcelableArrayList(key: String): Arr else -> @Suppress("DEPRECATION") getParcelableArrayListExtra(key) } -suspend fun Context.retrieveRelatedEntitySyncLocationIds(): List { - val selectedLocationStateMap = this.syncLocationIdsProtoStore.data.firstOrNull() - return selectedLocationStateMap - ?.values - ?.filter { +suspend fun Context.retrieveRelatedEntitySyncLocationState( + multiSelectViewAction: MultiSelectViewAction, + filterToggleableStateOn: Boolean = true, +): List { + val selectedLocationStateMap = + withContext(Dispatchers.IO) { + val context = this@retrieveRelatedEntitySyncLocationState + when (multiSelectViewAction) { + MultiSelectViewAction.SYNC_DATA -> context.syncLocationIdsProtoStore.data.firstOrNull() + MultiSelectViewAction.FILTER_DATA -> + context.dataFilterLocationIdsProtoStore.data.firstOrNull() + } + } + return if (filterToggleableStateOn) { + selectedLocationStateMap?.values?.filter { it.toggleableState == ToggleableState.On && selectedLocationStateMap[it.parentLocationId]?.toggleableState != ToggleableState.On } - ?.map { it.locationId } ?: emptyList() + } else { + selectedLocationStateMap?.values?.toList() + } ?: emptyList() } diff --git a/android/engine/src/main/res/drawable/ic_filter.xml b/android/engine/src/main/res/drawable/ic_filter.xml new file mode 100644 index 0000000000..435835305e --- /dev/null +++ b/android/engine/src/main/res/drawable/ic_filter.xml @@ -0,0 +1,9 @@ + + + diff --git a/android/engine/src/main/res/values/strings.xml b/android/engine/src/main/res/values/strings.xml index 7f112e57f0..cb1930e273 100644 --- a/android/engine/src/main/res/values/strings.xml +++ b/android/engine/src/main/res/values/strings.xml @@ -197,4 +197,5 @@ RETRY There\'s some un-synced data Supervisor contact missing or the provided phone number is invalid + APPLY FILTER diff --git a/android/geowidget/src/main/java/org/smartregister/fhircore/geowidget/screens/GeoJsonDataRequester.kt b/android/geowidget/src/main/java/org/smartregister/fhircore/geowidget/screens/GeoJsonDataRequester.kt new file mode 100644 index 0000000000..ce36c09808 --- /dev/null +++ b/android/geowidget/src/main/java/org/smartregister/fhircore/geowidget/screens/GeoJsonDataRequester.kt @@ -0,0 +1,23 @@ +/* + * 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.geowidget.screens + +import org.smartregister.fhircore.geowidget.model.GeoJsonFeature + +interface GeoJsonDataRequester { + fun requestData(onReceiveData: (List) -> Unit) +} diff --git a/android/geowidget/src/main/java/org/smartregister/fhircore/geowidget/screens/GeoWidgetFragment.kt b/android/geowidget/src/main/java/org/smartregister/fhircore/geowidget/screens/GeoWidgetFragment.kt index f8b49da345..96d69fbf51 100644 --- a/android/geowidget/src/main/java/org/smartregister/fhircore/geowidget/screens/GeoWidgetFragment.kt +++ b/android/geowidget/src/main/java/org/smartregister/fhircore/geowidget/screens/GeoWidgetFragment.kt @@ -26,8 +26,10 @@ import androidx.core.content.ContextCompat import androidx.core.content.res.ResourcesCompat import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentManager -import androidx.fragment.app.viewModels +import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle import com.mapbox.geojson.FeatureCollection import com.mapbox.geojson.MultiPoint import com.mapbox.geojson.Point @@ -46,6 +48,7 @@ import io.ona.kujaku.plugin.switcher.BaseLayerSwitcherPlugin import io.ona.kujaku.plugin.switcher.layer.StreetsBaseLayer import io.ona.kujaku.utils.CoordinateUtils import io.ona.kujaku.views.KujakuMapView +import kotlinx.coroutines.launch import org.jetbrains.annotations.VisibleForTesting import org.json.JSONObject import org.smartregister.fhircore.engine.configuration.geowidget.MapLayer @@ -68,11 +71,11 @@ class GeoWidgetFragment : Fragment() { internal var onClickLocationCallback: (GeoJsonFeature, FragmentManager) -> Unit = { _: GeoJsonFeature, _: FragmentManager -> } - internal var useGpsOnAddingLocation: Boolean = false - internal var mapLayers: List = ArrayList() - internal var showCurrentLocationButton: Boolean = true - internal var showPlaneSwitcherButton: Boolean = true - internal var showAddLocationButton: Boolean = true + private var useGpsOnAddingLocation: Boolean = false + private var mapLayers: List = ArrayList() + private var showCurrentLocationButton: Boolean = true + private var showPlaneSwitcherButton: Boolean = true + private var showAddLocationButton: Boolean = true private var mapView: KujakuMapView? = null private lateinit var geoWidgetViewModel: GeoWidgetViewModel @@ -81,7 +84,8 @@ class GeoWidgetFragment : Fragment() { container: ViewGroup?, savedInstanceState: Bundle?, ): View { - setUpMapView(savedInstanceState) + Mapbox.getInstance(requireActivity(), BuildConfig.MAPBOX_SDK_TOKEN) + mapView = setUpMapView() return LinearLayout(requireContext()).apply { orientation = LinearLayout.VERTICAL addView(mapView) @@ -89,13 +93,8 @@ class GeoWidgetFragment : Fragment() { } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + mapView?.onCreate(savedInstanceState) geoWidgetViewModel = ViewModelProvider(this)[GeoWidgetViewModel::class.java] - geoWidgetViewModel.features.observe(viewLifecycleOwner) { result -> - if (result.isNotEmpty()) { - geoWidgetViewModel.updateMapFeatures(result) - zoomMapWithFeatures() - } - } } override fun onStart() { @@ -119,9 +118,9 @@ class GeoWidgetFragment : Fragment() { } override fun onDestroy() { - super.onDestroy() - geoWidgetViewModel.clearMapFeatures() mapView?.onDestroy() + super.onDestroy() + mapView = null } override fun onLowMemory() { @@ -134,31 +133,27 @@ class GeoWidgetFragment : Fragment() { mapView?.onSaveInstanceState(outState) } - private fun setUpMapView(savedInstanceState: Bundle?) { - geoWidgetViewModel = viewModels().value - Mapbox.getInstance(requireContext(), BuildConfig.MAPBOX_SDK_TOKEN) - mapView = - try { - KujakuMapView(requireActivity()).apply { - id = R.id.kujaku_widget - val builder = Style.Builder().fromUri(context.getString(R.string.style_map_fhir_core)) - getMapAsync { mapboxMap -> - mapboxMap.setStyle(builder) { style -> - addIconsLayer(style) - addMapStyle(style) - } + private fun setUpMapView(): KujakuMapView? { + return try { + KujakuMapView(requireContext()).apply { + id = R.id.kujaku_widget + val builder = Style.Builder().fromUri(MAP_STYLE) + getMapAsync { mapboxMap -> + mapboxMap.setStyle(builder) { style -> + addIconsLayer(style) + addMapStyle(style) } + } - if (showAddLocationButton) { - setOnAddLocationListener(this) - } - setOnClickLocationListener(this) + if (showAddLocationButton) { + setOnAddLocationListener(this) } - } catch (mapboxConfigurationException: MapboxConfigurationException) { - Timber.e(mapboxConfigurationException) - null + setOnClickLocationListener(this) } - mapView?.onCreate(savedInstanceState) + } catch (mapboxConfigurationException: MapboxConfigurationException) { + Timber.e(mapboxConfigurationException) + null + } } private fun addIconsLayer(mMapboxMapStyle: Style) { @@ -193,11 +188,7 @@ class GeoWidgetFragment : Fragment() { ) icon?.let { mMapboxMapStyle.addImage(key, icon) - val symbolLayer = - SymbolLayer( - String.format("%s.layer", key), - getString(R.string.data_set_quest), - ) + val symbolLayer = SymbolLayer(String.format("%s.layer", key), DATA_SET) symbolLayer.setProperties( PropertyFactory.iconImage(key), PropertyFactory.iconSize(dynamicIconSize), @@ -237,11 +228,7 @@ class GeoWidgetFragment : Fragment() { val baseKey = "base-image" mMapboxMapStyle.addImage(baseKey, it) - val symbolLayer = - SymbolLayer( - String.format("%s.layer", baseKey), - getString(R.string.data_set_quest), - ) + val symbolLayer = SymbolLayer(String.format("%s.layer", baseKey), DATA_SET) symbolLayer.setProperties( PropertyFactory.iconImage(baseKey), PropertyFactory.iconSize(dynamicBaseIconSize), @@ -249,7 +236,7 @@ class GeoWidgetFragment : Fragment() { PropertyFactory.symbolSortKey(1f), PropertyFactory.iconOffset(arrayOf(0f, 8.5f)), ) - mMapboxMapStyle.addLayerBelow(symbolLayer, getString(R.string.quest_data_points)) + mMapboxMapStyle.addLayerBelow(symbolLayer, DATA_POINTS) } } @@ -313,93 +300,90 @@ class GeoWidgetFragment : Fragment() { private fun zoomMapWithFeatures() { mapView?.getMapAsync { mapboxMap -> - val featureCollection = - FeatureCollection.fromFeatures(geoWidgetViewModel.mapFeatures.toList()) - val locationPoints = - featureCollection - .features() - ?.asSequence() - ?.filter { it.geometry() is Point } - ?.map { it.geometry() as Point } - ?.toMutableList() ?: emptyList() - - val bbox = TurfMeasurement.bbox(MultiPoint.fromLngLats(locationPoints)) - val paddedBbox = CoordinateUtils.getPaddedBbox(bbox, 1000.0) - val bounds = LatLngBounds.from(paddedBbox[3], paddedBbox[2], paddedBbox[1], paddedBbox[0]) - val finalCameraPosition = CameraUpdateFactory.newLatLngBounds(bounds, 50) - - with(mapboxMap) { - (style?.getSourceAs(requireContext().getString(R.string.data_set_quest)) as GeoJsonSource?) - ?.apply { setGeoJson(featureCollection) } - easeCamera(finalCameraPosition) + val features = geoWidgetViewModel.mapFeatures.toList() + if (features.isNotEmpty()) { + val featureCollection = FeatureCollection.fromFeatures(features) + val locationPoints = + featureCollection + .features() + ?.asSequence() + ?.filter { it.geometry() is Point } + ?.map { it.geometry() as Point } + ?.toMutableList() ?: emptyList() + mapboxMap.getStyle { style -> + style.getSourceAs(DATA_SET)?.setGeoJson(featureCollection) + } + val bbox = TurfMeasurement.bbox(MultiPoint.fromLngLats(locationPoints)) + val paddedBbox = CoordinateUtils.getPaddedBbox(bbox, PADDING_IN_METRES) + val bounds = LatLngBounds.from(paddedBbox[3], paddedBbox[2], paddedBbox[1], paddedBbox[0]) + val finalCameraPosition = + CameraUpdateFactory.newLatLngBounds(bounds, CAMERA_POSITION_PADDING) + mapboxMap.easeCamera(finalCameraPosition) } } } - class Builder { - - private var onAddLocationCallback: (GeoJsonFeature) -> Unit = {} - private var onCancelAddingLocationCallback: () -> Unit = {} - private var onClickLocationCallback: (GeoJsonFeature, FragmentManager) -> Unit = - { _: GeoJsonFeature, _: FragmentManager -> - } - private var useGpsOnAddingLocation: Boolean = false - private var mapLayers: List = ArrayList() - private var showCurrentLocationButton: Boolean = true - private var showPlaneSwitcherButton: Boolean = true - private var showAddLocationButton: Boolean = true - - fun setOnAddLocationListener(onAddLocationCallback: (GeoJsonFeature) -> Unit) = apply { - this.onAddLocationCallback = onAddLocationCallback - } + fun setOnAddLocationListener(onAddLocationCallback: (GeoJsonFeature) -> Unit) = apply { + this.onAddLocationCallback = onAddLocationCallback + } - fun setOnCancelAddingLocationListener(onCancelAddingLocationCallback: () -> Unit) = apply { - this.onCancelAddingLocationCallback = onCancelAddingLocationCallback - } + fun setOnCancelAddingLocationListener(onCancelAddingLocationCallback: () -> Unit) = apply { + this.onCancelAddingLocationCallback = onCancelAddingLocationCallback + } - fun setOnClickLocationListener( - onClickLocationCallback: (GeoJsonFeature, FragmentManager) -> Unit, - ) = apply { this.onClickLocationCallback = onClickLocationCallback } + fun setOnClickLocationListener( + onClickLocationCallback: (GeoJsonFeature, FragmentManager) -> Unit, + ) = apply { this.onClickLocationCallback = onClickLocationCallback } - fun setUseGpsOnAddingLocation(value: Boolean) = apply { this.useGpsOnAddingLocation = value } + fun setUseGpsOnAddingLocation(value: Boolean) = apply { this.useGpsOnAddingLocation = value } - fun setMapLayers(list: List) = apply { this.mapLayers = list } + fun setMapLayers(list: List) = apply { this.mapLayers = list } - fun showCurrentLocationButtonVisibility(show: Boolean) = apply { - this.showCurrentLocationButton = show - } + fun showCurrentLocationButtonVisibility(show: Boolean) = apply { + this.showCurrentLocationButton = show + } - fun setAddLocationButtonVisibility(show: Boolean) = apply { this.showAddLocationButton = show } + fun setAddLocationButtonVisibility(show: Boolean) = apply { this.showAddLocationButton = show } - fun setPlaneSwitcherButtonVisibility(show: Boolean) = apply { - this.showPlaneSwitcherButton = show - } + fun setPlaneSwitcherButtonVisibility(show: Boolean) = apply { + this.showPlaneSwitcherButton = show + } - fun build(): GeoWidgetFragment { - return GeoWidgetFragment().apply { - this.onAddLocationCallback = this@Builder.onAddLocationCallback - this.onCancelAddingLocationCallback = this@Builder.onCancelAddingLocationCallback - this.onClickLocationCallback = this@Builder.onClickLocationCallback - this.useGpsOnAddingLocation = this@Builder.useGpsOnAddingLocation - this.mapLayers = this@Builder.mapLayers - this.showCurrentLocationButton = this@Builder.showCurrentLocationButton - this.showPlaneSwitcherButton = this@Builder.showPlaneSwitcherButton - this.showAddLocationButton = this@Builder.showAddLocationButton + fun observerGeoJsonFeatures(mutableLiveData: MutableLiveData>) { + with(viewLifecycleOwner) { + lifecycleScope.launch { + repeatOnLifecycle(androidx.lifecycle.Lifecycle.State.CREATED) { + mutableLiveData.observe(this@with) { geoJsonFeatures -> + if (geoJsonFeatures.isNotEmpty()) { + geoWidgetViewModel.updateMapFeatures(geoJsonFeatures) + zoomMapWithFeatures() + } + } + } } } } - fun submitFeatures(geoJsonFeatures: List) { - if (this::geoWidgetViewModel.isInitialized) { - geoWidgetViewModel.submitFeatures(geoJsonFeatures) + fun observerMapReset(clearMapLiveData: MutableLiveData) { + with(viewLifecycleOwner) { + lifecycleScope.launch { + repeatOnLifecycle(androidx.lifecycle.Lifecycle.State.CREATED) { + clearMapLiveData.observe(this@with) { reset -> + if (reset) { + geoWidgetViewModel.clearMapFeatures() + } + } + } + } } } - fun clearMapFeatures() = geoWidgetViewModel.clearMapFeatures() - companion object { const val MAP_FEATURES_LIMIT = 1000 - - fun builder() = Builder() + const val PADDING_IN_METRES = 1000.0 + const val CAMERA_POSITION_PADDING = 50 + const val MAP_STYLE = "asset://fhircore_style.json" + const val DATA_SET = "quest-data-set" + const val DATA_POINTS = "quest-data-points" } } diff --git a/android/geowidget/src/main/java/org/smartregister/fhircore/geowidget/screens/GeoWidgetViewModel.kt b/android/geowidget/src/main/java/org/smartregister/fhircore/geowidget/screens/GeoWidgetViewModel.kt index ac36ca170c..87520d5630 100644 --- a/android/geowidget/src/main/java/org/smartregister/fhircore/geowidget/screens/GeoWidgetViewModel.kt +++ b/android/geowidget/src/main/java/org/smartregister/fhircore/geowidget/screens/GeoWidgetViewModel.kt @@ -16,8 +16,6 @@ package org.smartregister.fhircore.geowidget.screens -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import com.mapbox.geojson.Feature import org.smartregister.fhircore.geowidget.model.GeoJsonFeature @@ -27,13 +25,6 @@ import org.smartregister.fhircore.geowidget.screens.GeoWidgetFragment.Companion. class GeoWidgetViewModel : ViewModel() { val mapFeatures = ArrayDeque() - private val _features = MutableLiveData>(mutableListOf()) - val features: LiveData> - get() = _features - - fun submitFeatures(geoJsonFeatures: List) { - _features.postValue(geoJsonFeatures) - } fun updateMapFeatures(geoJsonFeatures: List) { if (mapFeatures.size <= MAP_FEATURES_LIMIT) { @@ -77,9 +68,4 @@ class GeoWidgetViewModel : ViewModel() { map[ServicePointType.LYCÉE.name.lowercase()] = ServicePointType.LYCÉE return map } - - override fun onCleared() { - super.onCleared() - clearMapFeatures() - } } diff --git a/android/geowidget/src/main/res/values/ids.xml b/android/geowidget/src/main/res/values/ids.xml index 420f4fae43..1bc2412530 100644 --- a/android/geowidget/src/main/res/values/ids.xml +++ b/android/geowidget/src/main/res/values/ids.xml @@ -1,4 +1,5 @@ + diff --git a/android/geowidget/src/main/res/values/strings.xml b/android/geowidget/src/main/res/values/strings.xml deleted file mode 100644 index 26b969e8e6..0000000000 --- a/android/geowidget/src/main/res/values/strings.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - asset://fhircore_style.json - quest-data-set - quest-data-points - diff --git a/android/geowidget/src/test/java/org/smartregister/fhircore/geowidget/screens/GeoWidgetViewModelTest.kt b/android/geowidget/src/test/java/org/smartregister/fhircore/geowidget/screens/GeoWidgetViewModelTest.kt index 2bc05b9705..b7e325c273 100644 --- a/android/geowidget/src/test/java/org/smartregister/fhircore/geowidget/screens/GeoWidgetViewModelTest.kt +++ b/android/geowidget/src/test/java/org/smartregister/fhircore/geowidget/screens/GeoWidgetViewModelTest.kt @@ -143,9 +143,9 @@ class GeoWidgetViewModelTest { serverVersion = serverVersion, ), ) - geoWidgetViewModel.submitFeatures(geoJsonFeatures) + geoWidgetViewModel.updateMapFeatures(geoJsonFeatures) - Assert.assertEquals(geoWidgetViewModel.features.value!!.size, geoJsonFeatures.size) + Assert.assertEquals(geoWidgetViewModel.mapFeatures.size, geoJsonFeatures.size) } @Test diff --git a/android/gradle/libs.versions.toml b/android/gradle/libs.versions.toml index 0cc0cac3ef..0e32299cd4 100644 --- a/android/gradle/libs.versions.toml +++ b/android/gradle/libs.versions.toml @@ -49,7 +49,7 @@ kotlinx-coroutines = "1.9.0" kotlinx-serialization-json = "1.6.0" kt3k-coveralls-ver="2.12.0" ktlint = "0.50.0" -kujaku-library = "0.10.7-SNAPSHOT" +kujaku-library = "0.10.8-SNAPSHOT" kujaku-mapbox-sdk-turf = "7.2.0" leakcanary-android = "2.10" lifecycle= "2.8.5" @@ -82,6 +82,7 @@ timber = "5.0.1" uiautomator = "2.3.0" work = "2.9.1" xercesImpl = "2.12.2" +androidFragmentCompose = "1.8.4" [libraries] accompanist-flowlayout = { group = "com.google.accompanist", name = "accompanist-flowlayout", version.ref = "accompanist" } @@ -196,6 +197,8 @@ work-runtime-ktx = { group = "androidx.work", name = "work-runtime-ktx", version work-testing = { group = "androidx.work", name = "work-testing", version.ref = "work" } workflow = { group = "org.smartregister", name = "workflow", version.ref = "fhir-sdk-workflow" } xercesImpl = { group = "xerces", name = "xercesImpl", version.ref = "xercesImpl" } +androidx-fragment-compose = { group = "androidx.fragment", name = "fragment-compose", version.ref = "androidFragmentCompose" } + [plugins] android-junit5 = {id="de.mannodermaus.android-junit5", version.ref="androidJunit5"} diff --git a/android/quest/build.gradle.kts b/android/quest/build.gradle.kts index 1d22c6e27b..b7855080e3 100644 --- a/android/quest/build.gradle.kts +++ b/android/quest/build.gradle.kts @@ -444,7 +444,7 @@ dependencies { implementation(libs.dagger.hilt.android) implementation(libs.hilt.work) implementation(libs.mlkit.barcode.scanning) - + implementation(libs.androidx.fragment.compose) implementation(libs.bundles.cameraX) // Annotation processors diff --git a/android/quest/src/main/AndroidManifest.xml b/android/quest/src/main/AndroidManifest.xml index 1c69856d16..46fe396c60 100644 --- a/android/quest/src/main/AndroidManifest.xml +++ b/android/quest/src/main/AndroidManifest.xml @@ -4,6 +4,7 @@ + diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/event/AppEvent.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/event/AppEvent.kt index ef04e7acac..68ea1fc31a 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/event/AppEvent.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/event/AppEvent.kt @@ -22,5 +22,5 @@ sealed class AppEvent { data class OnSubmitQuestionnaire(val questionnaireSubmission: QuestionnaireSubmission) : AppEvent() - data object RefreshRegisterData : AppEvent() + data object RefreshData : AppEvent() } 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 71d500c8b8..a3ff11c5cc 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 @@ -54,7 +54,7 @@ sealed class MainNavigationScreen( route = org.smartregister.fhircore.quest.R.id.profileFragment, ) - object GeoWidgetLauncher : + data object GeoWidgetLauncher : MainNavigationScreen(route = org.smartregister.fhircore.quest.R.id.geoWidgetLauncherFragment) data object Insight : diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/geowidget/GeoWidgetEvent.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/geowidget/GeoWidgetEvent.kt index 0038324b78..195c11e822 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/geowidget/GeoWidgetEvent.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/geowidget/GeoWidgetEvent.kt @@ -20,8 +20,10 @@ import org.smartregister.fhircore.engine.configuration.geowidget.GeoWidgetConfig import org.smartregister.fhircore.quest.ui.shared.models.SearchQuery sealed class GeoWidgetEvent { - data class SearchFeatures( - val searchQuery: SearchQuery = SearchQuery.emptyText, + data object ClearMap : GeoWidgetEvent() + + data class RetrieveFeatures( val geoWidgetConfig: GeoWidgetConfiguration, + val searchQuery: SearchQuery = SearchQuery.emptyText, ) : GeoWidgetEvent() } 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 348231fcf0..2b4d302b1e 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 @@ -26,13 +26,11 @@ import androidx.compose.foundation.layout.padding import androidx.compose.material.Scaffold import androidx.compose.material.rememberScaffoldState import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.platform.ViewCompositionStrategy import androidx.fragment.app.Fragment -import androidx.fragment.app.FragmentManager import androidx.fragment.app.activityViewModels import androidx.fragment.app.viewModels import androidx.lifecycle.Lifecycle @@ -53,21 +51,18 @@ import org.hl7.fhir.r4.model.ResourceType import org.smartregister.fhircore.engine.configuration.ConfigType import org.smartregister.fhircore.engine.configuration.ConfigurationRegistry import org.smartregister.fhircore.engine.configuration.geowidget.GeoWidgetConfiguration -import org.smartregister.fhircore.engine.domain.model.ResourceData import org.smartregister.fhircore.engine.sync.OnSyncListener import org.smartregister.fhircore.engine.sync.SyncListenerManager import org.smartregister.fhircore.engine.ui.base.AlertDialogue import org.smartregister.fhircore.engine.ui.base.AlertIntent import org.smartregister.fhircore.engine.ui.theme.AppTheme import org.smartregister.fhircore.engine.util.DispatcherProvider +import org.smartregister.fhircore.engine.util.extension.getActivity import org.smartregister.fhircore.engine.util.extension.showToast -import org.smartregister.fhircore.geowidget.model.GeoJsonFeature -import org.smartregister.fhircore.geowidget.screens.GeoWidgetFragment import org.smartregister.fhircore.quest.R import org.smartregister.fhircore.quest.event.AppEvent import org.smartregister.fhircore.quest.event.EventBus import org.smartregister.fhircore.quest.navigation.MainNavigationScreen -import org.smartregister.fhircore.quest.ui.bottomsheet.SummaryBottomSheetFragment import org.smartregister.fhircore.quest.ui.main.AppMainEvent import org.smartregister.fhircore.quest.ui.main.AppMainUiState import org.smartregister.fhircore.quest.ui.main.AppMainViewModel @@ -92,7 +87,6 @@ class GeoWidgetLauncherFragment : Fragment(), OnSyncListener { @Inject lateinit var dispatcherProvider: DispatcherProvider - private lateinit var geoWidgetFragment: GeoWidgetFragment private lateinit var geoWidgetConfiguration: GeoWidgetConfiguration private val navArgs by navArgs() private val geoWidgetLauncherViewModel by viewModels() @@ -104,11 +98,18 @@ class GeoWidgetLauncherFragment : Fragment(), OnSyncListener { container: ViewGroup?, savedInstanceState: Bundle?, ): View { - buildGeoWidgetFragment() - geoWidgetLauncherViewModel.retrieveLocations( - geoWidgetConfig = geoWidgetConfiguration, - searchText = searchViewModel.searchQuery.value.query, - ) + geoWidgetConfiguration = + configurationRegistry.retrieveConfiguration( + configType = ConfigType.GeoWidget, + configId = navArgs.geoWidgetId, + ) + if (geoWidgetConfiguration.resourceConfig.baseResource.resource != ResourceType.Location) { + val message = getString(R.string.invalid_base_resource) + requireContext().showToast(message) + Timber.e(message, geoWidgetConfiguration.toString()) + requireContext().getActivity()?.finish() + } + return ComposeView(requireContext()).apply { setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) setContent { @@ -168,29 +169,31 @@ class GeoWidgetLauncherFragment : Fragment(), OnSyncListener { }, ) { innerPadding -> Box(modifier = Modifier.padding(innerPadding)) { - val geoWidgetFragment = remember { geoWidgetFragment } GeoWidgetLauncherScreen( modifier = Modifier.fillMaxSize(), openDrawer = openDrawer, navController = findNavController(), toolBarHomeNavigation = navArgs.toolBarHomeNavigation, - fragmentManager = childFragmentManager, - geoWidgetFragment = geoWidgetFragment, geoWidgetConfiguration = geoWidgetConfiguration, searchQuery = searchViewModel.searchQuery, search = { searchText -> - geoWidgetFragment.clearMapFeatures() - geoWidgetLauncherViewModel.onEvent( - GeoWidgetEvent.SearchFeatures( - searchQuery = SearchQuery(searchText, SearchMode.KeyboardInput), - geoWidgetConfig = geoWidgetConfiguration, - ), - ) + geoWidgetLauncherViewModel.run { + onEvent(GeoWidgetEvent.ClearMap) + onEvent( + GeoWidgetEvent.RetrieveFeatures( + searchQuery = SearchQuery(searchText, SearchMode.KeyboardInput), + geoWidgetConfig = geoWidgetConfiguration, + ), + ) + } }, isFirstTimeSync = geoWidgetLauncherViewModel.isFirstTime(), appDrawerUIState = appDrawerUIState, + clearMapLiveData = geoWidgetLauncherViewModel.clearMapLiveData, + geoJsonFeatures = geoWidgetLauncherViewModel.geoJsonFeatures, + launchQuestionnaire = geoWidgetLauncherViewModel::launchQuestionnaire, + decodeImage = geoWidgetLauncherViewModel::getImageBitmap, onAppMainEvent = appMainViewModel::onEvent, - decodeImage = { TODO("Return bitmap") }, ) } } @@ -220,14 +223,16 @@ class GeoWidgetLauncherFragment : Fragment(), OnSyncListener { } is CurrentSyncJobStatus.Succeeded, is CurrentSyncJobStatus.Failed, -> { + appMainViewModel.updateAppDrawerUIState(currentSyncJobStatus = syncJobStatus) if (syncJobStatus is CurrentSyncJobStatus.Succeeded) { - geoWidgetFragment.clearMapFeatures() + geoWidgetLauncherViewModel.onEvent(GeoWidgetEvent.ClearMap) } - geoWidgetLauncherViewModel.retrieveLocations( - geoWidgetConfig = geoWidgetConfiguration, - searchText = searchViewModel.searchQuery.value.query, + geoWidgetLauncherViewModel.onEvent( + GeoWidgetEvent.RetrieveFeatures( + geoWidgetConfig = geoWidgetConfiguration, + searchQuery = searchViewModel.searchQuery.value, + ), ) - appMainViewModel.updateAppDrawerUIState(currentSyncJobStatus = syncJobStatus) } else -> appMainViewModel.updateAppDrawerUIState(currentSyncJobStatus = syncJobStatus) } @@ -235,6 +240,30 @@ class GeoWidgetLauncherFragment : Fragment(), OnSyncListener { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + viewLifecycleOwner.lifecycleScope.launch { + viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.CREATED) { + eventBus.events + .getFor(MainNavigationScreen.GeoWidgetLauncher.eventId(navArgs.geoWidgetId)) + .onEach { appEvent -> + when (appEvent) { + is AppEvent.RefreshData, + is AppEvent.OnSubmitQuestionnaire, -> { + appMainViewModel.countRegisterData() + geoWidgetLauncherViewModel.run { + onEvent(GeoWidgetEvent.ClearMap) + onEvent( + GeoWidgetEvent.RetrieveFeatures( + geoWidgetConfig = geoWidgetConfiguration, + searchQuery = searchViewModel.searchQuery.value, + ), + ) + } + } + } + } + .launchIn(lifecycleScope) + } + } geoWidgetLauncherViewModel.noLocationFoundDialog.observe(viewLifecycleOwner) { show -> if (show) { AlertDialogue.showAlert( @@ -254,8 +283,12 @@ class GeoWidgetLauncherFragment : Fragment(), OnSyncListener { ) } } - - setOnQuestionnaireSubmissionListener { geoWidgetFragment.submitFeatures(listOf(it)) } + geoWidgetLauncherViewModel.onEvent( + GeoWidgetEvent.RetrieveFeatures( + geoWidgetConfig = geoWidgetConfiguration, + searchQuery = searchViewModel.searchQuery.value, + ), + ) } override fun onPause() { @@ -267,75 +300,4 @@ class GeoWidgetLauncherFragment : Fragment(), OnSyncListener { super.onDestroy() appMainViewModel.updateAppDrawerUIState(false, null, 0) } - - private fun buildGeoWidgetFragment() { - geoWidgetConfiguration = - configurationRegistry.retrieveConfiguration( - configType = ConfigType.GeoWidget, - configId = navArgs.geoWidgetId, - ) - - if (geoWidgetConfiguration.resourceConfig.baseResource.resource != ResourceType.Location) { - val message = getString(R.string.invalid_base_resource) - requireContext().showToast(message) - Timber.e(message, geoWidgetConfiguration.toString()) - } - - geoWidgetFragment = - GeoWidgetFragment.builder() - .setUseGpsOnAddingLocation(false) - .setAddLocationButtonVisibility(geoWidgetConfiguration.showAddLocation) - .setOnAddLocationListener { feature: GeoJsonFeature -> - if (feature.geometry?.coordinates == null) return@setOnAddLocationListener - geoWidgetLauncherViewModel.launchQuestionnaire( - geoWidgetConfiguration.registrationQuestionnaire, - feature, - requireContext(), - ) - } - .setOnCancelAddingLocationListener { - requireContext().showToast("on cancel adding location") - } - .setOnClickLocationListener { - feature: GeoJsonFeature, - parentFragmentManager: FragmentManager, - -> - SummaryBottomSheetFragment( - geoWidgetConfiguration.summaryBottomSheetConfig!!, - ResourceData( - baseResourceId = feature.id, - baseResourceType = ResourceType.Location, - computedValuesMap = feature.properties.mapValues { it.value.content }, - ), - ) - .run { show(parentFragmentManager, SummaryBottomSheetFragment.TAG) } - } - .setMapLayers(geoWidgetConfiguration.mapLayers) - .showCurrentLocationButtonVisibility(geoWidgetConfiguration.showLocation) - .setPlaneSwitcherButtonVisibility(geoWidgetConfiguration.showPlaneSwitcher) - .build() - - lifecycleScope.launch { - geoWidgetLauncherViewModel.geoJsonFeatures.collect { geoWidgetFragment.submitFeatures(it) } - } - } - - private fun setOnQuestionnaireSubmissionListener(emitFeature: (GeoJsonFeature) -> Unit) { - viewLifecycleOwner.lifecycleScope.launch { - repeatOnLifecycle(Lifecycle.State.STARTED) { - eventBus.events - .getFor(MainNavigationScreen.GeoWidgetLauncher.eventId(geoWidgetConfiguration.id)) - .onEach { appEvent -> - if (appEvent is AppEvent.OnSubmitQuestionnaire) { - val extractedResourceIds = appEvent.questionnaireSubmission.extractedResourceIds - geoWidgetLauncherViewModel.onQuestionnaireSubmission( - extractedResourceIds = extractedResourceIds, - emitFeature = emitFeature, - ) - } - } - .launchIn(lifecycleScope) - } - } - } } diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/geowidget/GeoWidgetLauncherScreen.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/geowidget/GeoWidgetLauncherScreen.kt index 0c91a2b3c8..86dc61b6bc 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/geowidget/GeoWidgetLauncherScreen.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/geowidget/GeoWidgetLauncherScreen.kt @@ -16,29 +16,32 @@ package org.smartregister.fhircore.quest.ui.geowidget +import android.content.Context import android.graphics.Bitmap -import android.view.View -import android.widget.FrameLayout import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.padding import androidx.compose.material.Scaffold import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.MutableState -import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.viewinterop.AndroidView -import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentManager +import androidx.fragment.compose.AndroidFragment +import androidx.fragment.compose.rememberFragmentState +import androidx.lifecycle.MutableLiveData import androidx.navigation.NavController +import org.hl7.fhir.r4.model.ResourceType +import org.smartregister.fhircore.engine.configuration.QuestionnaireConfig import org.smartregister.fhircore.engine.configuration.geowidget.GeoWidgetConfiguration +import org.smartregister.fhircore.engine.domain.model.ResourceData import org.smartregister.fhircore.engine.domain.model.ToolBarHomeNavigation import org.smartregister.fhircore.engine.util.extension.showToast +import org.smartregister.fhircore.geowidget.model.GeoJsonFeature import org.smartregister.fhircore.geowidget.screens.GeoWidgetFragment import org.smartregister.fhircore.quest.R import org.smartregister.fhircore.quest.event.ToolbarClickEvent +import org.smartregister.fhircore.quest.ui.bottomsheet.SummaryBottomSheetFragment import org.smartregister.fhircore.quest.ui.main.AppMainEvent import org.smartregister.fhircore.quest.ui.main.components.TopScreenSection import org.smartregister.fhircore.quest.ui.shared.components.SyncBottomBar @@ -52,13 +55,14 @@ fun GeoWidgetLauncherScreen( openDrawer: (Boolean) -> Unit, navController: NavController, toolBarHomeNavigation: ToolBarHomeNavigation = ToolBarHomeNavigation.OPEN_DRAWER, - fragmentManager: FragmentManager, - geoWidgetFragment: GeoWidgetFragment, geoWidgetConfiguration: GeoWidgetConfiguration, searchQuery: MutableState, search: (String) -> Unit, isFirstTimeSync: Boolean, appDrawerUIState: AppDrawerUIState, + clearMapLiveData: MutableLiveData, + geoJsonFeatures: MutableLiveData>, + launchQuestionnaire: (QuestionnaireConfig, GeoJsonFeature, Context) -> Unit, decodeImage: ((String) -> Bitmap?)?, onAppMainEvent: (AppMainEvent) -> Unit, ) { @@ -113,33 +117,42 @@ fun GeoWidgetLauncherScreen( ) }, ) { innerPadding -> + val fragmentState = rememberFragmentState() Box(modifier = modifier.padding(innerPadding)) { - GeoWidgetFragmentView( - modifier = modifier, - fragmentManager = fragmentManager, - fragment = geoWidgetFragment, - ) - } - } -} - -@Composable -fun GeoWidgetFragmentView( - modifier: Modifier = Modifier, - fragmentManager: FragmentManager, - fragment: Fragment, -) { - val viewId = rememberSaveable { View.generateViewId() } + AndroidFragment(fragmentState = fragmentState) { fragment -> + fragment + .setUseGpsOnAddingLocation(false) + .setAddLocationButtonVisibility(geoWidgetConfiguration.showAddLocation) + .setOnAddLocationListener { feature: GeoJsonFeature -> + if (feature.geometry?.coordinates == null) return@setOnAddLocationListener + launchQuestionnaire(geoWidgetConfiguration.registrationQuestionnaire, feature, context) + } + .setOnCancelAddingLocationListener { + context.showToast(context.getString(R.string.on_cancel_adding_location)) + } + .setOnClickLocationListener { + feature: GeoJsonFeature, + parentFragmentManager: FragmentManager, + -> + SummaryBottomSheetFragment( + geoWidgetConfiguration.summaryBottomSheetConfig!!, + ResourceData( + baseResourceId = feature.id, + baseResourceType = ResourceType.Location, + computedValuesMap = feature.properties.mapValues { it.value.content }, + ), + ) + .run { show(parentFragmentManager, SummaryBottomSheetFragment.TAG) } + } + .setMapLayers(geoWidgetConfiguration.mapLayers) + .showCurrentLocationButtonVisibility(geoWidgetConfiguration.showLocation) + .setPlaneSwitcherButtonVisibility(geoWidgetConfiguration.showPlaneSwitcher) - AndroidView( - modifier = modifier, - factory = { context -> FrameLayout(context).apply { id = viewId } }, - ) - DisposableEffect(fragmentManager, fragment) { - fragmentManager.beginTransaction().run { - replace(viewId, fragment) - commitNow() + fragment.apply { + observerMapReset(clearMapLiveData) + observerGeoJsonFeatures(geoJsonFeatures) + } + } } - onDispose { fragmentManager.beginTransaction().remove(fragment).commitNowAllowingStateLoss() } } } diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/geowidget/GeoWidgetLauncherViewModel.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/geowidget/GeoWidgetLauncherViewModel.kt index 3d34c607e5..5c9eef34ff 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/geowidget/GeoWidgetLauncherViewModel.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/geowidget/GeoWidgetLauncherViewModel.kt @@ -18,6 +18,7 @@ package org.smartregister.fhircore.quest.ui.geowidget import android.content.Context import android.graphics.Bitmap +import androidx.compose.material.SnackbarDuration import androidx.compose.runtime.mutableStateMapOf import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData @@ -28,14 +29,12 @@ import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.qualifiers.ApplicationContext import javax.inject.Inject import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext import kotlinx.serialization.json.JsonPrimitive import org.hl7.fhir.r4.model.Enumerations -import org.hl7.fhir.r4.model.IdType import org.hl7.fhir.r4.model.Location import org.hl7.fhir.r4.model.ResourceType import org.smartregister.fhircore.engine.configuration.ConfigType @@ -47,18 +46,20 @@ import org.smartregister.fhircore.engine.configuration.register.ActiveResourceFi import org.smartregister.fhircore.engine.data.local.DefaultRepository import org.smartregister.fhircore.engine.domain.model.ActionParameter import org.smartregister.fhircore.engine.domain.model.ActionParameterType +import org.smartregister.fhircore.engine.domain.model.MultiSelectViewAction import org.smartregister.fhircore.engine.domain.model.SnackBarMessageConfig 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.extractLogicalIdUuid import org.smartregister.fhircore.engine.util.extension.interpolate -import org.smartregister.fhircore.engine.util.extension.retrieveRelatedEntitySyncLocationIds +import org.smartregister.fhircore.engine.util.extension.retrieveRelatedEntitySyncLocationState import org.smartregister.fhircore.geowidget.model.GeoJsonFeature import org.smartregister.fhircore.geowidget.model.Geometry +import org.smartregister.fhircore.quest.R import org.smartregister.fhircore.quest.ui.shared.QuestionnaireHandler import org.smartregister.fhircore.quest.util.extensions.referenceToBitmap +import timber.log.Timber @HiltViewModel class GeoWidgetLauncherViewModel @@ -71,6 +72,9 @@ constructor( val configurationRegistry: ConfigurationRegistry, @ApplicationContext val context: Context, ) : ViewModel() { + val clearMapLiveData: MutableLiveData = MutableLiveData() + val geoJsonFeatures: MutableLiveData> = MutableLiveData() + private val _snackBarStateFlow = MutableSharedFlow() val snackBarStateFlow = _snackBarStateFlow.asSharedFlow() @@ -82,11 +86,20 @@ constructor( configurationRegistry.retrieveConfiguration(ConfigType.Application) } - val geoJsonFeatures: MutableStateFlow> = MutableStateFlow(emptyList()) - private val decodedImageMap = mutableStateMapOf() - fun retrieveLocations(geoWidgetConfig: GeoWidgetConfiguration, searchText: String?) { + fun onEvent(geoWidgetEvent: GeoWidgetEvent) { + when (geoWidgetEvent) { + is GeoWidgetEvent.RetrieveFeatures -> + retrieveLocations(geoWidgetEvent.geoWidgetConfig, geoWidgetEvent.searchQuery.query) + GeoWidgetEvent.ClearMap -> clearMapLiveData.postValue(true) + } + } + + private fun retrieveLocations( + geoWidgetConfig: GeoWidgetConfiguration, + searchText: String?, + ) { viewModelScope.launch { val totalCount = withContext(dispatcherProvider.io()) { @@ -114,8 +127,10 @@ constructor( } var count = 0 var pageNumber = 0 + var locationsWithoutCoordinatesCount = 0L + var registerDataCount = 0L while (count < totalCount) { - val registerData = + val (locationsWithCoordinates, locationsWithoutCoordinates) = defaultRepository .searchResourcesRecursively( filterActiveResources = null, @@ -129,8 +144,13 @@ constructor( ) .asSequence() .filter { it.resource is Location } - .filter { (it.resource as Location).hasPosition() } - .filter { with((it.resource as Location).position) { hasLongitude() && hasLatitude() } } + .partition { + with((it.resource as Location).position) { hasLongitude() && hasLatitude() } + } + + val registerData = + locationsWithCoordinates + .asSequence() .map { Pair( it.resource as Location, @@ -159,7 +179,7 @@ constructor( ) } .toList() - geoJsonFeatures.value = + val features = if (searchText.isNullOrBlank()) { registerData } else { @@ -171,48 +191,86 @@ constructor( } == true } } + + geoJsonFeatures.postValue(features) + + Timber.w( + locationsWithoutCoordinates.joinToString("\n") { + val position = (it.resource as Location).position + "Location id ${it.resource.logicalId} coordinates (${position.longitude},${position.latitude}) invalid." + }, + ) pageNumber++ count += DefaultRepository.DEFAULT_BATCH_SIZE + registerDataCount += features.size + locationsWithoutCoordinatesCount += locationsWithoutCoordinates.size } - } - } - fun onEvent(geoWidgetEvent: GeoWidgetEvent) { - when (geoWidgetEvent) { - is GeoWidgetEvent.SearchFeatures -> - retrieveLocations(geoWidgetEvent.geoWidgetConfig, geoWidgetEvent.searchQuery.query) + val locationsCount = if (searchText.isNullOrBlank()) totalCount else registerDataCount + + // Account for locations without coordinates + if (locationsWithoutCoordinatesCount in 1..locationsCount) { + val message = + context.getString( + R.string.locations_without_coordinates, + locationsWithoutCoordinatesCount, + locationsCount, + ) + Timber.w(message) + emitSnackBarState( + SnackBarMessageConfig( + message = message, + actionLabel = context.getString(org.smartregister.fhircore.engine.R.string.ok), + duration = SnackbarDuration.Long, + ), + ) + } else { + val message = + if (searchText.isNullOrBlank()) { + context.getString(R.string.all_locations_rendered) + } else context.getString(R.string.all_matching_locations_rendered, locationsCount) + emitSnackBarState( + SnackBarMessageConfig( + message = message, + actionLabel = context.getString(org.smartregister.fhircore.engine.R.string.ok), + duration = SnackbarDuration.Short, + ), + ) + } + + // Account for missing locations + if (locationsCount == 0L) { + if (!searchText.isNullOrBlank()) { + val message = + context.getString( + R.string.no_found_locations_matching_text, + searchText, + ) + Timber.w(message) + emitSnackBarState( + SnackBarMessageConfig( + message = message, + actionLabel = context.getString(org.smartregister.fhircore.engine.R.string.ok), + duration = SnackbarDuration.Long, + ), + ) + } else { + SnackBarMessageConfig( + message = context.getString(R.string.no_locations_to_render), + actionLabel = context.getString(org.smartregister.fhircore.engine.R.string.ok), + duration = SnackbarDuration.Long, + ) + } + } } } suspend fun showNoLocationDialog(geoWidgetConfiguration: GeoWidgetConfiguration) { geoWidgetConfiguration.noResults?.let { - _noLocationFoundDialog.postValue(context.retrieveRelatedEntitySyncLocationIds().isEmpty()) - } - } - - suspend fun onQuestionnaireSubmission( - extractedResourceIds: List, - emitFeature: (GeoJsonFeature) -> Unit, - ) { - val locationId = - extractedResourceIds.firstOrNull { it.resourceType == ResourceType.Location.name } ?: return - val location = - defaultRepository.loadResource(locationId.valueAsString.extractLogicalIdUuid()) - ?: return - - val feature = - GeoJsonFeature( - id = location.id, - geometry = - Geometry( - coordinates = // MapBox coordinates are represented as Long,Lat (NOT Lat,Long) - listOf( - location.position.longitude.toDouble(), - location.position.latitude.toDouble(), - ), - ), + _noLocationFoundDialog.postValue( + context.retrieveRelatedEntitySyncLocationState(MultiSelectViewAction.SYNC_DATA).isEmpty(), ) - emitFeature(feature) + } } fun launchQuestionnaire( diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/main/AppMainViewModel.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/main/AppMainViewModel.kt index 63dea06500..ed921891e8 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/main/AppMainViewModel.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/main/AppMainViewModel.kt @@ -42,7 +42,6 @@ import java.util.TimeZone import javax.inject.Inject import kotlin.time.Duration import kotlinx.coroutines.async -import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.hl7.fhir.r4.model.QuestionnaireResponse @@ -57,8 +56,8 @@ import org.smartregister.fhircore.engine.configuration.navigation.NavigationMenu import org.smartregister.fhircore.engine.configuration.report.measure.MeasureReportConfiguration import org.smartregister.fhircore.engine.configuration.workflow.ActionTrigger import org.smartregister.fhircore.engine.data.local.register.RegisterRepository -import org.smartregister.fhircore.engine.datastore.syncLocationIdsProtoStore import org.smartregister.fhircore.engine.domain.model.LauncherType +import org.smartregister.fhircore.engine.domain.model.MultiSelectViewAction import org.smartregister.fhircore.engine.sync.CustomSyncWorker import org.smartregister.fhircore.engine.sync.SyncBroadcaster import org.smartregister.fhircore.engine.task.FhirCarePlanGenerator @@ -78,6 +77,7 @@ import org.smartregister.fhircore.engine.util.extension.getActivity import org.smartregister.fhircore.engine.util.extension.isDeviceOnline import org.smartregister.fhircore.engine.util.extension.reformatDate import org.smartregister.fhircore.engine.util.extension.refresh +import org.smartregister.fhircore.engine.util.extension.retrieveRelatedEntitySyncLocationState import org.smartregister.fhircore.engine.util.extension.setAppLocale import org.smartregister.fhircore.engine.util.extension.showToast import org.smartregister.fhircore.engine.util.extension.tryParse @@ -409,7 +409,9 @@ constructor( if (applicationConfiguration.syncStrategy.contains(SyncStrategy.RelatedEntityLocation)) { if ( applicationConfiguration.usePractitionerAssignedLocationOnSync || - context.syncLocationIdsProtoStore.data.firstOrNull()?.isNotEmpty() == true + context + .retrieveRelatedEntitySyncLocationState(MultiSelectViewAction.SYNC_DATA) + .isNotEmpty() ) { schedulePeriodicSync() } diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/main/components/TopScreenSection.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/main/components/TopScreenSection.kt index c56728f026..199dce8412 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/main/components/TopScreenSection.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/main/components/TopScreenSection.kt @@ -23,10 +23,9 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.RoundedCornerShape @@ -34,6 +33,8 @@ import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.Badge import androidx.compose.material.BadgedBox +import androidx.compose.material.DropdownMenu +import androidx.compose.material.DropdownMenuItem import androidx.compose.material.Icon import androidx.compose.material.IconButton import androidx.compose.material.MaterialTheme @@ -43,11 +44,16 @@ import androidx.compose.material.TextFieldDefaults import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.filled.Clear -import androidx.compose.material.icons.filled.FilterAlt import androidx.compose.material.icons.filled.Menu import androidx.compose.material.icons.filled.Search +import androidx.compose.material.icons.outlined.FilterAlt +import androidx.compose.material.icons.outlined.MoreVert import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -63,15 +69,18 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.navigation.NavController import androidx.navigation.compose.rememberNavController +import kotlin.math.min import org.smartregister.fhircore.engine.R import org.smartregister.fhircore.engine.configuration.navigation.ICON_TYPE_LOCAL import org.smartregister.fhircore.engine.configuration.navigation.ImageConfig import org.smartregister.fhircore.engine.configuration.view.ImageProperties import org.smartregister.fhircore.engine.domain.model.ToolBarHomeNavigation import org.smartregister.fhircore.engine.domain.model.TopScreenSectionConfig +import org.smartregister.fhircore.engine.ui.theme.DefaultColor import org.smartregister.fhircore.engine.ui.theme.GreyTextColor import org.smartregister.fhircore.engine.util.annotation.PreviewWithBackgroundExcludeGenerated import org.smartregister.fhircore.engine.util.extension.getActivity +import org.smartregister.fhircore.engine.util.extension.parseColor import org.smartregister.fhircore.quest.event.ToolbarClickEvent import org.smartregister.fhircore.quest.ui.shared.components.Image import org.smartregister.fhircore.quest.ui.shared.models.SearchMode @@ -134,62 +143,42 @@ fun TopScreenSection( TITLE_ROW_TEST_TAG, ), verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, ) { - Icon( - when (toolBarHomeNavigation) { - ToolBarHomeNavigation.OPEN_DRAWER -> Icons.Filled.Menu - ToolBarHomeNavigation.NAVIGATE_BACK -> Icons.AutoMirrored.Filled.ArrowBack - }, - contentDescription = DRAWER_MENU, - tint = Color.White, - modifier = - modifier.clickable { onClick(ToolbarClickEvent.Navigate) }.testTag(TOP_ROW_ICON_TEST_TAG), - ) - Text( - text = title, - fontSize = 20.sp, - color = Color.White, - modifier = modifier.padding(start = 8.dp).weight(1f).testTag(TOP_ROW_TEXT_TEST_TAG), - ) - - // if menu icons are more than two then we will add a overflow menu for other menu icons - // to support m3 guidelines - // https://m3.material.io/components/top-app-bar/guidelines#b1b64842-7d88-4c3f-8ffb-4183fe648c9e - SetupToolbarIcons( - menuIcons = topScreenSection?.menuIcons, - navController = navController, - modifier = modifier, - onClick = onClick, - decodeImage = decodeImage, - ) - - if (isFilterIconEnabled) Spacer(modifier = Modifier.width(24.dp)) - - if (isFilterIconEnabled) { - BadgedBox( - modifier = Modifier.padding(end = 8.dp), - badge = { - if (filteredRecordsCount != null && filteredRecordsCount > -1) { - Badge { - Text( - text = if (filteredRecordsCount > 99) "99+" else filteredRecordsCount.toString(), - overflow = TextOverflow.Clip, - maxLines = 1, - ) - } - } + Row(verticalAlignment = Alignment.CenterVertically) { + Icon( + when (toolBarHomeNavigation) { + ToolBarHomeNavigation.OPEN_DRAWER -> Icons.Filled.Menu + ToolBarHomeNavigation.NAVIGATE_BACK -> Icons.AutoMirrored.Filled.ArrowBack }, - ) { - Icon( - imageVector = Icons.Default.FilterAlt, - contentDescription = FILTER, - tint = Color.White, - modifier = - modifier - .clickable { onClick(ToolbarClickEvent.FilterData) } - .testTag(TOP_ROW_FILTER_ICON_TEST_TAG), - ) - } + contentDescription = DRAWER_MENU, + tint = Color.White, + modifier = + modifier + .clickable { onClick(ToolbarClickEvent.Navigate) } + .testTag(TOP_ROW_ICON_TEST_TAG), + ) + Text( + text = title, + fontSize = 20.sp, + color = Color.White, + modifier = modifier.padding(start = 16.dp).testTag(TOP_ROW_TEXT_TEST_TAG), + ) + } + + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.padding(start = 8.dp), + ) { + SetupToolbarIcons( + menuIcons = topScreenSection?.menuIcons, + isFilterIconEnabled = isFilterIconEnabled, + filteredRecordsCount = filteredRecordsCount, + navController = navController, + modifier = modifier, + onClick = onClick, + decodeImage = decodeImage, + ) } } if (isSearchBarVisible) { @@ -258,13 +247,19 @@ fun TopScreenSection( currentContext.getActivity()?.let { QrCodeScanUtils.scanQrCode(it) { code -> onSearchTextChanged( - SearchQuery(code ?: "", mode = SearchMode.QrCodeScan), + SearchQuery( + code ?: "", + mode = SearchMode.QrCodeScan, + ), performSearchOnValueChanged, ) } } }, - modifier = modifier.testTag(TRAILING_QR_SCAN_ICON_BUTTON_TEST_TAG), + modifier = + modifier.testTag( + TRAILING_QR_SCAN_ICON_BUTTON_TEST_TAG, + ), ) { Icon( painter = @@ -288,46 +283,118 @@ fun TopScreenSection( @Composable fun SetupToolbarIcons( menuIcons: List?, + isFilterIconEnabled: Boolean, + filteredRecordsCount: Long? = null, navController: NavController, modifier: Modifier, onClick: (ToolbarClickEvent) -> Unit, decodeImage: ((String) -> Bitmap?)?, ) { - if (menuIcons?.isNotEmpty() == true && menuIcons.size > 2) { - Row { - RenderMenuIcons( - menuIcons = menuIcons.take(2), - navController = navController, - modifier = modifier, - onClick = onClick, - decodeImage = decodeImage, - ) - } - } else { - menuIcons?.let { - RenderMenuIcons( - menuIcons = it, + var showOverflowMenu by remember { mutableStateOf(false) } + if (!menuIcons.isNullOrEmpty()) { + val iconsCount = remember { if (isFilterIconEnabled) 1 else 2 } + if (menuIcons.size <= iconsCount) { + RenderMenuIcon( + menuIcons = menuIcons.subList(0, min(iconsCount, menuIcons.size)), + isFilterIconEnabled = isFilterIconEnabled, + filteredRecordsCount = filteredRecordsCount, navController = navController, modifier = modifier, onClick = onClick, decodeImage = decodeImage, ) + } else { + Row(verticalAlignment = Alignment.CenterVertically) { + RenderMenuIcon( + menuIcons = menuIcons.subList(0, iconsCount), + isFilterIconEnabled = false, + filteredRecordsCount = null, + navController = navController, + modifier = modifier, + onClick = onClick, + decodeImage = decodeImage, + ) + Icon( + imageVector = Icons.Outlined.MoreVert, + contentDescription = null, + tint = Color.White, + modifier = + Modifier.padding(start = 8.dp).size(22.dp).clickable { + showOverflowMenu = !showOverflowMenu + }, + ) + DropdownMenu( + expanded = showOverflowMenu, + onDismissRequest = { showOverflowMenu = false }, + ) { + menuIcons.subList(iconsCount, menuIcons.size).forEach { + DropdownMenuItem( + onClick = { + onClick(ToolbarClickEvent.Actions(it.actions)) + showOverflowMenu = !showOverflowMenu + }, + ) { + Image( + imageProperties = it, + navController = navController, + tint = it.tint?.parseColor() ?: DefaultColor, + modifier = + modifier + .clickable { onClick(ToolbarClickEvent.Actions(it.actions)) } + .testTag(TOP_ROW_TOGGLE_ICON_TEST_tAG), + decodeImage = decodeImage, + ) + } + } + } + } } } } @Composable -fun RenderMenuIcons( +private fun RenderMenuIcon( menuIcons: List, + isFilterIconEnabled: Boolean, + filteredRecordsCount: Long? = null, navController: NavController, modifier: Modifier, onClick: (ToolbarClickEvent) -> Unit, decodeImage: ((String) -> Bitmap?)?, ) { - LazyRow(horizontalArrangement = Arrangement.spacedBy(16.dp)) { + LazyRow(horizontalArrangement = Arrangement.spacedBy(14.dp)) { + item { + if (isFilterIconEnabled) { + BadgedBox( + modifier = Modifier.padding(end = 8.dp), + badge = { + if (filteredRecordsCount != null && filteredRecordsCount > -1) { + Badge { + Text( + text = if (filteredRecordsCount > 99) "99+" else filteredRecordsCount.toString(), + overflow = TextOverflow.Clip, + maxLines = 1, + ) + } + } + }, + ) { + Icon( + imageVector = Icons.Outlined.FilterAlt, + contentDescription = FILTER, + tint = Color.White, + modifier = + modifier + .size(22.dp) + .clickable { onClick(ToolbarClickEvent.FilterData) } + .testTag(TOP_ROW_FILTER_ICON_TEST_TAG), + ) + } + } + } items(menuIcons) { Image( - imageProperties = ImageProperties(imageConfig = it.imageConfig), + imageProperties = it, navController = navController, tint = Color.White, modifier = @@ -359,7 +426,7 @@ fun TopScreenSectionWithFilterItemOverNinetyNinePreview() { listOf( ImageProperties( imageConfig = ImageConfig(ICON_TYPE_LOCAL, "ic_toggle_map_view"), - backgroundColor = Color.White.toString(), + backgroundColor = "#FFFFFF", size = 10, ), ), @@ -403,7 +470,7 @@ fun TopScreenSectionNoFilterIconPreview() { searchBar = null, title = "Service Point", menuIcons = - arrayListOf( + listOf( ImageProperties(imageConfig = ImageConfig(reference = "ic_service_points")), ), ), @@ -429,7 +496,7 @@ fun TopScreenSectionWithFilterIconAndToggleIconPreview() { searchBar = null, title = "Service Point", menuIcons = - arrayListOf( + listOf( ImageProperties(imageConfig = ImageConfig(reference = "ic_service_points")), ), ), @@ -439,13 +506,13 @@ fun TopScreenSectionWithFilterIconAndToggleIconPreview() { @PreviewWithBackgroundExcludeGenerated @Composable -fun TopScreenSectionWithToggleIconPreview() { +fun TopScreenSectionWithOpenDrawerIconPreview() { TopScreenSection( title = "All Clients", searchQuery = SearchQuery("Eddy"), filteredRecordsCount = 120, onSearchTextChanged = { _, _ -> }, - toolBarHomeNavigation = ToolBarHomeNavigation.NAVIGATE_BACK, + toolBarHomeNavigation = ToolBarHomeNavigation.OPEN_DRAWER, isFilterIconEnabled = false, onClick = {}, isSearchBarVisible = true, diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/multiselect/MultiSelectBottomSheetFragment.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/multiselect/MultiSelectBottomSheetFragment.kt index 8538bc6561..7afb0d5a4f 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/multiselect/MultiSelectBottomSheetFragment.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/multiselect/MultiSelectBottomSheetFragment.kt @@ -20,58 +20,65 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.widget.Toast +import androidx.compose.foundation.layout.Box +import androidx.compose.material.Text import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.ui.Alignment import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.res.stringResource import androidx.fragment.app.activityViewModels import androidx.fragment.app.viewModels import androidx.lifecycle.lifecycleScope -import androidx.lifecycle.viewModelScope import androidx.navigation.fragment.navArgs import com.google.android.material.bottomsheet.BottomSheetDialogFragment import dagger.hilt.android.AndroidEntryPoint import javax.inject.Inject import kotlinx.coroutines.launch +import org.smartregister.fhircore.engine.domain.model.MultiSelectViewAction import org.smartregister.fhircore.engine.ui.theme.AppTheme -import org.smartregister.fhircore.engine.util.extension.isDeviceOnline -import org.smartregister.fhircore.engine.util.extension.showToast +import org.smartregister.fhircore.engine.util.DefaultDispatcherProvider +import org.smartregister.fhircore.quest.R import org.smartregister.fhircore.quest.event.AppEvent import org.smartregister.fhircore.quest.event.EventBus +import org.smartregister.fhircore.quest.ui.main.AppMainEvent import org.smartregister.fhircore.quest.ui.main.AppMainViewModel @AndroidEntryPoint -class MultiSelectBottomSheetFragment() : BottomSheetDialogFragment() { +class MultiSelectBottomSheetFragment : BottomSheetDialogFragment() { + + @Inject lateinit var dispatcherProvider: DefaultDispatcherProvider @Inject lateinit var eventBus: EventBus - val bottomSheetArgs by navArgs() - val multiSelectViewModel by viewModels() + private val bottomSheetArgs by navArgs() + private val multiSelectViewModel by viewModels() private val appMainViewModel by activityViewModels() override fun onViewCreated(view: View, savedInstanceState: Bundle?) { isCancelable = false - val multiSelectViewConfig = bottomSheetArgs?.multiSelectViewConfig + val multiSelectViewConfig = bottomSheetArgs.multiSelectViewConfig if (multiSelectViewConfig != null) { multiSelectViewModel.populateLookupMap(requireContext(), multiSelectViewConfig) } } - private fun onSelectionDone() { + private fun onSelectionDone(viewActions: List) { + val context = requireContext() lifecycleScope.launch { - multiSelectViewModel.saveSelectedLocations(requireContext()) - appMainViewModel.run { - if (requireContext().isDeviceOnline()) { - viewModelScope.launch { syncBroadcaster.runOneTimeSync() } - schedulePeriodicSync() - } else { - requireContext() - .showToast( - getString(org.smartregister.fhircore.engine.R.string.sync_failed), - Toast.LENGTH_LONG, - ) - eventBus.triggerEvent(AppEvent.RefreshRegisterData) + multiSelectViewModel.saveSelectedLocations(context, viewActions) { + viewActions.distinct().forEach { viewAction -> + when (viewAction) { + MultiSelectViewAction.SYNC_DATA -> + appMainViewModel.onEvent(AppMainEvent.SyncData(context)) + MultiSelectViewAction.FILTER_DATA -> + lifecycleScope.launch { + eventBus.triggerEvent( + AppEvent.RefreshData, + ) + } + } } + dismiss() } - dismiss() } } @@ -83,17 +90,26 @@ class MultiSelectBottomSheetFragment() : BottomSheetDialogFragment() { return ComposeView(requireContext()).apply { setContent { AppTheme { - MultiSelectBottomSheetView( - rootTreeNodes = multiSelectViewModel.rootTreeNodes, - syncLocationStateMap = multiSelectViewModel.selectedNodes, - title = bottomSheetArgs.screenTitle, - onDismiss = { dismiss() }, - searchTextState = multiSelectViewModel.searchTextState, - onSearchTextChanged = multiSelectViewModel::onTextChanged, - onSelectionDone = ::onSelectionDone, - search = multiSelectViewModel::search, - isLoading = multiSelectViewModel.isLoading.observeAsState(), - ) + val multiSelectViewConfig = bottomSheetArgs.multiSelectViewConfig + if (multiSelectViewConfig != null) { + MultiSelectBottomSheetView( + rootTreeNodes = multiSelectViewModel.rootTreeNodes, + syncLocationStateMap = multiSelectViewModel.selectedNodes, + title = bottomSheetArgs.screenTitle, + onDismiss = { dismiss() }, + searchTextState = multiSelectViewModel.searchTextState, + onSearchTextChanged = multiSelectViewModel::onTextChanged, + onSelectionDone = ::onSelectionDone, + search = multiSelectViewModel::search, + isLoading = multiSelectViewModel.isLoading.observeAsState(), + multiSelectViewAction = multiSelectViewConfig.viewActions, + mutuallyExclusive = multiSelectViewConfig.mutuallyExclusive, + ) + } else { + Box(contentAlignment = Alignment.Center) { + Text(text = stringResource(R.string.missing_multi_select_view_configs)) + } + } } } } diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/multiselect/MultiSelectBottomSheetView.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/multiselect/MultiSelectBottomSheetView.kt index 4e399b75fa..4c8a451baa 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/multiselect/MultiSelectBottomSheetView.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/multiselect/MultiSelectBottomSheetView.kt @@ -52,6 +52,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.res.stringResource +import androidx.compose.ui.state.ToggleableState import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.ImeAction @@ -60,10 +61,13 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import org.smartregister.fhircore.engine.R +import org.smartregister.fhircore.engine.domain.model.MultiSelectViewAction import org.smartregister.fhircore.engine.domain.model.SyncLocationState import org.smartregister.fhircore.engine.ui.multiselect.MultiSelectView import org.smartregister.fhircore.engine.ui.multiselect.TreeNode +import org.smartregister.fhircore.engine.ui.multiselect.updateNestedCheckboxState import org.smartregister.fhircore.engine.ui.theme.DividerColor +import org.smartregister.fhircore.engine.util.extension.isIn @Composable fun MultiSelectBottomSheetView( @@ -73,9 +77,11 @@ fun MultiSelectBottomSheetView( onDismiss: () -> Unit, searchTextState: MutableState, onSearchTextChanged: (String) -> Unit, - onSelectionDone: () -> Unit, + onSelectionDone: (List) -> Unit, search: () -> Unit, isLoading: State, + multiSelectViewAction: List, + mutuallyExclusive: Boolean, ) { val keyboardController = LocalSoftwareKeyboardController.current Scaffold( @@ -171,11 +177,41 @@ fun MultiSelectBottomSheetView( LazyColumn( modifier = Modifier.padding(horizontal = 8.dp), ) { - items(rootTreeNodes, key = { item -> item.id }) { + items(rootTreeNodes, key = { item -> item.id }) { rootTreeNode -> Column { MultiSelectView( - rootTreeNode = it, + rootTreeNode = rootTreeNode, syncLocationStateMap = syncLocationStateMap, + onChecked = { + if (mutuallyExclusive) { + rootTreeNodes.forEach { currentNode -> + val currentNodeToggleableState = + syncLocationStateMap[currentNode.id]?.toggleableState + if ( + currentNode.id != rootTreeNode.id && + currentNodeToggleableState != null && + currentNodeToggleableState.isIn( + ToggleableState.On, + ToggleableState.Indeterminate, + ) + ) { + // De-select the root and its children + syncLocationStateMap[currentNode.id] = + SyncLocationState( + locationId = currentNode.id, + parentLocationId = currentNode.parent?.id, + toggleableState = ToggleableState.Off, + ) + + updateNestedCheckboxState( + currentTreeNode = currentNode, + syncLocationStateMap = syncLocationStateMap, + checked = false, + ) + } + } + } + }, ) { treeNode -> Column { Text(text = treeNode.data) } } @@ -184,11 +220,19 @@ fun MultiSelectBottomSheetView( item { if (syncLocationStateMap.isNotEmpty() && rootTreeNodes.isNotEmpty()) { Button( - onClick = { onSelectionDone() }, + onClick = { onSelectionDone(multiSelectViewAction) }, modifier = Modifier.fillMaxWidth().padding(vertical = 16.dp, horizontal = 8.dp), ) { Text( - text = stringResource(id = R.string.sync_data).uppercase(), + text = + stringResource( + id = + when (multiSelectViewAction.first()) { + MultiSelectViewAction.SYNC_DATA -> R.string.sync_data + MultiSelectViewAction.FILTER_DATA -> R.string.apply_filter + }, + ) + .uppercase(), modifier = Modifier.padding(8.dp), ) } diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/multiselect/MultiSelectViewModel.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/multiselect/MultiSelectViewModel.kt index 885990d23f..c7e7bb1b3b 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/multiselect/MultiSelectViewModel.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/multiselect/MultiSelectViewModel.kt @@ -26,17 +26,21 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.google.android.fhir.datacapture.extensions.logicalId import dagger.hilt.android.lifecycle.HiltViewModel +import java.io.IOException import javax.inject.Inject -import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.launch import org.smartregister.fhircore.engine.data.local.DefaultRepository +import org.smartregister.fhircore.engine.datastore.dataFilterLocationIdsProtoStore import org.smartregister.fhircore.engine.datastore.syncLocationIdsProtoStore +import org.smartregister.fhircore.engine.domain.model.MultiSelectViewAction import org.smartregister.fhircore.engine.domain.model.MultiSelectViewConfig import org.smartregister.fhircore.engine.domain.model.SyncLocationState import org.smartregister.fhircore.engine.ui.multiselect.TreeBuilder import org.smartregister.fhircore.engine.ui.multiselect.TreeNode import org.smartregister.fhircore.engine.util.extension.extractLogicalIdUuid +import org.smartregister.fhircore.engine.util.extension.retrieveRelatedEntitySyncLocationState import org.smartregister.fhircore.engine.util.fhirpath.FhirPathDataExtractor +import timber.log.Timber @HiltViewModel class MultiSelectViewModel @@ -55,10 +59,16 @@ constructor( fun populateLookupMap(context: Context, multiSelectViewConfig: MultiSelectViewConfig) { viewModelScope.launch { isLoading.postValue(true) - // Mark previously selected nodes - val previouslySelectedNodes = context.syncLocationIdsProtoStore.data.firstOrNull() - if (!previouslySelectedNodes.isNullOrEmpty()) { - previouslySelectedNodes.values.forEach { selectedNodes[it.locationId] = it } + // Populate previously selected nodes for every Multi-Select view action + multiSelectViewConfig.viewActions.forEach { + val previouslySelectedNodes = + context.retrieveRelatedEntitySyncLocationState( + multiSelectViewAction = it, + filterToggleableStateOn = false, + ) + previouslySelectedNodes.forEach { syncLocationState -> + selectedNodes[syncLocationState.locationId] = syncLocationState + } } val repositoryResourceData = @@ -143,8 +153,25 @@ constructor( } } - suspend fun saveSelectedLocations(context: Context) { - context.syncLocationIdsProtoStore.updateData { selectedNodes } + suspend fun saveSelectedLocations( + context: Context, + viewActions: List, + onSaveDone: () -> Unit, + ) { + try { + viewActions.forEach { + when (it) { + MultiSelectViewAction.SYNC_DATA -> + context.syncLocationIdsProtoStore.updateData { selectedNodes } + MultiSelectViewAction.FILTER_DATA -> + context.dataFilterLocationIdsProtoStore.updateData { selectedNodes } + } + } + + onSaveDone() + } catch (ioException: IOException) { + Timber.e("Error saving selected locations", ioException) + } } fun search() { diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/register/RegisterFragment.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/register/RegisterFragment.kt index 032a90cf1e..9d31df58c9 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/register/RegisterFragment.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/register/RegisterFragment.kt @@ -252,7 +252,7 @@ class RegisterFragment : Fragment(), OnSyncListener { when (appEvent) { is AppEvent.OnSubmitQuestionnaire -> handleQuestionnaireSubmission(appEvent.questionnaireSubmission) - is AppEvent.RefreshRegisterData -> { + is AppEvent.RefreshData -> { appMainViewModel.countRegisterData() refreshRegisterData() } diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/shared/components/Image.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/shared/components/Image.kt index 4e628518b3..0d3a0cc71d 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/shared/components/Image.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/shared/components/Image.kt @@ -56,6 +56,7 @@ import org.smartregister.fhircore.engine.configuration.view.ImageShape import org.smartregister.fhircore.engine.domain.model.ResourceData import org.smartregister.fhircore.engine.domain.model.ViewType import org.smartregister.fhircore.engine.ui.theme.DangerColor +import org.smartregister.fhircore.engine.ui.theme.SideMenuTopItemDarkColor import org.smartregister.fhircore.engine.util.annotation.PreviewWithBackgroundExcludeGenerated import org.smartregister.fhircore.engine.util.extension.extractLogicalIdUuid import org.smartregister.fhircore.engine.util.extension.parseColor @@ -90,7 +91,7 @@ fun Image( text = imageProperties.text!!, textAlign = TextAlign.Center, modifier = Modifier.padding(end = 8.dp), - color = imageProperties.textColor?.parseColor() ?: Color.Gray, + color = imageProperties.textColor?.parseColor() ?: SideMenuTopItemDarkColor, ) ClickableImageIcon( imageProperties = imageProperties, @@ -140,8 +141,8 @@ fun ClickableImageIcon( ) .conditional( imageProperties.size != null, - { size(imageProperties.size!!.dp) }, - { size(24.dp) }, + { size(if (imageProperties.size!! >= 22) imageProperties.size!!.dp else 16.dp) }, + { size(20.dp) }, ) .conditional( !imageProperties.backgroundColor.isNullOrEmpty(), 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 3e2c6b58e2..19e1e44dbf 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 @@ -164,7 +164,7 @@ fun ActionConfig.handleClickEvent( args = args, navOptions = navController.currentDestination?.id?.let { - navOptions(resId = it, inclusive = actionConfig.popNavigationBackStack == true) + navOptions(resId = it, inclusive = actionConfig.popNavigationBackStack != false) }, ) } @@ -199,7 +199,7 @@ fun ActionConfig.handleClickEvent( args = args, navOptions = navController.currentDestination?.id?.let { - navOptions(resId = it, inclusive = actionConfig.popNavigationBackStack == true) + navOptions(resId = it, inclusive = actionConfig.popNavigationBackStack != false) }, ) } diff --git a/android/quest/src/main/res/values/strings.xml b/android/quest/src/main/res/values/strings.xml index 77345f7495..113c8e6d31 100644 --- a/android/quest/src/main/res/values/strings.xml +++ b/android/quest/src/main/res/values/strings.xml @@ -87,7 +87,8 @@ Record as ANC Pregnancy Outcome Registers - No Location Set + No location Set + No locations to render on map Set location to sync data and load service points Set location @@ -135,4 +136,9 @@ \u0020\u002a Exit App Are you sure you want to exit from the app? + Missing required multi select view configs. Please provide the configurations for the view to render. + "%1$d out of %2$d Location(s) have no coordinates" + All the locations rendered successfully" + %1$d matching location(s) rendered successfully" + Cancel adding location diff --git a/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/geowidget/GeoWidgetLauncherViewModelTest.kt b/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/geowidget/GeoWidgetLauncherViewModelTest.kt index 446b1552b4..dcfa53e5ea 100644 --- a/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/geowidget/GeoWidgetLauncherViewModelTest.kt +++ b/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/geowidget/GeoWidgetLauncherViewModelTest.kt @@ -18,13 +18,9 @@ package org.smartregister.fhircore.quest.ui.geowidget import android.content.Context import androidx.test.core.app.ApplicationProvider -import com.google.android.fhir.datacapture.extensions.logicalId import dagger.hilt.android.testing.HiltAndroidRule import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.HiltTestApplication -import io.mockk.slot -import io.mockk.spyk -import io.mockk.verify import javax.inject.Inject import kotlin.test.assertTrue import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -33,7 +29,6 @@ import kotlinx.coroutines.flow.first import kotlinx.coroutines.runBlocking import kotlinx.coroutines.test.runTest import org.hl7.fhir.r4.model.DecimalType -import org.hl7.fhir.r4.model.IdType import org.hl7.fhir.r4.model.Location import org.hl7.fhir.r4.model.ResourceType import org.junit.Assert.assertEquals @@ -53,7 +48,6 @@ import org.smartregister.fhircore.engine.domain.model.SnackBarMessageConfig import org.smartregister.fhircore.engine.rulesengine.ResourceDataRulesExecutor import org.smartregister.fhircore.engine.util.DefaultDispatcherProvider import org.smartregister.fhircore.engine.util.SharedPreferencesHelper -import org.smartregister.fhircore.geowidget.model.GeoJsonFeature import org.smartregister.fhircore.quest.app.fakes.Faker import org.smartregister.fhircore.quest.robolectric.RobolectricTest @@ -145,38 +139,6 @@ class GeoWidgetLauncherViewModelTest : RobolectricTest() { assertTrue(value!!) } - @Test - @Ignore("Tech debt : Tracked by issue https://github.com/opensrp/fhircore/issues/3514") - fun testRetrieveLocationsShouldReturnGeoJsonFeatureList() = runTest { - viewModel.retrieveLocations(geoWidgetConfiguration, null) - assertTrue(viewModel.geoJsonFeatures.value.isNotEmpty()) - assertEquals("loc1", viewModel.geoJsonFeatures.value.first().id) - } - - @Test - @Ignore("Investigate why this test is not running") - fun testOnQuestionnaireSubmission() = runTest { - val emitFeature: (GeoJsonFeature) -> Unit = spyk({}) - val extractedResourceIds = listOf(IdType(ResourceType.Location.name, location.logicalId)) - - viewModel.onQuestionnaireSubmission( - extractedResourceIds = extractedResourceIds, - emitFeature = emitFeature, - ) - val geoJsonFeatureSlot = slot() - verify { emitFeature(capture(geoJsonFeatureSlot)) } - - val geoJsonFeature = geoJsonFeatureSlot.captured - assertEquals( - location.position.longitude.toDouble(), - geoJsonFeature.geometry?.coordinates?.first(), - ) - assertEquals( - location.position.latitude.toDouble(), - geoJsonFeature.geometry?.coordinates?.last(), - ) - } - @Test @Ignore("Fix kotlinx.coroutines.test.UncompletedCoroutinesError") fun testEmitSnackBarState() { diff --git a/android/quest/src/test/java/org/smartregister/fhircore/quest/util/extensions/ConfigExtensionsKtTest.kt b/android/quest/src/test/java/org/smartregister/fhircore/quest/util/extensions/ConfigExtensionsKtTest.kt index df659b410d..3e225ecddf 100644 --- a/android/quest/src/test/java/org/smartregister/fhircore/quest/util/extensions/ConfigExtensionsKtTest.kt +++ b/android/quest/src/test/java/org/smartregister/fhircore/quest/util/extensions/ConfigExtensionsKtTest.kt @@ -351,7 +351,7 @@ class ConfigExtensionsKtTest : RobolectricTest() { ToolBarHomeNavigation.NAVIGATE_BACK, slotBundle.captured.getSerializable(NavigationArg.TOOL_BAR_HOME_NAVIGATION), ) - Assert.assertFalse(navOptions.captured.isPopUpToInclusive()) + Assert.assertTrue(navOptions.captured.isPopUpToInclusive()) Assert.assertTrue(navOptions.captured.shouldLaunchSingleTop()) }