From 366249c5c93cb78d91c351498d42fe445d3ac636 Mon Sep 17 00:00:00 2001 From: Xavier Molloy <44061143+xavimolloy@users.noreply.github.com> Date: Tue, 26 Dec 2023 14:01:10 +0100 Subject: [PATCH] feat: [ANDROAPP-5726] RTS workflow needs to allow for translating the 3 transaction types (#3444) * [ANDROAPP-5726] manage transaction type label through configured data element * [ANDROAPP-5726] Add tests for new code * feat: [ANDROAPP-5726] refactor code * feat: [ANDROAPP-5726] adapt tests to new ui logic --- .../rtsm/data/models/TransactionItem.kt | 3 +- .../android/rtsm/services/MetadataManager.kt | 2 + .../rtsm/services/MetadataManagerImpl.kt | 7 + .../android/rtsm/ui/home/HomeActivity.kt | 2 +- .../android/rtsm/ui/home/HomeViewModel.kt | 112 ++++++++++++-- .../rtsm/ui/home/model/SettingsUiState.kt | 11 +- .../rtsm/ui/home/screens/HomeScreen.kt | 2 - .../screens/components/BackdropComponent.kt | 4 +- .../screens/components/DropdownComponents.kt | 23 ++- .../screens/components/FilterComponents.kt | 38 ++--- .../ui/home/screens/components/MainContent.kt | 2 +- stock-usecase/src/main/res/values/strings.xml | 9 ++ .../android/rtsm/data/DataElementFactory.kt | 14 ++ .../rtsm/ui/home/HomeViewModelUnitTest.kt | 144 +++++++++++++----- 14 files changed, 275 insertions(+), 98 deletions(-) create mode 100644 stock-usecase/src/test/java/org/dhis2/android/rtsm/data/DataElementFactory.kt diff --git a/stock-usecase/src/main/java/org/dhis2/android/rtsm/data/models/TransactionItem.kt b/stock-usecase/src/main/java/org/dhis2/android/rtsm/data/models/TransactionItem.kt index 7e21a54652..5b56380cce 100644 --- a/stock-usecase/src/main/java/org/dhis2/android/rtsm/data/models/TransactionItem.kt +++ b/stock-usecase/src/main/java/org/dhis2/android/rtsm/data/models/TransactionItem.kt @@ -4,5 +4,6 @@ import org.dhis2.android.rtsm.data.TransactionType data class TransactionItem( val icon: Int, - val transactionType: TransactionType, + val type: TransactionType, + var label: String, ) diff --git a/stock-usecase/src/main/java/org/dhis2/android/rtsm/services/MetadataManager.kt b/stock-usecase/src/main/java/org/dhis2/android/rtsm/services/MetadataManager.kt index 4d52a60c4e..bd49722fac 100644 --- a/stock-usecase/src/main/java/org/dhis2/android/rtsm/services/MetadataManager.kt +++ b/stock-usecase/src/main/java/org/dhis2/android/rtsm/services/MetadataManager.kt @@ -1,6 +1,7 @@ package org.dhis2.android.rtsm.services import io.reactivex.Single +import org.hisp.dhis.android.core.dataelement.DataElement import org.hisp.dhis.android.core.option.Option import org.hisp.dhis.android.core.organisationunit.OrganisationUnit import org.hisp.dhis.android.core.program.Program @@ -9,4 +10,5 @@ interface MetadataManager { fun stockManagementProgram(programUid: String): Single fun facilities(programUid: String): Single> fun destinations(distributedTo: String): Single> + fun transactionType(dataSetUid: String): Single } diff --git a/stock-usecase/src/main/java/org/dhis2/android/rtsm/services/MetadataManagerImpl.kt b/stock-usecase/src/main/java/org/dhis2/android/rtsm/services/MetadataManagerImpl.kt index b07171f7a9..e4704e0092 100644 --- a/stock-usecase/src/main/java/org/dhis2/android/rtsm/services/MetadataManagerImpl.kt +++ b/stock-usecase/src/main/java/org/dhis2/android/rtsm/services/MetadataManagerImpl.kt @@ -4,6 +4,7 @@ import io.reactivex.Single import org.dhis2.android.rtsm.exceptions.InitializationException import org.hisp.dhis.android.core.D2 import org.hisp.dhis.android.core.arch.repositories.scope.RepositoryScope +import org.hisp.dhis.android.core.dataelement.DataElement import org.hisp.dhis.android.core.option.Option import org.hisp.dhis.android.core.organisationunit.OrganisationUnit import org.hisp.dhis.android.core.program.Program @@ -28,6 +29,12 @@ class MetadataManagerImpl @Inject constructor( .get() } + override fun transactionType(dataSetUid: String): Single { + return Single.defer { + d2.dataElementModule().dataElements().uid(dataSetUid).get() + } + } + /** * Get the program OUs which the user has access to and also * set as the user's the data capture OU. This is simply the diff --git a/stock-usecase/src/main/java/org/dhis2/android/rtsm/ui/home/HomeActivity.kt b/stock-usecase/src/main/java/org/dhis2/android/rtsm/ui/home/HomeActivity.kt index 4ee95188b0..7539542dd2 100644 --- a/stock-usecase/src/main/java/org/dhis2/android/rtsm/ui/home/HomeActivity.kt +++ b/stock-usecase/src/main/java/org/dhis2/android/rtsm/ui/home/HomeActivity.kt @@ -62,7 +62,7 @@ class HomeActivity : AppCompatActivity() { setContent { val settingsUiState by viewModel.settingsUiState.collectAsState() - updateTheme(settingsUiState.transactionType) + updateTheme(settingsUiState.selectedTransactionItem.type) manageStockViewModel.setThemeColor(Color(colorResource(themeColor).toArgb())) MdcTheme { Surface( diff --git a/stock-usecase/src/main/java/org/dhis2/android/rtsm/ui/home/HomeViewModel.kt b/stock-usecase/src/main/java/org/dhis2/android/rtsm/ui/home/HomeViewModel.kt index 11687e9345..0e7f387a36 100644 --- a/stock-usecase/src/main/java/org/dhis2/android/rtsm/ui/home/HomeViewModel.kt +++ b/stock-usecase/src/main/java/org/dhis2/android/rtsm/ui/home/HomeViewModel.kt @@ -1,5 +1,8 @@ package org.dhis2.android.rtsm.ui.home +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue import androidx.lifecycle.SavedStateHandle import dagger.hilt.android.lifecycle.HiltViewModel import io.reactivex.disposables.CompositeDisposable @@ -12,6 +15,7 @@ import org.dhis2.android.rtsm.data.AppConfig import org.dhis2.android.rtsm.data.OperationState import org.dhis2.android.rtsm.data.TransactionType import org.dhis2.android.rtsm.data.models.Transaction +import org.dhis2.android.rtsm.data.models.TransactionItem import org.dhis2.android.rtsm.exceptions.InitializationException import org.dhis2.android.rtsm.exceptions.UserIntentParcelCreationException import org.dhis2.android.rtsm.services.MetadataManager @@ -40,17 +44,20 @@ class HomeViewModel @Inject constructor( val facilities: StateFlow>> get() = _facilities + var transactionItems by mutableStateOf(mapTransaction()) + private val _destinations = MutableStateFlow>>(OperationState.Loading) val destinationsList: StateFlow>> get() = _destinations - private val _settingsUiSate = MutableStateFlow(SettingsUiState(programUid = config.program)) + private val _settingsUiSate = MutableStateFlow(SettingsUiState(programUid = config.program, transactionItems = transactionItems)) val settingsUiState: StateFlow = _settingsUiSate init { loadFacilities() loadDestinations() + loadTransactionTypeLabels() } private fun loadDestinations() { @@ -70,6 +77,72 @@ class HomeViewModel @Inject constructor( ) } + private fun loadTransactionTypeLabels() { + disposable.add( + metadataManager.transactionType(config.stockDistribution) + .subscribeOn(schedulerProvider.io()) + .observeOn(schedulerProvider.ui()) + .subscribe( + { dataElement -> + transactionItems.find { it.type == TransactionType.DISTRIBUTION }?.label = dataElement.displayName() ?: TransactionType.DISTRIBUTION.name + _settingsUiSate.update { currentUiState -> + currentUiState.copy(transactionItems = transactionItems, selectedTransactionItem = transactionItems.find { it.type == TransactionType.DISTRIBUTION } ?: currentUiState.selectedTransactionItem) + } + }, + { + it.printStackTrace() + }, + ), + ) + disposable.add( + metadataManager.transactionType(config.stockCount) + .subscribeOn(schedulerProvider.io()) + .observeOn(schedulerProvider.ui()) + .subscribe( + { dataElement -> + transactionItems.find { it.type == TransactionType.CORRECTION }?.label = dataElement.displayName() ?: TransactionType.CORRECTION.name + _settingsUiSate.update { currentUiState -> + currentUiState.copy(transactionItems = transactionItems) + } + }, + { + it.printStackTrace() + }, + ), + ) + disposable.add( + metadataManager.transactionType(config.stockDiscarded) + .subscribeOn(schedulerProvider.io()) + .observeOn(schedulerProvider.ui()) + .subscribe( + { dataElement -> + transactionItems.find { it.type == TransactionType.DISCARD }?.label = dataElement.displayName() ?: TransactionType.DISCARD.name + _settingsUiSate.update { currentUiState -> + currentUiState.copy(transactionItems = transactionItems) + } + }, + { + it.printStackTrace() + }, + ), + ) + disposable.add( + metadataManager.transactionType(config.distributedTo) + .subscribeOn(schedulerProvider.io()) + .observeOn(schedulerProvider.ui()) + .subscribe( + { + _settingsUiSate.update { currentUiState -> + currentUiState.copy(deliverToLabel = it.displayName() ?: "") + } + }, + { + it.printStackTrace() + }, + ), + ) + } + private fun loadFacilities() { disposable.add( metadataManager.facilities(config.program) @@ -93,14 +166,13 @@ class HomeViewModel @Inject constructor( ) } - fun selectTransaction(type: TransactionType) { + fun selectTransaction(selectedItem: TransactionItem) { _settingsUiSate.update { currentUiState -> - currentUiState.copy(transactionType = type) + currentUiState.copy(selectedTransactionItem = selectedItem) } - // Distributed to cannot only be set for DISTRIBUTION, // so ensure you clear it for others if it has been set - if (type != TransactionType.DISTRIBUTION) { + if (selectedItem.type != TransactionType.DISTRIBUTION) { _settingsUiSate.update { currentUiState -> currentUiState.copy(destination = null) } @@ -114,7 +186,7 @@ class HomeViewModel @Inject constructor( } fun setDestination(destination: Option?) { - if (settingsUiState.value.transactionType != TransactionType.DISTRIBUTION) { + if (settingsUiState.value.selectedTransactionItem.type != TransactionType.DISTRIBUTION) { throw UnsupportedOperationException( "Cannot set 'distributed to' for non-distribution transactions", ) @@ -128,7 +200,7 @@ class HomeViewModel @Inject constructor( fun checkForFieldErrors(): Int? { return if (settingsUiState.value.facility == null) { R.string.mandatory_facility_selection - } else if (settingsUiState.value.transactionType == TransactionType.DISTRIBUTION && + } else if (settingsUiState.value.selectedTransactionItem.type == TransactionType.DISTRIBUTION && settingsUiState.value.destination == null ) { R.string.mandatory_distributed_to_selection @@ -144,7 +216,7 @@ class HomeViewModel @Inject constructor( ) } return Transaction( - settingsUiState.value.transactionType, + settingsUiState.value.selectedTransactionItem.type, ParcelUtils.facilityToIdentifiableModelParcel(settingsUiState.value.facility!!), settingsUiState.value.transactionDate.humanReadableDate(), settingsUiState.value.destination?.let { @@ -159,8 +231,30 @@ class HomeViewModel @Inject constructor( _settingsUiSate.update { SettingsUiState( programUid = config.program, + transactionItems = transactionItems, ) } - selectTransaction(TransactionType.DISTRIBUTION) + selectTransaction( + transactionItems.find { it.type == TransactionType.DISTRIBUTION } ?: TransactionItem( + R.drawable.ic_distribution, + TransactionType.DISTRIBUTION, TransactionType.DISTRIBUTION.name, + ), + ) + } + + private fun mapTransaction(): MutableList { + return mutableListOf( + TransactionItem( + R.drawable.ic_distribution, + TransactionType.DISTRIBUTION, + TransactionType.DISTRIBUTION.name, + ), + TransactionItem(R.drawable.ic_discard, TransactionType.DISCARD, TransactionType.DISCARD.name), + TransactionItem( + R.drawable.ic_correction, + TransactionType.CORRECTION, + TransactionType.CORRECTION.name, + ), + ) } } diff --git a/stock-usecase/src/main/java/org/dhis2/android/rtsm/ui/home/model/SettingsUiState.kt b/stock-usecase/src/main/java/org/dhis2/android/rtsm/ui/home/model/SettingsUiState.kt index df09665885..c9ee77d098 100644 --- a/stock-usecase/src/main/java/org/dhis2/android/rtsm/ui/home/model/SettingsUiState.kt +++ b/stock-usecase/src/main/java/org/dhis2/android/rtsm/ui/home/model/SettingsUiState.kt @@ -1,10 +1,10 @@ package org.dhis2.android.rtsm.ui.home.model import org.dhis2.android.rtsm.R -import org.dhis2.android.rtsm.data.TransactionType import org.dhis2.android.rtsm.data.TransactionType.CORRECTION import org.dhis2.android.rtsm.data.TransactionType.DISCARD import org.dhis2.android.rtsm.data.TransactionType.DISTRIBUTION +import org.dhis2.android.rtsm.data.models.TransactionItem import org.dhis2.android.rtsm.utils.UIText import org.hisp.dhis.android.core.option.Option import org.hisp.dhis.android.core.organisationunit.OrganisationUnit @@ -12,16 +12,19 @@ import java.time.LocalDateTime data class SettingsUiState( val programUid: String, - val transactionType: TransactionType = DISTRIBUTION, + val selectedTransactionItem: TransactionItem = TransactionItem(R.drawable.ic_distribution, DISTRIBUTION, DISTRIBUTION.name), + val transactionItems: MutableList, + val deliverToLabel: String = "", val facility: OrganisationUnit? = null, val destination: Option? = null, val transactionDate: LocalDateTime = LocalDateTime.now(), ) { + fun hasFacilitySelected() = facility != null fun hasDestinationSelected() = destination != null fun fromFacilitiesLabel(): UIText = facility?.let { val orgUnitName = it.displayName().toString() - return when (transactionType) { + return when (selectedTransactionItem.type) { DISTRIBUTION -> { UIText.StringRes(R.string.subtitle, orgUnitName) } @@ -34,7 +37,7 @@ data class SettingsUiState( } } ?: UIText.StringRes(R.string.from_facility) - fun deliverToLabel(): UIText? = when (transactionType) { + fun deliverToLabel(): UIText? = when (selectedTransactionItem.type) { DISTRIBUTION -> destination?.let { UIText.StringRes(R.string.subtitle, it.displayName().toString()) } ?: UIText.StringRes(R.string.to_facility) diff --git a/stock-usecase/src/main/java/org/dhis2/android/rtsm/ui/home/screens/HomeScreen.kt b/stock-usecase/src/main/java/org/dhis2/android/rtsm/ui/home/screens/HomeScreen.kt index 745543c025..098996237d 100644 --- a/stock-usecase/src/main/java/org/dhis2/android/rtsm/ui/home/screens/HomeScreen.kt +++ b/stock-usecase/src/main/java/org/dhis2/android/rtsm/ui/home/screens/HomeScreen.kt @@ -6,7 +6,6 @@ import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.foundation.layout.padding -import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.Icon import androidx.compose.material.Scaffold import androidx.compose.material.ScaffoldState @@ -30,7 +29,6 @@ import org.dhis2.android.rtsm.ui.home.screens.components.CompletionDialog import org.dhis2.android.rtsm.ui.managestock.ManageStockViewModel import org.dhis2.ui.buttons.FAButton -@OptIn(ExperimentalMaterialApi::class) @Composable fun HomeScreen( activity: Activity, diff --git a/stock-usecase/src/main/java/org/dhis2/android/rtsm/ui/home/screens/components/BackdropComponent.kt b/stock-usecase/src/main/java/org/dhis2/android/rtsm/ui/home/screens/components/BackdropComponent.kt index 9300862bef..b49a0285aa 100644 --- a/stock-usecase/src/main/java/org/dhis2/android/rtsm/ui/home/screens/components/BackdropComponent.kt +++ b/stock-usecase/src/main/java/org/dhis2/android/rtsm/ui/home/screens/components/BackdropComponent.kt @@ -84,7 +84,7 @@ fun Backdrop( modifier = modifier, appBar = { Toolbar( - settingsUiState.transactionType.name, + settingsUiState.selectedTransactionItem.label, settingsUiState.fromFacilitiesLabel().asString(), settingsUiState.deliverToLabel()?.asString(), themeColor, @@ -145,7 +145,7 @@ fun Backdrop( gesturesEnabled = false, frontLayerBackgroundColor = Color.White, frontLayerScrimColor = if ( - settingsUiState.transactionType == TransactionType.DISTRIBUTION + settingsUiState.selectedTransactionItem.type == TransactionType.DISTRIBUTION ) { if (settingsUiState.hasFacilitySelected() && settingsUiState.hasDestinationSelected()) { isFrontLayerDisabled = false diff --git a/stock-usecase/src/main/java/org/dhis2/android/rtsm/ui/home/screens/components/DropdownComponents.kt b/stock-usecase/src/main/java/org/dhis2/android/rtsm/ui/home/screens/components/DropdownComponents.kt index a49ba40979..4f60d8d9f7 100644 --- a/stock-usecase/src/main/java/org/dhis2/android/rtsm/ui/home/screens/components/DropdownComponents.kt +++ b/stock-usecase/src/main/java/org/dhis2/android/rtsm/ui/home/screens/components/DropdownComponents.kt @@ -45,7 +45,6 @@ import androidx.compose.ui.unit.toSize import androidx.compose.ui.zIndex import androidx.fragment.app.FragmentManager import org.dhis2.android.rtsm.R -import org.dhis2.android.rtsm.data.TransactionType import org.dhis2.android.rtsm.data.models.TransactionItem import org.dhis2.android.rtsm.ui.home.model.DataEntryStep import org.dhis2.android.rtsm.ui.home.model.DataEntryUiState @@ -60,17 +59,14 @@ import org.hisp.dhis.android.core.organisationunit.OrganisationUnit @Composable fun DropdownComponentTransactions( settingsUiState: SettingsUiState, - onTransitionSelected: (transition: TransactionType) -> Unit, + onTransitionSelected: (transition: TransactionItem) -> Unit, hasUnsavedData: Boolean, themeColor: Color = colorResource(R.color.colorPrimary), - data: MutableList, launchDialog: (msg: Int, (result: EditionDialogResult) -> Unit) -> Unit, ) { var isExpanded by remember { mutableStateOf(false) } - val itemIcon = data.find { - it.transactionType == settingsUiState.transactionType - }?.icon ?: data.first().icon + val itemIcon = settingsUiState.selectedTransactionItem.icon var selectedIndex by remember { mutableStateOf(0) } val paddingValue = if (selectedIndex >= 0) { @@ -94,7 +90,7 @@ fun DropdownComponentTransactions( Column(Modifier.padding(horizontal = 16.dp)) { OutlinedTextField( - value = capitalizeText(settingsUiState.transactionType.name), + value = capitalizeText(settingsUiState.selectedTransactionItem.label), onValueChange = { }, modifier = Modifier .fillMaxWidth() @@ -132,7 +128,7 @@ fun DropdownComponentTransactions( }, shape = RoundedCornerShape(30.dp), placeholder = { - Text(text = capitalizeText(data.first().transactionType.name)) + Text(text = capitalizeText(settingsUiState.selectedTransactionItem.label)) }, colors = TextFieldDefaults.outlinedTextFieldColors( focusedBorderColor = Color.White, @@ -151,7 +147,7 @@ fun DropdownComponentTransactions( .background(shape = RoundedCornerShape(16.dp), color = Color.White), offset = DpOffset(x = 0.dp, y = 2.dp), ) { - data.forEachIndexed { index, item -> + settingsUiState.transactionItems.forEachIndexed { index, item -> DropdownMenuItem( onClick = { if (selectedIndex != index && hasUnsavedData) { @@ -159,7 +155,7 @@ fun DropdownComponentTransactions( when (result) { EditionDialogResult.DISCARD -> { // Perform the transaction change and clear data - onTransitionSelected.invoke(item.transactionType) + onTransitionSelected.invoke(item) selectedIndex = index isExpanded = false } @@ -170,7 +166,7 @@ fun DropdownComponentTransactions( } } } else { - onTransitionSelected.invoke(item.transactionType) + onTransitionSelected.invoke(item) selectedIndex = index isExpanded = false } @@ -197,7 +193,7 @@ fun DropdownComponentTransactions( Modifier.padding(6.dp), tint = themeColor, ) - Text(text = capitalizeText(item.transactionType.name)) + Text(text = capitalizeText(item.label)) } } } @@ -304,6 +300,7 @@ fun DropdownComponentDistributedTo( themeColor: Color = colorResource(R.color.colorPrimary), data: List